diff --git a/.ci/macos/Brewfile b/.ci/macos/Brewfile index 0674eb61..717447fc 100644 --- a/.ci/macos/Brewfile +++ b/.ci/macos/Brewfile @@ -6,7 +6,7 @@ brew "clang-format" brew "cmake" brew "ninja" brew "openssl" -brew "qt5" +brew "qt6" brew "nlohmann_json" brew "gstreamer" brew "qtkeychain" diff --git a/.ci/macos/build.sh b/.ci/macos/build.sh index 40ab2571..aa515186 100755 --- a/.ci/macos/build.sh +++ b/.ci/macos/build.sh @@ -6,27 +6,32 @@ set -ue #TAG=$(git tag -l --points-at HEAD) # Add Qt binaries to path -PATH="$(brew --prefix qt5):${PATH}" +PATH="$(brew --prefix qt6)/bin/:${PATH}" export PATH -CMAKE_PREFIX_PATH="$(brew --prefix qt5)" +CMAKE_PREFIX_PATH="$(brew --prefix qt6)" export CMAKE_PREFIX_PATH cmake -GNinja -S. -Bbuild \ -DCMAKE_BUILD_TYPE=RelWithDebInfo \ - -DCMAKE_INSTALL_PREFIX=.deps/usr \ + -DCMAKE_INSTALL_PREFIX="nheko.temp" \ -DHUNTER_ROOT="../.hunter" \ -DHUNTER_ENABLED=ON -DBUILD_SHARED_LIBS=OFF \ -DCMAKE_BUILD_TYPE=RelWithDebInfo -DHUNTER_CONFIGURATION_TYPES=RelWithDebInfo \ -DUSE_BUNDLED_OPENSSL=ON \ -DCI_BUILD=ON cmake --build build +cmake --install build ( cd build git clone https://github.com/Nheko-Reborn/qt-jdenticon.git ( cd qt-jdenticon qmake make -j 4 - cp libqtjdenticon.dylib ../nheko.app/Contents/MacOS + cp libqtjdenticon.dylib ../../nheko.temp/nheko.app/Contents/MacOS ) - "$(brew --prefix qt5)/bin/macdeployqt" nheko.app -always-overwrite -qmldir=../resources/qml/ + # "$(brew --prefix qt6)/bin/macdeployqt" nheko.app -always-overwrite -qmldir=../resources/qml/ + # # workaround for https://bugreports.qt.io/browse/QTBUG-100686 + # cp "$(brew --prefix brotli)/lib/libbrotlicommon.1.dylib" nheko.app/Contents/Frameworks/libbrotlicommon.1.dylib ) + +mv nheko.temp/nheko.app nheko.app diff --git a/.ci/macos/notarize.sh b/.ci/macos/notarize.sh index 345f4828..33a6da50 100755 --- a/.ci/macos/notarize.sh +++ b/.ci/macos/notarize.sh @@ -6,7 +6,7 @@ set -u # 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}" +PATH="/usr/local/opt/qt@6/bin/:${PATH}" export PATH security unlock-keychain -p "${RUNNER_USER_PW}" login.keychain @@ -20,25 +20,23 @@ if [ -n "${CI_PIPELINE_TRIGGERED:-}" ] && [ "${TRIGGERED_BY:-}" = "cirrus" ]; th unzip binaries.zip # we zip 'build/nheko.app' in cirrus ci, cirrus itself puts it in a 'build' directory # so move it to the right place for the rest of the process. - ( cd build || exit - unzip nheko.zip - ) + unzip nheko.zip fi -if [ ! -d "build/nheko.app" ]; then +if [ ! -d "nheko.app" ]; then echo "nheko.app is missing, you did something wrong!" exit 1 fi echo "[INFO] Signing app contents" -find "build/nheko.app/Contents"|while read -r fname; do +find "nheko.app/Contents"|while read -r fname; do if [ -f "$fname" ]; then echo "[INFO] Signing $fname" codesign --force --timestamp --options=runtime --sign "${APPLE_DEV_IDENTITY}" "$fname" fi done -codesign --force --timestamp --options=runtime --sign "${APPLE_DEV_IDENTITY}" "build/nheko.app" +codesign --force --timestamp --options=runtime --sign "${APPLE_DEV_IDENTITY}" "nheko.app" NOTARIZE_SUBMIT_LOG=$(mktemp /tmp/notarize-submit.XXXXXX) NOTARIZE_STATUS_LOG=$(mktemp /tmp/notarize-status.XXXXXX) @@ -100,4 +98,4 @@ if [ -n "$VERSION" ]; then mv nheko.dmg "nheko-${VERSION}-${PLAT}.dmg" mkdir -p artifacts cp "nheko-${VERSION}-${PLAT}.dmg" artifacts/ -fi \ No newline at end of file +fi diff --git a/.ci/macos/settings.json b/.ci/macos/settings.json index d156a3b6..e1410b52 100644 --- a/.ci/macos/settings.json +++ b/.ci/macos/settings.json @@ -3,7 +3,7 @@ "compression-level": 9, "contents": [ { - "path": "./build/Nheko.app", + "path": "./nheko.app", "type": "file", "x": 140, "y": 120 diff --git a/.cirrus.yml b/.cirrus.yml index 11de4d1a..8618b7ee 100644 --- a/.cirrus.yml +++ b/.cirrus.yml @@ -13,9 +13,9 @@ task: - export PATH="$(brew --prefix qt5)/bin/:${PATH}" - ./.ci/macos/build.sh zip_script: - - ditto -c -k --sequesterRsrc --keepParent build/nheko.app build/nheko.zip + - ditto -c -k --sequesterRsrc --keepParent nheko.app nheko.zip gitlab_script: - > [ "${CIRRUS_BRANCH}" == "master" ] && curl -X POST --fail -F token="${GITLAB_TRIGGER_TOKEN}" -F ref="${CIRRUS_BRANCH}" -F "variables[TRIGGER_BUILD_ID]=${CIRRUS_BUILD_ID}" -F "variables[TRIGGERED_BY]=cirrus" "https://nheko.im/api/v4/projects/2/trigger/pipeline" || true binaries_artifacts: - path: build/nheko.zip + path: nheko.zip diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 3dfc7f59..ffa436d3 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -18,14 +18,14 @@ build-clazy: TRAVIS_OS_NAME: linux before_script: - echo -e "\e[0Ksection_start:`date +%s`:install_deps[collapsed=true]\r\e[0K\e[1m\e[95mInstalling apk dependencies" - - apk add asciidoctor cmake cmark-dev gst-plugins-bad-dev gst-plugins-base-dev gstreamer-dev lmdb-dev lmdbxx nlohmann-json olm-dev openssl-dev qt5-qtbase-dev qt5-qtdeclarative-dev qt5-qtmultimedia-dev qt5-qtquickcontrols2-dev qt5-qtsvg-dev qt5-qttools-dev qtkeychain-dev samurai spdlog-dev xcb-util-wm-dev zlib-dev ccache curl-dev libevent-dev meson clazy clang16 gcc musl-dev git re2-dev + - apk add asciidoctor cmake cmark-dev gst-plugins-bad-dev gst-plugins-base-dev gstreamer-dev lmdb-dev lmdbxx nlohmann-json olm-dev openssl-dev qt6-qtbase-dev qt6-qtdeclarative-dev qt6-qtmultimedia-dev qt6-qtsvg-dev qt6-qttools-dev samurai spdlog-dev xcb-util-wm-dev zlib-dev ccache curl-dev libevent-dev meson clazy clang16 gcc musl-dev git re2-dev libsecret-dev - echo -e "\e[0Ksection_end:`date +%s`:install_deps\r\e[0K" script: - export PATH="/usr/lib/ccache:${PATH}" - export CMAKE_BUILD_PARALLEL_LEVEL=$(cat /proc/cpuinfo | awk '/^processor/{print $3}' | wc -l) - cmake -GNinja -H. -Bbuild -DCMAKE_INSTALL_PREFIX=.deps/usr - -DHUNTER_ENABLED=OFF -DBUILD_SHARED_LIBS=OFF -DUSE_BUNDLED_OPENSSL=ON -DUSE_BUNDLED_MTXCLIENT=ON -DUSE_BUNDLED_COEURL=ON -DUSE_BUNDLED_OLM=ON + -DHUNTER_ENABLED=OFF -DBUILD_SHARED_LIBS=OFF -DUSE_BUNDLED_OPENSSL=ON -DUSE_BUNDLED_MTXCLIENT=ON -DUSE_BUNDLED_COEURL=ON -DUSE_BUNDLED_OLM=ON -DUSE_BUNDLED_QTKEYCHAIN=ON -DVOIP=OFF -DCMAKE_BUILD_TYPE=Release -DCI_BUILD=ON -DFETCHCONTENT_QUIET=OFF -DCMAKE_CXX_COMPILER=clazy @@ -37,7 +37,8 @@ build-clazy: paths: - .ccache -build-gcc11: +# disabled until I find a qt6.5 ppa +.build-gcc11: stage: build image: ${CI_DEPENDENCY_PROXY_GROUP_IMAGE_PREFIX}/ubuntu:22.04 tags: [docker] @@ -50,7 +51,7 @@ build-gcc11: libssl-dev libqt5multimedia5-plugins libqt5multimediagsttools5 libqt5multimediaquick5 libqt5svg5-dev qtmultimedia5-dev qtquickcontrols2-5-dev qttools5-dev qttools5-dev-tools qtdeclarative5-dev qml-module-qtmultimedia qml-module-qtquick-controls2 qml-module-qtquick-layouts qml-module-qt-labs-platform - qt5keychain-dev ccache clazy libcurl4-openssl-dev libevent-dev libspdlog-dev git nlohmann-json3-dev libcmark-dev asciidoc time # libolm-dev + qt5keychain-dev ccache libcurl4-openssl-dev libevent-dev libspdlog-dev git nlohmann-json3-dev libcmark-dev asciidoc time # libolm-dev # need recommended deps for wget - apt-get -y install wget - /usr/sbin/update-ccache-symlinks @@ -107,16 +108,15 @@ build-tw: "zlib-devel" "libQt5PlatformHeaders-devel" "cmake(re2)" - "cmake(Qt5Concurrent)" - "cmake(Qt5Core)" - "cmake(Qt5DBus)" - "cmake(Qt5Keychain)" - "cmake(Qt5LinguistTools)" - "cmake(Qt5Multimedia)" - "cmake(Qt5Network)" - "cmake(Qt5QuickControls2)" - "cmake(Qt5Svg)" - "cmake(Qt5Widgets)" + "cmake(Qt6Core)" + "cmake(Qt6DBus)" + "cmake(Qt6Keychain)" + "cmake(Qt6LinguistTools)" + "cmake(Qt6Multimedia)" + "cmake(Qt6QuickControls2)" + "cmake(Qt6Svg)" + "cmake(Qt6Widgets)" + "cmake(Qt6Gui)" "pkgconfig(libcurl)" "pkgconfig(libevent)" "pkgconfig(gstreamer-webrtc-1.0)" @@ -155,7 +155,7 @@ build-macos: - if : '$CI_PIPELINE_TRIGGERED == null' artifacts: paths: - - build/nheko.app # not putting this in 'artifacts' subdir because we don't want to put it on releases + - nheko.app # not putting this in 'artifacts' subdir because we don't want to put it on releases name: nheko-${CI_COMMIT_SHORT_SHA}-macos-app expose_as: 'macos-app' public: false @@ -200,7 +200,10 @@ build-flatpak: - docker-${ARCH} parallel: matrix: - - ARCH: [amd64, arm64] + - ARCH: amd64 + JOBS: 0 + - ARCH: arm64 + JOBS: 3 before_script: - echo -e "\e[0Ksection_start:`date +%s`:install_deps[collapsed=true]\r\e[0K\e[1m\e[95mInstalling apt dependencies" - apt-get update && apt-get -y install flatpak-builder git python3 curl python3-aiohttp python3-tenacity gir1.2-ostree-1.0 @@ -213,9 +216,9 @@ build-flatpak: - mkdir -p build-flatpak - cd build-flatpak - echo -e "\e[0Ksection_start:`date +%s`:build_flatpak[collapsed=true]\r\e[0K\e[1m\e[95mBuilding flatpak" - - flatpak-builder --install-deps-from=flathub --user --disable-rofiles-fuse --ccache --repo=repo --default-branch=${CI_COMMIT_REF_NAME//\//_} --subject="Build of Nheko ${VERSION} `date` for ${ARCH}" app ../io.github.NhekoReborn.Nheko.yaml + - flatpak-builder --install-deps-from=flathub --user --disable-rofiles-fuse --ccache --repo=repo --default-branch=${CI_COMMIT_REF_NAME//\//_} --subject="Build of Nheko ${VERSION} `date` for ${ARCH}" app ../im.nheko.Nheko.yaml --jobs=$JOBS - echo -e "\e[0Ksection_end:`date +%s`:build_flatpak\r\e[0K" - - flatpak build-bundle repo nheko-${ARCH}.flatpak io.github.NhekoReborn.Nheko ${CI_COMMIT_REF_NAME//\//_} + - flatpak build-bundle repo nheko-${ARCH}.flatpak im.nheko.Nheko ${CI_COMMIT_REF_NAME//\//_} after_script: - echo -e "\e[0Ksection_start:`date +%s`:upload_flatpak[collapsed=true]\r\e[0K\e[1m\e[95mUploading flatpak" - bash ./.ci/upload-nightly-gitlab.sh build-flatpak/nheko-${ARCH}.flatpak @@ -233,7 +236,8 @@ build-flatpak: paths: ['build-flatpak/nheko-${ARCH}.flatpak'] name: flatpak-${CI_COMMIT_REF_NAME}-${VERSION}-${ARCH} -appimage-amd64: +# disabled until I find a qt6.5 ppa for Ubuntu +.appimage-amd64: stage: build image: ${CI_DEPENDENCY_PROXY_GROUP_IMAGE_PREFIX}/ubuntu:22.04 tags: [docker] @@ -249,7 +253,7 @@ appimage-amd64: libssl-dev libqt5multimedia5-plugins libqt5multimediagsttools5 libqt5multimediaquick5 libqt5svg5-dev qtmultimedia5-dev qtquickcontrols2-5-dev qttools5-dev qttools5-dev-tools qtdeclarative5-dev qml-module-qtmultimedia qml-module-qtquick-controls2 qml-module-qtquick-layouts qml-module-qt-labs-platform - qt5keychain-dev ccache clazy libcurl4-openssl-dev libevent-dev libspdlog-dev nlohmann-json3-dev libcmark-dev asciidoc libre2-dev libgtest-dev libgl1-mesa-dev qml-module-qtquick-particles2 + qt5keychain-dev ccache libcurl4-openssl-dev libevent-dev libspdlog-dev nlohmann-json3-dev libcmark-dev asciidoc libre2-dev libgtest-dev libgl1-mesa-dev qml-module-qtquick-particles2 # Installing the packages needed to build AppImage - apt-get -yq install breeze-icon-theme desktop-file-utils elfutils fakeroot file gnupg2 gtk-update-icon-cache libgdk-pixbuf2.0-dev libgdk-pixbuf2.0-0 libglib2.0-bin librsvg2-dev libyaml-dev strace zsync squashfs-tools @@ -325,7 +329,7 @@ github-release: rules: - if: '$CI_COMMIT_TAG =~ /^v\d+\.\d+\.\d+$/' dependencies: - - appimage-amd64 + #- appimage-amd64 <- disabled because of missing packages - build-flatpak - codesign-macos before_script: diff --git a/.qmlformat.ini b/.qmlformat.ini new file mode 100644 index 00000000..c136c23b --- /dev/null +++ b/.qmlformat.ini @@ -0,0 +1,7 @@ +[General] +FunctionsSpacing= +IndentWidth=4 +NewlineType=native +NormalizeOrder=true +ObjectsSpacing= +UseTabs=false diff --git a/CMakeLists.txt b/CMakeLists.txt index b7c02ffb..1d43cfe6 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,4 +1,4 @@ -cmake_minimum_required(VERSION 3.13) +cmake_minimum_required(VERSION 3.13..3.21) option(APPVEYOR_BUILD "Build on appveyor" OFF) option(CI_BUILD "Set when building in CI. Enables -Werror where possible" OFF) @@ -57,7 +57,7 @@ option(USE_BUNDLED_OPENSSL "Use the bundled version of OpenSSL." OFF) option(USE_BUNDLED_MTXCLIENT "Use the bundled version of the Matrix Client library." ${HUNTER_ENABLED}) option(USE_BUNDLED_LMDB "Use the bundled version of lmdb." ${HUNTER_ENABLED}) option(USE_BUNDLED_LMDBXX "Use the bundled version of lmdb++." ${HUNTER_ENABLED}) -option(USE_BUNDLED_QTKEYCHAIN "Use the bundled version of Qt5Keychain." ${HUNTER_ENABLED}) +option(USE_BUNDLED_QTKEYCHAIN "Use the bundled version of Qt6Keychain." ${HUNTER_ENABLED}) option(USE_BUNDLED_COEURL "Use a bundled version of the Curl wrapper" ${HUNTER_ENABLED}) option(USE_BUNDLED_LIBEVENT "Use the bundled version of libevent." ${HUNTER_ENABLED}) @@ -72,7 +72,11 @@ if (APPLE OR WIN32) set(VOIP_DEFAULT OFF) endif() option(VOIP "Whether to enable voip support. Disable this, if you don't have gstreamer." ${VOIP_DEFAULT}) -cmake_dependent_option(SCREENSHARE_X11 "Whether to enable screenshare support on X11." ON "VOIP" OFF) +set(X11_DEFAULT) +if (WIN32 OR APPLE OR HAIKU) + set(X11_DEFAULT OFF) +endif() +option(X11 "Whether to enable X11 specific features (screenshare, window roles)." ${X11_DEFAULT}) cmake_dependent_option(SCREENSHARE_XDP "Whether to enable screenshare support using xdg-desktop-portal." ON "VOIP" OFF) list(APPEND CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/cmake") @@ -239,16 +243,17 @@ endif() # # Discover Qt dependencies. # -find_package(Qt5 5.15 COMPONENTS Core Widgets LinguistTools Concurrent Svg Multimedia Qml QuickControls2 QuickWidgets REQUIRED) -find_package(Qt5QuickCompiler) -find_package(Qt5DBus) +find_package(Qt6 6.5 COMPONENTS Core Widgets Gui LinguistTools Svg Multimedia Qml QuickControls2 REQUIRED) +#find_package(Qt6QuickCompiler) +find_package(Qt6DBus) if (USE_BUNDLED_QTKEYCHAIN) include(FetchContent) + set(BUILD_WITH_QT6 ON) FetchContent_Declare( - qt5keychain + qt6keychain GIT_REPOSITORY https://github.com/frankosterfeld/qtkeychain.git - GIT_TAG v0.13.1 + GIT_TAG v0.14.0 ) if (BUILD_SHARED_LIBS) set(QTKEYCHAIN_STATIC OFF CACHE INTERNAL "") @@ -256,21 +261,17 @@ if (USE_BUNDLED_QTKEYCHAIN) set(QTKEYCHAIN_STATIC ON CACHE INTERNAL "") endif() set(BUILD_TEST_APPLICATION OFF CACHE INTERNAL "") - FetchContent_MakeAvailable(qt5keychain) + FetchContent_MakeAvailable(qt6keychain) else() - find_package(Qt5Keychain REQUIRED) + find_package(Qt6Keychain REQUIRED) endif() -if (APPLE) - find_package(Qt5MacExtras REQUIRED) -endif(APPLE) - -if (Qt5Widgets_FOUND) - if (Qt5Widgets_VERSION VERSION_LESS 5.15.0) - message(STATUS "Qt version ${Qt5Widgets_VERSION}") - message(WARNING "Minimum supported Qt5 version is 5.15!") +if (Qt6Widgets_FOUND) + if (Qt6Widgets_VERSION VERSION_LESS 6.5.0) + message(STATUS "Qt version ${Qt6Widgets_VERSION}") + message(WARNING "Minimum supported Qt6 version is 6.5!") endif() -endif(Qt5Widgets_FOUND) +endif(Qt6Widgets_FOUND) set(CMAKE_INCLUDE_CURRENT_DIR ON) if(NOT MSVC) @@ -386,6 +387,8 @@ set(SRC_FILES # UI components src/ui/HiddenEvents.cpp src/ui/HiddenEvents.h + src/ui/EventExpiry.cpp + src/ui/EventExpiry.h src/ui/MxcAnimatedImage.cpp src/ui/MxcAnimatedImage.h src/ui/MxcMediaProxy.cpp @@ -394,8 +397,6 @@ set(SRC_FILES src/ui/NhekoCursorShape.h src/ui/NhekoDropArea.cpp src/ui/NhekoDropArea.h - src/ui/NhekoEventObserver.cpp - src/ui/NhekoEventObserver.h src/ui/NhekoGlobalObject.cpp src/ui/NhekoGlobalObject.h src/ui/RoomSettings.cpp @@ -498,8 +499,6 @@ set(SRC_FILES src/SingleImagePackModel.h src/TrayIcon.cpp src/TrayIcon.h - src/UserDirectoryModel.cpp - src/UserDirectoryModel.h src/UserSettingsPage.cpp src/UserSettingsPage.h src/UsersModel.cpp @@ -602,7 +601,7 @@ if(USE_BUNDLED_MTXCLIENT) FetchContent_Declare( MatrixClient GIT_REPOSITORY https://github.com/Nheko-Reborn/mtxclient.git - GIT_TAG e136bc27b28d3bb5683735eb5a65d6ef2534ca3a + GIT_TAG 0a4cc9421a97bea81a8921f3f5e040f0a34278fc ) set(BUILD_LIB_EXAMPLES OFF CACHE INTERNAL "") set(BUILD_LIB_TESTS OFF CACHE INTERNAL "") @@ -614,9 +613,10 @@ endif() if (VOIP) include(FindPkgConfig) pkg_check_modules(GSTREAMER REQUIRED IMPORTED_TARGET gstreamer-sdp-1.0>=1.18 gstreamer-webrtc-1.0>=1.18) - if (SCREENSHARE_X11 AND NOT WIN32 AND NOT APPLE) - pkg_check_modules(XCB REQUIRED IMPORTED_TARGET xcb xcb-ewmh) - endif() +endif() + +if (X11 AND NOT WIN32 AND NOT APPLE AND NOT HAIKU) + pkg_check_modules(XCB REQUIRED IMPORTED_TARGET xcb xcb-ewmh) endif() # single instance functionality @@ -631,10 +631,13 @@ if (NOT APPLE AND NOT WIN32) endif() # -# Bundle translations. +# Bundle resources # -include(Translations) -set(TRANSLATION_DEPS ${LANG_QRC} ${QRC} ${QM_SRC}) +if(Qt6QuickCompiler_FOUND AND COMPILE_QML) + qtquick_compiler_add_resources(QRC resources/res.qrc) +else() + qt_add_resources(QRC resources/res.qrc) +endif() if (APPLE) set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -framework Foundation -framework Cocoa -framework UserNotifications") @@ -666,7 +669,7 @@ endif () set(NHEKO_DEPS ${SRC_FILES} - ${TRANSLATION_DEPS} + ${QRC} ${META_FILES_TO_INCLUDE}) if(ASAN) @@ -674,10 +677,10 @@ if(ASAN) endif() if(WIN32) - add_executable (nheko WIN32 ${OS_BUNDLE} ${NHEKO_DEPS}) + qt_add_executable (nheko WIN32 ${OS_BUNDLE} ${NHEKO_DEPS}) target_compile_definitions(nheko PRIVATE _WIN32_WINNT=0x0601 NOMINMAX WIN32_LEAN_AND_MEAN STRICT) else() - add_executable (nheko ${OS_BUNDLE} ${NHEKO_DEPS}) + qt_add_executable (nheko ${OS_BUNDLE} ${NHEKO_DEPS}) if (HAVE_BACKTRACE_SYMBOLS_FD AND NOT CMAKE_BUILD_TYPE STREQUAL "Release") set_target_properties(nheko PROPERTIES ENABLE_EXPORTS ON) @@ -689,22 +692,150 @@ set_target_properties(nheko CMAKE_SKIP_INSTALL_RPATH TRUE AUTOMOC ON) -if(APPLE) - target_link_libraries (nheko PRIVATE Qt5::MacExtras) -elseif(WIN32) +# +# Bundle translations +# +file(GLOB LANG_TS_SRC "${CMAKE_CURRENT_SOURCE_DIR}/resources/langs/*.ts") +qt_add_translations(nheko RESOURCE_PREFIX "/translations" TS_FILES ${LANG_TS_SRC}) + + +# +# Add qml files +# + +set(QML_SOURCES + resources/qml/Root.qml + resources/qml/ChatPage.qml + resources/qml/CommunitiesList.qml + resources/qml/RoomList.qml + resources/qml/TimelineView.qml + resources/qml/Avatar.qml + resources/qml/Completer.qml + resources/qml/EncryptionIndicator.qml + resources/qml/ImageButton.qml + resources/qml/ElidedLabel.qml + resources/qml/MatrixText.qml + resources/qml/MatrixTextField.qml + resources/qml/ToggleButton.qml + resources/qml/UploadBox.qml + resources/qml/MessageInput.qml + resources/qml/MessageView.qml + resources/qml/PrivacyScreen.qml + resources/qml/Reactions.qml + resources/qml/ReplyPopup.qml + resources/qml/StatusIndicator.qml + resources/qml/TimelineRow.qml + resources/qml/TopBar.qml + resources/qml/QuickSwitcher.qml + resources/qml/ForwardCompleter.qml + resources/qml/SelfVerificationCheck.qml + resources/qml/TypingIndicator.qml + resources/qml/MessageInputWarning.qml + resources/qml/components/AdaptiveLayout.qml + resources/qml/components/AdaptiveLayoutElement.qml + resources/qml/components/AvatarListTile.qml + resources/qml/components/FlatButton.qml + resources/qml/components/MainWindowDialog.qml + resources/qml/components/NhekoTabButton.qml + resources/qml/components/NotificationBubble.qml + resources/qml/components/ReorderableListview.qml + resources/qml/components/SpaceMenuLevel.qml + resources/qml/components/TextButton.qml + resources/qml/components/UserListRow.qml + resources/qml/delegates/Encrypted.qml + resources/qml/delegates/FileMessage.qml + resources/qml/delegates/ImageMessage.qml + resources/qml/delegates/MessageDelegate.qml + resources/qml/delegates/NoticeMessage.qml + resources/qml/delegates/Pill.qml + resources/qml/delegates/Placeholder.qml + resources/qml/delegates/PlayableMediaMessage.qml + resources/qml/delegates/Redacted.qml + resources/qml/delegates/Reply.qml + resources/qml/delegates/TextMessage.qml + resources/qml/device-verification/DeviceVerification.qml + resources/qml/device-verification/DigitVerification.qml + resources/qml/device-verification/EmojiVerification.qml + resources/qml/device-verification/Failed.qml + resources/qml/device-verification/NewVerificationRequest.qml + resources/qml/device-verification/Success.qml + resources/qml/device-verification/Waiting.qml + resources/qml/dialogs/AliasEditor.qml + resources/qml/dialogs/ConfirmJoinRoomDialog.qml + resources/qml/dialogs/CreateDirect.qml + resources/qml/dialogs/CreateRoom.qml + resources/qml/dialogs/HiddenEventsDialog.qml + resources/qml/dialogs/EventExpirationDialog.qml + resources/qml/dialogs/ImageOverlay.qml + resources/qml/dialogs/ImagePackEditorDialog.qml + resources/qml/dialogs/ImagePackSettingsDialog.qml + resources/qml/dialogs/InputDialog.qml + resources/qml/dialogs/InviteDialog.qml + resources/qml/dialogs/JoinRoomDialog.qml + resources/qml/dialogs/LeaveRoomDialog.qml + resources/qml/dialogs/LogoutDialog.qml + resources/qml/dialogs/PhoneNumberInputDialog.qml + resources/qml/dialogs/PowerLevelEditor.qml + resources/qml/dialogs/PowerLevelSpacesApplyDialog.qml + resources/qml/dialogs/RawMessageDialog.qml + resources/qml/dialogs/ReadReceipts.qml + resources/qml/dialogs/RoomDirectory.qml + resources/qml/dialogs/RoomMembers.qml + resources/qml/dialogs/AllowedRoomsSettingsDialog.qml + resources/qml/dialogs/RoomSettings.qml + resources/qml/dialogs/UserProfile.qml + resources/qml/emoji/StickerPicker.qml + resources/qml/pages/LoginPage.qml + resources/qml/pages/RegisterPage.qml + resources/qml/pages/UserSettingsPage.qml + resources/qml/pages/WelcomePage.qml + resources/qml/ui/NhekoSlider.qml + resources/qml/ui/Ripple.qml + resources/qml/ui/Snackbar.qml + resources/qml/ui/Spinner.qml + resources/qml/ui/animations/BlinkAnimation.qml + resources/qml/ui/media/MediaControls.qml + resources/qml/voip/ActiveCallBar.qml + resources/qml/voip/CallDevices.qml + resources/qml/voip/CallInvite.qml + resources/qml/voip/CallInviteBar.qml + resources/qml/voip/DeviceError.qml + resources/qml/voip/PlaceCall.qml + resources/qml/voip/ScreenShare.qml + resources/qml/voip/VideoCall.qml + resources/qml/delegates/EncryptionEnabled.qml + resources/qml/ui/TimelineEffects.qml +) +qt_add_qml_module(nheko + URI im.nheko + NO_RESOURCE_TARGET_PATH + RESOURCE_PREFIX "/" + VERSION 1.1 + DEPENDENCIES QtQml QtQuick # https://bugreports.qt.io/browse/QTBUG-102554 + QML_FILES + ${QML_SOURCES} + SOURCES + src/UserDirectoryModel.cpp + src/UserDirectoryModel.h + ) + #qt_target_qml_sources(nheko + # #PREFIX "/" + #) + + +if(WIN32) target_compile_definitions(nheko PRIVATE WIN32_LEAN_AND_MEAN) - target_link_libraries (nheko PRIVATE ${NTDLIB} Qt5::WinMain) if(MSVC) target_compile_options(nheko PUBLIC "/Zc:__cplusplus") endif() else() - target_link_libraries (nheko PRIVATE Qt5::DBus) + target_link_libraries (nheko PRIVATE Qt6::DBus) if (FLATPAK) target_compile_definitions(nheko PRIVATE NHEKO_FLATPAK) endif() endif() -target_include_directories(nheko PRIVATE src includes) +target_include_directories(nheko PRIVATE src includes src/timeline/ src/ui/ src/encryption/ src/voip/) if (USE_BUNDLED_CPPHTTPLIB) target_include_directories(nheko PRIVATE third_party/cpp-httplib-0.5.12) @@ -729,7 +860,7 @@ endif() # Fixup bundled keychain include dirs if (USE_BUNDLED_QTKEYCHAIN) - target_include_directories(nheko PRIVATE ${qt5keychain_SOURCE_DIR} ${qt5keychain_BINARY_DIR}) + target_include_directories(nheko PRIVATE ${qt6keychain_SOURCE_DIR} ${qt6keychain_BINARY_DIR}) endif() if (NOT JSON_ImplicitConversions) @@ -744,14 +875,13 @@ target_link_libraries(nheko PRIVATE MatrixClient::MatrixClient cmark::cmark spdlog::spdlog - Qt5::Widgets - Qt5::Svg - Qt5::Concurrent - Qt5::Multimedia - Qt5::Qml - Qt5::QuickControls2 - Qt5::QuickWidgets - qt5keychain + Qt::Widgets + Qt::Svg + Qt::Gui + Qt::Multimedia + Qt::Qml + Qt::QuickControls2 + qt6keychain nlohmann_json::nlohmann_json lmdbxx::lmdbxx liblmdb::lmdb @@ -768,10 +898,10 @@ endif() if (TARGET PkgConfig::GSTREAMER) target_link_libraries(nheko PRIVATE PkgConfig::GSTREAMER) target_compile_definitions(nheko PRIVATE GSTREAMER_AVAILABLE) - if (TARGET PkgConfig::XCB) - target_link_libraries(nheko PRIVATE PkgConfig::XCB) - target_compile_definitions(nheko PRIVATE XCB_AVAILABLE) - endif() +endif() +if (TARGET PkgConfig::XCB) + target_link_libraries(nheko PRIVATE PkgConfig::XCB) + target_compile_definitions(nheko PRIVATE XCB_AVAILABLE) endif() if(MSVC) @@ -797,9 +927,23 @@ if(MAN) add_subdirectory(man) endif() +# potential workaround for macdeployqt issues +if(APPLE) + install(TARGETS nheko + BUNDLE DESTINATION . + RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} + ) + qt_generate_deploy_qml_app_script( + TARGET nheko + OUTPUT_SCRIPT deploy_script + NO_UNSUPPORTED_PLATFORM_ERROR + ) + install(SCRIPT ${deploy_script}) +endif() + if(UNIX AND NOT APPLE) if(FLATPAK) - set(APPID "io.github.NhekoReborn.Nheko") + set(APPID "im.nheko.Nheko") set_target_properties(nheko PROPERTIES OUTPUT_NAME "${APPID}") else() set(APPID "nheko") diff --git a/README.md b/README.md index fc20e0c9..73f8587d 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ nheko [![Build status](https://ci.appveyor.com/api/projects/status/07qrqbfylsg4hw2h/branch/master?svg=true)](https://ci.appveyor.com/project/redsky17/nheko/branch/master) [![Stable Version](https://img.shields.io/badge/download-stable-green.svg)](https://github.com/Nheko-Reborn/nheko/releases/latest) [![Nightly](https://img.shields.io/badge/download-nightly-green.svg)](https://matrix-static.neko.dev/room/!TshDrgpBNBDmfDeEGN:neko.dev/) -Download Nightly Flatpak +Download Nightly Flatpak [![#nheko-reborn:matrix.org](https://img.shields.io/matrix/nheko-reborn:matrix.org.svg?label=%23nheko-reborn:matrix.org)](https://matrix.to/#/#nheko-reborn:matrix.org) [![Arch package](https://repology.org/badge/version-for-repo/arch/nheko.svg)](https://archlinux.org/packages/community/x86_64/nheko/) Download on Flathub @@ -249,7 +249,7 @@ KDE has similar plugins, that can extend the supported image types even more. - Voice call support: dtls, opus, rtpmanager, srtp, webrtc - Video call support (optional): compositor, opengl, qmlgl, rtp, vpx - [libnice](https://gitlab.freedesktop.org/libnice/libnice) -- XCB, XCB-EWMH: For screensharing support on X11. VOIP needs to be enabled. Can be disabled with `-DSCREENSHARE_X11=OFF`. +- XCB, XCB-EWMH: For screensharing support on X11 and setting window roles. Can be disabled with `-DSCREENSHARE_X11=OFF`. - [qtkeychain](https://github.com/frankosterfeld/qtkeychain) (You need at least version 0.12 for proper Gnome Keychain support. The bundled version requires libsecret, unless you pass `-DLIBSECRET_SUPPORT=OFF`.) - A compiler that supports C++ 20: - Clang 16 (Only clazy 16 is tested in CI) diff --git a/appveyor.yml b/appveyor.yml index 4a0b1eec..49bb1e51 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -22,7 +22,7 @@ build: verbosity: minimal install: - - set QT_DIR=C:\Qt\5.15\msvc2019_64 + - set QT_DIR=C:\Qt\6.5\msvc2019_64 - set PATH=C:\Strawberry\perl\bin;C:\Python39-x64;%QT_DIR%\bin;%PATH% build_script: diff --git a/cmake/Translations.cmake b/cmake/Translations.cmake deleted file mode 100644 index 887697a8..00000000 --- a/cmake/Translations.cmake +++ /dev/null @@ -1,28 +0,0 @@ -# -# Generate the translation resource file -# - -file(GLOB LANG_TS_SRC "${CMAKE_CURRENT_SOURCE_DIR}/resources/langs/*.ts") - -qt5_add_translation(QM_SRC ${LANG_TS_SRC}) -qt5_create_translation(${QM_SRC}) -add_custom_target(LANG_QRC ALL DEPENDS ${QM_SRC}) - -# Generate a qrc file for the translations -set(_qrc ${CMAKE_CURRENT_BINARY_DIR}/translations.qrc) - -if(NOT EXISTS ${_qrc}) - file(WRITE ${_qrc} "\n \n") - foreach(_lang ${QM_SRC}) - get_filename_component(_filename ${_lang} NAME) - file(APPEND ${_qrc} " ${_filename}\n") - endforeach(_lang) - file(APPEND ${_qrc} " \n\n") -endif() - -qt5_add_resources(LANG_QRC ${_qrc}) -if(Qt5QuickCompiler_FOUND AND COMPILE_QML) - qtquick_compiler_add_resources(QRC resources/res.qrc) -else() - qt5_add_resources(QRC resources/res.qrc) -endif() diff --git a/io.github.NhekoReborn.Nheko.yaml b/im.nheko.Nheko.yaml similarity index 77% rename from io.github.NhekoReborn.Nheko.yaml rename to im.nheko.Nheko.yaml index d45dba3d..4fa8ccfb 100644 --- a/io.github.NhekoReborn.Nheko.yaml +++ b/im.nheko.Nheko.yaml @@ -1,7 +1,7 @@ -id: io.github.NhekoReborn.Nheko -command: io.github.NhekoReborn.Nheko +id: im.nheko.Nheko +command: im.nheko.Nheko runtime: org.kde.Platform -runtime-version: '5.15-22.08' +runtime-version: '6.5' sdk: org.kde.Sdk finish-args: - --device=dri @@ -44,9 +44,9 @@ cleanup: modules: - name: lmdb sources: - - sha256: f3927859882eb608868c8c31586bb7eb84562a40a6bf5cc3e13b6b564641ea28 + - sha256: 8c5a93ac3cc97427c54571ad5a6140b7469389d01e6d2f43df39f96d3a4ccef7 type: archive - url: https://github.com/LMDB/lmdb/archive/LMDB_0.9.22.tar.gz + url: https://git.openldap.org/openldap/openldap/-/archive/LMDB_0.9.30/openldap-LMDB_0.9.30.tar.gz make-install-args: - prefix=/app no-autogen: true @@ -109,45 +109,46 @@ modules: tag: 0.20.4 type: git url: https://gitlab.gnome.org/GNOME/libsecret.git - - config-opts: - - -DCMAKE_BUILD_TYPE=Release - - -DAVIF_CODEC_AOM=ON - #- -DBUILD_SHARED_LIBS=OFF - buildsystem: cmake-ninja - name: libavif - sources: - - sha256: 66e82854ceb84a3e542bc140a343bc90e56c68f3ecb4fff63e636c136ed9a05e - type: archive - url: https://github.com/AOMediaCodec/libavif/archive/refs/tags/v0.10.1.tar.gz - - config-opts: - - -DCMAKE_BUILD_TYPE=Release - - -DWITH_EXAMPLES=OFF - #- -DBUILD_SHARED_LIBS=OFF - buildsystem: cmake-ninja - name: libheif - sources: - - sha256: e1ac2abb354fdc8ccdca71363ebad7503ad731c84022cf460837f0839e171718 - type: archive - url: https://github.com/strukturag/libheif/releases/download/v1.12.0/libheif-1.12.0.tar.gz - - config-opts: - - -DCMAKE_BUILD_TYPE=Release - - -DKIMAGEFORMATS_HEIF=ON - buildsystem: cmake-ninja - name: KImageFormats - sources: - - commit: ae6b724824fc2fdf71d50dc7ae0052ad1551b25a - tag: v5.93.0 - type: git - url: https://invent.kde.org/frameworks/kimageformats.git + #- config-opts: + # - -DCMAKE_BUILD_TYPE=Release + # - -DAVIF_CODEC_AOM=ON + # #- -DBUILD_SHARED_LIBS=OFF + # buildsystem: cmake-ninja + # name: libavif + # sources: + # - sha256: 66e82854ceb84a3e542bc140a343bc90e56c68f3ecb4fff63e636c136ed9a05e + # type: archive + # url: https://github.com/AOMediaCodec/libavif/archive/refs/tags/v0.10.1.tar.gz + #- config-opts: + # - -DCMAKE_BUILD_TYPE=Release + # - -DWITH_EXAMPLES=OFF + # #- -DBUILD_SHARED_LIBS=OFF + # buildsystem: cmake-ninja + # name: libheif + # sources: + # - sha256: e1ac2abb354fdc8ccdca71363ebad7503ad731c84022cf460837f0839e171718 + # type: archive + # url: https://github.com/strukturag/libheif/releases/download/v1.12.0/libheif-1.12.0.tar.gz + #- config-opts: + # - -DCMAKE_BUILD_TYPE=Release + # - -DKIMAGEFORMATS_HEIF=ON + # buildsystem: cmake-ninja + # name: KImageFormats + # sources: + # - commit: ae6b724824fc2fdf71d50dc7ae0052ad1551b25a + # tag: v5.93.0 + # type: git + # url: https://invent.kde.org/frameworks/kimageformats.git - config-opts: - -DCMAKE_BUILD_TYPE=Release - -DBUILD_TEST_APPLICATION=OFF - -DQTKEYCHAIN_STATIC=ON + - -DBUILD_WITH_QT6=ON buildsystem: cmake-ninja name: QtKeychain sources: - - commit: f59ac26be709fd2d8d7a062fab1cf1e67a93806c - tag: v0.13.1 + - commit: 69f993c47efed7e557d79a30a367014d9a27d809 + tag: 0.14.1 type: git url: https://github.com/frankosterfeld/qtkeychain.git - config-opts: @@ -170,15 +171,15 @@ modules: - buildsystem: meson name: gstreamer sources: - - commit: f7806a854aad960eae3288db4a67a574f92428fe - tag: 1.20.5 + - commit: ecd471f5ea4645102b206a43d863f0f0fe7d04ec + tag: 1.22.3 type: git url: https://gitlab.freedesktop.org/gstreamer/gstreamer.git config-opts: - --auto-features=disabled - -Dgood=enabled - - -Dgst-plugins-good:qt5=enabled - - -Dqt5=enabled + - -Dgst-plugins-good:qt6=enabled + #- -Dqt6=enabled <- not available on 1.22 - -Dbase=enabled - -Dgst-plugins-base:gl=enabled - -Dgst-plugins-base:gl_platform=glx,egl @@ -213,7 +214,7 @@ modules: buildsystem: cmake-ninja name: mtxclient sources: - - commit: e136bc27b28d3bb5683735eb5a65d6ef2534ca3a + - commit: 0a4cc9421a97bea81a8921f3f5e040f0a34278fc #tag: v0.9.2 type: git url: https://github.com/Nheko-Reborn/mtxclient.git diff --git a/nheko-nightly.flatpakref b/nheko-nightly.flatpakref index 74e47ecd..484b5de0 100644 --- a/nheko-nightly.flatpakref +++ b/nheko-nightly.flatpakref @@ -1,6 +1,6 @@ [Flatpak Ref] Title=Nheko Nightly -Name=io.github.NhekoReborn.Nheko +Name=im.nheko.Nheko Branch=master Url=https://flatpak.neko.dev/repo/nightly SuggestRemoteName=nheko-nightlies diff --git a/resources/langs/nheko_ca.ts b/resources/langs/nheko_ca.ts index 035978f3..8de8ee24 100644 --- a/resources/langs/nheko_ca.ts +++ b/resources/langs/nheko_ca.ts @@ -1390,11 +1390,6 @@ You may optionally provide a reason for others to accept your knock: InputBar - - - Select a file - - All Files (*) diff --git a/resources/langs/nheko_cs.ts b/resources/langs/nheko_cs.ts index 9c4af145..87ea404f 100644 --- a/resources/langs/nheko_cs.ts +++ b/resources/langs/nheko_cs.ts @@ -1392,11 +1392,6 @@ You may optionally provide a reason for others to accept your knock: InputBar - - - Select a file - - All Files (*) diff --git a/resources/langs/nheko_de.ts b/resources/langs/nheko_de.ts index 24cd3f7b..d77eacf1 100644 --- a/resources/langs/nheko_de.ts +++ b/resources/langs/nheko_de.ts @@ -43,12 +43,12 @@ Failed to unpublish alias %1: %2 - Konnte den Alias %1 nicht depublizieren: %2 + Konnte die Raumadresse %1 nicht entfernen: %2 Failed to update aliases: %1 - Konnte die Aliasse nicht aktualisieren: %1 + Konnte die Raumadressen nicht aktualisieren: %1 @@ -56,12 +56,12 @@ Aliases to %1 - Aliasse für %1 + Aliase für %1 List of aliases to this room. Usually you can only add aliases on your server. You can have one canonical alias and many alternate aliases. - Aliasse dieses Raumes. Normalerweise kannst du Aliasse nur für deinen eigenen Server hinzufügen. Du kannst einen primären Alias und beliebig viele Zweitaliasse hinzufügen. + Aliase dieses Raumes. Normalerweise kannst du Aliase nur für deinen eigenen Server hinzufügen. Du kannst einen primären Alias und beliebig viele Zweitaliase hinzufügen. @@ -320,7 +320,7 @@ Do you really want to start a private chat with %1? - Möchtest du wirklich eine privaten Chat mit %1 beginnen? + Möchtest du wirklich eine private Konversation mit %1 beginnen? @@ -373,7 +373,7 @@ Wenn du glaubst, dass das ein Fehler ist, dann kannst du Nheko schließen und vi You failed to join %1. You can try to knock so that others can invite you in. Do you want to do so? You may optionally provide a reason for others to accept your knock: - Du konntest %1 nicht betreten. Du kannst versuchen, anzuklopfen, so dass andere Leute dich einladen können. Möchtest du das tun? + Du konntest %1 nicht betreten. Du kannst versuchen anzuklopfen, so dass andere Leute dich einladen können. Möchtest du das tun? Du kannst zusätzlich einen Grund angeben, warum die anderen dein Anklopfen annehmen sollten: @@ -399,17 +399,17 @@ Du kannst zusätzlich einen Grund angeben, warum die anderen dein Anklopfen anne Reason for the kick - Grund für den Rauswurf des Nutzers + Grund für das Entfernen des Nutzers Enter reason for kicking %1 (%2) or hit enter for no reason: - Gib einen Grund für den Rauswurf von %1 (%2) ein oder drücke Eingabe für keinen Grund: + Grund warum %1 (%2) aus dem Raum geworfen wird oder die Entertaste drücken um keinen Grund anzugeben: Failed to kick %1 from %2: %3 - Konnte %1 nicht aus %2 hinauswerfen: %3 + Konnte %1 nicht aus %2 entfernen: %3 @@ -562,38 +562,38 @@ Du kannst zusätzlich einen Grund angeben, warum die anderen dein Anklopfen anne Ask to join a room. Reason is optional. - Anfragen, einen Raum zu betreten zu dürfen. Der Grund ist optional. + Anfragen einen Raum zu betreten. Ein Grund ist optional. Leave a room. Reason is optional. - Raum verlassen. Der Grund ist optional. + Raum verlassen. Ein Grund ist optional. Invite a user into the current room. Reason is optional. - Einen Nutzer in diesen Raum einladen. Der Grund ist optional. + Einen Nutzer in diesen Raum einladen. Ein Grund ist optional. Kick a user from the current room. Reason is optional. - Einen Nutzer aus diesem Raum werfen. Der Grund ist optional. + Einen Nutzer aus diesem Raum werfen. Ein Grund ist optional. Ban a user from the current room. Reason is optional. - Einen Nutzer aus diesen Raum verbannen. Der Grund ist optional. + Einen Nutzer von diesem Raum verbannen. Ein Grund ist optional. Unban a user in the current room. Reason is optional. - Verbannung eines Nutzers aufheben. Der Grund ist optional. + Verbannung eines Nutzers aufheben. Ein Grund ist optional. Redact an event or all locally cached messages of a user. - Ein Event oder alle lokal zwischengespeicherten Nachrichten eines Nutzers zurückziehen. + Eine bestimmte Nachricht oder alle lokal geladenen Nachrichten eines Nutzers löschen. @@ -663,12 +663,12 @@ Du kannst zusätzlich einen Grund angeben, warum die anderen dein Anklopfen anne Send a bot message. - Sende eine Bot-Nachricht. + Sende eine Nachricht als wärst du ein Bot. Send a bot message in rainbow colors. - Sende eine Bot-Nachricht, aber in Regenbogenfarben. + Sende eine Botnachricht, aber in Regenbogenfarben. @@ -688,12 +688,12 @@ Du kannst zusätzlich einen Grund angeben, warum die anderen dein Anklopfen anne Convert this room to a direct chat. - Verwandle diesen Raum zu einen Direktchat. + Verwandel diesen Raum in eine Direktnachricht. Convert this direct chat into a room. - Verwandle diesen Direktchat zu einen Raum. + Verwandle diese Direktnachricht in einen normalen Chatraum. @@ -734,12 +734,12 @@ Du kannst zusätzlich einen Grund angeben, warum die anderen dein Anklopfen anne Direct Chats - Direktchats + Direktnachrichten Show direct chats. - Zeige Direktchats an. + Zeige 1:1 Konversationen an. @@ -749,7 +749,7 @@ Du kannst zusätzlich einen Grund angeben, warum die anderen dein Anklopfen anne Rooms you have favourited. - Von dir favorisierte Räume. + Favorisierte Räume. @@ -815,12 +815,12 @@ Du kannst zusätzlich einen Grund angeben, warum die anderen dein Anklopfen anne Do you want to join this room? You can optionally add a reason below: - Möchtest du den Raum betreten? Du kannst unten einen Grund angeben (optional): + Möchtest du den Raum betreten? Du kannst unten einen Grund angeben: This room can't be joined directly. You can, however, knock on the room and room members can accept or decline this join request. You can additionally provide a reason for them to let you in below: - Dieser Raum kann nicht direkt betreten werden. Du kannst jedoch anklopfen, und Raummitglieder können diese Beitrittsanfrage akzeptieren oder ablehnen. Optional kannst du auch einen Grund angeben, um eingelassen zu werden: + Dieser Raum kann nicht direkt betreten werden. Du kanns aber anklopfen und die Personen in dem Raum können dich dann reinlassen oder auch nicht. Optional kannst du auch einen Grund angeben, warum sie das tun sollten. @@ -848,7 +848,7 @@ Du kannst zusätzlich einen Grund angeben, warum die anderen dein Anklopfen anne @user:server.tld - @nutzer.server.tld + @nutzer:server.tld @@ -876,7 +876,7 @@ Du kannst zusätzlich einen Grund angeben, warum die anderen dein Anklopfen anne New Room - Neuer Raum + Neuer Chatraum @@ -916,7 +916,7 @@ Du kannst zusätzlich einen Grund angeben, warum die anderen dein Anklopfen anne All invitees are given the same power level as the creator - Alle eingeladenen Personen erhalten das gleiche Powerlevel wie der Ersteller + Alle eingeladenen Personen erhalten die gleichen Berechtigungen wie der Ersteller @@ -967,7 +967,7 @@ Du kannst zusätzlich einen Grund angeben, warum die anderen dein Anklopfen anne 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! - Bitte verifiziere die folgenden Ziffern. Du solltest die gleichen Zahlen auf beiden Seiten sehen. Wenn diese sich unterscheiden, bitte klicke auf „Sie stimmen nicht überein!“, um die Verifizierung abzubrechen! + Bitte verifiziere die folgenden Ziffern. Stelle sicher dass beide Seiten die gleichen Zahlen sehen. Wenn diese sich unterscheiden, bitte klicke auf 'Sie stimmen nicht überein!' um die Verifizierung abzubrechen! @@ -1038,12 +1038,12 @@ Du kannst zusätzlich einen Grund angeben, warum die anderen dein Anklopfen anne 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! - Bitte verifiziere die folgenden Emojis. Stelle sicher, dass du auf beiden Seiten das gleiche Emoji siehst. Wenn diese sich unterscheiden, bitte klicke auf „Sie stimmen nicht überein!“, um die Verifizierung abzubrechen! + Bitte verifiziere die folgenden Emoji. Stelle sicher dass beide Seiten die gleichen Emoji sehen. Wenn diese sich unterscheiden, bitte klicke auf 'Sie stimmen nicht überein!' um die Verifizierung abzubrechen! The displayed emoji might look different in different clients if a different font is used. Similarly they might be translated into different languages. Nonetheless they should depict one of 64 different objects or animals. For example a lion and a cat are different, but a cat is the same even if one client just shows a cat face, while another client shows a full cat body. - Je nach Schriftart können die angezeigten Emojis sich in unterschiedlichen Applikationen leicht unterscheiden. Auf die selbe Art kann sich die Übersetzung unter dem Emoji je nach Sprache unterscheiden. Trotzdem sollten die 64 möglichen Zeichen eindeutig genug sein. Z.B. sind eine Katze und ein Löwe unterschiedlich, aber in der einen Applikation ist die Katze eventuell nur als Gesicht dargestellt und nicht als ganze Katze. + Je nach Schriftart können die angezeigten Emoji sich in unterschiedlichen Applikationen leicht unterscheiden. Auf die selbe Art kann sich die Übersetzung unter dem Emoji je nach Sprache unterscheiden. Trotzdem sollten die 64 möglichen Zeichen eindeutig genug sein. Z.B. sind eine Katze und ein Löwe unterschiedlich, aber in der einen Applikation ist die Katze eventuell nur als Gesicht dargestellt und nicht als ganze Katze. @@ -1071,7 +1071,7 @@ Du kannst zusätzlich einen Grund angeben, warum die anderen dein Anklopfen anne There was an internal error reading the decryption key from the database. - Beim Lesen des Entschlüsselungsschlüssels aus der Datenbank ist ein interner Fehler aufgetreten. + Es ist ein interner Fehler beim Laden des Schlüssels aus der Datenbank aufgetreten. @@ -1081,12 +1081,12 @@ Du kannst zusätzlich einen Grund angeben, warum die anderen dein Anklopfen anne The message couldn't be parsed. - Die Nachricht konnte nicht geparst werden. + 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 Verschlüsselungsschlüssel wurde wiederverwendet. Möglicherweise versucht jemand, falsche Nachrichten in diesen Chat einzufügen! + Der Schlüssel dieser Nachricht wurde schon einmal verwendet! Eventuell versucht jemand, falsche Nachrichten in diese Unterhaltung einzufügen! @@ -1155,7 +1155,7 @@ Du kannst zusätzlich einen Grund angeben, warum die anderen dein Anklopfen anne Device verification timed out. - Zeitlimit für die Geräteverifizierung abgelaufen. + Verifizierung abgelaufen, die andere Seite antwortet nicht. @@ -1220,7 +1220,7 @@ Du kannst zusätzlich einen Grund angeben, warum die anderen dein Anklopfen anne User events - Benutzerevents + Benutzeränderungen @@ -1230,7 +1230,7 @@ Du kannst zusätzlich einen Grund angeben, warum die anderen dein Anklopfen anne Power level changes - Powerlevelveränderungen + Berechtigungsveränderungen @@ -1248,7 +1248,7 @@ Du kannst zusätzlich einen Grund angeben, warum die anderen dein Anklopfen anne Editing image pack - Bilderpaket bearbeiten + Bilderpackung bearbeiten @@ -1263,17 +1263,17 @@ Du kannst zusätzlich einen Grund angeben, warum die anderen dein Anklopfen anne Select images for pack - Wähle Bilder für dieses Paket + Wähle Bilder für diese Packung Add to pack - Zum Paket hinzufügen + Zur Packung hinzufügen Change the overview image for this pack - Ändere das Vorschaubild dieses Pakets + Ändere das Vorschaubild dieser Packung @@ -1283,12 +1283,12 @@ Du kannst zusätzlich einen Grund angeben, warum die anderen dein Anklopfen anne Select overview image for pack - Wähle ein Vorschaubild für das Paket aus + Wähle ein Vorschaubild für diese Packung aus State key - Zustandsschlüssel + Eindeutiger Name @@ -1338,37 +1338,37 @@ Du kannst zusätzlich einen Grund angeben, warum die anderen dein Anklopfen anne Image pack settings - Bilderpaketeinstellungen + Bilderpackungseinstellungen Create account pack - Kontopaket erstellen + Neue private Packung New room pack - Neues Raumpaket + Neue raumspezifische Packung Private pack - Privates Paket + Private Packung Pack from this room - Paket für diesen Raum + Packung aus diesem Raum Pack from parent community - Paket von übergeordneter Gruppe + Packung von übergeordneter Gruppe Globally enabled pack - Global aktiviertes Paket + Global aktivierte Packung @@ -1378,7 +1378,7 @@ Du kannst zusätzlich einen Grund angeben, warum die anderen dein Anklopfen anne Enables this pack to be used in all rooms - Macht dieses Paket in allen Räumen verfügbar + Macht diese Packung in allen Räumen verfügbar @@ -1393,11 +1393,6 @@ Du kannst zusätzlich einen Grund angeben, warum die anderen dein Anklopfen anne InputBar - - - Select a file - Datei auswählen - All Files (*) @@ -1406,7 +1401,7 @@ Du kannst zusätzlich einen Grund angeben, warum die anderen dein Anklopfen anne Upload of '%1' failed - Das Hochladen von „%1“ ist fehlgeschlagen + Das Hochladen von '%1' ist fehlgeschlagen @@ -1492,10 +1487,10 @@ Du kannst zusätzlich einen Grund angeben, warum die anderen dein Anklopfen anne 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. - Dein Anmeldename. Eine mxid sollte mit einem „@“ anfangen gefolgt von der Nutzer-ID. Nach dem Nutzernamen folgt dein Servername, getrennt durch einen „:“. -Wenn dein Server den „.well-known“-Lookup nicht unterstützt, kannst du auch deine Heimserveradresse angeben. + Dein Anmeldename. Eine mxid sollte mit eine @ anfangen gefolgt von der Nutzerid. Nach dem Nutzernamen folgt der servername, getrennt durch ein :. +Wenn dein Server keinen .well-known unterstützt, kannst du auch eine Serveradresse angeben. Beispiel: @nutzer:mein.server -Wenn Nheko den Server nicht finden kann, wird es dir ein Eingabefeld anzeigen, in das du den Server manuell eingeben kannst. +Wenn Nheko den Server nicht finden kann, wird es dich nach der Serveradresse fragen. @@ -1532,7 +1527,7 @@ Beispiel: https://mein.server:8787 server.my:8787 - mein.server:8787 + dein.server:8787 @@ -1560,7 +1555,7 @@ Beispiel: https://mein.server:8787 Autodiscovery failed. Unknown error when requesting .well-known. - Automatische Erkennung fehlgeschlagen. Unbekannter Fehler bei Anfrage von „.well-known“. + Automatische Erkennung fehlgeschlagen. Unbekannter Fehler bei Anfrage .well-known. @@ -1570,17 +1565,17 @@ Beispiel: https://mein.server:8787 Received malformed response. Make sure the homeserver domain is valid. - Erhaltene Antwort war fehlerhaft. Bitte prüfe, ob die Heimserver-Domain gültig ist. + 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 prüfe, ob die Heimserver-Domain gültig ist. + Ein unbekannter Fehler ist aufgetreten. Bitte Homeserverdomain prüfen. The selected server does not support a version of the Matrix protocol, that this client understands (v1.1 to v1.5). You can't sign in. - Der ausgewählte Server unterstützt keine Version des Matrixprotokolls, die Nheko versteht (v1.1 bis v1.5). Du kannst dich nicht anmelden. + Der ausgewählte Server unterstützt keine der Matrix versionen, die Nheko versteht (v1.1 bis v1.5). Du kannst dich nicht anmelden. @@ -1610,7 +1605,7 @@ Beispiel: https://mein.server:8787 SSO LOGIN - SSO-ANMELDUNG + SSO ANMELDUNG @@ -1620,7 +1615,7 @@ Beispiel: https://mein.server:8787 SSO login failed - SSO-Anmeldung fehlgeschlagen + SSO Anmeldung fehlgeschlagen @@ -1670,7 +1665,7 @@ Beispiel: https://mein.server:8787 %2 changed the room name to: %1 - %2 hat den Raumnamen geändert zu: %1 + %2 hat den Raumnamen geändert auf: %1 @@ -1680,7 +1675,7 @@ Beispiel: https://mein.server:8787 %2 changed the topic to: %1 - %2 hat das Thema geändert zu: %1 + %2 hat das Thema geändert auf: %1 @@ -1690,7 +1685,7 @@ Beispiel: https://mein.server:8787 %1 changed the room avatar - %1 hat das Raumavatar geändert + %1 hat dem Raumavatar geändert @@ -1784,7 +1779,7 @@ Beispiel: https://mein.server:8787 Write a message... - Schreibe eine Nachricht … + Schreibe eine Nachricht… @@ -1817,7 +1812,7 @@ Beispiel: https://mein.server:8787 React - Reagieren + Reaktion senden @@ -1847,7 +1842,7 @@ Beispiel: https://mein.server:8787 Enter reason for removal or hit enter for no reason: - Grund für die Löschung eingeben oder Eingabetaste drücken für keinen Grund: + Grund für das Nachrichtenlöschen oder Entertaste drücken für keinen Grund: @@ -1977,12 +1972,12 @@ Beispiel: https://mein.server:8787 %1 using the device %2 has requested to be verified. - %1 mit dem Gerät %2 hat angefragt, verifiziert zu werden. + %1 mit dem Gerät %2 hat angefragt verifiziert zu werden. Your device (%1) has requested to be verified. - Dein Gerät (%1) hat angefragt, verifiziert zu werden. + Dein Gerät %1 hat angefragt verifiziert zu werden. @@ -2157,7 +2152,7 @@ Beispiel: https://mein.server:8787 Move users up or down to change their permissions - Verschiebe Nutzer hoch oder runter, um ihre Berechtigungen zu ändern + Verschiebe Nutzer zwischen Rollen um deren Rolle zu ändern @@ -2195,7 +2190,7 @@ Beispiel: https://mein.server:8787 No permissions to apply the new permissions here - Keine Berechtigung, die Berechtigungen hier zu verändern + Keine Berechtigung die Berechtigungen hier zu verändern @@ -2218,7 +2213,7 @@ Beispiel: https://mein.server:8787 Failed to update powerlevel: %1 - Konnte Powerlevel nicht aktualisieren: %1 + Konnte Berechtigungen nicht aktualisieren: %1 @@ -2226,7 +2221,7 @@ Beispiel: https://mein.server:8787 Failed to update powerlevel: %1 - Konnte Powerlevel nicht aktualisieren: %1 + Konnte Berechtigungen nicht aktualisieren: %1 @@ -2259,7 +2254,7 @@ Beispiel: https://mein.server:8787 Redact events sent by others - Von anderen Teilnehmern gesendete Events zurückziehen + Fremde Nachrichten löschen @@ -2269,7 +2264,7 @@ Beispiel: https://mein.server:8787 Deprecated aliases events - Veraltete Aliasse-Events + Veraltetes Raumaddressenevent @@ -2339,12 +2334,12 @@ Beispiel: https://mein.server:8787 Redact own events - Eigene Events zurückziehen + Eigene Nachrichten löschen Change the pinned events - Angeheftete Events ändern + Angeheftete Nachrichten ändern @@ -2478,7 +2473,7 @@ Beispiel: https://mein.server:8787 Write a message... - Schreibe eine Nachricht … + Schreibe eine Nachricht… @@ -2573,7 +2568,7 @@ Beispiel: https://mein.server:8787 Autodiscovery failed. Unknown error when requesting .well-known. - Automatische Erkennung fehlgeschlagen. Unbekannter Fehler bei Anfrage nach „.well-known“. + Automatische Erkennung fehlgeschlagen. Unbekannter Fehler bei Anfrage .well-known. @@ -2583,12 +2578,12 @@ Beispiel: https://mein.server:8787 Received malformed response. Make sure the homeserver domain is valid. - Erhaltene Antwort war fehlerhaft. Bitte prüfe, ob die Heimserver-Domain gültig ist. + 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 prüfe, ob die Heimserver-Domain gültig ist. + Ein unbekannter Fehler ist aufgetreten. Bitte Homeserverdomain prüfen. @@ -2670,12 +2665,12 @@ Beispiel: https://mein.server:8787 New tag - Neues Tag + Neuer Tag Enter the tag you want to use: - Gib das Tag, das du verwenden willst, ein: + Gib den Tag, den du verwenden willst, ein: @@ -2720,12 +2715,12 @@ Beispiel: https://mein.server:8787 Create new tag... - Neues Tag erstellen … + Neuen Tag erstellen… Add or remove from community... - Zu Gruppe hinzufügen oder entfernen ... + Zu Gruppe hinzufügen oder entfernen... @@ -2840,7 +2835,7 @@ Beispiel: https://mein.server:8787 Search... - Suchen ... + Suchen... @@ -2860,7 +2855,7 @@ Beispiel: https://mein.server:8787 Power level - Powerlevel + Berechtigung @@ -2993,7 +2988,7 @@ Bitte beachte, dass die Verschlüsselung hinterher nicht mehr deaktiviert werden View and change the addresses/aliases of this room - Addressen/Aliasse dieses Raums betrachen und ändern + Raumadressen anzeigen und ändern @@ -3046,7 +3041,7 @@ Bitte beachte, dass die Verschlüsselung hinterher nicht mehr deaktiviert werden Allow guests to join - Erlaube Gästen, beizutreten + Erlaube Gästen beizutreten @@ -3056,12 +3051,12 @@ Bitte beachte, dass die Verschlüsselung hinterher nicht mehr deaktiviert werden Aliases - Aliasse + Raumaddressen Change what packs are enabled, remove packs, or create new ones - Ändere, welche Pakete aktiv sind, entferne oder erstelle neue Pakete + Ändere welche Packungen aktiv sind, entferne oder erstelle neue Packungen. @@ -3153,22 +3148,22 @@ Bitte beachte, dass die Verschlüsselung hinterher nicht mehr deaktiviert werden Please enter your login password to continue: - Bitte gib dein Anmeldepasswort ein, um fortzufahren: + Bitte gib dein Anmeldepassword an um fortzufahren: Please enter a valid email address to continue: - Bitte gib eine gültige E-Mail-Adresse an, um fortzufahren: + Bitte gib eine gültige Emailadresse an um fortzufahren: Please enter a valid phone number to continue: - Bitte gib eine gültige Telefonnummer an, um fortzufahren: + Bitte gib eine gültige Telefonnummer an um fortzufahren: Please enter the token which has been sent to you: - Bitte gib das Token ein, das dir geschickt wurde: + Bitte gib das Token ein, dass dir geschickt wurde: @@ -3240,7 +3235,7 @@ Bitte beachte, dass die Verschlüsselung hinterher nicht mehr deaktiviert werden 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 konnte sich nicht mit dem Dienst zum sicheren Speichern von Verschlüsselungsgeheimnissen verbinden. Das kann verschiedene Gründe haben. Prüfe, ob der D-Bus-Dienst aktiv ist und du einen Dienst wie KWallet, Gnome Keyring, KeePassXC oder das Äquivalent für deine Plattform konfiguriert hast. Wenn du Probleme hast, kannst du gerne einen Issue hier eröffnen (auf Englisch): https://github.com/Nheko-Reborn/nheko/issues + Nheko konnte sich nicht mit dem Dienst zum sicheren speichern von Schlüsseln verbinden. Das kann verschiedene Gründe haben. Prüfe, ob der D-Bus-Dienst aktiv ist und du einen Dienst wie KWallet, Gnome Keyring, KeePassXC oder das Äquivalent für deine Platform. Wenn du Probleme hast, scheue dich nicht Hilfe hier zu suchen: https://github.com/Nheko-Reborn/nheko/issues @@ -3248,7 +3243,7 @@ Bitte beachte, dass die Verschlüsselung hinterher nicht mehr deaktiviert werden 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! - Dies ist dein Wiederherstellungsschlüssel. Du brauchst ihn, um auf deine verschüsselten Nachrichten und Verifizierungsschlüssel zugreifen zu können. Pass gut auf ihn auf. Teile den Schlüssel mit niemandem und verliere ihn nicht! Gehe nicht über Los! Ziehe keine 2000€ ein! + Dies ist dein Wiederherstellungsschlüssel. Du brauchst diesen um auf deine verschüsselten Nachrichten und Verifizierungsschlüssel zugreifen zu können. Pass gut drauf auf. Teile den Schlüssel mit niemandem und verliere ihn nicht! Gehe nicht über Los! Ziehe nicht 2000€ ein! @@ -3270,7 +3265,7 @@ Bitte beachte, dass die Verschlüsselung hinterher nicht mehr deaktiviert werden 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! Hallo und willkommen zu Matrix! -Sieht so aus, als wärst du neu hier. Bevor du deine Nachrichten sicher verschlüsseln kannst, müssen wir ein paar Sachen konfigurieren. Du kannst entweder sofort auf „Akzeptieren“ drücken, oder ein paar grundlegende Einstellungen anpassen. Wir versuchen, einige der Grundlagen zu erklären. Du kannst diese Teile überspringen, aber sie könnten hilfreich sein! +Sieht so aus als wärst du neu hier. Bevor wir deine Nachrichten verschlüsseln können, müssen wir ein paar Sachen konfigurieren. Keine Panik, du kannst auch einfach weiter klicken, ohne irgendentwas umzustellen, aber du kannst natürlich auch ein paar der Optionen ändern. Die Erklärungen sind etwas länger in der Hoffnung, dass sie weiterhelfen. Du kannst sie überspringen, aber einmal durchlesen ist vielleicht vorteilhaft! @@ -3281,7 +3276,7 @@ Sieht so aus, als wärst du neu hier. Bevor du deine Nachrichten sicher verschl 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. - Es sieht so aus, als hättest du die Verschlüsselung für dieses Konto schon aktiviert. Damit dieses Gerät auf verschlüsselte Nachrichten zugreifen kann und vertrauenswürdig erscheint, kannst du es jetzt entweder mit einem anderen Gerät verifizieren oder (wenn du einen hast) mit deinem Wiederherstellungsschlüssel bestätigen. Bitte wähle eine der folgenden Optionen. + Es sieht so aus als hättest du die Verschlüsselung für dieses Konto schon aktiviert. Damit dieses Gerät auf verschlüsselte Nachrichten zugreifen kann und vertrauenswürdig erschein, kannst du es jetzt entweder mit einem anderen Gerät verifizieren oder (wenn du einen hast) mit deinem Wiederherstellungsschlüssel bestätigen. Bitte wähle eine der folgenden Optionen. Wenn du verifizieren wählst, musst du dein anderes Gerät zur Hand haben. Wenn du den Wiederherstellungsschlüssel wählst, brauchst du deine Wiederherstellungsphrase oder -passwort. Mit Abbrechen kannst du diesen Schritt auf später verschieben. @@ -3310,7 +3305,7 @@ Wenn du verifizieren wählst, musst du dein anderes Gerät zur Hand haben. Wenn Failed to create keys for secure server side secret storage! - Konnte Schlüssel für den sicheren serverseitigen Speicher nicht erzeugen! + Konnte Schlüssel für den sicheren, server-seitigen Speicher nicht erzeugen! @@ -3325,7 +3320,7 @@ Wenn du verifizieren wählst, musst du dein anderes Gerät zur Hand haben. Wenn Identity key changed. This breaks E2EE, so logging out. - Der Identitätschlüssel hat sich geändert. Das macht die Ende-zu-Ende-Verschlüsselung kaputt, also wirst du abgemeldet. + Die Identitätschlüssel haben sich geändert. Das stört die Verschlüsselung, deswegen wirst du abgemeldet. @@ -3334,12 +3329,12 @@ Wenn du verifizieren wählst, musst du dein anderes Gerät zur Hand haben. Wenn Failed to update image pack: %1 - Konnte das Bilderpaket nicht aktualisieren: %1 + Konnte die Bilderpackung nicht aktualisieren: %1 Failed to delete old image pack: %1 - Konnte das alte Bilderpaket nicht löschen: %1 + Konnte die alte Bilderpackung nicht löschen: %1 @@ -3447,13 +3442,13 @@ Wenn du verifizieren wählst, musst du dein anderes Gerät zur Hand haben. Wenn Message redaction failed: %1 - Zurückziehen der Nachricht fehlgeschlagen: %1 + Nachricht zurückziehen fehlgeschlagen: %1 Failed to encrypt event, sending aborted! - Event konnte nicht verschlüsselt werden, das Versenden wurde abgebrochen! + Event konnte nicht verschlüsselt werden, senden wurde abgebrochen! @@ -3492,32 +3487,32 @@ Wenn du verifizieren wählst, musst du dein anderes Gerät zur Hand haben. Wenn %1 allowed to join this room by knocking. - %1 hat erlaubt, diesen Raum durch Anklopfen beizutreten. + %1 hat erlaubt Leuten diesen Raum durch Anklopfen beizutreten. %1 allowed members of the following rooms to automatically join this room: %2 - %1 hat Mitgliedern aus den folgenden Räumen erlaubt, diesem Raum automatisch beizutreten: %2 + %1 hat Mitgliedern aus folgenden Räumen erlaubt diesen Raum automatisch zu betreten: %2 %1 made the room open to guests. - %1 hat Gästen erlaubt, den Raum zu betreten. + %1 hat Gästen erlaubt den Raum zu betreten. %1 has closed the room to guest access. - %1 hat Gästen verboten, den Raum zu betreten. + %1 hat Gästen verboten den Raum zu betreten. %1 made the room history world readable. Events may be now read by non-joined people. - %1 hat den Raum lesbar für alle gemacht. Nutzer, die nicht Teilnehmer dieses Raums sind, können nun Events in diesem Raum lesen. + %1 hat den Raum lesbar für alle gemacht. Nutzer, die nicht Teilnehmer dieses Raums sind, können nun Nachrichten in diesem Raum lesen. %1 set the room history visible to members from this point on. - %1 hat die Raumhistorie von diesen Zeitpunkt an sichtbar für Mitglieder gemacht. + %1 hat eingestellt, dass nur Teilnehmer Nachrichten in diesem Raum lesen können (ab diesem Punkt). @@ -3538,61 +3533,61 @@ Wenn du verifizieren wählst, musst du dein anderes Gerät zur Hand haben. Wenn %1 has changed the room's kick powerlevel from %2 to %3. - %1 hat das nötige Powerlevel, um Leute aus dem Raum zu werfen, von %2 auf %3 geändert. + %1 hat die benötigte Berechtigungen um Leute aus dem Raum zu werfen von %2 auf %3 geändert. %n member(s) can now kick room members. - %n Mitglied kann nun Raummitglieder hinauswerfen. - %n Mitglieder können nun Raummitglieder hinauswerfen. + %n Teilnehmer kann nun Mitglieder aus dem Raum entfernen. + %n Teilnehmer können nun Mitglieder aus dem Raum entfernen. %1 can now kick room members. - %1 kann nun Raummitglieder aus den Raum werfen. + %1 kann nun andere Nutzer aus dem Raum werfen. %1 has changed the room's redact powerlevel from %2 to %3. - %1 hat das nötige Powerlevel, um Nachrichten in diesem Raum zurückzuziehen, von %2 auf %3 geändert. + %1 hat die nötigen Berechtigungen um Nachrichten in diesem Raum zu löschen von %2 auf %3 geändert. %n member(s) can now redact room messages. - %n Mitglied kann nun Raumnachrichten zurückziehen. - %n Mitglieder können nun Raumnachrichten zurückziehen. + %1 Teilnehmer kann nun Nachrichten löschen. + %1 Teilnehmer können nun Nachrichten löschen. %1 can now redact room messages. - %1 kann nun Nachrichten zurückziehen. + %1 kann nun Nachrichten löschen. %1 has changed the room's ban powerlevel from %2 to %3. - %1 hat das nötige Powerlevel, um Leute aus dem Raum zu verbannen, von %2 auf %3 geändert. + %1 hat die benötigten Berechtigungen um Leute aus dem Raum zu verbannen von %2 auf %3 geändert. %n member(s) can now ban room members. - %n Mitglied kann nun Raummitglieder verbannen. - %n Mitglieder können nun Raummitglieder verbannen. + %n Teilnehmer kann nun Leute aus dem Rau verbannen. + %n Teilnehmer können nun Leute aus dem Rau verbannen. %1 can now ban room members. - %1 kann nun Raummitglieder verbannen. + %1 kann nun Leute aus dem Raum verbannen. %1 has changed the room's state_default powerlevel from %2 to %3. - %1 hat das Powerlevel für die restlichen Zustandsevents (state_default) von %2 auf %3 geändert. + %1 hat das Berechtigungslevel für die restlichen Zustandsevents von %2 auf %3 geändert. @@ -3610,67 +3605,67 @@ Wenn du verifizieren wählst, musst du dein anderes Gerät zur Hand haben. Wenn %1 has changed the room's invite powerlevel from %2 to %3. - %1 hat das nötige Powerlevel, um Nutzer einzuladen, von %2 auf %3 geändert. + %1 hat die nötigen Berechtigungen um Nutzer einzuladen von %2 auf %3 geändert. %1 has changed the room's events_default powerlevel from %2 to %3. New users can now not send any events. - %1 hat das „events_default“-Powerlevel von %2 auf %3 geändert. Neue Nutzer können ab jetzt keine Events mehr senden. + %1 hat die nötigen Berechtigungen für beliebige Events von %2 auf %3 geändert. Neue Nutzer können jetzt keine Events mehr senden. %1 has changed the room's events_default powerlevel from %2 to %3. New users can now send events that are not otherwise restricted. - %1 hat das nötige „events_default“-Powerlevel von %2 auf %3 geändert. Neue Nutzer können jetzt Events senden, solange keine anderen Einschränkungen vorliegen. + %1 hat die nötigen Berechtigungen für beliebige Events von %2 auf %3 geändert. Neue Nutzer können jetzt Events senden, solange keine anderen Einschränkungen vorliegen. %1 has changed the room's events_default powerlevel from %2 to %3. - %1 hat das „events_default“-Powerlevel dieses Raumes von %2 auf %3 geändert. + %1 hat die nötigen Berechtigungen für beliebige Events von %2 auf %3 geändert. %1 has made %2 an administrator of this room. - %1 hat %2 zum Administrator dieses Raumes befördert. + %1 hat %2 zum Administrator befördert. %1 has made %2 a moderator of this room. - %1 hat %2 zu einem Moderator in dieses Raumes gemacht. + %1 hat %2 zu einem Moderator gemacht. %1 has downgraded %2 to moderator of this room. - %1 hat %2 zum Moderator dieses Raumes degradiert. + %1 hat %2 zum Moderator degradiert. %1 has changed the powerlevel of %2 from %3 to %4. - %1 hat das Powerlevel von %2 von %3 auf %4 geändert. + %1 hat die Berechtigungen von %2 von %3 auf %4 geändert. %1 allowed only administrators to send "%2". - %1 hat nur Administratoren erlaubt, „%2“ zu senden. + %1 hat nur Administratoren erlaubt "%2" zu senden. %1 allowed only moderators to send "%2". - %1 hat nur Moderatoren erlaubt, „%2“ zu senden. + %1 hat nur Moderatoren erlaubt "%2" zu senden. %1 allowed everyone to send "%2". - %1 hat allen erlaubt, „%2“ zu senden. + %1 hat allen erlaubt "%2" zu senden. %1 has changed the powerlevel of event type "%2" from the default to %3. - %1 hat das Powerlevel für den Eventtyp „%2“ vom Standardwert auf %3 geändert. + %1 hat die Berechtigungen für Events vom Typ "%2" vom Standard auf %3 geändert. %1 has changed the powerlevel of event type "%2" from %3 to %4. - %1 hat das nötige Powerlevel für Events vom Typ „%2“ von %3 auf %4 geändert. + %1 hat die nötigen Berechtigungen für Events vom Typ "%2" von %3 auf %4 geändert. @@ -3680,12 +3675,12 @@ Wenn du verifizieren wählst, musst du dein anderes Gerät zur Hand haben. Wenn %1 removed the following images from the pack %2:<br>%3 - %1 hat die folgenden Bilder vom Paket %2 entfernt:<br>%3 + %1 hat die folgenden Bilder vom der Bilderpackung %2 entfernt:<br>%3 %1 added the following images to the pack %2:<br>%3 - %1 hat die folgenden Bilder zum Paket %2 hinzugefügt:<br>%3 + %1 hat die folgenden Bilder zu der Bilderpackung %2 hinzugefügt:<br>%3 @@ -3695,32 +3690,32 @@ Wenn du verifizieren wählst, musst du dein anderes Gerät zur Hand haben. Wenn %1 disabled the rule to ban users matching %2. - %1 hat die Regel zum Verbannen von Nutzern, deren Name mit %2 zusammenpasst, deaktiviert. + %1 hat die Regel zum bannen von Nutzern die %2 entsprechen deaktiviert. %1 added a rule to ban users matching %2 for '%3'. - %1 hat eine Regel für das Verbannen von Nutzern, deren Name mit %2 zusammenpasst, hinzugefügt. Grund: „%3“. + %1 hat eine Regel für das Verbannen von Nutzern wegen '%3', die %2 entsprechen, hinzugefügt. %1 disabled the rule to ban rooms matching %2. - %1 hat die Regel zum Verbannen von Räumen, deren Name mit %2 zusammenpasst, deaktiviert. + %1 hat die Regel zum bannen von Räumen, die %2 entsprechen deaktiviert. %1 added a rule to ban rooms matching %2 for '%3'. - %1 hat eine Regel für das Verbannen von Räumen, deren Namem mit %2 zusammenpasst, hinzugefügt. Grund: „%3“. + %1 hat eine Regel für das Verbannen von Räumen wegen '%3', die %2 entsprechen, hinzugefügt. %1 disabled the rule to ban servers matching %2. - %1 hat die Regel zum Verbannen von Servern, deren Name mit %2 zusammenpasst, deaktiviert. + %1 hat die Regel zum bannen von Servern, die %2 entsprechen deaktiviert. %1 added a rule to ban servers matching %2 for '%3'. - %1 hat die Regel zum Verbannen von Servern, deren Name mit %2 zusammenpasst, hinzugefügt. Grund: „%3“. + %1 hat eine Regel für das Verbannen von Servern wegen '%3', die %2 entsprechen, hinzugefügt. @@ -3782,7 +3777,7 @@ Grund: %4 %1 joined via authorisation from %2's server. - %1 hat den Raum durch Autorisierung vom Server von %2 betreten. + %1 hat den Raum durch Authorisierung von %2s Server betreten. @@ -3802,7 +3797,7 @@ Grund: %4 %1 redacted their knock. - %1 hat das eigene Anklopfen zurückgezogen. + %1 hat das Anklopfen zurückgezogen. @@ -3833,7 +3828,7 @@ Grund: %4 %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 hat den Raum verlassen, nachdem der Raum bereits von dieser Person verlassen wurde! + %1 hat den Raum verlassen, obwohl er gar nicht mehr am Raum teilnahm! @@ -4205,7 +4200,7 @@ Grund: %4 Large Emoji in timeline - Großes Emoji in der Nachrichtenliste + Große Emoji in der Nachrichtenliste @@ -4225,7 +4220,7 @@ Grund: %4 Send messages as Markdown - Nachrichten als Markdown formatiert senden + Sende Nachrichten als Markdown formatiert @@ -4315,17 +4310,17 @@ Grund: %4 Display fancy effects such as confetti - Ausgefallene Chateffekte wie Konfetti anzeigen + Lustige Chateffekte wie Konfetti anzeigen Reduce or disable animations - Animationen reduzieren oder deaktivieren + Reduziere oder deaktiviere Animationen Privacy Screen - Sichtschutz + Blickschutz @@ -4380,7 +4375,7 @@ Grund: %4 Allow fallback call assist server - Erlaube, den Fallback-Assistenzserver zu verwenden + Erlaube den Fallbackassistenzserver zu verwenden @@ -4405,7 +4400,7 @@ Grund: %4 User ID - Benutzer-ID + Anmeldename @@ -4495,7 +4490,7 @@ Grund: %4 Online backup key - Online-Backupschlüssel + Online Backupschlüssel @@ -4525,16 +4520,16 @@ Grund: %4 Set the notification sound to play when a call invite arrives - Setze den zu spielenden Benachrichtigungston für eingehende Anrufseinladungen + Ändere den Klingelton für eingehende Anrufe 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) - Zeitbegrenzung (in Sekunden), bis der Bildschirm verschwommen angezeigt wird, nachdem das Fenster den Fokus verliert. -Bei 0 wird der Sichtschutz sofort aktiv. -Maximaler Wert ist eine Stunde (3600 Sekunden) + Zeitbegrenzung (in Sekunden) bis der Bildschirm verschwommen angezeigt wird, nachdem das Fenster den Fokus verliert. +Bei 0 wird der Blickschutz sofort aktiv. +Maximaler Wert ist eine Stunde (3600 Sekunden). @@ -4544,7 +4539,7 @@ Maximaler Wert ist eine Stunde (3600 Sekunden) Make font size larger if messages with only a few emojis are displayed. - Erhöht die Schriftgröße, wenn die Nachricht nur aus ein paar Emojis besteht. + Erhöht die Schriftgröße, wenn die Nachricht nur aus ein paar Emoji besteht. @@ -4554,7 +4549,7 @@ Maximaler Wert ist eine Stunde (3600 Sekunden) Start the application in the background without showing the client window. - Starte die Applikation im Hintergrund, ohne ein Fenster zu öffnen. + Starte die Applikation im Hintergrund ohne ein Fenster zu öffnen. @@ -4565,13 +4560,13 @@ Maximaler Wert ist eine Stunde (3600 Sekunden) Allow using markdown in messages. When disabled, all messages are sent as a plain text. - Die Verwendung von Markdown in Nachrichten erlauben. -Wenn deaktiviert, werden alle Nachrichten als unformatierter Text gesendet. + Nutze Markdown als Format für Nachrichten. +Wenn deaktiviert werden alle Nachrichten als unformatierter Text gesendet. Invert the behavior of the enter key in the text input, making it send the message when shift+enter is pressed and starting a new line when enter is pressed. - Kehrt die Bedeutung der Eingabetaste im Texteingabefeld um, wodurch die Nachricht mit Umschalt+Eingabe gesendet wird und mit der Eingabetaste allein wird eine neue Zeile begonnen. + Erlaubt das invertieren des Verhaltens der Enter taste. Kontrolliert ob Umschalt+Enter eine Neuzeile einfügt oder die Nachricht sendet. @@ -4581,7 +4576,7 @@ Wenn deaktiviert, werden alle Nachrichten als unformatierter Text gesendet. Avatars are resized to fit above the message. - Die Größe von Avataren wird angepasst, damit sie oberhalb der Nachricht passen. + Benutzerbilder werden verkleinert um über die Nachricht zu passen. @@ -4592,55 +4587,55 @@ Wenn deaktiviert, werden alle Nachrichten als unformatierter Text gesendet. Show who is typing in a room. This will also enable or disable sending typing notifications to others. - Anzeigen, wer gerade in einem Raum tippt. -Diese Einstellung legt auch fest, ob das eigene Tippen an andere gesendet wird. + Zeige wer gerade in einem Raum tippt. +Diese Einstellung steuert auch, ob das eigene Tippen an andere gesendet wird. Show buttons to quickly reply, react or access additional options next to each message. - Zeige Buttons für das schnelle Antworten, Reagieren und zusätzliche Optionen neben jeder Nachricht. + Zeige Knöpfe für das schnelle Antworten, Reagieren und zusätzliche Optionen neben jeder Nachricht. Notify about received messages when the client is not currently focused. - Benachrichtigungen über neue Nachrichten, wenn der Client nicht im Vordergrund ist. + Benachrichtigungen pber neue Nachrichten, wenn der Client nicht im Vordergrund ist. Change the appearance of user avatars in chats. OFF - square, ON - circle. - Ändere das Aussehen der Chat-Avatare. -AUS: Quadratisch; AN: Kreisförmig. + Ändere das aussehen der Chatavatare. +AUS - Quadratisch, AN - Kreisförmig. Decrypt messages shown in notifications for encrypted chats. - Nachrichten, die in den Benachrichtigungen für verschlüsselte Chats angezeigt werden, entschlüsseln. + Nachrichten in Benachrichtigungen entschlüsseln in verschlüsselten Räumen. Choose where to show the total number of notifications contained within a community or tag. - Wähl aus, wo die Gesamtzahl der Benachrichtigungen innerhalb einer Gemeinschaft oder eines Tags gezeigt werden soll. + Wähle aus ob die Benachrichtigungsanzahl für Gruppen und Tags angezeigt werden oder nicht. Some messages can be sent with fancy effects. For example, messages sent with '/confetti' will show confetti on screen. - Manche Nachrichten können mit ausgefallenen Effekten versendet werden. Zum Beispiel werden Nachrichten, die mit „/confetti“ gesendet wurden, Konfetti auf den Bildschirm zeigen. + Manche Nachrichten lösen extra Effekte aus. Z.B. erzeugen Nachrichten, die mit /confetti gesendet wurden, einen kleinen Konfettischauer. Nheko uses animations in several places to make stuff pretty. This allows you to turn those off if they make you feel unwell. - Für besseres Aussehen verwendet Nheko an verschiedenen Stellen Animationen. Diese Option erlaubt dir, die Animationen zu deaktiveren, wenn diese bei dir Unwohlsein hervorrufen. + Für besseres Aussehen verwendet Nheko an verschiedenen Stellen Animationen. Diese Option erlaubt dir die Animationen zu deaktiveren, wenn diese bei dir Unwohlsein hervorrufen. Automatically replies to key requests from other users if they are verified, even if that device shouldn't have access to those keys otherwise. - Antwortet automatisch auf Schlüsselanfragen von anderen Benutzern, wenn diese verifiziert sind, selbst, wenn das entsprechende Gerät sonst keinen Zugriff auf diese Schlüssel hätte. + Teilt automatisch Schlüssel für Nachrichten mit verifizierten Nutzern (auf Anfrage), selbst wenn diese sonst keinen Zugriff darauf hätten. 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. - Der Schlüssel, um deine eigenen Geräte zu verifizieren. Wenn dieser im Cache ist, dann wird die Verifizierung eines deiner Geräte es als verifiziert für all deine anderen Geräte markieren und für Benutzer, die dich verfifiziert haben. + Der Schlüssel um deine eigenen Geräte zu verifizieren. Wenn dieser im Cache ist, dann werden alle deine Geräte als verifiziert für andere Nutzer erscheinen, wenn du diese verifiziert hast. @@ -4662,7 +4657,7 @@ Normalerweise animiert das den Taskbaricon oder färbt das Fenster orange ein. Set the max width of messages in the timeline (in pixels). This can help readability on wide screen when Nheko is maximized - Setze eine maximale Breite für Nachrichten im Chat (in Pixeln). Das kann die Lesbarkeit auf breiten Bildschirmen, wenn Nheko maximiert ist, erhöhen + Setze eine maximale Breite für Nachrichten im Chat (in Pixeln). Das kann die Lesbarkeit auf breiten Bildschirmen erhöhen. @@ -4723,7 +4718,7 @@ den Fokus verliert. Will prevent text selection in the timeline to make touch scrolling easier. - Wird die Textauswahl in der Nachrichtenliste verhindern, um das Scrolling mit Touchscreens zu vereinfachen. + Deaktiviert Textselektion in Nachrichten, damit das nicht beim Scrollen mit den Fingern stört. @@ -4748,12 +4743,12 @@ den Fokus verliert. 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. - Der Schlüssel, um Online-Schlüssel-Backups zu entschlüsseln. Wenn er im Cache liegt, kannst du das Online-Schlüssel-Backup aktivieren, um Verschlüsselungsschlüssel sicher verschlüsselt auf dem Server abzuspeichern. + Der Schlüssel um Schlüssel aus der Onlinesicherung zu laden. Wenn dieser vorhanden ist, können die Schlüssel für verschlüsselte Nachrichten sicher online gespeichert und wieder runtergeladen werden. The key to verify other users. If it is cached, verifying a user will verify all their devices. - Der Schlüssel, um andere Nutzer zu verifizieren. Wenn dieser lokal zwischengespeichert ist, dann wird die Verifikation eines Benutzers all seine Geräte verifizieren. + Der Schlüssel um andere Nutzer zu verifizieren. Wenn der lokal zwischengespeichert ist, dann werden durch eine Nutzerverifizierung alle Geräte verifiziert. @@ -4765,9 +4760,9 @@ den Fokus verliert. Allow third-party plugins and applications to load information about rooms you are in via D-Bus. This can have useful applications, but it also could be used for nefarious purposes. Enable at your own risk. This setting will take effect upon restart. - Erlaubt anderen Anwendungen und Plugins, Informationen über die Räume, in denen du dich befindest, mittels der D-Bus-Schnittstelle zu laden. Dies kann nützlich sein, aber auch missbraucht werden. Aktivieren auf eigene Gefahr. + Erlaubt anderen Anwendungen informationen über deine Matrixräume durch die D-Bus Schnittstelle zu laden. Dies kann nützlich sein, aber auch missbraucht werden. Aktivieren auf eigene Gefahr. -Diese Einstellung wird nach einem Neustart aktiv. +Diese Einstellung benötigt einen Neustart von Nheko. @@ -4798,7 +4793,7 @@ Diese Einstellung wird nach einem Neustart aktiv. File Password - Dateipasswort + Password für Datei @@ -4865,7 +4860,7 @@ Diese Einstellung wird nach einem Neustart aktiv. No encrypted private chat found with this user. Create an encrypted private chat with this user and try again. - Keinen verschlüsselten Privatchat mit diesem User gefunden. Erstelle einen verschlüsselten Privatchat mit diesem Nutzer und versuche es erneut. + Keinen verschlüsselten Chat mit diesem User gefunden. Erstelle einen verschlüsselten 1:1 Chat mit diesem Nutzer und versuche es erneut. @@ -4873,7 +4868,7 @@ Diese Einstellung wird nach einem Neustart aktiv. Waiting for other party… - Auf Gegenseite warten … + Auf Gegenseite warten… @@ -4926,7 +4921,7 @@ Diese Einstellung wird nach einem Neustart aktiv. Nheko uses animations in several places to make stuff pretty. This allows you to turn those off if they make you feel unwell. - Für besseres Aussehen verwendet Nheko an verschiedenen Stellen Animationen. Diese Option erlaubt dir, die Animationen zu deaktiveren, wenn diese bei dir Unwohlsein hervorrufen. + Für besseres Aussehen verwendet Nheko an verschiedenen Stellen Animationen. Diese Option erlaubt dir die Animationen zu deaktiveren, wenn diese bei dir Unwohlsein hervorrufen. @@ -4980,7 +4975,7 @@ Diese Einstellung wird nach einem Neustart aktiv. Solve the reCAPTCHA and press the confirm button - Löse das reCAPTCHA und drücke den Bestätigungsknopf + Löse das reCAPTCHA und drücke den "Bestätigen"-Knopf diff --git a/resources/langs/nheko_el.ts b/resources/langs/nheko_el.ts index 07c89a3e..b3e33751 100644 --- a/resources/langs/nheko_el.ts +++ b/resources/langs/nheko_el.ts @@ -1390,11 +1390,6 @@ You may optionally provide a reason for others to accept your knock: InputBar - - - Select a file - Διάλεξε ένα αρχείο - All Files (*) diff --git a/resources/langs/nheko_eo.ts b/resources/langs/nheko_eo.ts index 704da4a2..c87e169a 100644 --- a/resources/langs/nheko_eo.ts +++ b/resources/langs/nheko_eo.ts @@ -1394,11 +1394,6 @@ Vi povas aldoni noton, pri kial oni akceptu vian frapadon: InputBar - - - Select a file - Elektu dosieron - All Files (*) diff --git a/resources/langs/nheko_es.ts b/resources/langs/nheko_es.ts index 1f88c958..d5585db7 100644 --- a/resources/langs/nheko_es.ts +++ b/resources/langs/nheko_es.ts @@ -1392,11 +1392,6 @@ You may optionally provide a reason for others to accept your knock: InputBar - - - Select a file - Seleccionar un archivo - All Files (*) diff --git a/resources/langs/nheko_et.ts b/resources/langs/nheko_et.ts index 2221d7ed..380c3161 100644 --- a/resources/langs/nheko_et.ts +++ b/resources/langs/nheko_et.ts @@ -1393,11 +1393,6 @@ Kui soovid, siis võid lisada ka selgituse, miks peaks sinu koputusele reageerim InputBar - - - Select a file - Vali fail - All Files (*) @@ -1408,6 +1403,11 @@ Kui soovid, siis võid lisada ka selgituse, miks peaks sinu koputusele reageerim Upload of '%1' failed „%1“ üleslaadimine ei õnnestunud + + + Select file(s) + Vali fail(id) + InviteDialog diff --git a/resources/langs/nheko_fi.ts b/resources/langs/nheko_fi.ts index 1213d13c..08c88899 100644 --- a/resources/langs/nheko_fi.ts +++ b/resources/langs/nheko_fi.ts @@ -1393,11 +1393,6 @@ Voit antaa valinnaisen syyn muiden hyväksyäkseen koputuksesi: InputBar - - - Select a file - Valitse tiedosto - All Files (*) diff --git a/resources/langs/nheko_fr.ts b/resources/langs/nheko_fr.ts index 2d71d13b..69820a0a 100644 --- a/resources/langs/nheko_fr.ts +++ b/resources/langs/nheko_fr.ts @@ -96,7 +96,7 @@ Add - Ajouter + Ajouter @@ -229,7 +229,7 @@ Confirm logout - + Confirmer la déconnexion @@ -239,7 +239,7 @@ Failed to open database, logging out! - Impossible d'ouvrir la base de données, déconnexion ! + Impossible d'ouvrir la base de données, déconnexion ! @@ -249,7 +249,7 @@ Do you really want to knock on %1? You may optionally provide a reason for others to accept your knock: - Voulez-vous vraiment toquer à %1 ? Vous pouvez fournir une raison aux autres de l'accepter : + Voulez-vous vraiment frapper à %1 ? Vous pouvez donner une raison aux membres actuels de vous accepter : @@ -275,7 +275,7 @@ Do you really want to invite %1 (%2)? - Voulez-vous vraiment inviter %1 (%2) ? + Voulez-vous vraiment inviter %1 (%2) ? @@ -305,7 +305,7 @@ Do you really want to unban %1 (%2)? - Voulez-vous vraiment annuler le bannissement de %1 (%2) ? + Voulez-vous vraiment annuler le bannissement de %1 (%2) ? @@ -325,19 +325,21 @@ Cache migration failed! - Échec de la migration du cache ! + Échec de la migration du cache ! Because of the following reason Nheko wants to drop you to the login page: %1 If you think this is a mistake, you can close Nheko instead to possibly recover your encryption keys. After you have been dropped to the login page, you can sign in again using your usual methods. - + Nheko veut vous renvoyer à la page de connexion pour cette raison : +%1 +Si vous pensez qu'il s'agit d'une erreur, vous pouvez plutôt fermer Nheko pour essayer de récupérer vos clés de chiffrement. De retour à la page de connexion, vous pourrez vous reconnecter par vos méthodes habituelles. Migrating the cache to the current version failed. This can have different reasons. Please open an issue at https://github.com/Nheko-Reborn/nheko and try to use an older version in the meantime. Alternatively you can try deleting the cache manually. - + La migration du cache vers la version actuelle a échoué. Plusieurs causes sont possibles. Merci d'ouvrir un rapport d'anomalie sur https://github.com/Nheko-Reborn/nheko et essayez d'utiliser une version antérieure entretemps. Vous pouvez également tenter d'effacer le cache manuellement. @@ -371,7 +373,8 @@ If you think this is a mistake, you can close Nheko instead to possibly recover You failed to join %1. You can try to knock so that others can invite you in. Do you want to do so? You may optionally provide a reason for others to accept your knock: - + Vous n'avez pas pu rejoindre %1. Vous pouvez essayer de frapper au salon afin que les autres membres vous invitent. Voulez-vous le faire ? +Vous pouvez éventuellement fournir une raison afin que les membres acceptent votre requête : @@ -381,7 +384,7 @@ You may optionally provide a reason for others to accept your knock: Failed to remove invite: %1 - Impossible de supprimer l'invitation : %1 + Impossible de supprimer l'invitation : %1 @@ -406,7 +409,7 @@ You may optionally provide a reason for others to accept your knock: Failed to kick %1 from %2: %3 - Échec de l'expulsion de %1 de %2  : %3 + Échec de l'expulsion de %1 de %2  : %3 @@ -429,27 +432,27 @@ You may optionally provide a reason for others to accept your knock: /me <message> - + /me <message> /react <text> - + /react <texte> /part [reason] - + /part [raison] /leave [reason] - + /leave [raison] /roomnick <displayname> - + /roomnick <nomaffiché> @@ -565,57 +568,57 @@ You may optionally provide a reason for others to accept your knock: Leave a room. Reason is optional. - + Quitte un salon. La raison est optionnelle. Invite a user into the current room. Reason is optional. - + Invite un utilisateur dans le salon actuel. La raison est optionnelle. Kick a user from the current room. Reason is optional. - + Expulse un utilisateur du salon actuel. La raison est optionnelle. Ban a user from the current room. Reason is optional. - + Bannit un utilisateur du salon actuel. La raison est optionnelle. Unban a user in the current room. Reason is optional. - + Dé-bannit un utilisateur du salon actuel. La raison est optionnelle. Redact an event or all locally cached messages of a user. - + Efface un évènement ou tous les messages connus (présents dans la base de données locale) d'un utilisateur. Change your displayname in this room. - + Change votre nom affiché dans ce salon. ¯\_(ツ)_/¯ with an optional message. - + ¯\_(ツ)_/¯ avec un message optionnel. (╯°□°)╯︵ ┻━┻ - + (╯°□°)╯︵ ┻━┻ ┯━┯╭( º _ º╭) - + ┯━┯╭( º _ º╭) ノ┬─┬ノ ︵ ( \o°o)\ - + ノ┬─┬ノ ︵ ( \o°o)\ @@ -650,32 +653,32 @@ You may optionally provide a reason for others to accept your knock: Send a message in rainbow colors. - + Envoie un message aux couleurs de l'arc-en-ciel. Send /me in rainbow colors. - + Envoie /me aux couleurs de l'arc-en-ciel. Send a bot message. - + Envoie un message de robot. Send a bot message in rainbow colors. - + Envoie un message de robot aux couleurs de l'arc-en-ciel. Send a message with confetti. - + Envoie un message avec des confettis. Send a message in rainbow colors with confetti. - + Envoie des confettis avec un message aux couleurs de l'arc-en-ciel. @@ -698,12 +701,12 @@ You may optionally provide a reason for others to accept your knock: Do not show notification counts for this community or tag. - + Ne pas afficher le compteur de notifications pour cette communauté ou cette étiquette. Hide rooms with this tag or from this community by default. - + Cache par défaut les salons avec cette étiquette ou provenant de cette communauté. @@ -771,7 +774,7 @@ You may optionally provide a reason for others to accept your knock: Failed to update community: %1 - + Erreur lors de la mise à jour de cette communauté : %1 @@ -804,7 +807,7 @@ You may optionally provide a reason for others to accept your knock: %n member(s) - + %n membre %n membres @@ -964,17 +967,17 @@ You may optionally provide a reason for others to accept your knock: 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! - Veuillez vérifier les chiffres suivants. Vous devriez voir les mêmes chiffres des deux côtés. Si ceux-ci diffèrent, veuillez choisir « Ils sont différents ! » pour annuler la vérification ! + Veuillez vérifier les chiffres suivants. Vous devriez voir les mêmes chiffres des deux côtés. Si ceux-ci diffèrent, veuillez choisir « Ils sont différents ! » pour annuler la vérification ! They do not match! - Ils sont différents ! + Ils sont différents ! They match! - Ils sont identiques ! + Ils sont identiques ! @@ -1035,7 +1038,7 @@ You may optionally provide a reason for others to accept your knock: 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 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 ! + 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 ! @@ -1045,12 +1048,12 @@ You may optionally provide a reason for others to accept your knock: They do not match! - Ils sont différents ! + Ils sont différents ! They match! - Ils sont identiques ! + Ils sont identiques ! @@ -1114,7 +1117,7 @@ You may optionally provide a reason for others to accept your knock: This message is not encrypted! - Ce message n'est pas chiffré ! + Ce message n'est pas chiffré ! @@ -1147,7 +1150,7 @@ You may optionally provide a reason for others to accept your knock: Key mismatch detected! - Clés non correspondantes détectées ! + Clés non correspondantes détectées ! @@ -1162,7 +1165,7 @@ You may optionally provide a reason for others to accept your knock: Verification messages received out of order! - Messages de vérification reçus dans le désordre ! + Messages de vérification reçus dans le désordre ! @@ -1390,11 +1393,6 @@ You may optionally provide a reason for others to accept your knock: InputBar - - - Select a file - Sélectionnez un fichier - All Files (*) @@ -1543,7 +1541,7 @@ Example: https://server.my:8787 You have entered an invalid Matrix ID e.g @joe:matrix.org - Vous avez entré un identifiant Matrix invalide exemple correct : @moi:monserveur.example.com) + Vous avez entré un identifiant Matrix invalide exemple correct : @moi:monserveur.example.com) @@ -1626,12 +1624,12 @@ Example: https://server.my:8787 A call is in progress. Log out? - Un appel est en cours. Se déconnecter ? + Un appel est en cours. Se déconnecter ? Are you sure you want to log out? - Êtes-vous certain de vouloir vous déconnecter ? + Êtes-vous certain de vouloir vous déconnecter ? @@ -2041,7 +2039,7 @@ Example: https://server.my:8787 Place a call to %1? - Appeler %1 ? + Appeler %1 ? @@ -3174,17 +3172,17 @@ Veuillez noter qu'il ne pourra plus être désactivé par la suite. Share desktop with %1? - Partager le bureau avec %1  ? + Partager le bureau avec %1  ? Window: - Fenêtre : + Fenêtre : Frame rate: - Fréquence d'images : + Fréquence d'images : @@ -3233,7 +3231,7 @@ Veuillez noter qu'il ne pourra plus être désactivé par la suite. 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 n'a pas pu se connecter au stockage sécurisé afin d'y sauvegarder les clés de chiffrement. Cela peut avoir différentes causes. Vérifiez si votre service D-Bus est lancé, et si vous avez configuré un service tel que KWallet ; Gnome Keyring ; KeePassXC ou l'équivalent pour votre système. Si vous n'arrivez pas à résoudre le problème, n'hésitez pas à nous en faire part ici : https ://github.com/Nheko-Reborn/nheko/issues + Nheko n'a pas pu se connecter au stockage sécurisé afin d'y sauvegarder les clés de chiffrement. Cela peut avoir différentes causes. Vérifiez si votre service D-Bus est lancé, et si vous avez configuré un service tel que KWallet ; Gnome Keyring ; KeePassXC ou l'équivalent pour votre système. Si vous n'arrivez pas à résoudre le problème, n'hésitez pas à nous en faire part ici : https ://github.com/Nheko-Reborn/nheko/issues @@ -3241,7 +3239,7 @@ Veuillez noter qu'il ne pourra plus être désactivé par la suite. 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! - Ceci est votre clé de récupération. Vous en aurez besoin afin de restaurer l'accès à vos messages chiffrés et à vos clés de vérification. Gardez cette clé en sûreté. Ne la partagez pas avec qui que ce soit et ne la perdez pas ! Ne passez pas par la case départ et ne recevez pas 20 000 francs ! + Ceci est votre clé de récupération. Vous en aurez besoin afin de restaurer l'accès à vos messages chiffrés et à vos clés de vérification. Gardez cette clé en sûreté. Ne la partagez pas avec qui que ce soit et ne la perdez pas ! Ne passez pas par la case départ et ne recevez pas 20 000 francs ! @@ -3262,8 +3260,8 @@ Veuillez noter qu'il ne pourra plus être désactivé par la suite. 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! - Bonjour et bienvenue sur le réseau Matrix ! -Il semblerait que ce soit votre première fois ici. Avant de pouvoir chiffrer vos messages de manière sécurisée, nous devons configurer quelques détails. Vous pouvez soit accepter immédiatement, soit ajuster quelques options basiques. Nous essayons également d'expliquer le fonctionnement de certains mécanismes. Vous pouvez sauter ces étapes, mais celles-ci pourraient se montrer utiles par la suite ! + Bonjour et bienvenue sur le réseau Matrix ! +Il semblerait que ce soit votre première fois ici. Avant de pouvoir chiffrer vos messages de manière sécurisée, nous devons configurer quelques détails. Vous pouvez soit accepter immédiatement, soit ajuster quelques options basiques. Nous essayons également d'expliquer le fonctionnement de certains mécanismes. Vous pouvez sauter ces étapes, mais celles-ci pourraient se montrer utiles par la suite ! @@ -3293,17 +3291,17 @@ Si vous choisissez de vérifier, vous aurez besoin de l'autre appareil. Si Failed to create keys for cross-signing! - Échec de la création des clés pour l'auto-vérification (cross-signing) ! + Échec de la création des clés pour l'auto-vérification (cross-signing) ! Failed to create keys for online key backup! - Échec de la création de clés pour la sauvegarde en ligne ! + Échec de la création de clés pour la sauvegarde en ligne ! Failed to create keys for secure server side secret storage! - Échec de la création des clés pour le stockage sécurisé côté serveur ! + Échec de la création des clés pour le stockage sécurisé côté serveur ! @@ -3313,7 +3311,7 @@ Si vous choisissez de vérifier, vous aurez besoin de l'autre appareil. Si Encryption setup failed: %1 - Échec de la configuration du chiffrement : %1 + Échec de la configuration du chiffrement : %1 @@ -3426,7 +3424,7 @@ Si vous choisissez de vérifier, vous aurez besoin de l'autre appareil. Si 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 ! @@ -3446,7 +3444,7 @@ Si vous choisissez de vérifier, vous aurez besoin de l'autre appareil. Si Failed to encrypt event, sending aborted! - Échec du chiffrement de l'évènement, envoi abandonné ! + Échec du chiffrement de l'évènement, envoi abandonné ! @@ -3826,7 +3824,7 @@ Raison : %4 %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 a quitté le salon après l'avoir déjà quitté ! + %1 a quitté le salon après l'avoir déjà quitté ! @@ -4863,17 +4861,17 @@ This setting will take effect upon restart. Waiting for other side to accept the verification request. - Attente que le correspondant accepte la demande de vérification. + Attente d'acceptation de la demande de vérification par le correspondant. Waiting for other side to continue the verification process. - Attente que le correspondant poursuive le processus de vérification. + Attente de la poursuite du processus de vérification par le correspondant. Waiting for other side to complete the verification process. - Attente que le correspondant termine le processus de vérification. + Attente de la fin du processus de vérification par le correspondant. diff --git a/resources/langs/nheko_hu.ts b/resources/langs/nheko_hu.ts index d1a3f5e7..803dc2fe 100644 --- a/resources/langs/nheko_hu.ts +++ b/resources/langs/nheko_hu.ts @@ -1388,11 +1388,6 @@ You may optionally provide a reason for others to accept your knock: InputBar - - - Select a file - Fájl kiválasztása - All Files (*) diff --git a/resources/langs/nheko_id.ts b/resources/langs/nheko_id.ts index 2b6dcd2c..7af25e13 100644 --- a/resources/langs/nheko_id.ts +++ b/resources/langs/nheko_id.ts @@ -1391,11 +1391,6 @@ Kamu dapat memberikan alasan untuk orang lain untuk menerima ketukanmu: InputBar - - - Select a file - Pilih sebuah file - All Files (*) @@ -1406,6 +1401,11 @@ Kamu dapat memberikan alasan untuk orang lain untuk menerima ketukanmu:Upload of '%1' failed Pengunggahan '%1' gagal + + + Select file(s) + Pilih berkas + InviteDialog diff --git a/resources/langs/nheko_ie.ts b/resources/langs/nheko_ie.ts index c22fbb0e..4358c218 100644 --- a/resources/langs/nheko_ie.ts +++ b/resources/langs/nheko_ie.ts @@ -1390,11 +1390,6 @@ You may optionally provide a reason for others to accept your knock: InputBar - - - Select a file - - All Files (*) diff --git a/resources/langs/nheko_it.ts b/resources/langs/nheko_it.ts index d2144e3b..c6710f2a 100644 --- a/resources/langs/nheko_it.ts +++ b/resources/langs/nheko_it.ts @@ -1390,11 +1390,6 @@ You may optionally provide a reason for others to accept your knock: InputBar - - - Select a file - Seleziona un file - All Files (*) diff --git a/resources/langs/nheko_ja.ts b/resources/langs/nheko_ja.ts index 60f80848..55cf649f 100644 --- a/resources/langs/nheko_ja.ts +++ b/resources/langs/nheko_ja.ts @@ -1388,11 +1388,6 @@ You may optionally provide a reason for others to accept your knock: InputBar - - - Select a file - ファイルを選択 - All Files (*) diff --git a/resources/langs/nheko_ml.ts b/resources/langs/nheko_ml.ts index ece960ad..0a912b3a 100644 --- a/resources/langs/nheko_ml.ts +++ b/resources/langs/nheko_ml.ts @@ -1390,11 +1390,6 @@ You may optionally provide a reason for others to accept your knock: InputBar - - - Select a file - ഒരു ഫയൽ തിരഞ്ഞെടുക്കുക - All Files (*) diff --git a/resources/langs/nheko_nl.ts b/resources/langs/nheko_nl.ts index 8b78215a..49dadbb3 100644 --- a/resources/langs/nheko_nl.ts +++ b/resources/langs/nheko_nl.ts @@ -1393,11 +1393,6 @@ Je kan optioneel hier een reden invoeren dat je aanklopt: InputBar - - - Select a file - Selecteer een bestand - All Files (*) diff --git a/resources/langs/nheko_pl.ts b/resources/langs/nheko_pl.ts index cfd22a8f..435cfe24 100644 --- a/resources/langs/nheko_pl.ts +++ b/resources/langs/nheko_pl.ts @@ -1392,11 +1392,6 @@ You may optionally provide a reason for others to accept your knock: InputBar - - - Select a file - Wybierz plik - All Files (*) diff --git a/resources/langs/nheko_pt_BR.ts b/resources/langs/nheko_pt_BR.ts index 53a9b9f1..43ae3f0b 100644 --- a/resources/langs/nheko_pt_BR.ts +++ b/resources/langs/nheko_pt_BR.ts @@ -1390,11 +1390,6 @@ You may optionally provide a reason for others to accept your knock: InputBar - - - Select a file - - All Files (*) diff --git a/resources/langs/nheko_pt_PT.ts b/resources/langs/nheko_pt_PT.ts index 59221684..e1b86e2c 100644 --- a/resources/langs/nheko_pt_PT.ts +++ b/resources/langs/nheko_pt_PT.ts @@ -1390,11 +1390,6 @@ You may optionally provide a reason for others to accept your knock: InputBar - - - Select a file - Selecionar um ficheiro - All Files (*) diff --git a/resources/langs/nheko_ro.ts b/resources/langs/nheko_ro.ts index a90dfda7..59702740 100644 --- a/resources/langs/nheko_ro.ts +++ b/resources/langs/nheko_ro.ts @@ -1392,11 +1392,6 @@ You may optionally provide a reason for others to accept your knock: InputBar - - - Select a file - - All Files (*) diff --git a/resources/langs/nheko_ru.ts b/resources/langs/nheko_ru.ts index 27192dd3..444a6f38 100644 --- a/resources/langs/nheko_ru.ts +++ b/resources/langs/nheko_ru.ts @@ -1392,11 +1392,6 @@ You may optionally provide a reason for others to accept your knock: InputBar - - - Select a file - Выберите файл - All Files (*) diff --git a/resources/langs/nheko_si.ts b/resources/langs/nheko_si.ts index e4ba7628..2254bf24 100644 --- a/resources/langs/nheko_si.ts +++ b/resources/langs/nheko_si.ts @@ -1390,11 +1390,6 @@ You may optionally provide a reason for others to accept your knock: InputBar - - - Select a file - - All Files (*) diff --git a/resources/langs/nheko_sr_Latn.ts b/resources/langs/nheko_sr_Latn.ts index c8b54d06..c77ee762 100644 --- a/resources/langs/nheko_sr_Latn.ts +++ b/resources/langs/nheko_sr_Latn.ts @@ -1392,11 +1392,6 @@ You may optionally provide a reason for others to accept your knock: InputBar - - - Select a file - - All Files (*) diff --git a/resources/langs/nheko_sv.ts b/resources/langs/nheko_sv.ts index 9319efa4..ddb64aef 100644 --- a/resources/langs/nheko_sv.ts +++ b/resources/langs/nheko_sv.ts @@ -1390,11 +1390,6 @@ You may optionally provide a reason for others to accept your knock: InputBar - - - Select a file - Välj en fil - All Files (*) diff --git a/resources/langs/nheko_tr.ts b/resources/langs/nheko_tr.ts index 71fa01a2..573ff38c 100644 --- a/resources/langs/nheko_tr.ts +++ b/resources/langs/nheko_tr.ts @@ -1393,11 +1393,6 @@ You may optionally provide a reason for others to accept your knock: InputBar - - - Select a file - - All Files (*) diff --git a/resources/langs/nheko_uk.ts b/resources/langs/nheko_uk.ts index 99315c8a..72ff3953 100644 --- a/resources/langs/nheko_uk.ts +++ b/resources/langs/nheko_uk.ts @@ -1395,11 +1395,6 @@ You may optionally provide a reason for others to accept your knock: InputBar - - - Select a file - Виберіть файл - All Files (*) diff --git a/resources/langs/nheko_vi.ts b/resources/langs/nheko_vi.ts index 6d419b3b..f1525f92 100644 --- a/resources/langs/nheko_vi.ts +++ b/resources/langs/nheko_vi.ts @@ -1388,11 +1388,6 @@ You may optionally provide a reason for others to accept your knock: InputBar - - - Select a file - - All Files (*) diff --git a/resources/langs/nheko_zh_CN.ts b/resources/langs/nheko_zh_CN.ts index e6c3d4be..f392c51f 100644 --- a/resources/langs/nheko_zh_CN.ts +++ b/resources/langs/nheko_zh_CN.ts @@ -1391,11 +1391,6 @@ You may optionally provide a reason for others to accept your knock: InputBar - - - Select a file - 选择一个文件 - All Files (*) diff --git a/resources/qml/Avatar.qml b/resources/qml/Avatar.qml index 4951a9fb..a248114d 100644 --- a/resources/qml/Avatar.qml +++ b/resources/qml/Avatar.qml @@ -11,78 +11,67 @@ import im.nheko 1.0 AbstractButton { id: avatar + property alias color: bg.color + property bool crop: true + property string displayName + property string roomid + property alias textColor: label.color property string url property string userid - property string roomid - property string displayName - property alias textColor: label.color - property bool crop: true - property alias color: bg.color - width: 48 height: 48 + width: 48 + background: Rectangle { id: bg + + color: palette.alternateBase radius: Settings.avatarCircles ? height / 2 : height / 8 - color: Nheko.colors.alternateBase } Label { id: label - enabled: false - anchors.fill: parent - text: TimelineManager.escapeEmoji(displayName ? String.fromCodePoint(displayName.codePointAt(0)) : "") - textFormat: Text.RichText + color: palette.text + enabled: false font.pixelSize: avatar.height / 2 - verticalAlignment: Text.AlignVCenter horizontalAlignment: Text.AlignHCenter + text: TimelineManager.escapeEmoji(avatar.displayName ? String.fromCodePoint(avatar.displayName.codePointAt(0)) : "") + textFormat: Text.RichText + verticalAlignment: Text.AlignVCenter visible: img.status != Image.Ready && !Settings.useIdenticon - color: Nheko.colors.text } - Image { id: identicon anchors.fill: parent + source: Settings.useIdenticon ? ("image://jdenticon/" + (avatar.userid !== "" ? avatar.userid : avatar.roomid) + "?radius=" + (Settings.avatarCircles ? 100 : 25)) : "" visible: Settings.useIdenticon && img.status != Image.Ready - source: Settings.useIdenticon ? ("image://jdenticon/" + (userid !== "" ? userid : roomid) + "?radius=" + (Settings.avatarCircles ? 100 : 25)) : "" } - Image { id: img anchors.fill: parent asynchronous: true fillMode: avatar.crop ? Image.PreserveAspectCrop : Image.PreserveAspectFit - mipmap: true - smooth: true - sourceSize.width: avatar.width * Screen.devicePixelRatio - sourceSize.height: avatar.height * Screen.devicePixelRatio - source: if (avatar.url.startsWith('image://')) { + source: if (avatar.url.startsWith('image://colorimage')) { + return avatar.url + "&radius=" + (Settings.avatarCircles ? 100 : 25) + ((avatar.crop) ? "" : "&scale"); + } else if (avatar.url.startsWith('image://')) { return avatar.url + "?radius=" + (Settings.avatarCircles ? 100 : 25) + ((avatar.crop) ? "" : "&scale"); } else if (avatar.url.startsWith(':/')) { - return "image://colorimage/" + avatar.url + "?" + textColor; + return "image://colorimage/" + avatar.url + "?" + label.color; } else { return ""; } - + sourceSize.height: avatar.height * Screen.devicePixelRatio + sourceSize.width: avatar.width * Screen.devicePixelRatio } - Rectangle { id: onlineIndicator - anchors.bottom: avatar.bottom - anchors.right: avatar.right - visible: !!userid - height: avatar.height / 6 - width: height - radius: Settings.avatarCircles ? height / 2 : height / 8 - color: updatePresence() - function updatePresence() { - switch (Presence.userPresence(userid)) { + switch (Presence.userPresence(avatar.userid)) { case "online": return Nheko.theme.online; case "unavailable": @@ -94,22 +83,28 @@ AbstractButton { } } - Connections { - target: Presence + anchors.bottom: avatar.bottom + anchors.right: avatar.right + color: updatePresence() + height: avatar.height / 6 + radius: Settings.avatarCircles ? height / 2 : height / 8 + visible: !!avatar.userid + width: height + Connections { function onPresenceChanged(id) { - if (id == userid) onlineIndicator.color = onlineIndicator.updatePresence(); + if (id == avatar.userid) + onlineIndicator.color = onlineIndicator.updatePresence(); } + + target: Presence } } - - CursorShape { + NhekoCursorShape { anchors.fill: parent cursorShape: Qt.PointingHandCursor } - Ripple { - color: Qt.rgba(Nheko.colors.alternateBase.r, Nheko.colors.alternateBase.g, Nheko.colors.alternateBase.b, 0.5) + color: Qt.rgba(palette.alternateBase.r, palette.alternateBase.g, palette.alternateBase.b, 0.5) } - } diff --git a/resources/qml/ChatPage.qml b/resources/qml/ChatPage.qml index d4d2b845..2803e97d 100644 --- a/resources/qml/ChatPage.qml +++ b/resources/qml/ChatPage.qml @@ -14,19 +14,19 @@ import QtQml 2.15 Rectangle { id: chatPage - color: Nheko.colors.window + color: palette.window ColumnLayout { - spacing: 0 anchors.fill: parent + spacing: 0 Rectangle { id: offlineIndicator + Layout.fillWidth: true + Layout.preferredHeight: offlineLabel.height + Nheko.paddingMedium color: Nheko.theme.error visible: !TimelineManager.isConnected - Layout.preferredHeight: offlineLabel.height + Nheko.paddingMedium - Layout.fillWidth: true z: 1 Label { @@ -36,18 +36,9 @@ Rectangle { text: qsTr("No network connection") } } - AdaptiveLayout { id: adaptiveView - Layout.fillWidth: true - Layout.fillHeight: true - singlePageMode: communityListC.preferredWidth + roomListC.preferredWidth + timlineViewC.minimumWidth > width - pageIndex: 1 - - Component.onCompleted: initializePageIndex() - onSinglePageModeChanged: initializePageIndex() - function initializePageIndex() { if (!singlePageMode) adaptiveView.pageIndex = 0; @@ -57,67 +48,67 @@ Rectangle { adaptiveView.pageIndex = 1; } + Layout.fillHeight: true + Layout.fillWidth: true + pageIndex: 1 + singlePageMode: communityListC.preferredWidth + roomListC.preferredWidth + timlineViewC.minimumWidth > width + + Component.onCompleted: initializePageIndex() + onSinglePageModeChanged: initializePageIndex() + Connections { - target: Rooms function onCurrentRoomChanged() { adaptiveView.initializePageIndex(); } - } + target: Rooms + } AdaptiveLayoutElement { id: communityListC - visible: Settings.groupView - minimumWidth: communitiesList.avatarSize * 4 + Nheko.paddingMedium * 2 collapsedWidth: communitiesList.avatarSize + 2 * Nheko.paddingMedium - preferredWidth: Settings.communityListWidth >= minimumWidth ? Settings.communityListWidth : collapsedWidth maximumWidth: communitiesList.avatarSize * 10 + 2 * Nheko.paddingMedium + minimumWidth: communitiesList.avatarSize * 4 + Nheko.paddingMedium * 2 + preferredWidth: Settings.communityListWidth >= minimumWidth ? Settings.communityListWidth : collapsedWidth + visible: Settings.groupView CommunitiesList { id: communitiesList collapsed: parent.collapsed } - Binding { - target: Settings + delayed: true property: 'communityListWidth' + restoreMode: Binding.RestoreBindingOrValue + target: Settings value: communityListC.preferredWidth when: !adaptiveView.singlePageMode - delayed: true - restoreMode: Binding.RestoreBindingOrValue } - } - AdaptiveLayoutElement { id: roomListC - minimumWidth: roomlist.avatarSize * 4 + Nheko.paddingSmall * 2 - preferredWidth: (Settings.roomListWidth == - 1) - ? (roomlist.avatarSize * 5 + Nheko.paddingSmall * 2) - : (Settings.roomListWidth >= minimumWidth ? Settings.roomListWidth : collapsedWidth) - maximumWidth: roomlist.avatarSize * 10 + Nheko.paddingSmall * 2 collapsedWidth: roomlist.avatarSize + 2 * Nheko.paddingMedium + maximumWidth: roomlist.avatarSize * 10 + Nheko.paddingSmall * 2 + minimumWidth: roomlist.avatarSize * 4 + Nheko.paddingSmall * 2 + preferredWidth: (Settings.roomListWidth == -1) ? (roomlist.avatarSize * 5 + Nheko.paddingSmall * 2) : (Settings.roomListWidth >= minimumWidth ? Settings.roomListWidth : collapsedWidth) RoomList { id: roomlist - height: adaptiveView.height collapsed: parent.collapsed + height: adaptiveView.height } - Binding { - target: Settings + delayed: true property: 'roomListWidth' + restoreMode: Binding.RestoreBindingOrValue + target: Settings value: roomListC.preferredWidth when: !adaptiveView.singlePageMode - delayed: true - restoreMode: Binding.RestoreBindingOrValue } - } - AdaptiveLayoutElement { id: timlineViewC @@ -127,25 +118,20 @@ Rectangle { id: timeline privacyScreen: privacyScreen - showBackButton: adaptiveView.singlePageMode room: Rooms.currentRoom roomPreview: Rooms.currentRoomPreview.roomid ? Rooms.currentRoomPreview : null + showBackButton: adaptiveView.singlePageMode } - } - } - } - PrivacyScreen { id: privacyScreen anchors.fill: parent - visible: Settings.privacyScreen screenTimeout: Settings.privacyScreenTimeout timelineRoot: adaptiveView + visible: Settings.privacyScreen windowTarget: MainWindow } - } diff --git a/resources/qml/CommunitiesList.qml b/resources/qml/CommunitiesList.qml index ee49ae2d..81f0640e 100644 --- a/resources/qml/CommunitiesList.qml +++ b/resources/qml/CommunitiesList.qml @@ -13,19 +13,24 @@ import im.nheko 1.0 Page { id: communitySidebar + //leftPadding: Nheko.paddingSmall //rightPadding: Nheko.paddingSmall property int avatarSize: Math.ceil(fontMetrics.lineSpacing * 1.6) property bool collapsed: false + background: Rectangle { + color: Nheko.theme.sidebarBackground + } + // HACK: https://bugreports.qt.io/browse/QTBUG-83972, qtwayland cannot auto hide menu Connections { function onHideMenu() { - communityContextMenu.close() + communityContextMenu.close(); } + target: MainWindow } - ListView { id: communitiesList @@ -36,21 +41,161 @@ Page { ScrollBar.vertical: ScrollBar { id: scrollbar + parent: !collapsed && Settings.scrollbarsInRoomlist ? communitiesList : null } + delegate: ItemDelegate { + id: communityItem - ScrollHelper { - flickable: parent - anchors.fill: parent - enabled: !Settings.mobileMode + property color backgroundColor: palette.window + property color bubbleBackground: palette.highlight + property color bubbleText: palette.highlightedText + property color importantText: palette.text + required property var model + property color unimportantText: palette.buttonText + + ToolTip.delay: Nheko.tooltipDelay + ToolTip.text: model.tooltip + ToolTip.visible: hovered && collapsed + height: avatarSize + 2 * Nheko.paddingMedium + state: "normal" + width: ListView.view.width - ((scrollbar.interactive && scrollbar.visible && scrollbar.parent) ? scrollbar.width : 0) + + background: Rectangle { + color: communityItem.backgroundColor + } + states: [ + State { + name: "highlight" + when: (communityItem.hovered || model.hidden) && !(Communities.currentTagId === model.id) + + PropertyChanges { + backgroundColor: palette.dark + bubbleBackground: palette.highlight + bubbleText: palette.highlightedText + importantText: palette.brightText + target: communityItem + unimportantText: palette.brightText + } + }, + State { + name: "selected" + when: Communities.currentTagId == model.id + + PropertyChanges { + backgroundColor: palette.highlight + bubbleBackground: palette.highlightedText + bubbleText: palette.highlight + importantText: palette.highlightedText + target: communityItem + unimportantText: palette.highlightedText + } + } + ] + + onClicked: Communities.setCurrentTagId(model.id) + onPressAndHold: communityContextMenu.show(model.id, model.hidden, model.muted) + + Item { + anchors.fill: parent + + TapHandler { + acceptedButtons: Qt.RightButton + acceptedDevices: PointerDevice.Mouse | PointerDevice.Stylus | PointerDevice.TouchPad + gesturePolicy: TapHandler.ReleaseWithinBounds + + onSingleTapped: communityContextMenu.show(model.id, model.hidden, model.muted) + } + } + RowLayout { + id: r + + anchors.fill: parent + anchors.leftMargin: Nheko.paddingMedium + (communitySidebar.collapsed ? 0 : (fontMetrics.lineSpacing * model.depth)) + anchors.margins: Nheko.paddingMedium + spacing: Nheko.paddingMedium + + ImageButton { + Layout.alignment: Qt.AlignVCenter + Layout.preferredHeight: fontMetrics.lineSpacing + Layout.preferredWidth: fontMetrics.lineSpacing + ToolTip.delay: Nheko.tooltipDelay + ToolTip.text: model.collapsed ? qsTr("Expand") : qsTr("Collapse") + ToolTip.visible: hovered + height: fontMetrics.lineSpacing + hoverEnabled: true + image: model.collapsed ? ":/icons/icons/ui/collapsed.svg" : ":/icons/icons/ui/expanded.svg" + visible: !communitySidebar.collapsed && model.collapsible + width: fontMetrics.lineSpacing + + onClicked: model.collapsed = !model.collapsed + } + Item { + Layout.preferredWidth: fontMetrics.lineSpacing + visible: !communitySidebar.collapsed && !model.collapsible && Communities.containsSubspaces + } + Avatar { + id: avatar + + Layout.alignment: Qt.AlignVCenter + color: communityItem.backgroundColor + displayName: model.displayName + enabled: false + height: avatarSize + roomid: model.id + textColor: model.avatarUrl.startsWith(":/") ? communityItem.unimportantText : communityItem.importantText + url: { + if (model.avatarUrl.startsWith("mxc://")) + return model.avatarUrl.replace("mxc://", "image://MxcImage/"); + else if (model.avatarUrl.length > 0) + return model.avatarUrl; + else + return ""; + } + width: avatarSize + + NotificationBubble { + anchors.bottom: avatar.bottom + anchors.margins: -Nheko.paddingSmall + anchors.right: avatar.right + bubbleBackgroundColor: communityItem.bubbleBackground + bubbleTextColor: communityItem.bubbleText + font.pixelSize: fontMetrics.font.pixelSize * 0.6 + hasLoudNotification: model.hasLoudNotification + mayBeVisible: communitySidebar.collapsed && !model.muted && Settings.spaceNotifications + notificationCount: model.unreadMessages + } + } + ElidedLabel { + Layout.alignment: Qt.AlignVCenter + Layout.fillWidth: true + color: communityItem.importantText + elideWidth: width + fullText: model.displayName + textFormat: Text.PlainText + visible: !communitySidebar.collapsed + } + Item { + Layout.fillWidth: true + } + NotificationBubble { + Layout.alignment: Qt.AlignRight + Layout.leftMargin: Nheko.paddingSmall + bubbleBackgroundColor: communityItem.bubbleBackground + bubbleTextColor: communityItem.bubbleText + hasLoudNotification: model.hasLoudNotification + mayBeVisible: !communitySidebar.collapsed && !model.muted && Settings.spaceNotifications + notificationCount: model.unreadMessages + } + } } Platform.Menu { id: communityContextMenu - property string tagId property bool hidden property bool muted + property string tagId function show(id_, hidden_, muted_) { tagId = id_; @@ -60,177 +205,19 @@ Page { } Platform.MenuItem { - text: qsTr("Do not show notification counts for this community or tag.") checkable: true checked: communityContextMenu.muted + text: qsTr("Do not show notification counts for this community or tag.") + onTriggered: Communities.toggleTagMute(communityContextMenu.tagId) } - Platform.MenuItem { - text: qsTr("Hide rooms with this tag or from this community by default.") checkable: true checked: communityContextMenu.hidden + text: qsTr("Hide rooms with this tag or from this community by default.") + onTriggered: Communities.toggleTagId(communityContextMenu.tagId) } - } - - delegate: ItemDelegate { - id: communityItem - - 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 - required property var model - - height: avatarSize + 2 * Nheko.paddingMedium - width: ListView.view.width - ((scrollbar.interactive && scrollbar.visible && scrollbar.parent) ? scrollbar.width : 0) - state: "normal" - ToolTip.visible: hovered && collapsed - ToolTip.text: model.tooltip - ToolTip.delay: Nheko.tooltipDelay - onClicked: Communities.setCurrentTagId(model.id) - onPressAndHold: communityContextMenu.show(model.id, model.hidden, model.muted) - states: [ - State { - name: "highlight" - when: (communityItem.hovered || model.hidden) && !(Communities.currentTagId === model.id) - - PropertyChanges { - target: communityItem - backgroundColor: Nheko.colors.dark - importantText: Nheko.colors.brightText - unimportantText: Nheko.colors.brightText - bubbleBackground: Nheko.colors.highlight - bubbleText: Nheko.colors.highlightedText - } - - }, - State { - name: "selected" - when: Communities.currentTagId == model.id - - PropertyChanges { - target: communityItem - backgroundColor: Nheko.colors.highlight - importantText: Nheko.colors.highlightedText - unimportantText: Nheko.colors.highlightedText - bubbleBackground: Nheko.colors.highlightedText - bubbleText: Nheko.colors.highlight - } - - } - ] - - Item { - anchors.fill: parent - - TapHandler { - acceptedButtons: Qt.RightButton - onSingleTapped: communityContextMenu.show(model.id, model.hidden, model.muted) - gesturePolicy: TapHandler.ReleaseWithinBounds - acceptedDevices: PointerDevice.Mouse | PointerDevice.Stylus | PointerDevice.TouchPad - } - - } - - RowLayout { - id: r - spacing: Nheko.paddingMedium - anchors.fill: parent - anchors.margins: Nheko.paddingMedium - anchors.leftMargin: Nheko.paddingMedium + (communitySidebar.collapsed ? 0 : (fontMetrics.lineSpacing * model.depth)) - - ImageButton { - visible: !communitySidebar.collapsed && model.collapsible - Layout.preferredHeight: fontMetrics.lineSpacing - Layout.preferredWidth: fontMetrics.lineSpacing - Layout.alignment: Qt.AlignVCenter - height: fontMetrics.lineSpacing - width: fontMetrics.lineSpacing - image: model.collapsed ? ":/icons/icons/ui/collapsed.svg" : ":/icons/icons/ui/expanded.svg" - ToolTip.visible: hovered - ToolTip.delay: Nheko.tooltipDelay - ToolTip.text: model.collapsed ? qsTr("Expand") : qsTr("Collapse") - hoverEnabled: true - - onClicked: model.collapsed = !model.collapsed - } - - Item { - Layout.preferredWidth: fontMetrics.lineSpacing - visible: !communitySidebar.collapsed && !model.collapsible && Communities.containsSubspaces - } - - Avatar { - id: avatar - - enabled: false - Layout.alignment: Qt.AlignVCenter - height: avatarSize - width: avatarSize - url: { - if (model.avatarUrl.startsWith("mxc://")) - return model.avatarUrl.replace("mxc://", "image://MxcImage/"); - else - return "image://colorimage/" + model.avatarUrl + "?" + communityItem.unimportantText; - } - roomid: model.id - displayName: model.displayName - color: communityItem.backgroundColor - - NotificationBubble { - notificationCount: model.unreadMessages - hasLoudNotification: model.hasLoudNotification - bubbleBackgroundColor: communityItem.bubbleBackground - bubbleTextColor: communityItem.bubbleText - font.pixelSize: fontMetrics.font.pixelSize * 0.6 - mayBeVisible: communitySidebar.collapsed && !model.muted && Settings.spaceNotifications - anchors.right: avatar.right - anchors.bottom: avatar.bottom - anchors.margins: -Nheko.paddingSmall - } - - } - - ElidedLabel { - visible: !communitySidebar.collapsed - Layout.alignment: Qt.AlignVCenter - color: communityItem.importantText - Layout.fillWidth: true - elideWidth: width - fullText: model.displayName - textFormat: Text.PlainText - } - - Item { - Layout.fillWidth: true - } - - NotificationBubble { - notificationCount: model.unreadMessages - hasLoudNotification: model.hasLoudNotification - bubbleBackgroundColor: communityItem.bubbleBackground - bubbleTextColor: communityItem.bubbleText - mayBeVisible: !communitySidebar.collapsed && !model.muted && Settings.spaceNotifications - Layout.alignment: Qt.AlignRight - Layout.leftMargin: Nheko.paddingSmall - } - - } - - background: Rectangle { - color: communityItem.backgroundColor - } - - } - } - - background: Rectangle { - color: Nheko.theme.sidebarBackground - } - } diff --git a/resources/qml/Completer.qml b/resources/qml/Completer.qml index 0a9c41ed..9aebfdf5 100644 --- a/resources/qml/Completer.qml +++ b/resources/qml/Completer.qml @@ -11,124 +11,107 @@ import im.nheko 1.0 Control { id: popup - property alias currentIndex: listView.currentIndex - property string roomId - property string completerName - property var completer - property bool bottomToTop: true - property bool fullWidth: false - property bool centerRowContent: true property int avatarHeight: 24 property int avatarWidth: 24 + property bool bottomToTop: true + property bool centerRowContent: true + property var completer + property string completerName + property alias count: listView.count + property alias currentIndex: listView.currentIndex + property bool fullWidth: false + property string roomId property int rowMargin: 0 property int rowSpacing: Nheko.paddingSmall - property alias count: listView.count signal completionClicked(string completion) signal completionSelected(string id) - function up() { - if (bottomToTop) - down_(); - else - up_(); + function changeCompleter() { + if (completerName) { + completer = TimelineManager.completerFor(completerName, completerName == "room" ? "" : (popup.roomId != "" ? popup.roomId : room.roomId)); + completer.setSearchString(""); + } else { + completer = undefined; + } + currentIndex = -1; } - - function down() { - if (bottomToTop) - up_(); - else - down_(); - } - - function up_() { - currentIndex = currentIndex - 1; - if (currentIndex == -2) - currentIndex = listView.count - 1; - - } - - function down_() { - currentIndex = currentIndex + 1; - if (currentIndex >= listView.count) - currentIndex = -1; - - } - function currentCompletion() { if (currentIndex > -1 && currentIndex < listView.count) return completer.completionAt(currentIndex); else return null; } - + function down() { + if (bottomToTop) + up_(); + else + down_(); + } + function down_() { + currentIndex = currentIndex + 1; + if (currentIndex >= listView.count) + currentIndex = -1; + } function finishCompletion() { if (popup.completerName == "room") popup.completionSelected(listView.itemAtIndex(currentIndex).modelData.roomid); else if (popup.completerName == "user") popup.completionSelected(listView.itemAtIndex(currentIndex).modelData.userid); - } - - function changeCompleter() { - if (completerName) { - completer = TimelineManager.completerFor(completerName, completerName == "room" ? "" : (popup.roomId != "" ? popup.roomId : room.roomId)); - completer.setSearchString(""); - } else { - completer = undefined; - } - currentIndex = -1 + function up() { + if (bottomToTop) + down_(); + else + up_(); + } + function up_() { + currentIndex = currentIndex - 1; + if (currentIndex == -2) + currentIndex = listView.count - 1; } - onCompleterNameChanged: changeCompleter() - onRoomIdChanged: changeCompleter() bottomPadding: 1 leftPadding: 1 - topPadding: 1 - rightPadding: 1 + // Workaround palettes not inheriting for popups + palette: timelineRoot.palette + rightPadding: 1 + topPadding: 1 + + background: Rectangle { + border.color: palette.mid + color: palette.base + } contentItem: ListView { id: listView - // If we have fewer than 7 items, just use the list view's content height. + clip: true + displayMarginBeginning: height / 2 + displayMarginEnd: height / 2 + highlightFollowsCurrentItem: true + + // If we have fewer than 7 items, just use the list view's content height. // Otherwise, we want to show 7 items. Each item consists of row spacing between rows, row margins // on each side of a row, 1px of padding above the first item and below the last item, and nominally // some kind of content height. avatarHeight is used for just about every delegate, so we're using // that until we find something better. Put is all together and you have the formula below! - implicitHeight: Math.min(contentHeight, 6*rowSpacing + 7*(popup.avatarHeight + 2*rowMargin)) - clip: true - ScrollHelper { - flickable: parent - anchors.fill: parent - enabled: !Settings.mobileMode - } - - Timer { - id: deadTimer - interval: 50 - } - - onContentYChanged: deadTimer.restart() + implicitHeight: Math.min(contentHeight, 6 * rowSpacing + 7 * (popup.avatarHeight + 2 * rowMargin)) // Broken, see https://bugreports.qt.io/browse/QTBUG-102811 //reuseItems: true - implicitWidth: listView.contentItem.childrenRect.width + implicitWidth: Math.max(listView.contentItem.childrenRect.width, 20) model: completer - verticalLayoutDirection: popup.bottomToTop ? ListView.BottomToTop : ListView.TopToBottom - spacing: rowSpacing pixelAligned: true - highlightFollowsCurrentItem: true - - displayMarginBeginning: height / 2 - displayMarginEnd: height / 2 + spacing: rowSpacing + verticalLayoutDirection: popup.bottomToTop ? ListView.BottomToTop : ListView.TopToBottom delegate: Rectangle { property variant modelData: model ListView.delayRemove: true - - color: model.index == popup.currentIndex ? Nheko.colors.highlight : Nheko.colors.base - height: chooser.child.implicitHeight + 2 * popup.rowMargin + color: model.index == popup.currentIndex ? palette.highlight : palette.base + height: (chooser.child?.implicitHeight ?? 0) + 2 * popup.rowMargin implicitWidth: fullWidth ? ListView.view.width : chooser.child.implicitWidth + 4 MouseArea { @@ -136,26 +119,27 @@ Control { anchors.fill: parent hoverEnabled: true - onPositionChanged: if (!listView.moving && !deadTimer.running) popup.currentIndex = model.index + onClicked: { - popup.completionClicked(completer.completionAt(model.index)); - if (popup.completerName == "room") - popup.completionSelected(model.roomid); - else if (popup.completerName == "user") - popup.completionSelected(model.userid); + popup.completionClicked(completer.completionAt(model.index)); + if (popup.completerName == "room") + popup.completionSelected(model.roomid); + else if (popup.completerName == "user") + popup.completionSelected(model.userid); } + onPositionChanged: if (!listView.moving && !deadTimer.running) + popup.currentIndex = model.index } Ripple { - color: Qt.rgba(Nheko.colors.base.r, Nheko.colors.base.g, Nheko.colors.base.b, 0.5) + color: Qt.rgba(palette.base.r, palette.base.g, palette.base.b, 0.5) } - DelegateChooser { id: chooser - roleValue: popup.completerName anchors.fill: parent anchors.margins: popup.rowMargin enabled: false + roleValue: popup.completerName DelegateChoice { roleValue: "user" @@ -167,28 +151,23 @@ Control { spacing: rowSpacing Avatar { - height: popup.avatarHeight - width: popup.avatarWidth displayName: model.displayName - userid: model.userid - url: model.avatarUrl.replace("mxc://", "image://MxcImage/") enabled: false + height: popup.avatarHeight + url: model.avatarUrl.replace("mxc://", "image://MxcImage/") + userid: model.userid + width: popup.avatarWidth } - Label { + color: model.index == popup.currentIndex ? palette.highlightedText : palette.text text: model.displayName - color: model.index == popup.currentIndex ? Nheko.colors.highlightedText : Nheko.colors.text } - Label { + color: model.index == popup.currentIndex ? palette.highlightedText : palette.buttonText text: "(" + model.userid + ")" - color: model.index == popup.currentIndex ? Nheko.colors.highlightedText : Nheko.colors.buttonText } - } - } - DelegateChoice { roleValue: "emoji" @@ -199,39 +178,33 @@ Control { spacing: rowSpacing Label { - visible: !!model.unicode - text: model.unicode - color: model.index == popup.currentIndex ? Nheko.colors.highlightedText : Nheko.colors.text + color: model.index == popup.currentIndex ? palette.highlightedText : palette.text font: Settings.emojiFont + text: model.unicode + visible: !!model.unicode } - Avatar { - visible: !model.unicode - height: popup.avatarHeight - width: popup.avatarWidth + crop: false displayName: model.shortcode + enabled: false + height: popup.avatarHeight //userid: model.shortcode url: (model.url ? model.url : "").replace("mxc://", "image://MxcImage/") - enabled: false - crop: false + visible: !model.unicode + width: popup.avatarWidth } - Label { Layout.leftMargin: Nheko.paddingSmall Layout.rightMargin: Nheko.paddingSmall + color: model.index == popup.currentIndex ? palette.highlightedText : palette.text text: model.shortcode - color: model.index == popup.currentIndex ? Nheko.colors.highlightedText : Nheko.colors.text } - Label { + color: model.index == popup.currentIndex ? palette.highlightedText : palette.buttonText text: "(" + model.packname + ")" - color: model.index == popup.currentIndex ? Nheko.colors.highlightedText : Nheko.colors.buttonText } - } - } - DelegateChoice { roleValue: "command" @@ -242,20 +215,16 @@ Control { spacing: rowSpacing Label { - text: model.name - color: model.index == popup.currentIndex ? Nheko.colors.highlightedText : Nheko.colors.text + color: model.index == popup.currentIndex ? palette.highlightedText : palette.text font.bold: true + text: model.name } - Label { + color: model.index == popup.currentIndex ? palette.highlightedText : palette.buttonText text: model.description - color: model.index == popup.currentIndex ? Nheko.colors.highlightedText : Nheko.colors.buttonText } - } - } - DelegateChoice { roleValue: "room" @@ -266,26 +235,22 @@ Control { spacing: rowSpacing Avatar { - height: popup.avatarHeight - width: popup.avatarWidth displayName: model.roomName + enabled: false + height: popup.avatarHeight roomid: model.roomid url: model.avatarUrl.replace("mxc://", "image://MxcImage/") - enabled: false + width: popup.avatarWidth } - Label { - text: model.roomName - font.pixelSize: popup.avatarHeight * 0.5 - color: model.index == popup.currentIndex ? Nheko.colors.highlightedText : Nheko.colors.text + color: model.index == popup.currentIndex ? palette.highlightedText : palette.text font.italic: model.isTombstoned + font.pixelSize: popup.avatarHeight * 0.5 + text: model.roomName textFormat: Text.RichText } - } - } - DelegateChoice { roleValue: "roomAliases" @@ -296,41 +261,38 @@ Control { spacing: rowSpacing Avatar { - height: popup.avatarHeight - width: popup.avatarWidth displayName: model.roomName + enabled: false + height: popup.avatarHeight roomid: model.roomid url: model.avatarUrl.replace("mxc://", "image://MxcImage/") - enabled: false + width: popup.avatarWidth } - Label { - text: model.roomName - color: model.index == popup.currentIndex ? Nheko.colors.highlightedText : Nheko.colors.text + color: model.index == popup.currentIndex ? palette.highlightedText : palette.text font.italic: model.isTombstoned + text: model.roomName textFormat: Text.RichText } - Label { + color: model.index == popup.currentIndex ? palette.highlightedText : palette.buttonText text: "(" + model.roomAlias + ")" - color: model.index == popup.currentIndex ? Nheko.colors.highlightedText : Nheko.colors.buttonText textFormat: Text.RichText } - } - } - } - } + onContentYChanged: deadTimer.restart() + + Timer { + id: deadTimer + + interval: 50 + } } - - background: Rectangle { - color: Nheko.colors.base - border.color: Nheko.colors.mid - } - + onCompleterNameChanged: changeCompleter() + onRoomIdChanged: changeCompleter() } diff --git a/resources/qml/ElidedLabel.qml b/resources/qml/ElidedLabel.qml index 180259b1..153d7c33 100644 --- a/resources/qml/ElidedLabel.qml +++ b/resources/qml/ElidedLabel.qml @@ -9,21 +9,20 @@ import im.nheko 1.0 Label { id: root - property alias fullText: metrics.text property alias elideWidth: metrics.elideWidth + property alias fullText: metrics.text property int fullTextWidth: Math.ceil(metrics.advanceWidth) - color: Nheko.colors.text - text: (textFormat == Text.PlainText) ? metrics.elidedText : TimelineManager.escapeEmoji(metrics.elidedText) - maximumLineCount: 1 + color: palette.text elide: Text.ElideRight + maximumLineCount: 1 + text: (textFormat == Text.PlainText) ? metrics.elidedText : TimelineManager.escapeEmoji(metrics.elidedText) textFormat: Text.PlainText TextMetrics { id: metrics - font.pointSize: root.font.pointSize elide: Text.ElideRight + font.pointSize: root.font.pointSize } - } diff --git a/resources/qml/EncryptionIndicator.qml b/resources/qml/EncryptionIndicator.qml index 5338b6be..fb9dc7b5 100644 --- a/resources/qml/EncryptionIndicator.qml +++ b/resources/qml/EncryptionIndicator.qml @@ -11,51 +11,29 @@ Image { id: stateImg property bool encrypted: false - property int trust: Crypto.Unverified - property string unencryptedIcon: ":/icons/icons/ui/shield-filled-cross.svg" - property color unencryptedColor: Nheko.theme.error - property color unencryptedHoverColor: unencryptedColor property bool hovered: ma.hovered - property string sourceUrl: { if (!encrypted) - return "image://colorimage/" + unencryptedIcon + "?"; - + return "image://colorimage/" + unencryptedIcon + "?"; switch (trust) { - case Crypto.Verified: + case Crypto.Verified: return "image://colorimage/:/icons/icons/ui/shield-filled-checkmark.svg?"; - case Crypto.TOFU: + case Crypto.TOFU: return "image://colorimage/:/icons/icons/ui/shield-filled.svg?"; - case Crypto.Unverified: + case Crypto.Unverified: return "image://colorimage/:/icons/icons/ui/shield-filled-exclamation-mark.svg?"; - default: + default: return "image://colorimage/:/icons/icons/ui/shield-filled-cross.svg?"; } } + property int trust: Crypto.Unverified + property color unencryptedColor: Nheko.theme.error + property color unencryptedHoverColor: unencryptedColor + property string unencryptedIcon: ":/icons/icons/ui/shield-filled-cross.svg" - width: 16 - height: 16 - sourceSize.height: height - sourceSize.width: width - source: { - if (encrypted) { - switch (trust) { - case Crypto.Verified: - return sourceUrl + Nheko.theme.green; - case Crypto.TOFU: - return sourceUrl + Nheko.colors.buttonText; - default: - return sourceUrl + Nheko.theme.error; - } - } else { - return sourceUrl + (stateImg.hovered ? unencryptedHoverColor : unencryptedColor); - } - } - ToolTip.visible: stateImg.hovered ToolTip.text: { if (!encrypted) return qsTr("This message is not encrypted!"); - switch (trust) { case Crypto.Verified: return qsTr("Encrypted by a verified device"); @@ -65,9 +43,28 @@ Image { return qsTr("Encrypted by an unverified device or the key is from an untrusted source like the key backup."); } } + ToolTip.visible: stateImg.hovered + height: 16 + source: { + if (encrypted) { + switch (trust) { + case Crypto.Verified: + return sourceUrl + Nheko.theme.green; + case Crypto.TOFU: + return sourceUrl + palette.buttonText; + default: + return sourceUrl + Nheko.theme.error; + } + } else { + return sourceUrl + (stateImg.hovered ? unencryptedHoverColor : unencryptedColor); + } + } + sourceSize.height: height + sourceSize.width: width + width: 16 HoverHandler { id: ma - } + } } diff --git a/resources/qml/ForwardCompleter.qml b/resources/qml/ForwardCompleter.qml index 4ab23a4f..0174e0f6 100644 --- a/resources/qml/ForwardCompleter.qml +++ b/resources/qml/ForwardCompleter.qml @@ -16,14 +16,24 @@ Popup { mid = mid_in; } + leftPadding: 10 + modal: true + + // Workaround palettes not inheriting for popups + palette: timelineRoot.palette + parent: Overlay.overlay + rightPadding: 10 + width: timelineRoot.width * 0.8 x: Math.round(parent.width / 2 - width / 2) y: Math.round(parent.height / 4) - modal: true - palette: Nheko.colors - parent: Overlay.overlay - width: timelineRoot.width * 0.8 - leftPadding: 10 - rightPadding: 10 + + Overlay.modal: Rectangle { + color: Qt.rgba(palette.window.r, palette.window.g, palette.window.b, 0.7) + } + background: Rectangle { + color: palette.window + } + onOpened: { roomTextInput.forceActiveFocus(); } @@ -36,46 +46,40 @@ Popup { Label { id: titleLabel - text: qsTr("Forward Message") - font.bold: true bottomPadding: 10 - color: Nheko.colors.text + color: palette.text + font.bold: true + text: qsTr("Forward Message") } - Reply { id: replyPreview - property var modelData: room ? room.getDump(mid, "") : { - } + property var modelData: room ? room.getDump(mid, "") : {} - width: parent.width - - userColor: TimelineManager.userColor(modelData.userId, Nheko.colors.window) blurhash: modelData.blurhash ?? "" body: modelData.body ?? "" - formattedBody: modelData.formattedBody ?? "" + encryptionError: modelData.encryptionError ?? "" eventId: modelData.eventId ?? "" filename: modelData.filename ?? "" filesize: modelData.filesize ?? "" + formattedBody: modelData.formattedBody ?? "" + isOnlyEmoji: modelData.isOnlyEmoji ?? false + originalWidth: modelData.originalWidth ?? 0 proportionalHeight: modelData.proportionalHeight ?? 1 type: modelData.type ?? MtxEvent.UnknownMessage typeString: modelData.typeString ?? "" url: modelData.url ?? "" - originalWidth: modelData.originalWidth ?? 0 - isOnlyEmoji: modelData.isOnlyEmoji ?? false + userColor: TimelineManager.userColor(modelData.userId, palette.window) userId: modelData.userId ?? "" userName: modelData.userName ?? "" - encryptionError: modelData.encryptionError ?? "" + width: parent.width } - MatrixTextField { id: roomTextInput + color: palette.text width: forwardMessagePopup.width - forwardMessagePopup.leftPadding * 2 - color: Nheko.colors.text - onTextEdited: { - completerPopup.completer.searchString = text; - } + Keys.onPressed: { if (event.key == Qt.Key_Up || event.key == Qt.Key_Backtab) { event.accepted = true; @@ -91,43 +95,32 @@ Popup { event.accepted = true; } } + onTextEdited: { + completerPopup.completer.searchString = text; + } } - Completer { id: completerPopup - width: forwardMessagePopup.width - forwardMessagePopup.leftPadding * 2 - completerName: "room" - fullWidth: true - centerRowContent: false avatarHeight: 24 avatarWidth: 24 bottomToTop: false + centerRowContent: false + completerName: "room" + fullWidth: true + width: forwardMessagePopup.width - forwardMessagePopup.leftPadding * 2 } - } - Connections { function onCompletionSelected(id) { room.forwardMessage(messageContextMenu.eventId, id); forwardMessagePopup.close(); } - function onCountChanged() { if (completerPopup.count > 0 && (completerPopup.currentIndex < 0 || completerPopup.currentIndex >= completerPopup.count)) completerPopup.currentIndex = 0; - } target: completerPopup } - - background: Rectangle { - color: Nheko.colors.window - } - - Overlay.modal: Rectangle { - color: Qt.rgba(Nheko.colors.window.r, Nheko.colors.window.g, Nheko.colors.window.b, 0.7) - } - } diff --git a/resources/qml/ImageButton.qml b/resources/qml/ImageButton.qml index 547b4a12..ddc0b7d8 100644 --- a/resources/qml/ImageButton.qml +++ b/resources/qml/ImageButton.qml @@ -2,6 +2,7 @@ // // SPDX-License-Identifier: GPL-3.0-or-later +pragma ComponentBehavior: Bound import "./ui" import QtQuick 2.3 import QtQuick.Controls 2.3 @@ -10,38 +11,35 @@ import im.nheko 1.0 // for cursor shape AbstractButton { id: button - property alias cursor: mouseArea.cursorShape - property string image: undefined - property color highlightColor: Nheko.colors.highlight - property color buttonTextColor: Nheko.colors.buttonText + property color buttonTextColor: palette.buttonText property bool changeColorOnHover: true + property alias cursor: mouseArea.cursorShape + property color highlightColor: palette.highlight + property string image: undefined property bool ripple: true focusPolicy: Qt.NoFocus - width: 16 height: 16 + width: 16 Image { id: buttonImg // Workaround, can't get icon.source working for now... anchors.fill: parent - source: image != "" ? ("image://colorimage/" + image + "?" + ((button.hovered && changeColorOnHover) ? highlightColor : buttonTextColor)) : "" + fillMode: Image.PreserveAspectFit + source: button.image != "" ? ("image://colorimage/" + button.image + "?" + ((button.hovered && button.changeColorOnHover) ? button.highlightColor : button.buttonTextColor)) : "" sourceSize.height: button.height sourceSize.width: button.width - fillMode: Image.PreserveAspectFit } - - CursorShape { + NhekoCursorShape { id: mouseArea anchors.fill: parent cursorShape: Qt.PointingHandCursor } - Ripple { + color: Qt.rgba(button.buttonTextColor.r, button.buttonTextColor.g, button.buttonTextColor.b, 0.5) enabled: button.ripple - color: Qt.rgba(buttonTextColor.r, buttonTextColor.g, buttonTextColor.b, 0.5) } - } diff --git a/resources/qml/MatrixText.qml b/resources/qml/MatrixText.qml index 7956f0b6..94b8bb98 100644 --- a/resources/qml/MatrixText.qml +++ b/resources/qml/MatrixText.qml @@ -11,28 +11,42 @@ TextEdit { property alias cursorShape: cs.cursorShape - textFormat: TextEdit.RichText - readOnly: true - focus: false - wrapMode: Text.Wrap - selectByMouse: !Settings.mobileMode + //leftInset: 0 + //bottomInset: 0 + //rightInset: 0 + //topInset: 0 + //leftPadding: 0 + //bottomPadding: 0 + //rightPadding: 0 + //topPadding: 0 + //background: null + + ToolTip.text: hoveredLink + ToolTip.visible: hoveredLink || false // this always has to be enabled, otherwise you can't click links anymore! //enabled: selectByMouse - color: Nheko.colors.text - onLinkActivated: Nheko.openLink(link) - ToolTip.visible: hoveredLink || false - ToolTip.text: hoveredLink + color: palette.text + focus: false + readOnly: true + selectByMouse: !Settings.mobileMode + textFormat: TextEdit.RichText + wrapMode: Text.Wrap + // Setting a tooltip delay makes the hover text empty .-. //ToolTip.delay: Nheko.tooltipDelay Component.onCompleted: { TimelineManager.fixImageRendering(r.textDocument, r); } + onLinkActivated: Nheko.openLink(link) - CursorShape { + //// propagate events up + //onPressAndHold: (event) => event.accepted = false + //onPressed: (event) => event.accepted = (event.button == Qt.LeftButton) + + NhekoCursorShape { id: cs anchors.fill: parent cursorShape: hoveredLink ? Qt.PointingHandCursor : Qt.ArrowCursor } - } diff --git a/resources/qml/MatrixTextField.qml b/resources/qml/MatrixTextField.qml index 74cacf33..7209a5aa 100644 --- a/resources/qml/MatrixTextField.qml +++ b/resources/qml/MatrixTextField.qml @@ -7,68 +7,63 @@ import QtQuick.Controls 2.12 import QtQuick.Layouts 1.12 import im.nheko 1.0 - ColumnLayout { id: c - property color backgroundColor: Nheko.colors.base + + property color backgroundColor: palette.base property alias color: labelC.color - property alias textPadding: input.padding - property alias text: input.text + property alias echoMode: input.echoMode + property alias font: input.font + property var hasClear: false property alias label: labelC.text property alias placeholderText: input.placeholderText - property alias font: input.font - property alias echoMode: input.echoMode property alias selectByMouse: input.selectByMouse - property var hasClear: false + property alias text: input.text + property alias textPadding: input.padding - Timer { - id: timer - interval: 350 - onTriggered: editingFinished() - } - - onTextChanged: timer.restart() - - signal textEdited signal accepted signal editingFinished - - function forceActiveFocus() { - input.forceActiveFocus(); - } + signal textEdited function clear() { input.clear(); } + function forceActiveFocus() { + input.forceActiveFocus(); + } ToolTip.delay: Nheko.tooltipDelay ToolTip.visible: hover.hovered - spacing: 0 - Item { - Layout.fillWidth: true - Layout.preferredHeight: labelC.contentHeight - Layout.margins: input.padding - Layout.bottomMargin: Nheko.paddingSmall - visible: labelC.text + onTextChanged: timer.restart() + Timer { + id: timer + + interval: 350 + + onTriggered: editingFinished() + } + Item { + Layout.bottomMargin: Nheko.paddingSmall + Layout.fillWidth: true + Layout.margins: input.padding + Layout.preferredHeight: labelC.contentHeight + visible: labelC.text z: 1 Label { id: labelC - y: contentHeight + input.padding + Nheko.paddingSmall + color: palette.text enabled: false - - palette: Nheko.colors - color: Nheko.colors.text + font.letterSpacing: input.font.pixelSize * 0.02 font.pixelSize: input.font.pixelSize font.weight: Font.DemiBold - font.letterSpacing: input.font.pixelSize * 0.02 - width: parent.width - state: labelC.text && (input.activeFocus == true || input.text) ? "focused" : "" + width: parent.width + y: contentHeight + input.padding + Nheko.paddingSmall states: State { name: "focused" @@ -77,51 +72,40 @@ ColumnLayout { target: labelC y: 0 } - PropertyChanges { - target: input opacity: 1 + target: input } - } - transitions: Transition { from: "" - to: "focused" reversible: true + to: "focused" NumberAnimation { - target: labelC + alwaysRunToEnd: true + duration: 210 + easing.type: Easing.InCubic properties: "y" - duration: 210 - easing.type: Easing.InCubic - alwaysRunToEnd: true + target: labelC } - NumberAnimation { - target: input - properties: "opacity" + alwaysRunToEnd: true duration: 210 easing.type: Easing.InCubic - alwaysRunToEnd: true + properties: "opacity" + target: input } - } } } - TextField { id: input + Layout.fillWidth: true - - palette: Nheko.colors color: labelC.color - opacity: labelC.text ? 0 : 1 focus: true - - onTextEdited: c.textEdited() - onAccepted: c.accepted() - onEditingFinished: c.editingFinished() + opacity: labelC.text ? 0 : 1 background: Rectangle { id: backgroundRect @@ -129,44 +113,46 @@ ColumnLayout { color: labelC.text ? "transparent" : backgroundColor } + onAccepted: c.accepted() + onEditingFinished: c.editingFinished() + onTextEdited: c.textEdited() + ImageButton { id: clearText + focusPolicy: Qt.NoFocus + hoverEnabled: true + image: ":/icons/icons/ui/round-remove-button.svg" visible: c.hasClear && searchField.text !== '' - image: ":/icons/icons/ui/round-remove-button.svg" - focusPolicy: Qt.NoFocus onClicked: { - searchField.clear() + searchField.clear(); topBar.searchString = ""; } - hoverEnabled: true + anchors { - top: parent.top bottom: parent.bottom right: parent.right rightMargin: Nheko.paddingSmall + top: parent.top } } - } - Rectangle { id: blueBar Layout.fillWidth: true - - color: Nheko.colors.highlight + color: palette.highlight height: 1 Rectangle { id: blackBar - anchors.top: parent.top anchors.horizontalCenter: parent.horizontalCenter - height: parent.height*2 + anchors.top: parent.top + color: palette.text + height: parent.height * 2 width: 0 - color: Nheko.colors.text states: State { name: "focused" @@ -176,31 +162,25 @@ ColumnLayout { target: blackBar width: blueBar.width } - } - transitions: Transition { from: "" - to: "focused" reversible: true - + to: "focused" NumberAnimation { - target: blackBar - properties: "width" + alwaysRunToEnd: true duration: 310 easing.type: Easing.InCubic - alwaysRunToEnd: true + properties: "width" + target: blackBar } - } - } - } - HoverHandler { id: hover + enabled: c.ToolTip.text } } diff --git a/resources/qml/MessageInput.qml b/resources/qml/MessageInput.qml index 8e72f458..e196b06d 100644 --- a/resources/qml/MessageInput.qml +++ b/resources/qml/MessageInput.qml @@ -14,60 +14,54 @@ import im.nheko 1.0 Rectangle { id: inputBar + property bool showAllButtons: width > 450 || (messageInput.length == 0 && !messageInput.inputMethodComposing) readonly property string text: messageInput.text - color: Nheko.colors.window Layout.fillWidth: true - Layout.preferredHeight: row.implicitHeight Layout.minimumHeight: 40 - property bool showAllButtons: width > 450 || (messageInput.length == 0 && !messageInput.inputMethodComposing) - + Layout.preferredHeight: row.implicitHeight + color: palette.window Component { id: placeCallDialog PlaceCall { } - } - Component { id: screenShareDialog ScreenShare { } - } - RowLayout { id: row - visible: room ? room.permissions.canSend(MtxEvent.TextMessage) : false anchors.fill: parent spacing: 0 + visible: room ? room.permissions.canSend(MtxEvent.TextMessage) : false ImageButton { - visible: CallManager.callsSupported && showAllButtons - opacity: (CallManager.haveCallInvite || CallManager.isOnCallOnOtherDevice) ? 0.3 : 1 Layout.alignment: Qt.AlignBottom - hoverEnabled: true - width: 22 - height: 22 - image: CallManager.isOnCall ? ":/icons/icons/ui/end-call.svg" : ":/icons/icons/ui/place-call.svg" - ToolTip.visible: hovered - ToolTip.text: CallManager.isOnCall ? qsTr("Hang up") : (CallManager.isOnCallOnOtherDevice ? qsTr("Already on a call") : qsTr("Place a call")) Layout.margins: 8 + ToolTip.text: CallManager.isOnCall ? qsTr("Hang up") : (CallManager.isOnCallOnOtherDevice ? qsTr("Already on a call") : qsTr("Place a call")) + ToolTip.visible: hovered + height: 22 + hoverEnabled: true + image: CallManager.isOnCall ? ":/icons/icons/ui/end-call.svg" : ":/icons/icons/ui/place-call.svg" + opacity: (CallManager.haveCallInvite || CallManager.isOnCallOnOtherDevice) ? 0.3 : 1 + visible: CallManager.callsSupported && showAllButtons + width: 22 + onClicked: { if (room) { if (CallManager.haveCallInvite) { - return ; + return; } else if (CallManager.isOnCall) { CallManager.hangUp(); - } - else if(CallManager.isOnCallOnOtherDevice) { + } else if (CallManager.isOnCallOnOtherDevice) { return; - } - else { + } else { var dialog = placeCallDialog.createObject(timelineRoot); dialog.open(); timelineRoot.destroyOnClose(dialog); @@ -75,22 +69,22 @@ Rectangle { } } } - ImageButton { - visible: showAllButtons Layout.alignment: Qt.AlignBottom - hoverEnabled: true - width: 22 - height: 22 - image: ":/icons/icons/ui/attach.svg" Layout.margins: 8 - onClicked: room.input.openFileSelection() - ToolTip.visible: hovered ToolTip.text: qsTr("Send a file") + ToolTip.visible: hovered + height: 22 + hoverEnabled: true + image: ":/icons/icons/ui/attach.svg" + visible: showAllButtons + width: 22 + + onClicked: room.input.openFileSelection() Rectangle { anchors.fill: parent - color: Nheko.colors.window + color: palette.window visible: room && room.input.uploading Spinner { @@ -98,112 +92,67 @@ Rectangle { height: parent.height / 2 running: parent.visible } - } - } - ScrollView { id: textInput Layout.alignment: Qt.AlignVCenter + Layout.fillWidth: true Layout.maximumHeight: Window.height / 4 Layout.minimumHeight: fontMetrics.lineSpacing Layout.preferredHeight: contentHeight - Layout.fillWidth: true - ScrollBar.horizontal.policy: ScrollBar.AlwaysOff - contentWidth: availableWidth TextArea { id: messageInput property int completerTriggeredAt: 0 + property string lastChar function insertCompletion(completion) { messageInput.remove(completerTriggeredAt, cursorPosition); messageInput.insert(cursorPosition, completion); } - function openCompleter(pos, type) { - if (popup.opened) return; + if (popup.opened) + return; completerTriggeredAt = pos; completer.completerName = type; popup.open(); - completer.completer.setSearchString(messageInput.getText(completerTriggeredAt, cursorPosition)+messageInput.preeditText); + completer.completer.setSearchString(messageInput.getText(completerTriggeredAt, cursorPosition) + messageInput.preeditText); } - function positionCursorAtEnd() { cursorPosition = messageInput.length; } - function positionCursorAtStart() { cursorPosition = 0; } - selectByMouse: true - placeholderText: qsTr("Write a message...") - placeholderTextColor: Nheko.colors.buttonText - color: Nheko.colors.text - width: textInput.width - verticalAlignment: TextEdit.AlignVCenter - wrapMode: TextEdit.Wrap - padding: 0 - topPadding: 8 + background: null bottomPadding: 8 - leftPadding: inputBar.showAllButtons? 0 : 8 + color: palette.text focus: true - property string lastChar - onTextChanged: { - if (room) - room.input.updateState(selectionStart, selectionEnd, cursorPosition, text); - forceActiveFocus(); - if (cursorPosition > 0) - lastChar = text.charAt(cursorPosition-1) - else - lastChar = '' - if (lastChar == '@') { - messageInput.openCompleter(selectionStart-1, "user"); - } else if (lastChar == ':') { - messageInput.openCompleter(selectionStart-1, "emoji"); - } else if (lastChar == '#') { - messageInput.openCompleter(selectionStart-1, "roomAliases"); - } else if (lastChar == "/" && cursorPosition == 1) { - messageInput.openCompleter(selectionStart-1, "command"); - } - } - onCursorPositionChanged: { - if (!room) - return ; + leftPadding: inputBar.showAllButtons ? 0 : 8 + padding: 0 + placeholderText: qsTr("Write a message...") + placeholderTextColor: palette.buttonText + selectByMouse: true + topPadding: 8 + verticalAlignment: TextEdit.AlignVCenter + width: textInput.width + wrapMode: TextEdit.Wrap - room.input.updateState(selectionStart, selectionEnd, cursorPosition, text); - if (popup.opened && cursorPosition <= completerTriggeredAt) - popup.close(); - - if (popup.opened) - completer.completer.setSearchString(messageInput.getText(completerTriggeredAt, cursorPosition)+messageInput.preeditText); - - } - onPreeditTextChanged: { - if (popup.opened) - completer.completer.setSearchString(messageInput.getText(completerTriggeredAt, cursorPosition)+messageInput.preeditText); - } - 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 = (popup.opened && (event.key === Qt.Key_Escape || event.key === Qt.Key_Tab || event.key === Qt.Key_Enter || event.key === Qt.Key_Space)) - Keys.onPressed: { + Keys.onPressed: event => { if (event.matches(StandardKey.Paste)) { event.accepted = room.input.tryPasteAttachment(false); } else if (event.key == Qt.Key_Space) { // close popup if user enters space after colon if (cursorPosition == completerTriggeredAt + 1) popup.close(); - if (popup.opened && completer.count <= 0) popup.close(); - } else if (event.modifiers == Qt.ControlModifier && event.key == Qt.Key_U) { messageInput.clear(); } else if (event.modifiers == Qt.ControlModifier && event.key == Qt.Key_P) { @@ -218,8 +167,8 @@ Rectangle { completer.completerName = ""; popup.close(); } else if (event.matches(StandardKey.InsertLineSeparator)) { - if (popup.opened) popup.close(); - + if (popup.opened) + popup.close(); if (Settings.invertEnterKey && (!Qt.inputMethod.visible || Qt.platform.os === "windows")) { room.input.send(); event.accepted = true; @@ -253,16 +202,16 @@ Rectangle { console.log('"' + t + '"'); if (t == '@') { messageInput.openCompleter(pos, "user"); - return ; + return; } else if (t == ' ' || t == '\t') { messageInput.openCompleter(pos + 1, "user"); - return ; + return; } else if (t == ':') { messageInput.openCompleter(pos, "emoji"); - return ; + return; } else if (t == '~') { messageInput.openCompleter(pos, "customEmoji"); - return ; + return; } pos = pos - 1; } @@ -312,21 +261,53 @@ Rectangle { } } } - background: null + // Ensure that we get escape key press events first. + Keys.onShortcutOverride: event => event.accepted = (popup.opened && (event.key === Qt.Key_Escape || event.key === Qt.Key_Tab || event.key === Qt.Key_Enter || event.key === Qt.Key_Space)) + onCursorPositionChanged: { + if (!room) + return; + room.input.updateState(selectionStart, selectionEnd, cursorPosition, text); + if (popup.opened && cursorPosition <= completerTriggeredAt) + popup.close(); + if (popup.opened) + completer.completer.setSearchString(messageInput.getText(completerTriggeredAt, cursorPosition) + messageInput.preeditText); + } + onPreeditTextChanged: { + if (popup.opened) + completer.completer.setSearchString(messageInput.getText(completerTriggeredAt, cursorPosition) + messageInput.preeditText); + } + onSelectionEndChanged: room.input.updateState(selectionStart, selectionEnd, cursorPosition, text) + onSelectionStartChanged: room.input.updateState(selectionStart, selectionEnd, cursorPosition, text) + onTextChanged: { + if (room) + room.input.updateState(selectionStart, selectionEnd, cursorPosition, text); + forceActiveFocus(); + if (cursorPosition > 0) + lastChar = text.charAt(cursorPosition - 1); + else + lastChar = ''; + if (lastChar == '@') { + messageInput.openCompleter(selectionStart - 1, "user"); + } else if (lastChar == ':') { + messageInput.openCompleter(selectionStart - 1, "emoji"); + } else if (lastChar == '#') { + messageInput.openCompleter(selectionStart - 1, "roomAliases"); + } else if (lastChar == "/" && cursorPosition == 1) { + messageInput.openCompleter(selectionStart - 1, "command"); + } + } Connections { function onRoomChanged() { messageInput.clear(); if (room) messageInput.append(room.input.text); - completer.completerName = ""; messageInput.forceActiveFocus(); } target: timelineView } - Connections { function onCompletionClicked(completion) { messageInput.insertCompletion(completion); @@ -334,43 +315,39 @@ Rectangle { target: completer } - Popup { id: popup + background: null + padding: 0 x: messageInput.positionToRectangle(messageInput.completerTriggeredAt).x y: messageInput.positionToRectangle(messageInput.completerTriggeredAt).y - height - background: null - padding: 0 + enter: Transition { + NumberAnimation { + duration: 100 + from: 0 + property: "opacity" + to: 1 + } + } + exit: Transition { + NumberAnimation { + duration: 100 + from: 1 + property: "opacity" + to: 0 + } + } Completer { - anchors.fill: parent id: completer + + anchors.fill: parent rowMargin: 2 rowSpacing: 0 } - - enter: Transition { - NumberAnimation { - property: "opacity" - from: 0 - to: 1 - duration: 100 - } - - } - - exit: Transition { - NumberAnimation { - property: "opacity" - from: 1 - to: 0 - duration: 100 - } - } } - Connections { function onTextChanged(newText) { messageInput.text = newText; @@ -380,16 +357,13 @@ Rectangle { ignoreUnknownSignals: true target: room ? room.input : null } - Connections { - function onReplyChanged() { - messageInput.forceActiveFocus(); - } - function onEditChanged() { messageInput.forceActiveFocus(); } - + function onReplyChanged() { + messageInput.forceActiveFocus(); + } function onThreadChanged() { messageInput.forceActiveFocus(); } @@ -397,7 +371,6 @@ Rectangle { ignoreUnknownSignals: true target: room } - Connections { function onFocusInput() { messageInput.forceActiveFocus(); @@ -405,91 +378,82 @@ Rectangle { target: TimelineManager } - MouseArea { + acceptedButtons: Qt.MiddleButton // workaround for wrong cursor shape on some platforms anchors.fill: parent - acceptedButtons: Qt.MiddleButton cursorShape: Qt.IBeamCursor - onPressed: (mouse) => mouse.accepted = room.input.tryPasteAttachment(true) + + onPressed: mouse => mouse.accepted = room.input.tryPasteAttachment(true) } - } - } - ImageButton { id: stickerButton - visible: showAllButtons Layout.alignment: Qt.AlignRight | Qt.AlignBottom Layout.margins: 8 - hoverEnabled: true - width: 22 - height: 22 - image: ":/icons/icons/ui/sticky-note-solid.svg" - ToolTip.visible: hovered ToolTip.text: qsTr("Stickers") - onClicked: stickerPopup.visible ? stickerPopup.close() : stickerPopup.show(stickerButton, room.roomId, function(row) { - room.input.sticker(row); - TimelineManager.focusMessageInput(); - }) + ToolTip.visible: hovered + height: 22 + hoverEnabled: true + image: ":/icons/icons/ui/sticky-note-solid.svg" + visible: showAllButtons + width: 22 + + onClicked: stickerPopup.visible ? stickerPopup.close() : stickerPopup.show(stickerButton, room.roomId, function (row) { + room.input.sticker(row); + TimelineManager.focusMessageInput(); + }) StickerPicker { id: stickerPopup - colors: Nheko.colors emoji: false } - } - ImageButton { id: emojiButton Layout.alignment: Qt.AlignRight | Qt.AlignBottom Layout.margins: 8 - hoverEnabled: true - width: 22 - height: 22 - image: ":/icons/icons/ui/smile.svg" - ToolTip.visible: hovered ToolTip.text: qsTr("Emoji") - onClicked: emojiPopup.visible ? emojiPopup.close() : emojiPopup.show(emojiButton, room.roomId, function(plaintext, markdown) { - messageInput.insert(messageInput.cursorPosition, markdown); - TimelineManager.focusMessageInput(); - }) + ToolTip.visible: hovered + height: 22 + hoverEnabled: true + image: ":/icons/icons/ui/smile.svg" + width: 22 + + onClicked: emojiPopup.visible ? emojiPopup.close() : emojiPopup.show(emojiButton, room.roomId, function (plaintext, markdown) { + messageInput.insert(messageInput.cursorPosition, markdown); + TimelineManager.focusMessageInput(); + }) StickerPicker { id: emojiPopup - colors: Nheko.colors emoji: true } } - ImageButton { Layout.alignment: Qt.AlignRight | Qt.AlignBottom Layout.margins: 8 - hoverEnabled: true - width: 22 - height: 22 - image: ":/icons/icons/ui/send.svg" Layout.rightMargin: 8 - ToolTip.visible: hovered ToolTip.text: qsTr("Send") + ToolTip.visible: hovered + height: 22 + hoverEnabled: true + image: ":/icons/icons/ui/send.svg" + width: 22 + onClicked: { room.input.send(); } } - } - Text { anchors.centerIn: parent - visible: room ? (!room.permissions.canSend(MtxEvent.TextMessage)) : false text: qsTr("You don't have permission to send messages in this room") - color: Nheko.colors.text + visible: room ? (!room.permissions.canSend(MtxEvent.TextMessage)) : false } - } diff --git a/resources/qml/MessageInputWarning.qml b/resources/qml/MessageInputWarning.qml index 9b0b0907..4d5578b3 100644 --- a/resources/qml/MessageInputWarning.qml +++ b/resources/qml/MessageInputWarning.qml @@ -10,38 +10,35 @@ import im.nheko 1.0 Rectangle { id: warningRoot - required property string text property color bubbleColor: Nheko.theme.error + required property string text - implicitHeight: visible ? warningDisplay.implicitHeight + 4 * Nheko.paddingSmall : 0 - height: implicitHeight Layout.fillWidth: true - color: Nheko.colors.window // required to hide the timeline behind this warning + color: palette.window // required to hide the timeline behind this warning + height: implicitHeight + implicitHeight: visible ? warningDisplay.implicitHeight + 4 * Nheko.paddingSmall : 0 Rectangle { id: warningRect - visible: warningRoot.visible - // TODO: Qt.alpha() would make more sense but it wasn't working... - color: Qt.rgba(bubbleColor.r, bubbleColor.g, bubbleColor.b, 0.3) - border.width: 1 - border.color: bubbleColor - radius: 3 anchors.fill: parent anchors.margins: visible ? Nheko.paddingSmall : 0 + border.color: bubbleColor + border.width: 1 + // TODO: Qt.alpha() would make more sense but it wasn't working... + color: Qt.rgba(bubbleColor.r, bubbleColor.g, bubbleColor.b, 0.3) + radius: 3 + visible: warningRoot.visible z: 3 Label { id: warningDisplay anchors.left: parent.left - anchors.verticalCenter: parent.verticalCenter anchors.margins: Nheko.paddingSmall - color: Nheko.colors.text + anchors.verticalCenter: parent.verticalCenter text: warningRoot.text textFormat: Text.PlainText } - } - } diff --git a/resources/qml/MessageView.qml b/resources/qml/MessageView.qml index 206b9a17..af3a3371 100644 --- a/resources/qml/MessageView.qml +++ b/resources/qml/MessageView.qml @@ -14,89 +14,270 @@ import QtQuick.Layouts 1.2 import QtQuick.Window 2.13 import im.nheko 1.0 - Item { id: chatRoot - property int padding: Nheko.paddingMedium property int availableWidth: width - + property int padding: Nheko.paddingMedium property string searchString: "" // HACK: https://bugreports.qt.io/browse/QTBUG-83972, qtwayland cannot auto hide menu Connections { function onHideMenu() { - messageContextMenu.close() - replyContextMenu.close() + messageContextMenu.close(); + replyContextMenu.close(); } + target: MainWindow } - ScrollBar { id: scrollbar - parent: chat.parent - anchors.top: parent.top - anchors.right: parent.right + anchors.bottom: parent.bottom + anchors.right: parent.right + anchors.top: parent.top + parent: chat.parent } ListView { id: chat - anchors.fill: parent - - property int delegateMaxWidth: ((Settings.timelineMaxWidth > 100 && Settings.timelineMaxWidth < chatRoot.availableWidth) ? Settings.timelineMaxWidth : chatRoot.availableWidth) - chatRoot.padding * 2 - (scrollbar.interactive? scrollbar.width : 0) - + property int delegateMaxWidth: ((Settings.timelineMaxWidth > 100 && Settings.timelineMaxWidth < chatRoot.availableWidth) ? Settings.timelineMaxWidth : chatRoot.availableWidth) - chatRoot.padding * 2 - (scrollbar.interactive ? scrollbar.width : 0) readonly property alias filteringInProgress: filteredTimeline.filteringInProgress - displayMarginBeginning: height / 2 - displayMarginEnd: height / 2 - - TimelineFilter { - id: filteredTimeline - source: room - filterByThread: room ? room.thread : "" - filterByContent: chatRoot.searchString - } - - model: (filteredTimeline.filterByThread || filteredTimeline.filterByContent) ? filteredTimeline : room + ScrollBar.vertical: scrollbar + anchors.fill: parent + anchors.rightMargin: scrollbar.interactive ? scrollbar.width : 0 // 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() //reuseItems: true boundsBehavior: Flickable.StopAtBounds + displayMarginBeginning: height / 2 + displayMarginEnd: height / 2 + model: (filteredTimeline.filterByThread || filteredTimeline.filterByContent) ? filteredTimeline : room //pixelAligned: true spacing: 2 verticalLayoutDirection: ListView.BottomToTop - onCountChanged: { - // Mark timeline as read - if (atYEnd && room) model.currentIndex = 0; + + delegate: Item { + id: wrapper + + required property string blurhash + required property string body + required property string callType + required property var day + required property string duration + required property int encryptionError + required property string eventId + required property string filename + required property string filesize + required property string formattedBody + required property int index + required property bool isEditable + required property bool isEdited + required property bool isEncrypted + required property bool isOnlyEmoji + required property bool isSender + required property bool isStateEvent + required property int notificationlevel + required property int originalWidth + property var previousMessageDay: (index + 1) >= chat.count ? 0 : chat.model.dataByIndex(index + 1, Room.Day) + property bool previousMessageIsStateEvent: (index + 1) >= chat.count ? true : chat.model.dataByIndex(index + 1, Room.IsStateEvent) + property string previousMessageUserId: (index + 1) >= chat.count ? "" : chat.model.dataByIndex(index + 1, Room.UserId) + required property double proportionalHeight + required property var reactions + required property int relatedEventCacheBuster + required property string replyTo + required property string roomName + required property string roomTopic + property bool scrolledToThis: eventId === room.scrollTarget && (y + height > chat.y + chat.contentY && y < chat.y + chat.height + chat.contentY) + required property int status + required property string threadId + required property string thumbnailUrl + required property var timestamp + required property int trustlevel + required property int type + required property string typeString + required property string url + required property string userId + required property string userName + + ListView.delayRemove: true + anchors.horizontalCenter: parent ? parent.horizontalCenter : undefined + height: (section.item?.height ?? 0) + timelinerow.height + width: chat.delegateMaxWidth + + Loader { + id: section + + property var day: wrapper.day + property bool isSender: wrapper.isSender + property bool isStateEvent: wrapper.isStateEvent + property int parentWidth: parent.width + property var previousMessageDay: wrapper.previousMessageDay + property bool previousMessageIsStateEvent: wrapper.previousMessageIsStateEvent + property string previousMessageUserId: wrapper.previousMessageUserId + property date timestamp: wrapper.timestamp + property string userId: wrapper.userId + property string userName: wrapper.userName + + active: previousMessageUserId !== userId || previousMessageDay !== day || previousMessageIsStateEvent !== isStateEvent + //asynchronous: true + sourceComponent: sectionHeader + visible: status == Loader.Ready + z: 4 + } + TimelineRow { + id: timelinerow + + blurhash: wrapper.blurhash + body: wrapper.body + callType: wrapper.callType + duration: wrapper.duration + encryptionError: wrapper.encryptionError + eventId: chat.model, wrapper.eventId + filename: wrapper.filename + filesize: wrapper.filesize + formattedBody: wrapper.formattedBody + index: wrapper.index + isEditable: wrapper.isEditable + isEdited: wrapper.isEdited + isEncrypted: wrapper.isEncrypted + isOnlyEmoji: wrapper.isOnlyEmoji + isSender: wrapper.isSender + isStateEvent: wrapper.isStateEvent + notificationlevel: wrapper.notificationlevel + originalWidth: wrapper.originalWidth + proportionalHeight: wrapper.proportionalHeight + reactions: wrapper.reactions + relatedEventCacheBuster: wrapper.relatedEventCacheBuster + replyTo: wrapper.replyTo + roomName: wrapper.roomName + roomTopic: wrapper.roomTopic + status: wrapper.status + threadId: wrapper.threadId + thumbnailUrl: wrapper.thumbnailUrl + timestamp: wrapper.timestamp + trustlevel: wrapper.trustlevel + type: chat.model, wrapper.type + typeString: wrapper.typeString + url: wrapper.url + userId: wrapper.userId + userName: wrapper.userName + width: wrapper.width + y: section.visible && section.active ? section.y + section.height : 0 + + background: Rectangle { + id: scrollHighlight + + color: palette.highlight + enabled: false + opacity: 0 + visible: true + z: 1 + + states: State { + name: "revealed" + when: wrapper.scrolledToThis + } + transitions: Transition { + from: "" + to: "revealed" + + SequentialAnimation { + PropertyAnimation { + duration: 500 + easing.type: Easing.InOutQuad + from: 0 + properties: "opacity" + target: scrollHighlight + to: 1 + } + PropertyAnimation { + duration: 500 + easing.type: Easing.InOutQuad + from: 1 + properties: "opacity" + target: scrollHighlight + to: 0 + } + ScriptAction { + script: room.eventShown() + } + } + } + } + + onHoveredChanged: { + if (!Settings.mobileMode && hovered) { + if (!messageActions.hovered) { + messageActions.attached = timelinerow; + messageActions.model = timelinerow; + } + } + } + } + Connections { + function onMovementEnded() { + if (y + height + 2 * chat.spacing > chat.contentY + chat.height && y < chat.contentY + chat.height) + chat.model.currentIndex = index; + } + + target: chat + } + } + footer: Item { + anchors.horizontalCenter: parent.horizontalCenter + anchors.margins: Nheko.paddingLarge + // hacky, but works + height: loadingSpinner.height + 2 * Nheko.paddingLarge + visible: (room && room.paginationInProgress) || chat.filteringInProgress + + Spinner { + id: loadingSpinner + + anchors.centerIn: parent + anchors.margins: Nheko.paddingLarge + foreground: palette.mid + running: (room && room.paginationInProgress) || chat.filteringInProgress + z: 3 + } } - ScrollBar.vertical: scrollbar + Window.onActiveChanged: readTimer.running = Window.active + onCountChanged: { + // Mark timeline as read + if (atYEnd && room) + model.currentIndex = 0; + } - anchors.rightMargin: scrollbar.interactive? scrollbar.width : 0 + TimelineFilter { + id: filteredTimeline + filterByContent: chatRoot.searchString + filterByThread: room ? room.thread : "" + source: room + } Control { id: messageActions property Item attached: null - property alias model: row.model // use comma to update on scroll property var attachedPos: chat.contentY, attached ? chat.mapFromItem(attached, attached ? attached.width - width : 0, -height) : null - padding: Nheko.paddingSmall + property alias model: row.model hoverEnabled: true + padding: Nheko.paddingSmall visible: Settings.buttonsInTimeline && !!attached && (attached.hovered || hovered) x: attached ? attachedPos.x : 0 y: attached ? attachedPos.y + Nheko.paddingSmall : 0 z: 10 background: Rectangle { - color: Nheko.colors.window - border.color: Nheko.colors.buttonText + border.color: palette.buttonText border.width: 1 + color: palette.window radius: padding } - contentItem: RowLayout { id: row @@ -106,139 +287,171 @@ Item { Repeater { model: Settings.recentReactions + visible: room ? room.permissions.canSend(MtxEvent.Reaction) : false - delegate: TextButton { + delegate: AbstractButton { + id: button + + property color buttonTextColor: palette.buttonText + property color highlightColor: palette.highlight required property string modelData + property bool showImage: modelData.startsWith("mxc://") - visible: room ? room.permissions.canSend(MtxEvent.Reaction) : false + //Layout.preferredHeight: fontMetrics.height + Layout.alignment: Qt.AlignBottom + focusPolicy: Qt.NoFocus + height: showImage ? 16 : buttonText.implicitHeight + implicitHeight: showImage ? 16 : buttonText.implicitHeight + implicitWidth: showImage ? 16 : buttonText.implicitWidth + width: showImage ? 16 : buttonText.implicitWidth - Layout.preferredHeight: fontMetrics.height - font.family: Settings.emojiFont - - text: modelData onClicked: { room.input.reaction(row.model.eventId, modelData); TimelineManager.focusMessageInput(); } + + Label { + id: buttonText + + anchors.centerIn: parent + color: button.hovered ? button.highlightColor : button.buttonTextColor + font.family: Settings.emojiFont + horizontalAlignment: Text.AlignHCenter + padding: 0 + text: button.modelData + verticalAlignment: Text.AlignVCenter + visible: !button.showImage + } + Image { + id: buttonImg + + // Workaround, can't get icon.source working for now... + anchors.fill: parent + fillMode: Image.PreserveAspectFit + source: button.showImage ? (button.modelData.replace("mxc://", "image://MxcImage/") + "?scale") : "" + sourceSize.height: button.height + sourceSize.width: button.width + } + NhekoCursorShape { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + } + Ripple { + color: Qt.rgba(buttonTextColor.r, buttonTextColor.g, buttonTextColor.b, 0.5) + } } } - ImageButton { - visible: !!row.model && row.model.isEditable - buttonTextColor: Nheko.colors.buttonText - width: 16 - hoverEnabled: true - image: ":/icons/icons/ui/edit.svg" - ToolTip.visible: hovered ToolTip.delay: Nheko.tooltipDelay ToolTip.text: qsTr("Edit") + ToolTip.visible: hovered + buttonTextColor: palette.buttonText + hoverEnabled: true + image: ":/icons/icons/ui/edit.svg" + visible: !!row.model && row.model.isEditable + width: 16 + onClicked: { - if (row.model.isEditable) room.edit = row.model.eventId; + if (row.model.isEditable) + room.edit = row.model.eventId; } } - ImageButton { id: reactButton - visible: room ? room.permissions.canSend(MtxEvent.Reaction) : false - width: 16 - hoverEnabled: true - image: ":/icons/icons/ui/smile-add.svg" - ToolTip.visible: hovered ToolTip.delay: Nheko.tooltipDelay ToolTip.text: qsTr("React") - onClicked: emojiPopup.visible ? emojiPopup.close() : emojiPopup.show(reactButton, room.roomId, function(plaintext, markdown) { - var event_id = row.model ? row.model.eventId : ""; - room.input.reaction(event_id, plaintext); - TimelineManager.focusMessageInput(); - }) - } - - ImageButton { - visible: room ? room.permissions.canSend(MtxEvent.TextMessage) : false - width: 16 - hoverEnabled: true - image: (row.model && row.model.threadId) ? ":/icons/icons/ui/thread.svg" : ":/icons/icons/ui/new-thread.svg" ToolTip.visible: hovered + hoverEnabled: true + image: ":/icons/icons/ui/smile-add.svg" + visible: room ? room.permissions.canSend(MtxEvent.Reaction) : false + width: 16 + + onClicked: emojiPopup.visible ? emojiPopup.close() : emojiPopup.show(reactButton, room.roomId, function (plaintext, markdown) { + var event_id = row.model ? row.model.eventId : ""; + room.input.reaction(event_id, plaintext); + TimelineManager.focusMessageInput(); + }) + } + ImageButton { ToolTip.delay: Nheko.tooltipDelay ToolTip.text: (row.model && row.model.threadId) ? qsTr("Reply in thread") : qsTr("New thread") - onClicked: room.thread = (row.model.threadId || row.model.eventId) - } - - ImageButton { + ToolTip.visible: hovered + hoverEnabled: true + image: (row.model && row.model.threadId) ? ":/icons/icons/ui/thread.svg" : ":/icons/icons/ui/new-thread.svg" visible: room ? room.permissions.canSend(MtxEvent.TextMessage) : false width: 16 - hoverEnabled: true - image: ":/icons/icons/ui/reply.svg" - ToolTip.visible: hovered + + onClicked: room.thread = (row.model.threadId || row.model.eventId) + } + ImageButton { ToolTip.delay: Nheko.tooltipDelay ToolTip.text: qsTr("Reply") + ToolTip.visible: hovered + hoverEnabled: true + image: ":/icons/icons/ui/reply.svg" + visible: room ? room.permissions.canSend(MtxEvent.TextMessage) : false + width: 16 + onClicked: room.reply = row.model.eventId } - ImageButton { - visible: !!row.model && filteredTimeline.filterByContent - buttonTextColor: Nheko.colors.buttonText - width: 16 - hoverEnabled: true - image: ":/icons/icons/ui/go-to.svg" - ToolTip.visible: hovered ToolTip.delay: Nheko.tooltipDelay ToolTip.text: qsTr("Go to message") + ToolTip.visible: hovered + buttonTextColor: palette.buttonText + hoverEnabled: true + image: ":/icons/icons/ui/go-to.svg" + visible: !!row.model && filteredTimeline.filterByContent + width: 16 + onClicked: { topBar.searchString = ""; room.showEvent(row.model.eventId); } } - ImageButton { id: optionsButton - width: 16 - hoverEnabled: true - image: ":/icons/icons/ui/options.svg" - ToolTip.visible: hovered ToolTip.delay: Nheko.tooltipDelay ToolTip.text: qsTr("Options") + ToolTip.visible: hovered + hoverEnabled: true + image: ":/icons/icons/ui/options.svg" + width: 16 + onClicked: messageContextMenu.show(row.model.eventId, row.model.threadId, row.model.type, row.model.isSender, row.model.isEncrypted, row.model.isEditable, "", row.model.body, optionsButton) } - } - } - - ScrollHelper { - flickable: parent - anchors.fill: parent - } - Shortcut { sequence: StandardKey.MoveToPreviousPage + onActivated: { chat.contentY = chat.contentY - chat.height * 0.9; chat.returnToBounds(); } } - Shortcut { sequence: StandardKey.MoveToNextPage + onActivated: { chat.contentY = chat.contentY + chat.height * 0.9; chat.returnToBounds(); } } - Shortcut { sequence: StandardKey.Cancel + onActivated: { - if(room.input.uploads.length > 0) + if (room.input.uploads.length > 0) room.input.declineUploads(); - else if(room.reply) + else if (room.reply) room.reply = undefined; else if (room.edit) room.edit = undefined; else - room.thread = undefined + room.thread = undefined; TimelineManager.focusMessageInput(); } } @@ -247,19 +460,20 @@ Item { // Better solution welcome. Shortcut { sequence: "Alt+Up" + onActivated: room.reply = room.indexToId(room.reply ? room.idToIndex(room.reply) + 1 : 0) } - Shortcut { sequence: "Alt+Down" + onActivated: { var idx = room.reply ? room.idToIndex(room.reply) - 1 : -1; room.reply = idx >= 0 ? room.indexToId(idx) : null; } } - Shortcut { sequence: "Alt+F" + onActivated: { if (room.reply) { var forwardMess = forwardCompleterComponent.createObject(timelineRoot); @@ -270,355 +484,162 @@ Item { } } } - Shortcut { sequence: "Ctrl+E" + onActivated: { room.edit = room.reply; } } - - Window.onActiveChanged: readTimer.running = Window.active - Timer { id: readTimer + interval: 1000 + // force current read index to update onTriggered: { if (room) - room.setCurrentIndex(room.currentIndex); - + room.setCurrentIndex(room.currentIndex); } - interval: 1000 } - Component { id: sectionHeader Column { - topPadding: userName_.visible? 4: 0 - bottomPadding: Settings.bubbles? (isSender && previousMessageDay == day? 0 : 2) : 3 + bottomPadding: Settings.bubbles ? (isSender && previousMessageDay == day ? 0 : 2) : 3 spacing: 8 + topPadding: userName_.visible ? 4 : 0 visible: (previousMessageUserId !== userId || previousMessageDay !== day || isStateEvent !== previousMessageIsStateEvent) width: parentWidth - height: ((previousMessageDay !== day) ? dateBubble.height : 0) + (isStateEvent? 0 : userName.height +8 ) Label { id: dateBubble anchors.horizontalCenter: parent ? parent.horizontalCenter : undefined - visible: room && previousMessageDay !== day - text: room ? room.formatDateSeparator(timestamp) : "" - color: Nheko.colors.text + color: palette.text height: Math.round(fontMetrics.height * 1.4) - width: contentWidth * 1.2 horizontalAlignment: Text.AlignHCenter + text: room ? room.formatDateSeparator(timestamp) : "" verticalAlignment: Text.AlignVCenter + visible: room && previousMessageDay !== day + width: contentWidth * 1.2 background: Rectangle { + color: palette.window radius: parent.height / 2 - color: Nheko.colors.window } - } - Row { + id: userInfo + + property int remainingWidth: chat.delegateMaxWidth - spacing - messageUserAvatar.width + height: userName_.height spacing: 8 visible: !isStateEvent && (!isSender || !Settings.bubbles) - id: userInfo Avatar { id: messageUserAvatar - width: Nheko.avatarSize * (Settings.smallAvatars? 0.5 : 1) - height: Nheko.avatarSize * (Settings.smallAvatars? 0.5 : 1) - url: !room ? "" : room.avatarUrl(userId).replace("mxc://", "image://MxcImage/") - displayName: userName - userid: userId - onClicked: room.openUserProfile(userId) - ToolTip.visible: messageUserAvatar.hovered ToolTip.delay: Nheko.tooltipDelay ToolTip.text: userid - } + ToolTip.visible: messageUserAvatar.hovered + displayName: userName + height: Nheko.avatarSize * (Settings.smallAvatars ? 0.5 : 1) + url: !room ? "" : room.avatarUrl(userId).replace("mxc://", "image://MxcImage/") + userid: userId + width: Nheko.avatarSize * (Settings.smallAvatars ? 0.5 : 1) + onClicked: room.openUserProfile(userId) + } Connections { function onRoomAvatarUrlChanged() { messageUserAvatar.url = room.avatarUrl(userId).replace("mxc://", "image://MxcImage/"); } - function onScrollToIndex(index) { chat.positionViewAtIndex(index, ListView.Center); } target: room } - property int remainingWidth: chat.delegateMaxWidth - spacing - messageUserAvatar.width AbstractButton { id: userNameButton - contentItem: ElidedLabel { - id: userName_ - fullText: userName - color: TimelineManager.userColor(userId, Nheko.colors.base) - textFormat: Text.RichText - elideWidth: Math.min(userInfo.remainingWidth-Math.min(statusMsg.implicitWidth,userInfo.remainingWidth/3), userName_.fullTextWidth) - } - ToolTip.visible: hovered + ToolTip.delay: Nheko.tooltipDelay ToolTip.text: userId - onClicked: room.openUserProfile(userId) + ToolTip.visible: hovered leftInset: 0 - rightInset: 0 leftPadding: 0 + rightInset: 0 rightPadding: 0 - CursorShape { + contentItem: Label { + id: userName_ + + color: TimelineManager.userColor(userId, palette.base) + text: TimelineManager.escapeEmoji(userNameTextMetrics.elidedText) + textFormat: Text.RichText + } + + onClicked: room.openUserProfile(userId) + + TextMetrics { + id: userNameTextMetrics + + elide: Text.ElideRight + elideWidth: userInfo.remainingWidth - Math.min(statusMsg.implicitWidth, userInfo.remainingWidth / 3) + text: userName + } + NhekoCursorShape { anchors.fill: parent cursorShape: Qt.PointingHandCursor } - } - Label { id: statusMsg - anchors.baseline: userNameButton.baseline - color: Nheko.colors.buttonText - text: userStatus.replace(/\n/g, " ") - textFormat: Text.PlainText - elide: Text.ElideRight - width: Math.min(implicitWidth, userInfo.remainingWidth - userName_.width - parent.spacing) - font.italic: true - font.pointSize: Math.floor(fontMetrics.font.pointSize * 0.8) + + property string userStatus: Presence.userStatus(userId) + + ToolTip.delay: Nheko.tooltipDelay ToolTip.text: qsTr("%1's status message").arg(userName) ToolTip.visible: statusMsgHoverHandler.hovered - ToolTip.delay: Nheko.tooltipDelay + anchors.baseline: userNameButton.baseline + color: palette.buttonText + elide: Text.ElideRight + font.italic: true + font.pointSize: Math.floor(fontMetrics.font.pointSize * 0.8) + text: userStatus.replace(/\n/g, " ") + textFormat: Text.PlainText + width: Math.min(implicitWidth, userInfo.remainingWidth - userName_.width - parent.spacing) HoverHandler { id: statusMsgHoverHandler - } - property string userStatus: Presence.userStatus(userId) + } Connections { - target: Presence function onPresenceChanged(id) { - if (id == userId) statusMsg.userStatus = Presence.userStatus(userId); + if (id == userId) + statusMsg.userStatus = Presence.userStatus(userId); } - } - } - } - - } - - } - - delegate: Item { - id: wrapper - - required property double proportionalHeight - required property int type - required property string typeString - required property int originalWidth - required property string blurhash - required property string body - required property string formattedBody - required property string eventId - required property string filename - required property string filesize - required property string url - required property string thumbnailUrl - required property string duration - required property bool isOnlyEmoji - required property bool isSender - required property bool isEncrypted - required property bool isEditable - required property bool isEdited - required property bool isStateEvent - property bool previousMessageIsStateEvent: (index + 1) >= chat.count ? true : chat.model.dataByIndex(index+1, Room.IsStateEvent) - required property string replyTo - required property string threadId - required property string userId - required property string roomTopic - required property string roomName - required property string callType - required property var reactions - required property int trustlevel - required property int notificationlevel - required property int encryptionError - required property var timestamp - required property int status - required property int index - required property int relatedEventCacheBuster - required property var day - property string previousMessageUserId: (index + 1) >= chat.count ? "" : chat.model.dataByIndex(index+1, Room.UserId) - property var previousMessageDay: (index + 1) >= chat.count ? 0 : chat.model.dataByIndex(index+1, Room.Day) - required property string userName - property bool scrolledToThis: eventId === room.scrollTarget && (y + height > chat.y + chat.contentY && y < chat.y + chat.height + chat.contentY) - - anchors.horizontalCenter: parent ? parent.horizontalCenter : undefined - width: chat.delegateMaxWidth - height: section.active ? section.height + timelinerow.height : timelinerow.height - ListView.delayRemove: true - - Loader { - id: section - - property int parentWidth: parent.width - property string userId: wrapper.userId - property string previousMessageUserId: wrapper.previousMessageUserId - property var day: wrapper.day - property var previousMessageDay: wrapper.previousMessageDay - property bool previousMessageIsStateEvent: wrapper.previousMessageIsStateEvent - property bool isStateEvent: wrapper.isStateEvent - property bool isSender: wrapper.isSender - property string userName: wrapper.userName - property date timestamp: wrapper.timestamp - - z: 4 - active: previousMessageUserId !== userId || previousMessageDay !== day || previousMessageIsStateEvent !== isStateEvent - //asynchronous: true - sourceComponent: sectionHeader - visible: status == Loader.Ready - } - - TimelineRow { - id: timelinerow - - proportionalHeight: wrapper.proportionalHeight - type: chat.model, wrapper.type - typeString: wrapper.typeString - originalWidth: wrapper.originalWidth - blurhash: wrapper.blurhash - body: wrapper.body - formattedBody: wrapper.formattedBody - eventId: chat.model, wrapper.eventId - filename: wrapper.filename - filesize: wrapper.filesize - url: wrapper.url - thumbnailUrl: wrapper.thumbnailUrl - duration: wrapper.duration - isOnlyEmoji: wrapper.isOnlyEmoji - isSender: wrapper.isSender - isEncrypted: wrapper.isEncrypted - isEditable: wrapper.isEditable - isEdited: wrapper.isEdited - isStateEvent: wrapper.isStateEvent - replyTo: wrapper.replyTo - threadId: wrapper.threadId - userId: wrapper.userId - userName: wrapper.userName - roomTopic: wrapper.roomTopic - roomName: wrapper.roomName - callType: wrapper.callType - reactions: wrapper.reactions - trustlevel: wrapper.trustlevel - notificationlevel: wrapper.notificationlevel - encryptionError: wrapper.encryptionError - timestamp: wrapper.timestamp - status: wrapper.status - index: wrapper.index - relatedEventCacheBuster: wrapper.relatedEventCacheBuster - y: section.visible && section.active ? section.y + section.height : 0 - - onHoveredChanged: { - if (!Settings.mobileMode && hovered) { - if (!messageActions.hovered) { - messageActions.attached = timelinerow; - messageActions.model = timelinerow; + target: Presence } } } - background: Rectangle { - id: scrollHighlight - - opacity: 0 - visible: true - z: 1 - enabled: false - color: Nheko.colors.highlight - - states: State { - name: "revealed" - when: wrapper.scrolledToThis - } - - transitions: Transition { - from: "" - to: "revealed" - - SequentialAnimation { - PropertyAnimation { - target: scrollHighlight - properties: "opacity" - easing.type: Easing.InOutQuad - from: 0 - to: 1 - duration: 500 - } - - PropertyAnimation { - target: scrollHighlight - properties: "opacity" - easing.type: Easing.InOutQuad - from: 1 - to: 0 - duration: 500 - } - - ScriptAction { - script: room.eventShown() - } - - } - - } - - } } - - Connections { - function onMovementEnded() { - if (y + height + 2 * chat.spacing > chat.contentY + chat.height && y < chat.contentY + chat.height) - chat.model.currentIndex = index; - - } - - target: chat - } - - } - - footer: Item { - anchors.horizontalCenter: parent.horizontalCenter - anchors.margins: Nheko.paddingLarge - visible: (room && room.paginationInProgress) || chat.filteringInProgress - // hacky, but works - height: loadingSpinner.height + 2 * Nheko.paddingLarge - - Spinner { - id: loadingSpinner - - anchors.centerIn: parent - anchors.margins: Nheko.paddingLarge - running: (room && room.paginationInProgress) || chat.filteringInProgress - foreground: Nheko.colors.mid - z: 3 - } - } } - Platform.Menu { id: messageContextMenu property string eventId - property string threadId + property int eventType + property bool isEditable + property bool isEncrypted + property bool isSender property string link property string text - property int eventType - property bool isEncrypted - property bool isEditable - property bool isSender + property string threadId function show(eventId_, threadId_, eventType_, isSender_, isEncrypted_, isEditable_, link_, text_, showAt_) { eventId = eventId_; @@ -628,104 +649,106 @@ Item { isEditable = isEditable_; isSender = isSender_; if (text_) - text = text_; + text = text_; else - text = ""; + text = ""; if (link_) - link = link_; + link = link_; else - link = ""; + link = ""; if (showAt_) - open(showAt_); + open(showAt_); else - open(); + open(); } Component { id: removeReason + InputDialog { id: removeReasonDialog property string eventId - title: qsTr("Reason for removal") prompt: qsTr("Enter reason for removal or hit enter for no reason:") - onAccepted: function(text) { + title: qsTr("Reason for removal") + + onAccepted: function (text) { room.redactEvent(eventId, text); } } } - Platform.MenuItem { - visible: filteredTimeline.filterByContent - enabled: visible - text: qsTr("Go to &message") - onTriggered: function() { + enabled: visible + text: qsTr("Go to &message") + visible: filteredTimeline.filterByContent + + onTriggered: function () { topBar.searchString = ""; room.showEvent(messageContextMenu.eventId); } - } - + } Platform.MenuItem { - visible: messageContextMenu.text enabled: visible text: qsTr("&Copy") + visible: messageContextMenu.text + onTriggered: Clipboard.text = messageContextMenu.text } - Platform.MenuItem { - visible: messageContextMenu.link enabled: visible text: qsTr("Copy &link location") + visible: messageContextMenu.link + onTriggered: Clipboard.text = messageContextMenu.link } - Platform.MenuItem { id: reactionOption - visible: room ? room.permissions.canSend(MtxEvent.Reaction) : false text: qsTr("Re&act") - onTriggered: emojiPopup.visible ? emojiPopup.close() : emojiPopup.show(null, room.roomId, function(plaintext, markdown) { - room.input.reaction(messageContextMenu.eventId, plaintext); - TimelineManager.focusMessageInput(); - }) - } + visible: room ? room.permissions.canSend(MtxEvent.Reaction) : false + onTriggered: emojiPopup.visible ? emojiPopup.close() : emojiPopup.show(null, room.roomId, function (plaintext, markdown) { + room.input.reaction(messageContextMenu.eventId, plaintext); + TimelineManager.focusMessageInput(); + }) + } Platform.MenuItem { - visible: room ? room.permissions.canSend(MtxEvent.TextMessage) : false text: qsTr("Repl&y") + visible: room ? room.permissions.canSend(MtxEvent.TextMessage) : false + onTriggered: room.reply = (messageContextMenu.eventId) } - Platform.MenuItem { - visible: messageContextMenu.isEditable && (room ? room.permissions.canSend(MtxEvent.TextMessage) : false) enabled: visible text: qsTr("&Edit") + visible: messageContextMenu.isEditable && (room ? room.permissions.canSend(MtxEvent.TextMessage) : false) + onTriggered: room.edit = (messageContextMenu.eventId) } - Platform.MenuItem { - visible: (room ? room.permissions.canSend(MtxEvent.TextMessage) : false) enabled: visible text: qsTr("&Thread") + visible: (room ? room.permissions.canSend(MtxEvent.TextMessage) : false) + onTriggered: room.thread = (messageContextMenu.threadId || messageContextMenu.eventId) } - Platform.MenuItem { - visible: (room ? room.permissions.canChange(MtxEvent.PinnedEvents) : false) enabled: visible text: visible && room.pinnedMessages.includes(messageContextMenu.eventId) ? qsTr("Un&pin") : qsTr("&Pin") + visible: (room ? room.permissions.canChange(MtxEvent.PinnedEvents) : false) + onTriggered: visible && room.pinnedMessages.includes(messageContextMenu.eventId) ? room.unpin(messageContextMenu.eventId) : room.pin(messageContextMenu.eventId) } - Platform.MenuItem { text: qsTr("&Read receipts") + onTriggered: room.showReadReceipts(messageContextMenu.eventId) } - Platform.MenuItem { - visible: messageContextMenu.eventType == MtxEvent.ImageMessage || messageContextMenu.eventType == MtxEvent.VideoMessage || messageContextMenu.eventType == MtxEvent.AudioMessage || messageContextMenu.eventType == MtxEvent.FileMessage || messageContextMenu.eventType == MtxEvent.Sticker || messageContextMenu.eventType == MtxEvent.TextMessage || messageContextMenu.eventType == MtxEvent.LocationMessage || messageContextMenu.eventType == MtxEvent.EmoteMessage || messageContextMenu.eventType == MtxEvent.NoticeMessage text: qsTr("&Forward") + visible: messageContextMenu.eventType == MtxEvent.ImageMessage || messageContextMenu.eventType == MtxEvent.VideoMessage || messageContextMenu.eventType == MtxEvent.AudioMessage || messageContextMenu.eventType == MtxEvent.FileMessage || messageContextMenu.eventType == MtxEvent.Sticker || messageContextMenu.eventType == MtxEvent.TextMessage || messageContextMenu.eventType == MtxEvent.LocationMessage || messageContextMenu.eventType == MtxEvent.EmoteMessage || messageContextMenu.eventType == MtxEvent.NoticeMessage + onTriggered: { var forwardMess = forwardCompleterComponent.createObject(timelineRoot); forwardMess.setMessageEventId(messageContextMenu.eventId); @@ -733,28 +756,27 @@ Item { timelineRoot.destroyOnClose(forwardMess); } } - Platform.MenuItem { text: qsTr("&Mark as read") } - Platform.MenuItem { text: qsTr("View raw message") + onTriggered: room.viewRawMessage(messageContextMenu.eventId) } - Platform.MenuItem { - // TODO(Nico): Fix this still being iterated over, when using keyboard to select options - visible: messageContextMenu.isEncrypted enabled: visible text: qsTr("View decrypted raw message") + // TODO(Nico): Fix this still being iterated over, when using keyboard to select options + visible: messageContextMenu.isEncrypted + onTriggered: room.viewDecryptedRawMessage(messageContextMenu.eventId) } - Platform.MenuItem { - visible: (room ? room.permissions.canRedact() : false) || messageContextMenu.isSender text: qsTr("Remo&ve message") - onTriggered: function() { + visible: (room ? room.permissions.canRedact() : false) || messageContextMenu.isSender + + onTriggered: function () { var dialog = removeReason.createObject(timelineRoot); dialog.eventId = messageContextMenu.eventId; dialog.show(); @@ -762,44 +784,40 @@ Item { timelineRoot.destroyOnClose(dialog); } } - Platform.MenuItem { - visible: messageContextMenu.eventType == MtxEvent.ImageMessage || messageContextMenu.eventType == MtxEvent.VideoMessage || messageContextMenu.eventType == MtxEvent.AudioMessage || messageContextMenu.eventType == MtxEvent.FileMessage || messageContextMenu.eventType == MtxEvent.Sticker enabled: visible text: qsTr("&Save as") + visible: messageContextMenu.eventType == MtxEvent.ImageMessage || messageContextMenu.eventType == MtxEvent.VideoMessage || messageContextMenu.eventType == MtxEvent.AudioMessage || messageContextMenu.eventType == MtxEvent.FileMessage || messageContextMenu.eventType == MtxEvent.Sticker + onTriggered: room.saveMedia(messageContextMenu.eventId) } - Platform.MenuItem { - visible: messageContextMenu.eventType == MtxEvent.ImageMessage || messageContextMenu.eventType == MtxEvent.VideoMessage || messageContextMenu.eventType == MtxEvent.AudioMessage || messageContextMenu.eventType == MtxEvent.FileMessage || messageContextMenu.eventType == MtxEvent.Sticker enabled: visible text: qsTr("&Open in external program") + visible: messageContextMenu.eventType == MtxEvent.ImageMessage || messageContextMenu.eventType == MtxEvent.VideoMessage || messageContextMenu.eventType == MtxEvent.AudioMessage || messageContextMenu.eventType == MtxEvent.FileMessage || messageContextMenu.eventType == MtxEvent.Sticker + onTriggered: room.openMedia(messageContextMenu.eventId) } - Platform.MenuItem { - visible: messageContextMenu.eventId enabled: visible text: qsTr("Copy link to eve&nt") + visible: messageContextMenu.eventId + onTriggered: room.copyLinkToEvent(messageContextMenu.eventId) } - } - Component { id: forwardCompleterComponent ForwardCompleter { } - } - Platform.Menu { id: replyContextMenu - property string text - property string link property string eventId + property string link + property string text function show(text_, link_, eventId_) { text = text_; @@ -809,85 +827,100 @@ Item { } Platform.MenuItem { - visible: replyContextMenu.text enabled: visible text: qsTr("&Copy") + visible: replyContextMenu.text + onTriggered: Clipboard.text = replyContextMenu.text } - Platform.MenuItem { - visible: replyContextMenu.link enabled: visible text: qsTr("Copy &link location") + visible: replyContextMenu.link + onTriggered: Clipboard.text = replyContextMenu.link } - Platform.MenuItem { - visible: true enabled: visible text: qsTr("&Go to quoted message") + visible: true + onTriggered: room.showEvent(replyContextMenu.eventId) } - } RoundButton { id: toEndButton - anchors { - bottom: parent.bottom - right: scrollbar.left - bottomMargin: Nheko.paddingMedium+(fullWidth-width)/2 - rightMargin: Nheko.paddingMedium+(fullWidth-width)/2 - } + property int fullWidth: 40 - width: 0 - height: width - radius: width/2 - onClicked: function() { chat.positionViewAtBeginning(); TimelineManager.focusMessageInput(); } + flat: true + height: width hoverEnabled: true + radius: width / 2 + width: 0 background: Rectangle { - color: toEndButton.down ? Nheko.colors.highlight : Nheko.colors.button - opacity: enabled ? 1 : 0.3 - border.color: toEndButton.hovered ? Nheko.colors.highlight : Nheko.colors.buttonText + border.color: toEndButton.hovered ? palette.highlight : palette.buttonText border.width: 1 + color: toEndButton.down ? palette.highlight : palette.button + opacity: enabled ? 1 : 0.3 radius: toEndButton.radius } - states: [ State { name: "" - PropertyChanges { target: toEndButton; width: 0 } + + PropertyChanges { + target: toEndButton + width: 0 + } }, State { name: "shown" when: !chat.atYEnd - PropertyChanges { target: toEndButton; width: toEndButton.fullWidth } + + PropertyChanges { + target: toEndButton + width: toEndButton.fullWidth + } } ] - - Image { - id: buttonImg - anchors.fill: parent - anchors.margins: Nheko.paddingMedium - source: "image://colorimage/:/icons/icons/ui/download.svg?" + (toEndButton.down ? Nheko.colors.highlightedText : Nheko.colors.buttonText) - fillMode: Image.PreserveAspectFit - } - transitions: Transition { from: "" - to: "shown" reversible: true + to: "shown" SequentialAnimation { - PauseAnimation { duration: 500 } + PauseAnimation { + duration: 500 + } PropertyAnimation { - target: toEndButton - properties: "width" - easing.type: Easing.InOutQuad duration: 200 + easing.type: Easing.InOutQuad + properties: "width" + target: toEndButton } } } + + onClicked: function () { + chat.positionViewAtBeginning(); + TimelineManager.focusMessageInput(); + } + + anchors { + bottom: parent.bottom + bottomMargin: Nheko.paddingMedium + (fullWidth - width) / 2 + right: scrollbar.left + rightMargin: Nheko.paddingMedium + (fullWidth - width) / 2 + } + Image { + id: buttonImg + + anchors.fill: parent + anchors.margins: Nheko.paddingMedium + fillMode: Image.PreserveAspectFit + source: "image://colorimage/:/icons/icons/ui/download.svg?" + (toEndButton.down ? palette.highlightedText : palette.buttonText) + } } } diff --git a/resources/qml/PrivacyScreen.qml b/resources/qml/PrivacyScreen.qml index 5967f25d..a3539df7 100644 --- a/resources/qml/PrivacyScreen.qml +++ b/resources/qml/PrivacyScreen.qml @@ -2,18 +2,17 @@ // // SPDX-License-Identifier: GPL-3.0-or-later -import QtGraphicalEffects 1.0 -import QtQuick 2.12 -import QtQuick.Window 2.2 -import im.nheko 1.0 +import QtQuick +import QtQuick.Window +import im.nheko +import QtQuick.Effects Item { id: privacyScreen readonly property bool active: Settings.privacyScreen && screenSaver.state === "Visible" - property var timelineRoot property int screenTimeout - + property var timelineRoot required property var windowTarget Connections { @@ -24,29 +23,28 @@ Item { } else { if (timelineRoot.visible) screenSaverTimer.start(); - } } target: windowTarget } - Timer { id: screenSaverTimer interval: screenTimeout * 1000 running: !windowTarget.active + onTriggered: { screenSaver.state = "Visible"; } } - Item { id: screenSaver - state: "Invisible" anchors.fill: parent + state: "Invisible" visible: false + states: [ State { name: "Visible" @@ -55,20 +53,18 @@ Item { target: screenSaver visible: true } - PropertyChanges { - target: screenSaver opacity: 1 + target: screenSaver } }, State { name: "Invisible" PropertyChanges { - target: screenSaver opacity: 0 + target: screenSaver } - PropertyChanges { target: screenSaver visible: false @@ -78,36 +74,33 @@ Item { transitions: [ Transition { from: "Invisible" - to: "Visible" reversible: true + to: "Visible" SequentialAnimation { NumberAnimation { - target: screenSaver - property: "visible" duration: 0 - } - - NumberAnimation { + property: "visible" target: screenSaver - property: "opacity" + } + NumberAnimation { duration: 300 easing.type: Easing.Linear + property: "opacity" + target: screenSaver } - } - } ] - FastBlur { + MultiEffect { id: blur anchors.fill: parent + blur: 1.0 + blurEnabled: true + blurMax: 32 source: timelineRoot - radius: 50 } - } - } diff --git a/resources/qml/QuickSwitcher.qml b/resources/qml/QuickSwitcher.qml index 916a4cee..67718ecb 100644 --- a/resources/qml/QuickSwitcher.qml +++ b/resources/qml/QuickSwitcher.qml @@ -11,84 +11,83 @@ Popup { id: quickSwitcher property int textHeight: Math.round(Qt.application.font.pixelSize * 2.4) + property int textMargin: Nheko.paddingSmall background: null - width: Math.min(Math.max(Math.round(parent.width / 2),450),parent.width) // limiting width to parent.width/2 can be a bit narrow + closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside + modal: true + + // Workaround palettes not inheriting for popups + palette: timelineRoot.palette + parent: Overlay.overlay + width: Math.min(Math.max(Math.round(parent.width / 2), 450), parent.width) // limiting width to parent.width/2 can be a bit narrow x: Math.round(parent.width / 2 - contentWidth / 2) y: Math.round(parent.height / 4) - modal: true - closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside - parent: Overlay.overlay - palette: Nheko.colors + + Overlay.modal: Rectangle { + color: "#aa1E1E1E" + } + + onClosed: TimelineManager.focusMessageInput() onOpened: { roomTextInput.forceActiveFocus(); } - onClosed: TimelineManager.focusMessageInput() - property int textMargin: Nheko.paddingSmall - Column{ + Column { anchors.fill: parent spacing: 1 MatrixTextField { id: roomTextInput - width: parent.width + color: palette.text font.pixelSize: Math.ceil(quickSwitcher.textHeight * 0.6) - color: Nheko.colors.text - onTextEdited: { - completerPopup.completer.searchString = text; - } - Keys.onPressed: { + width: parent.width + + Keys.onPressed: event => { if (event.key == Qt.Key_Up || event.key == Qt.Key_Backtab) { event.accepted = true; completerPopup.up(); } else if (event.key == Qt.Key_Down || event.key == Qt.Key_Tab) { event.accepted = true; if (event.key == Qt.Key_Tab && (event.modifiers & Qt.ShiftModifier)) - completerPopup.up(); + completerPopup.up(); else - completerPopup.down(); + completerPopup.down(); } else if (event.matches(StandardKey.InsertParagraphSeparator)) { completerPopup.finishCompletion(); event.accepted = true; } } + onTextEdited: { + completerPopup.completer.searchString = text; + } } - Completer { id: completerPopup - visible: roomTextInput.text.length > 0 - width: parent.width - completerName: "room" - bottomToTop: false - fullWidth: true avatarHeight: quickSwitcher.textHeight avatarWidth: quickSwitcher.textHeight + bottomToTop: false centerRowContent: false + completerName: "room" + fullWidth: true rowMargin: Math.round(quickSwitcher.textMargin / 2) rowSpacing: quickSwitcher.textMargin + visible: roomTextInput.text.length > 0 + width: parent.width } } - Connections { function onCompletionSelected(id) { Rooms.setCurrentRoom(id); quickSwitcher.close(); } - function onCountChanged() { if (completerPopup.count > 0 && (completerPopup.currentIndex < 0 || completerPopup.currentIndex >= completerPopup.count)) - completerPopup.currentIndex = 0; - + completerPopup.currentIndex = 0; } target: completerPopup } - - Overlay.modal: Rectangle { - color: "#aa1E1E1E" - } - } diff --git a/resources/qml/Reactions.qml b/resources/qml/Reactions.qml index cce8720a..5ab58beb 100644 --- a/resources/qml/Reactions.qml +++ b/resources/qml/Reactions.qml @@ -11,10 +11,11 @@ import im.nheko 1.0 Flow { id: reactionFlow - // lower-contrast colors to avoid distracting from text & to enhance hover effect - property color gentleHighlight: Qt.hsla(Nheko.colors.highlight.hslHue, Nheko.colors.highlight.hslSaturation, Nheko.colors.highlight.hslLightness, 0.8) - property color gentleText: Qt.hsla(Nheko.colors.text.hslHue, Nheko.colors.text.hslSaturation, Nheko.colors.text.hslLightness, 0.6) property string eventId + + // lower-contrast colors to avoid distracting from text & to enhance hover effect + property color gentleHighlight: Qt.hsla(palette.highlight.hslHue, palette.highlight.hslSaturation, palette.highlight.hslLightness, 0.8) + property color gentleText: Qt.hsla(palette.text.hslHue, palette.text.hslSaturation, palette.text.hslLightness, 0.6) property alias reactions: repeater.model spacing: 4 @@ -25,40 +26,39 @@ Flow { delegate: AbstractButton { id: reaction - hoverEnabled: true - ToolTip.visible: hovered ToolTip.delay: Nheko.tooltipDelay - onClicked: { - console.debug("Picked " + modelData.key + "in response to " + reactionFlow.eventId + ". selfReactedEvent: " + modelData.selfReactedEvent); - room.input.reaction(reactionFlow.eventId, modelData.key); - } - Component.onCompleted: { - ToolTip.text = Qt.binding(function() { - if (textMetrics.elidedText === textMetrics.text) { - return modelData.users; - } - return modelData.displayKey + "\n" + modelData.users; - }) - } - leftPadding: textMetrics.height / 2 - rightPadding: textMetrics.height / 2 + ToolTip.visible: hovered + hoverEnabled: true + leftPadding: textMetrics.height / 2 + rightPadding: textMetrics.height / 2 + background: Rectangle { + anchors.centerIn: parent + border.color: reaction.hovered ? palette.text : gentleText + border.width: 1 + color: reaction.hovered ? palette.highlight : (modelData.selfReactedEvent !== '' ? gentleHighlight : palette.window) + implicitHeight: reaction.implicitHeight + implicitWidth: reaction.implicitWidth + radius: reaction.height / 2 + } contentItem: Row { spacing: textMetrics.height / 4 TextMetrics { id: textMetrics - font.family: Settings.emojiFont elide: Text.ElideRight elideWidth: 150 + font.family: Settings.emojiFont text: modelData.displayKey } - Text { id: reactionText anchors.baseline: reactionCounter.baseline + color: (reaction.hovered || modelData.selfReactedEvent !== '') ? palette.highlightedText : palette.text + font.family: Settings.emojiFont + maximumLineCount: 1 text: { // When an emoji font is selected that doesn't have …, it is dropped from elidedText. So we add it back. if (textMetrics.elidedText !== modelData.displayKey) { @@ -68,51 +68,45 @@ Flow { } return textMetrics.elidedText; } - font.family: Settings.emojiFont - color: (reaction.hovered || modelData.selfReactedEvent !== '') ? Nheko.colors.highlightedText: Nheko.colors.text - maximumLineCount: 1 visible: !modelData.key.startsWith("mxc://") } Image { anchors.verticalCenter: divider.verticalCenter + fillMode: Image.PreserveAspectFit height: textMetrics.height - width: textMetrics.height source: modelData.key.startsWith("mxc://") ? (modelData.key.replace("mxc://", "image://MxcImage/") + "?scale") : "" visible: modelData.key.startsWith("mxc://") - fillMode: Image.PreserveAspectFit + width: textMetrics.height } - Rectangle { id: divider + color: reaction.hovered ? palette.text : gentleText height: Math.floor(reactionCounter.implicitHeight * 1.4) width: 1 - color: reaction.hovered ? Nheko.colors.text: gentleText } - Text { id: reactionCounter anchors.verticalCenter: divider.verticalCenter - text: modelData.count + color: (reaction.hovered || modelData.selfReactedEvent !== '') ? palette.highlightedText : palette.windowText font: reaction.font - color: (reaction.hovered || modelData.selfReactedEvent !== '') ? Nheko.colors.highlightedText: Nheko.colors.windowText + text: modelData.count } - } - background: Rectangle { - anchors.centerIn: parent - implicitWidth: reaction.implicitWidth - implicitHeight: reaction.implicitHeight - border.color: reaction.hovered ? Nheko.colors.text: gentleText - color: reaction.hovered ? Nheko.colors.highlight : (modelData.selfReactedEvent !== '' ? gentleHighlight : Nheko.colors.window) - border.width: 1 - radius: reaction.height / 2 + Component.onCompleted: { + ToolTip.text = Qt.binding(function () { + if (textMetrics.elidedText === textMetrics.text) { + return modelData.users; + } + return modelData.displayKey + "\n" + modelData.users; + }); + } + onClicked: { + console.debug("Picked " + modelData.key + "in response to " + reactionFlow.eventId + ". selfReactedEvent: " + modelData.selfReactedEvent); + room.input.reaction(reactionFlow.eventId, modelData.key); } - } - } - } diff --git a/resources/qml/ReplyPopup.qml b/resources/qml/ReplyPopup.qml index 365c5bff..ce24297c 100644 --- a/resources/qml/ReplyPopup.qml +++ b/resources/qml/ReplyPopup.qml @@ -12,91 +12,89 @@ Rectangle { id: replyPopup Layout.fillWidth: true - visible: room && (room.reply || room.edit || room.thread) + color: palette.window // Height of child, plus margins, plus border implicitHeight: (room && room.reply ? replyPreview.height : Math.max(closeEditButton.height, closeThreadButton.height)) + Nheko.paddingSmall - color: Nheko.colors.window + visible: room && (room.reply || room.edit || room.thread) z: 3 Reply { id: replyPreview - property var modelData: room ? room.getDump(room.reply, room.id) : { - } + property var modelData: room ? room.getDump(room.reply, room.id) : {} - visible: room && room.reply anchors.left: parent.left - anchors.leftMargin: replyPopup.width < 450? Nheko.paddingSmall : (CallManager.callsSupported? 2*(22+16) : 1*(22+16)) + anchors.leftMargin: replyPopup.width < 450 ? Nheko.paddingSmall : (CallManager.callsSupported ? 2 * (22 + 16) : 1 * (22 + 16)) anchors.right: parent.right - anchors.rightMargin: replyPopup.width < 450? 2*(22+16) : 3*(22+16) + anchors.rightMargin: replyPopup.width < 450 ? 2 * (22 + 16) : 3 * (22 + 16) anchors.top: parent.top anchors.topMargin: Nheko.paddingSmall - userColor: TimelineManager.userColor(modelData.userId, Nheko.colors.window) blurhash: modelData.blurhash ?? "" body: modelData.body ?? "" - formattedBody: modelData.formattedBody ?? "" + encryptionError: modelData.encryptionError ?? 0 eventId: modelData.eventId ?? "" filename: modelData.filename ?? "" filesize: modelData.filesize ?? "" + formattedBody: modelData.formattedBody ?? "" + isOnlyEmoji: modelData.isOnlyEmoji ?? false + originalWidth: modelData.originalWidth ?? 0 proportionalHeight: modelData.proportionalHeight ?? 1 type: modelData.type ?? MtxEvent.UnknownMessage typeString: modelData.typeString ?? "" url: modelData.url ?? "" - originalWidth: modelData.originalWidth ?? 0 - isOnlyEmoji: modelData.isOnlyEmoji ?? false + userColor: TimelineManager.userColor(modelData.userId, palette.window) userId: modelData.userId ?? "" userName: modelData.userName ?? "" - encryptionError: modelData.encryptionError ?? "" + visible: room && room.reply width: parent.width } - ImageButton { id: closeReplyButton - visible: room && room.reply + ToolTip.text: qsTr("Close") + ToolTip.visible: closeReplyButton.hovered + anchors.margins: Nheko.paddingSmall anchors.right: replyPreview.right anchors.top: replyPreview.top - anchors.margins: Nheko.paddingSmall - hoverEnabled: true - width: 16 height: 16 + hoverEnabled: true image: ":/icons/icons/ui/dismiss.svg" - ToolTip.visible: closeReplyButton.hovered - ToolTip.text: qsTr("Close") + visible: room && room.reply + width: 16 + onClicked: room.reply = undefined } - ImageButton { id: closeEditButton - visible: room && room.edit - anchors.right: closeThreadButton.left + ToolTip.text: qsTr("Cancel Edit") + ToolTip.visible: closeEditButton.hovered anchors.margins: 8 + anchors.right: closeThreadButton.left anchors.top: parent.top + height: 22 hoverEnabled: true image: ":/icons/icons/ui/dismiss_edit.svg" + visible: room && room.edit width: 22 - height: 22 - ToolTip.visible: closeEditButton.hovered - ToolTip.text: qsTr("Cancel Edit") + onClicked: room.edit = undefined } - ImageButton { id: closeThreadButton - visible: room && room.thread - anchors.right: parent.right - anchors.margins: 8 - anchors.top: parent.top - hoverEnabled: true - buttonTextColor: room ? TimelineManager.userColor(room.thread, Nheko.colors.base) : Nheko.colors.buttonText - image: ":/icons/icons/ui/dismiss_thread.svg" - width: 22 - height: 22 - ToolTip.visible: closeThreadButton.hovered ToolTip.text: qsTr("Cancel Thread") + ToolTip.visible: closeThreadButton.hovered + anchors.margins: 8 + anchors.right: parent.right + anchors.top: parent.top + buttonTextColor: room ? TimelineManager.userColor(room.thread, palette.base) : palette.buttonText + height: 22 + hoverEnabled: true + image: ":/icons/icons/ui/dismiss_thread.svg" + visible: room && room.thread + width: 22 + onClicked: room.thread = undefined } - } diff --git a/resources/qml/RoomList.qml b/resources/qml/RoomList.qml index 49a36d8f..92e7ef6d 100644 --- a/resources/qml/RoomList.qml +++ b/resources/qml/RoomList.qml @@ -18,454 +18,142 @@ Page { property int avatarSize: Math.ceil(fontMetrics.lineSpacing * 2.3) property bool collapsed: false - // HACK: https://bugreports.qt.io/browse/QTBUG-83972, qtwayland cannot auto hide menu - Connections { - function onHideMenu() { - userInfoMenu.close() - roomContextMenu.close() - } - target: MainWindow - } - - Component { - id: roomDirectoryComponent - - RoomDirectory { - } - - } - - Component { - id: createRoomComponent - - CreateRoom { - } - } - - Component { - id: createDirectComponent - - CreateDirect { - } - } - - ListView { - id: roomlist - - anchors.left: parent.left - anchors.right: parent.right - height: parent.height - model: Rooms - //reuseItems: true - - ScrollBar.vertical: ScrollBar { - id: scrollbar - parent: !collapsed && Settings.scrollbarsInRoomlist ? roomlist : null - } - - ScrollHelper { - flickable: parent - anchors.fill: parent - } - - Connections { - function onCurrentRoomChanged() { - if (Rooms.currentRoom) - roomlist.positionViewAtIndex(Rooms.roomidToIndex(Rooms.currentRoom.roomId), ListView.Contain); - - } - - target: Rooms - } - - Component { - id: roomWindowComponent - - ApplicationWindow { - id: roomWindowW - - property var room: null - property var roomPreview: null - - Component.onCompleted: { - MainWindow.addPerRoomWindow(room.roomId || roomPreview.roomid, roomWindowW); - Nheko.setTransientParent(roomWindowW, null); - } - Component.onDestruction: MainWindow.removePerRoomWindow(room.roomId || roomPreview.roomid, roomWindowW) - - height: 650 - width: 420 - minimumWidth: 150 - minimumHeight: 150 - palette: Nheko.colors - color: Nheko.colors.window - title: room.plainRoomName - //flags: Qt.Window | Qt.WindowCloseButtonHint | Qt.WindowTitleHint - - Shortcut { - sequence: StandardKey.Cancel - onActivated: roomWindowW.close() - } - - TimelineView { - id: timeline - - privacyScreen: privacyScreen - anchors.fill: parent - room: roomWindowW.room - roomPreview: roomWindowW.roomPreview.roomid ? roomWindowW.roomPreview : null - } - - PrivacyScreen { - id: privacyScreen - - anchors.fill: parent - visible: Settings.privacyScreen - screenTimeout: Settings.privacyScreenTimeout - timelineRoot: timeline - windowTarget: roomWindowW - } - - onActiveChanged: { room.lastReadIdOnWindowFocus(); } - } - - } - - - Component { - id: nestedSpaceMenuLevel - - SpaceMenuLevel { - roomid: roomContextMenu.roomid - childMenu: rootSpaceMenu.childMenu - } - } - - - Platform.Menu { - id: roomContextMenu - - property string roomid - property var tags - - function show(roomid_, tags_) { - roomid = roomid_; - tags = tags_; - open(); - } - - InputDialog { - id: newTag - - title: qsTr("New tag") - prompt: qsTr("Enter the tag you want to use:") - onAccepted: function(text) { - Rooms.toggleTag(roomContextMenu.roomid, "u." + text, true); - } - } - - Platform.MenuItem { - text: qsTr("Open separately") - onTriggered: { - var roomWindow = roomWindowComponent.createObject(null, { - "room": Rooms.getRoomById(roomContextMenu.roomid), - "roomPreview": Rooms.getRoomPreviewById(roomContextMenu.roomid) - }); - roomWindow.showNormal(); - destroyOnClose(roomWindow); - } - } - - Platform.MenuItem { - text: qsTr("Room settings") - onTriggered: TimelineManager.openRoomSettings(roomContextMenu.roomid) - } - - Platform.MenuItem { - text: qsTr("Leave room") - onTriggered: TimelineManager.openLeaveRoomDialog(roomContextMenu.roomid) - } - - Platform.MenuItem { - text: qsTr("Copy room link") - onTriggered: Rooms.copyLink(roomContextMenu.roomid) - } - - Platform.Menu { - id: tagsMenu - title: qsTr("Tag room as:") - - Instantiator { - model: Communities.tagsWithDefault - onObjectAdded: tagsMenu.insertItem(index, object) - onObjectRemoved: tagsMenu.removeItem(object) - - delegate: Platform.MenuItem { - property string t: modelData - - text: { - switch (t) { - case "m.favourite": - return qsTr("Favourite"); - case "m.lowpriority": - return qsTr("Low priority"); - case "m.server_notice": - return qsTr("Server notice"); - default: - return t.substring(2); - } - } - checkable: true - checked: roomContextMenu.tags !== undefined && roomContextMenu.tags.includes(t) - onTriggered: Rooms.toggleTag(roomContextMenu.roomid, t, checked) - } - - } - - Platform.MenuItem { - text: qsTr("Create new tag...") - onTriggered: newTag.show() - } - } - - SpaceMenuLevel { - id: rootSpaceMenu - - roomid: roomContextMenu.roomid - position: -1 - title: qsTr("Add or remove from community...") - childMenu: nestedSpaceMenuLevel - } - } - - delegate: ItemDelegate { - id: roomItem - - 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 - required property string roomName - required property string roomId - required property string avatarUrl - required property string time - required property string lastMessage - required property var tags - required property bool isInvite - required property bool isSpace - required property int notificationCount - required property bool hasLoudNotification - required property bool hasUnreadMessages - required property bool isDirect - required property string directChatOtherUserId - - Ripple { - color: Qt.rgba(Nheko.colors.dark.r, Nheko.colors.dark.g, Nheko.colors.dark.b, 0.5) - } - - height: avatarSize + 2 * Nheko.paddingMedium - width: ListView.view.width - ((scrollbar.interactive && scrollbar.visible && scrollbar.parent) ? scrollbar.width : 0) - state: "normal" - ToolTip.visible: hovered && collapsed - ToolTip.delay: Nheko.tooltipDelay - 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: roomItem.hovered && !((Rooms.currentRoom && roomId == Rooms.currentRoom.roomId) || Rooms.currentRoomPreview.roomid == roomId) - - PropertyChanges { - target: roomItem - backgroundColor: Nheko.colors.dark - importantText: Nheko.colors.brightText - unimportantText: Nheko.colors.brightText - bubbleBackground: Nheko.colors.highlight - bubbleText: Nheko.colors.highlightedText - } - - }, - State { - name: "selected" - when: (Rooms.currentRoom && roomId == Rooms.currentRoom.roomId) || Rooms.currentRoomPreview.roomid == roomId - - PropertyChanges { - target: roomItem - backgroundColor: Nheko.colors.highlight - importantText: Nheko.colors.highlightedText - unimportantText: Nheko.colors.highlightedText - bubbleBackground: Nheko.colors.highlightedText - bubbleText: Nheko.colors.highlight - } - - } - ] - - // NOTE(Nico): We want to prevent the touch areas from overlapping. For some reason we need to add 1px of padding for that... - Item { - anchors.fill: parent - anchors.margins: 1 - - TapHandler { - acceptedButtons: Qt.RightButton - onSingleTapped: { - if (!TimelineManager.isInvite) - roomContextMenu.show(roomId, tags); - - } - gesturePolicy: TapHandler.ReleaseWithinBounds - acceptedDevices: PointerDevice.Mouse | PointerDevice.Stylus | PointerDevice.TouchPad - } - - } - - RowLayout { - spacing: Nheko.paddingMedium - anchors.fill: parent - anchors.margins: Nheko.paddingMedium - - Avatar { - id: avatar - - enabled: false - Layout.alignment: Qt.AlignVCenter - height: avatarSize - width: avatarSize - url: avatarUrl.replace("mxc://", "image://MxcImage/") - displayName: roomName - userid: isDirect ? directChatOtherUserId : "" - roomid: roomId - - NotificationBubble { - id: collapsedNotificationBubble - - notificationCount: roomItem.notificationCount - hasLoudNotification: roomItem.hasLoudNotification - bubbleBackgroundColor: roomItem.bubbleBackground - bubbleTextColor: roomItem.bubbleText - anchors.right: parent.right - anchors.bottom: parent.bottom - anchors.margins: -Nheko.paddingSmall - mayBeVisible: collapsed && (isSpace ? Settings.spaceNotifications : true) - } - - } - - ColumnLayout { - id: textContent - - visible: !collapsed - Layout.alignment: Qt.AlignLeft - Layout.fillWidth: true - Layout.minimumWidth: 100 - width: parent.width - avatar.width - Layout.preferredWidth: parent.width - avatar.width - height: avatar.height - spacing: Nheko.paddingSmall - - NotificationBubble { - id: notificationBubble - - parent: isSpace ? titleRow : subtextRow - notificationCount: roomItem.notificationCount - hasLoudNotification: roomItem.hasLoudNotification - bubbleBackgroundColor: roomItem.bubbleBackground - bubbleTextColor: roomItem.bubbleText - Layout.alignment: Qt.AlignRight - Layout.leftMargin: Nheko.paddingSmall - Layout.preferredWidth: implicitWidth - Layout.preferredHeight: implicitHeight - mayBeVisible: !collapsed && (isSpace ? Settings.spaceNotifications : true) - } - - RowLayout { - id: titleRow - - Layout.alignment: Qt.AlignTop - Layout.fillWidth: true - spacing: Nheko.paddingSmall - - ElidedLabel { - id: rN - Layout.alignment: Qt.AlignBaseline - color: roomItem.importantText - elideWidth: width - fullText: TimelineManager.htmlEscape(roomName) - textFormat: Text.RichText - Layout.fillWidth: true - } - - Label { - id: timestamp - - visible: !isInvite && !isSpace - width: visible ? 0 : undefined - Layout.alignment: Qt.AlignRight | Qt.AlignBaseline - font.pixelSize: fontMetrics.font.pixelSize * 0.9 - color: roomItem.unimportantText - text: time - } - - } - - RowLayout { - id: subtextRow - - Layout.fillWidth: true - spacing: 0 - visible: !isSpace - height: visible ? 0 : undefined - Layout.alignment: Qt.AlignBottom - - ElidedLabel { - color: roomItem.unimportantText - font.pixelSize: fontMetrics.font.pixelSize * 0.9 - elideWidth: width - fullText: TimelineManager.htmlEscape(lastMessage) - textFormat: Text.RichText - Layout.fillWidth: true - } - - } - - } - - } - - Rectangle { - anchors.left: parent.left - anchors.verticalCenter: parent.verticalCenter - height: parent.height - Nheko.paddingSmall * 2 - width: 3 - color: Nheko.colors.highlight - visible: hasUnreadMessages - } - - background: Rectangle { - color: backgroundColor - } - - } - - } - background: Rectangle { color: Nheko.theme.sidebarBackground } + footer: ColumnLayout { + spacing: 0 + Rectangle { + Layout.fillWidth: true + color: Nheko.theme.separator + height: 1 + } + Pane { + Layout.alignment: Qt.AlignBottom + Layout.fillWidth: true + Layout.minimumHeight: 40 + horizontalPadding: Nheko.paddingMedium + verticalPadding: 0 + + background: Rectangle { + color: palette.window + } + contentItem: RowLayout { + id: buttonRow + + ImageButton { + Layout.fillWidth: true + Layout.margins: Nheko.paddingMedium + ToolTip.delay: Nheko.tooltipDelay + ToolTip.text: qsTr("Start a new chat") + ToolTip.visible: hovered + height: 22 + hoverEnabled: true + image: ":/icons/icons/ui/add-square-button.svg" + width: 22 + + onClicked: roomJoinCreateMenu.open(parent) + + Platform.Menu { + id: roomJoinCreateMenu + + Platform.MenuItem { + text: qsTr("Join a room") + + onTriggered: Nheko.openJoinRoomDialog() + } + Platform.MenuItem { + text: qsTr("Create a new room") + + onTriggered: { + var createRoom = createRoomComponent.createObject(timelineRoot); + createRoom.show(); + timelineRoot.destroyOnClose(createRoom); + } + } + Platform.MenuItem { + text: qsTr("Start a direct chat") + + onTriggered: { + var createDirect = createDirectComponent.createObject(timelineRoot); + createDirect.show(); + timelineRoot.destroyOnClose(createDirect); + } + } + Platform.MenuItem { + text: qsTr("Create a new community") + + onTriggered: { + var createRoom = createRoomComponent.createObject(timelineRoot, { + "space": true + }); + createRoom.show(); + timelineRoot.destroyOnClose(createRoom); + } + } + } + } + ImageButton { + Layout.fillWidth: true + Layout.margins: Nheko.paddingMedium + ToolTip.delay: Nheko.tooltipDelay + ToolTip.text: qsTr("Room directory") + ToolTip.visible: hovered + height: 22 + hoverEnabled: true + image: ":/icons/icons/ui/room-directory.svg" + visible: !collapsed + width: 22 + + onClicked: { + var win = roomDirectoryComponent.createObject(timelineRoot); + win.show(); + timelineRoot.destroyOnClose(win); + } + } + ImageButton { + Layout.fillWidth: true + Layout.margins: Nheko.paddingMedium + ToolTip.delay: Nheko.tooltipDelay + ToolTip.text: qsTr("Search rooms (Ctrl+K)") + ToolTip.visible: hovered + height: 22 + hoverEnabled: true + image: ":/icons/icons/ui/search.svg" + ripple: false + visible: !collapsed + width: 22 + + onClicked: { + var component = Qt.createComponent("qrc:/resources/qml/QuickSwitcher.qml"); + if (component.status == Component.Ready) { + var quickSwitch = component.createObject(timelineRoot); + quickSwitch.open(); + destroyOnClosed(quickSwitch); + } else { + console.error("Failed to create component: " + component.errorString()); + } + } + } + ImageButton { + Layout.fillWidth: true + Layout.margins: Nheko.paddingMedium + ToolTip.delay: Nheko.tooltipDelay + ToolTip.text: qsTr("User settings") + ToolTip.visible: hovered + height: 22 + hoverEnabled: true + image: ":/icons/icons/ui/settings.svg" + ripple: false + visible: !collapsed + width: 22 + + onClicked: mainWindow.push(userSettingsPage) + } + } + } + } header: ColumnLayout { spacing: 0 @@ -474,9 +162,11 @@ Page { function openUserProfile() { Nheko.updateUserProfile(); - var component = Qt.createComponent("qrc:/qml/dialogs/UserProfile.qml") + var component = Qt.createComponent("qrc:/resources/qml/dialogs/UserProfile.qml"); if (component.status == Component.Ready) { - var userProfile = component.createObject(timelineRoot, {"profile": Nheko.currentUser}); + var userProfile = component.createObject(timelineRoot, { + "profile": Nheko.currentUser + }); userProfile.show(); timelineRoot.destroyOnClose(userProfile); } else { @@ -484,55 +174,15 @@ Page { } } - - Layout.fillWidth: true Layout.alignment: Qt.AlignBottom + Layout.fillWidth: true + Layout.minimumHeight: 40 //Layout.preferredHeight: userInfoGrid.implicitHeight + 2 * Nheko.paddingMedium padding: Nheko.paddingMedium - Layout.minimumHeight: 40 - background: Rectangle {color: Nheko.colors.window} - - InputDialog { - id: statusDialog - - title: qsTr("Status Message") - prompt: qsTr("Enter your status message:") - onAccepted: function(text) { - Nheko.setStatusMessage(text); - } + background: Rectangle { + color: palette.window } - - Platform.Menu { - id: userInfoMenu - - Platform.MenuItem { - text: qsTr("Profile settings") - onTriggered: userInfoPanel.openUserProfile() - } - - Platform.MenuItem { - text: qsTr("Set status message") - onTriggered: statusDialog.show() - } - - } - - TapHandler { - margin: -Nheko.paddingSmall - acceptedButtons: Qt.LeftButton - onSingleTapped: userInfoPanel.openUserProfile() - onLongPressed: userInfoMenu.open() - gesturePolicy: TapHandler.ReleaseWithinBounds - } - - TapHandler { - margin: -Nheko.paddingSmall - acceptedButtons: Qt.RightButton - onSingleTapped: userInfoMenu.open() - gesturePolicy: TapHandler.ReleaseWithinBounds - } - contentItem: RowLayout { id: userInfoGrid @@ -544,91 +194,123 @@ Page { id: avatar Layout.alignment: Qt.AlignVCenter - Layout.preferredWidth: fontMetrics.lineSpacing * 2 Layout.preferredHeight: fontMetrics.lineSpacing * 2 - url: (userInfoGrid.profile ? userInfoGrid.profile.avatarUrl : "").replace("mxc://", "image://MxcImage/") + Layout.preferredWidth: fontMetrics.lineSpacing * 2 displayName: userInfoGrid.profile ? userInfoGrid.profile.displayName : "" - userid: userInfoGrid.profile ? userInfoGrid.profile.userid : "" enabled: false + url: (userInfoGrid.profile ? userInfoGrid.profile.avatarUrl : "").replace("mxc://", "image://MxcImage/") + userid: userInfoGrid.profile ? userInfoGrid.profile.userid : "" } - ColumnLayout { id: col - visible: !collapsed Layout.alignment: Qt.AlignLeft Layout.fillWidth: true - width: parent.width - avatar.width - logoutButton.width - Nheko.paddingMedium * 2 Layout.preferredWidth: parent.width - avatar.width - logoutButton.width - Nheko.paddingMedium * 2 spacing: 0 + visible: !collapsed + width: parent.width - avatar.width - logoutButton.width - Nheko.paddingMedium * 2 ElidedLabel { Layout.alignment: Qt.AlignBottom + elideWidth: col.width font.pointSize: fontMetrics.font.pointSize * 1.1 font.weight: Font.DemiBold fullText: userInfoGrid.profile ? userInfoGrid.profile.displayName : "" - elideWidth: col.width } - ElidedLabel { Layout.alignment: Qt.AlignTop - color: Nheko.colors.buttonText - font.pointSize: fontMetrics.font.pointSize * 0.9 + color: palette.buttonText elideWidth: col.width + font.pointSize: fontMetrics.font.pointSize * 0.9 fullText: userInfoGrid.profile ? userInfoGrid.profile.userid : "" } - } - Item { } - ImageButton { id: logoutButton - visible: !collapsed Layout.alignment: Qt.AlignVCenter - Layout.preferredWidth: fontMetrics.lineSpacing * 2 Layout.preferredHeight: fontMetrics.lineSpacing * 2 - image: ":/icons/icons/ui/power-off.svg" - ToolTip.visible: hovered + Layout.preferredWidth: fontMetrics.lineSpacing * 2 ToolTip.delay: Nheko.tooltipDelay ToolTip.text: qsTr("Logout") + ToolTip.visible: hovered + image: ":/icons/icons/ui/power-off.svg" + visible: !collapsed + onClicked: Nheko.openLogoutDialog() } - } - } + InputDialog { + id: statusDialog + prompt: qsTr("Enter your status message:") + title: qsTr("Status Message") + + onAccepted: function (text) { + Nheko.setStatusMessage(text); + } + } + Platform.Menu { + id: userInfoMenu + + Platform.MenuItem { + text: qsTr("Profile settings") + + onTriggered: userInfoPanel.openUserProfile() + } + Platform.MenuItem { + text: qsTr("Set status message") + + onTriggered: statusDialog.show() + } + } + TapHandler { + acceptedButtons: Qt.LeftButton + gesturePolicy: TapHandler.ReleaseWithinBounds + margin: -Nheko.paddingSmall + + onLongPressed: userInfoMenu.open() + onSingleTapped: userInfoPanel.openUserProfile() + } + TapHandler { + acceptedButtons: Qt.RightButton + gesturePolicy: TapHandler.ReleaseWithinBounds + margin: -Nheko.paddingSmall + + onSingleTapped: userInfoMenu.open() + } + } Rectangle { + Layout.fillWidth: true color: Nheko.theme.separator height: 2 - Layout.fillWidth: true } - Rectangle { id: unverifiedStuffBubble - color: Qt.lighter(Nheko.theme.orange, verifyButtonHovered.hovered ? 1.2 : 1) Layout.fillWidth: true + color: Qt.lighter(Nheko.theme.orange, verifyButtonHovered.hovered ? 1.2 : 1) 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 + width: parent.width Label { id: explanation + Layout.fillWidth: true Layout.margins: Nheko.paddingMedium Layout.rightMargin: Nheko.paddingSmall - color: Nheko.colors.buttonText - Layout.fillWidth: true + color: palette.buttonText text: { switch (SelfVerificationStatus.status) { case SelfVerificationStatus.NoMasterKey: @@ -647,34 +329,32 @@ Page { textFormat: Text.PlainText wrapMode: Text.Wrap } - ImageButton { id: closeUnverifiedBubble - Layout.rightMargin: Nheko.paddingMedium Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - hoverEnabled: true - width: fontMetrics.font.pixelSize - height: fontMetrics.font.pixelSize - image: ":/icons/icons/ui/dismiss.svg" - ToolTip.visible: closeUnverifiedBubble.hovered + Layout.rightMargin: Nheko.paddingMedium ToolTip.delay: Nheko.tooltipDelay ToolTip.text: qsTr("Close") + ToolTip.visible: closeUnverifiedBubble.hovered + height: fontMetrics.font.pixelSize + hoverEnabled: true + image: ":/icons/icons/ui/dismiss.svg" + width: fontMetrics.font.pixelSize + onClicked: unverifiedStuffBubble.visible = false } - } - HoverHandler { id: verifyButtonHovered - enabled: !closeUnverifiedBubble.hovered acceptedDevices: PointerDevice.Mouse | PointerDevice.Stylus | PointerDevice.TouchPad - } - - TapHandler { enabled: !closeUnverifiedBubble.hovered + } + TapHandler { acceptedButtons: Qt.LeftButton + enabled: !closeUnverifiedBubble.hovered + onSingleTapped: { if (SelfVerificationStatus.status == SelfVerificationStatus.UnverifiedDevices) SelfVerificationStatus.verifyUnverifiedDevices(); @@ -682,151 +362,431 @@ Page { SelfVerificationStatus.statusChanged(); } } - } - Rectangle { + Layout.fillWidth: true color: Nheko.theme.separator height: 1 - Layout.fillWidth: true visible: unverifiedStuffBubble.visible } - } - footer: ColumnLayout { - spacing: 0 - - Rectangle { - color: Nheko.theme.separator - height: 1 - Layout.fillWidth: true + // HACK: https://bugreports.qt.io/browse/QTBUG-83972, qtwayland cannot auto hide menu + Connections { + function onHideMenu() { + userInfoMenu.close(); + roomContextMenu.close(); } - Pane { - Layout.fillWidth: true - Layout.alignment: Qt.AlignBottom - Layout.minimumHeight: 40 + target: MainWindow + } + Component { + id: roomDirectoryComponent - horizontalPadding: Nheko.paddingMedium - verticalPadding: 0 + RoomDirectory { + } + } + Component { + id: createRoomComponent - background: Rectangle {color: Nheko.colors.window} - contentItem: RowLayout { - id: buttonRow + CreateRoom { + } + } + Component { + id: createDirectComponent - ImageButton { - Layout.fillWidth: true - hoverEnabled: true - width: 22 - height: 22 - image: ":/icons/icons/ui/add-square-button.svg" - ToolTip.visible: hovered - ToolTip.delay: Nheko.tooltipDelay - ToolTip.text: qsTr("Start a new chat") - Layout.margins: Nheko.paddingMedium - onClicked: roomJoinCreateMenu.open(parent) + CreateDirect { + } + } + ListView { + id: roomlist - Platform.Menu { - id: roomJoinCreateMenu + anchors.left: parent.left + anchors.right: parent.right + height: parent.height + model: Rooms - Platform.MenuItem { - text: qsTr("Join a room") - onTriggered: Nheko.openJoinRoomDialog() - } + //reuseItems: true + ScrollBar.vertical: ScrollBar { + id: scrollbar - Platform.MenuItem { - text: qsTr("Create a new room") - onTriggered: { - var createRoom = createRoomComponent.createObject(timelineRoot); - createRoom.show(); - timelineRoot.destroyOnClose(createRoom); - } - } + parent: !collapsed && Settings.scrollbarsInRoomlist ? roomlist : null + } + delegate: ItemDelegate { + id: roomItem - Platform.MenuItem { - text: qsTr("Start a direct chat") - onTriggered: { - var createDirect = createDirectComponent.createObject(timelineRoot); - createDirect.show(); - timelineRoot.destroyOnClose(createDirect); - } - } + required property string avatarUrl + property color backgroundColor: palette.window + property color bubbleBackground: palette.highlight + property color bubbleText: palette.highlightedText + required property string directChatOtherUserId + required property bool hasLoudNotification + required property bool hasUnreadMessages + property color importantText: palette.text + required property bool isDirect + required property bool isInvite + required property bool isSpace + required property string lastMessage + required property int notificationCount + required property string roomId + required property string roomName + required property var tags + required property string time + property color unimportantText: palette.buttonText - Platform.MenuItem { - text: qsTr("Create a new community") - onTriggered: { - var createRoom = createRoomComponent.createObject(timelineRoot, { "space": true }); - createRoom.show(); - timelineRoot.destroyOnClose(createRoom); - } - } + ToolTip.delay: Nheko.tooltipDelay + ToolTip.text: roomName + ToolTip.visible: hovered && collapsed + height: avatarSize + 2 * Nheko.paddingMedium + state: "normal" + width: ListView.view.width - ((scrollbar.interactive && scrollbar.visible && scrollbar.parent) ? scrollbar.width : 0) + background: Rectangle { + color: backgroundColor + } + states: [ + State { + name: "highlight" + when: roomItem.hovered && !((Rooms.currentRoom && roomId == Rooms.currentRoom.roomId) || Rooms.currentRoomPreview.roomid == roomId) + + PropertyChanges { + backgroundColor: palette.dark + bubbleBackground: palette.highlight + bubbleText: palette.highlightedText + importantText: palette.brightText + target: roomItem + unimportantText: palette.brightText } + }, + State { + name: "selected" + when: (Rooms.currentRoom && roomId == Rooms.currentRoom.roomId) || Rooms.currentRoomPreview.roomid == roomId - } - - ImageButton { - visible: !collapsed - Layout.fillWidth: true - hoverEnabled: true - width: 22 - height: 22 - image: ":/icons/icons/ui/room-directory.svg" - ToolTip.visible: hovered - ToolTip.delay: Nheko.tooltipDelay - ToolTip.text: qsTr("Room directory") - Layout.margins: Nheko.paddingMedium - onClicked: { - var win = roomDirectoryComponent.createObject(timelineRoot); - win.show(); - timelineRoot.destroyOnClose(win); + PropertyChanges { + backgroundColor: palette.highlight + bubbleBackground: palette.highlightedText + bubbleText: palette.highlight + importantText: palette.highlightedText + target: roomItem + unimportantText: palette.highlightedText } } + ] - ImageButton { - visible: !collapsed - Layout.fillWidth: true - hoverEnabled: true - ripple: false - width: 22 - height: 22 - image: ":/icons/icons/ui/search.svg" - ToolTip.visible: hovered - ToolTip.delay: Nheko.tooltipDelay - ToolTip.text: qsTr("Search rooms (Ctrl+K)") - Layout.margins: Nheko.paddingMedium - onClicked: { - var component = Qt.createComponent("qrc:/qml/QuickSwitcher.qml") - if (component.status == Component.Ready) { - var quickSwitch = component.createObject(timelineRoot); - quickSwitch.open(); - destroyOnClosed(quickSwitch); - } else { - console.error("Failed to create component: " + component.errorString()); - } - } - } - - ImageButton { - visible: !collapsed - Layout.fillWidth: true - hoverEnabled: true - ripple: false - width: 22 - height: 22 - image: ":/icons/icons/ui/settings.svg" - ToolTip.visible: hovered - ToolTip.delay: Nheko.tooltipDelay - ToolTip.text: qsTr("User settings") - Layout.margins: Nheko.paddingMedium - onClicked: mainWindow.push(userSettingsPage); - } - + 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); } + Ripple { + color: Qt.rgba(palette.dark.r, palette.dark.g, palette.dark.b, 0.5) + } + + // NOTE(Nico): We want to prevent the touch areas from overlapping. For some reason we need to add 1px of padding for that... + Item { + anchors.fill: parent + anchors.margins: 1 + + TapHandler { + acceptedButtons: Qt.RightButton + acceptedDevices: PointerDevice.Mouse | PointerDevice.Stylus | PointerDevice.TouchPad + gesturePolicy: TapHandler.ReleaseWithinBounds + + onSingleTapped: { + if (!TimelineManager.isInvite) + roomContextMenu.show(roomId, tags); + } + } + } + RowLayout { + anchors.fill: parent + anchors.margins: Nheko.paddingMedium + spacing: Nheko.paddingMedium + + Avatar { + id: avatar + + Layout.alignment: Qt.AlignVCenter + displayName: roomName + enabled: false + height: avatarSize + roomid: roomId + url: avatarUrl.replace("mxc://", "image://MxcImage/") + userid: isDirect ? directChatOtherUserId : "" + width: avatarSize + + NotificationBubble { + id: collapsedNotificationBubble + + anchors.bottom: parent.bottom + anchors.margins: -Nheko.paddingSmall + anchors.right: parent.right + bubbleBackgroundColor: roomItem.bubbleBackground + bubbleTextColor: roomItem.bubbleText + hasLoudNotification: roomItem.hasLoudNotification + mayBeVisible: collapsed && (isSpace ? Settings.spaceNotifications : true) + notificationCount: roomItem.notificationCount + } + } + ColumnLayout { + id: textContent + + Layout.alignment: Qt.AlignLeft + Layout.minimumWidth: 100 + Layout.preferredWidth: parent.width - avatar.width + height: avatar.height + spacing: Nheko.paddingSmall + visible: !collapsed + width: roomItem.width - avatar.width + + Item { + id: titleRow + + Layout.alignment: Qt.AlignTop + Layout.fillWidth: true + Layout.preferredHeight: subtitleText.implicitHeight + + ElidedLabel { + id: titleText + + anchors.left: parent.left + color: roomItem.importantText + elideWidth: parent.width - (timestamp.visible ? timestamp.implicitWidth : 0) - (spaceNotificationBubble.visible ? spaceNotificationBubble.implicitWidth : 0) + fullText: TimelineManager.htmlEscape(roomName) + textFormat: Text.RichText + } + Label { + id: timestamp + + anchors.baseline: titleText.baseline + anchors.right: parent.right + color: roomItem.unimportantText + font.pixelSize: fontMetrics.font.pixelSize * 0.9 + text: time + visible: !isInvite && !isSpace + } + NotificationBubble { + id: spaceNotificationBubble + + anchors.right: parent.right + bubbleBackgroundColor: roomItem.bubbleBackground + bubbleTextColor: roomItem.bubbleText + hasLoudNotification: roomItem.hasLoudNotification + mayBeVisible: !collapsed && (isSpace ? Settings.spaceNotifications : false) + notificationCount: roomItem.notificationCount + parent: isSpace ? titleRow : subtextRow + } + } + Item { + id: subtextRow + + Layout.alignment: Qt.AlignBottom + Layout.fillWidth: true + Layout.preferredHeight: subtitleText.implicitHeight + visible: !isSpace + + ElidedLabel { + id: subtitleText + + anchors.left: parent.left + color: roomItem.unimportantText + elideWidth: subtextRow.width - (subtextNotificationBubble.visible ? subtextNotificationBubble.implicitWidth : 0) + font.pixelSize: fontMetrics.font.pixelSize * 0.9 + fullText: TimelineManager.htmlEscape(lastMessage) + textFormat: Text.RichText + } + NotificationBubble { + id: subtextNotificationBubble + + anchors.baseline: subtitleText.baseline + anchors.right: parent.right + bubbleBackgroundColor: roomItem.bubbleBackground + bubbleTextColor: roomItem.bubbleText + hasLoudNotification: roomItem.hasLoudNotification + mayBeVisible: !collapsed + notificationCount: roomItem.notificationCount + } + } + } + } + Rectangle { + anchors.left: parent.left + anchors.verticalCenter: parent.verticalCenter + color: palette.highlight + height: parent.height - Nheko.paddingSmall * 2 + visible: hasUnreadMessages + width: 3 + } } - } + Connections { + function onCurrentRoomChanged() { + if (Rooms.currentRoom) + roomlist.positionViewAtIndex(Rooms.roomidToIndex(Rooms.currentRoom.roomId), ListView.Contain); + } + target: Rooms + } + Component { + id: roomWindowComponent + + ApplicationWindow { + id: roomWindowW + + property var room: null + property var roomPreview: null + + color: palette.window + height: 650 + minimumHeight: 150 + minimumWidth: 150 + title: room.plainRoomName + width: 420 + + Component.onCompleted: { + MainWindow.addPerRoomWindow(room.roomId || roomPreview.roomid, roomWindowW); + Nheko.setTransientParent(roomWindowW, null); + } + Component.onDestruction: MainWindow.removePerRoomWindow(room.roomId || roomPreview.roomid, roomWindowW) + onActiveChanged: { + room.lastReadIdOnWindowFocus(); + } + + //flags: Qt.Window | Qt.WindowCloseButtonHint | Qt.WindowTitleHint + Shortcut { + sequence: StandardKey.Cancel + + onActivated: roomWindowW.close() + } + TimelineView { + id: timeline + + anchors.fill: parent + privacyScreen: privacyScreen + room: roomWindowW.room + roomPreview: roomWindowW.roomPreview.roomid ? roomWindowW.roomPreview : null + } + PrivacyScreen { + id: privacyScreen + + anchors.fill: parent + screenTimeout: Settings.privacyScreenTimeout + timelineRoot: timeline + visible: Settings.privacyScreen + windowTarget: roomWindowW + } + } + } + Component { + id: nestedSpaceMenuLevel + + SpaceMenuLevel { + childMenu: rootSpaceMenu.childMenu + roomid: roomContextMenu.roomid + } + } + Platform.Menu { + id: roomContextMenu + + property string roomid + property var tags + + function show(roomid_, tags_) { + roomid = roomid_; + tags = tags_; + open(); + } + + InputDialog { + id: newTag + + prompt: qsTr("Enter the tag you want to use:") + title: qsTr("New tag") + + onAccepted: function (text) { + Rooms.toggleTag(roomContextMenu.roomid, "u." + text, true); + } + } + Platform.MenuItem { + text: qsTr("Open separately") + + onTriggered: { + var roomWindow = roomWindowComponent.createObject(null, { + "room": Rooms.getRoomById(roomContextMenu.roomid), + "roomPreview": Rooms.getRoomPreviewById(roomContextMenu.roomid) + }); + roomWindow.showNormal(); + destroyOnClose(roomWindow); + } + } + Platform.MenuItem { + text: qsTr("Room settings") + + onTriggered: TimelineManager.openRoomSettings(roomContextMenu.roomid) + } + Platform.MenuItem { + text: qsTr("Leave room") + + onTriggered: TimelineManager.openLeaveRoomDialog(roomContextMenu.roomid) + } + Platform.MenuItem { + text: qsTr("Copy room link") + + onTriggered: Rooms.copyLink(roomContextMenu.roomid) + } + Platform.Menu { + id: tagsMenu + + title: qsTr("Tag room as:") + + Instantiator { + model: Communities.tagsWithDefault + + delegate: Platform.MenuItem { + property string t: modelData + + checkable: true + checked: roomContextMenu.tags !== undefined && roomContextMenu.tags.includes(t) + text: { + switch (t) { + case "m.favourite": + return qsTr("Favourite"); + case "m.lowpriority": + return qsTr("Low priority"); + case "m.server_notice": + return qsTr("Server notice"); + default: + return t.substring(2); + } + } + + onTriggered: Rooms.toggleTag(roomContextMenu.roomid, t, checked) + } + + onObjectAdded: (index, object) => tagsMenu.insertItem(index, object) + onObjectRemoved: (index, object) => tagsMenu.removeItem(object) + } + Platform.MenuItem { + text: qsTr("Create new tag...") + + onTriggered: newTag.show() + } + } + SpaceMenuLevel { + id: rootSpaceMenu + + childMenu: nestedSpaceMenuLevel + position: -1 + roomid: roomContextMenu.roomid + title: qsTr("Add or remove from community...") + } + } + } } diff --git a/resources/qml/Root.qml b/resources/qml/Root.qml index 4b71af37..e26b386a 100644 --- a/resources/qml/Root.qml +++ b/resources/qml/Root.qml @@ -10,30 +10,23 @@ import "./pages" import "./voip" import "./ui" import Qt.labs.platform 1.1 as Platform -import QtQuick 2.15 -import QtQuick.Controls 2.15 -import QtQuick.Layouts 1.3 -import QtQuick.Window 2.15 -import im.nheko 1.0 -import im.nheko.EmojiModel 1.0 +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import QtQuick.Window +import im.nheko Pane { id: timelineRoot - palette: Nheko.colors - background: null - padding: 0 - - FontMetrics { - id: fontMetrics + function destroyOnClose(obj) { + if (obj.closing != undefined) + obj.closing.connect(() => obj.destroy(1000)); + else if (obj.aboutToHide != undefined) + obj.aboutToHide.connect(() => obj.destroy(1000)); } - - RoomDirectoryModel { - id: publicRooms - } - - UserDirectoryModel { - id: userDirectory + function destroyOnClosed(obj) { + obj.aboutToHide.connect(() => obj.destroy(1000)); } //Timer { @@ -42,54 +35,49 @@ Pane { // running: true // repeat: true //} - function showAliasEditor(settings) { - var component = Qt.createComponent("qrc:/qml/dialogs/AliasEditor.qml") + var component = Qt.createComponent("qrc:/resources/qml/dialogs/AliasEditor.qml"); if (component.status == Component.Ready) { var dialog = component.createObject(timelineRoot, { - "roomSettings": settings - }); - dialog.show(); - destroyOnClose(dialog); - } else { - console.error("Failed to create component: " + component.errorString()); - } - - } - - function showPLEditor(settings) { - var component = Qt.createComponent("qrc:/qml/dialogs/PowerLevelEditor.qml") - if (component.status == Component.Ready) { - var dialog = component.createObject(timelineRoot, { - "roomSettings": settings - }); + "roomSettings": settings + }); dialog.show(); destroyOnClose(dialog); } else { console.error("Failed to create component: " + component.errorString()); } } - - function showSpacePLApplyPrompt(settings, editingModel) { - var component = Qt.createComponent("qrc:/qml/dialogs/PowerLevelSpacesApplyDialog.qml") - if (component.status == Component.Ready) { - var dialog = component.createObject(timelineRoot, { - "roomSettings": settings, - "editingModel": editingModel - }); - dialog.show(); - destroyOnClose(dialog); - } else { - console.error("Failed to create component: " + component.errorString()); - } - } - function showAllowedRoomsEditor(settings) { - var component = Qt.createComponent("qrc:/qml/dialogs/AllowedRoomsSettingsDialog.qml") + var component = Qt.createComponent("qrc:/resources/qml/dialogs/AllowedRoomsSettingsDialog.qml"); if (component.status == Component.Ready) { var dialog = component.createObject(timelineRoot, { - "roomSettings": settings - }); + "roomSettings": settings + }); + dialog.show(); + destroyOnClose(dialog); + } else { + console.error("Failed to create component: " + component.errorString()); + } + } + function showPLEditor(settings) { + var component = Qt.createComponent("qrc:/resources/qml/dialogs/PowerLevelEditor.qml"); + if (component.status == Component.Ready) { + var dialog = component.createObject(timelineRoot, { + "roomSettings": settings + }); + dialog.show(); + destroyOnClose(dialog); + } else { + console.error("Failed to create component: " + component.errorString()); + } + } + function showSpacePLApplyPrompt(settings, editingModel) { + var component = Qt.createComponent("qrc:/resources/qml/dialogs/PowerLevelSpacesApplyDialog.qml"); + if (component.status == Component.Ready) { + var dialog = component.createObject(timelineRoot, { + "roomSettings": settings, + "editingModel": editingModel + }); dialog.show(); destroyOnClose(dialog); } else { @@ -97,23 +85,37 @@ Pane { } } + background: null + padding: 0 + + FontMetrics { + id: fontMetrics + + } + UserDirectoryModel { + id: userDirectory + + } + RoomDirectoryModel { + id: publicRooms + + } Component { id: readReceiptsDialog ReadReceipts { } - } - Shortcut { sequence: StandardKey.Quit + onActivated: Qt.quit() } - Shortcut { sequence: "Ctrl+K" + onActivated: { - var component = Qt.createComponent("qrc:/qml/QuickSwitcher.qml") + var component = Qt.createComponent("qrc:/resources/qml/QuickSwitcher.qml"); if (component.status == Component.Ready) { var quickSwitch = component.createObject(timelineRoot); quickSwitch.open(); @@ -123,37 +125,25 @@ Pane { } } } - Shortcut { // Add alternative shortcut, because sometimes Alt+A is stolen by the TextEdit sequences: ["Alt+A", "Ctrl+Shift+A"] + onActivated: Rooms.nextRoomWithActivity() } - Shortcut { sequence: "Ctrl+Down" + onActivated: Rooms.nextRoom() } - Shortcut { sequence: "Ctrl+Up" + onActivated: Rooms.previousRoom() } - Connections { - function onOpenLogoutDialog() { - var component = Qt.createComponent("qrc:/qml/dialogs/LogoutDialog.qml") - if (component.status == Component.Ready) { - var dialog = component.createObject(timelineRoot); - dialog.open(); - destroyOnClose(dialog); - } else { - console.error("Failed to create component: " + component.errorString()); - } - } - function onOpenJoinRoomDialog() { - var component = Qt.createComponent("qrc:/qml/dialogs/JoinRoomDialog.qml") + var component = Qt.createComponent("qrc:/resources/qml/dialogs/JoinRoomDialog.qml"); if (component.status == Component.Ready) { var dialog = component.createObject(timelineRoot); dialog.show(); @@ -162,11 +152,22 @@ Pane { console.error("Failed to create component: " + component.errorString()); } } - - function onShowRoomJoinPrompt(summary) { - var component = Qt.createComponent("qrc:/qml/dialogs/ConfirmJoinRoomDialog.qml") + function onOpenLogoutDialog() { + var component = Qt.createComponent("qrc:/resources/qml/dialogs/LogoutDialog.qml"); if (component.status == Component.Ready) { - var dialog = component.createObject(timelineRoot, {"summary": summary}); + var dialog = component.createObject(timelineRoot); + dialog.open(); + destroyOnClose(dialog); + } else { + console.error("Failed to create component: " + component.errorString()); + } + } + function onShowRoomJoinPrompt(summary) { + var component = Qt.createComponent("qrc:/resources/qml/dialogs/ConfirmJoinRoomDialog.qml"); + if (component.status == Component.Ready) { + var dialog = component.createObject(timelineRoot, { + "summary": summary + }); dialog.show(); destroyOnClose(dialog); } else { @@ -176,12 +177,13 @@ Pane { target: Nheko } - Connections { function onNewDeviceVerificationRequest(flow) { - var component = Qt.createComponent("qrc:/qml/device-verification/DeviceVerification.qml") + var component = Qt.createComponent("qrc:/resources/qml/device-verification/DeviceVerification.qml"); if (component.status == Component.Ready) { - var dialog = component.createObject(timelineRoot, {"flow": flow}); + var dialog = component.createObject(timelineRoot, { + "flow": flow + }); dialog.show(); destroyOnClose(dialog); } else { @@ -191,101 +193,71 @@ Pane { target: VerificationManager } - - function destroyOnClose(obj) { - if (obj.closing != undefined) obj.closing.connect(() => obj.destroy(1000)); - else if (obj.aboutToHide != undefined) obj.aboutToHide.connect(() => obj.destroy(1000)); - } - - function destroyOnClosed(obj) { - obj.aboutToHide.connect(() => obj.destroy(1000)); - } - Connections { - function onOpenProfile(profile) { - var component = Qt.createComponent("qrc:/qml/dialogs/UserProfile.qml") - if (component.status == Component.Ready) { - var userProfile = component.createObject(timelineRoot, {"profile": profile}); - userProfile.show(); - destroyOnClose(userProfile); - } else { - console.error("Failed to create component: " + component.errorString()); - } - } - - function onShowImagePackSettings(room, packlist) { - var component = Qt.createComponent("qrc:/qml/dialogs/ImagePackSettingsDialog.qml") - - if (component.status == Component.Ready) { - var packSet = component.createObject(timelineRoot, { - "room": room, - "packlist": packlist - }); - packSet.show(); - destroyOnClose(packSet); - } else { - console.error("Failed to create component: " + component.errorString()); - } - } - - function onOpenRoomMembersDialog(members, room) { - var component = Qt.createComponent("qrc:/qml/dialogs/RoomMembers.qml") - if (component.status == Component.Ready) { - var membersDialog = component.createObject(timelineRoot, { - "members": members, - "room": room - }); - membersDialog.show(); - destroyOnClose(membersDialog); - } else { - console.error("Failed to create component: " + component.errorString()); - } - - } - - function onOpenRoomSettingsDialog(settings) { - var component = Qt.createComponent("qrc:/qml/dialogs/RoomSettings.qml") - if (component.status == Component.Ready) { - var roomSettings = component.createObject(timelineRoot, { - "roomSettings": settings - }); - roomSettings.show(); - destroyOnClose(roomSettings); - } else { - console.error("Failed to create component: " + component.errorString()); - } - - } - function onOpenInviteUsersDialog(invitees) { - var component = Qt.createComponent("qrc:/qml/dialogs/InviteDialog.qml") + var component = Qt.createComponent("qrc:/resources/qml/dialogs/InviteDialog.qml"); if (component.status == Component.Ready) { var dialog = component.createObject(timelineRoot, { - "invitees": invitees - }); + "invitees": invitees + }); dialog.show(); destroyOnClose(dialog); } else { console.error("Failed to create component: " + component.errorString()); } } - function onOpenLeaveRoomDialog(roomid, reason) { - var component = Qt.createComponent("qrc:/qml/dialogs/LeaveRoomDialog.qml") + var component = Qt.createComponent("qrc:/resources/qml/dialogs/LeaveRoomDialog.qml"); if (component.status == Component.Ready) { var dialog = component.createObject(timelineRoot, { - "roomId": roomid, - "reason": reason - }); + "roomId": roomid, + "reason": reason + }); dialog.open(); destroyOnClose(dialog); } else { console.error("Failed to create component: " + component.errorString()); } } - + function onOpenProfile(profile) { + var component = Qt.createComponent("qrc:/resources/qml/dialogs/UserProfile.qml"); + if (component.status == Component.Ready) { + var userProfile = component.createObject(timelineRoot, { + "profile": profile + }); + userProfile.show(); + destroyOnClose(userProfile); + } else { + console.error("Failed to create component: " + component.errorString()); + } + } + function onOpenRoomMembersDialog(members, room) { + var component = Qt.createComponent("qrc:/resources/qml/dialogs/RoomMembers.qml"); + if (component.status == Component.Ready) { + var membersDialog = component.createObject(timelineRoot, { + "members": members, + "room": room + }); + membersDialog.show(); + destroyOnClose(membersDialog); + } else { + console.error("Failed to create component: " + component.errorString()); + } + } + function onOpenRoomSettingsDialog(settings) { + var component = Qt.createComponent("qrc:/resources/qml/dialogs/RoomSettings.qml"); + if (component.status == Component.Ready) { + var roomSettings = component.createObject(timelineRoot, { + "roomSettings": settings + }); + roomSettings.show(); + destroyOnClose(roomSettings); + } else { + console.error("Failed to create component: " + component.errorString()); + } + } function onShowImageOverlay(room, eventId, url, originalWidth, proportionalHeight) { - var component = Qt.createComponent("qrc:/qml/dialogs/ImageOverlay.qml") + var component = Qt.createComponent("qrc:/resources/qml/dialogs/ImageOverlay.qml"); if (component.status == Component.Ready) { var dialog = component.createObject(timelineRoot, { "room": room, @@ -293,22 +265,33 @@ Pane { "url": url, "originalWidth": originalWidth ?? 0, "proportionalHeight": proportionalHeight ?? 0 - } - ); + }); dialog.showFullScreen(); destroyOnClose(dialog); } else { console.error("Failed to create component: " + component.errorString()); } } + function onShowImagePackSettings(room, packlist) { + var component = Qt.createComponent("qrc:/resources/qml/dialogs/ImagePackSettingsDialog.qml"); + if (component.status == Component.Ready) { + var packSet = component.createObject(timelineRoot, { + "room": room, + "packlist": packlist + }); + packSet.show(); + destroyOnClose(packSet); + } else { + console.error("Failed to create component: " + component.errorString()); + } + } target: TimelineManager } - Connections { function onNewInviteState() { if (CallManager.haveCallInvite && Settings.mobileMode) { - var component = Qt.createComponent("qrc:/qml/voip/CallInvite.qml") + var component = Qt.createComponent("qrc:/resources/qml/voip/CallInvite.qml"); if (component.status == Component.Ready) { var dialog = component.createObject(timelineRoot); dialog.open(); @@ -321,141 +304,97 @@ Pane { target: CallManager } - SelfVerificationCheck { } - InputDialog { id: uiaPassPrompt echoMode: TextInput.Password - title: UIA.title prompt: qsTr("Please enter your login password to continue:") - onAccepted: (t) => { + title: UIA.title + + onAccepted: t => { return UIA.continuePassword(t); } } - InputDialog { id: uiaEmailPrompt - title: UIA.title prompt: qsTr("Please enter a valid email address to continue:") - onAccepted: (t) => { + title: UIA.title + + onAccepted: t => { return UIA.continueEmail(t); } } - PhoneNumberInputDialog { id: uiaPhoneNumberPrompt - title: UIA.title prompt: qsTr("Please enter a valid phone number to continue:") + title: UIA.title + 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) => { + title: UIA.title + + 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 onEmail() { + uiaEmailPrompt.show(); + } function onError(msg) { uiaErrorDialog.text = msg; uiaErrorDialog.open(); } + function onPassword() { + console.log("UIA: password needed"); + uiaPassPrompt.show(); + } + function onPhoneNumber() { + uiaPhoneNumberPrompt.show(); + } + function onPrompt3pidToken() { + uiaTokenPrompt.show(); + } target: UIA } - StackView { id: mainWindow - anchors.fill: parent - initialItem: welcomePage - - Transition { - id: reducedMotionTransitionExit - PropertyAnimation { - property: "opacity" - from: 1 - to:0 - duration: 200 - } - } - Transition { - id: reducedMotionTransitionEnter - SequentialAnimation { - PropertyAction { property: "opacity"; value: 0 } - PauseAnimation { duration: 200 } - PropertyAnimation { - property: "opacity" - from: 0 - to:1 - duration: 200 - } - } - } + property Transition popEnterOrg + property Transition popExitOrg // for some reason direct bindings to a hidden StackView don't work, so manually store and restore here. property Transition pushEnterOrg property Transition pushExitOrg - property Transition popEnterOrg - property Transition popExitOrg property Transition replaceEnterOrg property Transition replaceExitOrg - Component.onCompleted: { - pushEnterOrg = pushEnter; - popEnterOrg = popEnter; - replaceEnterOrg = replaceEnter; - pushExitOrg = pushExit; - popExitOrg = popExit; - replaceExitOrg = replaceExit; - - updateTrans() - } function updateTrans() { pushEnter = Settings.reducedMotion ? reducedMotionTransitionEnter : pushEnterOrg; @@ -466,65 +405,104 @@ Pane { replaceExit = Settings.reducedMotion ? reducedMotionTransitionExit : replaceExitOrg; } + anchors.fill: parent + initialItem: welcomePage + + Component.onCompleted: { + pushEnterOrg = pushEnter; + popEnterOrg = popEnter; + replaceEnterOrg = replaceEnter; + pushExitOrg = pushExit; + popExitOrg = popExit; + replaceExitOrg = replaceExit; + updateTrans(); + } + + Transition { + id: reducedMotionTransitionExit + + PropertyAnimation { + duration: 200 + from: 1 + property: "opacity" + to: 0 + } + } + Transition { + id: reducedMotionTransitionEnter + + SequentialAnimation { + PropertyAction { + property: "opacity" + value: 0 + } + PauseAnimation { + duration: 200 + } + PropertyAnimation { + duration: 200 + from: 0 + property: "opacity" + to: 1 + } + } + } Connections { - target: Settings function onReducedMotionChanged() { mainWindow.updateTrans(); } + + target: Settings } } - Component { id: welcomePage WelcomePage { } } - Component { id: chatPage ChatPage { } } - Component { id: loginPage LoginPage { } } - Component { id: registerPage RegisterPage { } } - Component { id: userSettingsPage UserSettingsPage { } + } + Snackbar { + id: snackbar } - - - Snackbar { id: snackbar } - Connections { - function onSwitchToChatPage() { - mainWindow.replace(null, chatPage); - } - function onSwitchToLoginPage(error) { - mainWindow.replace(welcomePage, {}, loginPage, {"error": error}, StackView.PopTransition); - } function onShowNotification(msg) { snackbar.showNotification(msg); console.log("New snack: " + msg); } + function onSwitchToChatPage() { + mainWindow.replace(null, chatPage); + } + function onSwitchToLoginPage(error) { + mainWindow.replace(welcomePage, {}, loginPage, { + "error": error + }, StackView.PopTransition); + } + target: MainWindow } - } diff --git a/resources/qml/ScrollHelper.qml b/resources/qml/ScrollHelper.qml deleted file mode 100644 index 04d060ec..00000000 --- a/resources/qml/ScrollHelper.qml +++ /dev/null @@ -1,96 +0,0 @@ -// Copyright (C) 2016 Michael Bohlender, -// Copyright (C) 2017 Christian Mollekopf, -// SPDX-FileCopyrightText: Nheko Contributors -// -// SPDX-License-Identifier: GPL-3.0-or-later - -/* -* Shamelessly stolen from: -* https://cgit.kde.org/kube.git/tree/framework/qml/ScrollHelper.qml -* -* The MouseArea + interactive: false + maximumFlickVelocity are required -* to fix scrolling for desktop systems where we don't want flicking behaviour. -* -* See also: -* ScrollView.qml in qtquickcontrols -* qquickwheelarea.cpp in qtquickcontrols -*/ - -import QtQuick 2.9 -import QtQuick.Controls 2.3 - -MouseArea { - // console.warn("Delta: ", wheel.pixelDelta.y); - // console.warn("Old position: ", flickable.contentY); - // console.warn("New position: ", newPos); - // breaks ListView's with headers... - //if (typeof (flickableItem.headerItem) !== "undefined" && flickableItem.headerItem) - // minYExtent += flickableItem.headerItem.height; - - id: root - - property Flickable flickable - property alias enabled: root.enabled - - function calculateNewPosition(flickableItem, wheel) { - //Nothing to scroll - if (flickableItem.contentHeight < flickableItem.height) - return flickableItem.contentY; - - //Ignore 0 events (happens at least with Christians trackpad) - if (wheel.pixelDelta.y == 0 && wheel.angleDelta.y == 0) - return flickableItem.contentY; - - //pixelDelta seems to be the same as angleDelta/8 - var pixelDelta = 0; - //The pixelDelta is a smaller number if both are provided, so pixelDelta can be 0 while angleDelta is still something. So we check the angleDelta - if (wheel.angleDelta.y) { - var wheelScrollLines = 3; //Default value of QApplication wheelScrollLines property - var pixelPerLine = 20; //Default value in Qt, originally comes from QTextEdit - var ticks = (wheel.angleDelta.y / 8) / 15; //Divide by 8 gives us pixels typically come in 15pixel steps. - pixelDelta = ticks * pixelPerLine * wheelScrollLines; - } else { - pixelDelta = wheel.pixelDelta.y; - } - pixelDelta = Math.round(pixelDelta); - if (!pixelDelta) - return flickableItem.contentY; - - var minYExtent = flickableItem.originY + flickableItem.topMargin; - var maxYExtent = (flickableItem.contentHeight + flickableItem.bottomMargin + flickableItem.originY) - flickableItem.height; - //Avoid overscrolling - return Math.max(minYExtent, Math.min(maxYExtent, flickableItem.contentY - pixelDelta)); - } - - propagateComposedEvents: true - //Place the mouse area under the flickable - z: -1 - onFlickableChanged: { - if (enabled) { - flickable.maximumFlickVelocity = 100000; - flickable.boundsBehavior = Flickable.StopAtBounds; - root.parent = flickable; - } - } - acceptedButtons: Qt.NoButton - onWheel: { - var newPos = calculateNewPosition(flickable, wheel); - // Show the scrollbars - flickable.flick(0, 0); - flickable.contentY = newPos; - cancelFlickStateTimer.restart(); - } - - Timer { - id: cancelFlickStateTimer - - //How long the scrollbar will remain visible - interval: 500 - // Hide the scrollbars - onTriggered: { - flickable.cancelFlick(); - flickable.movementEnded(); - } - } - -} diff --git a/resources/qml/SelfVerificationCheck.qml b/resources/qml/SelfVerificationCheck.qml index 4f2d9202..1752df0e 100644 --- a/resources/qml/SelfVerificationCheck.qml +++ b/resources/qml/SelfVerificationCheck.qml @@ -10,22 +10,32 @@ import QtQuick.Layouts 1.3 import im.nheko 1.0 Item { - visible: false enabled: false + visible: 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 + height: content.height + implicitFooterHeight + implicitHeaderHeight + modal: true + padding: 0 + + // Workaround palettes not inheriting for popups + palette: timelineRoot.palette + parent: Overlay.overlay + standardButtons: Dialog.Ok + width: content.width + + background: Rectangle { + border.color: Nheko.theme.separator + border.width: 1 + color: palette.window + radius: Nheko.paddingSmall + } ColumnLayout { id: content @@ -33,45 +43,33 @@ Item { spacing: 0 Label { + Layout.fillWidth: true Layout.margins: Nheko.paddingMedium Layout.maximumWidth: (Overlay.overlay ? Overlay.overlay.width : 400) - Nheko.paddingMedium * 4 - Layout.fillWidth: true + color: palette.text 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 + Layout.maximumWidth: (Overlay.overlay ? Overlay.overlay.width : 400) - Nheko.paddingMedium * 4 + color: palette.text + font.bold: true horizontalAlignment: TextEdit.AlignHCenter - verticalAlignment: TextEdit.AlignVCenter readOnly: true selectByMouse: true text: showRecoverKeyDialog.recoveryKey - color: Nheko.colors.text - font.bold: true + verticalAlignment: TextEdit.AlignVCenter 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 @@ -80,85 +78,89 @@ Item { buttons: P.MessageDialog.Ok text: qsTr("Failed to setup encryption: %1").arg(errorMessage) } - MainWindowDialog { id: bootstrapCrosssigning + // Workaround palettes not inheriting for popups + palette: timelineRoot.palette + + background: Rectangle { + border.color: Nheko.theme.separator + border.width: 1 + color: palette.window + radius: Nheko.paddingSmall + } + onAccepted: SelfVerificationStatus.setupCrosssigning(storeSecretsOnline.checked, usePassword.checked ? passwordField.text : "", useOnlineKeyBackup.checked) GridLayout { id: grid - width: bootstrapCrosssigning.useableWidth + columnSpacing: 0 columns: 2 rowSpacing: 0 - columnSpacing: 0 + width: bootstrapCrosssigning.useableWidth z: 1 Label { - Layout.margins: Nheko.paddingMedium Layout.alignment: Qt.AlignHCenter Layout.columnSpan: 2 + Layout.margins: Nheko.paddingMedium + color: palette.text 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.margins: Nheko.paddingMedium Layout.maximumWidth: grid.width - Nheko.paddingMedium * 2 + color: palette.text 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.margins: Nheko.paddingMedium Layout.maximumWidth: Math.floor(grid.width / 2) - Nheko.paddingMedium * 2 + color: palette.text 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 + Layout.margins: Nheko.paddingMedium + Layout.preferredHeight: storeSecretsOnline.height 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.margins: Nheko.paddingMedium Layout.maximumWidth: Math.floor(grid.width / 2) - Nheko.paddingMedium * 2 - visible: storeSecretsOnline.checked + Layout.rowSpan: 2 + color: palette.text 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 + visible: storeSecretsOnline.checked 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 + Layout.margins: Nheko.paddingMedium + Layout.preferredHeight: storeSecretsOnline.height + Layout.rowSpan: usePassword.checked ? 1 : 2 + Layout.topMargin: Nheko.paddingLarge visible: storeSecretsOnline.checked ToggleButton { @@ -166,113 +168,108 @@ Item { 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.maximumWidth: Math.floor(grid.width / 2) - Nheko.paddingMedium * 2 + echoMode: TextInput.Password + visible: storeSecretsOnline.checked && usePassword.checked + } + Label { Layout.alignment: Qt.AlignLeft Layout.columnSpan: 1 + Layout.margins: Nheko.paddingMedium Layout.maximumWidth: Math.floor(grid.width / 2) - Nheko.paddingMedium * 2 + color: palette.text 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 + Layout.margins: Nheko.paddingMedium + Layout.preferredHeight: storeSecretsOnline.height 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 + // Workaround palettes not inheriting for popups + palette: timelineRoot.palette standardButtons: Dialog.Cancel GridLayout { id: masterGrid - width: verifyMasterKey.useableWidth columns: 1 + width: verifyMasterKey.useableWidth z: 1 Label { - Layout.margins: Nheko.paddingMedium Layout.alignment: Qt.AlignHCenter + Layout.margins: Nheko.paddingMedium + color: palette.text //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.margins: Nheko.paddingMedium //Layout.columnSpan: 2 Layout.maximumWidth: grid.width - Nheko.paddingMedium * 2 + color: palette.text 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") + visible: SelfVerificationStatus.hasSSSS + onClicked: { SelfVerificationStatus.verifyMasterKeyWithPassphrase(); verifyMasterKey.close(); } } - } - } - Connections { + function onSetupCompleted() { + successDialog.open(); + } + function onSetupFailed(m) { + failureDialog.errorMessage = m; + failureDialog.open(); + } + function onShowRecoveryKey(key) { + showRecoverKeyDialog.recoveryKey = key; + showRecoverKeyDialog.open(); + } function onStatusChanged() { console.log("STATUS CHANGED: " + SelfVerificationStatus.status); if (SelfVerificationStatus.status == SelfVerificationStatus.NoMasterKey) { @@ -285,21 +282,6 @@ Item { } } - 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/StatusIndicator.qml b/resources/qml/StatusIndicator.qml index 862f9d7a..4a305ac5 100644 --- a/resources/qml/StatusIndicator.qml +++ b/resources/qml/StatusIndicator.qml @@ -9,15 +9,9 @@ import im.nheko 1.0 ImageButton { id: indicator - required property int status required property string eventId + required property int status - width: 16 - height: 16 - hoverEnabled: true - changeColorOnHover: (status == MtxEvent.Read) - cursor: (status == MtxEvent.Read) ? Qt.PointingHandCursor : Qt.ArrowCursor - ToolTip.visible: hovered && status != MtxEvent.Empty ToolTip.text: { switch (status) { case MtxEvent.Failed: @@ -32,11 +26,11 @@ ImageButton { return ""; } } - onClicked: { - if (status == MtxEvent.Read) - room.showReadReceipts(eventId); - - } + ToolTip.visible: hovered && status != MtxEvent.Empty + changeColorOnHover: (status == MtxEvent.Read) + cursor: (status == MtxEvent.Read) ? Qt.PointingHandCursor : Qt.ArrowCursor + height: 16 + hoverEnabled: true image: { switch (status) { case MtxEvent.Failed: @@ -51,4 +45,10 @@ ImageButton { return ""; } } + width: 16 + + onClicked: { + if (status == MtxEvent.Read) + room.showReadReceipts(eventId); + } } diff --git a/resources/qml/TimelineRow.qml b/resources/qml/TimelineRow.qml index d9deefa0..16a31a3c 100644 --- a/resources/qml/TimelineRow.qml +++ b/resources/qml/TimelineRow.qml @@ -13,72 +13,44 @@ import im.nheko 1.0 AbstractButton { id: r - required property double proportionalHeight - required property int type - required property string typeString - required property int originalWidth required property string blurhash required property string body - required property string formattedBody + required property string callType + required property int duration + required property int encryptionError required property string eventId required property string filename required property string filesize - required property string url - required property string thumbnailUrl - required property bool isOnlyEmoji - required property bool isSender - required property bool isEncrypted + required property string formattedBody + required property int index required property bool isEditable required property bool isEdited + required property bool isEncrypted + required property bool isOnlyEmoji + required property bool isSender required property bool isStateEvent + required property int notificationlevel + required property int originalWidth + required property double proportionalHeight + required property var reactions + required property int relatedEventCacheBuster required property string replyTo + required property string roomName + required property string roomTopic + required property int status required property string threadId + required property string thumbnailUrl + required property var timestamp + required property int trustlevel + required property int type + required property string typeString + required property string url required property string userId required property string userName - required property string roomTopic - required property string roomName - required property string callType - required property var reactions - required property int trustlevel - required property int notificationlevel - required property int encryptionError - required property int duration - required property var timestamp - required property int status - required property int index - required property int relatedEventCacheBuster + height: row.height + (reactionRow.height > 0 ? reactionRow.height - 2 : 0) + unreadRow.height hoverEnabled: true - width: parent.width - height: row.height+(reactionRow.height > 0 ? reactionRow.height-2 : 0 )+unreadRow.height - - Rectangle { - color: (Settings.messageHoverHighlight && hovered) ? Nheko.colors.alternateBase : "transparent" - anchors.fill: parent - // this looks better without margins - TapHandler { - acceptedButtons: Qt.RightButton - onSingleTapped: messageContextMenu.show(eventId, threadId, type, isSender, isEncrypted, isEditable, contentItem.child.hoveredLink, contentItem.child.copyText) - gesturePolicy: TapHandler.ReleaseWithinBounds - acceptedDevices: PointerDevice.Mouse | PointerDevice.Stylus | PointerDevice.TouchPad - } - } - - - onPressAndHold: messageContextMenu.show(eventId, threadId, type, isSender, isEncrypted, isEditable, contentItem.child.hoveredLink, contentItem.child.copyText) - onDoubleClicked: room.reply = eventId - - DragHandler { - id: draghandler - yAxis.enabled: false - xAxis.maximum: 100 - xAxis.minimum: -100 - onActiveChanged: { - if(!active && (x < -70 || x > 70)) - room.reply = eventId - } - } states: State { name: "dragging" when: draghandler.active @@ -86,265 +58,292 @@ AbstractButton { transitions: Transition { from: "dragging" to: "" + PropertyAnimation { - target: r - properties: "x" - easing.type: Easing.InOutQuad - to: 0 duration: 100 + easing.type: Easing.InOutQuad + properties: "x" + target: r + to: 0 } } onClicked: { - let link = contentItem.child.linkAt != undefined && contentItem.child.linkAt(pressX-row.x-msg.x, pressY-row.y-msg.y-contentItem.y); + let link = contentItem.child.linkAt != undefined && contentItem.child.linkAt(pressX - row.x - msg.x, pressY - row.y - msg.y - contentItem.y); if (link) { - Nheko.openLink(link) + Nheko.openLink(link); } } + onDoubleClicked: room.reply = eventId + onPressAndHold: messageContextMenu.show(eventId, threadId, type, isSender, isEncrypted, isEditable, contentItem.child.hoveredLink, contentItem.child.copyText) + Rectangle { + anchors.fill: parent + color: (Settings.messageHoverHighlight && hovered) ? palette.alternateBase : "transparent" + + // this looks better without margins + TapHandler { + acceptedButtons: Qt.RightButton + acceptedDevices: PointerDevice.Mouse | PointerDevice.Stylus | PointerDevice.TouchPad + gesturePolicy: TapHandler.ReleaseWithinBounds + + onSingleTapped: messageContextMenu.show(eventId, threadId, type, isSender, isEncrypted, isEditable, contentItem.child.hoveredLink, contentItem.child.copyText) + } + } + DragHandler { + id: draghandler + + xAxis.maximum: 100 + xAxis.minimum: -100 + yAxis.enabled: false + + onActiveChanged: { + if (!active && (x < -70 || x > 70)) + room.reply = eventId; + } + } AbstractButton { - anchors.leftMargin: Settings.smallAvatars? 0 : (Nheko.avatarSize + 8) // align bubble with section header + ToolTip.delay: Nheko.tooltipDelay + ToolTip.text: qsTr("Part of a thread") + ToolTip.visible: hovered anchors.left: parent.left + anchors.leftMargin: Settings.smallAvatars ? 0 : (Nheko.avatarSize + 8) // align bubble with section header + height: parent.height visible: threadId width: 4 - height: parent.height + + onClicked: room.thread = threadId Rectangle { id: threadLine - color: TimelineManager.userColor(threadId, Nheko.colors.base) anchors.fill: parent + color: TimelineManager.userColor(threadId, palette.base) } - - ToolTip.visible: hovered - ToolTip.delay: Nheko.tooltipDelay - ToolTip.text: qsTr("Part of a thread") - onClicked: room.thread = threadId } - Rectangle { id: row - property bool bubbleOnRight : isSender && Settings.bubbles - anchors.leftMargin: (isStateEvent || Settings.smallAvatars? 0 : (Nheko.avatarSize + 8)) + (threadId ? 6 : 0) // align bubble with section header - anchors.left: (isStateEvent || bubbleOnRight) ? undefined : parent.left - anchors.right: (isStateEvent || !bubbleOnRight) ? undefined : parent.right - anchors.horizontalCenter: isStateEvent? parent.horizontalCenter : undefined - property int maxWidth: (parent.width-(Settings.smallAvatars || isStateEvent? 0 : Nheko.avatarSize+8))*(Settings.bubbles && !isStateEvent? 0.9 : 1) - width: Settings.bubbles? Math.min(maxWidth,Math.max(reply.implicitWidth+8,contentItem.implicitWidth+metadata.width+20)) : maxWidth - height: msg.height+msg.anchors.margins*2 - property color userColor: TimelineManager.userColor(userId, Nheko.colors.base) - property color bgColor: Nheko.colors.base - color: (Settings.bubbles && !isStateEvent) ? Qt.tint(bgColor, Qt.hsla(userColor.hslHue, 0.5, userColor.hslLightness, 0.2)) : "#00000000" - radius: 4 - border.width: r.notificationlevel == MtxEvent.Highlight ? 1 : 0 + property color bgColor: palette.base + property bool bubbleOnRight: isSender && Settings.bubbles + property int maxWidth: (parent.width - (Settings.smallAvatars || isStateEvent ? 0 : Nheko.avatarSize + 8)) * (Settings.bubbles && !isStateEvent ? 0.9 : 1) + property color userColor: TimelineManager.userColor(userId, palette.base) + + anchors.horizontalCenter: isStateEvent ? parent.horizontalCenter : undefined + anchors.left: (isStateEvent || bubbleOnRight) ? undefined : parent.left + anchors.leftMargin: (isStateEvent || Settings.smallAvatars ? 0 : (Nheko.avatarSize + 8)) + (threadId ? 6 : 0) // align bubble with section header + anchors.right: (isStateEvent || !bubbleOnRight) ? undefined : parent.right border.color: Nheko.theme.red + border.width: r.notificationlevel == MtxEvent.Highlight ? 1 : 0 + color: (Settings.bubbles && !isStateEvent) ? Qt.tint(bgColor, Qt.hsla(userColor.hslHue, 0.5, userColor.hslLightness, 0.2)) : "#00000000" + height: msg.height + msg.anchors.margins * 2 + radius: 4 + width: Settings.bubbles ? Math.min(maxWidth, Math.max(reply.implicitWidth + 8, contentItem.implicitWidth + metadata.width + 20)) : maxWidth GridLayout { + id: msg + + columnSpacing: 2 + columns: Settings.bubbles ? 1 : 2 + rowSpacing: 0 + rows: Settings.bubbles ? 3 : 2 + anchors { left: parent.left - top: parent.top - right: parent.right - margins: (Settings.bubbles && ! isStateEvent)? 4 : 2 leftMargin: 4 + margins: (Settings.bubbles && !isStateEvent) ? 4 : 2 + right: parent.right rightMargin: 4 + top: parent.top } - id: msg - rowSpacing: 0 - columnSpacing: 2 - columns: Settings.bubbles? 1 : 2 - rows: Settings.bubbles? 3 : 2 // fancy reply, if this is a reply Reply { - Layout.row: 0 - Layout.column: 0 - Layout.fillWidth: true - Layout.maximumWidth: Settings.bubbles? Number.MAX_VALUE : implicitWidth - Layout.bottomMargin: visible? 2 : 0 - Layout.preferredHeight: height id: reply function fromModel(role) { return replyTo != "" ? room.dataById(replyTo, role, r.eventId) : null; } - visible: replyTo - userColor: r.relatedEventCacheBuster, TimelineManager.userColor(userId, Nheko.colors.base) + + Layout.bottomMargin: visible ? 2 : 0 + Layout.column: 0 + Layout.fillWidth: true + Layout.maximumWidth: Settings.bubbles ? Number.MAX_VALUE : implicitWidth + Layout.preferredHeight: height + Layout.row: 0 blurhash: r.relatedEventCacheBuster, fromModel(Room.Blurhash) ?? "" body: r.relatedEventCacheBuster, fromModel(Room.Body) ?? "" - formattedBody: r.relatedEventCacheBuster, fromModel(Room.FormattedBody) ?? "" + callType: r.relatedEventCacheBuster, fromModel(Room.Voip) ?? "" + duration: r.relatedEventCacheBuster, fromModel(Room.Duration) ?? 0 + encryptionError: r.relatedEventCacheBuster, fromModel(Room.EncryptionError) ?? 0 eventId: fromModel(Room.EventId) ?? "" filename: r.relatedEventCacheBuster, fromModel(Room.Filename) ?? "" filesize: r.relatedEventCacheBuster, fromModel(Room.Filesize) ?? "" + formattedBody: r.relatedEventCacheBuster, fromModel(Room.FormattedBody) ?? "" + isOnlyEmoji: r.relatedEventCacheBuster, fromModel(Room.IsOnlyEmoji) ?? false + isStateEvent: r.relatedEventCacheBuster, fromModel(Room.IsStateEvent) ?? false + originalWidth: r.relatedEventCacheBuster, fromModel(Room.OriginalWidth) ?? 0 proportionalHeight: r.relatedEventCacheBuster, fromModel(Room.ProportionalHeight) ?? 1 + relatedEventCacheBuster: r.relatedEventCacheBuster, fromModel(Room.RelatedEventCacheBuster) ?? 0 + roomName: r.relatedEventCacheBuster, fromModel(Room.RoomName) ?? "" + roomTopic: r.relatedEventCacheBuster, fromModel(Room.RoomTopic) ?? "" + thumbnailUrl: r.relatedEventCacheBuster, fromModel(Room.ThumbnailUrl) ?? "" type: r.relatedEventCacheBuster, fromModel(Room.Type) ?? MtxEvent.UnknownMessage typeString: r.relatedEventCacheBuster, fromModel(Room.TypeString) ?? "" url: r.relatedEventCacheBuster, fromModel(Room.Url) ?? "" - originalWidth: r.relatedEventCacheBuster, fromModel(Room.OriginalWidth) ?? 0 - isOnlyEmoji: r.relatedEventCacheBuster, fromModel(Room.IsOnlyEmoji) ?? false - isStateEvent: r.relatedEventCacheBuster, fromModel(Room.IsStateEvent) ?? false + userColor: r.relatedEventCacheBuster, TimelineManager.userColor(userId, palette.base) userId: r.relatedEventCacheBuster, fromModel(Room.UserId) ?? "" userName: r.relatedEventCacheBuster, fromModel(Room.UserName) ?? "" - thumbnailUrl: r.relatedEventCacheBuster, fromModel(Room.ThumbnailUrl) ?? "" - duration: r.relatedEventCacheBuster, fromModel(Room.Duration) ?? "" - roomTopic: r.relatedEventCacheBuster, fromModel(Room.RoomTopic) ?? "" - roomName: r.relatedEventCacheBuster, fromModel(Room.RoomName) ?? "" - callType: r.relatedEventCacheBuster, fromModel(Room.CallType) ?? "" - encryptionError: r.relatedEventCacheBuster, fromModel(Room.EncryptionError) ?? "" - relatedEventCacheBuster: r.relatedEventCacheBuster, fromModel(Room.RelatedEventCacheBuster) ?? 0 + visible: replyTo } // actual message content MessageDelegate { - Layout.row: 1 + id: contentItem + Layout.column: 0 Layout.fillWidth: true Layout.preferredHeight: height - id: contentItem - + Layout.row: 1 blurhash: r.blurhash body: r.body - formattedBody: r.formattedBody + callType: r.callType + duration: r.duration + encryptionError: r.encryptionError eventId: r.eventId filename: r.filename filesize: r.filesize + formattedBody: r.formattedBody + isOnlyEmoji: r.isOnlyEmoji + isReply: false + isStateEvent: r.isStateEvent + metadataWidth: metadata.width + originalWidth: r.originalWidth proportionalHeight: r.proportionalHeight + relatedEventCacheBuster: r.relatedEventCacheBuster + roomName: r.roomName + roomTopic: r.roomTopic + thumbnailUrl: r.thumbnailUrl type: r.type typeString: r.typeString ?? "" url: r.url - thumbnailUrl: r.thumbnailUrl - duration: r.duration - originalWidth: r.originalWidth - isOnlyEmoji: r.isOnlyEmoji - isStateEvent: r.isStateEvent userId: r.userId userName: r.userName - roomTopic: r.roomTopic - roomName: r.roomName - callType: r.callType - encryptionError: r.encryptionError - relatedEventCacheBuster: r.relatedEventCacheBuster - isReply: false - metadataWidth: metadata.width } - Row { id: metadata - Layout.column: Settings.bubbles? 0 : 1 - Layout.row: Settings.bubbles? 2 : 0 - Layout.rowSpan: Settings.bubbles? 1 : 2 - Layout.bottomMargin: -2 - Layout.topMargin: (contentItem.fitsMetadata && Settings.bubbles)? -height-Layout.bottomMargin : 0 + + property int iconSize: Math.floor(fontMetrics.ascent * scaling) + property double scaling: Settings.bubbles ? 0.75 : 1 + Layout.alignment: Qt.AlignTop | Qt.AlignRight + Layout.bottomMargin: -2 + Layout.column: Settings.bubbles ? 0 : 1 Layout.preferredWidth: implicitWidth - visible: !isStateEvent + Layout.row: Settings.bubbles ? 2 : 0 + Layout.rowSpan: Settings.bubbles ? 1 : 2 + Layout.topMargin: (contentItem.fitsMetadata && Settings.bubbles) ? -height - Layout.bottomMargin : 0 spacing: 2 - - property double scaling: Settings.bubbles? 0.75 : 1 - - property int iconSize: Math.floor(fontMetrics.ascent*scaling) + visible: !isStateEvent StatusIndicator { Layout.alignment: Qt.AlignRight | Qt.AlignTop - height: parent.iconSize - width: parent.iconSize - status: r.status - eventId: r.eventId anchors.verticalCenter: ts.verticalCenter - } - - Image { - visible: isEdited || eventId == room.edit - Layout.alignment: Qt.AlignRight | Qt.AlignTop + eventId: r.eventId height: parent.iconSize + status: r.status width: parent.iconSize - sourceSize.width: parent.iconSize * Screen.devicePixelRatio - sourceSize.height: parent.iconSize * Screen.devicePixelRatio - source: "image://colorimage/:/icons/icons/ui/edit.svg?" + ((eventId == room.edit) ? Nheko.colors.highlight : Nheko.colors.buttonText) - ToolTip.visible: editHovered.hovered + } + Image { + Layout.alignment: Qt.AlignRight | Qt.AlignTop ToolTip.delay: Nheko.tooltipDelay ToolTip.text: qsTr("Edited") + ToolTip.visible: editHovered.hovered anchors.verticalCenter: ts.verticalCenter + height: parent.iconSize + source: "image://colorimage/:/icons/icons/ui/edit.svg?" + ((eventId == room.edit) ? palette.highlight : palette.buttonText) + sourceSize.height: parent.iconSize * Screen.devicePixelRatio + sourceSize.width: parent.iconSize * Screen.devicePixelRatio + visible: isEdited || eventId == room.edit + width: parent.iconSize HoverHandler { id: editHovered + } - } - ImageButton { - visible: threadId Layout.alignment: Qt.AlignRight | Qt.AlignTop - height: parent.iconSize - width: parent.iconSize - image: ":/icons/icons/ui/thread.svg" - buttonTextColor: TimelineManager.userColor(threadId, Nheko.colors.base) - ToolTip.visible: hovered ToolTip.delay: Nheko.tooltipDelay ToolTip.text: qsTr("Part of a thread") + ToolTip.visible: hovered anchors.verticalCenter: ts.verticalCenter + buttonTextColor: TimelineManager.userColor(threadId, palette.base) + height: parent.iconSize + image: ":/icons/icons/ui/thread.svg" + visible: threadId + width: parent.iconSize + onClicked: room.thread = threadId } - EncryptionIndicator { - visible: room.isEncrypted - encrypted: isEncrypted - trust: trustlevel Layout.alignment: Qt.AlignRight | Qt.AlignTop - height: parent.iconSize - width: parent.iconSize - sourceSize.width: parent.iconSize * Screen.devicePixelRatio - sourceSize.height: parent.iconSize * Screen.devicePixelRatio anchors.verticalCenter: ts.verticalCenter + encrypted: isEncrypted + height: parent.iconSize + sourceSize.height: parent.iconSize * Screen.devicePixelRatio + sourceSize.width: parent.iconSize * Screen.devicePixelRatio + trust: trustlevel + visible: room.isEncrypted + width: parent.iconSize } - Label { id: ts + Layout.alignment: Qt.AlignRight | Qt.AlignTop Layout.preferredWidth: implicitWidth - text: timestamp.toLocaleTimeString(Locale.ShortFormat) - color: Nheko.inactiveColors.text - ToolTip.visible: ma.hovered ToolTip.delay: Nheko.tooltipDelay ToolTip.text: Qt.formatDateTime(timestamp, Qt.DefaultLocaleLongDate) - font.pointSize: fontMetrics.font.pointSize*parent.scaling + ToolTip.visible: ma.hovered + color: palette.inactive.text + font.pointSize: fontMetrics.font.pointSize * parent.scaling + text: timestamp.toLocaleTimeString(Locale.ShortFormat) + HoverHandler { id: ma - } + } } } } } - Reactions { - anchors { - top: row.bottom - topMargin: -4 - left: row.bubbleOnRight? undefined : row.left - right: row.bubbleOnRight? row.right : undefined - } - width: row.maxWidth - layoutDirection: row.bubbleOnRight? Qt.RightToLeft : Qt.LeftToRight - id: reactionRow - reactions: r.reactions eventId: r.eventId - } + layoutDirection: row.bubbleOnRight ? Qt.RightToLeft : Qt.LeftToRight + reactions: r.reactions + width: row.maxWidth + anchors { + left: row.bubbleOnRight ? undefined : row.left + right: row.bubbleOnRight ? row.right : undefined + top: row.bottom + topMargin: -4 + } + } Rectangle { id: unreadRow + + color: palette.highlight + height: visible ? 3 : 0 + visible: (r.index > 0 && (room.fullyReadEventId == r.eventId)) + anchors { - top: reactionRow.bottom - topMargin: 5 left: parent.left right: parent.right + top: reactionRow.bottom + topMargin: 5 } - color: Nheko.colors.highlight - - visible: (r.index > 0 && (room.fullyReadEventId == r.eventId)) - height: visible ? 3 : 0 - } } diff --git a/resources/qml/TimelineView.qml b/resources/qml/TimelineView.qml index c8b22616..bbcf2366 100644 --- a/resources/qml/TimelineView.qml +++ b/resources/qml/TimelineView.qml @@ -8,101 +8,97 @@ import "./device-verification" import "./emoji" import "./ui" import "./voip" -import Qt.labs.platform 1.1 as Platform -import QtQuick 2.15 -import QtQuick.Controls 2.5 -import QtQuick.Layouts 1.3 -import QtQuick.Particles 2.15 -import QtQuick.Window 2.13 -import im.nheko 1.0 -import im.nheko.EmojiModel 1.0 +import Qt.labs.platform as Platform +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import QtQuick.Particles +import QtQuick.Window +import im.nheko Item { id: timelineView + required property PrivacyScreen privacyScreen property var room: null property var roomPreview: null - property bool showBackButton: false property bool shouldEffectsRun: false - required property PrivacyScreen privacyScreen + property bool showBackButton: false + clip: true - onRoomChanged: if (room != null) room.triggerSpecialEffects() - - StickerPicker { - id: emojiPopup - - colors: Nheko.colors - emoji: true - } - // focus message input on key press, but not on Ctrl-C and such. - Keys.onPressed: { + Keys.onPressed: event => { if (event.text && event.key !== Qt.Key_Enter && event.key !== Qt.Key_Return && !topBar.searchHasFocus) { TimelineManager.focusMessageInput(); room.input.setText(room.input.text + event.text); } } + onRoomChanged: if (room != null) + room.triggerSpecialEffects() + StickerPicker { + id: emojiPopup + + emoji: true + } Shortcut { sequence: StandardKey.Close + onActivated: Rooms.resetCurrentRoom() } - Label { - visible: !room && !TimelineManager.isInitialSync && (!roomPreview || !roomPreview.roomid) anchors.centerIn: parent - text: qsTr("No room open") font.pointSize: 24 - color: Nheko.colors.text + text: qsTr("No room open") + visible: !room && !TimelineManager.isInitialSync && (!roomPreview || !roomPreview.roomid) } - Spinner { - visible: TimelineManager.isInitialSync anchors.centerIn: parent - foreground: Nheko.colors.mid - running: TimelineManager.isInitialSync + foreground: palette.mid // height is somewhat arbitrary here... don't set width because width scales w/ height height: parent.height / 16 - z: 3 opacity: hh.hovered ? 0.3 : 1 + running: TimelineManager.isInitialSync + visible: TimelineManager.isInitialSync + z: 3 - Behavior on opacity { - NumberAnimation { duration: 100; } + Behavior on opacity { + NumberAnimation { + duration: 100 + } } HoverHandler { id: hh + } } - ColumnLayout { id: timelineLayout - visible: room != null && !room.isSpace - enabled: visible anchors.fill: parent + enabled: visible spacing: 0 + visible: room != null && !room.isSpace TopBar { id: topBar showBackButton: timelineView.showBackButton } - Rectangle { Layout.fillWidth: true + color: Nheko.theme.separator height: 1 z: 3 - color: Nheko.theme.separator } - Rectangle { id: msgView - Layout.fillWidth: true Layout.fillHeight: true - color: Nheko.colors.base + Layout.fillWidth: true + color: palette.base ColumnLayout { anchors.fill: parent @@ -120,143 +116,121 @@ Item { target: timelineView } - MessageView { + Layout.fillWidth: true implicitHeight: msgView.height - typingIndicator.height searchString: topBar.searchString - Layout.fillWidth: true } - Loader { - source: CallManager.isOnCall && CallManager.callType != CallType.VOICE ? "voip/VideoCall.qml" : "" + source: CallManager.isOnCall && CallManager.callType != Voip.VOICE ? "voip/VideoCall.qml" : "" + onLoaded: TimelineManager.setVideoCallItem() } - } - TypingIndicator { id: typingIndicator + } - } - } - CallInviteBar { id: callInviteBar Layout.fillWidth: true z: 3 } - ActiveCallBar { Layout.fillWidth: true z: 3 } - Rectangle { Layout.fillWidth: true - z: 3 - height: 1 color: Nheko.theme.separator + height: 1 + z: 3 } - - UploadBox { } - MessageInputWarning { text: qsTr("You are about to notify the whole room") visible: (room && room.permissions.canPingRoom() && room.input.containsAtRoom) } - MessageInputWarning { text: qsTr("The command /%1 is not recognized and will be sent as part of your message").arg(room ? room.input.currentCommand : "") visible: room ? room.input.containsInvalidCommand && !room.input.containsIncompleteCommand : false } - MessageInputWarning { + bubbleColor: Nheko.theme.orange text: qsTr("/%1 looks like an incomplete command. To send it anyway, add a space to the end of your message.").arg(room ? room.input.currentCommand : "") visible: room ? room.input.containsIncompleteCommand : false - bubbleColor: Nheko.theme.orange } - ReplyPopup { } - MessageInput { } - } - ColumnLayout { id: preview + property string avatarUrl: room ? room.roomAvatarUrl : (roomPreview ? roomPreview.roomAvatarUrl : "") + property string reason: roomPreview ? roomPreview.reason : "" 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 : "") - property string reason: roomPreview ? roomPreview.reason : "" - visible: room != null && room.isSpace || roomPreview != null - enabled: visible anchors.fill: parent anchors.margins: Nheko.paddingLarge + enabled: visible spacing: Nheko.paddingLarge + visible: room != null && room.isSpace || roomPreview != null Item { Layout.fillHeight: true } - Avatar { - url: parent.avatarUrl.replace("mxc://", "image://MxcImage/") - roomid: parent.roomId + Layout.alignment: Qt.AlignHCenter displayName: parent.roomName - height: 130 - width: 130 - Layout.alignment: Qt.AlignHCenter enabled: false + height: 130 + roomid: parent.roomId + url: parent.avatarUrl.replace("mxc://", "image://MxcImage/") + width: 130 } - RowLayout { - spacing: Nheko.paddingMedium Layout.alignment: Qt.AlignHCenter + spacing: Nheko.paddingMedium MatrixText { - text: !roomPreview.isFetched ? qsTr("No preview available") : preview.roomName font.pixelSize: 24 + text: !(roomPreview?.isFetched ?? false) ? qsTr("No preview available") : preview.roomName } - ImageButton { + ToolTip.text: qsTr("Settings") + ToolTip.visible: hovered + hoverEnabled: true image: ":/icons/icons/ui/settings.svg" visible: !!room - hoverEnabled: true - ToolTip.visible: hovered - ToolTip.text: qsTr("Settings") + onClicked: TimelineManager.openRoomSettings(room.roomId) } - } - RowLayout { - visible: !!room - spacing: Nheko.paddingMedium Layout.alignment: Qt.AlignHCenter + spacing: Nheko.paddingMedium + visible: !!room MatrixText { text: qsTr("%n member(s)", "", room ? room.roomMemberCount : 0) } - ImageButton { - image: ":/icons/icons/ui/people.svg" - hoverEnabled: true - ToolTip.visible: hovered ToolTip.text: qsTr("View members of %1").arg(room ? room.roomName : "") + ToolTip.visible: hovered + hoverEnabled: true + image: ":/icons/icons/ui/people.svg" + onClicked: TimelineManager.openRoomMembers(room) } - } - ScrollView { Layout.alignment: Qt.AlignHCenter Layout.fillWidth: true @@ -264,55 +238,76 @@ Item { Layout.rightMargin: Nheko.paddingLarge TextArea { - text: roomPreview.isFetched ? TimelineManager.escapeEmoji(preview.roomTopic) : qsTr("This room is possibly inaccessible. If this room is private, you should remove it from this community.") - wrapMode: TextEdit.WordWrap - textFormat: TextEdit.RichText - readOnly: true background: null - selectByMouse: true - color: Nheko.colors.text horizontalAlignment: TextEdit.AlignHCenter + readOnly: true + selectByMouse: true + text: (roomPreview?.isFetched ?? false) ? TimelineManager.escapeEmoji(preview.roomTopic) : qsTr("This room is possibly inaccessible. If this room is private, you should remove it from this community.") + textFormat: TextEdit.RichText + wrapMode: TextEdit.WordWrap + onLinkActivated: Nheko.openLink(link) - CursorShape { + NhekoCursorShape { anchors.fill: parent cursorShape: parent.hoveredLink ? Qt.PointingHandCursor : Qt.ArrowCursor } - } - } - FlatButton { - visible: roomPreview && !roomPreview.isInvite Layout.alignment: Qt.AlignHCenter text: qsTr("join the conversation") + visible: roomPreview && !roomPreview.isInvite + onClicked: Rooms.joinPreview(roomPreview.roomid) } - FlatButton { - visible: roomPreview && roomPreview.isInvite Layout.alignment: Qt.AlignHCenter text: qsTr("accept invite") + visible: roomPreview && roomPreview.isInvite + onClicked: Rooms.acceptInvite(roomPreview.roomid) } - FlatButton { - visible: roomPreview && roomPreview.isInvite Layout.alignment: Qt.AlignHCenter text: qsTr("decline invite") + visible: roomPreview && roomPreview.isInvite + onClicked: Rooms.declineInvite(roomPreview.roomid) } - FlatButton { - visible: !!room Layout.alignment: Qt.AlignHCenter text: qsTr("leave") + visible: !!room + onClicked: TimelineManager.openLeaveRoomDialog(room.roomId) } + RowLayout { + Layout.alignment: Qt.AlignHCenter + spacing: Nheko.paddingMedium + visible: roomPreview && roomPreview.isInvite && reasonField.showReason + MatrixText { + text: qsTr("Invited by %1 (%2)").arg(TimelineManager.escapeEmoji(inviterAvatar.displayName)).arg(TimelineManager.escapeEmoji(TimelineManager.htmlEscape(inviterAvatar.userid))) + } + Avatar { + id: inviterAvatar + + Layout.alignment: Qt.AlignHCenter + displayName: roomPreview?.inviterDisplayName ?? "" + enabled: true + height: 48 + roomid: preview.roomId + url: (roomPreview?.inviterAvatarUrl ?? "").replace("mxc://", "image://MxcImage/") + userid: roomPreview?.inviterUserId ?? "" + width: 48 + + onClicked: TimelineManager.openGlobalUserProfile(roomPreview.inviterUserId) + } + } ScrollView { id: reasonField + property bool showReason: false Layout.alignment: Qt.AlignHCenter @@ -322,18 +317,15 @@ Item { visible: preview.reason !== "" && showReason TextArea { - text: TimelineManager.escapeEmoji(preview.reason) - wrapMode: TextEdit.WordWrap - textFormat: TextEdit.RichText - readOnly: true background: null - selectByMouse: true - color: Nheko.colors.text horizontalAlignment: TextEdit.AlignHCenter + readOnly: true + selectByMouse: true + text: TimelineManager.escapeEmoji(preview.reason) + textFormat: TextEdit.RichText + wrapMode: TextEdit.WordWrap } - } - Button { id: showReasonButton @@ -341,76 +333,95 @@ Item { //Layout.fillWidth: true Layout.leftMargin: Nheko.paddingLarge Layout.rightMargin: Nheko.paddingLarge - - visible: preview.reason !== "" text: reasonField.showReason ? qsTr("Hide invite reason") : qsTr("Show invite reason") + visible: roomPreview && roomPreview.isInvite + onClicked: { reasonField.showReason = !reasonField.showReason; } } - Item { - visible: room != null Layout.preferredHeight: Math.ceil(fontMetrics.lineSpacing * 2) + visible: room != null } - Item { Layout.fillHeight: true } - } - ImageButton { id: backToRoomsButton - anchors.top: parent.top + ToolTip.text: qsTr("Back to room list") + ToolTip.visible: hovered anchors.left: parent.left anchors.margins: Nheko.paddingMedium - width: Nheko.avatarSize - height: Nheko.avatarSize - visible: (room == null || room.isSpace) && showBackButton + anchors.top: parent.top enabled: visible + height: Nheko.avatarSize image: ":/icons/icons/ui/angle-arrow-left.svg" - ToolTip.visible: hovered - ToolTip.text: qsTr("Back to room list") + visible: (room == null || room.isSpace) && showBackButton + width: Nheko.avatarSize + onClicked: Rooms.resetCurrentRoom() } - TimelineEffects { id: timelineEffects anchors.fill: parent + shouldEffectsRun: timelineView.shouldEffectsRun } - NhekoDropArea { anchors.fill: parent roomid: room ? room.roomId : "" } - Timer { id: effectsTimer - onTriggered: shouldEffectsRun = false; + interval: timelineEffects.maxLifespan repeat: false running: false - } + onTriggered: shouldEffectsRun = false + } Connections { + function onConfetti() { + if (!Settings.fancyEffects) + return; + shouldEffectsRun = true; + timelineEffects.pulseConfetti(); + room.markSpecialEffectsDone(); + } + function onConfettiDone() { + if (!Settings.fancyEffects) + return; + effectsTimer.restart(); + } function onOpenReadReceiptsDialog(rr) { var dialog = readReceiptsDialog.createObject(timelineRoot, { - "readReceipts": rr, - "room": room - }); + "readReceipts": rr, + "room": room + }); dialog.show(); timelineRoot.destroyOnClose(dialog); } - + function onRainfall() { + if (!Settings.fancyEffects) + return; + shouldEffectsRun = true; + timelineEffects.pulseRainfall(); + room.markSpecialEffectsDone(); + } + function onRainfallDone() { + if (!Settings.fancyEffects) + return; + effectsTimer.restart(); + } function onShowRawMessageDialog(rawMessage) { - var component = Qt.createComponent("qrc:/qml/dialogs/RawMessageDialog.qml") + var component = Qt.createComponent("qrc:/resources/qml/dialogs/RawMessageDialog.qml"); if (component.status == Component.Ready) { var dialog = component.createObject(timelineRoot, { - "rawMessage": rawMessage - }); + "rawMessage": rawMessage + }); dialog.show(); timelineRoot.destroyOnClose(dialog); } else { @@ -418,43 +429,6 @@ Item { } } - function onConfetti() - { - if (!Settings.fancyEffects) - return - - shouldEffectsRun = true; - timelineEffects.pulseConfetti() - room.markSpecialEffectsDone() - } - - function onConfettiDone() - { - if (!Settings.fancyEffects) - return - - effectsTimer.restart(); - } - - function onRainfall() - { - if (!Settings.fancyEffects) - return - - shouldEffectsRun = true; - timelineEffects.pulseRainfall() - room.markSpecialEffectsDone() - } - - function onRainfallDone() - { - if (!Settings.fancyEffects) - return - - effectsTimer.restart(); - } - target: room } - } diff --git a/resources/qml/ToggleButton.qml b/resources/qml/ToggleButton.qml index 6b43bec5..f3bd5cce 100644 --- a/resources/qml/ToggleButton.qml +++ b/resources/qml/ToggleButton.qml @@ -11,17 +11,44 @@ Switch { id: toggleButton implicitWidth: indicatorItem.width - state: checked ? "on" : "off" + + indicator: Item { + id: indicatorItem + + implicitHeight: 24 + implicitWidth: 48 + y: parent.height / 2 - height / 2 + + Rectangle { + id: track + + color: Qt.rgba(border.color.r, border.color.g, border.color.b, 0.6) + height: parent.height * 0.6 + radius: height / 2 + width: parent.width - height + x: radius + y: parent.height / 2 - height / 2 + } + Rectangle { + id: handle + + border.color: "#767676" + color: palette.button + height: width + radius: width / 2 + width: parent.height * 0.9 + y: parent.height / 2 - height / 2 + } + } states: [ State { name: "off" PropertyChanges { - target: track border.color: "#767676" + target: track } - PropertyChanges { target: handle x: 0 @@ -31,10 +58,9 @@ Switch { name: "on" PropertyChanges { + border.color: palette.highlight target: track - border.color: Nheko.colors.highlight } - PropertyChanges { target: handle x: indicatorItem.width - handle.width @@ -43,55 +69,22 @@ Switch { ] transitions: [ Transition { - to: "off" reversible: true + to: "off" ParallelAnimation { NumberAnimation { - target: handle - property: "x" duration: 200 easing.type: Easing.InOutQuad + property: "x" + target: handle } - ColorAnimation { - target: track - properties: "color,border.color" duration: 200 + properties: "color,border.color" + target: track } } } ] - - indicator: Item { - id: indicatorItem - - implicitWidth: 48 - implicitHeight: 24 - y: parent.height / 2 - height / 2 - - Rectangle { - id: track - - height: parent.height * 0.6 - radius: height / 2 - width: parent.width - height - x: radius - y: parent.height / 2 - height / 2 - color: Qt.rgba(border.color.r, border.color.g, border.color.b, 0.6) - } - - Rectangle { - id: handle - - y: parent.height / 2 - height / 2 - width: parent.height * 0.9 - height: width - radius: width / 2 - color: Nheko.colors.button - border.color: "#767676" - } - - } - } diff --git a/resources/qml/TopBar.qml b/resources/qml/TopBar.qml index f23645a7..699595e6 100644 --- a/resources/qml/TopBar.qml +++ b/resources/qml/TopBar.qml @@ -8,212 +8,142 @@ import QtQuick.Controls 2.15 import QtQuick.Layouts 1.2 import QtQuick.Window 2.15 import im.nheko 1.0 - import "./delegates" Pane { id: topBar - 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 : "" - + property bool isDirect: room ? room.isDirect : false + property bool isEncrypted: room ? room.isEncrypted : false + property string roomId: room ? room.roomId : "" + property string roomName: room ? room.roomName : qsTr("No room selected") + property string roomTopic: room ? room.roomTopic : "" property bool searchHasFocus: searchField.focus && searchField.enabled - property string searchString: "" - - // HACK: https://bugreports.qt.io/browse/QTBUG-83972, qtwayland cannot auto hide menu - Connections { - function onHideMenu() { - roomOptionsMenu.close() - } - target: MainWindow - } - - onRoomIdChanged: { - searchString = ""; - searchButton.searchActive = false; - searchField.text = "" - } - - Shortcut { - sequence: StandardKey.Find - onActivated: searchButton.searchActive = !searchButton.searchActive - } + property bool showBackButton: false + property int trustlevel: room ? room.trustlevel : Crypto.Unverified Layout.fillWidth: true implicitHeight: topLayout.height + Nheko.paddingMedium * 2 + padding: 0 z: 3 - padding: 0 background: Rectangle { - color: Nheko.colors.window + color: palette.window } - - TapHandler { - onSingleTapped: { - if (eventPoint.position.y > topBar.height - (pinnedMessages.visible ? pinnedMessages.height : 0) - (widgets.visible ? widgets.height : 0)) { - eventPoint.accepted = true - return; - } - if (showBackButton && eventPoint.position.x < Nheko.paddingMedium + backToRoomsButton.width) { - eventPoint.accepted = true - return; - } - if (eventPoint.position.x > topBar.width - Nheko.paddingMedium - roomOptionsButton.width) { - eventPoint.accepted = true - return; - } - - if (communityLabel.visible && eventPoint.position.y < communityAvatar.height + Nheko.paddingMedium + Nheko.paddingSmall/2) { - if (!Communities.trySwitchToSpace(room.parentSpace.roomid)) - room.parentSpace.promptJoin(); - eventPoint.accepted = true - return; - } - - if (room) { - let p = topBar.mapToItem(roomTopicC, eventPoint.position.x, eventPoint.position.y); - let link = roomTopicC.linkAt(p.x, p.y); - - if (link) { - Nheko.openLink(link); - } else { - TimelineManager.openRoomSettings(room.roomId); - } - } - - eventPoint.accepted = true; - } - gesturePolicy: TapHandler.ReleaseWithinBounds - } - - HoverHandler { - grabPermissions: PointerHandler.TakeOverForbidden | PointerHandler.CanTakeOverFromAnything - } - contentItem: Item { GridLayout { id: topLayout anchors.left: parent.left - anchors.right: parent.right anchors.margins: Nheko.paddingMedium + anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter columnSpacing: Nheko.paddingSmall rowSpacing: Nheko.paddingSmall - Avatar { id: communityAvatar - visible: roomid && room.parentSpace.isLoaded && ("space:"+room.parentSpace.roomid != Communities.currentTagId) - property string avatarUrl: (Settings.groupView && room && room.parentSpace && room.parentSpace.roomAvatarUrl) || "" property string communityId: (Settings.groupView && room && room.parentSpace && room.parentSpace.roomid) || "" property string communityName: (Settings.groupView && room && room.parentSpace && room.parentSpace.roomName) || "" + Layout.alignment: Qt.AlignRight Layout.column: 1 Layout.row: 0 - Layout.alignment: Qt.AlignRight - width: fontMetrics.lineSpacing - height: fontMetrics.lineSpacing - url: avatarUrl.replace("mxc://", "image://MxcImage/") - roomid: communityId displayName: communityName enabled: false + height: fontMetrics.lineSpacing + roomid: communityId + url: avatarUrl.replace("mxc://", "image://MxcImage/") + visible: roomid && room.parentSpace.isLoaded && ("space:" + room.parentSpace.roomid != Communities.currentTagId) + width: fontMetrics.lineSpacing } - Label { id: communityLabel - visible: communityAvatar.visible Layout.column: 2 - Layout.row: 0 Layout.fillWidth: true - color: Nheko.colors.text - text: qsTr("In %1").arg(communityAvatar.displayName) - maximumLineCount: 1 + Layout.row: 0 + color: palette.text elide: Text.ElideRight + maximumLineCount: 1 + text: qsTr("In %1").arg(communityAvatar.displayName) textFormat: Text.RichText + visible: communityAvatar.visible } - ImageButton { id: backToRoomsButton - Layout.column: 0 - Layout.row: 1 - Layout.rowSpan: 2 Layout.alignment: Qt.AlignVCenter + Layout.column: 0 Layout.preferredHeight: Nheko.avatarSize - Nheko.paddingMedium Layout.preferredWidth: Nheko.avatarSize - Nheko.paddingMedium - visible: showBackButton - image: ":/icons/icons/ui/angle-arrow-left.svg" - ToolTip.visible: hovered + Layout.row: 1 + Layout.rowSpan: 2 ToolTip.text: qsTr("Back to room list") + ToolTip.visible: hovered + image: ":/icons/icons/ui/angle-arrow-left.svg" + visible: showBackButton + onClicked: Rooms.resetCurrentRoom() } - Avatar { + Layout.alignment: Qt.AlignVCenter Layout.column: 1 Layout.row: 1 Layout.rowSpan: 2 - Layout.alignment: Qt.AlignVCenter - width: Nheko.avatarSize - height: Nheko.avatarSize - url: avatarUrl.replace("mxc://", "image://MxcImage/") - roomid: roomId - userid: isDirect ? directChatOtherUserId : "" displayName: roomName enabled: false + height: Nheko.avatarSize + roomid: roomId + url: avatarUrl.replace("mxc://", "image://MxcImage/") + userid: isDirect ? directChatOtherUserId : "" + width: Nheko.avatarSize } - Label { - Layout.fillWidth: true Layout.column: 2 + Layout.fillWidth: true Layout.row: 1 - color: Nheko.colors.text - font.pointSize: fontMetrics.font.pointSize * 1.1 - font.bold: true - text: roomName - maximumLineCount: 1 + color: palette.text elide: Text.ElideRight + font.bold: true + font.pointSize: fontMetrics.font.pointSize * 1.1 + maximumLineCount: 1 + text: roomName textFormat: Text.RichText } - MatrixText { id: roomTopicC - Layout.fillWidth: true + Layout.column: 2 - Layout.row: 2 + Layout.fillWidth: true Layout.maximumHeight: fontMetrics.lineSpacing * 2 // show 2 lines - selectByMouse: false - enabled: false + Layout.row: 2 clip: true + enabled: false + selectByMouse: false text: roomTopic } - ImageButton { id: pinButton property bool pinsShown: !Settings.hiddenPins.includes(roomId) - visible: !!room && room.pinnedMessages.length > 0 - Layout.column: 3 - Layout.row: 1 - Layout.rowSpan: 2 Layout.alignment: Qt.AlignVCenter + Layout.column: 3 Layout.preferredHeight: Nheko.avatarSize - Nheko.paddingMedium Layout.preferredWidth: Nheko.avatarSize - Nheko.paddingMedium - image: pinsShown ? ":/icons/icons/ui/pin.svg" : ":/icons/icons/ui/pin-off.svg" - ToolTip.visible: hovered + Layout.row: 1 + Layout.rowSpan: 2 ToolTip.text: qsTr("Show or hide pinned messages") + ToolTip.visible: hovered + image: pinsShown ? ":/icons/icons/ui/pin.svg" : ":/icons/icons/ui/pin-off.svg" + visible: !!room && room.pinnedMessages.length > 0 + onClicked: { var ps = Settings.hiddenPins; if (pinsShown) { @@ -226,254 +156,280 @@ Pane { } Settings.hiddenPins = ps; } - } - AbstractButton { Layout.column: 4 - Layout.row: 1 - Layout.rowSpan: 2 Layout.preferredHeight: Nheko.avatarSize - Nheko.paddingMedium Layout.preferredWidth: Nheko.avatarSize - Nheko.paddingMedium + Layout.row: 1 + Layout.rowSpan: 2 + background: null contentItem: EncryptionIndicator { - encrypted: isEncrypted - trust: trustlevel - enabled: false - unencryptedIcon: ":/icons/icons/ui/people.svg" - unencryptedColor: Nheko.colors.buttonText - unencryptedHoverColor: Nheko.colors.highlight - hovered: parent.hovered - ToolTip.delay: Nheko.tooltipDelay ToolTip.text: { if (!isEncrypted) - return qsTr("Show room members."); - + return qsTr("Show room members."); switch (trustlevel) { - case Crypto.Verified: + case Crypto.Verified: return qsTr("This room contains only verified devices."); - case Crypto.TOFU: + case Crypto.TOFU: return qsTr("This room contains verified devices and devices which have never changed their master key."); - default: + default: return qsTr("This room contains unverified devices!"); } } + enabled: false + encrypted: isEncrypted + hovered: parent.hovered + trust: trustlevel + unencryptedColor: palette.buttonText + unencryptedHoverColor: palette.highlight + unencryptedIcon: ":/icons/icons/ui/people.svg" } - background: null onClicked: TimelineManager.openRoomMembers(room) } - ImageButton { id: searchButton property bool searchActive: false - visible: !!room - Layout.column: 5 - Layout.row: 1 - Layout.rowSpan: 2 Layout.alignment: Qt.AlignVCenter + Layout.column: 5 Layout.preferredHeight: Nheko.avatarSize - Nheko.paddingMedium Layout.preferredWidth: Nheko.avatarSize - Nheko.paddingMedium - image: ":/icons/icons/ui/search.svg" - ToolTip.visible: hovered + Layout.row: 1 + Layout.rowSpan: 2 ToolTip.text: qsTr("Search this room") - onClicked: searchActive = !searchActive + ToolTip.visible: hovered + image: ":/icons/icons/ui/search.svg" + visible: !!room + onClicked: searchActive = !searchActive onSearchActiveChanged: { if (searchActive) { searchField.forceActiveFocus(); - } - else { + } else { searchField.clear(); topBar.searchString = ""; } } } - ImageButton { id: roomOptionsButton - visible: !!room - Layout.column: 6 - Layout.row: 1 - Layout.rowSpan: 2 Layout.alignment: Qt.AlignVCenter + Layout.column: 6 Layout.preferredHeight: Nheko.avatarSize - Nheko.paddingMedium Layout.preferredWidth: Nheko.avatarSize - Nheko.paddingMedium - image: ":/icons/icons/ui/options.svg" - ToolTip.visible: hovered + Layout.row: 1 + Layout.rowSpan: 2 ToolTip.text: qsTr("Room options") + ToolTip.visible: hovered + image: ":/icons/icons/ui/options.svg" + visible: !!room + onClicked: roomOptionsMenu.open(roomOptionsButton) Platform.Menu { id: roomOptionsMenu Platform.MenuItem { - visible: room ? room.permissions.canInvite() : false text: qsTr("Invite users") + visible: room ? room.permissions.canInvite() : false + onTriggered: TimelineManager.openInviteUsers(roomId) } - Platform.MenuItem { text: qsTr("Members") + onTriggered: TimelineManager.openRoomMembers(room) } - Platform.MenuItem { text: qsTr("Leave room") + onTriggered: TimelineManager.openLeaveRoomDialog(roomId) } - Platform.MenuItem { text: qsTr("Settings") + onTriggered: TimelineManager.openRoomSettings(roomId) } - } - } - ScrollView { id: pinnedMessages - Layout.row: 3 Layout.column: 2 Layout.columnSpan: 4 - Layout.fillWidth: true Layout.preferredHeight: Math.min(contentHeight, Nheko.avatarSize * 4) - - visible: !!room && room.pinnedMessages.length > 0 && !Settings.hiddenPins.includes(roomId) - clip: true - - palette: Nheko.colors + Layout.row: 3 ScrollBar.horizontal.visible: false + clip: true + visible: !!room && room.pinnedMessages.length > 0 && !Settings.hiddenPins.includes(roomId) ListView { - - spacing: Nheko.paddingSmall model: room ? room.pinnedMessages : undefined + spacing: Nheko.paddingSmall + delegate: RowLayout { required property string modelData - width: ListView.view.width height: implicitHeight + width: ListView.view.width Reply { id: reply + property var e: room ? room.getDump(modelData, "pins") : {} - Connections { - function onPinnedMessagesChanged() { reply.e = room.getDump(modelData, "pins") } - target: room - } + Layout.fillWidth: true Layout.preferredHeight: height - - userColor: TimelineManager.userColor(e.userId, Nheko.colors.window) blurhash: e.blurhash ?? "" body: e.body ?? "" - formattedBody: e.formattedBody ?? "" + encryptionError: e.encryptionError ?? 0 eventId: e.eventId ?? "" filename: e.filename ?? "" filesize: e.filesize ?? "" + formattedBody: e.formattedBody ?? "" + isOnlyEmoji: e.isOnlyEmoji ?? false + keepFullText: true + originalWidth: e.originalWidth ?? 0 proportionalHeight: e.proportionalHeight ?? 1 type: e.type ?? MtxEvent.UnknownMessage typeString: e.typeString ?? "" url: e.url ?? "" - originalWidth: e.originalWidth ?? 0 - isOnlyEmoji: e.isOnlyEmoji ?? false + userColor: TimelineManager.userColor(e.userId, palette.window) userId: e.userId ?? "" userName: e.userName ?? "" - encryptionError: e.encryptionError ?? "" - keepFullText: true - } + Connections { + function onPinnedMessagesChanged() { + reply.e = room.getDump(modelData, "pins"); + } + + target: room + } + } ImageButton { id: deletePinButton + Layout.alignment: Qt.AlignTop | Qt.AlignLeft Layout.preferredHeight: 16 Layout.preferredWidth: 16 - Layout.alignment: Qt.AlignTop | Qt.AlignLeft - visible: room.permissions.canChange(MtxEvent.PinnedEvents) - + ToolTip.text: qsTr("Unpin") + ToolTip.visible: hovered hoverEnabled: true image: ":/icons/icons/ui/dismiss.svg" - ToolTip.visible: hovered - ToolTip.text: qsTr("Unpin") + visible: room.permissions.canChange(MtxEvent.PinnedEvents) onClicked: room.unpin(modelData) } } - - - ScrollHelper { - flickable: parent - anchors.fill: parent - enabled: !Settings.mobileMode - } } } - ScrollView { id: widgets - Layout.row: 4 Layout.column: 2 Layout.columnSpan: 4 - Layout.fillWidth: true Layout.preferredHeight: Math.min(contentHeight, Nheko.avatarSize * 1.5) - - visible: !!room && room.widgetLinks.length > 0 && !Settings.hiddenWidgets.includes(roomId) - clip: true - - palette: Nheko.colors + Layout.row: 4 ScrollBar.horizontal.visible: false + clip: true + visible: !!room && room.widgetLinks.length > 0 && !Settings.hiddenWidgets.includes(roomId) ListView { - - spacing: Nheko.paddingSmall model: room ? room.widgetLinks : undefined + spacing: Nheko.paddingSmall + delegate: MatrixText { required property var modelData - color: Nheko.colors.text + color: palette.text text: modelData } - - - ScrollHelper { - flickable: parent - anchors.fill: parent - enabled: !Settings.mobileMode - } } } - MatrixTextField { id: searchField - visible: searchButton.searchActive - enabled: visible - hasClear: true - Layout.row: 5 Layout.column: 2 Layout.columnSpan: 4 - Layout.fillWidth: true - + Layout.row: 5 + enabled: visible + hasClear: true placeholderText: qsTr("Enter search query") + visible: searchButton.searchActive + onAccepted: topBar.searchString = text } } - - CursorShape { - anchors.fill: parent + NhekoCursorShape { anchors.bottomMargin: (pinnedMessages.visible ? pinnedMessages.height : 0) + (widgets.visible ? widgets.height : 0) + anchors.fill: parent cursorShape: Qt.PointingHandCursor } } + + onRoomIdChanged: { + searchString = ""; + searchButton.searchActive = false; + searchField.text = ""; + } + + // HACK: https://bugreports.qt.io/browse/QTBUG-83972, qtwayland cannot auto hide menu + Connections { + function onHideMenu() { + roomOptionsMenu.close(); + } + + target: MainWindow + } + Shortcut { + sequence: StandardKey.Find + + onActivated: searchButton.searchActive = !searchButton.searchActive + } + TapHandler { + gesturePolicy: TapHandler.ReleaseWithinBounds + + onSingleTapped: { + if (eventPoint.position.y > topBar.height - (pinnedMessages.visible ? pinnedMessages.height : 0) - (widgets.visible ? widgets.height : 0)) { + eventPoint.accepted = true; + return; + } + if (showBackButton && eventPoint.position.x < Nheko.paddingMedium + backToRoomsButton.width) { + eventPoint.accepted = true; + return; + } + if (eventPoint.position.x > topBar.width - Nheko.paddingMedium - roomOptionsButton.width) { + eventPoint.accepted = true; + return; + } + if (communityLabel.visible && eventPoint.position.y < communityAvatar.height + Nheko.paddingMedium + Nheko.paddingSmall / 2) { + if (!Communities.trySwitchToSpace(room.parentSpace.roomid)) + room.parentSpace.promptJoin(); + eventPoint.accepted = true; + return; + } + if (room) { + let p = topBar.mapToItem(roomTopicC, eventPoint.position.x, eventPoint.position.y); + let link = roomTopicC.linkAt(p.x, p.y); + if (link) { + Nheko.openLink(link); + } else { + TimelineManager.openRoomSettings(room.roomId); + } + } + eventPoint.accepted = true; + } + } + HoverHandler { + grabPermissions: PointerHandler.TakeOverForbidden | PointerHandler.CanTakeOverFromAnything + } } diff --git a/resources/qml/TypingIndicator.qml b/resources/qml/TypingIndicator.qml index 027ae5b6..b6c502d8 100644 --- a/resources/qml/TypingIndicator.qml +++ b/resources/qml/TypingIndicator.qml @@ -8,30 +8,28 @@ import QtQuick.Layouts 1.2 import im.nheko 1.0 Item { - implicitHeight: Math.max(fontMetrics.height * 1.2, typingDisplay.height) Layout.fillWidth: true + implicitHeight: Math.max(fontMetrics.height * 1.2, typingDisplay.height) Rectangle { id: typingRect - visible: (room && room.typingUsers.length > 0) - color: Nheko.colors.base anchors.fill: parent + color: palette.base + visible: (room && room.typingUsers.length > 0) z: 3 Label { id: typingDisplay + anchors.bottom: parent.bottom anchors.left: parent.left anchors.leftMargin: 10 anchors.right: parent.right anchors.rightMargin: 10 - anchors.bottom: parent.bottom - color: Nheko.colors.text - text: room ? room.formatTypingUsers(room.typingUsers, Nheko.colors.base) : "" + color: palette.text + text: room ? room.formatTypingUsers(room.typingUsers, palette.base) : "" textFormat: Text.RichText } - } - } diff --git a/resources/qml/UploadBox.qml b/resources/qml/UploadBox.qml index adb8c12b..990fa422 100644 --- a/resources/qml/UploadBox.qml +++ b/resources/qml/UploadBox.qml @@ -4,7 +4,6 @@ import "./components" import "./ui" - import QtQuick 2.9 import QtQuick.Controls 2.5 import QtQuick.Layouts 1.3 @@ -12,79 +11,85 @@ import im.nheko 1.0 Page { id: uploadPopup - visible: room && room.input.uploads.length > 0 - Layout.preferredHeight: 200 - clip: true Layout.fillWidth: true - + Layout.preferredHeight: 200 + clip: true padding: Nheko.paddingMedium + visible: room && room.input.uploads.length > 0 + background: Rectangle { + color: palette.base + } contentItem: ListView { id: uploadsList + anchors.horizontalCenter: parent.horizontalCenter boundsBehavior: Flickable.StopAtBounds + model: room ? room.input.uploads : undefined + orientation: ListView.Horizontal + spacing: Nheko.paddingMedium + width: Math.min(contentWidth, parent.availableWidth) ScrollBar.horizontal: ScrollBar { id: scr + } - - orientation: ListView.Horizontal - width: Math.min(contentWidth, parent.availableWidth) - model: room ? room.input.uploads : undefined - spacing: Nheko.paddingMedium - delegate: Pane { + id: pane + + height: uploadPopup.availableHeight - buttons.height - (scr.visible ? scr.height : 0) padding: Nheko.paddingSmall - height: uploadPopup.availableHeight - buttons.height - (scr.visible? scr.height : 0) width: uploadPopup.availableHeight - buttons.height background: Rectangle { - color: Nheko.colors.window + color: palette.window radius: Nheko.paddingMedium } contentItem: ColumnLayout { Image { + property string typeStr: switch (modelData.mediaType) { + case MediaUpload.Video: + return "video-file"; + case MediaUpload.Audio: + return "music"; + case MediaUpload.Image: + return "image"; + default: + return "zip"; + } + Layout.fillHeight: true Layout.fillWidth: true - - sourceSize.height: parent.availableHeight - namefield.height - sourceSize.width: parent.availableWidth fillMode: Image.PreserveAspectFit - smooth: true mipmap: true - - property string typeStr: switch(modelData.mediaType) { - case MediaUpload.Video: return "video-file"; - case MediaUpload.Audio: return "music"; - case MediaUpload.Image: return "image"; - default: return "zip"; - } - source: (modelData.thumbnail != "") ? modelData.thumbnail : ("image://colorimage/:/icons/icons/ui/"+typeStr+".svg?" + Nheko.colors.buttonText) + smooth: true + source: (modelData.thumbnail != "") ? modelData.thumbnail : ("image://colorimage/:/icons/icons/ui/" + typeStr + ".svg?" + palette.buttonText) + sourceSize.height: pane.availableHeight - namefield.height + sourceSize.width: pane.availableWidth } MatrixTextField { id: namefield + Layout.fillWidth: true text: modelData.filename + onTextEdited: modelData.filename = text } } } } - footer: DialogButtonBox { id: buttons standardButtons: DialogButtonBox.Cancel - Button { - text: qsTr("Upload %n file(s)", "", (room ? room.input.uploads.length : 0)) - DialogButtonBox.buttonRole: DialogButtonBox.AcceptRole - } + onAccepted: room.input.acceptUploads() onRejected: room.input.declineUploads() - } - background: Rectangle { - color: Nheko.colors.base + Button { + DialogButtonBox.buttonRole: DialogButtonBox.AcceptRole + text: qsTr("Upload %n file(s)", "", (room ? room.input.uploads.length : 0)) + } } } diff --git a/resources/qml/components/AdaptiveLayout.qml b/resources/qml/components/AdaptiveLayout.qml index 9fc27055..eea74006 100644 --- a/resources/qml/components/AdaptiveLayout.qml +++ b/resources/qml/components/AdaptiveLayout.qml @@ -87,7 +87,7 @@ Container { x: parent.preferredWidth z: 3 - CursorShape { + NhekoCursorShape { height: parent.height width: container.splitterGrabMargin * 2 x: -container.splitterGrabMargin diff --git a/resources/qml/components/AvatarListTile.qml b/resources/qml/components/AvatarListTile.qml index 02c92a09..dad20e52 100644 --- a/resources/qml/components/AvatarListTile.qml +++ b/resources/qml/components/AvatarListTile.qml @@ -11,11 +11,11 @@ import im.nheko 1.0 Rectangle { id: tile - property color background: 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 + property color background: palette.window + property color importantText: palette.text + property color unimportantText: palette.buttonText + property color bubbleBackground: palette.highlight + property color bubbleText: palette.highlightedText property int avatarSize: Math.ceil(fontMetrics.lineSpacing * 2.3) required property string avatarUrl required property string title @@ -37,11 +37,11 @@ Rectangle { PropertyChanges { target: tile - background: Nheko.colors.dark - importantText: Nheko.colors.brightText - unimportantText: Nheko.colors.brightText - bubbleBackground: Nheko.colors.highlight - bubbleText: Nheko.colors.highlightedText + background: palette.dark + importantText: palette.brightText + unimportantText: palette.brightText + bubbleBackground: palette.highlight + bubbleText: palette.highlightedText } }, @@ -51,11 +51,11 @@ Rectangle { PropertyChanges { target: tile - background: Nheko.colors.highlight - importantText: Nheko.colors.highlightedText - unimportantText: Nheko.colors.highlightedText - bubbleBackground: Nheko.colors.highlightedText - bubbleText: Nheko.colors.highlight + background: palette.highlight + importantText: palette.highlightedText + unimportantText: palette.highlightedText + bubbleBackground: palette.highlightedText + bubbleText: palette.highlight } } diff --git a/resources/qml/components/FlatButton.qml b/resources/qml/components/FlatButton.qml index ec4b306a..0636bc04 100644 --- a/resources/qml/components/FlatButton.qml +++ b/resources/qml/components/FlatButton.qml @@ -2,11 +2,11 @@ // // SPDX-License-Identifier: GPL-3.0-or-later -import QtGraphicalEffects 1.12 -import QtQuick 2.9 -import QtQuick.Controls 2.5 -import QtQuick.Layouts 1.2 -import im.nheko 1.0 +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import QtQuick.Effects +import im.nheko // FIXME(Nico): Don't use hardcoded colors. Button { @@ -18,14 +18,13 @@ Button { property string iconImage: "" - DropShadow { + MultiEffect { anchors.fill: control.background - horizontalOffset: 3 - verticalOffset: 3 - radius: 8 - samples: 17 - cached: true - color: "#80000000" + shadowHorizontalOffset: 3 + shadowVerticalOffset: 3 + shadowBlur: 8 + shadowEnabled: true + shadowColor: "#80000000" source: control.background } @@ -48,7 +47,7 @@ Button { font.capitalization: Font.AllUppercase font.pointSize: Math.ceil(fontMetrics.font.pointSize * 1.5) //font.capitalization: Font.AllUppercase - color: Nheko.colors.light + color: palette.light horizontalAlignment: Text.AlignHCenter verticalAlignment: Text.AlignVCenter elide: Text.ElideRight @@ -59,7 +58,7 @@ Button { //height: control.contentItem.implicitHeight * 2 //width: control.contentItem.implicitWidth * 2 radius: height / 8 - color: Qt.lighter(Nheko.colors.dark, control.down ? 1.4 : (control.hovered ? 1.2 : 1)) + color: Qt.lighter(palette.dark, control.down ? 1.4 : (control.hovered ? 1.2 : 1)) } } diff --git a/resources/qml/components/MainWindowDialog.qml b/resources/qml/components/MainWindowDialog.qml index 1b063e0f..10c07aae 100644 --- a/resources/qml/components/MainWindowDialog.qml +++ b/resources/qml/components/MainWindowDialog.qml @@ -32,7 +32,7 @@ Dialog { ] background: Rectangle { - color: Nheko.colors.window + color: palette.window border.color: Nheko.theme.separator border.width: 1 radius: Nheko.paddingSmall diff --git a/resources/qml/components/NhekoTabButton.qml b/resources/qml/components/NhekoTabButton.qml index 13549068..796177e8 100644 --- a/resources/qml/components/NhekoTabButton.qml +++ b/resources/qml/components/NhekoTabButton.qml @@ -13,15 +13,15 @@ TabButton { text: control.text font: control.font opacity: enabled ? 1.0 : 0.3 - color: control.down ? Nheko.colors.highlightedText : Nheko.colors.text + color: control.down ? palette.highlightedText : palette.text horizontalAlignment: Text.AlignHCenter verticalAlignment: Text.AlignVCenter elide: Text.ElideRight } background: Rectangle { - border.color: control.down ? Nheko.colors.highlight : Nheko.theme.separator - color: control.checked ? Nheko.colors.highlight : Nheko.colors.base + border.color: control.down ? palette.highlight : Nheko.theme.separator + color: control.checked ? palette.highlight : palette.base border.width: 1 radius: 2 } diff --git a/resources/qml/components/NotificationBubble.qml b/resources/qml/components/NotificationBubble.qml index f0a526d0..d4838e92 100644 --- a/resources/qml/components/NotificationBubble.qml +++ b/resources/qml/components/NotificationBubble.qml @@ -15,6 +15,7 @@ Rectangle { required property color bubbleTextColor property bool mayBeVisible: true property alias font: notificationBubbleText.font + baselineOffset: notificationBubbleText.baseline - bubbleRoot.top visible: mayBeVisible && notificationCount > 0 implicitHeight: notificationBubbleText.height + Nheko.paddingMedium diff --git a/resources/qml/components/ReorderableListview.qml b/resources/qml/components/ReorderableListview.qml index 137e92f8..1e8ab7b0 100644 --- a/resources/qml/components/ReorderableListview.qml +++ b/resources/qml/components/ReorderableListview.qml @@ -47,9 +47,9 @@ Item { width: dragArea.width; height: actualDelegate.implicitHeight + 4 border.width: dragArea.enabled ? 1 : 0 - border.color: Nheko.colors.highlight + border.color: palette.highlight - color: dragArea.held ? Nheko.colors.highlight : Nheko.colors.base + color: dragArea.held ? palette.highlight : palette.base Behavior on color { ColorAnimation { duration: 100 } } radius: 2 @@ -105,10 +105,6 @@ Item { clip: true anchors { fill: parent; margins: 2 } - ScrollHelper { - flickable: parent - anchors.fill: parent - } model: visualModel diff --git a/resources/qml/components/TextButton.qml b/resources/qml/components/TextButton.qml index a48aee2b..b6153f22 100644 --- a/resources/qml/components/TextButton.qml +++ b/resources/qml/components/TextButton.qml @@ -11,8 +11,8 @@ AbstractButton { id: button property alias cursor: mouseArea.cursorShape - property color highlightColor: Nheko.colors.highlight - property color buttonTextColor: Nheko.colors.buttonText + property color highlightColor: palette.highlight + property color buttonTextColor: palette.buttonText focusPolicy: Qt.NoFocus width: buttonText.implicitWidth @@ -32,7 +32,7 @@ AbstractButton { horizontalAlignment: Text.AlignHCenter } - CursorShape { + NhekoCursorShape { id: mouseArea anchors.fill: parent diff --git a/resources/qml/components/UserListRow.qml b/resources/qml/components/UserListRow.qml index 316baab0..2047f700 100644 --- a/resources/qml/components/UserListRow.qml +++ b/resources/qml/components/UserListRow.qml @@ -36,7 +36,7 @@ ItemDelegate { Label { Layout.fillWidth: true text: displayName - color: TimelineManager.userColor(userid, Nheko.colors.window) + color: TimelineManager.userColor(userid, palette.window) font.pointSize: fontMetrics.font.pointSize } @@ -44,7 +44,7 @@ ItemDelegate { Layout.fillWidth: true Layout.alignment: Qt.AlignTop text: userid - color: Nheko.colors.buttonText + color: palette.buttonText font.pointSize: fontMetrics.font.pointSize * 0.9 } } diff --git a/resources/qml/delegates/Encrypted.qml b/resources/qml/delegates/Encrypted.qml index 74e1cb0d..fdfe958e 100644 --- a/resources/qml/delegates/Encrypted.qml +++ b/resources/qml/delegates/Encrypted.qml @@ -18,7 +18,7 @@ Rectangle { width: parent.width? parent.width : 0 implicitWidth: encryptedText.implicitWidth+24+Nheko.paddingMedium*3 // Column doesn't provide a useful implicitWidth, should be replaced by ColumnLayout height: contents.implicitHeight + Nheko.paddingMedium * 2 - color: Nheko.colors.alternateBase + color: palette.alternateBase RowLayout { id: contents @@ -58,12 +58,11 @@ Rectangle { return qsTr("Unknown decryption error"); } } - color: Nheko.colors.text + color: palette.text width: parent.width } Button { - palette: Nheko.colors visible: encryptionError == Olm.MissingSession || encryptionError == Olm.MissingSessionIndex text: qsTr("Request key") onClicked: room.requestKeyForEvent(eventId) diff --git a/resources/qml/delegates/EncryptionEnabled.qml b/resources/qml/delegates/EncryptionEnabled.qml index e38be4b0..0e2b7fc0 100644 --- a/resources/qml/delegates/EncryptionEnabled.qml +++ b/resources/qml/delegates/EncryptionEnabled.qml @@ -4,7 +4,6 @@ import ".." import QtQuick 2.15 -import QtQuick.Controls 2.15 import QtQuick.Layouts 1.15 import im.nheko 1.0 @@ -15,9 +14,8 @@ Rectangle { radius: fontMetrics.lineSpacing / 2 + Nheko.paddingMedium width: parent.width ? Math.min(parent.width, 700) : 0 - anchors.horizontalCenter: parent.horizontalCenter height: contents.implicitHeight + Nheko.paddingMedium * 2 - color: Nheko.colors.alternateBase + color: palette.alternateBase border.color: Nheko.theme.green border.width: 2 @@ -31,8 +29,8 @@ Rectangle { Image { source: "image://colorimage/:/icons/icons/ui/shield-filled-checkmark.svg?" + Nheko.theme.green Layout.alignment: Qt.AlignVCenter - width: 24 - height: width + Layout.preferredWidth: 24 + Layout.preferredHeight: 24 } Column { @@ -43,13 +41,13 @@ Rectangle { text: qsTr("%1 enabled end-to-end encryption").arg(r.username) font.bold: true font.pointSize: 14 - color: Nheko.colors.text + color: palette.text width: parent.width } MatrixText { text: qsTr("Encryption keeps your messages safe by only allowing the people you sent the message to to read it. For extra security, if you want to make sure you are talking to the right people, you can verify them in real life.") - color: Nheko.colors.text + color: palette.text width: parent.width } diff --git a/resources/qml/delegates/FileMessage.qml b/resources/qml/delegates/FileMessage.qml index b3c44af2..82b82c1b 100644 --- a/resources/qml/delegates/FileMessage.qml +++ b/resources/qml/delegates/FileMessage.qml @@ -11,14 +11,13 @@ Item { required property string filename required property string filesize - height: row.height + (Settings.bubbles? 16: 24) - width: parent.width - implicitWidth: row.implicitWidth+metadataWidth + height: rowa.height + (Settings.bubbles? 16: 24) + implicitWidth: rowa.implicitWidth + metadataWidth property int metadataWidth property bool fitsMetadata: true RowLayout { - id: row + id: rowa anchors.centerIn: parent width: parent.width - (Settings.bubbles? 16 : 24) @@ -27,7 +26,7 @@ Item { Rectangle { id: button - color: Nheko.colors.light + color: palette.light radius: 22 height: 44 width: 44 @@ -50,7 +49,7 @@ Item { gesturePolicy: TapHandler.ReleaseWithinBounds } - CursorShape { + NhekoCursorShape { anchors.fill: parent cursorShape: Qt.PointingHandCursor } @@ -67,7 +66,7 @@ Item { text: filename textFormat: Text.PlainText elide: Text.ElideRight - color: Nheko.colors.text + color: palette.text } Text { @@ -77,7 +76,7 @@ Item { text: filesize textFormat: Text.PlainText elide: Text.ElideRight - color: Nheko.colors.text + color: palette.text } } @@ -85,7 +84,7 @@ Item { } Rectangle { - color: Nheko.colors.alternateBase + color: palette.alternateBase z: -1 radius: 10 anchors.fill: parent diff --git a/resources/qml/delegates/ImageMessage.qml b/resources/qml/delegates/ImageMessage.qml index bed4b659..20d727c3 100644 --- a/resources/qml/delegates/ImageMessage.qml +++ b/resources/qml/delegates/ImageMessage.qml @@ -22,7 +22,7 @@ AbstractButton { property int tempWidth: originalWidth < 1? 400: originalWidth implicitWidth: Math.round(tempWidth*Math.min((timelineView.height/divisor)/(tempWidth*proportionalHeight), 1)) - width: Math.min(parent.width,implicitWidth) + width: Math.min(parent?.width ?? 2000,implicitWidth) height: width*proportionalHeight hoverEnabled: true @@ -106,14 +106,14 @@ AbstractButton { ] property int metadataWidth - property bool fitsMetadata: (parent.width - width) > metadataWidth+4 + property bool fitsMetadata: parent != null ? (parent.width - width) > metadataWidth+4 : false Image { id: img visible: !mxcimage.loaded anchors.fill: parent - source: url.replace("mxc://", "image://MxcImage/") + "?scale" + source: url != "" ? (url.replace("mxc://", "image://MxcImage/") + "?scale") : "" asynchronous: true fillMode: Image.PreserveAspectFit smooth: true @@ -137,7 +137,7 @@ AbstractButton { id: blurhash_ anchors.fill: parent - source: blurhash ? ("image://blurhash/" + blurhash) : ("image://colorimage/:/icons/icons/ui/image-failed.svg?" + Nheko.colors.buttonText) + source: blurhash ? ("image://blurhash/" + blurhash) : ("image://colorimage/:/icons/icons/ui/image-failed.svg?" + palette.buttonText) asynchronous: true fillMode: Image.PreserveAspectFit sourceSize.width: parent.width * Screen.devicePixelRatio @@ -158,7 +158,7 @@ AbstractButton { width: parent.width implicitHeight: imgcaption.implicitHeight anchors.bottom: overlay.bottom - color: Nheko.colors.window + color: palette.window opacity: 0.75 } @@ -171,7 +171,7 @@ AbstractButton { verticalAlignment: Text.AlignVCenter // See this MSC: https://github.com/matrix-org/matrix-doc/pull/2530 text: filename ? filename : body - color: Nheko.colors.text + color: palette.text } } diff --git a/resources/qml/delegates/MessageDelegate.qml b/resources/qml/delegates/MessageDelegate.qml index c0bcec0d..68f65062 100644 --- a/resources/qml/delegates/MessageDelegate.qml +++ b/resources/qml/delegates/MessageDelegate.qml @@ -13,7 +13,7 @@ Item { required property bool isReply property bool keepFullText: !isReply property alias child: chooser.child - implicitWidth: (chooser.child && chooser.child.implicitWidth) ? chooser.child.implicitWidth : 0 + //implicitWidth: chooser.child?.implicitWidth ?? 0 required property double proportionalHeight required property int type required property string typeString @@ -39,6 +39,8 @@ Item { property bool fitsMetadata: (chooser.child && chooser.child.fitsMetadata) ? chooser.child.fitsMetadata : false property int metadataWidth + implicitWidth: chooser.child?.implicitWidth + height: chooser.child ? chooser.child.height : Nheko.paddingLarge DelegateChooser { @@ -48,7 +50,7 @@ Item { roleValue: type //anchors.fill: parent - width: parent.width? parent.width: 0 // this should get rid of "cannot read property 'width' of null" + width: parent?.width ?? 0 // this should get rid of "cannot read property 'width' of null" DelegateChoice { roleValue: MtxEvent.UnknownEvent @@ -78,7 +80,6 @@ Item { } Button { - palette: Nheko.colors Layout.alignment: Qt.AlignHCenter text: qsTr("Go to replacement room") onClicked: room.joinReplacementRoom(eventId) @@ -149,7 +150,7 @@ Item { NoticeMessage { formatted: TimelineManager.escapeEmoji(d.userName) + " " + d.formattedBody - color: TimelineManager.userColor(d.userId, Nheko.colors.base) + color: TimelineManager.userColor(d.userId, palette.base) body: d.body isOnlyEmoji: d.isOnlyEmoji isReply: d.isReply @@ -281,6 +282,20 @@ Item { } + DelegateChoice { + roleValue: MtxEvent.ServerAcl + + NoticeMessage { + body: formatted + isOnlyEmoji: false + isReply: d.isReply + keepFullText: d.keepFullText + isStateEvent: d.isStateEvent + formatted: qsTr("%1 changed which servers are allowed in this room.").arg(d.userName) + } + + } + DelegateChoice { roleValue: MtxEvent.Name @@ -603,7 +618,7 @@ Item { roleValue: MtxEvent.Member ColumnLayout { - width: parent.width + width: parent?.width ?? 100 NoticeMessage { body: formatted @@ -617,7 +632,6 @@ Item { Button { visible: d.relatedEventCacheBuster, room.showAcceptKnockButton(d.eventId) - palette: Nheko.colors Layout.alignment: Qt.AlignHCenter text: qsTr("Allow them in") onClicked: room.acceptKnock(eventId) diff --git a/resources/qml/delegates/NoticeMessage.qml b/resources/qml/delegates/NoticeMessage.qml index d62afb96..88efe7b7 100644 --- a/resources/qml/delegates/NoticeMessage.qml +++ b/resources/qml/delegates/NoticeMessage.qml @@ -9,7 +9,7 @@ import im.nheko 1.0 TextMessage { property bool isStateEvent font.italic: true - color: Nheko.colors.buttonText + color: palette.buttonText font.pointSize: isStateEvent? 0.8*Settings.fontSize : Settings.fontSize horizontalAlignment: isStateEvent? Text.AlignHCenter : undefined } diff --git a/resources/qml/delegates/Pill.qml b/resources/qml/delegates/Pill.qml index b60781cb..3f981d4d 100644 --- a/resources/qml/delegates/Pill.qml +++ b/resources/qml/delegates/Pill.qml @@ -8,14 +8,14 @@ import im.nheko 1.0 Label { property bool isStateEvent - color: Nheko.colors.text + color: palette.text horizontalAlignment: Text.AlignHCenter height: Math.round(fontMetrics.height * 1.4) width: contentWidth * 1.2 background: Rectangle { radius: parent.height / 2 - color: Nheko.colors.alternateBase + color: palette.alternateBase } } diff --git a/resources/qml/delegates/Placeholder.qml b/resources/qml/delegates/Placeholder.qml index 08008765..66e28c03 100644 --- a/resources/qml/delegates/Placeholder.qml +++ b/resources/qml/delegates/Placeholder.qml @@ -10,5 +10,5 @@ MatrixText { text: qsTr("unimplemented event: ") + typeString // width: parent.width - color: Nheko.inactiveColors.text + color: palette.inactive.text } diff --git a/resources/qml/delegates/PlayableMediaMessage.qml b/resources/qml/delegates/PlayableMediaMessage.qml index 741369d2..fb7bf0cc 100644 --- a/resources/qml/delegates/PlayableMediaMessage.qml +++ b/resources/qml/delegates/PlayableMediaMessage.qml @@ -4,11 +4,11 @@ import "../" import "../ui/media" -import QtMultimedia 5.15 -import QtQuick 2.15 -import QtQuick.Controls 2.15 -import QtQuick.Layouts 1.15 -import im.nheko 1.0 +import QtMultimedia +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import im.nheko Item { id: content @@ -25,9 +25,9 @@ Item { property double divisor: isReply ? 4 : 2 property int tempWidth: originalWidth < 1? 400: originalWidth implicitWidth: type == MtxEvent.VideoMessage ? Math.round(tempWidth*Math.min((timelineView.height/divisor)/(tempWidth*proportionalHeight), 1)) : 500 - width: Math.min(parent.width, implicitWidth) + width: Math.min(parent?.width ?? implicitWidth, implicitWidth) height: (type == MtxEvent.VideoMessage ? width*proportionalHeight : 80) + fileInfoLabel.height - implicitHeight: height + //implicitHeight: height property int metadataWidth property bool fitsMetadata: (parent.width - fileInfoLabel.width) > metadataWidth+4 @@ -36,18 +36,18 @@ Item { id: mxcmedia // TODO: Show error in overlay or so? - onError: console.log(error) roomm: room - // desiredVolume is a float from 0.0 -> 1.0, MediaPlayer volume is an int from 0 to 100 - // this value automatically gets clamped for us between these two values. - volume: mediaControls.desiredVolume * 100 - muted: mediaControls.muted + audioOutput: AudioOutput { + muted: mediaControls.muted + volume: mediaControls.desiredVolume + } + videoOutput: videoOutput } Rectangle { id: videoContainer - color: type == MtxEvent.VideoMessage ? Nheko.colors.window : "transparent" + color: type == MtxEvent.VideoMessage ? palette.window : "transparent" width: parent.width height: parent.height - fileInfoLabel.height @@ -57,7 +57,7 @@ Item { Image { anchors.fill: parent - source: thumbnailUrl ? thumbnailUrl.replace("mxc://", "image://MxcImage/") + "?scale" : "image://colorimage/:/icons/icons/ui/video-file.svg?" + Nheko.colors.windowText + source: thumbnailUrl ? thumbnailUrl.replace("mxc://", "image://MxcImage/") + "?scale" : "image://colorimage/:/icons/icons/ui/video-file.svg?" + palette.windowText asynchronous: true fillMode: Image.PreserveAspectFit @@ -68,43 +68,40 @@ Item { clip: true anchors.fill: parent fillMode: VideoOutput.PreserveAspectFit - source: mxcmedia - flushMode: VideoOutput.FirstFrame orientation: mxcmedia.orientation } } - } + MediaControls { + id: mediaControls - MediaControls { - id: mediaControls - - anchors.left: content.left - anchors.right: content.right - anchors.bottom: fileInfoLabel.top - playingVideo: type == MtxEvent.VideoMessage - positionValue: mxcmedia.position - duration: mediaLoaded ? mxcmedia.duration : content.duration - mediaLoaded: mxcmedia.loaded - mediaState: mxcmedia.state - onPositionChanged: mxcmedia.position = position - onPlayPauseActivated: mxcmedia.state == MediaPlayer.PlayingState ? mxcmedia.pause() : mxcmedia.play() - onLoadActivated: mxcmedia.eventId = eventId + anchors.left: videoContainer.left + anchors.right: videoContainer.right + anchors.bottom: videoContainer.bottom + playingVideo: type == MtxEvent.VideoMessage + positionValue: mxcmedia.position + duration: mediaLoaded ? mxcmedia.duration : content.duration + mediaLoaded: mxcmedia.loaded + mediaState: mxcmedia.playbackState + onPositionChanged: mxcmedia.position = position + onPlayPauseActivated: mxcmedia.playbackState == MediaPlayer.PlayingState ? mxcmedia.pause() : mxcmedia.play() + onLoadActivated: mxcmedia.eventId = eventId + } } // information about file name and file size Label { id: fileInfoLabel - anchors.bottom: content.bottom + anchors.top: videoContainer.bottom text: body + " [" + filesize + "]" textFormat: Text.RichText elide: Text.ElideRight - color: Nheko.colors.text + color: palette.text background: Rectangle { - color: Nheko.colors.base + color: palette.base } } diff --git a/resources/qml/delegates/Redacted.qml b/resources/qml/delegates/Redacted.qml index 74d4e015..4a9700dc 100644 --- a/resources/qml/delegates/Redacted.qml +++ b/resources/qml/delegates/Redacted.qml @@ -13,7 +13,7 @@ Rectangle{ implicitWidth: redactedLayout.implicitWidth + 2 * Nheko.paddingMedium width: Math.min(parent.width,implicitWidth+1) radius: fontMetrics.lineSpacing / 2 + 2 * Nheko.paddingSmall - color: Nheko.colors.alternateBase + color: palette.alternateBase property int metadataWidth property bool fitsMetadata: parent.width - redactedLayout.width > metadataWidth + 4 @@ -28,7 +28,7 @@ Rectangle{ Layout.alignment: Qt.AlignLeft | Qt.AlignVCenter Layout.preferredWidth: fontMetrics.font.pixelSize Layout.preferredHeight: fontMetrics.font.pixelSize - source: "image://colorimage/:/icons/icons/ui/delete.svg?" + Nheko.colors.text + source: "image://colorimage/:/icons/icons/ui/delete.svg?" + palette.text } Label { id: redactedLabel @@ -39,7 +39,7 @@ Rectangle{ property var redactedPair: room.formatRedactedEvent(eventId) text: redactedPair["first"] wrapMode: Label.WordWrap - color: Nheko.colors.text + color: palette.text ToolTip.text: redactedPair["second"] ToolTip.visible: hh.hovered diff --git a/resources/qml/delegates/Reply.qml b/resources/qml/delegates/Reply.qml index c593a4f8..4d4983ac 100644 --- a/resources/qml/delegates/Reply.qml +++ b/resources/qml/delegates/Reply.qml @@ -43,7 +43,7 @@ AbstractButton { implicitHeight: replyContainer.height implicitWidth: visible? colorLine.width+Math.max(replyContainer.implicitWidth,userName_.fullTextWidth) : 0 // visible? seems to be causing issues - CursorShape { + NhekoCursorShape { anchors.fill: parent cursorShape: Qt.PointingHandCursor } @@ -54,7 +54,7 @@ AbstractButton { anchors.top: replyContainer.top anchors.bottom: replyContainer.bottom width: 4 - color: TimelineManager.userColor(userId, Nheko.colors.base) + color: TimelineManager.userColor(userId, palette.base) } onClicked: { @@ -135,8 +135,8 @@ AbstractButton { z: -1 anchors.fill: replyContainer - property color userColor: TimelineManager.userColor(userId, Nheko.colors.base) - property color bgColor: Nheko.colors.base + property color userColor: TimelineManager.userColor(userId, palette.base) + property color bgColor: palette.base color: Qt.tint(bgColor, Qt.hsla(userColor.hslHue, 0.5, userColor.hslLightness, 0.1)) } diff --git a/resources/qml/delegates/TextMessage.qml b/resources/qml/delegates/TextMessage.qml index eb46a9ac..1eb5e2c0 100644 --- a/resources/qml/delegates/TextMessage.qml +++ b/resources/qml/delegates/TextMessage.qml @@ -14,20 +14,19 @@ MatrixText { required property string formatted property string copyText: selectedText ? getText(selectionStart, selectionEnd) : body property int metadataWidth - property bool fitsMetadata: positionAt(width,height-4) == positionAt(width-metadataWidth-10, height-4) + property bool fitsMetadata: false //positionAt(width,height-4) == positionAt(width-metadataWidth-10, height-4) // table border-collapse doesn't seem to work text: " " + formatted.replace(//g, "").replace(/<\/del>/g, "").replace(//g, "").replace(/<\/strike>/g, "") - width: parent.width + width: parent?.width ?? 0 height: !keepFullText ? Math.round(Math.min(timelineView.height / 8, implicitHeight)) : implicitHeight clip: !keepFullText selectByMouse: !Settings.mobileMode && !isReply enabled: !Settings.mobileMode font.pointSize: (Settings.enlargeEmojiOnlyMessages && isOnlyEmoji > 0 && isOnlyEmoji < 4) ? Settings.fontSize * 3 : Settings.fontSize - CursorShape { + NhekoCursorShape { 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 d44fd9cf..afc6fd0a 100644 --- a/resources/qml/device-verification/DeviceVerification.qml +++ b/resources/qml/device-verification/DeviceVerification.qml @@ -15,8 +15,7 @@ ApplicationWindow { onClosing: VerificationManager.removeVerificationFlow(flow) title: stack.currentItem ? (stack.currentItem.title_ || "") : "" modality: Qt.NonModal - palette: Nheko.colors - color: Nheko.colors.window + color: palette.window //height: stack.currentItem.implicitHeight minimumHeight: stack.currentItem.implicitHeight + 2 * Nheko.paddingLarge height: stack.currentItem.implicitHeight + 2 * Nheko.paddingMedium @@ -25,7 +24,7 @@ ApplicationWindow { flags: Qt.Dialog | Qt.WindowCloseButtonHint | Qt.WindowTitleHint background: Rectangle { - color: Nheko.colors.window + color: palette.window } diff --git a/resources/qml/device-verification/DigitVerification.qml b/resources/qml/device-verification/DigitVerification.qml index 10ba4c55..33cc59f4 100644 --- a/resources/qml/device-verification/DigitVerification.qml +++ b/resources/qml/device-verification/DigitVerification.qml @@ -17,7 +17,7 @@ ColumnLayout { Layout.fillWidth: true wrapMode: Text.Wrap text: qsTr("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!") - color: Nheko.colors.text + color: palette.text verticalAlignment: Text.AlignVCenter } @@ -28,19 +28,19 @@ ColumnLayout { Label { font.pixelSize: Qt.application.font.pixelSize * 2 text: flow.sasList[0] - color: Nheko.colors.text + color: palette.text } Label { font.pixelSize: Qt.application.font.pixelSize * 2 text: flow.sasList[1] - color: Nheko.colors.text + color: palette.text } Label { font.pixelSize: Qt.application.font.pixelSize * 2 text: flow.sasList[2] - color: Nheko.colors.text + color: palette.text } } diff --git a/resources/qml/device-verification/EmojiVerification.qml b/resources/qml/device-verification/EmojiVerification.qml index a6f6ff09..0ee279cd 100644 --- a/resources/qml/device-verification/EmojiVerification.qml +++ b/resources/qml/device-verification/EmojiVerification.qml @@ -17,7 +17,7 @@ ColumnLayout { Layout.fillWidth: true wrapMode: Text.Wrap text: qsTr("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!") - color: Nheko.colors.text + color: palette.text verticalAlignment: Text.AlignVCenter } @@ -373,13 +373,13 @@ ColumnLayout { text: col.emoji.emoji font.pixelSize: Qt.application.font.pixelSize * 2 font.family: Settings.emojiFont - color: Nheko.colors.text + color: palette.text } Label { Layout.alignment: Qt.AlignHCenter | Qt.AlignBottom text: col.emoji.description - color: Nheko.colors.text + color: palette.text } } @@ -396,7 +396,7 @@ ColumnLayout { Layout.fillWidth: true wrapMode: Text.Wrap text: qsTr("The displayed emoji might look different in different clients if a different font is used. Similarly they might be translated into different languages. Nonetheless they should depict one of 64 different objects or animals. For example a lion and a cat are different, but a cat is the same even if one client just shows a cat face, while another client shows a full cat body.") - color: Nheko.colors.text + color: palette.text verticalAlignment: Text.AlignVCenter } diff --git a/resources/qml/device-verification/Failed.qml b/resources/qml/device-verification/Failed.qml index fe514df0..5847894b 100644 --- a/resources/qml/device-verification/Failed.qml +++ b/resources/qml/device-verification/Failed.qml @@ -35,7 +35,7 @@ ColumnLayout { return qsTr("Unknown verification error."); } } - color: Nheko.colors.text + color: palette.text verticalAlignment: Text.AlignVCenter } diff --git a/resources/qml/device-verification/NewVerificationRequest.qml b/resources/qml/device-verification/NewVerificationRequest.qml index 84bee834..9a9ab703 100644 --- a/resources/qml/device-verification/NewVerificationRequest.qml +++ b/resources/qml/device-verification/NewVerificationRequest.qml @@ -36,7 +36,7 @@ ColumnLayout { return qsTr("Your device (%1) has requested to be verified.").arg(flow.deviceId); } } - color: Nheko.colors.text + color: palette.text verticalAlignment: Text.AlignVCenter } diff --git a/resources/qml/device-verification/Success.qml b/resources/qml/device-verification/Success.qml index 4e7bd5d1..4b60a5a3 100644 --- a/resources/qml/device-verification/Success.qml +++ b/resources/qml/device-verification/Success.qml @@ -19,7 +19,7 @@ ColumnLayout { Layout.fillWidth: true wrapMode: Text.Wrap text: qsTr("Verification successful! Both sides verified their devices!") - color: Nheko.colors.text + color: palette.text verticalAlignment: Text.AlignVCenter } diff --git a/resources/qml/device-verification/Waiting.qml b/resources/qml/device-verification/Waiting.qml index a7e68ae9..5b45fc7c 100644 --- a/resources/qml/device-verification/Waiting.qml +++ b/resources/qml/device-verification/Waiting.qml @@ -30,14 +30,14 @@ ColumnLayout { return ""; } } - color: Nheko.colors.text + color: palette.text verticalAlignment: Text.AlignVCenter } Item { Layout.fillHeight: true; } Spinner { Layout.alignment: Qt.AlignHCenter - foreground: Nheko.colors.mid + foreground: palette.mid } Item { Layout.fillHeight: true; } diff --git a/resources/qml/dialogs/AliasEditor.qml b/resources/qml/dialogs/AliasEditor.qml index 8a79f7d2..c49ad321 100644 --- a/resources/qml/dialogs/AliasEditor.qml +++ b/resources/qml/dialogs/AliasEditor.qml @@ -41,7 +41,7 @@ ApplicationWindow { font.pixelSize: Math.floor(fontMetrics.font.pixelSize * 1.1) Layout.fillWidth: true Layout.fillHeight: false - color: Nheko.colors.text + color: palette.text Layout.bottomMargin: Nheko.paddingMedium } @@ -53,10 +53,6 @@ ApplicationWindow { clip: true - ScrollHelper { - flickable: parent - anchors.fill: parent - } model: editingModel spacing: 4 @@ -69,7 +65,7 @@ ApplicationWindow { Text { Layout.fillWidth: true text: model.name - color: model.isPublished ? Nheko.colors.text : Nheko.theme.error + color: model.isPublished ? palette.text : Nheko.theme.error textFormat: Text.PlainText } @@ -78,8 +74,8 @@ ApplicationWindow { Layout.margins: 2 image: ":/icons/icons/ui/star.svg" hoverEnabled: true - buttonTextColor: model.isCanonical ? Nheko.colors.highlight : Nheko.colors.text - highlightColor: editingModel.canAdvertize ? Nheko.colors.highlight : buttonTextColor + buttonTextColor: model.isCanonical ? palette.highlight : palette.text + highlightColor: editingModel.canAdvertize ? palette.highlight : buttonTextColor ToolTip.visible: hovered ToolTip.text: model.isCanonical ? qsTr("Primary alias") : qsTr("Make primary alias") @@ -92,8 +88,8 @@ ApplicationWindow { Layout.margins: 2 image: ":/icons/icons/ui/building-shop.svg" hoverEnabled: true - buttonTextColor: model.isAdvertized ? Nheko.colors.highlight : Nheko.colors.text - highlightColor: editingModel.canAdvertize ? Nheko.colors.highlight : buttonTextColor + buttonTextColor: model.isAdvertized ? palette.highlight : palette.text + highlightColor: editingModel.canAdvertize ? palette.highlight : buttonTextColor ToolTip.visible: hovered ToolTip.text: qsTr("Advertise as an alias in this room") @@ -106,7 +102,7 @@ ApplicationWindow { Layout.margins: 2 image: ":/icons/icons/ui/room-directory.svg" hoverEnabled: true - buttonTextColor: model.isPublished ? Nheko.colors.highlight : Nheko.colors.text + buttonTextColor: model.isPublished ? palette.highlight : palette.text ToolTip.visible: hovered ToolTip.text: qsTr("Publish in room directory") @@ -139,7 +135,7 @@ ApplicationWindow { Layout.fillWidth: true selectByMouse: true font.pixelSize: fontMetrics.font.pixelSize - color: Nheko.colors.text + color: palette.text placeholderText: qsTr("#new-alias:server.tld") Component.onCompleted: forceActiveFocus() diff --git a/resources/qml/dialogs/AllowedRoomsSettingsDialog.qml b/resources/qml/dialogs/AllowedRoomsSettingsDialog.qml index d93f1f18..89ea5e04 100644 --- a/resources/qml/dialogs/AllowedRoomsSettingsDialog.qml +++ b/resources/qml/dialogs/AllowedRoomsSettingsDialog.qml @@ -20,8 +20,7 @@ ApplicationWindow { minimumHeight: 450 width: 450 height: 680 - palette: Nheko.colors - color: Nheko.colors.window + color: palette.window modality: Qt.NonModal flags: Qt.Dialog | Qt.WindowCloseButtonHint | Qt.WindowTitleHint title: qsTr("Allowed rooms settings") @@ -42,7 +41,7 @@ ApplicationWindow { font.pixelSize: Math.floor(fontMetrics.font.pixelSize * 1.1) Layout.fillWidth: true Layout.fillHeight: false - color: Nheko.colors.text + color: palette.text Layout.bottomMargin: Nheko.paddingMedium } @@ -54,10 +53,6 @@ ApplicationWindow { clip: true - ScrollHelper { - flickable: parent - anchors.fill: parent - } model: roomSettings.allowedRoomsModel spacing: 4 @@ -72,14 +67,14 @@ ApplicationWindow { Text { Layout.fillWidth: true text: model.name - color: Nheko.colors.text + color: palette.text textFormat: Text.PlainText } Text { Layout.fillWidth: true text: model.isParent ? qsTr("Parent community") : qsTr("Other room") - color: Nheko.colors.buttonText + color: palette.buttonText textFormat: Text.PlainText } } @@ -122,7 +117,7 @@ ApplicationWindow { placeholderText: qsTr("Enter additional rooms not in the list yet...") - color: Nheko.colors.text + color: palette.text onTextEdited: { roomCompleter.completer.searchString = text; } diff --git a/resources/qml/dialogs/ConfirmJoinRoomDialog.qml b/resources/qml/dialogs/ConfirmJoinRoomDialog.qml index b37630c8..a3fb9831 100644 --- a/resources/qml/dialogs/ConfirmJoinRoomDialog.qml +++ b/resources/qml/dialogs/ConfirmJoinRoomDialog.qml @@ -19,8 +19,7 @@ ApplicationWindow { title: summary.isSpace ? qsTr("Confirm community join") : qsTr("Confirm room join") modality: Qt.WindowModal flags: Qt.Dialog | Qt.WindowCloseButtonHint | Qt.WindowTitleHint - palette: Nheko.colors - color: Nheko.colors.window + color: palette.window width: 350 height: content.implicitHeight + Nheko.paddingLarge + footer.implicitHeight @@ -48,7 +47,7 @@ ApplicationWindow { Spinner { Layout.alignment: Qt.AlignHCenter visible: !summary.isLoaded - foreground: Nheko.colors.mid + foreground: palette.mid running: !summary.isLoaded } @@ -57,7 +56,7 @@ ApplicationWindow { textFormat: TextEdit.RichText text: summary.roomName font.pixelSize: fontMetrics.font.pixelSize * 2 - color: Nheko.colors.text + color: palette.text Layout.alignment: Qt.AlignHCenter Layout.fillWidth: true @@ -70,7 +69,7 @@ ApplicationWindow { textFormat: TextEdit.RichText text: summary.roomid font.pixelSize: fontMetrics.font.pixelSize * 0.8 - color: Nheko.colors.text + color: palette.text Layout.alignment: Qt.AlignHCenter Layout.fillWidth: true @@ -96,7 +95,7 @@ ApplicationWindow { readOnly: true textFormat: TextEdit.RichText text: summary.roomTopic - color: Nheko.colors.text + color: palette.text Layout.alignment: Qt.AlignHCenter Layout.fillWidth: true @@ -109,7 +108,7 @@ ApplicationWindow { id: promptLabel text: summary.isKnockOnly ? qsTr("This room can't be joined directly. You can, however, knock on the room and room members can accept or decline this join request. You can additionally provide a reason for them to let you in below:") : qsTr("Do you want to join this room? You can optionally add a reason below:") - color: Nheko.colors.text + color: palette.text Layout.fillWidth: true horizontalAlignment: Text.AlignHCenter wrapMode: Text.Wrap diff --git a/resources/qml/dialogs/CreateDirect.qml b/resources/qml/dialogs/CreateDirect.qml index 4ce568bb..75013970 100644 --- a/resources/qml/dialogs/CreateDirect.qml +++ b/resources/qml/dialogs/CreateDirect.qml @@ -55,14 +55,14 @@ ApplicationWindow { Label { Layout.fillWidth: true text: profile? profile.displayName : "" - color: TimelineManager.userColor(userID.text, Nheko.colors.window) + color: TimelineManager.userColor(userID.text, palette.window) font.pointSize: fontMetrics.font.pointSize } Label { Layout.fillWidth: true text: userID.text - color: Nheko.colors.buttonText + color: palette.buttonText font.pointSize: fontMetrics.font.pointSize * 0.9 } } @@ -89,7 +89,7 @@ ApplicationWindow { Layout.fillWidth: true Layout.alignment: Qt.AlignLeft text: qsTr("Encryption") - color: Nheko.colors.text + color: palette.text } ToggleButton { Layout.alignment: Qt.AlignRight diff --git a/resources/qml/dialogs/CreateRoom.qml b/resources/qml/dialogs/CreateRoom.qml index cb198bb8..2164ba50 100644 --- a/resources/qml/dialogs/CreateRoom.qml +++ b/resources/qml/dialogs/CreateRoom.qml @@ -64,7 +64,7 @@ ApplicationWindow { Label { Layout.preferredWidth: implicitWidth text: "#" - color: Nheko.colors.text + color: palette.text } MatrixTextField { id: newRoomAlias @@ -75,14 +75,14 @@ ApplicationWindow { Layout.preferredWidth: implicitWidth property string userName: userInfoGrid.profile.userid text: userName.substring(userName.indexOf(":")) - color: Nheko.colors.text + color: palette.text } } Label { Layout.preferredWidth: implicitWidth Layout.alignment: Qt.AlignLeft text: qsTr("Public") - color: Nheko.colors.text + color: palette.text HoverHandler { id: privateHover } @@ -101,7 +101,7 @@ ApplicationWindow { Layout.preferredWidth: implicitWidth Layout.alignment: Qt.AlignLeft text: qsTr("Trusted") - color: Nheko.colors.text + color: palette.text HoverHandler { id: trustedHover } @@ -122,7 +122,7 @@ ApplicationWindow { Layout.preferredWidth: implicitWidth Layout.alignment: Qt.AlignLeft text: qsTr("Encryption") - color: Nheko.colors.text + color: palette.text HoverHandler { id: encryptionHover } diff --git a/resources/qml/dialogs/EventExpirationDialog.qml b/resources/qml/dialogs/EventExpirationDialog.qml new file mode 100644 index 00000000..5d12bda8 --- /dev/null +++ b/resources/qml/dialogs/EventExpirationDialog.qml @@ -0,0 +1,167 @@ +// SPDX-FileCopyrightText: Nheko Contributors +// +// SPDX-License-Identifier: GPL-3.0-or-later + +import ".." +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import im.nheko + +ApplicationWindow { + id: dialog + + property string roomid: "" + property string roomName: "" + property var onAccepted: undefined + + modality: Qt.NonModal + flags: Qt.Dialog | Qt.WindowTitleHint + width: 275 + height: 330 + minimumWidth: 250 + minimumHeight: 220 + + EventExpiry { + id: eventExpiry + + roomid: dialog.roomid + } + + title: { + if (roomid) { + return qsTr("Event expiration for %1").arg(roomName); + } + else { + return qsTr("Event expiration"); + } + } + + Shortcut { + sequence: StandardKey.Cancel + onActivated: dbb.rejected() + } + + ColumnLayout { + spacing: Nheko.paddingMedium + anchors.margins: Nheko.paddingMedium + anchors.fill: parent + + MatrixText { + id: promptLabel + text: { + if (roomid) { + return qsTr("You can configure when your messages will be deleted in %1. This only happens when Nheko is open and has permissions to delete messages until Matrix servers support this feature natively. In general 0 means disable.").arg(roomName); + } + else { + return qsTr("You can configure when your messages will be deleted in all rooms unless configured otherwise. This only happens when Nheko is open and has permissions to delete messages until Matrix servers support this feature natively. In general 0 means disable."); + } + } + font.pixelSize: Math.floor(fontMetrics.font.pixelSize * 1.2) + Layout.fillWidth: true + Layout.fillHeight: false + } + + GridLayout { + columns: 2 + rowSpacing: Nheko.paddingMedium + Layout.fillWidth: true + Layout.fillHeight: true + + MatrixText { + text: qsTr("Expire events after X days") + ToolTip.text: qsTr("Automatically redacts messages after X days, unless otherwise protected. Set to 0 to disable.") + ToolTip.visible: hh1.hovered + Layout.fillWidth: true + + HoverHandler { + id: hh1 + } + } + + SpinBox { + Layout.alignment: Qt.AlignRight | Qt.AlignVCenter + from: 0 + to: 1000 + stepSize: 1 + value: eventExpiry.expireEventsAfterDays + onValueChanged: eventExpiry.expireEventsAfterDays = value + editable: true + } + + MatrixText { + text: qsTr("Only keep latest X events") + ToolTip.text: qsTr("Deletes your events in this room if there are more than X newer messages unless otherwise protected. Set to 0 to disable.") + ToolTip.visible: hh2.hovered + Layout.fillWidth: true + + HoverHandler { + id: hh2 + } + } + + + SpinBox { + Layout.alignment: Qt.AlignRight | Qt.AlignVCenter + from: 0 + to: 1000 + stepSize: 1 + value: eventExpiry.expireEventsAfterCount + onValueChanged: eventExpiry.expireEventsAfterCount = value + editable: true + } + + MatrixText { + text: qsTr("Always keep latest X events") + ToolTip.text: qsTr("This prevents events to be deleted by the above 2 settings if they are the latest X messages from you in the room.") + ToolTip.visible: hh3.hovered + Layout.fillWidth: true + + HoverHandler { + id: hh3 + } + } + + + SpinBox { + Layout.alignment: Qt.AlignRight | Qt.AlignVCenter + from: 0 + to: 1000 + stepSize: 1 + value: eventExpiry.protectLatestEvents + onValueChanged: eventExpiry.protectLatestEvents = value + editable: true + } + + MatrixText { + text: qsTr("Include state events") + ToolTip.text: qsTr("If this is turned on, old state events also get redacted. The latest state event of any type+key combination is excluded from redaction to not remove the room name and similar state by accident.") + ToolTip.visible: hh4.hovered + Layout.fillWidth: true + + HoverHandler { + id: hh4 + } + } + + ToggleButton { + Layout.alignment: Qt.AlignRight + checked: eventExpiry.expireStateEvents + onToggled: eventExpiry.expireStateEvents = checked + } + } + } + + footer: DialogButtonBox { + id: dbb + + standardButtons: DialogButtonBox.Ok | DialogButtonBox.Cancel + onAccepted: { + eventExpiry.save(); + dialog.close(); + } + onRejected: dialog.close(); + } + +} + diff --git a/resources/qml/dialogs/ImageOverlay.qml b/resources/qml/dialogs/ImageOverlay.qml index fa874529..b914829e 100644 --- a/resources/qml/dialogs/ImageOverlay.qml +++ b/resources/qml/dialogs/ImageOverlay.qml @@ -25,12 +25,12 @@ Window { Component.onCompleted: Nheko.setWindowRole(imageOverlay, "imageoverlay") Shortcut { - sequence: StandardKey.Cancel + sequences: [StandardKey.Cancel] onActivated: imageOverlay.close() } Shortcut { - sequence: StandardKey.Copy + sequences: [StandardKey.Copy] onActivated: { if (room) { room.copyMedia(eventId); @@ -98,6 +98,10 @@ Window { WheelHandler { property: "scale" + // workaround for QTBUG-87646 / QTBUG-112394 / QTBUG-112432: + // Magic Mouse pretends to be a trackpad but doesn't work with PinchHandler + // and we don't yet distinguish mice and trackpads on Wayland either + acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad target: imgContainer } diff --git a/resources/qml/dialogs/ImagePackEditorDialog.qml b/resources/qml/dialogs/ImagePackEditorDialog.qml index 4f30e78a..4cb2c1f6 100644 --- a/resources/qml/dialogs/ImagePackEditorDialog.qml +++ b/resources/qml/dialogs/ImagePackEditorDialog.qml @@ -22,8 +22,7 @@ ApplicationWindow { title: qsTr("Editing image pack") height: 600 width: 600 - palette: Nheko.colors - color: Nheko.colors.base + color: palette.base modality: Qt.WindowModal flags: Qt.Dialog | Qt.WindowCloseButtonHint | Qt.WindowTitleHint @@ -50,11 +49,6 @@ ApplicationWindow { model: imagePack - ScrollHelper { - flickable: parent - anchors.fill: parent - enabled: !Settings.mobileMode - } header: AvatarListTile { title: imagePack.packname @@ -73,13 +67,12 @@ ApplicationWindow { anchors.verticalCenter: parent.verticalCenter height: parent.height - Nheko.paddingSmall * 2 width: 3 - color: Nheko.colors.highlight + color: palette.highlight } } footer: Button { - palette: Nheko.colors onClicked: addFilesDialog.open() width: ListView.view.width text: qsTr("Add images") @@ -100,11 +93,11 @@ ApplicationWindow { delegate: AvatarListTile { id: packItem - property color background: 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 + property color background: palette.window + property color importantText: palette.text + property color unimportantText: palette.buttonText + property color bubbleBackground: palette.highlight + property color bubbleText: palette.highlightedText required property string shortCode required property string url required property string body @@ -129,7 +122,7 @@ ApplicationWindow { id: packinfoC Rectangle { - color: Nheko.colors.window + color: palette.window GridLayout { anchors.fill: parent diff --git a/resources/qml/dialogs/ImagePackSettingsDialog.qml b/resources/qml/dialogs/ImagePackSettingsDialog.qml index 76d84a07..b7aab2a6 100644 --- a/resources/qml/dialogs/ImagePackSettingsDialog.qml +++ b/resources/qml/dialogs/ImagePackSettingsDialog.qml @@ -23,8 +23,7 @@ ApplicationWindow { title: qsTr("Image pack settings") height: 600 width: 800 - palette: Nheko.colors - color: Nheko.colors.base + color: palette.base modality: Qt.NonModal flags: Qt.Dialog | Qt.WindowCloseButtonHint | Qt.WindowTitleHint @@ -56,15 +55,10 @@ ApplicationWindow { model: packlist clip: true - ScrollHelper { - flickable: parent - anchors.fill: parent - enabled: !Settings.mobileMode - } + footer: ColumnLayout { Button { - palette: Nheko.colors onClicked: { var dialog = packEditor.createObject(timelineRoot, { "imagePack": packlist.newPack(false) @@ -78,7 +72,6 @@ ApplicationWindow { } Button { - palette: Nheko.colors onClicked: { var dialog = packEditor.createObject(timelineRoot, { "imagePack": packlist.newPack(true) @@ -96,11 +89,11 @@ ApplicationWindow { delegate: AvatarListTile { id: packItem - property color background: 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 + property color background: palette.window + property color importantText: palette.text + property color unimportantText: palette.buttonText + property color bubbleBackground: palette.highlight + property color bubbleText: palette.highlightedText required property string displayName required property bool fromAccountData required property bool fromCurrentRoom @@ -135,7 +128,7 @@ ApplicationWindow { id: packinfoC Rectangle { - color: Nheko.colors.window + color: palette.window ColumnLayout { id: packinfo @@ -220,11 +213,6 @@ ApplicationWindow { currentIndex: -1 // prevent sorting from stealing focus cacheBuffer: 500 - ScrollHelper { - flickable: parent - anchors.fill: parent - enabled: !Settings.mobileMode - } // Individual emoji delegate: AbstractButton { @@ -243,7 +231,7 @@ ApplicationWindow { background: Rectangle { anchors.fill: parent - color: hovered ? Nheko.colors.highlight : 'transparent' + color: hovered ? palette.highlight : 'transparent' radius: 5 } diff --git a/resources/qml/dialogs/InputDialog.qml b/resources/qml/dialogs/InputDialog.qml index a4ca1683..49becc67 100644 --- a/resources/qml/dialogs/InputDialog.qml +++ b/resources/qml/dialogs/InputDialog.qml @@ -37,7 +37,7 @@ ApplicationWindow { Label { id: promptLabel - color: Nheko.colors.text + color: palette.text } MatrixTextField { diff --git a/resources/qml/dialogs/InviteDialog.qml b/resources/qml/dialogs/InviteDialog.qml index b142818d..58bae7fd 100644 --- a/resources/qml/dialogs/InviteDialog.qml +++ b/resources/qml/dialogs/InviteDialog.qml @@ -40,8 +40,7 @@ ApplicationWindow { title: qsTr("Invite users to %1").arg(invitees.room.plainRoomName) height: 380 width: 340 - palette: Nheko.colors - color: Nheko.colors.window + color: palette.window flags: Qt.Dialog | Qt.WindowCloseButtonHint | Qt.WindowTitleHint Shortcut { @@ -74,12 +73,12 @@ ApplicationWindow { anchors.centerIn: parent id: inviteeUserid text: model.displayName != "" ? model.displayName : model.userid - color: inviteeButton.hovered ? Nheko.colors.highlightedText: Nheko.colors.text + color: inviteeButton.hovered ? palette.highlightedText: palette.text maximumLineCount: 1 } background: Rectangle { - border.color: Nheko.colors.text - color: inviteeButton.hovered ? Nheko.colors.highlight : Nheko.colors.window + border.color: palette.text + color: inviteeButton.hovered ? palette.highlight : palette.window border.width: 1 radius: inviteeButton.height / 2 } @@ -90,7 +89,7 @@ ApplicationWindow { Label { text: qsTr("Search user") Layout.fillWidth: true - color: Nheko.colors.text + color: palette.text } RowLayout { spacing: Nheko.paddingMedium @@ -100,7 +99,7 @@ ApplicationWindow { property bool isValidMxid: text.match("@.+?:.{3,}") - backgroundColor: Nheko.colors.window + backgroundColor: palette.window placeholderText: qsTr("@joe:matrix.org", "Example user id. The name 'joe' can be localized however you want.") Layout.fillWidth: true onAccepted: { @@ -158,7 +157,7 @@ ApplicationWindow { avatarUrl: profile? profile.avatarUrl : "" userid: inviteeEntry.text onClicked: addInvite(inviteeEntry.text, displayName, avatarUrl) - bgColor: del3.hovered ? Nheko.colors.dark : inviteDialogRoot.color + bgColor: del3.hovered ? palette.dark : inviteDialogRoot.color } ListView { visible: !inviteeEntry.isValidMxid @@ -175,7 +174,7 @@ ApplicationWindow { userid: model.userid avatarUrl: model.avatarUrl onClicked: addInvite(userid, displayName, avatarUrl) - bgColor: del2.hovered ? Nheko.colors.dark : inviteDialogRoot.color + bgColor: del2.hovered ? palette.dark : inviteDialogRoot.color } } Rectangle { @@ -202,7 +201,7 @@ ApplicationWindow { userid: model.mxid avatarUrl: model.avatarUrl displayName: model.displayName - bgColor: del.hovered ? Nheko.colors.dark : inviteDialogRoot.color + bgColor: del.hovered ? palette.dark : inviteDialogRoot.color ImageButton { anchors.right: parent.right anchors.rightMargin: Nheko.paddingSmall @@ -213,7 +212,7 @@ ApplicationWindow { onClicked: invitees.removeUser(model.mxid) } - CursorShape { + NhekoCursorShape { anchors.fill: parent cursorShape: Qt.PointingHandCursor } diff --git a/resources/qml/dialogs/JoinRoomDialog.qml b/resources/qml/dialogs/JoinRoomDialog.qml index 57c7bad8..0974325a 100644 --- a/resources/qml/dialogs/JoinRoomDialog.qml +++ b/resources/qml/dialogs/JoinRoomDialog.qml @@ -14,8 +14,7 @@ ApplicationWindow { title: qsTr("Join room") modality: Qt.WindowModal flags: Qt.Dialog | Qt.WindowCloseButtonHint | Qt.WindowTitleHint - palette: Nheko.colors - color: Nheko.colors.window + color: palette.window width: 350 height: fontMetrics.lineSpacing * 7 @@ -33,7 +32,7 @@ ApplicationWindow { id: promptLabel text: qsTr("Room ID or alias") - color: Nheko.colors.text + color: palette.text } MatrixTextField { diff --git a/resources/qml/dialogs/PhoneNumberInputDialog.qml b/resources/qml/dialogs/PhoneNumberInputDialog.qml index f7719800..f1b8eef8 100644 --- a/resources/qml/dialogs/PhoneNumberInputDialog.qml +++ b/resources/qml/dialogs/PhoneNumberInputDialog.qml @@ -31,7 +31,7 @@ ApplicationWindow { id: promptLabel Layout.columnSpan: 2 - color: Nheko.colors.text + color: palette.text } ComboBox { diff --git a/resources/qml/dialogs/PowerLevelEditor.qml b/resources/qml/dialogs/PowerLevelEditor.qml index 7125c712..9fc9ee15 100644 --- a/resources/qml/dialogs/PowerLevelEditor.qml +++ b/resources/qml/dialogs/PowerLevelEditor.qml @@ -41,14 +41,13 @@ ApplicationWindow { font.pixelSize: Math.floor(fontMetrics.font.pixelSize * 1.1) Layout.fillWidth: true Layout.fillHeight: false - color: Nheko.colors.text + color: palette.text Layout.bottomMargin: Nheko.paddingMedium } TabBar { id: bar width: parent.width - palette: Nheko.colors NhekoTabButton { text: qsTr("Roles") @@ -60,7 +59,7 @@ ApplicationWindow { Rectangle { Layout.fillWidth: true Layout.fillHeight: true - color: Nheko.colors.alternateBase + color: palette.alternateBase border.width: 1 border.color: Nheko.theme.separator @@ -78,7 +77,7 @@ ApplicationWindow { font.pixelSize: Math.floor(fontMetrics.font.pixelSize * 1.1) Layout.fillWidth: true Layout.fillHeight: false - color: Nheko.colors.text + color: palette.text } ReorderableListview { @@ -91,7 +90,7 @@ ApplicationWindow { Column { Layout.fillWidth: true - Text { visible: model.isType; text: model.displayName; color: Nheko.colors.text} + Text { visible: model.isType; text: model.displayName; color: palette.text} Text { visible: !model.isType; text: { @@ -104,7 +103,7 @@ ApplicationWindow { else return qsTr("Custom (%1)").arg(model.powerlevel) } - color: Nheko.colors.text + color: palette.text } } @@ -137,7 +136,7 @@ ApplicationWindow { z: 5 visible: false - color: Nheko.colors.text + color: palette.text Keys.onPressed: { if (typeEntry.text.includes('.') && event.matches(StandardKey.InsertParagraphSeparator)) { @@ -166,7 +165,7 @@ ApplicationWindow { anchors.fill: parent visible: false - color: Nheko.colors.alternateBase + color: palette.alternateBase RowLayout { spacing: Nheko.paddingMedium @@ -238,7 +237,7 @@ ApplicationWindow { width: parent.width //font.pixelSize: Math.ceil(quickSwitcher.textHeight * 0.6) - color: Nheko.colors.text + color: palette.text onTextEdited: { userCompleter.completer.searchString = text; } @@ -318,11 +317,11 @@ ApplicationWindow { if (model.isUser) return model.avatarUrl.replace("mxc://", "image://MxcImage/") else if (editingModel.adminLevel >= model.powerlevel) - return "image://colorimage/:/icons/icons/ui/ribbon_star.svg?" + Nheko.colors.buttonText; + return "image://colorimage/:/icons/icons/ui/ribbon_star.svg?" + palette.buttonText; else if (editingModel.moderatorLevel >= model.powerlevel) - return "image://colorimage/:/icons/icons/ui/ribbon.svg?" + Nheko.colors.buttonText; + return "image://colorimage/:/icons/icons/ui/ribbon.svg?" + palette.buttonText; else - return "image://colorimage/:/icons/icons/ui/person.svg?" + Nheko.colors.buttonText; + return "image://colorimage/:/icons/icons/ui/person.svg?" + palette.buttonText; } displayName: model.displayName enabled: false @@ -330,8 +329,8 @@ ApplicationWindow { Column { Layout.fillWidth: true - Text { visible: model.isUser; text: model.displayName; color: Nheko.colors.text} - Text { visible: model.isUser; text: model.mxid; color: Nheko.colors.text} + Text { visible: model.isUser; text: model.displayName; color: palette.text} + Text { visible: model.isUser; text: model.mxid; color: palette.text} Text { visible: !model.isUser; text: { @@ -342,7 +341,7 @@ ApplicationWindow { else return qsTr("Custom (%1)").arg(model.powerlevel) } - color: Nheko.colors.text + color: palette.text } } diff --git a/resources/qml/dialogs/PowerLevelSpacesApplyDialog.qml b/resources/qml/dialogs/PowerLevelSpacesApplyDialog.qml index e66f92a2..01ec8b61 100644 --- a/resources/qml/dialogs/PowerLevelSpacesApplyDialog.qml +++ b/resources/qml/dialogs/PowerLevelSpacesApplyDialog.qml @@ -21,8 +21,7 @@ ApplicationWindow { minimumHeight: 450 width: 450 height: 680 - palette: Nheko.colors - color: Nheko.colors.window + color: palette.window modality: Qt.NonModal flags: Qt.Dialog | Qt.WindowCloseButtonHint | Qt.WindowTitleHint title: qsTr("Apply permission changes") @@ -43,7 +42,7 @@ ApplicationWindow { font.pixelSize: Math.floor(fontMetrics.font.pixelSize * 1.1) Layout.fillWidth: true Layout.fillHeight: false - color: Nheko.colors.text + color: palette.text Layout.bottomMargin: Nheko.paddingMedium } @@ -55,7 +54,7 @@ ApplicationWindow { Label { text: qsTr("Apply permissions recursively") Layout.fillWidth: true - color: Nheko.colors.text + color: palette.text } ToggleButton { @@ -67,7 +66,7 @@ ApplicationWindow { Label { text: qsTr("Overwrite exisiting modifications in rooms") Layout.fillWidth: true - color: Nheko.colors.text + color: palette.text } ToggleButton { @@ -85,11 +84,6 @@ ApplicationWindow { clip: true - ScrollHelper { - flickable: parent - anchors.fill: parent - } - model: editingModel.spaces spacing: 4 cacheBuffer: 50 @@ -103,7 +97,7 @@ ApplicationWindow { Text { Layout.fillWidth: true text: model.displayName - color: Nheko.colors.text + color: palette.text textFormat: Text.PlainText elide: Text.ElideRight } @@ -117,7 +111,7 @@ ApplicationWindow { return qsTr("Permissions synchronized with community") } elide: Text.ElideRight - color: Nheko.colors.buttonText + color: palette.buttonText textFormat: Text.PlainText } } diff --git a/resources/qml/dialogs/RawMessageDialog.qml b/resources/qml/dialogs/RawMessageDialog.qml index ef7159cb..a27d988e 100644 --- a/resources/qml/dialogs/RawMessageDialog.qml +++ b/resources/qml/dialogs/RawMessageDialog.qml @@ -13,8 +13,7 @@ ApplicationWindow { height: 420 width: 420 - palette: Nheko.colors - color: Nheko.colors.window + color: palette.window flags: Qt.Tool | Qt.WindowStaysOnTopHint | Qt.WindowCloseButtonHint | Qt.WindowTitleHint Shortcut { @@ -25,14 +24,13 @@ ApplicationWindow { ScrollView { anchors.margins: Nheko.paddingMedium anchors.fill: parent - palette: Nheko.colors padding: Nheko.paddingMedium TextArea { id: rawMessageView font: Nheko.monospaceFont() - color: Nheko.colors.text + color: palette.text readOnly: true selectByMouse: !Settings.mobileMode textFormat: Text.PlainText @@ -40,7 +38,7 @@ ApplicationWindow { anchors.fill: parent background: Rectangle { - color: Nheko.colors.base + color: palette.base } } diff --git a/resources/qml/dialogs/ReadReceipts.qml b/resources/qml/dialogs/ReadReceipts.qml index 3d23a5fc..d65de73c 100644 --- a/resources/qml/dialogs/ReadReceipts.qml +++ b/resources/qml/dialogs/ReadReceipts.qml @@ -18,8 +18,7 @@ ApplicationWindow { width: 340 minimumHeight: 380 minimumWidth: headerTitle.width + 2 * Nheko.paddingMedium - palette: Nheko.colors - color: Nheko.colors.window + color: palette.window flags: Qt.Dialog | Qt.WindowCloseButtonHint | Qt.WindowTitleHint Shortcut { @@ -35,14 +34,13 @@ ApplicationWindow { Label { id: headerTitle - color: Nheko.colors.text + color: palette.text Layout.alignment: Qt.AlignCenter text: qsTr("Read receipts") font.pointSize: fontMetrics.font.pointSize * 1.5 } ScrollView { - palette: Nheko.colors padding: Nheko.paddingMedium ScrollBar.horizontal.visible: false Layout.fillHeight: true @@ -67,7 +65,7 @@ ApplicationWindow { ToolTip.visible: hovered ToolTip.text: model.mxid background: Rectangle { - color: del.hovered ? Nheko.colors.dark : readReceiptsRoot.color + color: del.hovered ? palette.dark : readReceiptsRoot.color } RowLayout { @@ -93,16 +91,16 @@ ApplicationWindow { Layout.fillWidth: true ElidedLabel { - text: model.displayName - color: TimelineManager.userColor(model ? model.mxid : "", Nheko.colors.window) + fullText: model.displayName + color: TimelineManager.userColor(model ? model.mxid : "", palette.window) font.pointSize: fontMetrics.font.pointSize elideWidth: del.width - Nheko.paddingMedium - avatar.width Layout.fillWidth: true } ElidedLabel { - text: model.timestamp - color: Nheko.colors.buttonText + fullText: model.timestamp + color: palette.buttonText font.pointSize: fontMetrics.font.pointSize * 0.9 elideWidth: del.width - Nheko.paddingMedium - avatar.width Layout.fillWidth: true @@ -112,7 +110,7 @@ ApplicationWindow { } - CursorShape { + NhekoCursorShape { anchors.fill: parent cursorShape: Qt.PointingHandCursor } diff --git a/resources/qml/dialogs/RoomDirectory.qml b/resources/qml/dialogs/RoomDirectory.qml index 85de9b45..a6f53d2e 100644 --- a/resources/qml/dialogs/RoomDirectory.qml +++ b/resources/qml/dialogs/RoomDirectory.qml @@ -18,8 +18,7 @@ ApplicationWindow { minimumHeight: 340 height: 420 width: 650 - palette: Nheko.colors - color: Nheko.colors.window + color: palette.window modality: Qt.NonModal flags: Qt.Dialog | Qt.WindowCloseButtonHint | Qt.WindowTitleHint title: qsTr("Explore Public Rooms") @@ -35,17 +34,12 @@ ApplicationWindow { anchors.fill: parent model: publicRooms - ScrollHelper { - flickable: parent - anchors.fill: parent - } - delegate: Rectangle { id: roomDirDelegate - property color background: Nheko.colors.window - property color importantText: Nheko.colors.text - property color unimportantText: Nheko.colors.buttonText + property color background: palette.window + property color importantText: palette.text + property color unimportantText: palette.buttonText property int avatarSize: fontMetrics.height * 3.2 color: background @@ -143,7 +137,7 @@ ApplicationWindow { anchors.centerIn: parent anchors.margins: Nheko.paddingLarge running: visible - foreground: Nheko.colors.mid + foreground: palette.mid } } @@ -164,7 +158,7 @@ ApplicationWindow { Layout.fillWidth: true selectByMouse: true font.pixelSize: fontMetrics.font.pixelSize - color: Nheko.colors.text + color: palette.text placeholderText: qsTr("Search for public rooms") onTextChanged: searchTimer.restart() @@ -176,7 +170,7 @@ ApplicationWindow { Layout.minimumWidth: 0.3 * header.width Layout.maximumWidth: 0.3 * header.width - color: Nheko.colors.text + color: palette.text placeholderText: qsTr("Choose custom homeserver") onTextChanged: publicRooms.setMatrixServer(text) } diff --git a/resources/qml/dialogs/RoomMembers.qml b/resources/qml/dialogs/RoomMembers.qml index 1cfbe077..bbf1605d 100644 --- a/resources/qml/dialogs/RoomMembers.qml +++ b/resources/qml/dialogs/RoomMembers.qml @@ -20,8 +20,7 @@ ApplicationWindow { height: 650 width: 420 minimumHeight: 420 - palette: Nheko.colors - color: Nheko.colors.window + color: palette.window flags: Qt.Dialog | Qt.WindowCloseButtonHint | Qt.WindowTitleHint Shortcut { @@ -77,7 +76,7 @@ ApplicationWindow { Label { text: qsTr("Sort by: ") - color: Nheko.colors.text + color: palette.text } ComboBox { @@ -94,7 +93,6 @@ ApplicationWindow { } ScrollView { - palette: Nheko.colors padding: Nheko.paddingMedium ScrollBar.horizontal.visible: false Layout.fillHeight: true @@ -108,11 +106,6 @@ ApplicationWindow { boundsBehavior: Flickable.StopAtBounds model: members - ScrollHelper { - flickable: parent - anchors.fill: parent - enabled: !Settings.mobileMode - } delegate: ItemDelegate { id: del @@ -123,7 +116,7 @@ ApplicationWindow { height: memberLayout.implicitHeight + Nheko.paddingSmall * 2 hoverEnabled: true background: Rectangle { - color: del.hovered ? Nheko.colors.dark : roomMembersRoot.color + color: del.hovered ? palette.dark : roomMembersRoot.color } RowLayout { @@ -158,7 +151,7 @@ ApplicationWindow { ElidedLabel { fullText: model.mxid - color: del.hovered ? Nheko.colors.brightText : Nheko.colors.buttonText + color: del.hovered ? palette.brightText : palette.buttonText font.pixelSize: Math.ceil(fontMetrics.font.pixelSize * 0.9) elideWidth: del.width - Nheko.paddingMedium * 2 - avatar.width - encryptInd.width Layout.fillWidth: true @@ -184,7 +177,7 @@ ApplicationWindow { Layout.preferredHeight: 16 sourceSize.width: width sourceSize.height: height - source: sourceUrl + (ma.hovered ? Nheko.colors.highlight : Nheko.colors.buttonText) + source: sourceUrl + (ma.hovered ? palette.highlight : palette.buttonText) ToolTip.visible: ma.hovered ToolTip.text: { if (isAdmin) @@ -227,7 +220,7 @@ ApplicationWindow { } - CursorShape { + NhekoCursorShape { anchors.fill: parent cursorShape: Qt.PointingHandCursor } diff --git a/resources/qml/dialogs/RoomSettings.qml b/resources/qml/dialogs/RoomSettings.qml index 845f4d7a..3b8e1903 100644 --- a/resources/qml/dialogs/RoomSettings.qml +++ b/resources/qml/dialogs/RoomSettings.qml @@ -20,8 +20,7 @@ ApplicationWindow { minimumHeight: 450 width: 450 height: 680 - palette: Nheko.colors - color: Nheko.colors.window + color: palette.window modality: Qt.NonModal flags: Qt.Dialog | Qt.WindowCloseButtonHint | Qt.WindowTitleHint title: qsTr("Room Settings") @@ -30,10 +29,7 @@ ApplicationWindow { sequence: StandardKey.Cancel onActivated: roomSettingsDialog.close() } - ScrollHelper { - flickable: flickable - anchors.fill: flickable - } + Flickable { id: flickable boundsBehavior: Flickable.StopAtBounds @@ -79,7 +75,7 @@ ApplicationWindow { Spinner { Layout.alignment: Qt.AlignHCenter visible: roomSettings.isLoading - foreground: Nheko.colors.mid + foreground: palette.mid running: roomSettings.isLoading } @@ -130,7 +126,7 @@ ApplicationWindow { textFormat: isNameEditingAllowed ? TextEdit.PlainText : TextEdit.RichText text: isNameEditingAllowed ? roomSettings.plainRoomName : roomSettings.roomName font.pixelSize: fontMetrics.font.pixelSize * 2 - color: Nheko.colors.text + color: palette.text Layout.alignment: Qt.AlignHCenter Layout.maximumWidth: parent.width - (Nheko.paddingSmall * 2) - nameChangeButton.anchors.leftMargin - (nameChangeButton.width * 2) @@ -178,7 +174,7 @@ ApplicationWindow { Label { text: qsTr("%n member(s)", "", roomSettings.memberCount) - color: Nheko.colors.text + color: palette.text } ImageButton { @@ -213,11 +209,11 @@ ApplicationWindow { wrapMode: TextEdit.WordWrap background: null selectByMouse: !Settings.mobileMode - color: Nheko.colors.text + color: palette.text horizontalAlignment: TextEdit.AlignHCenter onLinkActivated: Nheko.openLink(link) - CursorShape { + NhekoCursorShape { anchors.fill: parent cursorShape: parent.hoveredLink ? Qt.PointingHandCursor : Qt.ArrowCursor } @@ -261,19 +257,18 @@ ApplicationWindow { Layout.fillWidth: true Label { - text: qsTr("SETTINGS") + text: qsTr("NOTIFICATIONS") font.bold: true - color: Nheko.colors.text - } - - Item { + color: palette.text + Layout.columnSpan: 2 Layout.fillWidth: true + Layout.topMargin: Nheko.paddingLarge } Label { text: qsTr("Notifications") Layout.fillWidth: true - color: Nheko.colors.text + color: palette.text } ComboBox { @@ -286,10 +281,19 @@ ApplicationWindow { WheelHandler{} // suppress scrolling changing values } + Label { + text: qsTr("ENTRY PERMISSIONS") + font.bold: true + color: palette.text + Layout.columnSpan: 2 + Layout.fillWidth: true + Layout.topMargin: Nheko.paddingLarge + } + Label { text: qsTr("Anyone can join") Layout.fillWidth: true - color: Nheko.colors.text + color: palette.text } ToggleButton { @@ -303,7 +307,7 @@ ApplicationWindow { Label { text: qsTr("Allow knocking") Layout.fillWidth: true - color: Nheko.colors.text + color: palette.text visible: knockingButton.visible } @@ -322,7 +326,7 @@ ApplicationWindow { Label { text: qsTr("Allow joining via other rooms") Layout.fillWidth: true - color: Nheko.colors.text + color: palette.text visible: restrictedButton.visible } @@ -341,7 +345,7 @@ ApplicationWindow { Label { text: qsTr("Rooms to join via") Layout.fillWidth: true - color: Nheko.colors.text + color: palette.text visible: allowedRoomsButton.visible } @@ -360,7 +364,7 @@ ApplicationWindow { Label { text: qsTr("Allow guests to join") Layout.fillWidth: true - color: Nheko.colors.text + color: palette.text } ToggleButton { @@ -381,9 +385,152 @@ ApplicationWindow { Layout.fillWidth: true } + Label { + text: qsTr("MESSAGE VISIBILITY") + font.bold: true + color: palette.text + Layout.columnSpan: 2 + Layout.fillWidth: true + Layout.topMargin: Nheko.paddingLarge + } + + Label { + text: qsTr("Allow viewing history without joining") + Layout.fillWidth: true + color: palette.text + ToolTip.text: qsTr("This is useful to see previews of the room or view it on public websites.") + ToolTip.visible: publicHistoryHover.hovered + ToolTip.delay: Nheko.tooltipDelay + + HoverHandler { + id: publicHistoryHover + + } + } + + ToggleButton { + id: publicHistoryButton + + enabled: roomSettings.canChangeHistoryVisibility + checked: roomSettings.historyVisibility == RoomSettings.WorldReadable + Layout.alignment: Qt.AlignRight + } + + Label { + visible: !publicHistoryButton.checked + text: qsTr("Members can see messages since") + Layout.fillWidth: true + color: palette.text + Layout.alignment: Qt.AlignTop | Qt.AlignLeft + ToolTip.text: qsTr("How much of the history is visible to joined members. Changing this won't affect the visibility of already sent messages. It only applies to new messages.") + ToolTip.visible: privateHistoryHover.hovered + ToolTip.delay: Nheko.tooltipDelay + + HoverHandler { + id: privateHistoryHover + + } + } + + ColumnLayout { + Layout.fillWidth: true + visible: !publicHistoryButton.checked + enabled: roomSettings.canChangeHistoryVisibility + Layout.alignment: Qt.AlignTop | Qt.AlignRight + + RadioButton { + id: sharedHistory + checked: roomSettings.historyVisibility == RoomSettings.Shared + text: qsTr("Everything") + ToolTip.text: qsTr("As long as the user joined, they can see all previous messages.") + ToolTip.visible: hovered + ToolTip.delay: Nheko.tooltipDelay + } + RadioButton { + id: invitedHistory + checked: roomSettings.historyVisibility == RoomSettings.Invited + text: qsTr("They got invited") + ToolTip.text: qsTr("Members can only see messages from when they got invited going forward.") + ToolTip.visible: hovered + ToolTip.delay: Nheko.tooltipDelay + } + RadioButton { + id: joinedHistory + checked: roomSettings.historyVisibility == RoomSettings.Joined || roomSettings.historyVisibility == RoomSettings.WorldReadable + text: qsTr("They joined") + ToolTip.text: qsTr("Members can only see messages since after they joined.") + ToolTip.visible: hovered + ToolTip.delay: Nheko.tooltipDelay + } + } + + Button { + visible: roomSettings.historyVisibility != selectedVisibility + enabled: roomSettings.canChangeHistoryVisibility + + text: qsTr("Apply visibility changes") + property int selectedVisibility: { + if (publicHistoryButton.checked) + return RoomSettings.WorldReadable; + else if (sharedHistory.checked) + return RoomSettings.Shared; + else if (invitedHistory.checked) + return RoomSettings.Invited; + return RoomSettings.Joined; + } + onClicked: roomSettings.changeHistoryVisibility(selectedVisibility) + Layout.columnSpan: 2 + Layout.fillWidth: true + } + + Label { + text: qsTr("Locally hidden events") + color: palette.text + } + + HiddenEventsDialog { + id: hiddenEventsDialog + roomid: roomSettings.roomId + roomName: roomSettings.roomName + } + + Button { + text: qsTr("Configure") + ToolTip.text: qsTr("Select events to hide in this room") + onClicked: hiddenEventsDialog.show() + Layout.alignment: Qt.AlignRight + } + + Label { + text: qsTr("Automatic event deletion") + color: palette.text + } + + EventExpirationDialog { + id: eventExpirationDialog + roomid: roomSettings.roomId + roomName: roomSettings.roomName + } + + Button { + text: qsTr("Configure") + ToolTip.text: qsTr("Select if your events get automatically deleted in this room.") + onClicked: eventExpirationDialog.show() + Layout.alignment: Qt.AlignRight + } + + Label { + text: qsTr("GENERAL SETTINGS") + font.bold: true + color: palette.text + Layout.columnSpan: 2 + Layout.fillWidth: true + Layout.topMargin: Nheko.paddingLarge + } + Label { text: qsTr("Encryption") - color: Nheko.colors.text + color: palette.text } ToggleButton { @@ -422,7 +569,7 @@ ApplicationWindow { Label { text: qsTr("Permission") - color: Nheko.colors.text + color: palette.text } Button { @@ -434,7 +581,7 @@ ApplicationWindow { Label { text: qsTr("Aliases") - color: Nheko.colors.text + color: palette.text } Button { @@ -446,7 +593,7 @@ ApplicationWindow { Label { text: qsTr("Sticker & Emote Settings") - color: Nheko.colors.text + color: palette.text } Button { @@ -456,47 +603,18 @@ ApplicationWindow { Layout.alignment: Qt.AlignRight } - Label { - text: qsTr("Hidden events") - color: Nheko.colors.text - } - - HiddenEventsDialog { - id: hiddenEventsDialog - roomid: roomSettings.roomId - roomName: roomSettings.roomName - } - - Button { - text: qsTr("Configure") - ToolTip.text: qsTr("Select events to hide in this room") - onClicked: hiddenEventsDialog.show() - Layout.alignment: Qt.AlignRight - } - - Item { - // for adding extra space between sections - Layout.fillWidth: true - } - - Item { - // for adding extra space between sections - Layout.fillWidth: true - } - Label { text: qsTr("INFO") font.bold: true - color: Nheko.colors.text - } - - Item { + color: palette.text + Layout.columnSpan: 2 + Layout.topMargin: Nheko.paddingLarge Layout.fillWidth: true } Label { text: qsTr("Internal ID") - color: Nheko.colors.text + color: palette.text } AbstractButton { // AbstractButton does not allow setting text color @@ -507,7 +625,7 @@ ApplicationWindow { id: idLabel text: roomSettings.roomId font.pixelSize: Math.floor(fontMetrics.font.pixelSize * 0.8) - color: Nheko.colors.text + color: palette.text width: parent.width horizontalAlignment: Text.AlignRight wrapMode: Text.WrapAnywhere @@ -531,14 +649,14 @@ ApplicationWindow { Label { text: qsTr("Room Version") - color: Nheko.colors.text + color: palette.text } Label { text: roomSettings.roomVersion font.pixelSize: fontMetrics.font.pixelSize Layout.alignment: Qt.AlignRight - color: Nheko.colors.text + color: palette.text } } diff --git a/resources/qml/dialogs/UserProfile.qml b/resources/qml/dialogs/UserProfile.qml index 95800746..b54b52a4 100644 --- a/resources/qml/dialogs/UserProfile.qml +++ b/resources/qml/dialogs/UserProfile.qml @@ -22,8 +22,7 @@ ApplicationWindow { width: 420 minimumWidth: 150 minimumHeight: 150 - palette: Nheko.colors - color: Nheko.colors.window + color: palette.window title: profile.isGlobalUserProfile ? qsTr("Global User Profile") : qsTr("Room User Profile") modality: Qt.NonModal flags: Qt.Dialog | Qt.WindowCloseButtonHint | Qt.WindowTitleHint @@ -47,12 +46,6 @@ ApplicationWindow { anchors.margins: 10 footerPositioning: ListView.OverlayFooter - ScrollHelper { - flickable: parent - anchors.fill: parent - enabled: !Settings.mobileMode - } - header: ColumnLayout { id: contentL @@ -89,7 +82,7 @@ ApplicationWindow { Layout.alignment: Qt.AlignHCenter running: profile.isLoading visible: profile.isLoading - foreground: Nheko.colors.mid + foreground: palette.mid } Text { @@ -137,7 +130,7 @@ ApplicationWindow { readOnly: !isUsernameEditingAllowed text: profile.displayName font.pixelSize: 20 - color: TimelineManager.userColor(profile.userid, Nheko.colors.window) + color: TimelineManager.userColor(profile.userid, palette.window) font.bold: true Layout.alignment: Qt.AlignHCenter Layout.maximumWidth: parent.width - (Nheko.paddingSmall * 2) - usernameChangeButton.anchors.leftMargin - (usernameChangeButton.width * 2) @@ -315,7 +308,6 @@ ApplicationWindow { onCurrentIndexChanged: devicelist.selectedTab = currentIndex - palette: Nheko.colors NhekoTabButton { text: qsTr("Devices") @@ -354,7 +346,7 @@ ApplicationWindow { Layout.alignment: Qt.AlignLeft elide: Text.ElideRight font.bold: true - color: Nheko.colors.text + color: palette.text text: deviceId } @@ -400,7 +392,7 @@ ApplicationWindow { readOnly: !deviceNameRow.isEditingAllowed text: deviceName - color: Nheko.colors.text + color: palette.text Layout.alignment: Qt.AlignLeft Layout.fillWidth: true selectByMouse: true @@ -435,7 +427,7 @@ ApplicationWindow { Layout.fillWidth: true Layout.alignment: Qt.AlignLeft elide: Text.ElideRight - color: Nheko.colors.text + color: palette.text text: qsTr("Last seen %1 from %2").arg(new Date(lastTs).toLocaleString(Locale.ShortFormat)).arg(lastIp ? lastIp : "???") } @@ -504,7 +496,7 @@ ApplicationWindow { ElidedLabel { Layout.alignment: Qt.AlignVCenter - color: Nheko.colors.text + color: palette.text Layout.fillWidth: true elideWidth: width fullText: roomName @@ -527,7 +519,7 @@ ApplicationWindow { background: Rectangle { anchors.fill: parent - color: Nheko.colors.window + color: palette.window } } diff --git a/resources/qml/emoji/StickerPicker.qml b/resources/qml/emoji/StickerPicker.qml index 38788899..b7721db6 100644 --- a/resources/qml/emoji/StickerPicker.qml +++ b/resources/qml/emoji/StickerPicker.qml @@ -3,25 +3,22 @@ // SPDX-License-Identifier: GPL-3.0-or-later import "../" -import QtGraphicalEffects 1.0 -import QtQuick 2.9 -import QtQuick.Controls 2.3 -import QtQuick.Layouts 1.3 -import im.nheko 1.0 -import im.nheko.EmojiModel 1.0 +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import im.nheko Menu { id: stickerPopup property var callback - property var colors property string roomid property alias model: gridView.model required property bool emoji property var textArea - property real highlightHue: Nheko.colors.highlight.hslHue - property real highlightSat: Nheko.colors.highlight.hslSaturation - property real highlightLight: Nheko.colors.highlight.hslLightness + property real highlightHue: palette.highlight.hslHue + property real highlightSat: palette.highlight.hslSaturation + property real highlightLight: palette.highlight.hslLightness readonly property int stickerDim: emoji ? 48 : 128 readonly property int stickerDimPad: stickerDim + Nheko.paddingSmall readonly property int stickersPerRow: emoji ? 7 : 3 @@ -45,7 +42,7 @@ Menu { width: sidebarAvatarSize + Nheko.paddingSmall + stickersPerRow * stickerDimPad + 20 Rectangle { - color: Nheko.colors.window + color: palette.window height: columnView.implicitHeight + Nheko.paddingSmall*2 width: sidebarAvatarSize + Nheko.paddingSmall + stickersPerRow * stickerDimPad + 20 @@ -67,10 +64,8 @@ Menu { Layout.preferredWidth: stickersPerRow * stickerDimPad + 20 - Nheko.paddingSmall Layout.row: 0 Layout.column: 1 - palette: Nheko.colors background: null - placeholderTextColor: Nheko.colors.buttonText - color: Nheko.colors.text + placeholderTextColor: palette.buttonText placeholderText: qsTr("Search") selectByMouse: true rightPadding: clearSearch.width @@ -126,7 +121,7 @@ Menu { section.delegate: Rectangle { width: gridView.width height: childrenRect.height - color: Nheko.colors.alternateBase + color: palette.alternateBase required property string section @@ -134,7 +129,6 @@ Menu { anchors.left: parent.left anchors.right: parent.right text: parent.section - color: Nheko.colors.text font.bold: true } } @@ -142,12 +136,6 @@ Menu { spacing: Nheko.paddingSmall - ScrollHelper { - flickable: parent - anchors.fill: parent - enabled: !Settings.mobileMode - } - // Individual emoji delegate: Row { required property var row; @@ -197,7 +185,6 @@ Menu { font.family: Settings.emojiFont font.pixelSize: 36 text: del.modelData.unicode.replace('\ufe0f', '') - color: Nheko.colors.text } } @@ -214,7 +201,7 @@ Menu { background: Rectangle { anchors.fill: parent - color: hovered ? Nheko.colors.highlight : 'transparent' + color: hovered ? palette.highlight : 'transparent' radius: 5 } @@ -243,6 +230,7 @@ Menu { height: sidebarAvatarSize width: sidebarAvatarSize url: modelData.url.replace("mxc://", "image://MxcImage/") + textColor: modelData.url.startsWith("mxc://") ? palette.text : palette.buttonText displayName: modelData.name roomid: modelData.name diff --git a/resources/qml/pages/LoginPage.qml b/resources/qml/pages/LoginPage.qml index 4273617f..9bf4e97e 100644 --- a/resources/qml/pages/LoginPage.qml +++ b/resources/qml/pages/LoginPage.qml @@ -25,7 +25,6 @@ Item { id: scroll clip: false - palette: Nheko.colors ScrollBar.horizontal.visible: false anchors.left: parent.left anchors.right: parent.right @@ -71,7 +70,7 @@ Item { visible: running running: login.lookingUpHs - foreground: Nheko.colors.mid + foreground: palette.mid } } @@ -127,7 +126,7 @@ Item { visible: running running: login.loggingIn - foreground: Nheko.colors.mid + foreground: palette.mid } } diff --git a/resources/qml/pages/RegisterPage.qml b/resources/qml/pages/RegisterPage.qml index 8536a254..c1bc5310 100644 --- a/resources/qml/pages/RegisterPage.qml +++ b/resources/qml/pages/RegisterPage.qml @@ -25,7 +25,6 @@ Item { id: scroll clip: false - palette: Nheko.colors ScrollBar.horizontal.visible: false anchors.left: parent.left anchors.right: parent.right @@ -70,7 +69,7 @@ Item { visible: running running: regis.lookingUpHs - foreground: Nheko.colors.mid + foreground: palette.mid } } @@ -102,7 +101,7 @@ Item { visible: running running: regis.lookingUpUsername - foreground: Nheko.colors.mid + foreground: palette.mid } Image { @@ -178,7 +177,7 @@ Item { visible: running running: regis.registering - foreground: Nheko.colors.mid + foreground: palette.mid } } diff --git a/resources/qml/pages/UserSettingsPage.qml b/resources/qml/pages/UserSettingsPage.qml index 5c2ebf5f..f23095b6 100644 --- a/resources/qml/pages/UserSettingsPage.qml +++ b/resources/qml/pages/UserSettingsPage.qml @@ -2,6 +2,7 @@ // // SPDX-License-Identifier: GPL-3.0-or-later +pragma ComponentBehavior: Bound import ".." import "../ui" import Qt.labs.platform 1.1 as Platform @@ -16,12 +17,11 @@ Rectangle { property int collapsePoint: 600 property bool collapsed: width < collapsePoint - color: Nheko.colors.window + color: palette.window ScrollView { id: scroll - palette: Nheko.colors ScrollBar.horizontal.visible: false anchors.fill: parent anchors.topMargin: (collapsed? backButton.height : 0)+Nheko.paddingLarge @@ -34,15 +34,17 @@ Rectangle { spacing: Nheko.paddingMedium + width: scroll.availableWidth anchors.fill: parent anchors.leftMargin: userSettingsDialog.collapsed ? 0 : (userSettingsDialog.width-userSettingsDialog.collapsePoint) * 0.4 + Nheko.paddingLarge anchors.rightMargin: anchors.leftMargin + Repeater { model: UserSettingsModel - Layout.fillWidth:true delegate: GridLayout { + width: scroll.availableWidth columns: collapsed? 1 : 2 rows: collapsed? 2: 1 required property var model @@ -51,7 +53,7 @@ Rectangle { Label { Layout.alignment: Qt.AlignLeft Layout.fillWidth: true - color: Nheko.colors.text + color: palette.text text: model.name //Layout.column: 0 Layout.columnSpan: (model.type == UserSettingsModel.SectionTitle && !userSettingsDialog.collapsed) ? 2 : 1 @@ -79,7 +81,7 @@ Rectangle { Layout.columnSpan: (model.type == UserSettingsModel.SectionTitle && !userSettingsDialog.collapsed) ? 2 : 1 Layout.preferredHeight: child.height - Layout.preferredWidth: Math.min(child.implicitWidth, child.width || 1000) + Layout.preferredWidth: child.implicitWidth Layout.maximumWidth: model.type == UserSettingsModel.SectionTitle ? Number.POSITIVE_INFINITY : 400 Layout.fillWidth: model.type == UserSettingsModel.SectionTitle || model.type == UserSettingsModel.Options || model.type == UserSettingsModel.Number Layout.rightMargin: model.type == UserSettingsModel.SectionTitle ? 0 : Nheko.paddingMedium @@ -96,10 +98,11 @@ Rectangle { roleValue: UserSettingsModel.Options ComboBox { anchors.right: parent.right - width: Math.min(parent.width, implicitWidth) model: r.model.values currentIndex: r.model.value + width: Math.min(implicitWidth, scroll.availableWidth - Nheko.paddingMedium) onCurrentIndexChanged: r.model.value = currentIndex + implicitContentWidthPolicy: ComboBox.WidestTextWhenCompleted WheelHandler{} // suppress scrolling changing values } @@ -109,7 +112,6 @@ Rectangle { SpinBox { anchors.right: parent.right - width: Math.min(parent.width, implicitWidth) from: model.valueLowerBound to: model.valueUpperBound stepSize: model.valueStep @@ -130,7 +132,6 @@ Rectangle { readonly property int decimals: 2 anchors.right: parent.right - width: Math.min(parent.width, implicitWidth) from: model.valueLowerBound * div to: model.valueUpperBound * div stepSize: model.valueStep * div @@ -159,7 +160,7 @@ Rectangle { DelegateChoice { roleValue: UserSettingsModel.ReadOnlyText TextEdit { - color: Nheko.colors.text + color: palette.text text: model.value readOnly: true selectByMouse: !Settings.mobileMode @@ -176,7 +177,7 @@ Rectangle { anchors.top: parent.top anchors.left: parent.left anchors.right: parent.right - color: Nheko.colors.buttonText + color: palette.buttonText height: 1 } } diff --git a/resources/qml/pages/WelcomePage.qml b/resources/qml/pages/WelcomePage.qml index 6555cc29..3acdc18f 100644 --- a/resources/qml/pages/WelcomePage.qml +++ b/resources/qml/pages/WelcomePage.qml @@ -28,7 +28,7 @@ ColumnLayout { Layout.alignment: Qt.AlignHCenter Layout.fillWidth: true text: qsTr("Welcome to nheko! The desktop client for the Matrix protocol.") - color: Nheko.colors.text + color: palette.text font.pointSize: fontMetrics.font.pointSize*2 wrapMode: Text.Wrap horizontalAlignment: Text.AlignHCenter @@ -38,7 +38,7 @@ ColumnLayout { Layout.alignment: Qt.AlignHCenter Layout.fillWidth: true text: qsTr("Enjoy your stay!") - color: Nheko.colors.text + color: palette.text font.pointSize: fontMetrics.font.pointSize*1.5 wrapMode: Text.Wrap horizontalAlignment: Text.AlignHCenter @@ -86,7 +86,7 @@ ColumnLayout { Layout.alignment: Qt.AlignLeft Layout.margins: Nheko.paddingLarge text: qsTr("Reduce animations") - color: Nheko.colors.text + color: palette.text HoverHandler { id: hovered diff --git a/resources/qml/ui/NhekoSlider.qml b/resources/qml/ui/NhekoSlider.qml index 724e6e48..5e3a77d8 100644 --- a/resources/qml/ui/NhekoSlider.qml +++ b/resources/qml/ui/NhekoSlider.qml @@ -9,7 +9,7 @@ import im.nheko 1.0 Slider { id: control - property color progressColor: Nheko.colors.highlight + property color progressColor: palette.highlight property bool alwaysShowSlider: true property int sliderRadius: 16 @@ -25,7 +25,7 @@ Slider { width: control.availableWidth - handle.width height: implicitHeight radius: height / 2 - color: Nheko.colors.buttonText + color: palette.buttonText Rectangle { width: control.visualPosition * parent.width diff --git a/resources/qml/ui/Ripple.qml b/resources/qml/ui/Ripple.qml index 192909b2..911b88cf 100644 --- a/resources/qml/ui/Ripple.qml +++ b/resources/qml/ui/Ripple.qml @@ -2,9 +2,8 @@ // // SPDX-License-Identifier: GPL-3.0-or-later -import QtGraphicalEffects 1.0 -import QtQuick 2.15 -import QtQuick.Controls 2.15 +import QtQuick +import QtQuick.Controls Item { id: ripple @@ -21,7 +20,7 @@ Item { PointHandler { id: ph - onGrabChanged: { + onGrabChanged: (_, point) => { circle.centerX = point.position.x circle.centerY = point.position.y } diff --git a/resources/qml/ui/Snackbar.qml b/resources/qml/ui/Snackbar.qml index 051db70d..0d334079 100644 --- a/resources/qml/ui/Snackbar.qml +++ b/resources/qml/ui/Snackbar.qml @@ -9,6 +9,9 @@ import im.nheko 1.0 Popup { id: snackbar + // Workaround palettes not inheriting for popups + palette: timelineRoot.palette + property var messages: [] property string currentMessage: "" @@ -45,7 +48,7 @@ Popup { padding: Nheko.paddingLarge contentItem: Label { - color: Nheko.colors.light + color: palette.light width: Math.max(Overlay.overlay? Overlay.overlay.width/2 : 0, 400) text: snackbar.currentMessage font.bold: true @@ -53,7 +56,7 @@ Popup { background: Rectangle { radius: Nheko.paddingLarge - color: Nheko.colors.dark + color: palette.dark opacity: 0.8 } diff --git a/resources/qml/ui/Spinner.qml b/resources/qml/ui/Spinner.qml index a88318c6..9c0c8a31 100644 --- a/resources/qml/ui/Spinner.qml +++ b/resources/qml/ui/Spinner.qml @@ -3,8 +3,8 @@ // SPDX-License-Identifier: GPL-3.0-or-later import "./animations" -import QtGraphicalEffects 1.12 -import QtQuick 2.12 +import QtQuick +import QtQuick.Effects Item { id: spinner @@ -139,11 +139,11 @@ Item { } - Glow { + MultiEffect { anchors.fill: row - radius: 14 - samples: 17 - color: spinner.foreground + shadowBlur: 14 + shadowEnabled: true + shadowColor: spinner.foreground source: row transform: Matrix4x4 { diff --git a/resources/qml/ui/TimelineEffects.qml b/resources/qml/ui/TimelineEffects.qml index aaff04a0..72237e31 100644 --- a/resources/qml/ui/TimelineEffects.qml +++ b/resources/qml/ui/TimelineEffects.qml @@ -6,34 +6,36 @@ import QtQuick 2.15 import QtQuick.Particles 2.15 Item { + id: effectRoot readonly property int maxLifespan: Math.max(confettiEmitter.lifeSpan, rainfallEmitter.lifeSpan) + required property bool shouldEffectsRun function pulseConfetti() { - confettiEmitter.pulse(parent.height * 2) + confettiEmitter.pulse(effectRoot.height * 2) } function pulseRainfall() { - rainfallEmitter.pulse(parent.height * 3.3) + rainfallEmitter.pulse(effectRoot.height * 3.3) } ParticleSystem { id: particleSystem Component.onCompleted: pause(); - paused: !shouldEffectsRun + paused: !effectRoot.shouldEffectsRun } Emitter { id: confettiEmitter group: "confetti" - width: parent.width * 3/4 + width: effectRoot.width * 3/4 enabled: false - anchors.horizontalCenter: parent.horizontalCenter - y: parent.height - emitRate: Math.min(400 * Math.sqrt(parent.width * parent.height) / 870, 1000) + anchors.horizontalCenter: effectRoot.horizontalCenter + y: effectRoot.height + emitRate: Math.min(400 * Math.sqrt(effectRoot.width * effectRoot.height) / 870, 1000) lifeSpan: 15000 system: particleSystem maximumEmitted: 500 @@ -42,8 +44,8 @@ Item { sizeVariation: 4 velocity: PointDirection { x: 0 - y: -Math.min(450 * parent.height / 700, 1000) - xVariation: Math.min(4 * parent.width / 7, 450) + y: -Math.min(450 * effectRoot.height / 700, 1000) + xVariation: Math.min(4 * effectRoot.width / 7, 450) yVariation: 250 } } @@ -74,7 +76,7 @@ Item { Gravity { system: particleSystem groups: ["confetti"] - anchors.fill: parent + anchors.fill: effectRoot magnitude: 350 angle: 90 } @@ -83,11 +85,11 @@ Item { id: rainfallEmitter group: "rain" - width: parent.width + width: effectRoot.width enabled: false - anchors.horizontalCenter: parent.horizontalCenter + anchors.horizontalCenter: effectRoot.horizontalCenter y: -60 - emitRate: parent.width / 50 + emitRate: effectRoot.width / 50 lifeSpan: 10000 system: particleSystem velocity: PointDirection { diff --git a/resources/qml/ui/animations/BlinkAnimation.qml b/resources/qml/ui/animations/BlinkAnimation.qml index ae730452..de2a11d8 100644 --- a/resources/qml/ui/animations/BlinkAnimation.qml +++ b/resources/qml/ui/animations/BlinkAnimation.qml @@ -2,8 +2,7 @@ // // SPDX-License-Identifier: GPL-3.0-or-later -import QtGraphicalEffects 1.12 -import QtQuick 2.12 +import QtQuick SequentialAnimation { property alias target: numberAnimation.target diff --git a/resources/qml/ui/animations/qmldir b/resources/qml/ui/animations/qmldir deleted file mode 100644 index 14f9ad86..00000000 --- a/resources/qml/ui/animations/qmldir +++ /dev/null @@ -1,2 +0,0 @@ -module im.nheko.UI.Animations -BlinkAnimation 1.0 BlinkAnimation.qml diff --git a/resources/qml/ui/media/MediaControls.qml b/resources/qml/ui/media/MediaControls.qml index f1cb7ca1..bd5f6ddc 100644 --- a/resources/qml/ui/media/MediaControls.qml +++ b/resources/qml/ui/media/MediaControls.qml @@ -4,11 +4,11 @@ import "../" import "../../" -import QtMultimedia 5.15 -import QtQuick 2.15 -import QtQuick.Controls 2.15 -import QtQuick.Layouts 1.15 -import im.nheko 1.0 +import QtMultimedia +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import im.nheko Rectangle { id: control @@ -50,7 +50,7 @@ Rectangle { } color: { - var wc = Nheko.colors.alternateBase; + var wc = palette.alternateBase; return Qt.rgba(wc.r, wc.g, wc.b, 0.5); } opacity: control.shouldShowControls ? 1 : 0 @@ -95,7 +95,7 @@ Rectangle { id: playbackStateImage Layout.alignment: Qt.AlignLeft - buttonTextColor: Nheko.colors.text + buttonTextColor: palette.text Layout.preferredHeight: 24 Layout.preferredWidth: 24 image: { @@ -115,7 +115,7 @@ Rectangle { id: volumeButton Layout.alignment: Qt.AlignLeft - buttonTextColor: Nheko.colors.text + buttonTextColor: palette.text Layout.preferredHeight: 24 Layout.preferredWidth: 24 image: { @@ -130,7 +130,7 @@ Rectangle { NhekoSlider { id: volumeSlider - property real desiredVolume: QtMultimedia.convertVolume(volumeSlider.value, QtMultimedia.LogarithmicVolumeScale, QtMultimedia.LinearVolumeScale) + property real desiredVolume: volumeSlider.value state: "" Layout.alignment: Qt.AlignLeft @@ -214,7 +214,7 @@ Rectangle { Label { Layout.alignment: Qt.AlignRight text: (!control.mediaLoaded ? "-- " : durationToString(control.positionValue)) + " / " + durationToString(control.duration) - color: Nheko.colors.text + color: palette.text } Item { diff --git a/resources/qml/ui/media/qmldir b/resources/qml/ui/media/qmldir deleted file mode 100644 index 143b603d..00000000 --- a/resources/qml/ui/media/qmldir +++ /dev/null @@ -1,3 +0,0 @@ -module im.nheko.UI.Media -VolumeSlider 1.0 VolumeSlider.qml -MediaControls 1.0 MediaControls.qml \ No newline at end of file diff --git a/resources/qml/ui/qmldir b/resources/qml/ui/qmldir deleted file mode 100644 index a2ce7514..00000000 --- a/resources/qml/ui/qmldir +++ /dev/null @@ -1,4 +0,0 @@ -module im.nheko.UI -NhekoSlider 1.0 NhekoSlider.qml -Ripple 1.0 Ripple.qml -Spinner 1.0 Spinner.qml \ No newline at end of file diff --git a/resources/qml/voip/ActiveCallBar.qml b/resources/qml/voip/ActiveCallBar.qml index d055c013..45afedab 100644 --- a/resources/qml/voip/ActiveCallBar.qml +++ b/resources/qml/voip/ActiveCallBar.qml @@ -16,7 +16,7 @@ Rectangle { MouseArea { anchors.fill: parent onClicked: { - if (CallManager.callType != CallType.VOICE) + if (CallManager.callType != Voip.VOICE) stackLayout.currentIndex = stackLayout.currentIndex ? 0 : 1; } @@ -58,7 +58,7 @@ Rectangle { states: [ State { name: "VOICE" - when: CallManager.callType == CallType.VOICE + when: CallManager.callType == Voip.VOICE PropertyChanges { target: callTypeIcon @@ -68,7 +68,7 @@ Rectangle { }, State { name: "VIDEO" - when: CallManager.callType == CallType.VIDEO + when: CallManager.callType == Voip.VIDEO PropertyChanges { target: callTypeIcon @@ -78,7 +78,7 @@ Rectangle { }, State { name: "SCREEN" - when: CallManager.callType == CallType.SCREEN + when: CallManager.callType == Voip.SCREEN PropertyChanges { target: callTypeIcon @@ -100,7 +100,7 @@ Rectangle { states: [ State { name: "OFFERSENT" - when: CallManager.callState == WebRTCState.OFFERSENT + when: CallManager.callState == Voip.OFFERSENT PropertyChanges { target: callStateLabel @@ -110,7 +110,7 @@ Rectangle { }, State { name: "CONNECTING" - when: CallManager.callState == WebRTCState.CONNECTING + when: CallManager.callState == Voip.CONNECTING PropertyChanges { target: callStateLabel @@ -120,7 +120,7 @@ Rectangle { }, State { name: "ANSWERSENT" - when: CallManager.callState == WebRTCState.ANSWERSENT + when: CallManager.callState == Voip.ANSWERSENT PropertyChanges { target: callStateLabel @@ -130,7 +130,7 @@ Rectangle { }, State { name: "CONNECTED" - when: CallManager.callState == WebRTCState.CONNECTED + when: CallManager.callState == Voip.CONNECTED PropertyChanges { target: callStateLabel @@ -144,13 +144,13 @@ Rectangle { PropertyChanges { target: stackLayout - currentIndex: CallManager.callType != CallType.VOICE ? 1 : 0 + currentIndex: CallManager.callType != Voip.VOICE ? 1 : 0 } }, State { name: "DISCONNECTED" - when: CallManager.callState == WebRTCState.DISCONNECTED + when: CallManager.callState == Voip.DISCONNECTED PropertyChanges { target: callStateLabel @@ -176,7 +176,7 @@ Rectangle { } interval: 1000 - running: CallManager.callState == WebRTCState.CONNECTED + running: CallManager.callState == Voip.CONNECTED repeat: true onTriggered: { var d = new Date(); @@ -190,7 +190,7 @@ Rectangle { Label { Layout.leftMargin: 16 - visible: CallManager.callType == CallType.SCREEN && CallManager.callState == WebRTCState.CONNECTED + visible: CallManager.callType == Voip.SCREEN && CallManager.callState == Voip.CONNECTED text: qsTr("You are screen sharing") font.pointSize: fontMetrics.font.pointSize * 1.1 color: "#000000" diff --git a/resources/qml/voip/CallDevices.qml b/resources/qml/voip/CallDevices.qml index 5a1792c9..46c8cde3 100644 --- a/resources/qml/voip/CallDevices.qml +++ b/resources/qml/voip/CallDevices.qml @@ -9,7 +9,6 @@ import im.nheko 1.0 Popup { modal: true - palette: Nheko.colors // only set the anchors on Qt 5.12 or higher // see https://doc.qt.io/qt-5/qml-qtquick-controls2-popup.html#anchors.centerIn-prop Component.onCompleted: { @@ -31,7 +30,7 @@ Popup { Image { Layout.preferredWidth: 22 Layout.preferredHeight: 22 - source: "image://colorimage/:/icons/icons/ui/microphone-unmute.svg?" + Nheko.colors.windowText + source: "image://colorimage/:/icons/icons/ui/microphone-unmute.svg?" + palette.windowText } ComboBox { @@ -44,12 +43,12 @@ Popup { } RowLayout { - visible: CallManager.callType == CallType.VIDEO && CallManager.cameras.length > 0 + visible: CallManager.callType == Voip.VIDEO && CallManager.cameras.length > 0 Image { Layout.preferredWidth: 22 Layout.preferredHeight: 22 - source: "image://colorimage/:/icons/icons/ui/video-call.svg?" + Nheko.colors.windowText + source: "image://colorimage/:/icons/icons/ui/video-call.svg?" + palette.windowText } ComboBox { @@ -81,8 +80,8 @@ Popup { } background: Rectangle { - color: Nheko.colors.window - border.color: Nheko.colors.windowText + color: palette.window + border.color: palette.windowText } } diff --git a/resources/qml/voip/CallInvite.qml b/resources/qml/voip/CallInvite.qml index 8ffe9892..8a609c32 100644 --- a/resources/qml/voip/CallInvite.qml +++ b/resources/qml/voip/CallInvite.qml @@ -14,7 +14,6 @@ Popup { closePolicy: Popup.NoAutoClose width: parent.width height: parent.height - palette: Nheko.colors Component { id: deviceError @@ -45,7 +44,7 @@ Popup { Layout.fillWidth: true text: CallManager.callPartyDisplayName font.pointSize: fontMetrics.font.pointSize * 2 - color: Nheko.colors.windowText + color: palette.windowText horizontalAlignment: Text.AlignHCenter } @@ -63,19 +62,19 @@ Popup { Layout.bottomMargin: callInv.height / 25 Image { - property string image: CallManager.callType == CallType.VIDEO ? ":/icons/icons/ui/video.svg" : ":/icons/icons/ui/place-call.svg" + property string image: CallManager.callType == Voip.VIDEO ? ":/icons/icons/ui/video.svg" : ":/icons/icons/ui/place-call.svg" Layout.alignment: Qt.AlignCenter Layout.preferredWidth: callInv.height / 10 Layout.preferredHeight: callInv.height / 10 - source: "image://colorimage/" + image + "?" + Nheko.colors.windowText + source: "image://colorimage/" + image + "?" + palette.windowText } Label { Layout.alignment: Qt.AlignCenter - text: CallManager.callType == CallType.VIDEO ? qsTr("Video Call") : qsTr("Voice Call") + text: CallManager.callType == Voip.VIDEO ? qsTr("Video Call") : qsTr("Voice Call") font.pointSize: fontMetrics.font.pointSize * 2 - color: Nheko.colors.windowText + color: palette.windowText } } @@ -94,7 +93,7 @@ Popup { Image { Layout.preferredWidth: deviceCombos.imageSize Layout.preferredHeight: deviceCombos.imageSize - source: "image://colorimage/:/icons/icons/ui/microphone-unmute.svg?" + Nheko.colors.windowText + source: "image://colorimage/:/icons/icons/ui/microphone-unmute.svg?" + palette.windowText } ComboBox { @@ -107,13 +106,13 @@ Popup { } RowLayout { - visible: CallManager.callType == CallType.VIDEO && CallManager.cameras.length > 0 + visible: CallManager.callType == Voip.VIDEO && CallManager.cameras.length > 0 Layout.alignment: Qt.AlignCenter Image { Layout.preferredWidth: deviceCombos.imageSize Layout.preferredHeight: deviceCombos.imageSize - source: "image://colorimage/:/icons/icons/ui/video.svg?" + Nheko.colors.windowText + source: "image://colorimage/:/icons/icons/ui/video.svg?" + palette.windowText } ComboBox { @@ -170,7 +169,7 @@ Popup { RoundButton { id: acceptButton - property string image: CallManager.callType == CallType.VIDEO ? ":/icons/icons/ui/video.svg" : ":/icons/icons/ui/place-call.svg" + property string image: CallManager.callType == Voip.VIDEO ? ":/icons/icons/ui/video.svg" : ":/icons/icons/ui/place-call.svg" implicitWidth: buttonLayout.buttonSize implicitHeight: buttonLayout.buttonSize @@ -201,8 +200,8 @@ Popup { } background: Rectangle { - color: Nheko.colors.window - border.color: Nheko.colors.windowText + color: palette.window + border.color: palette.windowText } } diff --git a/resources/qml/voip/CallInviteBar.qml b/resources/qml/voip/CallInviteBar.qml index 4eaff020..d82bd143 100644 --- a/resources/qml/voip/CallInviteBar.qml +++ b/resources/qml/voip/CallInviteBar.qml @@ -57,12 +57,12 @@ Rectangle { Layout.leftMargin: 4 Layout.preferredWidth: 24 Layout.preferredHeight: 24 - source: CallManager.callType == CallType.VIDEO ? "qrc:/icons/icons/ui/video.svg" : "qrc:/icons/icons/ui/place-call.svg" + source: CallManager.callType == Voip.VIDEO ? "qrc:/icons/icons/ui/video.svg" : "qrc:/icons/icons/ui/place-call.svg" } Label { font.pointSize: fontMetrics.font.pointSize * 1.1 - text: CallManager.callType == CallType.VIDEO ? qsTr("Video Call") : qsTr("Voice Call") + text: CallManager.callType == Voip.VIDEO ? qsTr("Video Call") : qsTr("Voice Call") color: "#000000" } @@ -88,9 +88,8 @@ Rectangle { Button { Layout.rightMargin: 4 - icon.source: CallManager.callType == CallType.VIDEO ? "qrc:/icons/icons/ui/video.svg" : "qrc:/icons/icons/ui/place-call.svg" + icon.source: CallManager.callType == Voip.VIDEO ? "qrc:/icons/icons/ui/video.svg" : "qrc:/icons/icons/ui/place-call.svg" text: qsTr("Accept") - palette: Nheko.colors onClicked: { if (CallManager.mics.length == 0) { var dialog = deviceError.createObject(timelineRoot, { @@ -109,7 +108,7 @@ Rectangle { timelineRoot.destroyOnClose(dialog); return ; } - if (CallManager.callType == CallType.VIDEO && CallManager.cameras.length > 0 && !CallManager.cameras.includes(Settings.camera)) { + if (CallManager.callType == Voip.VIDEO && CallManager.cameras.length > 0 && !CallManager.cameras.includes(Settings.camera)) { var dialog = deviceError.createObject(timelineRoot, { "errorString": qsTr("Unknown camera: %1").arg(Settings.camera), "image": ":/icons/icons/ui/video.svg" @@ -126,7 +125,6 @@ Rectangle { Layout.rightMargin: 16 icon.source: "qrc:/icons/icons/ui/end-call.svg" text: qsTr("Decline") - palette: Nheko.colors onClicked: { CallManager.rejectInvite(); } diff --git a/resources/qml/voip/DeviceError.qml b/resources/qml/voip/DeviceError.qml index 9328e385..4b9b4675 100644 --- a/resources/qml/voip/DeviceError.qml +++ b/resources/qml/voip/DeviceError.qml @@ -5,9 +5,9 @@ import QtQuick 2.9 import QtQuick.Controls 2.3 import QtQuick.Layouts 1.2 -import im.nheko 1.0 Popup { + id: r property string errorString property var image @@ -24,19 +24,19 @@ Popup { Image { Layout.preferredWidth: 16 Layout.preferredHeight: 16 - source: "image://colorimage/" + image + "?" + Nheko.colors.windowText + source: "image://colorimage/" + r.image + "?" + palette.windowText } Label { - text: errorString - color: Nheko.colors.windowText + text: r.errorString + color: palette.windowText } } background: Rectangle { - color: Nheko.colors.window - border.color: Nheko.colors.windowText + color: palette.window + border.color: palette.windowText } } diff --git a/resources/qml/voip/PlaceCall.qml b/resources/qml/voip/PlaceCall.qml index c7a64342..c0724828 100644 --- a/resources/qml/voip/PlaceCall.qml +++ b/resources/qml/voip/PlaceCall.qml @@ -17,7 +17,6 @@ Popup { anchors.centerIn = parent; } - palette: Nheko.colors Component { id: deviceError @@ -38,7 +37,7 @@ Popup { Label { text: qsTr("Place a call to %1?").arg(room.roomName) - color: Nheko.colors.windowText + color: palette.windowText } Item { @@ -68,8 +67,8 @@ Popup { Avatar { Layout.rightMargin: cameraCombo.visible ? 16 : 64 - width: Nheko.avatarSize - height: Nheko.avatarSize + Layout.preferredWidth: Nheko.avatarSize + Layout.preferredHeight: Nheko.avatarSize url: room.roomAvatarUrl.replace("mxc://", "image://MxcImage/") displayName: room.roomName roomid: room.roomId @@ -82,7 +81,7 @@ Popup { onClicked: { if (buttonLayout.validateMic()) { Settings.microphone = micCombo.currentText; - CallManager.sendInvite(room.roomId, CallType.VOICE); + CallManager.sendInvite(room.roomId, Voip.VOICE); close(); } } @@ -96,7 +95,7 @@ Popup { if (buttonLayout.validateMic()) { Settings.microphone = micCombo.currentText; Settings.camera = cameraCombo.currentText; - CallManager.sendInvite(room.roomId, CallType.VIDEO); + CallManager.sendInvite(room.roomId, Voip.VIDEO); close(); } } @@ -138,7 +137,7 @@ Popup { Image { Layout.preferredWidth: 22 Layout.preferredHeight: 22 - source: "image://colorimage/:/icons/icons/ui/microphone-unmute.svg?" + Nheko.colors.windowText + source: "image://colorimage/:/icons/icons/ui/microphone-unmute.svg?" + palette.windowText } ComboBox { @@ -159,7 +158,7 @@ Popup { Image { Layout.preferredWidth: 22 Layout.preferredHeight: 22 - source: "image://colorimage/:/icons/icons/ui/video.svg?" + Nheko.colors.windowText + source: "image://colorimage/:/icons/icons/ui/video.svg?" + palette.windowText } ComboBox { @@ -176,8 +175,8 @@ Popup { } background: Rectangle { - color: Nheko.colors.window - border.color: Nheko.colors.windowText + color: palette.window + border.color: palette.windowText } } diff --git a/resources/qml/voip/ScreenShare.qml b/resources/qml/voip/ScreenShare.qml index 1a82a5ce..7f8665bc 100644 --- a/resources/qml/voip/ScreenShare.qml +++ b/resources/qml/voip/ScreenShare.qml @@ -19,7 +19,6 @@ Popup { Component.onDestruction: { CallManager.closeScreenShare(); } - palette: Nheko.colors ColumnLayout { Label { @@ -29,7 +28,7 @@ Popup { Layout.rightMargin: 8 Layout.alignment: Qt.AlignLeft text: qsTr("Share desktop with %1?").arg(room.roomName) - color: Nheko.colors.windowText + color: palette.windowText } RowLayout { @@ -40,7 +39,7 @@ Popup { Label { Layout.alignment: Qt.AlignLeft text: qsTr("Method:") - color: Nheko.colors.windowText + color: palette.windowText } ComboBox { @@ -60,11 +59,11 @@ Popup { Label { Layout.alignment: Qt.AlignLeft text: qsTr("Window:") - color: Nheko.colors.windowText + color: palette.windowText } ComboBox { - visible: CallManager.screenShareType == ScreenShareType.X11 + visible: CallManager.screenShareType == Voip.X11 id: windowCombo Layout.fillWidth: true @@ -72,7 +71,7 @@ Popup { } Button { - visible: CallManager.screenShareType == ScreenShareType.XDP + visible: CallManager.screenShareType == Voip.XDP highlighted: !CallManager.screenShareReady text: qsTr("Request screencast") onClicked: { @@ -91,7 +90,7 @@ Popup { Label { Layout.alignment: Qt.AlignLeft text: qsTr("Frame rate:") - color: Nheko.colors.windowText + color: palette.windowText } ComboBox { @@ -166,7 +165,7 @@ Popup { Settings.screenShareRemoteVideo = remoteVideoCheckBox.checked; Settings.screenShareHideCursor = hideCursorCheckBox.checked; - CallManager.sendInvite(room.roomId, CallType.SCREEN, windowCombo.currentIndex); + CallManager.sendInvite(room.roomId, Voip.SCREEN, windowCombo.currentIndex); close(); } } @@ -191,8 +190,8 @@ Popup { } background: Rectangle { - color: Nheko.colors.window - border.color: Nheko.colors.windowText + color: palette.window + border.color: palette.windowText } } diff --git a/resources/res.qrc b/resources/res.qrc index bcef8841..fb857d4a 100644 --- a/resources/res.qrc +++ b/resources/res.qrc @@ -90,115 +90,9 @@ nheko-32.png nheko-16.png - - styles/system.qss - styles/nheko.qss - styles/nheko-dark.qss - qtquickcontrols2.conf - qml/Root.qml - qml/ChatPage.qml - qml/CommunitiesList.qml - qml/RoomList.qml - qml/TimelineView.qml - qml/Avatar.qml - qml/Completer.qml - qml/EncryptionIndicator.qml - qml/ImageButton.qml - qml/ElidedLabel.qml - qml/MatrixText.qml - qml/MatrixTextField.qml - qml/ToggleButton.qml - qml/UploadBox.qml - qml/MessageInput.qml - qml/MessageView.qml - qml/PrivacyScreen.qml - qml/Reactions.qml - qml/ReplyPopup.qml - qml/ScrollHelper.qml - qml/StatusIndicator.qml - qml/TimelineRow.qml - qml/TopBar.qml - qml/QuickSwitcher.qml - qml/ForwardCompleter.qml - qml/SelfVerificationCheck.qml - qml/TypingIndicator.qml - qml/MessageInputWarning.qml - qml/components/AdaptiveLayout.qml - qml/components/AdaptiveLayoutElement.qml - qml/components/AvatarListTile.qml - qml/components/FlatButton.qml - qml/components/MainWindowDialog.qml - qml/components/NhekoTabButton.qml - qml/components/NotificationBubble.qml - qml/components/ReorderableListview.qml - qml/components/SpaceMenuLevel.qml - qml/components/TextButton.qml - qml/components/UserListRow.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/Redacted.qml - qml/delegates/Reply.qml - qml/delegates/TextMessage.qml - qml/device-verification/DeviceVerification.qml - qml/device-verification/DigitVerification.qml - qml/device-verification/EmojiVerification.qml - qml/device-verification/Failed.qml - qml/device-verification/NewVerificationRequest.qml - qml/device-verification/Success.qml - qml/device-verification/Waiting.qml - qml/dialogs/AliasEditor.qml - qml/dialogs/ConfirmJoinRoomDialog.qml - qml/dialogs/CreateDirect.qml - qml/dialogs/CreateRoom.qml - qml/dialogs/HiddenEventsDialog.qml - qml/dialogs/ImageOverlay.qml - qml/dialogs/ImagePackEditorDialog.qml - qml/dialogs/ImagePackSettingsDialog.qml - qml/dialogs/InputDialog.qml - qml/dialogs/InviteDialog.qml - qml/dialogs/JoinRoomDialog.qml - qml/dialogs/LeaveRoomDialog.qml - qml/dialogs/LogoutDialog.qml - qml/dialogs/PhoneNumberInputDialog.qml - qml/dialogs/PowerLevelEditor.qml - qml/dialogs/PowerLevelSpacesApplyDialog.qml - qml/dialogs/RawMessageDialog.qml - qml/dialogs/ReadReceipts.qml - qml/dialogs/RoomDirectory.qml - qml/dialogs/RoomMembers.qml - qml/dialogs/AllowedRoomsSettingsDialog.qml - qml/dialogs/RoomSettings.qml - qml/dialogs/UserProfile.qml - qml/emoji/StickerPicker.qml - qml/pages/LoginPage.qml - qml/pages/RegisterPage.qml - qml/pages/UserSettingsPage.qml - qml/pages/WelcomePage.qml - qml/ui/NhekoSlider.qml - qml/ui/Ripple.qml - qml/ui/Snackbar.qml - qml/ui/Spinner.qml - qml/ui/animations/BlinkAnimation.qml - qml/ui/media/MediaControls.qml - qml/voip/ActiveCallBar.qml - qml/voip/CallDevices.qml - qml/voip/CallInvite.qml - qml/voip/CallInviteBar.qml - qml/voip/DeviceError.qml - qml/voip/PlaceCall.qml - qml/voip/ScreenShare.qml - qml/voip/VideoCall.qml confettiparticle.svg - qml/delegates/EncryptionEnabled.qml - qml/ui/TimelineEffects.qml media/ring.ogg diff --git a/resources/styles/nheko-dark.qss b/resources/styles/nheko-dark.qss deleted file mode 100644 index 597397cd..00000000 --- a/resources/styles/nheko-dark.qss +++ /dev/null @@ -1,77 +0,0 @@ -TextLabel, -QLabel { - color: #caccd1; -} - -TextLabel::a { - color: #38a3d8; -} - -QuickSwitcher, -ReplyPopup, -SuggestionsPopup, -UserSettingsPage, -#scroll_widget, -#UserSettingScrollWidget { - background-color: #202228; -} - -QLineEdit, -EditModal, -dialogs--ReCaptcha, -dialogs--JoinRoom { - background-color: #202228; - color: #caccd1; -} - -PopupItem { - background-color: #202228; - qproperty-hoverColor: rgba(45, 49, 57, 120); -} - -FlatButton { - qproperty-foregroundColor: #727274; - qproperty-backgroundColor: #333; - qproperty-disabledForegroundColor: #222; -} - -RaisedButton { - qproperty-foregroundColor: #caccd1; - qproperty-backgroundColor: #333; -} - -FloatingButton { - qproperty-backgroundColor: #2d3139; - qproperty-foregroundColor: white; -} - -TextField { - qproperty-backgroundColor: #202228; - qproperty-inkColor: #caccd1; - qproperty-labelColor: #caccd1; -} - -TextInputWidget { - border: none; -} - -TextInputWidget, -TextInputWidget > QTextEdit, -TextInputWidget > QLineEdit { - background-color: #2d3139; - color: #caccd1; -} - -Toggle { - qproperty-activeColor: #38a3d8; - qproperty-disabledColor: gray; - qproperty-inactiveColor: gray; - qproperty-trackColor: rgb(240, 240, 240); -} - -QListWidget { - color: #caccd1; - background-color: #202228; -} - -QSplitter::handle { image: none; } diff --git a/resources/styles/nheko.qss b/resources/styles/nheko.qss deleted file mode 100644 index b4b7d427..00000000 --- a/resources/styles/nheko.qss +++ /dev/null @@ -1,68 +0,0 @@ -TextLabel, -QLabel { - color: #333; -} - -TextLabel::a { - color: #0077b5; -} - - -PopupItem { - background-color: white; - qproperty-hoverColor: rgba(192, 193, 195, 120); -} - -FlatButton { - qproperty-foregroundColor: #495057; -} - -RaisedButton { - qproperty-foregroundColor: white; -} - -dialogs--ReCaptcha, -dialogs--JoinRoom, -EditModal, -QListWidget { - background-color: white; - color: #495057; -} - -QComboBox, -QPushButton { - background-color: white; - color: #333; -} - -FloatingButton { - qproperty-backgroundColor: #efefef; - qproperty-foregroundColor: black; -} - -TextField { - qproperty-backgroundColor: white; - qproperty-inkColor: #333; - qproperty-labelColor: #333; -} - -QListWidget, -TextInputWidget, -QTextEdit, -QLineEdit { - background-color: white; - color: #333; -} - -TextInputWidget { - border: none; -} - -Toggle { - qproperty-activeColor: #38a3d8; - qproperty-disabledColor: gray; - qproperty-inactiveColor: gray; - qproperty-trackColor: rgb(240, 240, 240); -} - -QSplitter::handle { image: none; } diff --git a/resources/styles/system.qss b/resources/styles/system.qss deleted file mode 100644 index d2305974..00000000 --- a/resources/styles/system.qss +++ /dev/null @@ -1,39 +0,0 @@ -TextInputWidget { - border: none; -} - -PopupItem { - qproperty-hoverColor: palette(base); -} - -FlatButton { - qproperty-foregroundColor: palette(text); -} - -RaisedButton { - qproperty-foregroundColor: palette(button-text); -} - -TextField { - qproperty-backgroundColor: palette(window); -} - -QTextEdit, -QLineEdit, -QListWidget { - background-color: palette(window); -} - -FloatingButton { - qproperty-backgroundColor: palette(base); - qproperty-foregroundColor: palette(text); -} - -Toggle { - qproperty-activeColor: palette(highlight); - qproperty-disabledColor: palette(dark); - qproperty-inactiveColor: palette(mid); - qproperty-trackColor: palette(base); -} - -QSplitter::handle { image: none; } diff --git a/src/AliasEditModel.h b/src/AliasEditModel.h index 2263659b..04de5016 100644 --- a/src/AliasEditModel.h +++ b/src/AliasEditModel.h @@ -5,6 +5,7 @@ #pragma once #include +#include #include #include @@ -29,6 +30,9 @@ signals: class AliasEditingModel final : public QAbstractListModel { Q_OBJECT + QML_ELEMENT + QML_UNCREATABLE("Please use editAliases to create the models") + Q_PROPERTY(bool canAdvertize READ canAdvertize CONSTANT) public: diff --git a/src/Cache.cpp b/src/Cache.cpp index a2fecd3e..fc158e82 100644 --- a/src/Cache.cpp +++ b/src/Cache.cpp @@ -21,7 +21,7 @@ #if __has_include() #include #else -#include +#include #endif #include @@ -94,11 +94,6 @@ static constexpr auto MEGOLM_SESSIONS_DATA_DB("megolm_sessions_data_db"); using CachedReceipts = std::multimap>; using Receipts = std::map>; -Q_DECLARE_METATYPE(RoomMember) -Q_DECLARE_METATYPE(mtx::responses::Timeline) -Q_DECLARE_METATYPE(RoomInfo) -Q_DECLARE_METATYPE(mtx::responses::QueryKeys) - namespace { std::unique_ptr instance_ = nullptr; } @@ -440,7 +435,7 @@ Cache::loadSecretsFromStore( if (job->error() && job->error() != QKeychain::Error::EntryNotFound) { nhlog::db()->error("Restoring secret '{}' failed ({}): {}", name.toStdString(), - job->error(), + static_cast(job->error()), job->errorString().toStdString()); fatalSecretError(); @@ -2140,8 +2135,16 @@ Cache::saveInvite(lmdb::txn &txn, auto display_name = msg->content.display_name.empty() ? msg->state_key : msg->content.display_name; - MemberInfo tmp{ - display_name, msg->content.avatar_url, msg->content.reason, msg->content.is_direct}; + std::string inviter = ""; + if (msg->content.membership == mtx::events::state::Membership::Invite) { + inviter = msg->sender; + } + + MemberInfo tmp{display_name, + msg->content.avatar_url, + inviter, + msg->content.reason, + msg->content.is_direct}; membersdb.put(txn, msg->state_key, nlohmann::json(tmp).dump()); } else { @@ -4436,7 +4439,9 @@ Cache::displayName(const QString &room_id, const QString &user_id) static bool isDisplaynameSafe(const std::string &s) { - for (QChar c : QString::fromStdString(s).toStdU32String()) { + const auto str = QString::fromStdString(s); + + for (QChar c : str) { if (c.isPrint() && !c.isSpace()) return false; } @@ -5183,6 +5188,8 @@ to_json(nlohmann::json &j, const MemberInfo &info) { j["name"] = info.name; j["avatar_url"] = info.avatar_url; + if (!info.inviter.empty()) + j["inviter"] = info.inviter; if (info.is_direct) j["is_direct"] = info.is_direct; if (!info.reason.empty()) @@ -5196,6 +5203,7 @@ from_json(const nlohmann::json &j, MemberInfo &info) info.avatar_url = j.value("avatar_url", ""); info.is_direct = j.value("is_direct", false); info.reason = j.value("reason", ""); + info.inviter = j.value("inviter", ""); } void @@ -5300,13 +5308,6 @@ namespace cache { void init(const QString &user_id) { - qRegisterMetaType(); - qRegisterMetaType(); - qRegisterMetaType>(); - qRegisterMetaType>(); - qRegisterMetaType>(); - qRegisterMetaType(); - instance_ = std::make_unique(user_id); } diff --git a/src/CacheCryptoStructs.h b/src/CacheCryptoStructs.h index 96fc35ec..2a5b895f 100644 --- a/src/CacheCryptoStructs.h +++ b/src/CacheCryptoStructs.h @@ -5,6 +5,7 @@ #pragma once #include +#include #include #include @@ -16,6 +17,8 @@ namespace crypto { Q_NAMESPACE +QML_NAMED_ELEMENT(Crypto) + //! How much a participant is trusted. enum Trust { diff --git a/src/CacheStructs.h b/src/CacheStructs.h index 6dad4b19..6e2f800a 100644 --- a/src/CacheStructs.h +++ b/src/CacheStructs.h @@ -117,8 +117,9 @@ struct MemberInfo { std::string name; std::string avatar_url; - std::string reason = ""; - bool is_direct = false; + std::string inviter = ""; + std::string reason = ""; + bool is_direct = false; }; void diff --git a/src/Cache_p.h b/src/Cache_p.h index f8716e81..121e7e66 100644 --- a/src/Cache_p.h +++ b/src/Cache_p.h @@ -394,8 +394,19 @@ private: auto display_name = e->content.display_name.empty() ? e->state_key : e->content.display_name; + std::string inviter = ""; + if (e->content.membership == mtx::events::state::Membership::Invite) { + inviter = e->sender; + } + // Lightweight representation of a member. - MemberInfo tmp{display_name, e->content.avatar_url, e->content.reason}; + MemberInfo tmp{ + display_name, + e->content.avatar_url, + inviter, + e->content.reason, + e->content.is_direct, + }; membersdb.put(txn, e->state_key, nlohmann::json(tmp).dump()); break; diff --git a/src/ChatPage.cpp b/src/ChatPage.cpp index 8feabfd0..4686b0f5 100644 --- a/src/ChatPage.cpp +++ b/src/ChatPage.cpp @@ -36,12 +36,6 @@ static constexpr int CHECK_CONNECTIVITY_INTERVAL = 15'000; static constexpr int RETRY_TIMEOUT = 5'000; static constexpr size_t MAX_ONETIME_KEYS = 50; -Q_DECLARE_METATYPE(std::optional) -Q_DECLARE_METATYPE(std::optional) -Q_DECLARE_METATYPE(mtx::presence::PresenceState) -Q_DECLARE_METATYPE(mtx::secret_storage::AesHmacSha2KeyDescription) -Q_DECLARE_METATYPE(SecretsToDecrypt) - ChatPage::ChatPage(QSharedPointer userSettings, QObject *parent) : QObject(parent) , isConnected_(true) @@ -53,12 +47,6 @@ ChatPage::ChatPage(QSharedPointer userSettings, QObject *parent) instance_ = this; - qRegisterMetaType>(); - qRegisterMetaType>(); - qRegisterMetaType(); - qRegisterMetaType(); - qRegisterMetaType(); - view_manager_ = new TimelineViewManager(callManager_, this); connect(this, @@ -99,6 +87,7 @@ ChatPage::ChatPage(QSharedPointer userSettings, QObject *parent) if (lastSpacesUpdate < QDateTime::currentDateTime().addSecs(-20 * 60)) { lastSpacesUpdate = QDateTime::currentDateTime(); utils::updateSpaceVias(); + utils::removeExpiredEvents(); } if (!isConnected_) diff --git a/src/Clipboard.h b/src/Clipboard.h index bad9fd10..8bf89c22 100644 --- a/src/Clipboard.h +++ b/src/Clipboard.h @@ -5,11 +5,15 @@ #pragma once #include +#include #include class Clipboard final : public QObject { Q_OBJECT + QML_ELEMENT + QML_SINGLETON + Q_PROPERTY(QString text READ text WRITE setText NOTIFY textChanged) public: diff --git a/src/CompletionProxyModel.cpp b/src/CompletionProxyModel.cpp index 638b5fb4..eea1e6aa 100644 --- a/src/CompletionProxyModel.cpp +++ b/src/CompletionProxyModel.cpp @@ -29,7 +29,7 @@ CompletionProxyModel::CompletionProxyModel(QAbstractItemModel *model, finder.toNextBoundary(); auto end = finder.position(); - auto ref = str.midRef(start, end - start).trimmed(); + auto ref = QStringView(str).mid(start, end - start).trimmed(); if (!ref.isEmpty()) trie_.insert(ref.toUcs4(), id); } while (finder.position() < str.size()); diff --git a/src/GridImagePackModel.cpp b/src/GridImagePackModel.cpp index bd39405e..cfd014f0 100644 --- a/src/GridImagePackModel.cpp +++ b/src/GridImagePackModel.cpp @@ -13,11 +13,6 @@ #include "Cache_p.h" #include "emoji/Provider.h" -Q_DECLARE_METATYPE(StickerImage) -Q_DECLARE_METATYPE(TextEmoji) -Q_DECLARE_METATYPE(SectionDescription) -Q_DECLARE_METATYPE(QList) - QString emoji::categoryToName(emoji::Emoji::Category cat) { @@ -73,11 +68,6 @@ GridImagePackModel::GridImagePackModel(const std::string &roomId, bool stickers, , room_id(roomId) , columns(stickers ? 3 : 7) { - [[maybe_unused]] static auto id = qRegisterMetaType(); - [[maybe_unused]] static auto id2 = qRegisterMetaType(); - [[maybe_unused]] static auto id3 = qRegisterMetaType(); - [[maybe_unused]] static auto id4 = qRegisterMetaType>(); - if (!stickers) { for (const auto &category : { emoji::Emoji::Category::People, @@ -118,6 +108,8 @@ GridImagePackModel::GridImagePackModel(const std::string &roomId, bool stickers, PackDesc newPack{}; newPack.packname = pack.pack.pack ? QString::fromStdString(pack.pack.pack->display_name) : QString(); + newPack.packavatar = + pack.pack.pack ? QString::fromStdString(pack.pack.pack->avatar_url) : QString(); newPack.room_id = pack.source_room; newPack.state_key = pack.state_key; @@ -144,7 +136,7 @@ GridImagePackModel::GridImagePackModel(const std::string &roomId, bool stickers, finder.toNextBoundary(); auto end = finder.position(); - auto ref = str.midRef(start, end - start).trimmed(); + auto ref = QStringView(str).mid(start, end - start).trimmed(); if (!ref.isEmpty()) trie_.insert(ref.toUcs4(), id); } while (finder.position() < str.size()); diff --git a/src/ImagePackListModel.h b/src/ImagePackListModel.h index 435c3902..f2c718b4 100644 --- a/src/ImagePackListModel.h +++ b/src/ImagePackListModel.h @@ -8,10 +8,14 @@ #include #include -class SingleImagePackModel; -class ImagePackListModel final : public QAbstractListModel +#include "SingleImagePackModel.h" + +class ImagePackListModel : public QAbstractListModel { Q_OBJECT + QML_ELEMENT + QML_UNCREATABLE("") + Q_PROPERTY(bool containsAccountPack READ containsAccountPack CONSTANT) public: enum Roles diff --git a/src/InviteesModel.h b/src/InviteesModel.h index ab8fbdb4..b9b4b862 100644 --- a/src/InviteesModel.h +++ b/src/InviteesModel.h @@ -6,9 +6,10 @@ #define INVITEESMODEL_H #include +#include #include -class TimelineModel; +#include "timeline/TimelineModel.h" class Invitee final : public QObject { @@ -34,6 +35,8 @@ private: class InviteesModel final : public QAbstractListModel { Q_OBJECT + QML_ELEMENT + QML_UNCREATABLE("") Q_PROPERTY(int count READ rowCount NOTIFY countChanged) Q_PROPERTY(TimelineModel *room READ room CONSTANT) diff --git a/src/JdenticonProvider.h b/src/JdenticonProvider.h index 5a475ded..da4d73e1 100644 --- a/src/JdenticonProvider.h +++ b/src/JdenticonProvider.h @@ -52,13 +52,7 @@ public: QImage m_pixmap; }; -class JdenticonProvider - : -#if QT_VERSION < 0x60000 - public QObject - , -#endif - public QQuickAsyncImageProvider +class JdenticonProvider : public QQuickAsyncImageProvider { Q_OBJECT @@ -77,7 +71,7 @@ public slots: if (queryStart != -1) { id_ = id.left(queryStart); auto query = id.mid(queryStart + 1); - auto queryBits = query.splitRef('&'); + auto queryBits = QStringView(query).split('&'); for (const auto &b : queryBits) { if (b.startsWith(QStringView(u"radius="))) { diff --git a/src/LoginPage.cpp b/src/LoginPage.cpp index 95b46d04..55487502 100644 --- a/src/LoginPage.cpp +++ b/src/LoginPage.cpp @@ -19,18 +19,13 @@ #include "SSOHandler.h" #include "UserSettingsPage.h" -Q_DECLARE_METATYPE(LoginPage::LoginMethod) -Q_DECLARE_METATYPE(SSOProvider) - using namespace mtx::identifiers; LoginPage::LoginPage(QObject *parent) : QObject(parent) , inferredServerAddress_() { - [[maybe_unused]] static auto ignored = - qRegisterMetaType("LoginPage::LoginMethod"); - [[maybe_unused]] static auto ignored2 = qRegisterMetaType(); + [[maybe_unused]] static auto ignored = qRegisterMetaType(); connect(this, &LoginPage::versionOkCb, this, &LoginPage::versionOk, Qt::QueuedConnection); connect(this, &LoginPage::versionErrorCb, this, &LoginPage::versionError, Qt::QueuedConnection); diff --git a/src/LoginPage.h b/src/LoginPage.h index f20ba0c6..90515263 100644 --- a/src/LoginPage.h +++ b/src/LoginPage.h @@ -5,13 +5,10 @@ #pragma once #include +#include #include -namespace mtx { -namespace responses { -struct Login; -} -} +#include struct SSOProvider { @@ -33,6 +30,7 @@ public: class LoginPage : public QObject { Q_OBJECT + QML_NAMED_ELEMENT(Login) Q_PROPERTY(QString mxid READ mxid WRITE setMxid NOTIFY matrixIdChanged) Q_PROPERTY(QString homeserver READ homeserver WRITE setHomeserver NOTIFY homeserverChanged) diff --git a/src/MainWindow.cpp b/src/MainWindow.cpp index a8cc66b7..d06171de 100644 --- a/src/MainWindow.cpp +++ b/src/MainWindow.cpp @@ -5,6 +5,7 @@ #include #include +#include #include #include @@ -24,21 +25,15 @@ #include "InviteesModel.h" #include "JdenticonProvider.h" #include "Logging.h" -#include "LoginPage.h" #include "MainWindow.h" #include "MatrixClient.h" #include "MemberList.h" #include "MxcImageProvider.h" #include "PowerlevelsEditModels.h" -#include "ReadReceiptsModel.h" -#include "RegisterPage.h" -#include "RoomDirectoryModel.h" -#include "RoomsModel.h" #include "SingleImagePackModel.h" #include "TrayIcon.h" #include "UserDirectoryModel.h" #include "UserSettingsPage.h" -#include "UsersModel.h" #include "Utils.h" #include "dock/Dock.h" #include "emoji/Provider.h" @@ -47,12 +42,6 @@ #include "timeline/DelegateChooser.h" #include "timeline/TimelineFilter.h" #include "timeline/TimelineViewManager.h" -#include "ui/HiddenEvents.h" -#include "ui/MxcAnimatedImage.h" -#include "ui/MxcMediaProxy.h" -#include "ui/NhekoCursorShape.h" -#include "ui/NhekoDropArea.h" -#include "ui/NhekoEventObserver.h" #include "ui/NhekoGlobalObject.h" #include "ui/RoomSummary.h" #include "ui/UIA.h" @@ -63,13 +52,6 @@ #include "dbus/NhekoDBusApi.h" #endif -Q_DECLARE_METATYPE(mtx::events::collections::TimelineEvents) -Q_DECLARE_METATYPE(std::vector) -Q_DECLARE_METATYPE(std::vector) -Q_DECLARE_METATYPE(mtx::responses::PublicRoom) -Q_DECLARE_METATYPE(mtx::responses::Profile) -Q_DECLARE_METATYPE(mtx::responses::User) - MainWindow *MainWindow::instance_ = nullptr; MainWindow::MainWindow(QWindow *parent) @@ -89,7 +71,7 @@ MainWindow::MainWindow(QWindow *parent) registerQmlTypes(); setColor(Theme::paletteFromTheme(userSettings_->theme()).window().color()); - setSource(QUrl(QStringLiteral("qrc:///qml/Root.qml"))); + setSource(QUrl(QStringLiteral("qrc:///resources/qml/Root.qml"))); trayIcon_ = new TrayIcon(QStringLiteral(":/logos/nheko.svg"), this); @@ -138,180 +120,57 @@ MainWindow::MainWindow(QWindow *parent) void MainWindow::registerQmlTypes() { - qRegisterMetaType(); - qRegisterMetaType(); - qRegisterMetaType(); - qRegisterMetaType(); - qRegisterMetaType(); - qRegisterMetaType(); - qRegisterMetaType(); - qRegisterMetaType(); - qRegisterMetaType(); - qRegisterMetaType(); - qRegisterMetaType(); - qRegisterMetaType(); - qRegisterMetaType(); - qRegisterMetaType(); - qRegisterMetaType(); - qRegisterMetaType>(); + // qmlRegisterUncreatableType( + // "im.nheko", + // 1, + // 0, + // "DeviceVerificationFlow", + // QStringLiteral("Can't create verification flow from QML!")); + // qmlRegisterUncreatableType( + // "im.nheko", + // 1, + // 0, + // "UserProfileModel", + // QStringLiteral("UserProfile needs to be instantiated on the C++ side")); + // qmlRegisterUncreatableType( + // "im.nheko", + // 1, + // 0, + // "MemberList", + // QStringLiteral("MemberList needs to be instantiated on the C++ side")); + // qmlRegisterUncreatableType( + // "im.nheko", + // 1, + // 0, + // "RoomSettingsModel", + // QStringLiteral("Room Settings needs to be instantiated on the C++ side")); + // qmlRegisterUncreatableType( + // "im.nheko", 1, 0, "Room", QStringLiteral("Room needs to be instantiated on the C++ side")); + // qmlRegisterUncreatableType( + // "im.nheko", + // 1, + // 0, + // "ImagePackListModel", + // QStringLiteral("ImagePackListModel needs to be instantiated on the C++ side")); + // qmlRegisterUncreatableType( + // "im.nheko", + // 1, + // 0, + // "SingleImagePackModel", + // QStringLiteral("SingleImagePackModel needs to be instantiated on the C++ side")); + // qmlRegisterUncreatableType( + // "im.nheko", + // 1, + // 0, + // "InviteesModel", + // QStringLiteral("InviteesModel needs to be instantiated on the C++ side")); - qRegisterMetaType>(); - qRegisterMetaType>(); - - qRegisterMetaType(); - qmlRegisterUncreatableMetaObject(qml_mtx_events::staticMetaObject, - "im.nheko", - 1, - 0, - "MtxEvent", - QStringLiteral("Can't instantiate enum!")); - qmlRegisterUncreatableMetaObject( - olm::staticMetaObject, "im.nheko", 1, 0, "Olm", QStringLiteral("Can't instantiate enum!")); - qmlRegisterUncreatableMetaObject(crypto::staticMetaObject, - "im.nheko", - 1, - 0, - "Crypto", - QStringLiteral("Can't instantiate enum!")); - qmlRegisterUncreatableMetaObject(verification::staticMetaObject, - "im.nheko", - 1, - 0, - "VerificationStatus", - QStringLiteral("Can't instantiate enum!")); - - qmlRegisterType("im.nheko", 1, 0, "DelegateChoice"); - qmlRegisterType("im.nheko", 1, 0, "DelegateChooser"); - qmlRegisterType("im.nheko", 1, 0, "NhekoDropArea"); - qmlRegisterType("im.nheko", 1, 0, "CursorShape"); - qmlRegisterType("im.nheko", 1, 0, "EventObserver"); - qmlRegisterType("im.nheko", 1, 0, "MxcAnimatedImage"); - qmlRegisterType("im.nheko", 1, 0, "MxcMedia"); - qmlRegisterType("im.nheko", 1, 0, "RoomDirectoryModel"); - qmlRegisterType("im.nheko", 1, 0, "UserDirectoryModel"); - qmlRegisterType("im.nheko", 1, 0, "Login"); - qmlRegisterType("im.nheko", 1, 0, "Registration"); - qmlRegisterType("im.nheko", 1, 0, "HiddenEvents"); - qmlRegisterType("im.nheko", 1, 0, "TimelineFilter"); - qmlRegisterUncreatableType( - "im.nheko", - 1, - 0, - "RoomSummary", - QStringLiteral("Please use joinRoom to create a room summary.")); - qmlRegisterUncreatableType( - "im.nheko", - 1, - 0, - "AliasEditingModel", - QStringLiteral("Please use editAliases to create the models")); - - qmlRegisterUncreatableType( - "im.nheko", - 1, - 0, - "PowerlevelEditingModels", - QStringLiteral("Please use editPowerlevels to create the models")); - qmlRegisterUncreatableType( - "im.nheko", - 1, - 0, - "DeviceVerificationFlow", - QStringLiteral("Can't create verification flow from QML!")); - qmlRegisterUncreatableType( - "im.nheko", - 1, - 0, - "UserProfileModel", - QStringLiteral("UserProfile needs to be instantiated on the C++ side")); - qmlRegisterUncreatableType( - "im.nheko", - 1, - 0, - "MemberList", - QStringLiteral("MemberList needs to be instantiated on the C++ side")); - qmlRegisterUncreatableType( - "im.nheko", - 1, - 0, - "RoomSettingsModel", - QStringLiteral("Room Settings needs to be instantiated on the C++ side")); - qmlRegisterUncreatableType( - "im.nheko", 1, 0, "Room", QStringLiteral("Room needs to be instantiated on the C++ side")); - qmlRegisterUncreatableType( - "im.nheko", - 1, - 0, - "ImagePackListModel", - QStringLiteral("ImagePackListModel needs to be instantiated on the C++ side")); - qmlRegisterUncreatableType( - "im.nheko", - 1, - 0, - "SingleImagePackModel", - QStringLiteral("SingleImagePackModel needs to be instantiated on the C++ side")); - qmlRegisterUncreatableType( - "im.nheko", - 1, - 0, - "InviteesModel", - QStringLiteral("InviteesModel needs to be instantiated on the C++ side")); - qmlRegisterUncreatableType( - "im.nheko", - 1, - 0, - "ReadReceiptsProxy", - QStringLiteral("ReadReceiptsProxy needs to be instantiated on the C++ side")); - - qmlRegisterSingletonType( - "im.nheko", 1, 0, "Clipboard", [](QQmlEngine *, QJSEngine *) -> QObject * { - return new Clipboard(); - }); - qmlRegisterSingletonType( - "im.nheko", 1, 0, "Nheko", [](QQmlEngine *, QJSEngine *) -> QObject * { - return new Nheko(); - }); - qmlRegisterSingletonType( - "im.nheko", 1, 0, "UserSettingsModel", [](QQmlEngine *, QJSEngine *) -> QObject * { - return new UserSettingsModel(); - }); - - qmlRegisterSingletonInstance("im.nheko", 1, 0, "Settings", userSettings_.data()); - - qRegisterMetaType(); - qRegisterMetaType>(); - - qmlRegisterUncreatableType( - "im.nheko", - 1, - 0, - "FilteredCommunitiesModel", - QStringLiteral("Use Communities.filtered() to create a FilteredCommunitiesModel")); - - qmlRegisterUncreatableType( - "im.nheko", 1, 0, "MediaUpload", QStringLiteral("MediaUploads can not be created in Qml")); - qmlRegisterUncreatableMetaObject(emoji::staticMetaObject, - "im.nheko.EmojiModel", - 1, - 0, - "EmojiCategory", - QStringLiteral("Error: Only enums")); - - qmlRegisterType("im.nheko", 1, 0, "RoomDirectoryModel"); - - qmlRegisterSingletonType( - "im.nheko", 1, 0, "SelfVerificationStatus", [](QQmlEngine *, QJSEngine *) -> QObject * { - auto ptr = new SelfVerificationStatus(); - QObject::connect(ChatPage::instance(), - &ChatPage::initializeEmptyViews, - ptr, - &SelfVerificationStatus::invalidate); - return ptr; - }); - qmlRegisterSingletonInstance("im.nheko", 1, 0, "MainWindow", this); - qmlRegisterSingletonInstance("im.nheko", 1, 0, "UIA", UIA::instance()); - qmlRegisterSingletonInstance( - "im.nheko", 1, 0, "CallManager", ChatPage::instance()->callManager()); + // qmlRegisterUncreatableMetaObject(emoji::staticMetaObject, + // "im.nheko.EmojiModel", + // 1, + // 0, + // "EmojiCategory", + // QStringLiteral("Error: Only enums")); imgProvider = new MxcImageProvider(); engine()->addImageProvider(QStringLiteral("MxcImage"), imgProvider); @@ -348,18 +207,6 @@ MainWindow::setWindowTitle(int notificationCount) QQuickView::setTitle(name); } -bool -MainWindow::event(QEvent *event) -{ - auto type = event->type(); - - if (type == QEvent::Close) { - closeEvent(static_cast(event)); - } - - return QQuickView::event(event); -} - // HACK: https://bugreports.qt.io/browse/QTBUG-83972, qtwayland cannot auto hide menu void MainWindow::mousePressEvent(QMouseEvent *event) @@ -418,6 +265,20 @@ MainWindow::showChatPage() emit switchToChatPage(); } +bool +NhekoFixupPaletteEventFilter::eventFilter(QObject *obj, QEvent *event) +{ + // Workaround for the QGuiApplication palette not being applied to toplevel windows for some + // reason?!? + if (event->type() == QEvent::ChildAdded && + obj->metaObject()->className() == QStringLiteral("QQuickRootItem")) { + for (const auto window : QGuiApplication::topLevelWindows()) { + QGuiApplication::postEvent(window, new QEvent(QEvent::ApplicationPaletteChange)); + } + } + return false; +} + void MainWindow::closeEvent(QCloseEvent *event) { @@ -433,6 +294,7 @@ MainWindow::closeEvent(QCloseEvent *event) if (!qApp->isSavingSession() && isVisible() && pageSupportsTray() && userSettings_->tray()) { event->ignore(); hide(); + return; } } diff --git a/src/MainWindow.h b/src/MainWindow.h index 08ffcbba..20e81efc 100644 --- a/src/MainWindow.h +++ b/src/MainWindow.h @@ -37,14 +37,48 @@ class MemberList; class ReCaptcha; } -class MainWindow final : public QQuickView +class NhekoFixupPaletteEventFilter final : public QObject { Q_OBJECT public: - explicit MainWindow(QWindow *parent = nullptr); + NhekoFixupPaletteEventFilter(QObject *parent) + : QObject(parent) + { + } + + bool eventFilter(QObject *obj, QEvent *event) override; +}; + +class MainWindow : public QQuickView +{ + Q_OBJECT + QML_ELEMENT + QML_SINGLETON + +public: + explicit MainWindow(QWindow *parent); static MainWindow *instance() { return instance_; } + static MainWindow *create(QQmlEngine *qmlEngine, QJSEngine *) + { + // The instance has to exist before it is used. We cannot replace it. + Q_ASSERT(instance_); + + // The engine has to have the same thread affinity as the singleton. + Q_ASSERT(qmlEngine->thread() == instance_->thread()); + + // There can only be one engine accessing the singleton. + static QJSEngine *s_engine = nullptr; + if (s_engine) + Q_ASSERT(qmlEngine == s_engine); + else + s_engine = qmlEngine; + + QJSEngine::setObjectOwnership(instance_, QJSEngine::CppOwnership); + return instance_; + } + void saveCurrentWindowSize(); void openJoinRoomDialog(std::function callback); @@ -64,8 +98,7 @@ public: QString focusedRoom() const; protected: - void closeEvent(QCloseEvent *event); - bool event(QEvent *event) override; + void closeEvent(QCloseEvent *event) override; // HACK: https://bugreports.qt.io/browse/QTBUG-83972, qtwayland cannot auto hide menu void mousePressEvent(QMouseEvent *) override; diff --git a/src/MatrixClient.cpp b/src/MatrixClient.cpp index 1452257a..2fd2eac9 100644 --- a/src/MatrixClient.cpp +++ b/src/MatrixClient.cpp @@ -15,19 +15,6 @@ #include "nlohmann/json.hpp" #include -Q_DECLARE_METATYPE(mtx::responses::Login) -Q_DECLARE_METATYPE(mtx::responses::Messages) -Q_DECLARE_METATYPE(mtx::responses::Notifications) -Q_DECLARE_METATYPE(mtx::responses::Rooms) -Q_DECLARE_METATYPE(mtx::responses::Sync) -Q_DECLARE_METATYPE(mtx::responses::StateEvents) - -// Q_DECLARE_METATYPE(nlohmann::json) -Q_DECLARE_METATYPE(std::string) -Q_DECLARE_METATYPE(std::vector) -Q_DECLARE_METATYPE(std::vector) -Q_DECLARE_METATYPE(std::set) - namespace http { mtx::http::Client * @@ -52,18 +39,6 @@ is_logged_in() void init() { - qRegisterMetaType(); - qRegisterMetaType(); - qRegisterMetaType(); - qRegisterMetaType(); - qRegisterMetaType(); - qRegisterMetaType(); - qRegisterMetaType(); - // qRegisterMetaType(); - qRegisterMetaType>(); - qRegisterMetaType>(); - qRegisterMetaType>("std::map"); - qRegisterMetaType>(); } } // namespace http diff --git a/src/MemberList.h b/src/MemberList.h index ceaa9c14..f1d39336 100644 --- a/src/MemberList.h +++ b/src/MemberList.h @@ -5,6 +5,7 @@ #pragma once #include +#include #include #include @@ -79,6 +80,8 @@ private: class MemberList final : public QSortFilterProxyModel { Q_OBJECT + QML_ELEMENT + QML_UNCREATABLE("") Q_PROPERTY(QString roomName READ roomName NOTIFY roomNameChanged) Q_PROPERTY(int memberCount READ memberCount NOTIFY memberCountChanged) diff --git a/src/MxcImageProvider.cpp b/src/MxcImageProvider.cpp index cb21fa0d..47e0344f 100644 --- a/src/MxcImageProvider.cpp +++ b/src/MxcImageProvider.cpp @@ -24,12 +24,8 @@ QHash infos; -MxcImageProvider::MxcImageProvider(QObject *parent) -#if QT_VERSION < 0x60000 - : QObject(parent) -#else - : QQuickAsyncImageProvider(parent) -#endif +MxcImageProvider::MxcImageProvider() + : QQuickAsyncImageProvider() { auto timer = new QTimer(this); timer->setInterval(std::chrono::hours(1)); diff --git a/src/MxcImageProvider.h b/src/MxcImageProvider.h index 8096a7dc..5c3e5c58 100644 --- a/src/MxcImageProvider.h +++ b/src/MxcImageProvider.h @@ -70,18 +70,12 @@ public: QImage m_image; }; -class MxcImageProvider - : -#if QT_VERSION < 0x60000 - public QObject - , -#endif - public QQuickAsyncImageProvider +class MxcImageProvider : public QQuickAsyncImageProvider { Q_OBJECT public: - MxcImageProvider(QObject *parent = nullptr); + MxcImageProvider(); public slots: QQuickImageResponse * diff --git a/src/PowerlevelsEditModels.h b/src/PowerlevelsEditModels.h index fe9735d3..c9d262d8 100644 --- a/src/PowerlevelsEditModels.h +++ b/src/PowerlevelsEditModels.h @@ -5,6 +5,7 @@ #pragma once #include +#include #include #include @@ -196,6 +197,9 @@ class PowerlevelEditingModels final : public QObject { Q_OBJECT + QML_ELEMENT + QML_UNCREATABLE("Please use editPowerlevels to create the models") + Q_PROPERTY(PowerlevelsUserListModel *users READ users CONSTANT) Q_PROPERTY(PowerlevelsTypeListModel *types READ types CONSTANT) Q_PROPERTY(PowerlevelsSpacesListModel *spaces READ spaces CONSTANT) diff --git a/src/ReadReceiptsModel.h b/src/ReadReceiptsModel.h index b870061a..56f67509 100644 --- a/src/ReadReceiptsModel.h +++ b/src/ReadReceiptsModel.h @@ -8,6 +8,7 @@ #include #include #include +#include #include #include @@ -54,6 +55,9 @@ class ReadReceiptsProxy final : public QSortFilterProxyModel { Q_OBJECT + QML_ELEMENT + QML_UNCREATABLE("") + Q_PROPERTY(QString eventId READ eventId CONSTANT) Q_PROPERTY(QString roomId READ roomId CONSTANT) diff --git a/src/RegisterPage.h b/src/RegisterPage.h index 7c58b40c..e26acaba 100644 --- a/src/RegisterPage.h +++ b/src/RegisterPage.h @@ -5,6 +5,7 @@ #pragma once #include +#include #include #include @@ -13,6 +14,7 @@ class RegisterPage : public QObject { Q_OBJECT + QML_NAMED_ELEMENT(Registration) Q_PROPERTY(QString error READ error NOTIFY errorChanged) Q_PROPERTY(QString hsError READ hsError NOTIFY hsErrorChanged) diff --git a/src/RoomDirectoryModel.h b/src/RoomDirectoryModel.h index 8a367e2e..a5103112 100644 --- a/src/RoomDirectoryModel.h +++ b/src/RoomDirectoryModel.h @@ -5,6 +5,7 @@ #pragma once #include +#include #include #include #include @@ -32,6 +33,7 @@ signals: class RoomDirectoryModel : public QAbstractListModel { Q_OBJECT + QML_ELEMENT Q_PROPERTY(bool loadingMoreRooms READ loadingMoreRooms NOTIFY loadingMoreRoomsChanged) Q_PROPERTY( diff --git a/src/SingleImagePackModel.cpp b/src/SingleImagePackModel.cpp index 99338f2e..686184da 100644 --- a/src/SingleImagePackModel.cpp +++ b/src/SingleImagePackModel.cpp @@ -20,8 +20,6 @@ #include "timeline/Permissions.h" #include "timeline/TimelineModel.h" -Q_DECLARE_METATYPE(mtx::common::ImageInfo) - SingleImagePackModel::SingleImagePackModel(ImagePackInfo pack_, QObject *parent) : QAbstractListModel(parent) , roomid_(std::move(pack_.source_room)) @@ -30,8 +28,6 @@ SingleImagePackModel::SingleImagePackModel(ImagePackInfo pack_, QObject *parent) , pack(std::move(pack_.pack)) , fromSpace_(pack_.from_space) { - [[maybe_unused]] static auto imageInfoType = qRegisterMetaType(); - if (!pack.pack) pack.pack = mtx::events::msc2545::ImagePack::PackDescription{}; diff --git a/src/SingleImagePackModel.h b/src/SingleImagePackModel.h index 595f5a78..e1ab98fb 100644 --- a/src/SingleImagePackModel.h +++ b/src/SingleImagePackModel.h @@ -6,6 +6,7 @@ #include #include +#include #include #include @@ -15,6 +16,8 @@ class SingleImagePackModel final : public QAbstractListModel { Q_OBJECT + QML_ELEMENT + QML_UNCREATABLE("") Q_PROPERTY(QString roomid READ roomid CONSTANT) Q_PROPERTY(bool fromSpace READ fromSpace CONSTANT) diff --git a/src/TrayIcon.cpp b/src/TrayIcon.cpp index 5d500553..8eaf522e 100644 --- a/src/TrayIcon.cpp +++ b/src/TrayIcon.cpp @@ -12,10 +12,6 @@ #include "TrayIcon.h" -#if defined(Q_OS_MAC) -#include -#endif - MsgCountComposedIcon::MsgCountComposedIcon(const QString &filename) : QIconEngine() , icon_{QIcon{filename}} @@ -125,30 +121,5 @@ TrayIcon::TrayIcon(const QString &filename, QWindow *parent) void TrayIcon::setUnreadCount(int count) { -// Use the native badge counter in MacOS. -#if defined(Q_OS_MAC) -// 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); - - if (labelText == QtMac::badgeLabelText()) - return; - - 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; - - // Custom drawing on Linux. - MsgCountComposedIcon *tmp = static_cast(icon_->clone()); - tmp->msgCount = count; - - setIcon(QIcon(tmp)); - - icon_ = tmp; -#endif + qGuiApp->setBadgeNumber(count); } diff --git a/src/UserDirectoryModel.h b/src/UserDirectoryModel.h index f0416ecf..ffa9ae93 100644 --- a/src/UserDirectoryModel.h +++ b/src/UserDirectoryModel.h @@ -5,6 +5,7 @@ #pragma once #include +#include #include #include #include @@ -26,6 +27,7 @@ signals: class UserDirectoryModel : public QAbstractListModel { Q_OBJECT + QML_ELEMENT Q_PROPERTY(bool searchingUsers READ searchingUsers NOTIFY searchingUsersChanged) diff --git a/src/UserSettingsPage.cpp b/src/UserSettingsPage.cpp index d5b7289c..7c30f877 100644 --- a/src/UserSettingsPage.cpp +++ b/src/UserSettingsPage.cpp @@ -101,6 +101,8 @@ UserSettings::load(std::optional profile) exposeDBusApi_ = settings.value(QStringLiteral("user/expose_dbus_api"), false).toBool(); updateSpaceVias_ = settings.value(QStringLiteral("user/space_background_maintenance"), true).toBool(); + expireEvents_ = + settings.value(QStringLiteral("user/expired_events_background_maintenance"), false).toBool(); mobileMode_ = settings.value(QStringLiteral("user/mobile_mode"), false).toBool(); emojiFont_ = settings.value(QStringLiteral("user/emoji_font_family"), "emoji").toString(); @@ -308,6 +310,17 @@ UserSettings::setUpdateSpaceVias(bool state) save(); } +void +UserSettings::setExpireEvents(bool state) +{ + if (expireEvents_ == state) + return; + + expireEvents_ = state; + emit expireEventsChanged(state); + save(); +} + void UserSettings::setMarkdown(bool state) { @@ -852,21 +865,8 @@ UserSettings::setOpenVideoExternal(bool state) void UserSettings::applyTheme() { - QFile stylefile; - - if (this->theme() == QLatin1String("light")) { - stylefile.setFileName(QStringLiteral(":/styles/styles/nheko.qss")); - } else if (this->theme() == QLatin1String("dark")) { - stylefile.setFileName(QStringLiteral(":/styles/styles/nheko-dark.qss")); - } else { - stylefile.setFileName(QStringLiteral(":/styles/styles/system.qss")); - } + QGuiApplication::setPalette(Theme::paletteFromTheme(this->theme())); QApplication::setPalette(Theme::paletteFromTheme(this->theme())); - - stylefile.open(QFile::ReadOnly); - QString stylesheet = QString(stylefile.readAll()); - - qobject_cast(QApplication::instance())->setStyleSheet(stylesheet); } void @@ -937,6 +937,7 @@ UserSettings::save() settings.setValue(QStringLiteral("open_video_external"), openVideoExternal_); settings.setValue(QStringLiteral("expose_dbus_api"), exposeDBusApi_); settings.setValue(QStringLiteral("space_background_maintenance"), updateSpaceVias_); + settings.setValue(QStringLiteral("expired_events_background_maintenance"), expireEvents_); settings.endGroup(); // user @@ -1142,6 +1143,8 @@ UserSettingsModel::data(const QModelIndex &index, int role) const return tr("Expose room information via D-Bus"); case UpdateSpaceVias: return tr("Periodically update community routing information"); + case ExpireEvents: + return tr("Periodically delete expired events"); } } else if (role == Value) { switch (index.row()) { @@ -1279,6 +1282,8 @@ UserSettingsModel::data(const QModelIndex &index, int role) const return i->exposeDBusApi(); case UpdateSpaceVias: return i->updateSpaceVias(); + case ExpireEvents: + return i->expireEvents(); } } else if (role == Description) { switch (index.row()) { @@ -1462,6 +1467,10 @@ UserSettingsModel::data(const QModelIndex &index, int role) const "information about what servers participate in a room to community members. Since " "the room participants can change over time, this needs to be updated from time to " "time. This setting enables a background job to do that automatically."); + case ExpireEvents: + return tr("Regularly redact expired events as specified in the event expiration " + "configuration. Since this is currently not executed server side, you need " + "to have one client running this regularly."); } } else if (role == Type) { switch (index.row()) { @@ -1512,6 +1521,7 @@ UserSettingsModel::data(const QModelIndex &index, int role) const case UseOnlineKeyBackup: case ExposeDBusApi: case UpdateSpaceVias: + case ExpireEvents: case SpaceNotifications: case FancyEffects: case ReducedMotion: @@ -1585,8 +1595,6 @@ UserSettingsModel::data(const QModelIndex &index, int role) const l.push_back(QString::fromStdString(d)); return l; }; - static QFontDatabase fontDb; - switch (index.row()) { case Theme: return QStringList{ @@ -1605,9 +1613,9 @@ UserSettingsModel::data(const QModelIndex &index, int role) const i->camera().toStdString(), i->cameraResolution().toStdString())); case Font: - return fontDb.families(); + return QFontDatabase::families(); case EmojiFont: - return fontDb.families(QFontDatabase::WritingSystem::Symbol); + return QFontDatabase::families(QFontDatabase::WritingSystem::Symbol); case Ringtone: { QStringList l{ QStringLiteral("Mute"), @@ -1651,8 +1659,6 @@ UserSettingsModel::data(const QModelIndex &index, int role) const bool UserSettingsModel::setData(const QModelIndex &index, const QVariant &value, int role) { - static QFontDatabase fontDb; - auto i = UserSettings::instance(); if (role == Value) { switch (index.row()) { @@ -1677,7 +1683,7 @@ UserSettingsModel::setData(const QModelIndex &index, const QVariant &value, int return false; } case ScaleFactor: { - if (value.canConvert(QMetaType::Double)) { + if (value.canConvert(QMetaType::fromType())) { utils::setScaleFactor(static_cast(value.toDouble())); return true; } else @@ -1782,7 +1788,7 @@ UserSettingsModel::setData(const QModelIndex &index, const QVariant &value, int return false; } case TimelineMaxWidth: { - if (value.canConvert(QMetaType::Int)) { + if (value.canConvert(QMetaType::fromType())) { i->setTimelineMaxWidth(value.toInt()); return true; } else @@ -1880,7 +1886,7 @@ UserSettingsModel::setData(const QModelIndex &index, const QVariant &value, int return false; } case PrivacyScreenTimeout: { - if (value.canConvert(QMetaType::Int)) { + if (value.canConvert(QMetaType::fromType())) { i->setPrivacyScreenTimeout(value.toInt()); return true; } else @@ -1894,7 +1900,7 @@ UserSettingsModel::setData(const QModelIndex &index, const QVariant &value, int return false; } case FontSize: { - if (value.canConvert(QMetaType::Double)) { + if (value.canConvert(QMetaType::fromType())) { i->setFontSize(value.toDouble()); return true; } else @@ -1902,7 +1908,7 @@ UserSettingsModel::setData(const QModelIndex &index, const QVariant &value, int } case Font: { if (value.userType() == QMetaType::Int) { - i->setFontFamily(fontDb.families().at(value.toInt())); + i->setFontFamily(QFontDatabase::families().at(value.toInt())); return true; } else return false; @@ -1910,7 +1916,7 @@ UserSettingsModel::setData(const QModelIndex &index, const QVariant &value, int case EmojiFont: { if (value.userType() == QMetaType::Int) { i->setEmojiFontFamily( - fontDb.families(QFontDatabase::WritingSystem::Symbol).at(value.toInt())); + QFontDatabase::families(QFontDatabase::WritingSystem::Symbol).at(value.toInt())); return true; } else return false; @@ -2011,6 +2017,13 @@ UserSettingsModel::setData(const QModelIndex &index, const QVariant &value, int } else return false; } + case ExpireEvents: { + if (value.userType() == QMetaType::Bool) { + i->setExpireEvents(value.toBool()); + return true; + } else + return false; + } } } return false; @@ -2266,4 +2279,7 @@ UserSettingsModel::UserSettingsModel(QObject *p) connect(s.get(), &UserSettings::updateSpaceViasChanged, this, [this] { emit dataChanged(index(UpdateSpaceVias), index(UpdateSpaceVias), {Value}); }); + connect(s.get(), &UserSettings::expireEventsChanged, this, [this] { + emit dataChanged(index(ExpireEvents), index(ExpireEvents), {Value}); + }); } diff --git a/src/UserSettingsPage.h b/src/UserSettingsPage.h index 657a362d..4e2691e5 100644 --- a/src/UserSettingsPage.h +++ b/src/UserSettingsPage.h @@ -6,6 +6,7 @@ #include #include +#include #include #include @@ -23,6 +24,8 @@ class QVBoxLayout; class UserSettings final : public QObject { Q_OBJECT + QML_NAMED_ELEMENT(Settings) + QML_SINGLETON Q_PROPERTY(QString theme READ theme WRITE setTheme NOTIFY themeChanged) Q_PROPERTY(bool messageHoverHighlight READ messageHoverHighlight WRITE setMessageHoverHighlight @@ -125,12 +128,31 @@ class UserSettings final : public QObject bool exposeDBusApi READ exposeDBusApi WRITE setExposeDBusApi NOTIFY exposeDBusApiChanged) Q_PROPERTY(bool updateSpaceVias READ updateSpaceVias WRITE setUpdateSpaceVias NOTIFY updateSpaceViasChanged) + Q_PROPERTY(bool expireEvents READ expireEvents WRITE setExpireEvents NOTIFY expireEventsChanged) UserSettings(); public: static QSharedPointer instance(); static void initialize(std::optional profile); + static UserSettings *create(QQmlEngine *qmlEngine, QJSEngine *) + { + // The instance has to exist before it is used. We cannot replace it. + Q_ASSERT(instance()); + + // The engine has to have the same thread affinity as the singleton. + Q_ASSERT(qmlEngine->thread() == instance()->thread()); + + // There can only be one engine accessing the singleton. + static QJSEngine *s_engine = nullptr; + if (s_engine) + Q_ASSERT(qmlEngine == s_engine); + else + s_engine = qmlEngine; + + QJSEngine::setObjectOwnership(instance().get(), QJSEngine::CppOwnership); + return instance().get(); + } QSettings *qsettings() { return &settings; } @@ -212,6 +234,7 @@ public: void setCollapsedSpaces(QList spaces); void setExposeDBusApi(bool state); void setUpdateSpaceVias(bool state); + void setExpireEvents(bool state); QString theme() const { return !theme_.isEmpty() ? theme_ : defaultTheme_; } bool messageHoverHighlight() const { return messageHoverHighlight_; } @@ -287,6 +310,7 @@ public: QList collapsedSpaces() const { return collapsedSpaces_; } bool exposeDBusApi() const { return exposeDBusApi_; } bool updateSpaceVias() const { return updateSpaceVias_; } + bool expireEvents() const { return expireEvents_; } signals: void groupViewStateChanged(bool state); @@ -351,6 +375,7 @@ signals: void recentReactionsChanged(); void exposeDBusApiChanged(bool state); void updateSpaceViasChanged(bool state); + void expireEventsChanged(bool state); private: // Default to system theme if QT_QPA_PLATFORMTHEME var is set. @@ -425,15 +450,18 @@ private: bool openVideoExternal_; bool exposeDBusApi_; bool updateSpaceVias_; + bool expireEvents_; QSettings settings; static QSharedPointer instance_; }; -class UserSettingsModel final : public QAbstractListModel +class UserSettingsModel : public QAbstractListModel { Q_OBJECT + QML_ELEMENT + QML_SINGLETON enum Indices { @@ -455,6 +483,7 @@ class UserSettingsModel final : public QAbstractListModel ExposeDBusApi, #endif UpdateSpaceVias, + ExpireEvents, AccessibilitySection, ReducedMotion, diff --git a/src/Utils.cpp b/src/Utils.cpp index c9a6fb55..26e894b9 100644 --- a/src/Utils.cpp +++ b/src/Utils.cpp @@ -22,6 +22,7 @@ #include #include +#include #include #include @@ -66,9 +67,9 @@ utils::stripReplyFromBody(const std::string &bodyi) if (body.startsWith(QLatin1String("> <"))) { auto segments = body.split('\n'); while (!segments.isEmpty() && segments.begin()->startsWith('>')) - segments.erase(segments.begin()); + segments.erase(segments.cbegin()); if (!segments.empty() && segments.first().isEmpty()) - segments.erase(segments.begin()); + segments.erase(segments.cbegin()); body = segments.join('\n'); } @@ -80,8 +81,9 @@ std::string utils::stripReplyFromFormattedBody(const std::string &formatted_bodyi) { QString formatted_body = QString::fromStdString(formatted_bodyi); - formatted_body.remove(QRegularExpression(QStringLiteral(".*"), - QRegularExpression::DotMatchesEverythingOption)); + static QRegularExpression replyRegex(QStringLiteral(".*"), + QRegularExpression::DotMatchesEverythingOption); + formatted_body.remove(replyRegex); formatted_body.replace(QLatin1String("@room"), QString::fromUtf8("@\u2060room")); return formatted_body.toStdString(); } @@ -290,7 +292,7 @@ utils::firstChar(const QString &input) return QString::fromUcs4(&c, 1).toUpper(); } - return QString::fromUcs4(&input.toUcs4().at(0), 1).toUpper(); + return QString::fromUcs4(&input.toStdU32String().at(0), 1).toUpper(); } QString @@ -409,9 +411,10 @@ utils::linkifyMessage(const QString &body) // Convert to valid XML. auto doc = body; doc.replace(conf::strings::url_regex, conf::strings::url_html); - doc.replace( - QRegularExpression(QStringLiteral("\\b(?(matrix:[\\S]{5,}))(?![\"'])\\b")), - conf::strings::url_html); + + static QRegularExpression matrixURIRegex( + QStringLiteral("\\b(?(matrix:[\\S]{5,}))(?![\"'])\\b")); + doc.replace(matrixURIRegex, conf::strings::url_html); return doc; } @@ -1602,3 +1605,225 @@ utils::updateSpaceVias() ApplySpaceUpdatesState::next(std::move(asus)); } + +std::atomic event_expiration_running = false; +void +utils::removeExpiredEvents() +{ + if (!UserSettings::instance()->expireEvents()) + return; + + if (event_expiration_running.exchange(true)) { + nhlog::net()->info("Event expiration still running, not starting second job."); + return; + } + + nhlog::net()->info("Remove expired events starting."); + + auto rooms = cache::roomInfo(false); + + auto us = http::client()->user_id().to_string(); + + using ExpType = + mtx::events::AccountDataEvent; + static auto getExpEv = [](const std::string &room = "") -> std::optional { + if (auto accountEvent = + cache::client()->getAccountData(mtx::events::EventType::NhekoEventExpiry, room)) + if (auto ev = std::get_if(&*accountEvent); + ev && (ev->content.expire_after_ms || ev->content.keep_only_latest)) + return std::optional{*ev}; + return std::nullopt; + }; + + struct ApplyEventExpiration + { + std::optional globalExpiry; + std::vector roomsToUpdate; + std::string filter; + + std::string currentRoom; + std::uint64_t currentRoomCount = 0; + std::string currentRoomPrevToken; + std::set> currentRoomStateEvents; + std::vector currentRoomRedactionQueue; + mtx::events::account_data::nheko_extensions::EventExpiry currentExpiry; + + static void next(std::shared_ptr state) + { + if (!state->currentRoomRedactionQueue.empty()) { + auto evid = state->currentRoomRedactionQueue.back(); + auto room = state->currentRoom; + http::client()->redact_event( + room, + evid, + [state = std::move(state), evid](const mtx::responses::EventId &, + mtx::http::RequestErr e) mutable { + if (e) { + if (e->status_code == 429 && e->matrix_error.retry_after.count() != 0) { + ChatPage::instance()->callFunctionOnGuiThread( + [state = std::move(state), + interval = e->matrix_error.retry_after]() { + QTimer::singleShot(interval, + ChatPage::instance(), + [self = std::move(state)]() mutable { + next(std::move(self)); + }); + }); + return; + } else { + nhlog::net()->error("Failed to redact event {} in {}: {}", + evid, + state->currentRoom, + *e); + state->currentRoomRedactionQueue.pop_back(); + next(std::move(state)); + } + } else { + nhlog::net()->info("Redacted event {} in {}", evid, state->currentRoom); + state->currentRoomRedactionQueue.pop_back(); + next(std::move(state)); + } + }); + } else if (!state->currentRoom.empty()) { + mtx::http::MessagesOpts opts{}; + opts.dir = mtx::http::PaginationDirection::Backwards; + opts.from = state->currentRoomPrevToken; + opts.limit = 1000; + opts.filter = state->filter; + opts.room_id = state->currentRoom; + + http::client()->messages( + opts, + [state = std::move(state)](const mtx::responses::Messages &msgs, + mtx::http::RequestErr error) mutable { + if (error || msgs.chunk.empty()) { + state->currentRoom.clear(); + state->currentRoomCount = 0; + state->currentRoomPrevToken.clear(); + } else { + if (!msgs.end.empty()) + state->currentRoomPrevToken = msgs.end; + + auto now = (uint64_t)QDateTime::currentMSecsSinceEpoch(); + auto us = http::client()->user_id().to_string(); + + for (const auto &e : msgs.chunk) { + if (std::holds_alternative< + mtx::events::RedactionEvent>(e)) + continue; + + if (std::holds_alternative< + mtx::events::RoomEvent>(e)) + continue; + + if (std::holds_alternative< + mtx::events::StateEvent>(e)) + continue; + + // skip events we don't know to protect us from mistakes. + if (std::holds_alternative< + mtx::events::RoomEvent>(e)) + continue; + + if (mtx::accessors::sender(e) != us) + continue; + + state->currentRoomCount++; + if (state->currentRoomCount <= state->currentExpiry.protect_latest) { + continue; + } + + if (state->currentExpiry.exclude_state_events && + mtx::accessors::is_state_event(e)) + continue; + + if (mtx::accessors::is_state_event(e)) { + // skip the first state event of a type + if (std::visit( + [&state](const auto &se) { + if constexpr (requires { se.state_key; }) + return state->currentRoomStateEvents + .emplace(to_string(se.type), se.state_key) + .second; + else + return false; + }, + e)) + continue; + } + + if (state->currentExpiry.keep_only_latest && + state->currentRoomCount > state->currentExpiry.keep_only_latest) { + state->currentRoomRedactionQueue.push_back( + mtx::accessors::event_id(e)); + } else if (state->currentExpiry.expire_after_ms && + (state->currentExpiry.expire_after_ms + + mtx::accessors::origin_server_ts(e).toMSecsSinceEpoch()) < + now) { + state->currentRoomRedactionQueue.push_back( + mtx::accessors::event_id(e)); + } + } + } + + if (msgs.end.empty() && state->currentRoomRedactionQueue.empty()) { + state->currentRoom.clear(); + state->currentRoomCount = 0; + state->currentRoomPrevToken.clear(); + state->currentRoomStateEvents.clear(); + } + + next(std::move(state)); + }); + } else if (!state->roomsToUpdate.empty()) { + const auto &room = state->roomsToUpdate.back(); + + auto localExp = getExpEv(room); + if (localExp) { + state->currentRoom = room; + state->currentExpiry = localExp->content; + } else if (state->globalExpiry) { + state->currentRoom = room; + state->currentExpiry = state->globalExpiry->content; + } + state->roomsToUpdate.pop_back(); + next(std::move(state)); + } else { + nhlog::net()->info("Finished event expiry"); + event_expiration_running = false; + } + } + }; + + auto asus = std::make_shared(); + + nlohmann::json filter; + filter["timeline"]["senders"] = nlohmann::json::array({us}); + filter["timeline"]["not_types"] = nlohmann::json::array({"m.room.redaction"}); + + asus->filter = filter.dump(); + + asus->globalExpiry = getExpEv(); + + for (const auto &[roomid_, info] : rooms.toStdMap()) { + auto roomid = roomid_.toStdString(); + + if (!asus->globalExpiry && !getExpEv(roomid)) + continue; + + if (auto pl = cache::client() + ->getStateEvent(roomid) + .value_or(mtx::events::StateEvent{}) + .content; + pl.user_level(us) < pl.event_level(to_string(mtx::events::EventType::RoomRedaction))) { + nhlog::net()->warn("Can't react events in {}, not running expiration.", roomid); + continue; + } + + asus->roomsToUpdate.push_back(roomid); + } + + nhlog::db()->info("Running expiration in {} rooms", asus->roomsToUpdate.size()); + + ApplyEventExpiration::next(std::move(asus)); +} diff --git a/src/Utils.h b/src/Utils.h index af5ea340..83f2cad1 100644 --- a/src/Utils.h +++ b/src/Utils.h @@ -339,4 +339,7 @@ roomVias(const std::string &roomid); void updateSpaceVias(); + +void +removeExpiredEvents(); } diff --git a/src/dbus/NhekoDBusApi.cpp b/src/dbus/NhekoDBusApi.cpp index a613b610..6b481941 100644 --- a/src/dbus/NhekoDBusApi.cpp +++ b/src/dbus/NhekoDBusApi.cpp @@ -190,20 +190,17 @@ operator<<(QDBusArgument &arg, const QImage &image) return arg; } - QImage i = image.height() > 100 || image.width() > 100 - ? image.scaledToHeight(100, Qt::SmoothTransformation) - : image; - i = std::move(i).convertToFormat(QImage::Format_RGBA8888); + QImage i = image.height() > 100 || image.width() > 100 + ? image.scaledToHeight(100, Qt::SmoothTransformation) + : image; + bool hasAlpha = i.hasAlphaChannel(); + i = std::move(i).convertToFormat(hasAlpha ? QImage::Format_RGBA8888 : QImage::Format_RGB888); + int channels = hasAlpha ? 4 : 3; + QByteArray arr(reinterpret_cast(i.bits()), static_cast(i.sizeInBytes())); arg.beginStructure(); - arg << i.width(); - arg << i.height(); - arg << i.bytesPerLine(); - arg << i.hasAlphaChannel(); - int channels = i.hasAlphaChannel() ? 4 : 3; - arg << i.depth() / channels; - arg << channels; - arg << QByteArray(reinterpret_cast(i.bits()), static_cast(i.sizeInBytes())); + arg << i.width() << i.height() << (int)i.bytesPerLine() << i.hasAlphaChannel() + << i.depth() / channels << channels << arr; arg.endStructure(); return arg; diff --git a/src/dbus/NhekoDBusApi.h b/src/dbus/NhekoDBusApi.h index 5a34f0b7..ce265a17 100644 --- a/src/dbus/NhekoDBusApi.h +++ b/src/dbus/NhekoDBusApi.h @@ -92,7 +92,6 @@ operator<<(QDBusArgument &arg, const RoomInfoItem &item); const QDBusArgument & operator>>(const QDBusArgument &arg, RoomInfoItem &item); } // nheko::dbus -Q_DECLARE_METATYPE(nheko::dbus::RoomInfoItem) QDBusArgument & operator<<(QDBusArgument &arg, const QImage &image); diff --git a/src/dock/Dock.cpp b/src/dock/Dock.cpp index 08e7adba..a4745e54 100644 --- a/src/dock/Dock.cpp +++ b/src/dock/Dock.cpp @@ -85,7 +85,8 @@ Dock::Dock(QObject *parent) { } void -Dock::setUnreadCount(const int) +Dock::setUnreadCount(const int count) { + qGuiApp->setBadgeNumber(count); } #endif diff --git a/src/emoji/Provider.h b/src/emoji/Provider.h index 5d8000a4..d0441ad3 100644 --- a/src/emoji/Provider.h +++ b/src/emoji/Provider.h @@ -94,4 +94,3 @@ public: QString categoryToName(emoji::Emoji::Category cat); } // namespace emoji -Q_DECLARE_METATYPE(emoji::Emoji) diff --git a/src/encryption/DeviceVerificationFlow.cpp b/src/encryption/DeviceVerificationFlow.cpp index a240a095..1e7ed7bc 100644 --- a/src/encryption/DeviceVerificationFlow.cpp +++ b/src/encryption/DeviceVerificationFlow.cpp @@ -37,7 +37,7 @@ DeviceVerificationFlow::DeviceVerificationFlow(QObject *, , deviceIds(std::move(deviceIds_)) , model_(model) { - nhlog::crypto()->debug("CREATING NEW FLOW, {}, {}", flow_type, (void *)this); + nhlog::crypto()->debug("CREATING NEW FLOW, {}, {}", static_cast(flow_type), (void *)this); if (deviceIds.size() == 1) deviceId = deviceIds.front(); diff --git a/src/encryption/Olm.cpp b/src/encryption/Olm.cpp index 733ce94f..8993f715 100644 --- a/src/encryption/Olm.cpp +++ b/src/encryption/Olm.cpp @@ -1306,7 +1306,7 @@ send_encrypted_to_device_messages(const std::map, qint64> rateLimit; + static QMap, qint64> rateLimit; nlohmann::json ev_json = std::visit([](const auto &e) { return nlohmann::json(e); }, event); @@ -1363,12 +1363,12 @@ send_encrypted_to_device_messages(const std::mapwarn("Not creating new session with {}:{} " "because of rate limit", @@ -1569,13 +1569,13 @@ send_encrypted_to_device_messages(const std::mapwarn("Not creating new session with {}:{} " "because of rate limit", diff --git a/src/encryption/Olm.h b/src/encryption/Olm.h index f0e51070..726b9590 100644 --- a/src/encryption/Olm.h +++ b/src/encryption/Olm.h @@ -9,10 +9,13 @@ #include #include +#include + #include namespace olm { Q_NAMESPACE +QML_NAMED_ELEMENT(Olm) enum DecryptionErrorCode { diff --git a/src/encryption/SelfVerificationStatus.cpp b/src/encryption/SelfVerificationStatus.cpp index 6fa737d4..d9d3d787 100644 --- a/src/encryption/SelfVerificationStatus.cpp +++ b/src/encryption/SelfVerificationStatus.cpp @@ -29,6 +29,11 @@ SelfVerificationStatus::SelfVerificationStatus(QObject *o) Qt::UniqueConnection); cache::client()->markUserKeysOutOfDate({http::client()->user_id().to_string()}); }); + + connect(ChatPage::instance(), + &ChatPage::initializeEmptyViews, + this, + &SelfVerificationStatus::invalidate); } void diff --git a/src/encryption/SelfVerificationStatus.h b/src/encryption/SelfVerificationStatus.h index ea790c8b..c65fffd0 100644 --- a/src/encryption/SelfVerificationStatus.h +++ b/src/encryption/SelfVerificationStatus.h @@ -5,11 +5,15 @@ #pragma once #include +#include class SelfVerificationStatus final : public QObject { Q_OBJECT + QML_ELEMENT + QML_SINGLETON + Q_PROPERTY(Status status READ status NOTIFY statusChanged) Q_PROPERTY(bool hasSSSS READ hasSSSS NOTIFY hasSSSSChanged) diff --git a/src/encryption/VerificationManager.cpp b/src/encryption/VerificationManager.cpp index 802a8177..d1248755 100644 --- a/src/encryption/VerificationManager.cpp +++ b/src/encryption/VerificationManager.cpp @@ -15,6 +15,7 @@ VerificationManager::VerificationManager(TimelineViewManager *o) : QObject(o) , rooms_(o->rooms()) { + instance_ = this; } static bool diff --git a/src/encryption/VerificationManager.h b/src/encryption/VerificationManager.h index 7b32bc98..cdc8af30 100644 --- a/src/encryption/VerificationManager.h +++ b/src/encryption/VerificationManager.h @@ -6,6 +6,7 @@ #include #include +#include #include #include @@ -21,8 +22,30 @@ class VerificationManager final : public QObject { Q_OBJECT + QML_ELEMENT + QML_SINGLETON + public: - VerificationManager(TimelineViewManager *o = nullptr); + VerificationManager(TimelineViewManager *o); + + static VerificationManager *create(QQmlEngine *qmlEngine, QJSEngine *) + { + // The instance has to exist before it is used. We cannot replace it. + Q_ASSERT(instance_); + + // The engine has to have the same thread affinity as the singleton. + Q_ASSERT(qmlEngine->thread() == instance_->thread()); + + // There can only be one engine accessing the singleton. + static QJSEngine *s_engine = nullptr; + if (s_engine) + Q_ASSERT(qmlEngine == s_engine); + else + s_engine = qmlEngine; + + QJSEngine::setObjectOwnership(instance_, QJSEngine::CppOwnership); + return instance_; + } Q_INVOKABLE void removeVerificationFlow(DeviceVerificationFlow *flow); void verifyUser(QString userid); @@ -45,4 +68,6 @@ private: QHash> dvList; bool isInitialSync_ = false; RoomlistModel *rooms_; + + inline static VerificationManager *instance_ = nullptr; }; diff --git a/src/main.cpp b/src/main.cpp index 90e1cb4c..07397d62 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -154,8 +154,6 @@ main(int argc, char *argv[]) QCoreApplication::setApplicationVersion(nheko::version); QCoreApplication::setOrganizationName(QStringLiteral("nheko")); QCoreApplication::setAttribute(Qt::AA_DontCreateNativeWidgetSiblings); - QCoreApplication::setAttribute(Qt::AA_UseHighDpiPixmaps); - QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling); // Disable the qml disk cache by default to prevent crashes on updates. See // https://github.com/Nheko-Reborn/nheko/issues/1383 @@ -256,7 +254,7 @@ main(int argc, char *argv[]) app.setWindowIcon(QIcon::fromTheme(QStringLiteral("nheko"), QIcon{":/logos/nheko.png"})); #endif #ifdef NHEKO_FLATPAK - app.setDesktopFileName(QStringLiteral("io.github.NhekoReborn.Nheko")); + app.setDesktopFileName(QStringLiteral("im.nheko.Nheko")); #else app.setDesktopFileName(QStringLiteral("nheko")); #endif @@ -311,6 +309,9 @@ main(int argc, char *argv[]) std::exit(1); } + auto filter = new NhekoFixupPaletteEventFilter(&app); + app.installEventFilter(filter); + if (parser.isSet(configName)) UserSettings::initialize(parser.value(configName)); else @@ -332,18 +333,20 @@ main(int argc, char *argv[]) QLocale::setDefault(QLocale(QLocale::English, QLocale::UnitedKingdom)); QTranslator qtTranslator; - qtTranslator.load(QLocale(), - QStringLiteral("qt"), - QStringLiteral("_"), - QLibraryInfo::location(QLibraryInfo::TranslationsPath)); - app.installTranslator(&qtTranslator); + if (qtTranslator.load(QLocale(), + QStringLiteral("qt"), + QStringLiteral("_"), + QLibraryInfo::path(QLibraryInfo::TranslationsPath))) + app.installTranslator(&qtTranslator); QTranslator appTranslator; - appTranslator.load( - QLocale(), QStringLiteral("nheko"), QStringLiteral("_"), QStringLiteral(":/translations")); - app.installTranslator(&appTranslator); + if (appTranslator.load(QLocale(), + QStringLiteral("nheko"), + QStringLiteral("_"), + QStringLiteral(":/translations"))) + app.installTranslator(&appTranslator); - MainWindow w; + MainWindow w(nullptr); // QQuickView w; // Move the MainWindow to the center diff --git a/src/notifications/Manager.h b/src/notifications/Manager.h index bc37dbd8..7686d78e 100644 --- a/src/notifications/Manager.h +++ b/src/notifications/Manager.h @@ -111,10 +111,3 @@ private: // Only populated on Linux atm QMap notificationIds; }; - -#if defined(NHEKO_DBUS_SYS) -QDBusArgument & -operator<<(QDBusArgument &arg, const QImage &image); -const QDBusArgument & -operator>>(const QDBusArgument &arg, QImage &); -#endif diff --git a/src/notifications/ManagerMac.mm b/src/notifications/ManagerMac.mm index 73d4287f..69914ac6 100644 --- a/src/notifications/ManagerMac.mm +++ b/src/notifications/ManagerMac.mm @@ -14,7 +14,6 @@ #import #include -#include @interface UNNotificationAttachment (UNNotificationAttachmentAdditions) + (UNNotificationAttachment*)createFromImageData:(NSData*)imgData diff --git a/src/timeline/CommunitiesModel.cpp b/src/timeline/CommunitiesModel.cpp index c563639b..3c09d747 100644 --- a/src/timeline/CommunitiesModel.cpp +++ b/src/timeline/CommunitiesModel.cpp @@ -17,15 +17,12 @@ #include "Utils.h" #include "timeline/TimelineModel.h" -Q_DECLARE_METATYPE(SpaceItem) - CommunitiesModel::CommunitiesModel(QObject *parent) : QAbstractListModel(parent) , hiddenTagIds_{UserSettings::instance()->hiddenTags()} , mutedTagIds_{UserSettings::instance()->mutedTags()} { - static auto ignore = qRegisterMetaType(); - (void)ignore; + instance_ = this; } QHash diff --git a/src/timeline/CommunitiesModel.h b/src/timeline/CommunitiesModel.h index a90fa6a2..7af6d050 100644 --- a/src/timeline/CommunitiesModel.h +++ b/src/timeline/CommunitiesModel.h @@ -6,6 +6,7 @@ #include #include +#include #include #include #include @@ -21,6 +22,8 @@ class CommunitiesModel; class FilteredCommunitiesModel final : public QSortFilterProxyModel { Q_OBJECT + QML_ELEMENT + QML_UNCREATABLE("Use Communities.filtered() to create a FilteredCommunitiesModel") public: explicit FilteredCommunitiesModel(CommunitiesModel *model, QObject *parent = nullptr); @@ -73,6 +76,9 @@ public: class CommunitiesModel final : public QAbstractListModel { Q_OBJECT + QML_NAMED_ELEMENT(Communities) + QML_SINGLETON + Q_PROPERTY(QString currentTagId READ currentTagId WRITE setCurrentTagId NOTIFY currentTagIdChanged RESET resetCurrentTagId) Q_PROPERTY(QStringList tags READ tags NOTIFY tagsChanged) @@ -148,7 +154,27 @@ public: void restoreCollapsed(); }; - CommunitiesModel(QObject *parent = nullptr); + CommunitiesModel(QObject *parent); + + static CommunitiesModel *create(QQmlEngine *qmlEngine, QJSEngine *) + { + // The instance has to exist before it is used. We cannot replace it. + Q_ASSERT(instance_); + + // The engine has to have the same thread affinity as the singleton. + Q_ASSERT(qmlEngine->thread() == instance_->thread()); + + // There can only be one engine accessing the singleton. + static QJSEngine *s_engine = nullptr; + if (s_engine) + Q_ASSERT(qmlEngine == s_engine); + else + s_engine = qmlEngine; + + QJSEngine::setObjectOwnership(instance_, QJSEngine::CppOwnership); + return instance_; + } + QHash roleNames() const override; int rowCount(const QModelIndex &parent = QModelIndex()) const override { @@ -221,4 +247,6 @@ private: mtx::responses::UnreadNotifications dmUnreads{}; friend class FilteredCommunitiesModel; + + inline static CommunitiesModel *instance_ = nullptr; }; diff --git a/src/timeline/DelegateChooser.cpp b/src/timeline/DelegateChooser.cpp index 7f80d5f6..91b2194b 100644 --- a/src/timeline/DelegateChooser.cpp +++ b/src/timeline/DelegateChooser.cpp @@ -77,13 +77,13 @@ DelegateChooser::appendChoice(QQmlListProperty *p, DelegateChoic dc->choices_.append(c); } -int +qsizetype DelegateChooser::choiceCount(QQmlListProperty *p) { return static_cast(p->object)->choices_.count(); } DelegateChoice * -DelegateChooser::choice(QQmlListProperty *p, int index) +DelegateChooser::choice(QQmlListProperty *p, qsizetype index) { return static_cast(p->object)->choices_.at(index); } diff --git a/src/timeline/DelegateChooser.h b/src/timeline/DelegateChooser.h index 8f72e73b..ac227382 100644 --- a/src/timeline/DelegateChooser.h +++ b/src/timeline/DelegateChooser.h @@ -19,6 +19,7 @@ class QQmlAdaptorModel; class DelegateChoice : public QObject { Q_OBJECT + QML_ELEMENT Q_CLASSINFO("DefaultProperty", "delegate") public: @@ -45,6 +46,7 @@ private: class DelegateChooser : public QQuickItem { Q_OBJECT + QML_ELEMENT Q_CLASSINFO("DefaultProperty", "choices") public: @@ -86,7 +88,7 @@ private: DelegateIncubator incubator{*this}; static void appendChoice(QQmlListProperty *, DelegateChoice *); - static int choiceCount(QQmlListProperty *); - static DelegateChoice *choice(QQmlListProperty *, int index); + static qsizetype choiceCount(QQmlListProperty *); + static DelegateChoice *choice(QQmlListProperty *, qsizetype index); static void clearChoices(QQmlListProperty *); }; diff --git a/src/timeline/EventStore.cpp b/src/timeline/EventStore.cpp index 84b6dcd4..63b67474 100644 --- a/src/timeline/EventStore.cpp +++ b/src/timeline/EventStore.cpp @@ -15,12 +15,9 @@ #include "EventAccessors.h" #include "Logging.h" #include "MatrixClient.h" -#include "TimelineModel.h" #include "UserSettingsPage.h" #include "Utils.h" -Q_DECLARE_METATYPE(Reaction) - QCache EventStore::decryptedEvents_{1000}; QCache EventStore::events_by_id_{ 1000}; @@ -29,9 +26,6 @@ QCache EventStore:: EventStore::EventStore(std::string room_id, QObject *) : room_id_(std::move(room_id)) { - static auto reactionType = qRegisterMetaType(); - (void)reactionType; - auto range = cache::client()->getTimelineRange(room_id_); if (range) { @@ -264,7 +258,8 @@ EventStore::EventStore(std::string room_id, QObject *) auto idx = idToIndex(pending_event_id); events_by_id_.remove({room_id_, pending_event_id}); - events_.remove({room_id_, toInternalIdx(*idx)}); + if (idx) + events_.remove({room_id_, toInternalIdx(*idx)}); } } @@ -292,12 +287,12 @@ EventStore::EventStore(std::string room_id, QObject *) } void -EventStore::addPending(mtx::events::collections::TimelineEvents event) +EventStore::addPending(const mtx::events::collections::TimelineEvents &event) { if (this->thread() != QThread::currentThread()) nhlog::db()->warn("{} called from a different thread!", __func__); - cache::client()->savePendingMessage(this->room_id_, {event}); + cache::client()->savePendingMessage(this->room_id_, event); mtx::responses::Timeline events; events.limited = false; events.events.emplace_back(event); @@ -760,11 +755,11 @@ EventStore::decryptEvent(const IdIndex &idx, } void -EventStore::refetchOnlineKeyBackupKeys(TimelineModel *room) +EventStore::refetchOnlineKeyBackupKeys() { - for (const auto &[session_id, request] : room->events.pending_key_requests) { + for (const auto &[session_id, request] : this->pending_key_requests) { (void)request; - olm::lookup_keybackup(room->events.room_id_, session_id); + olm::lookup_keybackup(this->room_id_, session_id); } } diff --git a/src/timeline/EventStore.h b/src/timeline/EventStore.h index bf905fc6..ee92a795 100644 --- a/src/timeline/EventStore.h +++ b/src/timeline/EventStore.h @@ -11,6 +11,7 @@ #include #include +#include #include #include #include @@ -18,8 +19,6 @@ #include "Reaction.h" #include "encryption/Olm.h" -class TimelineModel; - class EventStore final : public QObject { Q_OBJECT @@ -27,7 +26,7 @@ class EventStore final : public QObject public: EventStore(std::string room_id, QObject *parent); - static void refetchOnlineKeyBackupKeys(TimelineModel *room); + void refetchOnlineKeyBackupKeys(); // taken from QtPrivate::QHashCombine static uint hashCombine(uint hash, uint seed) @@ -108,7 +107,7 @@ signals: void newEncryptedImage(mtx::crypto::EncryptedFile encryptionInfo); void eventFetched(std::string id, std::string relatedTo, - mtx::events::collections::TimelineEvents timeline); + const mtx::events::collections::TimelineEvents &timeline); void oldMessagesRetrieved(const mtx::responses::Messages &); void fetchedMore(); @@ -120,7 +119,7 @@ signals: void updateFlowEventId(std::string event_id); public slots: - void addPending(mtx::events::collections::TimelineEvents event); + void addPending(const mtx::events::collections::TimelineEvents &event); void receivedSessionKey(const std::string &session_id); void clearTimeline(); void enableKeyRequests(bool suppressKeyRequests_); diff --git a/src/timeline/InputBar.cpp b/src/timeline/InputBar.cpp index 0c2e3752..a59bc4c6 100644 --- a/src/timeline/InputBar.cpp +++ b/src/timeline/InputBar.cpp @@ -16,6 +16,8 @@ #include #include #include +#include +#include #include #include @@ -74,55 +76,6 @@ MediaUpload::thumbnailDataUrl() const return QString("data:image/png;base64,") + base64; } -bool -InputVideoSurface::present(const QVideoFrame &frame) -{ - QImage::Format format = QVideoFrame::imageFormatFromPixelFormat(frame.pixelFormat()); - - if (format == QImage::Format_Invalid) { - emit newImage({}); - return false; - } else { - QVideoFrame frametodraw(frame); - - if (!frametodraw.map(QAbstractVideoBuffer::ReadOnly)) { - emit newImage({}); - return false; - } - - // this is a shallow operation. it just refer the frame buffer - QImage image(qAsConst(frametodraw).bits(), - frametodraw.width(), - frametodraw.height(), - frametodraw.bytesPerLine(), - format); - image.detach(); - - frametodraw.unmap(); - - emit newImage(std::move(image)); - return true; - } -} - -QList -InputVideoSurface::supportedPixelFormats(QAbstractVideoBuffer::HandleType type) const -{ - if (type == QAbstractVideoBuffer::NoHandle) { - return { - QVideoFrame::Format_ARGB32, - QVideoFrame::Format_ARGB32_Premultiplied, - QVideoFrame::Format_RGB24, - QVideoFrame::Format_BGR24, - QVideoFrame::Format_RGB32, - QVideoFrame::Format_RGB565, - QVideoFrame::Format_RGB555, - }; - } else { - return {}; - } -} - bool InputBar::tryPasteAttachment(bool fromMouse) { @@ -536,7 +489,7 @@ InputBar::message(const QString &msg, MarkdownOverride useMarkdown, bool rainbow if (!related.quoted_user.startsWith("@room:")) { QString body; bool firstLine = true; - auto lines = related.quoted_body.splitRef(u'\n'); + auto lines = QStringView(related.quoted_body).split(u'\n'); for (auto line : qAsConst(lines)) { if (firstLine) { firstLine = false; @@ -823,7 +776,9 @@ InputBar::getCommandAndArgs(const QString ¤tText) const if (!currentText.startsWith('/')) return {{}, currentText}; - int command_end = currentText.indexOf(QRegularExpression(QStringLiteral("\\s"))); + static QRegularExpression spaceRegex(QStringLiteral("\\s")); + + int command_end = currentText.indexOf(spaceRegex); if (command_end == -1) command_end = currentText.size(); auto name = currentText.mid(1, command_end - 1); @@ -1036,83 +991,83 @@ MediaUpload::MediaUpload(std::unique_ptr source_, blurhash_ = QString::fromStdString(blurhash::encode(data_.data(), img.width(), img.height(), 4, 3)); } else if (mimeClass_ == u"video" || mimeClass_ == u"audio") { - auto mediaPlayer = new QMediaPlayer( - this, - mimeClass_ == u"video" ? QFlags{QMediaPlayer::VideoSurface} : QMediaPlayer::Flags{}); - mediaPlayer->setMuted(true); + auto mediaPlayer = new QMediaPlayer(this); + mediaPlayer->setAudioOutput(nullptr); if (mimeClass_ == u"video") { - auto newSurface = new InputVideoSurface(this); - connect( - newSurface, &InputVideoSurface::newImage, this, [this, mediaPlayer](QImage img) { - if (img.size().isEmpty()) - return; + auto newSurface = new QVideoSink(this); + connect(newSurface, + &QVideoSink::videoFrameChanged, + this, + [this, mediaPlayer](const QVideoFrame &frame) { + QImage img = frame.toImage(); + if (img.size().isEmpty()) + return; - mediaPlayer->stop(); + mediaPlayer->stop(); - auto orientation = mediaPlayer->metaData(QMediaMetaData::Orientation).toInt(); - if (orientation == 90 || orientation == 270 || orientation == 180) { - img = - img.transformed(QTransform().rotate(orientation), Qt::SmoothTransformation); - } + auto orientation = + mediaPlayer->metaData().value(QMediaMetaData::Orientation).toInt(); + if (orientation == 90 || orientation == 270 || orientation == 180) { + img = img.transformed(QTransform().rotate(orientation), + Qt::SmoothTransformation); + } - nhlog::ui()->debug("Got image {}x{}", img.width(), img.height()); + nhlog::ui()->debug("Got image {}x{}", img.width(), img.height()); - this->setThumbnail(img); + this->setThumbnail(img); - if (!dimensions_.isValid()) - this->dimensions_ = img.size(); + if (!dimensions_.isValid()) + this->dimensions_ = img.size(); - if (img.height() > 200 && img.width() > 360) - img = img.scaled(360, 200, Qt::KeepAspectRatioByExpanding); - std::vector data_; - for (int y = 0; y < img.height(); y++) { - for (int x = 0; x < img.width(); x++) { - auto p = img.pixel(x, y); - data_.push_back(static_cast(qRed(p))); - data_.push_back(static_cast(qGreen(p))); - data_.push_back(static_cast(qBlue(p))); - } - } - blurhash_ = QString::fromStdString( - blurhash::encode(data_.data(), img.width(), img.height(), 4, 3)); - }); + if (img.height() > 200 && img.width() > 360) + img = img.scaled(360, 200, Qt::KeepAspectRatioByExpanding); + std::vector data_; + for (int y = 0; y < img.height(); y++) { + for (int x = 0; x < img.width(); x++) { + auto p = img.pixel(x, y); + data_.push_back(static_cast(qRed(p))); + data_.push_back(static_cast(qGreen(p))); + data_.push_back(static_cast(qBlue(p))); + } + } + blurhash_ = QString::fromStdString( + blurhash::encode(data_.data(), img.width(), img.height(), 4, 3)); + }); mediaPlayer->setVideoOutput(newSurface); } connect(mediaPlayer, - qOverload(&QMediaPlayer::error), + &QMediaPlayer::errorOccurred, this, - [mediaPlayer](QMediaPlayer::Error error) { + [](QMediaPlayer::Error error, QString errorString) { nhlog::ui()->debug("Media player error {} and errorStr {}", - error, - mediaPlayer->errorString().toStdString()); + static_cast(error), + errorString.toStdString()); }); connect(mediaPlayer, &QMediaPlayer::mediaStatusChanged, [mediaPlayer](QMediaPlayer::MediaStatus status) { - nhlog::ui()->debug( - "Media player status {} and error {}", status, mediaPlayer->error()); + nhlog::ui()->debug("Media player status {} and error {}", + static_cast(status), + static_cast(mediaPlayer->error())); }); - connect(mediaPlayer, - qOverload(&QMediaPlayer::metaDataChanged), - this, - [this, mediaPlayer](const QString &t, const QVariant &) { - nhlog::ui()->debug("Got metadata {}", t.toStdString()); + connect(mediaPlayer, &QMediaPlayer::metaDataChanged, this, [this, mediaPlayer]() { + nhlog::ui()->debug("Got metadata {}"); - if (mediaPlayer->duration() > 0) - this->duration_ = mediaPlayer->duration(); + if (mediaPlayer->duration() > 0) + this->duration_ = mediaPlayer->duration(); - auto dimensions = mediaPlayer->metaData(QMediaMetaData::Resolution).toSize(); - if (!dimensions.isEmpty()) { - dimensions_ = dimensions; - auto orientation = - mediaPlayer->metaData(QMediaMetaData::Orientation).toInt(); - if (orientation == 90 || orientation == 270) { - dimensions_.transpose(); - } - } - }); + auto dimensions = mediaPlayer->metaData().value(QMediaMetaData::Resolution).toSize(); + if (!dimensions.isEmpty()) { + dimensions_ = dimensions; + auto orientation = + mediaPlayer->metaData().value(QMediaMetaData::Orientation).toInt(); + if (orientation == 90 || orientation == 270) { + dimensions_.transpose(); + } + } + }); connect( mediaPlayer, &QMediaPlayer::durationChanged, this, [this, mediaPlayer](qint64 duration) { if (duration > 0) { @@ -1125,8 +1080,8 @@ MediaUpload::MediaUpload(std::unique_ptr source_, auto originalFile = qobject_cast(source.get()); - mediaPlayer->setMedia( - QMediaContent(originalFile ? originalFile->fileName() : originalFilename_), source.get()); + mediaPlayer->setSourceDevice( + source.get(), QUrl(originalFile ? originalFile->fileName() : originalFilename_)); mediaPlayer->play(); } diff --git a/src/timeline/InputBar.h b/src/timeline/InputBar.h index 1f1d6fe1..3cd65524 100644 --- a/src/timeline/InputBar.h +++ b/src/timeline/InputBar.h @@ -4,15 +4,16 @@ #pragma once -#include #include #include #include +#include #include #include #include #include #include + #include #include @@ -41,28 +42,13 @@ enum class MarkdownOverride CMARK, }; -class InputVideoSurface final : public QAbstractVideoSurface -{ - Q_OBJECT - -public: - InputVideoSurface(QObject *parent) - : QAbstractVideoSurface(parent) - { - } - - bool present(const QVideoFrame &frame) override; - - QList - supportedPixelFormats(QAbstractVideoBuffer::HandleType type) const override; - -signals: - void newImage(QImage img); -}; - class MediaUpload final : public QObject { Q_OBJECT + + QML_ELEMENT + QML_UNCREATABLE("") + Q_PROPERTY(int mediaType READ type NOTIFY mediaTypeChanged) // https://stackoverflow.com/questions/33422265/pass-qimage-to-qml/68554646#68554646 Q_PROPERTY(QUrl thumbnail READ thumbnailDataUrl NOTIFY thumbnailChanged) diff --git a/src/timeline/PresenceEmitter.h b/src/timeline/PresenceEmitter.h index e89fb316..09ad1301 100644 --- a/src/timeline/PresenceEmitter.h +++ b/src/timeline/PresenceEmitter.h @@ -5,6 +5,7 @@ #pragma once #include +#include #include @@ -15,10 +16,33 @@ class PresenceEmitter final : public QObject { Q_OBJECT + QML_NAMED_ELEMENT(Presence) + QML_SINGLETON + public: PresenceEmitter(QObject *p = nullptr) : QObject(p) { + instance_ = this; + } + + static PresenceEmitter *create(QQmlEngine *qmlEngine, QJSEngine *) + { + // The instance has to exist before it is used. We cannot replace it. + Q_ASSERT(instance_); + + // The engine has to have the same thread affinity as the singleton. + Q_ASSERT(qmlEngine->thread() == instance_->thread()); + + // There can only be one engine accessing the singleton. + static QJSEngine *s_engine = nullptr; + if (s_engine) + Q_ASSERT(qmlEngine == s_engine); + else + s_engine = qmlEngine; + + QJSEngine::setObjectOwnership(instance_, QJSEngine::CppOwnership); + return instance_; } void sync(const std::vector> &presences); @@ -28,4 +52,7 @@ public: signals: void presenceChanged(QString userid); + +private: + inline static PresenceEmitter *instance_ = nullptr; }; diff --git a/src/timeline/RoomlistModel.cpp b/src/timeline/RoomlistModel.cpp index 6bd02a17..8d8d2977 100644 --- a/src/timeline/RoomlistModel.cpp +++ b/src/timeline/RoomlistModel.cpp @@ -28,8 +28,6 @@ RoomlistModel::RoomlistModel(TimelineViewManager *parent) : QAbstractListModel(parent) , manager(parent) { - [[maybe_unused]] static auto id = qRegisterMetaType(); - connect(ChatPage::instance(), &ChatPage::decryptSidebarChanged, this, [this]() { auto decrypt = ChatPage::instance()->userSettings()->decryptSidebar(); QHash>::iterator i; @@ -819,7 +817,7 @@ RoomlistModel::refetchOnlineKeyBackupKeys() auto ptr = i.value(); if (!ptr.isNull()) { - EventStore::refetchOnlineKeyBackupKeys(ptr.data()); + ptr->refetchOnlineKeyBackupKeys(); } } } @@ -911,6 +909,8 @@ FilteredRoomlistModel::FilteredRoomlistModel(RoomlistModel *model, QObject *pare : QSortFilterProxyModel(parent) , roomlistmodel(model) { + instance_ = this; + this->sortByImportance = UserSettings::instance()->sortByImportance(); this->sortByAlphabet = UserSettings::instance()->sortByAlphabet(); setSourceModel(model); @@ -1237,3 +1237,49 @@ FilteredRoomlistModel::previousRoom() } } } + +QString +RoomPreview::inviterAvatarUrl() const +{ + if (isInvite_) { + auto self = cache::client()->getInviteMember(roomid_.toStdString(), + http::client()->user_id().to_string()); + if (self && !self->inviter.empty()) { + auto other = cache::client()->getInviteMember(roomid_.toStdString(), self->inviter); + if (other && other->avatar_url.starts_with("mxc://")) { + return QString::fromStdString(other->avatar_url); + } + } + } + + return QString(); +} +QString +RoomPreview::inviterDisplayName() const +{ + if (isInvite_) { + auto self = cache::client()->getInviteMember(roomid_.toStdString(), + http::client()->user_id().to_string()); + if (self && !self->inviter.empty()) { + auto other = cache::client()->getInviteMember(roomid_.toStdString(), self->inviter); + if (other) { + return QString::fromStdString(other->name).toHtmlEscaped(); + } + } + } + + return QString(); +} +QString +RoomPreview::inviterUserId() const +{ + if (isInvite_) { + auto self = cache::client()->getInviteMember(roomid_.toStdString(), + http::client()->user_id().to_string()); + if (self && !self->inviter.empty()) { + return QString::fromStdString(self->inviter); + } + } + + return QString(); +} diff --git a/src/timeline/RoomlistModel.h b/src/timeline/RoomlistModel.h index 9aaafc06..34bf3f9a 100644 --- a/src/timeline/RoomlistModel.h +++ b/src/timeline/RoomlistModel.h @@ -31,6 +31,9 @@ class RoomPreview Q_PROPERTY(QString roomTopic READ roomTopic CONSTANT) Q_PROPERTY(QString roomAvatarUrl READ roomAvatarUrl CONSTANT) Q_PROPERTY(QString reason READ reason CONSTANT) + Q_PROPERTY(QString inviterAvatarUrl READ inviterAvatarUrl CONSTANT) + Q_PROPERTY(QString inviterDisplayName READ inviterDisplayName CONSTANT) + Q_PROPERTY(QString inviterUserId READ inviterUserId CONSTANT) Q_PROPERTY(bool isInvite READ isInvite CONSTANT) Q_PROPERTY(bool isFetched READ isFetched CONSTANT) @@ -42,6 +45,9 @@ public: QString roomTopic() const { return roomTopic_; } QString roomAvatarUrl() const { return roomAvatarUrl_; } QString reason() const { return reason_; } + QString inviterAvatarUrl() const; + QString inviterDisplayName() const; + QString inviterUserId() const; bool isInvite() const { return isInvite_; } bool isFetched() const { return isFetched_; } @@ -161,12 +167,36 @@ private: class FilteredRoomlistModel final : public QSortFilterProxyModel { Q_OBJECT + + QML_NAMED_ELEMENT(Rooms) + QML_SINGLETON + Q_PROPERTY( TimelineModel *currentRoom READ currentRoom NOTIFY currentRoomChanged RESET resetCurrentRoom) Q_PROPERTY(RoomPreview currentRoomPreview READ currentRoomPreview NOTIFY currentRoomChanged RESET resetCurrentRoom) public: FilteredRoomlistModel(RoomlistModel *model, QObject *parent = nullptr); + + static FilteredRoomlistModel *create(QQmlEngine *qmlEngine, QJSEngine *) + { + // The instance has to exist before it is used. We cannot replace it. + Q_ASSERT(instance_); + + // The engine has to have the same thread affinity as the singleton. + Q_ASSERT(qmlEngine->thread() == instance_->thread()); + + // There can only be one engine accessing the singleton. + static QJSEngine *s_engine = nullptr; + if (s_engine) + Q_ASSERT(qmlEngine == s_engine); + else + s_engine = qmlEngine; + + QJSEngine::setObjectOwnership(instance_, QJSEngine::CppOwnership); + return instance_; + } + bool lessThan(const QModelIndex &left, const QModelIndex &right) const override; bool filterAcceptsRow(int sourceRow, const QModelIndex &) const override; @@ -243,4 +273,6 @@ private: FilterBy filterType = FilterBy::Nothing; QStringList hiddenTags, hiddenSpaces; bool hideDMs = false; + + inline static FilteredRoomlistModel *instance_ = nullptr; }; diff --git a/src/timeline/TimelineFilter.h b/src/timeline/TimelineFilter.h index 1c92c89a..658a8c57 100644 --- a/src/timeline/TimelineFilter.h +++ b/src/timeline/TimelineFilter.h @@ -4,6 +4,7 @@ #pragma once +#include #include #include @@ -14,6 +15,7 @@ class TimelineFilter : public QSortFilterProxyModel { Q_OBJECT + QML_ELEMENT Q_PROPERTY(QString filterByThread READ filterByThread WRITE setThreadId NOTIFY threadIdChanged) Q_PROPERTY(QString filterByContent READ filterByContent WRITE setContentFilter NOTIFY diff --git a/src/timeline/TimelineModel.cpp b/src/timeline/TimelineModel.cpp index 5996bea8..22fe63d4 100644 --- a/src/timeline/TimelineModel.cpp +++ b/src/timeline/TimelineModel.cpp @@ -31,8 +31,6 @@ #include "Utils.h" #include "encryption/Olm.h" -Q_DECLARE_METATYPE(QModelIndex) - namespace std { inline uint // clazy:exclude=qhash-namespace qHash(const std::string &key, uint seed = 0) @@ -218,6 +216,8 @@ qml_mtx_events::toRoomEventType(mtx::events::EventType e) return qml_mtx_events::EventType::Topic; case EventType::RoomTombstone: return qml_mtx_events::EventType::Tombstone; + case EventType::RoomServerAcl: + return qml_mtx_events::EventType::ServerAcl; case EventType::RoomRedaction: return qml_mtx_events::EventType::Redaction; case EventType::RoomPinnedEvents: @@ -336,6 +336,9 @@ qml_mtx_events::fromRoomEventType(qml_mtx_events::EventType t) /// m.room.tombstone case qml_mtx_events::Tombstone: return mtx::events::EventType::RoomTombstone; + /// m.room.server_acl + case qml_mtx_events::ServerAcl: + return mtx::events::EventType::RoomServerAcl; /// m.room.topic case qml_mtx_events::Topic: return mtx::events::EventType::RoomTopic; @@ -877,7 +880,6 @@ QVariant TimelineModel::data(const QModelIndex &index, int role) const { using namespace mtx::accessors; - namespace acc = mtx::accessors; if (index.row() < 0 && index.row() >= rowCount()) return {}; @@ -893,6 +895,28 @@ TimelineModel::data(const QModelIndex &index, int role) const return data(*event, role); } +void +TimelineModel::multiData(const QModelIndex &index, QModelRoleDataSpan roleDataSpan) const +{ + if (index.row() < 0 && index.row() >= rowCount()) + return; + + // HACK(Nico): fetchMore likes to break with dynamically sized delegates and reuseItems + if (index.row() + 1 == rowCount() && !m_paginationInProgress) + const_cast(this)->fetchMore(index); + + auto event = events.get(rowCount() - index.row() - 1); + + if (!event) + return; + + for (QModelRoleData &roleData : roleDataSpan) { + int role = roleData.role(); + + roleData.setData(data(*event, role)); + } +} + QVariant TimelineModel::dataById(const QString &id, int role, const QString &relatedTo) { @@ -1321,7 +1345,7 @@ TimelineModel::formatDateSeparator(QDate date) const QString fmt = QLocale::system().dateFormat(QLocale::LongFormat); if (now.date().year() == date.year()) { - QRegularExpression rx(QStringLiteral("[^a-zA-Z]*y+[^a-zA-Z]*")); + static QRegularExpression rx(QStringLiteral("[^a-zA-Z]*y+[^a-zA-Z]*")); fmt = fmt.remove(rx); } diff --git a/src/timeline/TimelineModel.h b/src/timeline/TimelineModel.h index b0d81441..fd1a4396 100644 --- a/src/timeline/TimelineModel.h +++ b/src/timeline/TimelineModel.h @@ -11,6 +11,7 @@ #include #include +#include #include #include "CacheCryptoStructs.h" @@ -36,6 +37,7 @@ struct RelatedInfo; namespace qml_mtx_events { Q_NAMESPACE +QML_NAMED_ELEMENT(MtxEvent) enum EventType { @@ -85,6 +87,8 @@ enum EventType PowerLevels, /// m.room.tombstone Tombstone, + /// m.room.server_acl + ServerAcl, /// m.room.topic Topic, /// m.room.redaction @@ -191,6 +195,9 @@ class TimelineViewManager; class TimelineModel final : public QAbstractListModel { Q_OBJECT + QML_NAMED_ELEMENT(Room) + QML_UNCREATABLE("") + Q_PROPERTY(int currentIndex READ currentIndex WRITE setCurrentIndex NOTIFY currentIndexChanged) Q_PROPERTY(std::vector typingUsers READ typingUsers WRITE updateTypingUsers NOTIFY typingUsersChanged) @@ -277,6 +284,7 @@ public: QHash roleNames() const override; int rowCount(const QModelIndex &parent = QModelIndex()) const override; QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + void multiData(const QModelIndex &index, QModelRoleDataSpan roleDataSpan) const override; QVariant data(const mtx::events::collections::TimelineEvents &event, int role) const; Q_INVOKABLE QVariant dataById(const QString &id, int role, const QString &relatedTo); Q_INVOKABLE QVariant dataByIndex(int i, int role = Qt::DisplayRole) const @@ -374,6 +382,8 @@ public: return std::nullopt; } + void refetchOnlineKeyBackupKeys() { events.refetchOnlineKeyBackupKeys(); }; + public slots: void setCurrentIndex(int index); int currentIndex() const { return idToIndex(currentId); } @@ -537,8 +547,6 @@ private: std::unique_ptr parentSummary = nullptr; bool parentChecked = false; - - friend void EventStore::refetchOnlineKeyBackupKeys(TimelineModel *room); }; Q_DECLARE_OPERATORS_FOR_FLAGS(TimelineModel::SpecialEffects) diff --git a/src/timeline/TimelineViewManager.cpp b/src/timeline/TimelineViewManager.cpp index e062dde2..b8bd679b 100644 --- a/src/timeline/TimelineViewManager.cpp +++ b/src/timeline/TimelineViewManager.cpp @@ -96,29 +96,21 @@ TimelineViewManager::userColor(QString id, QColor background) TimelineViewManager::TimelineViewManager(CallManager *, ChatPage *parent) : QObject(parent) , rooms_(new RoomlistModel(this)) + , frooms_(new FilteredRoomlistModel(this->rooms_)) , communities_(new CommunitiesModel(this)) , verificationManager_(new VerificationManager(this)) , presenceEmitter(new PresenceEmitter(this)) { - static auto self = this; - qmlRegisterSingletonInstance("im.nheko", 1, 0, "TimelineManager", self); - qmlRegisterSingletonType( - "im.nheko", 1, 0, "Rooms", [](QQmlEngine *, QJSEngine *) -> QObject * { - auto ptr = new FilteredRoomlistModel(self->rooms_); + instance_ = this; - connect(self->communities_, - &CommunitiesModel::currentTagIdChanged, - ptr, - &FilteredRoomlistModel::updateFilterTag); - connect(self->communities_, - &CommunitiesModel::hiddenTagsChanged, - ptr, - &FilteredRoomlistModel::updateHiddenTagsAndSpaces); - return ptr; - }); - qmlRegisterSingletonInstance("im.nheko", 1, 0, "Communities", self->communities_); - qmlRegisterSingletonInstance("im.nheko", 1, 0, "VerificationManager", verificationManager_); - qmlRegisterSingletonInstance("im.nheko", 1, 0, "Presence", presenceEmitter); + connect(this->communities_, + &CommunitiesModel::currentTagIdChanged, + frooms_, + &FilteredRoomlistModel::updateFilterTag); + connect(this->communities_, + &CommunitiesModel::hiddenTagsChanged, + frooms_, + &FilteredRoomlistModel::updateHiddenTagsAndSpaces); updateColorPalette(); @@ -280,7 +272,7 @@ TimelineViewManager::saveMedia(QString mxcUrl) { const QString downloadsFolder = QStandardPaths::writableLocation(QStandardPaths::DownloadLocation); - const QString openLocation = downloadsFolder + "/" + mxcUrl.splitRef(u'/').constLast(); + const QString openLocation = downloadsFolder + "/" + mxcUrl.split(u'/').constLast(); const QString filename = QFileDialog::getSaveFileName(nullptr, {}, openLocation); @@ -444,7 +436,7 @@ TimelineViewManager::focusMessageInput() emit focusInput(); } -QObject * +QAbstractItemModel * TimelineViewManager::completerFor(const QString &completerName, const QString &roomId) { if (completerName == QLatin1String("user")) { diff --git a/src/timeline/TimelineViewManager.h b/src/timeline/TimelineViewManager.h index e3279e21..53179b44 100644 --- a/src/timeline/TimelineViewManager.h +++ b/src/timeline/TimelineViewManager.h @@ -34,6 +34,9 @@ class TimelineViewManager final : public QObject { Q_OBJECT + QML_NAMED_ELEMENT(TimelineManager) + QML_SINGLETON + Q_PROPERTY( bool isInitialSync MEMBER isInitialSync_ READ isInitialSync NOTIFY initialSyncChanged) Q_PROPERTY(bool isConnected READ isConnected NOTIFY isConnectedChanged) @@ -41,6 +44,25 @@ class TimelineViewManager final : public QObject public: TimelineViewManager(CallManager *callManager, ChatPage *parent = nullptr); + static TimelineViewManager *create(QQmlEngine *qmlEngine, QJSEngine *) + { + // The instance has to exist before it is used. We cannot replace it. + Q_ASSERT(instance_); + + // The engine has to have the same thread affinity as the singleton. + Q_ASSERT(qmlEngine->thread() == instance_->thread()); + + // There can only be one engine accessing the singleton. + static QJSEngine *s_engine = nullptr; + if (s_engine) + Q_ASSERT(qmlEngine == s_engine); + else + s_engine = qmlEngine; + + QJSEngine::setObjectOwnership(instance_, QJSEngine::CppOwnership); + return instance_; + } + void sync(const mtx::responses::Sync &sync_); VerificationManager *verificationManager() { return verificationManager_; } @@ -112,8 +134,8 @@ public slots: void setVideoCallItem(); - QObject *completerFor(const QString &completerName, - const QString &roomId = QLatin1String(QLatin1String(""))); + QAbstractItemModel *completerFor(const QString &completerName, + const QString &roomId = QLatin1String(QLatin1String(""))); void forwardMessageToRoom(mtx::events::collections::TimelineEvents const *e, QString roomId); RoomlistModel *rooms() { return rooms_; } @@ -123,6 +145,7 @@ private: bool isConnected_ = true; RoomlistModel *rooms_ = nullptr; + FilteredRoomlistModel *frooms_ = nullptr; CommunitiesModel *communities_ = nullptr; // don't move this above the rooms_ @@ -130,12 +153,6 @@ private: PresenceEmitter *presenceEmitter = nullptr; QHash, QColor> userColors; + + inline static TimelineViewManager *instance_ = nullptr; }; -Q_DECLARE_METATYPE(mtx::events::msg::KeyVerificationAccept) -Q_DECLARE_METATYPE(mtx::events::msg::KeyVerificationCancel) -Q_DECLARE_METATYPE(mtx::events::msg::KeyVerificationDone) -Q_DECLARE_METATYPE(mtx::events::msg::KeyVerificationKey) -Q_DECLARE_METATYPE(mtx::events::msg::KeyVerificationMac) -Q_DECLARE_METATYPE(mtx::events::msg::KeyVerificationReady) -Q_DECLARE_METATYPE(mtx::events::msg::KeyVerificationRequest) -Q_DECLARE_METATYPE(mtx::events::msg::KeyVerificationStart) diff --git a/src/ui/EventExpiry.cpp b/src/ui/EventExpiry.cpp new file mode 100644 index 00000000..ef3f0933 --- /dev/null +++ b/src/ui/EventExpiry.cpp @@ -0,0 +1,124 @@ +// SPDX-FileCopyrightText: Nheko Contributors +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "EventExpiry.h" + +#include "Cache_p.h" +#include "MainWindow.h" +#include "MatrixClient.h" +#include "timeline/TimelineModel.h" + +void +EventExpiry::load() +{ + using namespace mtx::events; + + this->event = {}; + + if (auto temp = cache::client()->getAccountData(mtx::events::EventType::NhekoEventExpiry, "")) { + auto h = std::get< + mtx::events::AccountDataEvent>( + *temp); + this->event = std::move(h.content); + } + + if (!roomid_.isEmpty()) { + if (auto temp = cache::client()->getAccountData(mtx::events::EventType::NhekoEventExpiry, + roomid_.toStdString())) { + auto h = std::get>(*temp); + this->event = std::move(h.content); + } + } + + emit expireEventsAfterDaysChanged(); + emit expireEventsAfterCountChanged(); + emit protectLatestEventsChanged(); + emit expireStateEventsChanged(); +} + +void +EventExpiry::save() +{ + if (roomid_.isEmpty()) + http::client()->put_account_data(event, [](mtx::http::RequestErr e) { + if (e) { + nhlog::net()->error("Failed to set hidden events: {}", *e); + MainWindow::instance()->showNotification( + tr("Failed to set hidden events: %1") + .arg(QString::fromStdString(e->matrix_error.error))); + } + }); + else + http::client()->put_room_account_data( + roomid_.toStdString(), event, [](mtx::http::RequestErr e) { + if (e) { + nhlog::net()->error("Failed to set hidden events: {}", *e); + MainWindow::instance()->showNotification( + tr("Failed to set hidden events: %1") + .arg(QString::fromStdString(e->matrix_error.error))); + } + }); +} + +int +EventExpiry::expireEventsAfterDays() const +{ + return event.expire_after_ms / (1000 * 60 * 60 * 24); +} + +int +EventExpiry::expireEventsAfterCount() const +{ + return event.keep_only_latest; +} + +int +EventExpiry::protectLatestEvents() const +{ + return event.protect_latest; +} + +bool +EventExpiry::expireStateEvents() const +{ + return !event.exclude_state_events; +} + +void +EventExpiry::setExpireEventsAfterDays(int val) +{ + if (val > 0) + this->event.expire_after_ms = std::uint64_t(val) * (1000 * 60 * 60 * 24); + else + this->event.expire_after_ms = 0; + emit expireEventsAfterDaysChanged(); +} + +void +EventExpiry::setProtectLatestEvents(int val) +{ + if (val > 0) + this->event.protect_latest = std::uint64_t(val); + else + this->event.expire_after_ms = 0; + emit protectLatestEventsChanged(); +} + +void +EventExpiry::setExpireEventsAfterCount(int val) +{ + if (val > 0) + this->event.keep_only_latest = std::uint64_t(val); + else + this->event.keep_only_latest = 0; + emit expireEventsAfterCountChanged(); +} + +void +EventExpiry::setExpireStateEvents(bool val) +{ + this->event.exclude_state_events = !val; + emit expireEventsAfterCountChanged(); +} diff --git a/src/ui/EventExpiry.h b/src/ui/EventExpiry.h new file mode 100644 index 00000000..378c4484 --- /dev/null +++ b/src/ui/EventExpiry.h @@ -0,0 +1,66 @@ +// SPDX-FileCopyrightText: Nheko Contributors +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include +#include +#include +#include + +#include + +class EventExpiry : public QObject +{ + Q_OBJECT + QML_ELEMENT + Q_PROPERTY(QString roomid READ roomid WRITE setRoomid NOTIFY roomidChanged REQUIRED) + Q_PROPERTY(int expireEventsAfterDays READ expireEventsAfterDays WRITE setExpireEventsAfterDays + NOTIFY expireEventsAfterDaysChanged) + Q_PROPERTY(bool expireStateEvents READ expireStateEvents WRITE setExpireStateEvents NOTIFY + expireStateEventsChanged) + Q_PROPERTY(int expireEventsAfterCount READ expireEventsAfterCount WRITE + setExpireEventsAfterCount NOTIFY expireEventsAfterCountChanged) + Q_PROPERTY(int protectLatestEvents READ protectLatestEvents WRITE setProtectLatestEvents NOTIFY + protectLatestEventsChanged) +public: + explicit EventExpiry(QObject *p = nullptr) + : QObject(p) + { + } + + Q_INVOKABLE void save(); + + [[nodiscard]] QString roomid() const { return roomid_; } + void setRoomid(const QString &r) + { + roomid_ = r; + emit roomidChanged(); + + load(); + } + + [[nodiscard]] int expireEventsAfterDays() const; + [[nodiscard]] int expireEventsAfterCount() const; + [[nodiscard]] int protectLatestEvents() const; + [[nodiscard]] bool expireStateEvents() const; + void setExpireEventsAfterDays(int); + void setExpireEventsAfterCount(int); + void setProtectLatestEvents(int); + void setExpireStateEvents(bool); + +signals: + void roomidChanged(); + + void expireEventsAfterDaysChanged(); + void expireEventsAfterCountChanged(); + void protectLatestEventsChanged(); + void expireStateEventsChanged(); + +private: + QString roomid_; + mtx::events::account_data::nheko_extensions::EventExpiry event = {}; + + void load(); +}; diff --git a/src/ui/HiddenEvents.h b/src/ui/HiddenEvents.h index bb68e0fa..4f0d23b4 100644 --- a/src/ui/HiddenEvents.h +++ b/src/ui/HiddenEvents.h @@ -5,6 +5,7 @@ #pragma once #include +#include #include #include @@ -13,6 +14,7 @@ class HiddenEvents : public QObject { Q_OBJECT + QML_ELEMENT Q_PROPERTY(QString roomid READ roomid WRITE setRoomid NOTIFY roomidChanged REQUIRED) Q_PROPERTY(QVariantList hiddenEvents READ hiddenEvents NOTIFY hiddenEventsChanged) public: diff --git a/src/ui/MxcAnimatedImage.cpp b/src/ui/MxcAnimatedImage.cpp index 7544537d..14f5dbd8 100644 --- a/src/ui/MxcAnimatedImage.cpp +++ b/src/ui/MxcAnimatedImage.cpp @@ -152,9 +152,9 @@ MxcAnimatedImage::startDownload() } void -MxcAnimatedImage::geometryChanged(const QRectF &newGeometry, const QRectF &oldGeometry) +MxcAnimatedImage::geometryChange(const QRectF &newGeometry, const QRectF &oldGeometry) { - QQuickItem::geometryChanged(newGeometry, oldGeometry); + QQuickItem::geometryChange(newGeometry, oldGeometry); if (newGeometry.size() != oldGeometry.size()) { if (height() != 0 && width() != 0) { diff --git a/src/ui/MxcAnimatedImage.h b/src/ui/MxcAnimatedImage.h index a32de6ee..c9f89764 100644 --- a/src/ui/MxcAnimatedImage.h +++ b/src/ui/MxcAnimatedImage.h @@ -15,6 +15,7 @@ class MxcAnimatedImage : public QQuickItem { Q_OBJECT + QML_ELEMENT Q_PROPERTY(TimelineModel *roomm READ room WRITE setRoom NOTIFY roomChanged REQUIRED) Q_PROPERTY(QString eventId READ eventId WRITE setEventId NOTIFY eventIdChanged) Q_PROPERTY(bool animatable READ animatable NOTIFY animatableChanged) @@ -59,7 +60,7 @@ public: } } - void geometryChanged(const QRectF &newGeometry, const QRectF &oldGeometry) override; + void geometryChange(const QRectF &newGeometry, const QRectF &oldGeometry) override; QSGNode *updatePaintNode(QSGNode *oldNode, QQuickItem::UpdatePaintNodeData *updatePaintNodeData) override; diff --git a/src/ui/MxcMediaProxy.cpp b/src/ui/MxcMediaProxy.cpp index 9a439c30..5fae0654 100644 --- a/src/ui/MxcMediaProxy.cpp +++ b/src/ui/MxcMediaProxy.cpp @@ -8,17 +8,11 @@ #include #include #include -#include #include #include #include #include -#if defined(Q_OS_MACOS) -// TODO (red_sky): Remove for Qt6. See other ifdef below -#include -#endif - #include "ChatPage.h" #include "EventAccessors.h" #include "Logging.h" @@ -31,50 +25,31 @@ MxcMediaProxy::MxcMediaProxy(QObject *parent) { connect(this, &MxcMediaProxy::eventIdChanged, &MxcMediaProxy::startDownload); connect(this, &MxcMediaProxy::roomChanged, &MxcMediaProxy::startDownload); - connect(this, - qOverload(&MxcMediaProxy::error), - [this](QMediaPlayer::Error error) { - nhlog::ui()->info("Media player error {} and errorStr {}", - error, - this->errorString().toStdString()); - }); + connect( + this, &QMediaPlayer::errorOccurred, this, [](QMediaPlayer::Error error, QString errorString) { + nhlog::ui()->debug("Media player error {} and errorStr {}", + static_cast(error), + errorString.toStdString()); + }); connect(this, &MxcMediaProxy::mediaStatusChanged, [this](QMediaPlayer::MediaStatus status) { - nhlog::ui()->info("Media player status {} and error {}", status, this->error()); + nhlog::ui()->info("Media player status {} and error {}", + static_cast(status), + static_cast(this->error())); }); - connect(this, - qOverload(&MxcMediaProxy::metaDataChanged), - [this](const QString &t, const QVariant &) { - if (t == QMediaMetaData::Orientation) - emit orientationChanged(); - }); + connect(this, &MxcMediaProxy::metaDataChanged, [this]() { emit orientationChanged(); }); connect(ChatPage::instance()->timelineManager()->rooms(), &RoomlistModel::currentRoomChanged, this, &MxcMediaProxy::pause); } -void -MxcMediaProxy::setVideoSurface(QAbstractVideoSurface *surface) -{ - if (surface != m_surface) { - qDebug() << "Changing surface"; - m_surface = surface; - setVideoOutput(m_surface); - emit videoSurfaceChanged(); - } -} - -QAbstractVideoSurface * -MxcMediaProxy::getVideoSurface() -{ - return m_surface; -} int MxcMediaProxy::orientation() const { - nhlog::ui()->debug("metadata: {}", availableMetaData().join(QStringLiteral(",")).toStdString()); - auto orientation = metaData(QMediaMetaData::Orientation).toInt(); + // nhlog::ui()->debug("metadata: {}", + // availableMetaData().join(QStringLiteral(",")).toStdString()); + auto orientation = metaData().value(QMediaMetaData::Orientation).toInt(); nhlog::ui()->debug("Video orientation: {}", orientation); return orientation; } @@ -135,34 +110,10 @@ MxcMediaProxy::startDownload() buffer.open(QIODevice::ReadOnly); buffer.reset(); - QTimer::singleShot(0, this, [this, filename, suffix, encryptionInfo] { -#if defined(Q_OS_MACOS) - if (encryptionInfo) { - // macOS has issues reading from a buffer in setMedia for whatever reason. - // Instead, write the buffer to a temporary file and read from that. - // This should be fixed in Qt6, so update this when we do that! - // TODO: REMOVE IN QT6 - QTemporaryFile tempFile; - tempFile.setFileTemplate(tempFile.fileTemplate() + QLatin1Char('.') + suffix); - tempFile.open(); - tempFile.write(buffer.data()); - tempFile.close(); - nhlog::ui()->debug("Playing media from temp buffer file: {}. Remove in QT6!", - filename.filePath().toStdString()); - this->setMedia(QUrl::fromLocalFile(tempFile.fileName())); - } else { - nhlog::ui()->info( - "Playing buffer with size: {}, {}", buffer.bytesAvailable(), buffer.isOpen()); - this->setMedia(QUrl::fromLocalFile(filename.filePath())); - } -#else - Q_UNUSED(suffix) - Q_UNUSED(encryptionInfo) - + QTimer::singleShot(0, this, [this, filename] { nhlog::ui()->info( "Playing buffer with size: {}, {}", buffer.bytesAvailable(), buffer.isOpen()); - this->setMedia(QMediaContent(filename.fileName()), &buffer); -#endif + this->setSourceDevice(&buffer, QUrl(filename.fileName())); emit loadedChanged(); }); }; diff --git a/src/ui/MxcMediaProxy.h b/src/ui/MxcMediaProxy.h index 48de5a4f..d245dcae 100644 --- a/src/ui/MxcMediaProxy.h +++ b/src/ui/MxcMediaProxy.h @@ -4,13 +4,14 @@ #pragma once -#include #include -#include #include #include #include +#include #include +#include +#include #include "Logging.h" @@ -21,10 +22,10 @@ class TimelineModel; class MxcMediaProxy : public QMediaPlayer { Q_OBJECT + QML_NAMED_ELEMENT(MxcMedia) + Q_PROPERTY(TimelineModel *roomm READ room WRITE setRoom NOTIFY roomChanged REQUIRED) Q_PROPERTY(QString eventId READ eventId WRITE setEventId NOTIFY eventIdChanged) - Q_PROPERTY(QAbstractVideoSurface *videoSurface READ getVideoSurface WRITE setVideoSurface NOTIFY - videoSurfaceChanged) Q_PROPERTY(bool loaded READ loaded NOTIFY loadedChanged) Q_PROPERTY(int orientation READ orientation NOTIFY orientationChanged) @@ -44,16 +45,13 @@ public: room_ = room; emit roomChanged(); } - void setVideoSurface(QAbstractVideoSurface *surface); - QAbstractVideoSurface *getVideoSurface(); - int orientation() const; signals: void roomChanged(); void eventIdChanged(); void loadedChanged(); - void newBuffer(QMediaContent, QIODevice *buf); + void newBuffer(QUrl, QIODevice *buf); void orientationChanged(); void videoSurfaceChanged(); @@ -66,5 +64,5 @@ private: QString eventId_; QString filename_; QBuffer buffer; - QAbstractVideoSurface *m_surface = nullptr; + QObject *m_surface = nullptr; }; diff --git a/src/ui/NhekoCursorShape.h b/src/ui/NhekoCursorShape.h index 84d56fad..123852f9 100644 --- a/src/ui/NhekoCursorShape.h +++ b/src/ui/NhekoCursorShape.h @@ -12,7 +12,7 @@ class NhekoCursorShape : public QQuickItem { Q_OBJECT - + QML_ELEMENT Q_PROPERTY( Qt::CursorShape cursorShape READ cursorShape WRITE setCursorShape NOTIFY cursorShapeChanged) diff --git a/src/ui/NhekoDropArea.h b/src/ui/NhekoDropArea.h index 91116844..46a02da5 100644 --- a/src/ui/NhekoDropArea.h +++ b/src/ui/NhekoDropArea.h @@ -7,6 +7,7 @@ class NhekoDropArea : public QQuickItem { Q_OBJECT + QML_ELEMENT Q_PROPERTY(QString roomid READ roomid WRITE setRoomid NOTIFY roomidChanged) public: NhekoDropArea(QQuickItem *parent = nullptr); diff --git a/src/ui/NhekoEventObserver.cpp b/src/ui/NhekoEventObserver.cpp deleted file mode 100644 index 713a0733..00000000 --- a/src/ui/NhekoEventObserver.cpp +++ /dev/null @@ -1,60 +0,0 @@ -// SPDX-FileCopyrightText: Nheko Contributors -// -// SPDX-License-Identifier: GPL-3.0-or-later - -#include "NhekoEventObserver.h" - -#include - -#include "Logging.h" - -NhekoEventObserver::NhekoEventObserver(QQuickItem *parent) - : QQuickItem(parent) -{ - setFiltersChildMouseEvents(true); -} - -bool -NhekoEventObserver::childMouseEventFilter(QQuickItem * /*item*/, QEvent *event) -{ - // nhlog::ui()->debug("Touched {}", item->metaObject()->className()); - - auto setTouched = [this](bool touched) { - if (touched != this->wasTouched_) { - this->wasTouched_ = touched; - emit wasTouchedChanged(); - } - }; - - // see - // https://code.qt.io/cgit/qt/qtdeclarative.git/tree/src/quicktemplates2/qquickscrollview.cpp?id=7f29e89c26ae2babc358b1c4e6f965af6ec759f4#n471 - switch (event->type()) { - case QEvent::TouchBegin: - case QEvent::TouchEnd: - setTouched(true); - break; - - case QEvent::MouseButtonPress: - if (static_cast(event)->source() == Qt::MouseEventNotSynthesized) { - setTouched(false); - } - break; - - case QEvent::MouseMove: - case QEvent::MouseButtonRelease: - if (static_cast(event)->source() == Qt::MouseEventNotSynthesized) - setTouched(false); - break; - - case QEvent::HoverEnter: - case QEvent::HoverMove: - case QEvent::Wheel: - setTouched(false); - break; - - default: - break; - } - - return false; -} diff --git a/src/ui/NhekoEventObserver.h b/src/ui/NhekoEventObserver.h deleted file mode 100644 index 63739d4a..00000000 --- a/src/ui/NhekoEventObserver.h +++ /dev/null @@ -1,27 +0,0 @@ -// SPDX-FileCopyrightText: Nheko Contributors -// -// SPDX-License-Identifier: GPL-3.0-or-later - -#pragma once - -#include - -class NhekoEventObserver : public QQuickItem -{ - Q_OBJECT - - Q_PROPERTY(bool wasTouched READ wasTouched NOTIFY wasTouchedChanged) - -public: - explicit NhekoEventObserver(QQuickItem *parent = 0); - - bool childMouseEventFilter(QQuickItem *item, QEvent *event) override; - -private: - bool wasTouched() { return wasTouched_; } - - bool wasTouched_ = false; - -signals: - void wasTouchedChanged(); -}; diff --git a/src/ui/NhekoGlobalObject.cpp b/src/ui/NhekoGlobalObject.cpp index a6f9abe7..1bab73b5 100644 --- a/src/ui/NhekoGlobalObject.cpp +++ b/src/ui/NhekoGlobalObject.cpp @@ -6,15 +6,11 @@ #include #include +#include #include #include #include -// for some reason that is not installed in our macOS env... -#ifndef Q_OS_MAC -#include -#endif - #include "Cache_p.h" #include "ChatPage.h" #include "Logging.h" @@ -22,6 +18,10 @@ #include "Utils.h" #include "voip/WebRTCSession.h" +#if XCB_AVAILABLE +#include +#endif + Nheko::Nheko() { connect( @@ -186,7 +186,30 @@ Nheko::createRoom(bool space, void Nheko::setWindowRole([[maybe_unused]] QWindow *win, [[maybe_unused]] QString newRole) const { -#ifndef Q_OS_MAC - QXcbWindowFunctions::setWmWindowRole(win, newRole.toUtf8()); +#if XCB_AVAILABLE + const QNativeInterface::QX11Application *x11Interface = + qGuiApp->nativeInterface(); + + if (!x11Interface) + return; + + auto connection = x11Interface->connection(); + + auto role = newRole.toStdString(); + + char WM_WINDOW_ROLE[] = "WM_WINDOW_ROLE"; + auto cookie = xcb_intern_atom(connection, false, std::size(WM_WINDOW_ROLE) - 1, WM_WINDOW_ROLE); + xcb_intern_atom_reply_t *reply = xcb_intern_atom_reply(connection, cookie, nullptr); + auto atom = reply->atom; + free(reply); + + xcb_change_property(connection, + XCB_PROP_MODE_REPLACE, + win->winId(), + atom, + XCB_ATOM_STRING, + 8, + role.size(), + role.data()); #endif } diff --git a/src/ui/NhekoGlobalObject.h b/src/ui/NhekoGlobalObject.h index b7a7a637..91210c54 100644 --- a/src/ui/NhekoGlobalObject.h +++ b/src/ui/NhekoGlobalObject.h @@ -7,6 +7,7 @@ #include #include #include +#include #include #include "AliasEditModel.h" @@ -19,6 +20,9 @@ class Nheko final : public QObject { Q_OBJECT + QML_ELEMENT + QML_SINGLETON + Q_PROPERTY(QPalette colors READ colors NOTIFY colorsChanged) Q_PROPERTY(QPalette inactiveColors READ inactiveColors NOTIFY colorsChanged) Q_PROPERTY(Theme theme READ theme NOTIFY colorsChanged) diff --git a/src/ui/RoomSettings.cpp b/src/ui/RoomSettings.cpp index 97c7cd61..769f2c8d 100644 --- a/src/ui/RoomSettings.cpp +++ b/src/ui/RoomSettings.cpp @@ -74,6 +74,11 @@ RoomSettings::RoomSettings(QString roomid, QObject *parent) guestRules_ = info_.guest_access ? AccessState::CanJoin : AccessState::Forbidden; emit accessJoinRulesChanged(); + if (auto ev = cache::client()->getStateEvent( + roomid_.toStdString())) { + this->historyVisibility_ = ev->content.history_visibility; + } + this->allowedRoomsModel = new RoomSettingsAllowedRoomsModel(this); } @@ -151,6 +156,22 @@ RoomSettings::notifications() return notifications_; } +RoomSettings::Visibility +RoomSettings::historyVisibility() const +{ + switch (this->historyVisibility_) { + case mtx::events::state::Visibility::WorldReadable: + return WorldReadable; + case mtx::events::state::Visibility::Joined: + return Joined; + case mtx::events::state::Visibility::Invited: + return Invited; + case mtx::events::state::Visibility::Shared: + return Shared; + } + return Shared; +} + bool RoomSettings::privateAccess() const { @@ -278,6 +299,20 @@ RoomSettings::canChangeAvatar() const return false; } +bool +RoomSettings::canChangeHistoryVisibility() const +{ + try { + return cache::hasEnoughPowerLevel({EventType::RoomHistoryVisibility}, + roomid_.toStdString(), + utils::localUser().toStdString()); + } catch (const lmdb::error &e) { + nhlog::db()->warn("lmdb error: {}", e.what()); + } + + return false; +} + bool RoomSettings::isEncryptionEnabled() const { @@ -457,6 +492,52 @@ RoomSettings::changeName(const QString &name) }); } +void +RoomSettings::changeHistoryVisibility(Visibility value) +{ + auto tempVis = mtx::events::state::Visibility::Shared; + + switch (value) { + case WorldReadable: + tempVis = mtx::events::state::Visibility::WorldReadable; + break; + case Joined: + tempVis = mtx::events::state::Visibility::Joined; + break; + case Invited: + tempVis = mtx::events::state::Visibility::Invited; + break; + case Shared: + tempVis = mtx::events::state::Visibility::Shared; + break; + default: + return; + } + + using namespace mtx::events; + auto proxy = std::make_shared(); + connect(proxy.get(), &ThreadProxy::eventSent, this, [this, tempVis]() { + this->historyVisibility_ = tempVis; + emit historyVisibilityChanged(); + }); + connect(proxy.get(), &ThreadProxy::error, this, &RoomSettings::displayError); + + state::HistoryVisibility body; + body.history_visibility = tempVis; + + http::client()->send_state_event( + roomid_.toStdString(), + body, + [proxy](const mtx::responses::EventId &, mtx::http::RequestErr err) { + if (err) { + emit proxy->error(QString::fromStdString(err->matrix_error.error)); + return; + } + + emit proxy->eventSent(); + }); +} + void RoomSettings::changeTopic(const QString &topic) { diff --git a/src/ui/RoomSettings.h b/src/ui/RoomSettings.h index f8d0857c..5ca1e997 100644 --- a/src/ui/RoomSettings.h +++ b/src/ui/RoomSettings.h @@ -6,6 +6,7 @@ #include #include +#include #include #include @@ -13,6 +14,7 @@ #include #include +#include #include "CacheStructs.h" @@ -26,6 +28,7 @@ signals: void error(const QString &msg); void nameEventSent(const QString &); void topicEventSent(const QString &); + void eventSent(); void stopLoading(); }; @@ -69,6 +72,9 @@ private: class RoomSettings final : public QObject { Q_OBJECT + QML_ELEMENT + QML_UNCREATABLE("") + Q_PROPERTY(QString roomId READ roomId CONSTANT) Q_PROPERTY(QString roomVersion READ roomVersion CONSTANT) Q_PROPERTY(QString roomName READ roomName NOTIFY roomNameChanged) @@ -78,6 +84,8 @@ class RoomSettings final : public QObject Q_PROPERTY(QString roomAvatarUrl READ roomAvatarUrl NOTIFY avatarUrlChanged) Q_PROPERTY(int memberCount READ memberCount CONSTANT) Q_PROPERTY(int notifications READ notifications NOTIFY notificationsChanged) + Q_PROPERTY(Visibility historyVisibility READ historyVisibility WRITE changeHistoryVisibility + NOTIFY historyVisibilityChanged) Q_PROPERTY(bool privateAccess READ privateAccess NOTIFY accessJoinRulesChanged) Q_PROPERTY(bool guestAccess READ guestAccess NOTIFY accessJoinRulesChanged) Q_PROPERTY(bool knockingEnabled READ knockingEnabled NOTIFY accessJoinRulesChanged) @@ -87,6 +95,7 @@ class RoomSettings final : public QObject Q_PROPERTY(bool canChangeJoinRules READ canChangeJoinRules CONSTANT) Q_PROPERTY(bool canChangeName READ canChangeName CONSTANT) Q_PROPERTY(bool canChangeTopic READ canChangeTopic CONSTANT) + Q_PROPERTY(bool canChangeHistoryVisibility READ canChangeHistoryVisibility CONSTANT) Q_PROPERTY(bool isEncryptionEnabled READ isEncryptionEnabled NOTIFY encryptionChanged) Q_PROPERTY(bool supportsKnocking READ supportsKnocking CONSTANT) Q_PROPERTY(bool supportsRestricted READ supportsRestricted CONSTANT) @@ -98,6 +107,16 @@ class RoomSettings final : public QObject bool allowedRoomsModified READ allowedRoomsModified NOTIFY allowedRoomsModifiedChanged) public: + // match mtx::events::state::Visibility + enum Visibility + { + WorldReadable, + Shared, + Invited, + Joined, + }; + Q_ENUM(Visibility) + RoomSettings(QString roomid, QObject *parent = nullptr); QString roomId() const; @@ -122,6 +141,7 @@ public: bool canChangeTopic() const; //! Whether the user has enough power level to send m.room.avatar event. bool canChangeAvatar() const; + bool canChangeHistoryVisibility() const; bool isEncryptionEnabled() const; bool supportsKnocking() const; bool supportsRestricted() const; @@ -130,6 +150,9 @@ public: void setAllowedRooms(QStringList rooms); bool allowedRoomsModified() const { return allowedRoomsModified_; } + Visibility historyVisibility() const; + Q_INVOKABLE void changeHistoryVisibility(Visibility visibility); + Q_INVOKABLE void enableEncryption(); Q_INVOKABLE void updateAvatar(); Q_INVOKABLE void changeAccessRules(bool private_, @@ -153,6 +176,7 @@ signals: void allowedRoomsChanged(); void displayError(const QString &errorMessage); void allowedRoomsModifiedChanged(); + void historyVisibilityChanged(); public slots: void stopLoading(); @@ -173,7 +197,8 @@ private: int notifications_ = 0; mtx::events::state::JoinRules accessRules_; - mtx::events::state::AccessState guestRules_ = mtx::events::state::AccessState::Forbidden; + mtx::events::state::Visibility historyVisibility_ = mtx::events::state::Visibility::Shared; + mtx::events::state::AccessState guestRules_ = mtx::events::state::AccessState::Forbidden; RoomSettingsAllowedRoomsModel *allowedRoomsModel; }; diff --git a/src/ui/RoomSummary.h b/src/ui/RoomSummary.h index c02ea5d5..8225f0ae 100644 --- a/src/ui/RoomSummary.h +++ b/src/ui/RoomSummary.h @@ -7,6 +7,7 @@ #include #include +#include #include @@ -25,6 +26,9 @@ class RoomSummary final : public QObject { Q_OBJECT + QML_ELEMENT + QML_UNCREATABLE("Please use joinRoom to create a room summary.") + Q_PROPERTY(QString reason READ reason WRITE setReason NOTIFY reasonChanged) Q_PROPERTY(QString roomid READ roomid NOTIFY loaded) diff --git a/src/ui/Theme.cpp b/src/ui/Theme.cpp index 48cb8bce..159fc2ae 100644 --- a/src/ui/Theme.cpp +++ b/src/ui/Theme.cpp @@ -4,12 +4,9 @@ #include "Theme.h" -Q_DECLARE_METATYPE(Theme) - QPalette Theme::paletteFromTheme(QStringView theme) { - [[maybe_unused]] static auto meta = qRegisterMetaType("Theme"); static QPalette original; if (theme == u"light") { static QPalette lightActive = [] { diff --git a/src/ui/Theme.h b/src/ui/Theme.h index 0293bc28..d581ffe4 100644 --- a/src/ui/Theme.h +++ b/src/ui/Theme.h @@ -6,10 +6,13 @@ #include #include +#include class Theme final : public QPalette { Q_GADGET + QML_ANONYMOUS + Q_PROPERTY(QColor sidebarBackground READ sidebarBackground CONSTANT) Q_PROPERTY(QColor alternateButton READ alternateButton CONSTANT) Q_PROPERTY(QColor separator READ separator CONSTANT) diff --git a/src/ui/UIA.h b/src/ui/UIA.h index 7d23d88e..414cb804 100644 --- a/src/ui/UIA.h +++ b/src/ui/UIA.h @@ -5,6 +5,7 @@ #pragma once #include +#include #include @@ -12,10 +13,31 @@ class UIA final : public QObject { Q_OBJECT + QML_ELEMENT + QML_SINGLETON + Q_PROPERTY(QString title READ title NOTIFY titleChanged) public: static UIA *instance(); + static UIA *create(QQmlEngine *qmlEngine, QJSEngine *) + { + // The instance has to exist before it is used. We cannot replace it. + Q_ASSERT(instance()); + + // The engine has to have the same thread affinity as the singleton. + Q_ASSERT(qmlEngine->thread() == instance()->thread()); + + // There can only be one engine accessing the singleton. + static QJSEngine *s_engine = nullptr; + if (s_engine) + Q_ASSERT(qmlEngine == s_engine); + else + s_engine = qmlEngine; + + QJSEngine::setObjectOwnership(instance(), QJSEngine::CppOwnership); + return instance(); + } UIA(QObject *parent = nullptr) : QObject(parent) diff --git a/src/ui/UserProfile.h b/src/ui/UserProfile.h index a880f320..d8e06aa1 100644 --- a/src/ui/UserProfile.h +++ b/src/ui/UserProfile.h @@ -6,6 +6,7 @@ #include #include +#include #include #include #include @@ -16,6 +17,7 @@ namespace verification { Q_NAMESPACE +QML_NAMED_ELEMENT(VerificationStatus) enum Status { diff --git a/src/voip/CallManager.cpp b/src/voip/CallManager.cpp index 993937f0..5479ba31 100644 --- a/src/voip/CallManager.cpp +++ b/src/voip/CallManager.cpp @@ -10,7 +10,6 @@ #include #include -#include #include #include "Cache.h" @@ -42,10 +41,6 @@ extern "C" } #endif -Q_DECLARE_METATYPE(std::vector) -Q_DECLARE_METATYPE(mtx::events::voip::CallCandidates::Candidate) -Q_DECLARE_METATYPE(mtx::responses::TurnServer) - using namespace mtx::events; using namespace mtx::events::voip; @@ -59,15 +54,32 @@ std::vector getTurnURIs(const mtx::responses::TurnServer &turnServer); } +CallManager * +CallManager::create(QQmlEngine *qmlEngine, QJSEngine *) +{ + // The instance has to exist before it is used. We cannot replace it. + auto instance = ChatPage::instance()->callManager(); + Q_ASSERT(instance); + + // The engine has to have the same thread affinity as the singleton. + Q_ASSERT(qmlEngine->thread() == instance->thread()); + + // There can only be one engine accessing the singleton. + static QJSEngine *s_engine = nullptr; + if (s_engine) + Q_ASSERT(qmlEngine == s_engine); + else + s_engine = qmlEngine; + + QJSEngine::setObjectOwnership(instance, QJSEngine::CppOwnership); + return instance; +} + CallManager::CallManager(QObject *parent) : QObject(parent) , session_(WebRTCSession::instance()) , turnServerTimer_(this) { - qRegisterMetaType>(); - qRegisterMetaType(); - qRegisterMetaType(); - #ifdef GSTREAMER_AVAILABLE std::string errorMessage; if (session_.havePlugins(true, true, ScreenShareType::XDP, &errorMessage)) { @@ -180,9 +192,9 @@ CallManager::CallManager(QObject *parent) }); connect(&player_, - QOverload::of(&QMediaPlayer::error), + &QMediaPlayer::errorOccurred, this, - [this](QMediaPlayer::Error error) { + [this](QMediaPlayer::Error error, QString errorString) { stopRingtone(); switch (error) { case QMediaPlayer::FormatError: @@ -193,7 +205,8 @@ CallManager::CallManager(QObject *parent) nhlog::ui()->error("WebRTC: access to ringtone file denied"); break; default: - nhlog::ui()->error("WebRTC: unable to play ringtone"); + nhlog::ui()->error("WebRTC: unable to play ringtone, {}", + errorString.toStdString()); break; } }); @@ -827,19 +840,16 @@ CallManager::retrieveTurnServer() 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); + player_.setLoops(repeat ? QMediaPlayer::Infinite : 1); + player_.setSource(ringtone); + // player_.audioOutput()->setVolume(100); + player_.play(); } void CallManager::stopRingtone() { - player_.setPlaylist(nullptr); + player_.stop(); } bool diff --git a/src/voip/CallManager.h b/src/voip/CallManager.h index bbc7a903..c0fd0831 100644 --- a/src/voip/CallManager.h +++ b/src/voip/CallManager.h @@ -9,6 +9,7 @@ #include #include +#include #include #include #include @@ -26,9 +27,13 @@ struct TurnServer; class QUrl; -class CallManager final : public QObject +class CallManager : public QObject { Q_OBJECT + + QML_ELEMENT + QML_SINGLETON + Q_PROPERTY(bool haveCallInvite READ haveCallInvite NOTIFY newInviteState) Q_PROPERTY(bool isOnCall READ isOnCall NOTIFY newCallState) Q_PROPERTY(bool isOnCallOnOtherDevice READ isOnCallOnOtherDevice NOTIFY newCallDeviceState) @@ -49,6 +54,8 @@ class CallManager final : public QObject public: CallManager(QObject *); + static CallManager *create(QQmlEngine *qmlEngine, QJSEngine *); + bool haveCallInvite() const { return haveCallInvite_; } bool isOnCall() const { return (session_.state() != webrtc::State::DISCONNECTED); } bool isOnCallOnOtherDevice() const { return (isOnCallOnOtherDevice_ != ""); } diff --git a/src/voip/ScreenCastPortal.cpp b/src/voip/ScreenCastPortal.cpp index 31cddba0..6cd91e51 100644 --- a/src/voip/ScreenCastPortal.cpp +++ b/src/voip/ScreenCastPortal.cpp @@ -438,8 +438,6 @@ struct PipeWireStream QVariantMap map; }; -Q_DECLARE_METATYPE(PipeWireStream) - const QDBusArgument & operator>>(const QDBusArgument &argument, PipeWireStream &stream) { diff --git a/src/voip/WebRTCSession.cpp b/src/voip/WebRTCSession.cpp index c0cab4ac..ff459bf9 100644 --- a/src/voip/WebRTCSession.cpp +++ b/src/voip/WebRTCSession.cpp @@ -41,10 +41,6 @@ extern "C" // https://github.com/vector-im/riot-web/issues/10173 #define STUN_SERVER "stun://turn.matrix.org:3478" -Q_DECLARE_METATYPE(webrtc::CallType) -Q_DECLARE_METATYPE(webrtc::ScreenShareType) -Q_DECLARE_METATYPE(webrtc::State) - using webrtc::CallType; using webrtc::ScreenShareType; using webrtc::State; @@ -52,29 +48,26 @@ using webrtc::State; WebRTCSession::WebRTCSession() : devices_(CallDevices::instance()) { - qRegisterMetaType(); - qmlRegisterUncreatableMetaObject(webrtc::staticMetaObject, - "im.nheko", - 1, - 0, - "CallType", - QStringLiteral("Can't instantiate enum")); + // qmlRegisterUncreatableMetaObject(webrtc::staticMetaObject, + // "im.nheko", + // 1, + // 0, + // "CallType", + // QStringLiteral("Can't instantiate enum")); - qRegisterMetaType(); - qmlRegisterUncreatableMetaObject(webrtc::staticMetaObject, - "im.nheko", - 1, - 0, - "ScreenShareType", - QStringLiteral("Can't instantiate enum")); + // qmlRegisterUncreatableMetaObject(webrtc::staticMetaObject, + // "im.nheko", + // 1, + // 0, + // "ScreenShareType", + // QStringLiteral("Can't instantiate enum")); - qRegisterMetaType(); - qmlRegisterUncreatableMetaObject(webrtc::staticMetaObject, - "im.nheko", - 1, - 0, - "WebRTCState", - QStringLiteral("Can't instantiate enum")); + // qmlRegisterUncreatableMetaObject(webrtc::staticMetaObject, + // "im.nheko", + // 1, + // 0, + // "WebRTCState", + // QStringLiteral("Can't instantiate enum")); connect(this, &WebRTCSession::stateChanged, this, &WebRTCSession::setState); init(); diff --git a/src/voip/WebRTCSession.h b/src/voip/WebRTCSession.h index 82753372..3357bff7 100644 --- a/src/voip/WebRTCSession.h +++ b/src/voip/WebRTCSession.h @@ -8,6 +8,7 @@ #include #include +#include #include "mtx/events/voip.hpp" @@ -17,6 +18,7 @@ class QQuickItem; namespace webrtc { Q_NAMESPACE +QML_NAMED_ELEMENT(Voip) enum class CallType {