diff --git a/.ci/format.sh b/.ci/format.sh index d3b629c3bfce13c7a789ea1b687514fbf137e52e..e1e6c1e41e78c6473f4a50136c1501439c9d92bc 100755 --- a/.ci/format.sh +++ b/.ci/format.sh @@ -11,5 +11,7 @@ FILES=$(find src -type f -type f \( -iname "*.cpp" -o -iname "*.h" \)) for f in $FILES do - clang-format -i "$f" && git diff --exit-code + clang-format -i "$f" done; + +git diff --exit-code diff --git a/.ci/install.sh b/.ci/install.sh index c1f4235736b178a5ef4977c6d15f2901bf192d95..2c7c71e3f9309da4c1b76f6ef5d5f1f7c034da10 100755 --- a/.ci/install.sh +++ b/.ci/install.sh @@ -31,8 +31,8 @@ if [ "$TRAVIS_OS_NAME" = "linux" ]; then QT_PKG="59" fi - wget https://cmake.org/files/v3.12/cmake-3.12.2-Linux-x86_64.sh - sudo sh cmake-3.12.2-Linux-x86_64.sh --skip-license --prefix=/usr/local + wget https://cmake.org/files/v3.15/cmake-3.15.5-Linux-x86_64.sh + sudo sh cmake-3.15.5-Linux-x86_64.sh --skip-license --prefix=/usr/local mkdir -p build-libsodium ( cd build-libsodium @@ -54,5 +54,7 @@ if [ "$TRAVIS_OS_NAME" = "linux" ]; then qt${QT_PKG}tools \ qt${QT_PKG}svg \ qt${QT_PKG}multimedia \ + qt${QT_PKG}quickcontrols2 \ + qt${QT_PKG}graphicaleffects \ liblmdb-dev fi diff --git a/.ci/linux/deploy.sh b/.ci/linux/deploy.sh index 2caf5e0f8a6ad7127d2be0a08b79a5471899ed50..524d72d50db1f68d7c089b5d8b4db76ab4eda00f 100755 --- a/.ci/linux/deploy.sh +++ b/.ci/linux/deploy.sh @@ -44,8 +44,7 @@ do linuxdeployqt=$res done -./"$linuxdeployqt" ${DIR}/usr/share/applications/*.desktop -unsupported-allow-new-glibc -bundle-non-qt-libs -./"$linuxdeployqt" ${DIR}/usr/share/applications/*.desktop -unsupported-allow-new-glibc -appimage +./"$linuxdeployqt" ${DIR}/usr/share/applications/*.desktop -unsupported-allow-new-glibc -bundle-non-qt-libs -qmldir=./resources/qml -appimage chmod +x nheko-*x86_64.AppImage diff --git a/.ci/macos/deploy.sh b/.ci/macos/deploy.sh index ee4acaede401126ecbfd87a9a277e4fbb1b87086..1dc9472d7bfac5f975388d6611f4a1f525c90ad0 100755 --- a/.ci/macos/deploy.sh +++ b/.ci/macos/deploy.sh @@ -16,7 +16,7 @@ PATH=/usr/local/opt/qt/bin/:${PATH} mkdir -p nheko.app/Contents/Frameworks find "${ICU_LIB}" -type l -name "*.dylib" -exec cp -a -n {} nheko.app/Contents/Frameworks/ \; || true - sudo macdeployqt nheko.app -dmg -always-overwrite + sudo macdeployqt nheko.app -dmg -always-overwrite -qmldir=../resources/qml/ user=$(id -nu) sudo chown "${user}" nheko.dmg diff --git a/.ci/script.sh b/.ci/script.sh index ac6bfed60fbf2bd0ab9cd8de506b415af1b373c7..06536278e3b8dbea4b4ef14166712a76966c5394 100755 --- a/.ci/script.sh +++ b/.ci/script.sh @@ -13,6 +13,9 @@ if [ "$TRAVIS_OS_NAME" = "linux" ]; then sudo update-alternatives --set gcc "/usr/bin/${C_COMPILER}" sudo update-alternatives --set g++ "/usr/bin/${CXX_COMPILER}" + + export PATH="/usr/local/bin/:${PATH}" + cmake --version fi if [ "$TRAVIS_OS_NAME" = "linux" ]; then @@ -35,7 +38,8 @@ cmake --build .deps # Build nheko cmake -GNinja -H. -Bbuild \ -DCMAKE_BUILD_TYPE=RelWithDebInfo \ - -DCMAKE_INSTALL_PREFIX=.deps/usr + -DCMAKE_INSTALL_PREFIX=.deps/usr \ + -DBUILD_SHARED_LIBS=ON # weird workaround, as the boost 1.70 cmake files seem to be broken? cmake --build build if [ "$TRAVIS_OS_NAME" = "osx" ]; then diff --git a/.gitignore b/.gitignore index 43c9b7b4aa81f8ef44e27a0d860aa22f20aa9113..2d772e581911128df766a031e84aecf286c6b954 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,11 @@ -build +/build* tags cscope* .clang_complete *wintoastlib* +/.ccls-cache +/.exrc +.gdb_history # GTAGS GTAGS @@ -49,6 +52,10 @@ ui_*.h *.qmlproject.user *.qmlproject.user.* +# Vim +*.swp +*.swo + #####=== CMake ===##### CMakeCache.txt diff --git a/.travis.yml b/.travis.yml index 6a699043b0fff4f2674f9fe64bdf500d6cf86675..4ab6408a83c8290ad7ef205349ce2d2088467da3 100644 --- a/.travis.yml +++ b/.travis.yml @@ -15,7 +15,8 @@ matrix: include: - os: osx compiler: clang - osx_image: xcode9 + # Use the default osx image, because that one is actually tested to work with homebrew and probably the oldest supported version + # osx_image: xcode9 env: - DEPLOYMENT=1 - USE_BUNDLED_BOOST=0 @@ -42,8 +43,8 @@ matrix: env: - CXX_COMPILER=g++-8 - C_COMPILER=gcc-8 - - QT_VERSION=571 - - QT_PKG=57 + - QT_VERSION=592 + - QT_PKG=59 - USE_BUNDLED_BOOST=1 - USE_BUNDLED_CMARK=1 - USE_BUNDLED_JSON=1 diff --git a/CMakeLists.txt b/CMakeLists.txt index 4d5aff7aa49e9deac2c447c1912569f528f37c55..67a1dfb09c1074ddcdc27bbfb286669e809a9b79 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -69,7 +69,8 @@ include(LMDB) # # Discover Qt dependencies. # -find_package(Qt5 COMPONENTS Core Widgets LinguistTools Concurrent Svg Multimedia REQUIRED) +find_package(Qt5 COMPONENTS Core Widgets LinguistTools Concurrent Svg Multimedia Qml QuickControls2 QuickWidgets REQUIRED) +find_package(Qt5QuickCompiler) find_package(Qt5DBus) if (APPLE) @@ -192,12 +193,8 @@ set(SRC_FILES # Timeline src/timeline/TimelineViewManager.cpp - src/timeline/TimelineItem.cpp - src/timeline/TimelineView.cpp - src/timeline/widgets/AudioItem.cpp - src/timeline/widgets/FileItem.cpp - src/timeline/widgets/ImageItem.cpp - src/timeline/widgets/VideoItem.cpp + src/timeline/TimelineModel.cpp + src/timeline/DelegateChooser.cpp # UI components src/ui/Avatar.cpp @@ -229,6 +226,8 @@ set(SRC_FILES src/Logging.cpp src/MainWindow.cpp src/MatrixClient.cpp + src/MxcImageProvider.cpp + src/ColorImageProvider.cpp src/QuickSwitcher.cpp src/Olm.cpp src/RegisterPage.cpp @@ -260,7 +259,7 @@ include(FeatureSummary) set(Boost_USE_STATIC_LIBS OFF) set(Boost_USE_STATIC_RUNTIME OFF) set(Boost_USE_MULTITHREADED ON) -find_package(Boost 1.66 REQUIRED +find_package(Boost 1.70 REQUIRED COMPONENTS atomic chrono date_time @@ -333,13 +332,9 @@ qt5_wrap_cpp(MOC_HEADERS src/emoji/PickButton.h # Timeline - src/timeline/TimelineItem.h - src/timeline/TimelineView.h src/timeline/TimelineViewManager.h - src/timeline/widgets/AudioItem.h - src/timeline/widgets/FileItem.h - src/timeline/widgets/ImageItem.h - src/timeline/widgets/VideoItem.h + src/timeline/TimelineModel.h + src/timeline/DelegateChooser.h # UI components src/ui/Avatar.h @@ -370,7 +365,7 @@ qt5_wrap_cpp(MOC_HEADERS src/CommunitiesList.h src/LoginPage.h src/MainWindow.h - src/MatrixClient.h + src/MxcImageProvider.h src/InviteeItem.h src/QuickSwitcher.h src/RegisterPage.h @@ -405,6 +400,9 @@ set(COMMON_LIBS Qt5::Svg Qt5::Concurrent Qt5::Multimedia + Qt5::Qml + Qt5::QuickControls2 + Qt5::QuickWidgets nlohmann_json::nlohmann_json) if(APPVEYOR_BUILD) @@ -448,6 +446,7 @@ if(APPLE) target_link_libraries (nheko ${NHEKO_LIBS} Qt5::MacExtras) elseif(WIN32) add_executable (nheko ${OS_BUNDLE} ${ICON_FILE} ${NHEKO_DEPS}) + target_compile_definitions(nheko PRIVATE WIN32_LEAN_AND_MEAN) target_link_libraries (nheko ${NTDLIB} ${NHEKO_LIBS} Qt5::WinMain) else() add_executable (nheko ${OS_BUNDLE} ${NHEKO_DEPS}) diff --git a/Dockerfile b/Dockerfile index 2e01b40bfce8df0b95238e2fbff74902bc46b8aa..dddd1c6f71c996407b286d27b19b69753f5ad640 100644 --- a/Dockerfile +++ b/Dockerfile @@ -7,7 +7,7 @@ RUN \ add-apt-repository -y ppa:ubuntu-toolchain-r/test && \ apt-get update -qq && \ apt-get install -y \ - qt510base qt510tools qt510svg qt510multimedia \ + qt510base qt510tools qt510svg qt510multimedia qt510quickcontrols2 qt510graphicaleffects \ gcc-5 g++-5 RUN \ @@ -44,4 +44,4 @@ ENV PATH=/opt/qt510/bin:$PATH RUN mkdir /build -WORKDIR /build \ No newline at end of file +WORKDIR /build diff --git a/Makefile b/Makefile index 2f688d3bbfff562b301cac9ca1c1a3d4c6292bc6..7f603dcb8593c56f16499cd1ab07e6adc07e4316 100644 --- a/Makefile +++ b/Makefile @@ -68,7 +68,7 @@ update-translations: -locations relative \ -Iinclude/dialogs \ -Iinclude \ - src/ -ts resources/langs/nheko_*.ts -no-obsolete + src/ resources/qml/ -ts resources/langs/nheko_*.ts -no-obsolete clean: rm -rf build diff --git a/README.md b/README.md index 1a9609abf58f11eca1c1299768a461a464a48b2c..1179463da576c2e461c0514e89e6956554921992 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,11 @@ nheko The motivation behind the project is to provide a native desktop app for [Matrix] that feels more like a mainstream chat app ([Riot], Telegram etc) and less like an IRC client. +### Translations ### +[](http://weblate.nheko.im/engage/nheko/?utm_source=widget) + +Help us with translations so as many people as possible will be able to use nheko! + ### Note regarding End-to-End encryption Currently the implementation is at best a **proof of concept** and it should only be used for @@ -84,13 +89,14 @@ sudo port install nheko ### Build Requirements -- Qt5 (5.7 or greater). Qt 5.7 adds support for color font rendering with - Freetype, which is essential to properly support emoji. -- CMake 3.1 or greater. +- Qt5 (5.9 or greater). Qt 5.7 adds support for color font rendering with + Freetype, which is essential to properly support emoji, 5.8 adds some features + to make interopability with Qml easier. +- CMake 3.15 or greater. (Lower version may work, but may break boost linking) - [mtxclient](https://github.com/Nheko-Reborn/mtxclient) - [LMDB](https://symas.com/lightning-memory-mapped-database/) - [cmark](https://github.com/commonmark/cmark) -- Boost 1.66 or greater. +- Boost 1.70 or greater. - [libolm](https://git.matrix.org/git/olm) - [libsodium](https://github.com/jedisct1/libsodium) - [spdlog](https://github.com/gabime/spdlog) @@ -126,7 +132,7 @@ sudo pacman -S qt5-base \ ##### Gentoo Linux ```bash -sudo emerge -a ">=dev-qt/qtgui-5.7.1" media-libs/fontconfig +sudo emerge -a ">=dev-qt/qtgui-5.9.0" media-libs/fontconfig ``` ##### Ubuntu (e.g 14.04) diff --git a/appveyor.yml b/appveyor.yml index 08251174aaf932cee1e6818a8603ec21dffca53e..8572418f38d51978aad91efa38f5540547ec1a13 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -34,6 +34,7 @@ install: lmdb:%PLATFORM%-windows openssl:%PLATFORM%-windows zlib:%PLATFORM%-windows + - vcpkg upgrade --no-dry-run build_script: # VERSION format: branch-master/branch-1.2 diff --git a/cmake/Translations.cmake b/cmake/Translations.cmake index 8ca918830c8ddcc661a5d9ea7455c4608ae3dce8..16120219eed4b6998eb0db0924de3b9cdd2a1e87 100644 --- a/cmake/Translations.cmake +++ b/cmake/Translations.cmake @@ -21,4 +21,8 @@ if(NOT EXISTS ${_qrc}) endif() qt5_add_resources(LANG_QRC ${_qrc}) -qt5_add_resources(QRC resources/res.qrc) +if(Qt5QuickCompiler_FOUND) + qtquick_compiler_add_resources(QRC resources/res.qrc) +else() + qt5_add_resources(QRC resources/res.qrc) +endif() diff --git a/deps/CMakeLists.txt b/deps/CMakeLists.txt index d0a715e058b4cb441a088d285b860457a2442517..0da4a67151f4b75e23a92a16b2f1b9210e874302 100644 --- a/deps/CMakeLists.txt +++ b/deps/CMakeLists.txt @@ -33,23 +33,23 @@ option(USE_BUNDLED_JSON "Use the bundled version of nlohmann json." ${USE_BUNDLE option(MTX_STATIC "Compile / link bundled mtx client statically" OFF) if(USE_BUNDLED_BOOST) - # bundled boost is 1.68, which requires CMake 3.12 or greater. - cmake_minimum_required(VERSION 3.12) + # bundled boost is 1.70, which requires CMake 3.15 or greater. + cmake_minimum_required(VERSION 3.15) endif() include(ExternalProject) set(BOOST_URL - https://dl.bintray.com/boostorg/release/1.69.0/source/boost_1_69_0.tar.bz2) + https://dl.bintray.com/boostorg/release/1.70.0/source/boost_1_70_0.tar.bz2) set(BOOST_SHA256 - 8f32d4617390d1c2d16f26a27ab60d97807b35440d45891fa340fc2648b04406) + 430ae8354789de4fd19ee52f3b1f739e1fba576f0aded0897c3c2bc00fb38778) set( MTXCLIENT_URL - https://github.com/Nheko-Reborn/mtxclient/archive/6eee767cc25a9db9f125843e584656cde1ebb6c5.tar.gz + https://github.com/Nheko-Reborn/mtxclient/archive/64182a84e35378113f7d3a80f3073894416480e7.zip ) set(MTXCLIENT_HASH - 72fe77da4fed98b3cf069299f66092c820c900359a27ec26070175f9ad208a03) + c9973501920046f04c72983472451736343d00e7a40f4d4a12181191093a5fab) set( TWEENY_URL https://github.com/mobius3/tweeny/archive/b94ce07cfb02a0eb8ac8aaf66137dabdaea857cf.tar.gz diff --git a/deps/cmake/Boost.cmake b/deps/cmake/Boost.cmake index 47eb723bac1b29131f5f6849ce2a144b8826af65..5d11fd938403d4293ffd99f2c1478d0809e0ffcd 100644 --- a/deps/cmake/Boost.cmake +++ b/deps/cmake/Boost.cmake @@ -3,6 +3,10 @@ if(WIN32) return() endif() +include(BoostToolsetId) +set(BOOST_TOOLSET "gcc") +Boost_Get_ToolsetId(BOOST_TOOLSET) + ExternalProject_Add( Boost @@ -16,6 +20,7 @@ ExternalProject_Add( CONFIGURE_COMMAND ${DEPS_BUILD_DIR}/boost/bootstrap.sh --with-libraries=random,thread,system,iostreams,atomic,chrono,date_time,regex --prefix=${DEPS_INSTALL_DIR} + --with-toolset=${BOOST_TOOLSET} BUILD_COMMAND ${DEPS_BUILD_DIR}/boost/b2 -d0 cxxstd=14 variant=release link=shared runtime-link=shared threading=multi --layout=system INSTALL_COMMAND ${DEPS_BUILD_DIR}/boost/b2 -d0 install ) diff --git a/deps/cmake/BoostToolsetId.cmake b/deps/cmake/BoostToolsetId.cmake new file mode 100644 index 0000000000000000000000000000000000000000..f6c231a51bdaf5d8002545ba0f0ef1e38ec8476c --- /dev/null +++ b/deps/cmake/BoostToolsetId.cmake @@ -0,0 +1,35 @@ +# - Translate CMake compilers to the Boost.Build toolset equivalents +# To build Boost reliably when a non-system compiler may be used, we +# need to both specify the toolset when running bootstrap.sh *and* in +# the user-config.jam file. +# +# This module provides the following functions to help translate between +# the systems: +# +# function Boost_Get_ToolsetId(<var>) +# Set var equal to Boost's name for the CXX toolchain picked +# up by CMake. Only supports GNU and Clang families at present. +# Intel support is provisional +# +# downloaded from https://github.com/drbenmorgan/BoostBuilder/blob/master/BoostToolsetId.cmake + +function(Boost_Get_ToolsetId _var) + set(BOOST_TOOLSET) + + if(CMAKE_CXX_COMPILER_ID MATCHES "GNU") + if(APPLE) + set(BOOST_TOOLSET "darwin") + else() + set(BOOST_TOOLSET "gcc") + endif() + elseif(CMAKE_CXX_COMPILER_ID MATCHES ".*Clang") + set(BOOST_TOOLSET "clang") + elseif(CMAKE_CXX_COMPILER_ID MATCHES "Intel") + set(BOOST_TOOLSET "intel") + elseif(CMAKE_CXX_COMPILER_ID MATCHES "MSVC") + set(BOOST_TOOLSET "msvc") + endif() + + set(${_var} ${BOOST_TOOLSET} PARENT_SCOPE) +endfunction() + diff --git a/resources/langs/nheko_de.ts b/resources/langs/nheko_de.ts index e92bf966091f2626b8c80e012a277748f911a8be..59c6dffd85c5cc196f6c2ec2214279f0f3fd521a 100644 --- a/resources/langs/nheko_de.ts +++ b/resources/langs/nheko_de.ts @@ -1,38 +1,15 @@ <?xml version="1.0" encoding="utf-8"?> <!DOCTYPE TS> <TS version="2.1" language="de"> -<context> - <name>AudioItem</name> - <message> - <location filename="../../src/timeline/widgets/AudioItem.cpp" line="+118"/> - <source>Save File</source> - <translation>In Datei speichern</translation> - </message> -</context> <context> <name>ChatPage</name> <message> - <location filename="../../src/ChatPage.cpp" line="+330"/> - <source>Failed to upload image. Please try again.</source> - <translation>Hochladen des Bildes fehlgeschlagen. Bitte versuche es erneut.</translation> - </message> - <message> - <location line="+45"/> - <source>Failed to upload file. Please try again.</source> - <translation>Hochladen der Datei fehlgeschlagen. Bitte versuche es erneut.</translation> + <location filename="../../src/ChatPage.cpp" line="+346"/> + <source>Failed to upload media. Please try again.</source> + <translation>Medienupload fehlgeschlagen. Bitte versuche es erneut.</translation> </message> <message> - <location line="+43"/> - <source>Failed to upload audio. Please try again.</source> - <translation>Hochladen der Audiodatei fehlgeschlagen. Bitte versuche es erneut.</translation> - </message> - <message> - <location line="+42"/> - <source>Failed to upload video. Please try again.</source> - <translation>Hochladen der Videodatei fehlgeschlagen. Bitte versuche es erneut.</translation> - </message> - <message> - <location line="+380"/> + <location line="+389"/> <source>Failed to restore OLM account. Please login again.</source> <translation>Wiederherstellung des OLM Accounts fehlgeschlagen. Bitte logge dich erneut ein.</translation> </message> @@ -42,18 +19,18 @@ <translation>Gespeicherte Nachrichten konnten nicht wiederhergestellt werden. Bitte melde Dich erneut an.</translation> </message> <message> - <location line="+198"/> + <location line="+181"/> <source>Failed to setup encryption keys. Server response: %1 %2. Please try again later.</source> <translation>Fehler beim Setup der Verschlüsselungsschlüssel. Servermeldung: %1 %2. Bitte versuche es später erneut.</translation> </message> <message> <location line="+51"/> - <location line="+153"/> + <location line="+155"/> <source>Please try to login again: %1</source> <translation>Bitte melde dich erneut an: %1</translation> </message> <message> - <location line="-45"/> + <location line="-47"/> <source>Room creation failed: %1</source> <translation>Raum konnte nicht erstellt werden: %1</translation> </message> @@ -116,19 +93,11 @@ </message> </context> <context> - <name>FileItem</name> + <name>EncryptionIndicator</name> <message> - <location filename="../../src/timeline/widgets/FileItem.cpp" line="+107"/> - <source>Save File</source> - <translation>Datei speichern</translation> - </message> -</context> -<context> - <name>ImageItem</name> - <message> - <location filename="../../src/timeline/widgets/ImageItem.cpp" line="+241"/> - <source>Save image</source> - <translation>Bild speichern</translation> + <location filename="../qml/EncryptionIndicator.qml" line="+11"/> + <source>Encrypted</source> + <translation>Verschlüsselt</translation> </message> </context> <context> @@ -200,7 +169,7 @@ <context> <name>MemberList</name> <message> - <location filename="../../src/dialogs/MemberList.cpp" line="+96"/> + <location filename="../../src/dialogs/MemberList.cpp" line="+89"/> <source>Room members</source> <translation>Teilnehmerliste</translation> </message> @@ -210,6 +179,27 @@ <translation>OK</translation> </message> </context> +<context> + <name>MessageDelegate</name> + <message> + <location filename="../qml/delegates/MessageDelegate.qml" line="+43"/> + <source>redacted</source> + <translation>gelöscht</translation> + </message> + <message> + <location line="+6"/> + <source>Encryption enabled</source> + <translation>Verschlüsselung aktiviert</translation> + </message> +</context> +<context> + <name>Placeholder</name> + <message> + <location filename="../qml/delegates/Placeholder.qml" line="+4"/> + <source>unimplemented event: </source> + <translation>unimplementiertes event: </translation> + </message> +</context> <context> <name>QuickSwitcher</name> <message> @@ -277,7 +267,7 @@ <context> <name>RoomInfo</name> <message> - <location filename="../../src/Cache.cpp" line="+2205"/> + <location filename="../../src/Cache.cpp" line="+2307"/> <source>no version stored</source> <translation>keine Version gespeichert</translation> </message> @@ -285,12 +275,12 @@ <context> <name>RoomInfoListItem</name> <message> - <location filename="../../src/RoomInfoListItem.cpp" line="+93"/> + <location filename="../../src/RoomInfoListItem.cpp" line="+95"/> <source>Leave room</source> <translation>Raum verlassen</translation> </message> <message> - <location line="+181"/> + <location line="+161"/> <source>Accept</source> <translation>Akzeptieren</translation> </message> @@ -331,36 +321,36 @@ <context> <name>StatusIndicator</name> <message> - <location filename="../../src/timeline/TimelineItem.cpp" line="+107"/> - <source>Encrypted</source> - <translation>Verschlüsselt</translation> + <location filename="../qml/StatusIndicator.qml" line="+13"/> + <source>Failed</source> + <translation>Fehlgeschlagen</translation> </message> <message> - <location line="+3"/> - <source>Delivered</source> - <translation>Erhalten</translation> + <location line="+1"/> + <source>Sent</source> + <translation>Gesendet</translation> </message> <message> - <location line="+3"/> - <source>Seen</source> - <translation>Gelesen</translation> + <location line="+1"/> + <source>Received</source> + <translation>Empfangen</translation> </message> <message> - <location line="+3"/> - <source>Sent</source> - <translation>Gesendet</translation> + <location line="+1"/> + <source>Read</source> + <translation>Gelesen</translation> </message> </context> <context> <name>TextInputWidget</name> <message> - <location filename="../../src/TextInputWidget.cpp" line="+507"/> + <location filename="../../src/TextInputWidget.cpp" line="+502"/> <source>Send a file</source> <translation>Versende Datei</translation> </message> <message> <location line="+13"/> - <location filename="../../src/TextInputWidget.h" line="+164"/> + <location filename="../../src/TextInputWidget.h" line="+161"/> <source>Write a message...</source> <translation>Schreibe eine Nachricht…</translation> </message> @@ -375,7 +365,7 @@ <translation>Emoji</translation> </message> <message> - <location line="+75"/> + <location line="+72"/> <source>Select a file</source> <translation>Datei auswählen</translation> </message> @@ -391,32 +381,9 @@ </message> </context> <context> - <name>TimelineItem</name> - <message> - <location filename="../../src/timeline/TimelineItem.cpp" line="+85"/> - <source>Message redaction failed: %1</source> - <translation>Nachricht zurückziehen fehlgeschlagen: %1</translation> - </message> - <message> - <location line="+39"/> - <source>Reply</source> - <translation>Antworten</translation> - </message> - <message> - <location line="+11"/> - <source>Options</source> - <translation>Optionen</translation> - </message> -</context> -<context> - <name>TimelineView</name> - <message> - <location filename="../../src/timeline/TimelineView.cpp" line="+245"/> - <source>Encryption is enabled</source> - <translation>Verschlüsselung aktiv</translation> - </message> + <name>TimelineModel</name> <message> - <location line="+65"/> + <location filename="../../src/timeline/TimelineModel.cpp" line="+835"/> <source>-- Encrypted Event (No keys found for decryption) --</source> <comment>Placeholder, when the message was not decrypted yet or can't be decrypted</comment> <translation>-- verschlüsselter Event (keine Schlüssel zur Entschlüsselung gefunden) --</translation> @@ -440,16 +407,87 @@ <translation>-- Entschlüsselungsfehler (%1) --</translation> </message> <message> - <location line="+27"/> + <location line="+25"/> <source>-- Encrypted Event (Unknown event type) --</source> <comment>Placeholder, when the message was decrypted, but we couldn't parse it, because Nheko/mtxclient don't support that event type yet</comment> <translation>-- verschlüsselter Event (Unbekannter Eventtyp) --</translation> </message> + <message> + <location line="+54"/> + <source>Message redaction failed: %1</source> + <translation>Nachricht zurückziehen fehlgeschlagen: %1</translation> + </message> + <message> + <location line="+453"/> + <source>Save image</source> + <translation>Bild speichern</translation> + </message> + <message> + <location line="+2"/> + <source>Save video</source> + <translation>Video speichern</translation> + </message> + <message> + <location line="+2"/> + <source>Save audio</source> + <translation>Audiodatei speichern</translation> + </message> + <message> + <location line="+2"/> + <source>Save file</source> + <translation>Datei speichern</translation> + </message> +</context> +<context> + <name>TimelineRow</name> + <message> + <location filename="../qml/TimelineRow.qml" line="+57"/> + <source>Reply</source> + <translation>Antworten</translation> + </message> + <message> + <location line="+14"/> + <source>Options</source> + <translation>Optionen</translation> + </message> + <message> + <location line="+12"/> + <source>Read receipts</source> + <translation>Lesebestätigungen</translation> + </message> + <message> + <location line="+4"/> + <source>Mark as read</source> + <translation>Als gelesen markieren</translation> + </message> + <message> + <location line="+3"/> + <source>View raw message</source> + <translation>Zeige rohen Nachrichteninhalt</translation> + </message> + <message> + <location line="+4"/> + <source>Redact message</source> + <translation>Nachricht löschen</translation> + </message> + <message> + <location line="+5"/> + <source>Save as</source> + <translation>Speichern als...</translation> + </message> +</context> +<context> + <name>TimelineView</name> + <message> + <location filename="../qml/TimelineView.qml" line="+24"/> + <source>No room open</source> + <translation>Kein Raum geöffnet</translation> + </message> </context> <context> <name>TopRoomBar</name> <message> - <location filename="../../src/TopRoomBar.cpp" line="+79"/> + <location filename="../../src/TopRoomBar.cpp" line="+78"/> <source>Room options</source> <translation>Raumoptionen</translation> </message> @@ -515,7 +553,7 @@ <context> <name>UserSettingsPage</name> <message> - <location filename="../../src/UserSettingsPage.cpp" line="+166"/> + <location filename="../../src/UserSettingsPage.cpp" line="+171"/> <source>Minimize to tray</source> <translation>Ins Benachrichtigungsfeld minimieren</translation> </message> @@ -529,6 +567,11 @@ <source>Group's sidebar</source> <translation>Gruppen-Seitenleiste</translation> </message> + <message> + <location line="+9"/> + <source>Circular Avatars</source> + <translation>Runde Profilbilder</translation> + </message> <message> <location line="+9"/> <source>Typing notifications</source> @@ -605,7 +648,7 @@ <translation>ALLGEMEINES</translation> </message> <message> - <location line="+156"/> + <location line="+161"/> <source>Open Sessions File</source> <translation>Öffne Sessions Datei</translation> </message> @@ -825,7 +868,7 @@ Medien-Größe: %2 <context> <name>dialogs::ReadReceipts</name> <message> - <location filename="../../src/dialogs/ReadReceipts.cpp" line="+121"/> + <location filename="../../src/dialogs/ReadReceipts.cpp" line="+117"/> <source>Read receipts</source> <translation>Lesebestätigungen</translation> </message> @@ -951,7 +994,7 @@ Medien-Größe: %2 <translation>Aktivierung der Verschlüsselung fehlgeschlagen: %1</translation> </message> <message> - <location line="+149"/> + <location line="+148"/> <source>Select an avatar</source> <translation>Wähle einen Avatar</translation> </message> @@ -977,19 +1020,6 @@ Medien-Größe: %2 <translation>Hochladen der Bilddatei fehlgeschlagen: %s</translation> </message> </context> -<context> - <name>dialogs::UserMentions</name> - <message> - <location filename="../../src/dialogs/UserMentions.cpp" line="+53"/> - <source>This Room</source> - <translation>Dieser Raum</translation> - </message> - <message> - <location line="+1"/> - <source>All Rooms</source> - <translation>Alle Räume</translation> - </message> -</context> <context> <name>dialogs::UserProfile</name> <message> @@ -1013,7 +1043,7 @@ Medien-Größe: %2 <translation>Gespräch beginnen</translation> </message> <message> - <location line="+57"/> + <location line="+56"/> <source>Devices</source> <translation>Geräte</translation> </message> @@ -1064,69 +1094,103 @@ Medien-Größe: %2 <context> <name>message-description sent:</name> <message> - <location filename="../../src/Utils.h" line="+104"/> - <source>%1 an audio clip</source> - <translation>%1 einen Audioclip</translation> + <location filename="../../src/Utils.h" line="+95"/> + <source>You sent an audio clip</source> + <translation>Du hast eine Audiodatei gesendet.</translation> </message> <message> <location line="+3"/> - <source>%1 an image</source> - <translation>%1 ein Bild</translation> + <source>%1 sent an audio clip</source> + <translation>%1 hat eine Audiodatei gesendet.</translation> + </message> + <message> + <location line="+5"/> + <source>You sent an image</source> + <translation>Du hast ein Bild gesendet.</translation> </message> <message> <location line="+3"/> - <source>%1 a file</source> - <translation>%1 eine Datei</translation> + <source>%1 sent an image</source> + <translation>%1 hat ein Bild gesendet.</translation> + </message> + <message> + <location line="+5"/> + <source>You sent a file</source> + <translation>Du hast eine Datei gesendet.</translation> + </message> + <message> + <location line="+3"/> + <source>%1 sent a file</source> + <translation>%1 hat eine Datei gesendet.</translation> + </message> + <message> + <location line="+5"/> + <source>You sent a video</source> + <translation>Du hast ein Video gesendet.</translation> + </message> + <message> + <location line="+3"/> + <source>%1 sent a video</source> + <translation>%1 hat ein Video gesendet.</translation> + </message> + <message> + <location line="+5"/> + <source>You sent a sticker</source> + <translation>Du hast einen Sticker gesendet.</translation> </message> <message> <location line="+3"/> - <source>%1 a video clip</source> - <translation>%1 einen Videoclip</translation> + <source>%1 sent a sticker</source> + <translation>%1 hat einen Sticker gesendet.</translation> + </message> + <message> + <location line="+5"/> + <source>You sent a notification</source> + <translation>Du hast eine Benachrichtigung gesendet.</translation> </message> <message> <location line="+3"/> - <source>%1 a sticker</source> - <translation>%1 einen Sticker</translation> + <source>%1 sent a notification</source> + <translation>%1 hat eine Benachrichtigung gesendet.</translation> + </message> + <message> + <location line="+5"/> + <source>You: %1</source> + <translation>Du: %1</translation> </message> <message> <location line="+3"/> - <source>%1 a notification</source> - <translation>1% eine Benachrichtigung</translation> + <source>%1: %2</source> + <translation>%1: %2</translation> </message> <message> <location line="+7"/> - <source>%1 an encrypted message</source> - <translation>1% eine verschüsselte Nachricht</translation> + <source>You sent an encrypted message</source> + <translation>Du hast eine verschlüsselte Nachricht gesendet.</translation> </message> -</context> -<context> - <name>message-description:</name> <message> - <location line="-26"/> - <source>sent</source> - <comment>For when someone else is the sender</comment> - <translation type="unfinished"></translation> + <location line="+3"/> + <source>%1 sent an encrypted message</source> + <translation>%1 hat eine verschlüsselte Nachricht gesendet.</translation> </message> </context> <context> - <name>message-description: </name> + <name>popups::UserMentions</name> <message> - <location line="-2"/> - <source>sent</source> - <comment>For when you are the sender</comment> - <translation type="unfinished"></translation> + <location filename="../../src/popups/UserMentions.cpp" line="+61"/> + <source>This Room</source> + <translation>Dieser Raum</translation> + </message> + <message> + <location line="+1"/> + <source>All Rooms</source> + <translation>Alle Räume</translation> </message> </context> <context> <name>utils</name> <message> - <location filename="../../src/Utils.cpp" line="+46"/> - <location filename="../../src/Utils.h" line="+55"/> - <source>You</source> - <translation>Du</translation> - </message> - <message> - <location line="+219"/> + <location filename="../../src/Utils.cpp" line="+282"/> <source>sent a file.</source> <translation type="unfinished"></translation> </message> @@ -1146,7 +1210,7 @@ Medien-Größe: %2 <translation type="unfinished"></translation> </message> <message> - <location filename="../../src/Utils.h" line="-23"/> + <location filename="../../src/Utils.h" line="+4"/> <source>Unknown Message Type</source> <translation>Unbekannter Nachrichtentyp</translation> </message> diff --git a/resources/langs/nheko_el.ts b/resources/langs/nheko_el.ts index 700c3d575fb6ecac4e3bdbf3f396dc6efa5d522f..fe65785bdaf2a0e1b33be757ca7b01977fb366cd 100644 --- a/resources/langs/nheko_el.ts +++ b/resources/langs/nheko_el.ts @@ -1,38 +1,15 @@ <?xml version="1.0" encoding="utf-8"?> <!DOCTYPE TS> <TS version="2.1" language="el"> -<context> - <name>AudioItem</name> - <message> - <location filename="../../src/timeline/widgets/AudioItem.cpp" line="+118"/> - <source>Save File</source> - <translation>Αποθήκευση</translation> - </message> -</context> <context> <name>ChatPage</name> <message> - <location filename="../../src/ChatPage.cpp" line="+330"/> - <source>Failed to upload image. Please try again.</source> - <translation type="unfinished"></translation> - </message> - <message> - <location line="+45"/> - <source>Failed to upload file. Please try again.</source> - <translation type="unfinished"></translation> - </message> - <message> - <location line="+43"/> - <source>Failed to upload audio. Please try again.</source> + <location filename="../../src/ChatPage.cpp" line="+346"/> + <source>Failed to upload media. Please try again.</source> <translation type="unfinished"></translation> </message> <message> - <location line="+42"/> - <source>Failed to upload video. Please try again.</source> - <translation type="unfinished"></translation> - </message> - <message> - <location line="+380"/> + <location line="+389"/> <source>Failed to restore OLM account. Please login again.</source> <translation type="unfinished"></translation> </message> @@ -42,18 +19,18 @@ <translation type="unfinished"></translation> </message> <message> - <location line="+198"/> + <location line="+181"/> <source>Failed to setup encryption keys. Server response: %1 %2. Please try again later.</source> <translation type="unfinished"></translation> </message> <message> <location line="+51"/> - <location line="+153"/> + <location line="+155"/> <source>Please try to login again: %1</source> <translation type="unfinished"></translation> </message> <message> - <location line="-45"/> + <location line="-47"/> <source>Room creation failed: %1</source> <translation type="unfinished"></translation> </message> @@ -116,19 +93,11 @@ </message> </context> <context> - <name>FileItem</name> - <message> - <location filename="../../src/timeline/widgets/FileItem.cpp" line="+107"/> - <source>Save File</source> - <translation>Αποθήκευση</translation> - </message> -</context> -<context> - <name>ImageItem</name> + <name>EncryptionIndicator</name> <message> - <location filename="../../src/timeline/widgets/ImageItem.cpp" line="+241"/> - <source>Save image</source> - <translation>Αποθήκευση Εικόνας</translation> + <location filename="../qml/EncryptionIndicator.qml" line="+11"/> + <source>Encrypted</source> + <translation type="unfinished"></translation> </message> </context> <context> @@ -200,7 +169,7 @@ <context> <name>MemberList</name> <message> - <location filename="../../src/dialogs/MemberList.cpp" line="+96"/> + <location filename="../../src/dialogs/MemberList.cpp" line="+89"/> <source>Room members</source> <translation>ÎœÎλη</translation> </message> @@ -210,6 +179,27 @@ <translation type="unfinished"></translation> </message> </context> +<context> + <name>MessageDelegate</name> + <message> + <location filename="../qml/delegates/MessageDelegate.qml" line="+43"/> + <source>redacted</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+6"/> + <source>Encryption enabled</source> + <translation type="unfinished"></translation> + </message> +</context> +<context> + <name>Placeholder</name> + <message> + <location filename="../qml/delegates/Placeholder.qml" line="+4"/> + <source>unimplemented event: </source> + <translation type="unfinished"></translation> + </message> +</context> <context> <name>QuickSwitcher</name> <message> @@ -277,7 +267,7 @@ <context> <name>RoomInfo</name> <message> - <location filename="../../src/Cache.cpp" line="+2205"/> + <location filename="../../src/Cache.cpp" line="+2307"/> <source>no version stored</source> <translation type="unfinished"></translation> </message> @@ -285,12 +275,12 @@ <context> <name>RoomInfoListItem</name> <message> - <location filename="../../src/RoomInfoListItem.cpp" line="+93"/> + <location filename="../../src/RoomInfoListItem.cpp" line="+95"/> <source>Leave room</source> <translation>ΒγÎÏ‚</translation> </message> <message> - <location line="+181"/> + <location line="+161"/> <source>Accept</source> <translation>Αποδοχή</translation> </message> @@ -331,36 +321,36 @@ <context> <name>StatusIndicator</name> <message> - <location filename="../../src/timeline/TimelineItem.cpp" line="+107"/> - <source>Encrypted</source> + <location filename="../qml/StatusIndicator.qml" line="+13"/> + <source>Failed</source> <translation type="unfinished"></translation> </message> <message> - <location line="+3"/> - <source>Delivered</source> + <location line="+1"/> + <source>Sent</source> <translation type="unfinished"></translation> </message> <message> - <location line="+3"/> - <source>Seen</source> + <location line="+1"/> + <source>Received</source> <translation type="unfinished"></translation> </message> <message> - <location line="+3"/> - <source>Sent</source> + <location line="+1"/> + <source>Read</source> <translation type="unfinished"></translation> </message> </context> <context> <name>TextInputWidget</name> <message> - <location filename="../../src/TextInputWidget.cpp" line="+507"/> + <location filename="../../src/TextInputWidget.cpp" line="+502"/> <source>Send a file</source> <translation type="unfinished"></translation> </message> <message> <location line="+13"/> - <location filename="../../src/TextInputWidget.h" line="+164"/> + <location filename="../../src/TextInputWidget.h" line="+161"/> <source>Write a message...</source> <translation>ΓÏάψε Îνα μήνυμα...</translation> </message> @@ -375,7 +365,7 @@ <translation type="unfinished"></translation> </message> <message> - <location line="+75"/> + <location line="+72"/> <source>Select a file</source> <translation>Διάλεξε Îνα αÏχείο</translation> </message> @@ -391,65 +381,113 @@ </message> </context> <context> - <name>TimelineItem</name> + <name>TimelineModel</name> <message> - <location filename="../../src/timeline/TimelineItem.cpp" line="+85"/> + <location filename="../../src/timeline/TimelineModel.cpp" line="+835"/> + <source>-- Encrypted Event (No keys found for decryption) --</source> + <comment>Placeholder, when the message was not decrypted yet or can't be decrypted</comment> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+15"/> + <source>-- Decryption Error (failed to communicate with DB) --</source> + <comment>Placeholder, when the message can't be decrypted, because the DB access failed when trying to lookup the session.</comment> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+19"/> + <source>-- Decryption Error (failed to retrieve megolm keys from db) --</source> + <comment>Placeholder, when the message can't be decrypted, because the DB access failed.</comment> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+12"/> + <source>-- Decryption Error (%1) --</source> + <comment>Placeholder, when the message can't be decrypted. In this case, the Olm decrytion returned an error, which is passed ad %1</comment> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+25"/> + <source>-- Encrypted Event (Unknown event type) --</source> + <comment>Placeholder, when the message was decrypted, but we couldn't parse it, because Nheko/mtxclient don't support that event type yet</comment> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+54"/> <source>Message redaction failed: %1</source> <translation type="unfinished"></translation> </message> <message> - <location line="+39"/> - <source>Reply</source> + <location line="+453"/> + <source>Save image</source> + <translation type="unfinished">Αποθήκευση Εικόνας</translation> + </message> + <message> + <location line="+2"/> + <source>Save video</source> <translation type="unfinished"></translation> </message> <message> - <location line="+11"/> - <source>Options</source> + <location line="+2"/> + <source>Save audio</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+2"/> + <source>Save file</source> <translation type="unfinished"></translation> </message> </context> <context> - <name>TimelineView</name> + <name>TimelineRow</name> <message> - <location filename="../../src/timeline/TimelineView.cpp" line="+245"/> - <source>Encryption is enabled</source> + <location filename="../qml/TimelineRow.qml" line="+57"/> + <source>Reply</source> <translation type="unfinished"></translation> </message> <message> - <location line="+65"/> - <source>-- Encrypted Event (No keys found for decryption) --</source> - <comment>Placeholder, when the message was not decrypted yet or can't be decrypted</comment> + <location line="+14"/> + <source>Options</source> <translation type="unfinished"></translation> </message> <message> - <location line="+15"/> - <source>-- Decryption Error (failed to communicate with DB) --</source> - <comment>Placeholder, when the message can't be decrypted, because the DB access failed when trying to lookup the session.</comment> + <location line="+12"/> + <source>Read receipts</source> <translation type="unfinished"></translation> </message> <message> - <location line="+19"/> - <source>-- Decryption Error (failed to retrieve megolm keys from db) --</source> - <comment>Placeholder, when the message can't be decrypted, because the DB access failed.</comment> + <location line="+4"/> + <source>Mark as read</source> <translation type="unfinished"></translation> </message> <message> - <location line="+12"/> - <source>-- Decryption Error (%1) --</source> - <comment>Placeholder, when the message can't be decrypted. In this case, the Olm decrytion returned an error, which is passed ad %1</comment> + <location line="+3"/> + <source>View raw message</source> <translation type="unfinished"></translation> </message> <message> - <location line="+27"/> - <source>-- Encrypted Event (Unknown event type) --</source> - <comment>Placeholder, when the message was decrypted, but we couldn't parse it, because Nheko/mtxclient don't support that event type yet</comment> + <location line="+4"/> + <source>Redact message</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+5"/> + <source>Save as</source> + <translation type="unfinished"></translation> + </message> +</context> +<context> + <name>TimelineView</name> + <message> + <location filename="../qml/TimelineView.qml" line="+24"/> + <source>No room open</source> <translation type="unfinished"></translation> </message> </context> <context> <name>TopRoomBar</name> <message> - <location filename="../../src/TopRoomBar.cpp" line="+79"/> + <location filename="../../src/TopRoomBar.cpp" line="+78"/> <source>Room options</source> <translation type="unfinished"></translation> </message> @@ -515,7 +553,7 @@ <context> <name>UserSettingsPage</name> <message> - <location filename="../../src/UserSettingsPage.cpp" line="+166"/> + <location filename="../../src/UserSettingsPage.cpp" line="+171"/> <source>Minimize to tray</source> <translation>Ελαχιστοποίηση</translation> </message> @@ -529,6 +567,11 @@ <source>Group's sidebar</source> <translation type="unfinished"></translation> </message> + <message> + <location line="+9"/> + <source>Circular Avatars</source> + <translation type="unfinished"></translation> + </message> <message> <location line="+9"/> <source>Typing notifications</source> @@ -605,7 +648,7 @@ <translation>ΓΕÎΙΚΑ</translation> </message> <message> - <location line="+156"/> + <location line="+161"/> <source>Open Sessions File</source> <translation type="unfinished"></translation> </message> @@ -823,7 +866,7 @@ Media size: %2 <context> <name>dialogs::ReadReceipts</name> <message> - <location filename="../../src/dialogs/ReadReceipts.cpp" line="+121"/> + <location filename="../../src/dialogs/ReadReceipts.cpp" line="+117"/> <source>Read receipts</source> <translation type="unfinished"></translation> </message> @@ -949,7 +992,7 @@ Media size: %2 <translation type="unfinished"></translation> </message> <message> - <location line="+149"/> + <location line="+148"/> <source>Select an avatar</source> <translation type="unfinished"></translation> </message> @@ -975,19 +1018,6 @@ Media size: %2 <translation type="unfinished"></translation> </message> </context> -<context> - <name>dialogs::UserMentions</name> - <message> - <location filename="../../src/dialogs/UserMentions.cpp" line="+53"/> - <source>This Room</source> - <translation type="unfinished"></translation> - </message> - <message> - <location line="+1"/> - <source>All Rooms</source> - <translation type="unfinished"></translation> - </message> -</context> <context> <name>dialogs::UserProfile</name> <message> @@ -1011,7 +1041,7 @@ Media size: %2 <translation type="unfinished"></translation> </message> <message> - <location line="+57"/> + <location line="+56"/> <source>Devices</source> <translation type="unfinished"></translation> </message> @@ -1062,69 +1092,103 @@ Media size: %2 <context> <name>message-description sent:</name> <message> - <location filename="../../src/Utils.h" line="+104"/> - <source>%1 an audio clip</source> + <location filename="../../src/Utils.h" line="+95"/> + <source>You sent an audio clip</source> <translation type="unfinished"></translation> </message> <message> <location line="+3"/> - <source>%1 an image</source> + <source>%1 sent an audio clip</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+5"/> + <source>You sent an image</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+3"/> + <source>%1 sent an image</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+5"/> + <source>You sent a file</source> <translation type="unfinished"></translation> </message> <message> <location line="+3"/> - <source>%1 a file</source> + <source>%1 sent a file</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+5"/> + <source>You sent a video</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+3"/> + <source>%1 sent a video</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+5"/> + <source>You sent a sticker</source> <translation type="unfinished"></translation> </message> <message> <location line="+3"/> - <source>%1 a video clip</source> + <source>%1 sent a sticker</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+5"/> + <source>You sent a notification</source> <translation type="unfinished"></translation> </message> <message> <location line="+3"/> - <source>%1 a sticker</source> + <source>%1 sent a notification</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+5"/> + <source>You: %1</source> <translation type="unfinished"></translation> </message> <message> <location line="+3"/> - <source>%1 a notification</source> + <source>%1: %2</source> <translation type="unfinished"></translation> </message> <message> <location line="+7"/> - <source>%1 an encrypted message</source> + <source>You sent an encrypted message</source> <translation type="unfinished"></translation> </message> -</context> -<context> - <name>message-description:</name> <message> - <location line="-26"/> - <source>sent</source> - <comment>For when someone else is the sender</comment> + <location line="+3"/> + <source>%1 sent an encrypted message</source> <translation type="unfinished"></translation> </message> </context> <context> - <name>message-description: </name> + <name>popups::UserMentions</name> <message> - <location line="-2"/> - <source>sent</source> - <comment>For when you are the sender</comment> + <location filename="../../src/popups/UserMentions.cpp" line="+61"/> + <source>This Room</source> <translation type="unfinished"></translation> </message> -</context> -<context> - <name>utils</name> <message> - <location filename="../../src/Utils.cpp" line="+46"/> - <location filename="../../src/Utils.h" line="+55"/> - <source>You</source> + <location line="+1"/> + <source>All Rooms</source> <translation type="unfinished"></translation> </message> +</context> +<context> + <name>utils</name> <message> - <location line="+219"/> + <location filename="../../src/Utils.cpp" line="+282"/> <source>sent a file.</source> <translation type="unfinished"></translation> </message> @@ -1144,7 +1208,7 @@ Media size: %2 <translation type="unfinished"></translation> </message> <message> - <location filename="../../src/Utils.h" line="-23"/> + <location filename="../../src/Utils.h" line="+4"/> <source>Unknown Message Type</source> <translation type="unfinished"></translation> </message> diff --git a/resources/langs/nheko_en.ts b/resources/langs/nheko_en.ts index edb953137793d052c5bb195af44da0747220da10..49ea7439b3de103141e7e7e58b59e513e66a9cd9 100644 --- a/resources/langs/nheko_en.ts +++ b/resources/langs/nheko_en.ts @@ -1,38 +1,15 @@ <?xml version="1.0" encoding="utf-8"?> <!DOCTYPE TS> <TS version="2.1" language="en"> -<context> - <name>AudioItem</name> - <message> - <location filename="../../src/timeline/widgets/AudioItem.cpp" line="+118"/> - <source>Save File</source> - <translation>Save File</translation> - </message> -</context> <context> <name>ChatPage</name> <message> - <location filename="../../src/ChatPage.cpp" line="+330"/> - <source>Failed to upload image. Please try again.</source> - <translation>Failed to upload image. Please try again.</translation> - </message> - <message> - <location line="+45"/> - <source>Failed to upload file. Please try again.</source> - <translation>Failed to upload file. Please try again.</translation> - </message> - <message> - <location line="+43"/> - <source>Failed to upload audio. Please try again.</source> - <translation>Failed to upload audio. Please try again.</translation> - </message> - <message> - <location line="+42"/> - <source>Failed to upload video. Please try again.</source> - <translation>Failed to upload video. Please try again.</translation> + <location filename="../../src/ChatPage.cpp" line="+346"/> + <source>Failed to upload media. Please try again.</source> + <translation type="unfinished"></translation> </message> <message> - <location line="+380"/> + <location line="+389"/> <source>Failed to restore OLM account. Please login again.</source> <translation>Failed to restore OLM account. Please login again.</translation> </message> @@ -42,18 +19,18 @@ <translation>Failed to restore save data. Please login again.</translation> </message> <message> - <location line="+198"/> + <location line="+181"/> <source>Failed to setup encryption keys. Server response: %1 %2. Please try again later.</source> <translation>Failed to setup encryption keys. Server response: %1 %2. Please try again later.</translation> </message> <message> <location line="+51"/> - <location line="+153"/> + <location line="+155"/> <source>Please try to login again: %1</source> <translation>Please try to login again: %1</translation> </message> <message> - <location line="-45"/> + <location line="-47"/> <source>Room creation failed: %1</source> <translation>Room creation failed: %1</translation> </message> @@ -116,19 +93,11 @@ </message> </context> <context> - <name>FileItem</name> - <message> - <location filename="../../src/timeline/widgets/FileItem.cpp" line="+107"/> - <source>Save File</source> - <translation>Save File</translation> - </message> -</context> -<context> - <name>ImageItem</name> + <name>EncryptionIndicator</name> <message> - <location filename="../../src/timeline/widgets/ImageItem.cpp" line="+241"/> - <source>Save image</source> - <translation>Save image</translation> + <location filename="../qml/EncryptionIndicator.qml" line="+11"/> + <source>Encrypted</source> + <translation type="unfinished"></translation> </message> </context> <context> @@ -200,7 +169,7 @@ <context> <name>MemberList</name> <message> - <location filename="../../src/dialogs/MemberList.cpp" line="+96"/> + <location filename="../../src/dialogs/MemberList.cpp" line="+89"/> <source>Room members</source> <translation>Room members</translation> </message> @@ -210,6 +179,27 @@ <translation>OK</translation> </message> </context> +<context> + <name>MessageDelegate</name> + <message> + <location filename="../qml/delegates/MessageDelegate.qml" line="+43"/> + <source>redacted</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+6"/> + <source>Encryption enabled</source> + <translation type="unfinished"></translation> + </message> +</context> +<context> + <name>Placeholder</name> + <message> + <location filename="../qml/delegates/Placeholder.qml" line="+4"/> + <source>unimplemented event: </source> + <translation type="unfinished"></translation> + </message> +</context> <context> <name>QuickSwitcher</name> <message> @@ -277,7 +267,7 @@ <context> <name>RoomInfo</name> <message> - <location filename="../../src/Cache.cpp" line="+2205"/> + <location filename="../../src/Cache.cpp" line="+2307"/> <source>no version stored</source> <translation>no version stored</translation> </message> @@ -285,12 +275,12 @@ <context> <name>RoomInfoListItem</name> <message> - <location filename="../../src/RoomInfoListItem.cpp" line="+93"/> + <location filename="../../src/RoomInfoListItem.cpp" line="+95"/> <source>Leave room</source> <translation>Leave room</translation> </message> <message> - <location line="+181"/> + <location line="+161"/> <source>Accept</source> <translation>Accept</translation> </message> @@ -331,36 +321,36 @@ <context> <name>StatusIndicator</name> <message> - <location filename="../../src/timeline/TimelineItem.cpp" line="+107"/> - <source>Encrypted</source> - <translation>Encrypted</translation> + <location filename="../qml/StatusIndicator.qml" line="+13"/> + <source>Failed</source> + <translation type="unfinished"></translation> </message> <message> - <location line="+3"/> - <source>Delivered</source> - <translation>Delivered</translation> + <location line="+1"/> + <source>Sent</source> + <translation type="unfinished"></translation> </message> <message> - <location line="+3"/> - <source>Seen</source> - <translation>Seen</translation> + <location line="+1"/> + <source>Received</source> + <translation type="unfinished"></translation> </message> <message> - <location line="+3"/> - <source>Sent</source> - <translation>Sent</translation> + <location line="+1"/> + <source>Read</source> + <translation type="unfinished"></translation> </message> </context> <context> <name>TextInputWidget</name> <message> - <location filename="../../src/TextInputWidget.cpp" line="+507"/> + <location filename="../../src/TextInputWidget.cpp" line="+502"/> <source>Send a file</source> <translation>Send a file</translation> </message> <message> <location line="+13"/> - <location filename="../../src/TextInputWidget.h" line="+164"/> + <location filename="../../src/TextInputWidget.h" line="+161"/> <source>Write a message...</source> <translation>Write a message…</translation> </message> @@ -375,7 +365,7 @@ <translation>Emoji</translation> </message> <message> - <location line="+75"/> + <location line="+72"/> <source>Select a file</source> <translation>Select a file</translation> </message> @@ -391,65 +381,113 @@ </message> </context> <context> - <name>TimelineItem</name> - <message> - <location filename="../../src/timeline/TimelineItem.cpp" line="+85"/> - <source>Message redaction failed: %1</source> - <translation>Message redaction failed: %1</translation> - </message> - <message> - <location line="+39"/> - <source>Reply</source> - <translation>Reply</translation> - </message> - <message> - <location line="+11"/> - <source>Options</source> - <translation>Options</translation> - </message> -</context> -<context> - <name>TimelineView</name> + <name>TimelineModel</name> <message> - <location filename="../../src/timeline/TimelineView.cpp" line="+245"/> - <source>Encryption is enabled</source> - <translation>Encryption is enabled</translation> - </message> - <message> - <location line="+65"/> + <location filename="../../src/timeline/TimelineModel.cpp" line="+835"/> <source>-- Encrypted Event (No keys found for decryption) --</source> <comment>Placeholder, when the message was not decrypted yet or can't be decrypted</comment> - <translation>-- Encrypted Event (No keys found for decryption) --</translation> + <translation type="unfinished">-- Encrypted Event (No keys found for decryption) --</translation> </message> <message> <location line="+15"/> <source>-- Decryption Error (failed to communicate with DB) --</source> <comment>Placeholder, when the message can't be decrypted, because the DB access failed when trying to lookup the session.</comment> - <translation>-- Decryption Error (failed to communicate with DB) --</translation> + <translation type="unfinished">-- Decryption Error (failed to communicate with DB) --</translation> </message> <message> <location line="+19"/> <source>-- Decryption Error (failed to retrieve megolm keys from db) --</source> <comment>Placeholder, when the message can't be decrypted, because the DB access failed.</comment> - <translation>-- Decryption Error (failed to retrieve megolm keys from db) --</translation> + <translation type="unfinished">-- Decryption Error (failed to retrieve megolm keys from db) --</translation> </message> <message> <location line="+12"/> <source>-- Decryption Error (%1) --</source> <comment>Placeholder, when the message can't be decrypted. In this case, the Olm decrytion returned an error, which is passed ad %1</comment> - <translation>-- Decryption Error (%1) --</translation> + <translation type="unfinished">-- Decryption Error (%1) --</translation> </message> <message> - <location line="+27"/> + <location line="+25"/> <source>-- Encrypted Event (Unknown event type) --</source> <comment>Placeholder, when the message was decrypted, but we couldn't parse it, because Nheko/mtxclient don't support that event type yet</comment> - <translation>-- Encrypted Event (Unknown event type) --</translation> + <translation type="unfinished">-- Encrypted Event (Unknown event type) --</translation> + </message> + <message> + <location line="+54"/> + <source>Message redaction failed: %1</source> + <translation type="unfinished">Message redaction failed: %1</translation> + </message> + <message> + <location line="+453"/> + <source>Save image</source> + <translation type="unfinished">Save image</translation> + </message> + <message> + <location line="+2"/> + <source>Save video</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+2"/> + <source>Save audio</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+2"/> + <source>Save file</source> + <translation type="unfinished"></translation> + </message> +</context> +<context> + <name>TimelineRow</name> + <message> + <location filename="../qml/TimelineRow.qml" line="+57"/> + <source>Reply</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+14"/> + <source>Options</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+12"/> + <source>Read receipts</source> + <translation type="unfinished">Read receipts</translation> + </message> + <message> + <location line="+4"/> + <source>Mark as read</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+3"/> + <source>View raw message</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+4"/> + <source>Redact message</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+5"/> + <source>Save as</source> + <translation type="unfinished"></translation> + </message> +</context> +<context> + <name>TimelineView</name> + <message> + <location filename="../qml/TimelineView.qml" line="+24"/> + <source>No room open</source> + <translation type="unfinished"></translation> </message> </context> <context> <name>TopRoomBar</name> <message> - <location filename="../../src/TopRoomBar.cpp" line="+79"/> + <location filename="../../src/TopRoomBar.cpp" line="+78"/> <source>Room options</source> <translation>Room options</translation> </message> @@ -515,7 +553,7 @@ <context> <name>UserSettingsPage</name> <message> - <location filename="../../src/UserSettingsPage.cpp" line="+166"/> + <location filename="../../src/UserSettingsPage.cpp" line="+171"/> <source>Minimize to tray</source> <translation>Minimize to tray</translation> </message> @@ -529,6 +567,11 @@ <source>Group's sidebar</source> <translation>Group's sidebar</translation> </message> + <message> + <location line="+9"/> + <source>Circular Avatars</source> + <translation type="unfinished"></translation> + </message> <message> <location line="+9"/> <source>Typing notifications</source> @@ -605,7 +648,7 @@ <translation>GENERAL</translation> </message> <message> - <location line="+156"/> + <location line="+161"/> <source>Open Sessions File</source> <translation>Open Sessions File</translation> </message> @@ -825,7 +868,7 @@ Media size: %2 <context> <name>dialogs::ReadReceipts</name> <message> - <location filename="../../src/dialogs/ReadReceipts.cpp" line="+121"/> + <location filename="../../src/dialogs/ReadReceipts.cpp" line="+117"/> <source>Read receipts</source> <translation>Read receipts</translation> </message> @@ -953,7 +996,7 @@ Media size: %2 <translation>Failed to enable encryption: %1</translation> </message> <message> - <location line="+149"/> + <location line="+148"/> <source>Select an avatar</source> <translation>Select an avatar</translation> </message> @@ -979,19 +1022,6 @@ Media size: %2 <translation>Failed to upload image: %s</translation> </message> </context> -<context> - <name>dialogs::UserMentions</name> - <message> - <location filename="../../src/dialogs/UserMentions.cpp" line="+53"/> - <source>This Room</source> - <translation>This Room</translation> - </message> - <message> - <location line="+1"/> - <source>All Rooms</source> - <translation>All Rooms</translation> - </message> -</context> <context> <name>dialogs::UserProfile</name> <message> @@ -1015,7 +1045,7 @@ Media size: %2 <translation>Start a conversation</translation> </message> <message> - <location line="+57"/> + <location line="+56"/> <source>Devices</source> <translation>Devices</translation> </message> @@ -1066,69 +1096,103 @@ Media size: %2 <context> <name>message-description sent:</name> <message> - <location filename="../../src/Utils.h" line="+104"/> - <source>%1 an audio clip</source> - <translation>%1 an audio clip</translation> + <location filename="../../src/Utils.h" line="+95"/> + <source>You sent an audio clip</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+3"/> + <source>%1 sent an audio clip</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+5"/> + <source>You sent an image</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+3"/> + <source>%1 sent an image</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+5"/> + <source>You sent a file</source> + <translation type="unfinished"></translation> </message> <message> <location line="+3"/> - <source>%1 an image</source> - <translation>%1 an image</translation> + <source>%1 sent a file</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+5"/> + <source>You sent a video</source> + <translation type="unfinished"></translation> </message> <message> <location line="+3"/> - <source>%1 a file</source> - <translation>%1 a file</translation> + <source>%1 sent a video</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+5"/> + <source>You sent a sticker</source> + <translation type="unfinished"></translation> </message> <message> <location line="+3"/> - <source>%1 a video clip</source> - <translation>%1 a video clip</translation> + <source>%1 sent a sticker</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+5"/> + <source>You sent a notification</source> + <translation type="unfinished"></translation> </message> <message> <location line="+3"/> - <source>%1 a sticker</source> - <translation>%1 a sticker</translation> + <source>%1 sent a notification</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+5"/> + <source>You: %1</source> + <translation type="unfinished"></translation> </message> <message> <location line="+3"/> - <source>%1 a notification</source> - <translation>%1 a notification</translation> + <source>%1: %2</source> + <translation type="unfinished"></translation> </message> <message> <location line="+7"/> - <source>%1 an encrypted message</source> - <translation>%1 an encrypted message</translation> + <source>You sent an encrypted message</source> + <translation type="unfinished"></translation> </message> -</context> -<context> - <name>message-description:</name> <message> - <location line="-26"/> - <source>sent</source> - <comment>For when someone else is the sender</comment> - <translation>sent</translation> + <location line="+3"/> + <source>%1 sent an encrypted message</source> + <translation type="unfinished"></translation> </message> </context> <context> - <name>message-description: </name> + <name>popups::UserMentions</name> <message> - <location line="-2"/> - <source>sent</source> - <comment>For when you are the sender</comment> - <translation>sent</translation> + <location filename="../../src/popups/UserMentions.cpp" line="+61"/> + <source>This Room</source> + <translation type="unfinished">This Room</translation> + </message> + <message> + <location line="+1"/> + <source>All Rooms</source> + <translation type="unfinished">All Rooms</translation> </message> </context> <context> <name>utils</name> <message> - <location filename="../../src/Utils.cpp" line="+46"/> - <location filename="../../src/Utils.h" line="+55"/> - <source>You</source> - <translation>You</translation> - </message> - <message> - <location line="+219"/> + <location filename="../../src/Utils.cpp" line="+282"/> <source>sent a file.</source> <translation>sent a file.</translation> </message> @@ -1148,7 +1212,7 @@ Media size: %2 <translation>sent a video.</translation> </message> <message> - <location filename="../../src/Utils.h" line="-23"/> + <location filename="../../src/Utils.h" line="+4"/> <source>Unknown Message Type</source> <translation>Unknown Message Type</translation> </message> diff --git a/resources/langs/nheko_fi.ts b/resources/langs/nheko_fi.ts index 89eb33b7874c70e5f7eca92dc184da5513862787..4bb20e3098f0a40afa07d6d26ffc5e629812549f 100644 --- a/resources/langs/nheko_fi.ts +++ b/resources/langs/nheko_fi.ts @@ -1,38 +1,15 @@ <?xml version="1.0" encoding="utf-8"?> <!DOCTYPE TS> <TS version="2.1" language="fi"> -<context> - <name>AudioItem</name> - <message> - <location filename="../../src/timeline/widgets/AudioItem.cpp" line="+118"/> - <source>Save File</source> - <translation>Tallenna tiedosto</translation> - </message> -</context> <context> <name>ChatPage</name> <message> - <location filename="../../src/ChatPage.cpp" line="+330"/> - <source>Failed to upload image. Please try again.</source> - <translation>Kuvan lähettäminen epäonnistui. Ole hyvä ja yritä uudelleen.</translation> - </message> - <message> - <location line="+45"/> - <source>Failed to upload file. Please try again.</source> - <translation>Tiedoston lähettäminen epäonnistui. Ole hyvä ja yritä uudelleen.</translation> - </message> - <message> - <location line="+43"/> - <source>Failed to upload audio. Please try again.</source> - <translation>Äänitiedoston lähettäminen epäonnistui. Ole hyvä ja yritä uudelleen.</translation> - </message> - <message> - <location line="+42"/> - <source>Failed to upload video. Please try again.</source> - <translation>Videon lähettäminen epäonnistui. Ole hyvä ja yritä uudelleen.</translation> + <location filename="../../src/ChatPage.cpp" line="+346"/> + <source>Failed to upload media. Please try again.</source> + <translation type="unfinished"></translation> </message> <message> - <location line="+380"/> + <location line="+389"/> <source>Failed to restore OLM account. Please login again.</source> <translation>OLM-tilin palauttaminen epäonnistui. Ole hyvä ja kirjaudu sisään uudelleen.</translation> </message> @@ -42,18 +19,18 @@ <translation>Tallennettujen tietojen palauttaminen epäonnistui. Ole hyvä ja kirjaudu sisään uudelleen.</translation> </message> <message> - <location line="+198"/> + <location line="+181"/> <source>Failed to setup encryption keys. Server response: %1 %2. Please try again later.</source> <translation>Salausavainten lähetys epäonnistui. Palvelimen vastaus: %1 %2. Ole hyvä ja yritä uudelleen myöhemmin.</translation> </message> <message> <location line="+51"/> - <location line="+153"/> + <location line="+155"/> <source>Please try to login again: %1</source> <translation>Ole hyvä ja yritä kirjautua sisään uudelleen: %1</translation> </message> <message> - <location line="-45"/> + <location line="-47"/> <source>Room creation failed: %1</source> <translation>Huoneen luominen epäonnistui: %1</translation> </message> @@ -116,19 +93,11 @@ </message> </context> <context> - <name>FileItem</name> + <name>EncryptionIndicator</name> <message> - <location filename="../../src/timeline/widgets/FileItem.cpp" line="+107"/> - <source>Save File</source> - <translation>Tallenna tiedosto</translation> - </message> -</context> -<context> - <name>ImageItem</name> - <message> - <location filename="../../src/timeline/widgets/ImageItem.cpp" line="+241"/> - <source>Save image</source> - <translation>Tallenna kuva</translation> + <location filename="../qml/EncryptionIndicator.qml" line="+11"/> + <source>Encrypted</source> + <translation type="unfinished"></translation> </message> </context> <context> @@ -200,7 +169,7 @@ <context> <name>MemberList</name> <message> - <location filename="../../src/dialogs/MemberList.cpp" line="+96"/> + <location filename="../../src/dialogs/MemberList.cpp" line="+89"/> <source>Room members</source> <translation>Huoneen jäsenet</translation> </message> @@ -210,6 +179,27 @@ <translation>OK</translation> </message> </context> +<context> + <name>MessageDelegate</name> + <message> + <location filename="../qml/delegates/MessageDelegate.qml" line="+43"/> + <source>redacted</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+6"/> + <source>Encryption enabled</source> + <translation type="unfinished"></translation> + </message> +</context> +<context> + <name>Placeholder</name> + <message> + <location filename="../qml/delegates/Placeholder.qml" line="+4"/> + <source>unimplemented event: </source> + <translation type="unfinished"></translation> + </message> +</context> <context> <name>QuickSwitcher</name> <message> @@ -277,7 +267,7 @@ <context> <name>RoomInfo</name> <message> - <location filename="../../src/Cache.cpp" line="+2205"/> + <location filename="../../src/Cache.cpp" line="+2307"/> <source>no version stored</source> <translation>ei tallennettua versiota</translation> </message> @@ -285,12 +275,12 @@ <context> <name>RoomInfoListItem</name> <message> - <location filename="../../src/RoomInfoListItem.cpp" line="+93"/> + <location filename="../../src/RoomInfoListItem.cpp" line="+95"/> <source>Leave room</source> <translation>Poistu huoneesta</translation> </message> <message> - <location line="+181"/> + <location line="+161"/> <source>Accept</source> <translation>Hyväksy</translation> </message> @@ -331,36 +321,36 @@ <context> <name>StatusIndicator</name> <message> - <location filename="../../src/timeline/TimelineItem.cpp" line="+107"/> - <source>Encrypted</source> - <translation>Salattu</translation> + <location filename="../qml/StatusIndicator.qml" line="+13"/> + <source>Failed</source> + <translation type="unfinished"></translation> </message> <message> - <location line="+3"/> - <source>Delivered</source> - <translation>Toimitettu</translation> + <location line="+1"/> + <source>Sent</source> + <translation type="unfinished"></translation> </message> <message> - <location line="+3"/> - <source>Seen</source> - <translation>Luettu</translation> + <location line="+1"/> + <source>Received</source> + <translation type="unfinished"></translation> </message> <message> - <location line="+3"/> - <source>Sent</source> - <translation>Lähetetty</translation> + <location line="+1"/> + <source>Read</source> + <translation type="unfinished"></translation> </message> </context> <context> <name>TextInputWidget</name> <message> - <location filename="../../src/TextInputWidget.cpp" line="+507"/> + <location filename="../../src/TextInputWidget.cpp" line="+502"/> <source>Send a file</source> <translation>Lähetä tiedosto</translation> </message> <message> <location line="+13"/> - <location filename="../../src/TextInputWidget.h" line="+164"/> + <location filename="../../src/TextInputWidget.h" line="+161"/> <source>Write a message...</source> <translation>Kirjoita viesti…</translation> </message> @@ -375,7 +365,7 @@ <translation>Emoji</translation> </message> <message> - <location line="+75"/> + <location line="+72"/> <source>Select a file</source> <translation>Valitse tiedosto</translation> </message> @@ -391,65 +381,113 @@ </message> </context> <context> - <name>TimelineItem</name> - <message> - <location filename="../../src/timeline/TimelineItem.cpp" line="+85"/> - <source>Message redaction failed: %1</source> - <translation>Viestin poisto epäonnistui: %1</translation> - </message> - <message> - <location line="+39"/> - <source>Reply</source> - <translation>Vastaa</translation> - </message> - <message> - <location line="+11"/> - <source>Options</source> - <translation>Asetukset</translation> - </message> -</context> -<context> - <name>TimelineView</name> + <name>TimelineModel</name> <message> - <location filename="../../src/timeline/TimelineView.cpp" line="+245"/> - <source>Encryption is enabled</source> - <translation>Salaus on käytössä</translation> - </message> - <message> - <location line="+65"/> + <location filename="../../src/timeline/TimelineModel.cpp" line="+835"/> <source>-- Encrypted Event (No keys found for decryption) --</source> <comment>Placeholder, when the message was not decrypted yet or can't be decrypted</comment> - <translation>-- Salattu viesti (salauksen purkuavaimia ei löydetty) --</translation> + <translation type="unfinished">-- Salattu viesti (salauksen purkuavaimia ei löydetty) --</translation> </message> <message> <location line="+15"/> <source>-- Decryption Error (failed to communicate with DB) --</source> <comment>Placeholder, when the message can't be decrypted, because the DB access failed when trying to lookup the session.</comment> - <translation>-- Virhe purkaessa salausta (tietokannan kanssa kommunikointi epäonnistui) --</translation> + <translation type="unfinished">-- Virhe purkaessa salausta (tietokannan kanssa kommunikointi epäonnistui) --</translation> </message> <message> <location line="+19"/> <source>-- Decryption Error (failed to retrieve megolm keys from db) --</source> <comment>Placeholder, when the message can't be decrypted, because the DB access failed.</comment> - <translation>-- Virhe purkaessa salausta (megolm-avaimien hakeminen tietokannasta epäonnistui) --</translation> + <translation type="unfinished">-- Virhe purkaessa salausta (megolm-avaimien hakeminen tietokannasta epäonnistui) --</translation> </message> <message> <location line="+12"/> <source>-- Decryption Error (%1) --</source> <comment>Placeholder, when the message can't be decrypted. In this case, the Olm decrytion returned an error, which is passed ad %1</comment> - <translation>-- Virhe purkaessa salausta (%1) --</translation> + <translation type="unfinished">-- Virhe purkaessa salausta (%1) --</translation> </message> <message> - <location line="+27"/> + <location line="+25"/> <source>-- Encrypted Event (Unknown event type) --</source> <comment>Placeholder, when the message was decrypted, but we couldn't parse it, because Nheko/mtxclient don't support that event type yet</comment> - <translation>-- Salattu viesti (tuntematon viestityyppi) --</translation> + <translation type="unfinished">-- Salattu viesti (tuntematon viestityyppi) --</translation> + </message> + <message> + <location line="+54"/> + <source>Message redaction failed: %1</source> + <translation type="unfinished">Viestin poisto epäonnistui: %1</translation> + </message> + <message> + <location line="+453"/> + <source>Save image</source> + <translation type="unfinished">Tallenna kuva</translation> + </message> + <message> + <location line="+2"/> + <source>Save video</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+2"/> + <source>Save audio</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+2"/> + <source>Save file</source> + <translation type="unfinished"></translation> + </message> +</context> +<context> + <name>TimelineRow</name> + <message> + <location filename="../qml/TimelineRow.qml" line="+57"/> + <source>Reply</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+14"/> + <source>Options</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+12"/> + <source>Read receipts</source> + <translation type="unfinished">Lukukuittaukset</translation> + </message> + <message> + <location line="+4"/> + <source>Mark as read</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+3"/> + <source>View raw message</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+4"/> + <source>Redact message</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+5"/> + <source>Save as</source> + <translation type="unfinished"></translation> + </message> +</context> +<context> + <name>TimelineView</name> + <message> + <location filename="../qml/TimelineView.qml" line="+24"/> + <source>No room open</source> + <translation type="unfinished"></translation> </message> </context> <context> <name>TopRoomBar</name> <message> - <location filename="../../src/TopRoomBar.cpp" line="+79"/> + <location filename="../../src/TopRoomBar.cpp" line="+78"/> <source>Room options</source> <translation>Huonevaihtoehdot</translation> </message> @@ -515,7 +553,7 @@ <context> <name>UserSettingsPage</name> <message> - <location filename="../../src/UserSettingsPage.cpp" line="+166"/> + <location filename="../../src/UserSettingsPage.cpp" line="+171"/> <source>Minimize to tray</source> <translation>Pienennä ilmoitusalueelle</translation> </message> @@ -529,6 +567,11 @@ <source>Group's sidebar</source> <translation>Ryhmäsivupalkki</translation> </message> + <message> + <location line="+9"/> + <source>Circular Avatars</source> + <translation type="unfinished"></translation> + </message> <message> <location line="+9"/> <source>Typing notifications</source> @@ -605,7 +648,7 @@ <translation>YLEISET ASETUKSET</translation> </message> <message> - <location line="+156"/> + <location line="+161"/> <source>Open Sessions File</source> <translation>Avaa Istuntoavaintiedosto</translation> </message> @@ -825,7 +868,7 @@ Median koko: %2 <context> <name>dialogs::ReadReceipts</name> <message> - <location filename="../../src/dialogs/ReadReceipts.cpp" line="+121"/> + <location filename="../../src/dialogs/ReadReceipts.cpp" line="+117"/> <source>Read receipts</source> <translation>Lukukuittaukset</translation> </message> @@ -953,7 +996,7 @@ Median koko: %2 <translation>Salauksen aktivointi epäonnistui: %1</translation> </message> <message> - <location line="+149"/> + <location line="+148"/> <source>Select an avatar</source> <translation>Valitse profiilikuva</translation> </message> @@ -979,19 +1022,6 @@ Median koko: %2 <translation>Kuvan lähetys epäonnistui: %s</translation> </message> </context> -<context> - <name>dialogs::UserMentions</name> - <message> - <location filename="../../src/dialogs/UserMentions.cpp" line="+53"/> - <source>This Room</source> - <translation type="unfinished"></translation> - </message> - <message> - <location line="+1"/> - <source>All Rooms</source> - <translation type="unfinished"></translation> - </message> -</context> <context> <name>dialogs::UserProfile</name> <message> @@ -1015,7 +1045,7 @@ Median koko: %2 <translation>Aloita keskustelu</translation> </message> <message> - <location line="+57"/> + <location line="+56"/> <source>Devices</source> <translation>Laitteet</translation> </message> @@ -1066,69 +1096,103 @@ Median koko: %2 <context> <name>message-description sent:</name> <message> - <location filename="../../src/Utils.h" line="+104"/> - <source>%1 an audio clip</source> + <location filename="../../src/Utils.h" line="+95"/> + <source>You sent an audio clip</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+3"/> + <source>%1 sent an audio clip</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+5"/> + <source>You sent an image</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+3"/> + <source>%1 sent an image</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+5"/> + <source>You sent a file</source> <translation type="unfinished"></translation> </message> <message> <location line="+3"/> - <source>%1 an image</source> + <source>%1 sent a file</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+5"/> + <source>You sent a video</source> <translation type="unfinished"></translation> </message> <message> <location line="+3"/> - <source>%1 a file</source> + <source>%1 sent a video</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+5"/> + <source>You sent a sticker</source> <translation type="unfinished"></translation> </message> <message> <location line="+3"/> - <source>%1 a video clip</source> + <source>%1 sent a sticker</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+5"/> + <source>You sent a notification</source> <translation type="unfinished"></translation> </message> <message> <location line="+3"/> - <source>%1 a sticker</source> + <source>%1 sent a notification</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+5"/> + <source>You: %1</source> <translation type="unfinished"></translation> </message> <message> <location line="+3"/> - <source>%1 a notification</source> + <source>%1: %2</source> <translation type="unfinished"></translation> </message> <message> <location line="+7"/> - <source>%1 an encrypted message</source> + <source>You sent an encrypted message</source> <translation type="unfinished"></translation> </message> -</context> -<context> - <name>message-description:</name> <message> - <location line="-26"/> - <source>sent</source> - <comment>For when someone else is the sender</comment> + <location line="+3"/> + <source>%1 sent an encrypted message</source> <translation type="unfinished"></translation> </message> </context> <context> - <name>message-description: </name> + <name>popups::UserMentions</name> <message> - <location line="-2"/> - <source>sent</source> - <comment>For when you are the sender</comment> + <location filename="../../src/popups/UserMentions.cpp" line="+61"/> + <source>This Room</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+1"/> + <source>All Rooms</source> <translation type="unfinished"></translation> </message> </context> <context> <name>utils</name> <message> - <location filename="../../src/Utils.cpp" line="+46"/> - <location filename="../../src/Utils.h" line="+55"/> - <source>You</source> - <translation>Sinä</translation> - </message> - <message> - <location line="+219"/> + <location filename="../../src/Utils.cpp" line="+282"/> <source>sent a file.</source> <translation type="unfinished"></translation> </message> @@ -1148,7 +1212,7 @@ Median koko: %2 <translation type="unfinished"></translation> </message> <message> - <location filename="../../src/Utils.h" line="-23"/> + <location filename="../../src/Utils.h" line="+4"/> <source>Unknown Message Type</source> <translation type="unfinished"></translation> </message> diff --git a/resources/langs/nheko_fr.ts b/resources/langs/nheko_fr.ts index 42f82b0fcb59c86a01a04f3089e30847286e5c89..8ef2226822ed8831b683b970782caf68bde918cf 100644 --- a/resources/langs/nheko_fr.ts +++ b/resources/langs/nheko_fr.ts @@ -1,38 +1,15 @@ <?xml version="1.0" encoding="utf-8"?> <!DOCTYPE TS> <TS version="2.1" language="fr"> -<context> - <name>AudioItem</name> - <message> - <location filename="../../src/timeline/widgets/AudioItem.cpp" line="+118"/> - <source>Save File</source> - <translation>Enregistrer le fichier</translation> - </message> -</context> <context> <name>ChatPage</name> <message> - <location filename="../../src/ChatPage.cpp" line="+330"/> - <source>Failed to upload image. Please try again.</source> - <translation type="unfinished"></translation> - </message> - <message> - <location line="+45"/> - <source>Failed to upload file. Please try again.</source> - <translation type="unfinished"></translation> - </message> - <message> - <location line="+43"/> - <source>Failed to upload audio. Please try again.</source> - <translation type="unfinished"></translation> - </message> - <message> - <location line="+42"/> - <source>Failed to upload video. Please try again.</source> + <location filename="../../src/ChatPage.cpp" line="+346"/> + <source>Failed to upload media. Please try again.</source> <translation type="unfinished"></translation> </message> <message> - <location line="+380"/> + <location line="+389"/> <source>Failed to restore OLM account. Please login again.</source> <translation type="unfinished"></translation> </message> @@ -42,18 +19,18 @@ <translation type="unfinished"></translation> </message> <message> - <location line="+198"/> + <location line="+181"/> <source>Failed to setup encryption keys. Server response: %1 %2. Please try again later.</source> <translation type="unfinished"></translation> </message> <message> <location line="+51"/> - <location line="+153"/> + <location line="+155"/> <source>Please try to login again: %1</source> <translation type="unfinished"></translation> </message> <message> - <location line="-45"/> + <location line="-47"/> <source>Room creation failed: %1</source> <translation type="unfinished"></translation> </message> @@ -116,19 +93,11 @@ </message> </context> <context> - <name>FileItem</name> + <name>EncryptionIndicator</name> <message> - <location filename="../../src/timeline/widgets/FileItem.cpp" line="+107"/> - <source>Save File</source> - <translation>Enregistrer le fichier</translation> - </message> -</context> -<context> - <name>ImageItem</name> - <message> - <location filename="../../src/timeline/widgets/ImageItem.cpp" line="+241"/> - <source>Save image</source> - <translation>Enregistrer l'image</translation> + <location filename="../qml/EncryptionIndicator.qml" line="+11"/> + <source>Encrypted</source> + <translation type="unfinished"></translation> </message> </context> <context> @@ -200,7 +169,7 @@ <context> <name>MemberList</name> <message> - <location filename="../../src/dialogs/MemberList.cpp" line="+96"/> + <location filename="../../src/dialogs/MemberList.cpp" line="+89"/> <source>Room members</source> <translation>Membres du salon</translation> </message> @@ -210,6 +179,27 @@ <translation type="unfinished"></translation> </message> </context> +<context> + <name>MessageDelegate</name> + <message> + <location filename="../qml/delegates/MessageDelegate.qml" line="+43"/> + <source>redacted</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+6"/> + <source>Encryption enabled</source> + <translation type="unfinished"></translation> + </message> +</context> +<context> + <name>Placeholder</name> + <message> + <location filename="../qml/delegates/Placeholder.qml" line="+4"/> + <source>unimplemented event: </source> + <translation type="unfinished"></translation> + </message> +</context> <context> <name>QuickSwitcher</name> <message> @@ -278,7 +268,7 @@ <context> <name>RoomInfo</name> <message> - <location filename="../../src/Cache.cpp" line="+2205"/> + <location filename="../../src/Cache.cpp" line="+2307"/> <source>no version stored</source> <translation type="unfinished"></translation> </message> @@ -286,12 +276,12 @@ <context> <name>RoomInfoListItem</name> <message> - <location filename="../../src/RoomInfoListItem.cpp" line="+93"/> + <location filename="../../src/RoomInfoListItem.cpp" line="+95"/> <source>Leave room</source> <translation>Quitter le salon</translation> </message> <message> - <location line="+181"/> + <location line="+161"/> <source>Accept</source> <translation>Accepter</translation> </message> @@ -332,36 +322,36 @@ <context> <name>StatusIndicator</name> <message> - <location filename="../../src/timeline/TimelineItem.cpp" line="+107"/> - <source>Encrypted</source> + <location filename="../qml/StatusIndicator.qml" line="+13"/> + <source>Failed</source> <translation type="unfinished"></translation> </message> <message> - <location line="+3"/> - <source>Delivered</source> + <location line="+1"/> + <source>Sent</source> <translation type="unfinished"></translation> </message> <message> - <location line="+3"/> - <source>Seen</source> + <location line="+1"/> + <source>Received</source> <translation type="unfinished"></translation> </message> <message> - <location line="+3"/> - <source>Sent</source> + <location line="+1"/> + <source>Read</source> <translation type="unfinished"></translation> </message> </context> <context> <name>TextInputWidget</name> <message> - <location filename="../../src/TextInputWidget.cpp" line="+507"/> + <location filename="../../src/TextInputWidget.cpp" line="+502"/> <source>Send a file</source> <translation type="unfinished"></translation> </message> <message> <location line="+13"/> - <location filename="../../src/TextInputWidget.h" line="+164"/> + <location filename="../../src/TextInputWidget.h" line="+161"/> <source>Write a message...</source> <translation>Écrivez un message...</translation> </message> @@ -376,7 +366,7 @@ <translation type="unfinished"></translation> </message> <message> - <location line="+75"/> + <location line="+72"/> <source>Select a file</source> <translation>Sélectionnez un fichier</translation> </message> @@ -392,65 +382,113 @@ </message> </context> <context> - <name>TimelineItem</name> + <name>TimelineModel</name> + <message> + <location filename="../../src/timeline/TimelineModel.cpp" line="+835"/> + <source>-- Encrypted Event (No keys found for decryption) --</source> + <comment>Placeholder, when the message was not decrypted yet or can't be decrypted</comment> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+15"/> + <source>-- Decryption Error (failed to communicate with DB) --</source> + <comment>Placeholder, when the message can't be decrypted, because the DB access failed when trying to lookup the session.</comment> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+19"/> + <source>-- Decryption Error (failed to retrieve megolm keys from db) --</source> + <comment>Placeholder, when the message can't be decrypted, because the DB access failed.</comment> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+12"/> + <source>-- Decryption Error (%1) --</source> + <comment>Placeholder, when the message can't be decrypted. In this case, the Olm decrytion returned an error, which is passed ad %1</comment> + <translation type="unfinished"></translation> + </message> <message> - <location filename="../../src/timeline/TimelineItem.cpp" line="+85"/> + <location line="+25"/> + <source>-- Encrypted Event (Unknown event type) --</source> + <comment>Placeholder, when the message was decrypted, but we couldn't parse it, because Nheko/mtxclient don't support that event type yet</comment> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+54"/> <source>Message redaction failed: %1</source> <translation type="unfinished"></translation> </message> <message> - <location line="+39"/> - <source>Reply</source> + <location line="+453"/> + <source>Save image</source> + <translation type="unfinished">Enregistrer l'image</translation> + </message> + <message> + <location line="+2"/> + <source>Save video</source> <translation type="unfinished"></translation> </message> <message> - <location line="+11"/> - <source>Options</source> + <location line="+2"/> + <source>Save audio</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+2"/> + <source>Save file</source> <translation type="unfinished"></translation> </message> </context> <context> - <name>TimelineView</name> + <name>TimelineRow</name> <message> - <location filename="../../src/timeline/TimelineView.cpp" line="+245"/> - <source>Encryption is enabled</source> + <location filename="../qml/TimelineRow.qml" line="+57"/> + <source>Reply</source> <translation type="unfinished"></translation> </message> <message> - <location line="+65"/> - <source>-- Encrypted Event (No keys found for decryption) --</source> - <comment>Placeholder, when the message was not decrypted yet or can't be decrypted</comment> + <location line="+14"/> + <source>Options</source> <translation type="unfinished"></translation> </message> <message> - <location line="+15"/> - <source>-- Decryption Error (failed to communicate with DB) --</source> - <comment>Placeholder, when the message can't be decrypted, because the DB access failed when trying to lookup the session.</comment> + <location line="+12"/> + <source>Read receipts</source> + <translation type="unfinished">Accusés de lecture</translation> + </message> + <message> + <location line="+4"/> + <source>Mark as read</source> <translation type="unfinished"></translation> </message> <message> - <location line="+19"/> - <source>-- Decryption Error (failed to retrieve megolm keys from db) --</source> - <comment>Placeholder, when the message can't be decrypted, because the DB access failed.</comment> + <location line="+3"/> + <source>View raw message</source> <translation type="unfinished"></translation> </message> <message> - <location line="+12"/> - <source>-- Decryption Error (%1) --</source> - <comment>Placeholder, when the message can't be decrypted. In this case, the Olm decrytion returned an error, which is passed ad %1</comment> + <location line="+4"/> + <source>Redact message</source> <translation type="unfinished"></translation> </message> <message> - <location line="+27"/> - <source>-- Encrypted Event (Unknown event type) --</source> - <comment>Placeholder, when the message was decrypted, but we couldn't parse it, because Nheko/mtxclient don't support that event type yet</comment> + <location line="+5"/> + <source>Save as</source> + <translation type="unfinished"></translation> + </message> +</context> +<context> + <name>TimelineView</name> + <message> + <location filename="../qml/TimelineView.qml" line="+24"/> + <source>No room open</source> <translation type="unfinished"></translation> </message> </context> <context> <name>TopRoomBar</name> <message> - <location filename="../../src/TopRoomBar.cpp" line="+79"/> + <location filename="../../src/TopRoomBar.cpp" line="+78"/> <source>Room options</source> <translation type="unfinished"></translation> </message> @@ -516,7 +554,7 @@ <context> <name>UserSettingsPage</name> <message> - <location filename="../../src/UserSettingsPage.cpp" line="+166"/> + <location filename="../../src/UserSettingsPage.cpp" line="+171"/> <source>Minimize to tray</source> <translation>Réduire à la barre des tâches</translation> </message> @@ -530,6 +568,11 @@ <source>Group's sidebar</source> <translation>Barre latérale des groupes</translation> </message> + <message> + <location line="+9"/> + <source>Circular Avatars</source> + <translation type="unfinished"></translation> + </message> <message> <location line="+9"/> <source>Typing notifications</source> @@ -606,7 +649,7 @@ <translation>GÉNÉRAL</translation> </message> <message> - <location line="+156"/> + <location line="+161"/> <source>Open Sessions File</source> <translation type="unfinished"></translation> </message> @@ -826,7 +869,7 @@ Taille du média : %2 <context> <name>dialogs::ReadReceipts</name> <message> - <location filename="../../src/dialogs/ReadReceipts.cpp" line="+121"/> + <location filename="../../src/dialogs/ReadReceipts.cpp" line="+117"/> <source>Read receipts</source> <translation>Accusés de lecture</translation> </message> @@ -952,7 +995,7 @@ Taille du média : %2 <translation type="unfinished"></translation> </message> <message> - <location line="+149"/> + <location line="+148"/> <source>Select an avatar</source> <translation type="unfinished"></translation> </message> @@ -978,19 +1021,6 @@ Taille du média : %2 <translation type="unfinished"></translation> </message> </context> -<context> - <name>dialogs::UserMentions</name> - <message> - <location filename="../../src/dialogs/UserMentions.cpp" line="+53"/> - <source>This Room</source> - <translation type="unfinished"></translation> - </message> - <message> - <location line="+1"/> - <source>All Rooms</source> - <translation type="unfinished"></translation> - </message> -</context> <context> <name>dialogs::UserProfile</name> <message> @@ -1014,7 +1044,7 @@ Taille du média : %2 <translation type="unfinished"></translation> </message> <message> - <location line="+57"/> + <location line="+56"/> <source>Devices</source> <translation type="unfinished"></translation> </message> @@ -1065,69 +1095,103 @@ Taille du média : %2 <context> <name>message-description sent:</name> <message> - <location filename="../../src/Utils.h" line="+104"/> - <source>%1 an audio clip</source> + <location filename="../../src/Utils.h" line="+95"/> + <source>You sent an audio clip</source> <translation type="unfinished"></translation> </message> <message> <location line="+3"/> - <source>%1 an image</source> + <source>%1 sent an audio clip</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+5"/> + <source>You sent an image</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+3"/> + <source>%1 sent an image</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+5"/> + <source>You sent a file</source> <translation type="unfinished"></translation> </message> <message> <location line="+3"/> - <source>%1 a file</source> + <source>%1 sent a file</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+5"/> + <source>You sent a video</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+3"/> + <source>%1 sent a video</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+5"/> + <source>You sent a sticker</source> <translation type="unfinished"></translation> </message> <message> <location line="+3"/> - <source>%1 a video clip</source> + <source>%1 sent a sticker</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+5"/> + <source>You sent a notification</source> <translation type="unfinished"></translation> </message> <message> <location line="+3"/> - <source>%1 a sticker</source> + <source>%1 sent a notification</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+5"/> + <source>You: %1</source> <translation type="unfinished"></translation> </message> <message> <location line="+3"/> - <source>%1 a notification</source> + <source>%1: %2</source> <translation type="unfinished"></translation> </message> <message> <location line="+7"/> - <source>%1 an encrypted message</source> + <source>You sent an encrypted message</source> <translation type="unfinished"></translation> </message> -</context> -<context> - <name>message-description:</name> <message> - <location line="-26"/> - <source>sent</source> - <comment>For when someone else is the sender</comment> + <location line="+3"/> + <source>%1 sent an encrypted message</source> <translation type="unfinished"></translation> </message> </context> <context> - <name>message-description: </name> + <name>popups::UserMentions</name> <message> - <location line="-2"/> - <source>sent</source> - <comment>For when you are the sender</comment> + <location filename="../../src/popups/UserMentions.cpp" line="+61"/> + <source>This Room</source> <translation type="unfinished"></translation> </message> -</context> -<context> - <name>utils</name> <message> - <location filename="../../src/Utils.cpp" line="+46"/> - <location filename="../../src/Utils.h" line="+55"/> - <source>You</source> + <location line="+1"/> + <source>All Rooms</source> <translation type="unfinished"></translation> </message> +</context> +<context> + <name>utils</name> <message> - <location line="+219"/> + <location filename="../../src/Utils.cpp" line="+282"/> <source>sent a file.</source> <translation type="unfinished"></translation> </message> @@ -1147,7 +1211,7 @@ Taille du média : %2 <translation type="unfinished"></translation> </message> <message> - <location filename="../../src/Utils.h" line="-23"/> + <location filename="../../src/Utils.h" line="+4"/> <source>Unknown Message Type</source> <translation type="unfinished"></translation> </message> diff --git a/resources/langs/nheko_nl.ts b/resources/langs/nheko_nl.ts index 53840f8244ed72f90c5ddc13bf4153dd95fc687e..aaeae41c242c3a73e928cf187f0970582f69f88e 100644 --- a/resources/langs/nheko_nl.ts +++ b/resources/langs/nheko_nl.ts @@ -1,38 +1,15 @@ <?xml version="1.0" encoding="utf-8"?> <!DOCTYPE TS> <TS version="2.1" language="nl_NL"> -<context> - <name>AudioItem</name> - <message> - <location filename="../../src/timeline/widgets/AudioItem.cpp" line="+118"/> - <source>Save File</source> - <translation>Bestand opslaan</translation> - </message> -</context> <context> <name>ChatPage</name> <message> - <location filename="../../src/ChatPage.cpp" line="+330"/> - <source>Failed to upload image. Please try again.</source> - <translation type="unfinished"></translation> - </message> - <message> - <location line="+45"/> - <source>Failed to upload file. Please try again.</source> - <translation type="unfinished"></translation> - </message> - <message> - <location line="+43"/> - <source>Failed to upload audio. Please try again.</source> - <translation type="unfinished"></translation> - </message> - <message> - <location line="+42"/> - <source>Failed to upload video. Please try again.</source> + <location filename="../../src/ChatPage.cpp" line="+346"/> + <source>Failed to upload media. Please try again.</source> <translation type="unfinished"></translation> </message> <message> - <location line="+380"/> + <location line="+389"/> <source>Failed to restore OLM account. Please login again.</source> <translation type="unfinished"></translation> </message> @@ -42,18 +19,18 @@ <translation type="unfinished"></translation> </message> <message> - <location line="+198"/> + <location line="+181"/> <source>Failed to setup encryption keys. Server response: %1 %2. Please try again later.</source> <translation type="unfinished"></translation> </message> <message> <location line="+51"/> - <location line="+153"/> + <location line="+155"/> <source>Please try to login again: %1</source> <translation type="unfinished"></translation> </message> <message> - <location line="-45"/> + <location line="-47"/> <source>Room creation failed: %1</source> <translation type="unfinished"></translation> </message> @@ -116,19 +93,11 @@ </message> </context> <context> - <name>FileItem</name> + <name>EncryptionIndicator</name> <message> - <location filename="../../src/timeline/widgets/FileItem.cpp" line="+107"/> - <source>Save File</source> - <translation>Bestand opslaan</translation> - </message> -</context> -<context> - <name>ImageItem</name> - <message> - <location filename="../../src/timeline/widgets/ImageItem.cpp" line="+241"/> - <source>Save image</source> - <translation>Afbeelding opslaan</translation> + <location filename="../qml/EncryptionIndicator.qml" line="+11"/> + <source>Encrypted</source> + <translation type="unfinished"></translation> </message> </context> <context> @@ -200,7 +169,7 @@ <context> <name>MemberList</name> <message> - <location filename="../../src/dialogs/MemberList.cpp" line="+96"/> + <location filename="../../src/dialogs/MemberList.cpp" line="+89"/> <source>Room members</source> <translation>Kamerleden</translation> </message> @@ -210,6 +179,27 @@ <translation type="unfinished"></translation> </message> </context> +<context> + <name>MessageDelegate</name> + <message> + <location filename="../qml/delegates/MessageDelegate.qml" line="+43"/> + <source>redacted</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+6"/> + <source>Encryption enabled</source> + <translation type="unfinished"></translation> + </message> +</context> +<context> + <name>Placeholder</name> + <message> + <location filename="../qml/delegates/Placeholder.qml" line="+4"/> + <source>unimplemented event: </source> + <translation type="unfinished"></translation> + </message> +</context> <context> <name>QuickSwitcher</name> <message> @@ -277,7 +267,7 @@ <context> <name>RoomInfo</name> <message> - <location filename="../../src/Cache.cpp" line="+2205"/> + <location filename="../../src/Cache.cpp" line="+2307"/> <source>no version stored</source> <translation type="unfinished"></translation> </message> @@ -285,12 +275,12 @@ <context> <name>RoomInfoListItem</name> <message> - <location filename="../../src/RoomInfoListItem.cpp" line="+93"/> + <location filename="../../src/RoomInfoListItem.cpp" line="+95"/> <source>Leave room</source> <translation>Kamer verlaten</translation> </message> <message> - <location line="+181"/> + <location line="+161"/> <source>Accept</source> <translation>Accepteren</translation> </message> @@ -331,36 +321,36 @@ <context> <name>StatusIndicator</name> <message> - <location filename="../../src/timeline/TimelineItem.cpp" line="+107"/> - <source>Encrypted</source> + <location filename="../qml/StatusIndicator.qml" line="+13"/> + <source>Failed</source> <translation type="unfinished"></translation> </message> <message> - <location line="+3"/> - <source>Delivered</source> + <location line="+1"/> + <source>Sent</source> <translation type="unfinished"></translation> </message> <message> - <location line="+3"/> - <source>Seen</source> + <location line="+1"/> + <source>Received</source> <translation type="unfinished"></translation> </message> <message> - <location line="+3"/> - <source>Sent</source> + <location line="+1"/> + <source>Read</source> <translation type="unfinished"></translation> </message> </context> <context> <name>TextInputWidget</name> <message> - <location filename="../../src/TextInputWidget.cpp" line="+507"/> + <location filename="../../src/TextInputWidget.cpp" line="+502"/> <source>Send a file</source> <translation type="unfinished"></translation> </message> <message> <location line="+13"/> - <location filename="../../src/TextInputWidget.h" line="+164"/> + <location filename="../../src/TextInputWidget.h" line="+161"/> <source>Write a message...</source> <translation>Typ een bericht...</translation> </message> @@ -375,7 +365,7 @@ <translation type="unfinished"></translation> </message> <message> - <location line="+75"/> + <location line="+72"/> <source>Select a file</source> <translation>Kies een bestand</translation> </message> @@ -391,65 +381,113 @@ </message> </context> <context> - <name>TimelineItem</name> + <name>TimelineModel</name> + <message> + <location filename="../../src/timeline/TimelineModel.cpp" line="+835"/> + <source>-- Encrypted Event (No keys found for decryption) --</source> + <comment>Placeholder, when the message was not decrypted yet or can't be decrypted</comment> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+15"/> + <source>-- Decryption Error (failed to communicate with DB) --</source> + <comment>Placeholder, when the message can't be decrypted, because the DB access failed when trying to lookup the session.</comment> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+19"/> + <source>-- Decryption Error (failed to retrieve megolm keys from db) --</source> + <comment>Placeholder, when the message can't be decrypted, because the DB access failed.</comment> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+12"/> + <source>-- Decryption Error (%1) --</source> + <comment>Placeholder, when the message can't be decrypted. In this case, the Olm decrytion returned an error, which is passed ad %1</comment> + <translation type="unfinished"></translation> + </message> <message> - <location filename="../../src/timeline/TimelineItem.cpp" line="+85"/> + <location line="+25"/> + <source>-- Encrypted Event (Unknown event type) --</source> + <comment>Placeholder, when the message was decrypted, but we couldn't parse it, because Nheko/mtxclient don't support that event type yet</comment> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+54"/> <source>Message redaction failed: %1</source> <translation type="unfinished"></translation> </message> <message> - <location line="+39"/> - <source>Reply</source> + <location line="+453"/> + <source>Save image</source> + <translation type="unfinished">Afbeelding opslaan</translation> + </message> + <message> + <location line="+2"/> + <source>Save video</source> <translation type="unfinished"></translation> </message> <message> - <location line="+11"/> - <source>Options</source> + <location line="+2"/> + <source>Save audio</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+2"/> + <source>Save file</source> <translation type="unfinished"></translation> </message> </context> <context> - <name>TimelineView</name> + <name>TimelineRow</name> <message> - <location filename="../../src/timeline/TimelineView.cpp" line="+245"/> - <source>Encryption is enabled</source> + <location filename="../qml/TimelineRow.qml" line="+57"/> + <source>Reply</source> <translation type="unfinished"></translation> </message> <message> - <location line="+65"/> - <source>-- Encrypted Event (No keys found for decryption) --</source> - <comment>Placeholder, when the message was not decrypted yet or can't be decrypted</comment> + <location line="+14"/> + <source>Options</source> <translation type="unfinished"></translation> </message> <message> - <location line="+15"/> - <source>-- Decryption Error (failed to communicate with DB) --</source> - <comment>Placeholder, when the message can't be decrypted, because the DB access failed when trying to lookup the session.</comment> + <location line="+12"/> + <source>Read receipts</source> + <translation type="unfinished">Leesbevestigingen</translation> + </message> + <message> + <location line="+4"/> + <source>Mark as read</source> <translation type="unfinished"></translation> </message> <message> - <location line="+19"/> - <source>-- Decryption Error (failed to retrieve megolm keys from db) --</source> - <comment>Placeholder, when the message can't be decrypted, because the DB access failed.</comment> + <location line="+3"/> + <source>View raw message</source> <translation type="unfinished"></translation> </message> <message> - <location line="+12"/> - <source>-- Decryption Error (%1) --</source> - <comment>Placeholder, when the message can't be decrypted. In this case, the Olm decrytion returned an error, which is passed ad %1</comment> + <location line="+4"/> + <source>Redact message</source> <translation type="unfinished"></translation> </message> <message> - <location line="+27"/> - <source>-- Encrypted Event (Unknown event type) --</source> - <comment>Placeholder, when the message was decrypted, but we couldn't parse it, because Nheko/mtxclient don't support that event type yet</comment> + <location line="+5"/> + <source>Save as</source> + <translation type="unfinished"></translation> + </message> +</context> +<context> + <name>TimelineView</name> + <message> + <location filename="../qml/TimelineView.qml" line="+24"/> + <source>No room open</source> <translation type="unfinished"></translation> </message> </context> <context> <name>TopRoomBar</name> <message> - <location filename="../../src/TopRoomBar.cpp" line="+79"/> + <location filename="../../src/TopRoomBar.cpp" line="+78"/> <source>Room options</source> <translation type="unfinished"></translation> </message> @@ -515,7 +553,7 @@ <context> <name>UserSettingsPage</name> <message> - <location filename="../../src/UserSettingsPage.cpp" line="+166"/> + <location filename="../../src/UserSettingsPage.cpp" line="+171"/> <source>Minimize to tray</source> <translation>Minimaliseren naar systeemvak</translation> </message> @@ -529,6 +567,11 @@ <source>Group's sidebar</source> <translation>Zijbalk van groep</translation> </message> + <message> + <location line="+9"/> + <source>Circular Avatars</source> + <translation type="unfinished"></translation> + </message> <message> <location line="+9"/> <source>Typing notifications</source> @@ -605,7 +648,7 @@ <translation>ALGEMEEN</translation> </message> <message> - <location line="+156"/> + <location line="+161"/> <source>Open Sessions File</source> <translation type="unfinished"></translation> </message> @@ -825,7 +868,7 @@ Mediagrootte: %2 <context> <name>dialogs::ReadReceipts</name> <message> - <location filename="../../src/dialogs/ReadReceipts.cpp" line="+121"/> + <location filename="../../src/dialogs/ReadReceipts.cpp" line="+117"/> <source>Read receipts</source> <translation>Leesbevestigingen</translation> </message> @@ -951,7 +994,7 @@ Mediagrootte: %2 <translation type="unfinished"></translation> </message> <message> - <location line="+149"/> + <location line="+148"/> <source>Select an avatar</source> <translation type="unfinished"></translation> </message> @@ -977,19 +1020,6 @@ Mediagrootte: %2 <translation type="unfinished"></translation> </message> </context> -<context> - <name>dialogs::UserMentions</name> - <message> - <location filename="../../src/dialogs/UserMentions.cpp" line="+53"/> - <source>This Room</source> - <translation type="unfinished"></translation> - </message> - <message> - <location line="+1"/> - <source>All Rooms</source> - <translation type="unfinished"></translation> - </message> -</context> <context> <name>dialogs::UserProfile</name> <message> @@ -1013,7 +1043,7 @@ Mediagrootte: %2 <translation type="unfinished"></translation> </message> <message> - <location line="+57"/> + <location line="+56"/> <source>Devices</source> <translation type="unfinished"></translation> </message> @@ -1064,69 +1094,103 @@ Mediagrootte: %2 <context> <name>message-description sent:</name> <message> - <location filename="../../src/Utils.h" line="+104"/> - <source>%1 an audio clip</source> + <location filename="../../src/Utils.h" line="+95"/> + <source>You sent an audio clip</source> <translation type="unfinished"></translation> </message> <message> <location line="+3"/> - <source>%1 an image</source> + <source>%1 sent an audio clip</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+5"/> + <source>You sent an image</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+3"/> + <source>%1 sent an image</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+5"/> + <source>You sent a file</source> <translation type="unfinished"></translation> </message> <message> <location line="+3"/> - <source>%1 a file</source> + <source>%1 sent a file</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+5"/> + <source>You sent a video</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+3"/> + <source>%1 sent a video</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+5"/> + <source>You sent a sticker</source> <translation type="unfinished"></translation> </message> <message> <location line="+3"/> - <source>%1 a video clip</source> + <source>%1 sent a sticker</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+5"/> + <source>You sent a notification</source> <translation type="unfinished"></translation> </message> <message> <location line="+3"/> - <source>%1 a sticker</source> + <source>%1 sent a notification</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+5"/> + <source>You: %1</source> <translation type="unfinished"></translation> </message> <message> <location line="+3"/> - <source>%1 a notification</source> + <source>%1: %2</source> <translation type="unfinished"></translation> </message> <message> <location line="+7"/> - <source>%1 an encrypted message</source> + <source>You sent an encrypted message</source> <translation type="unfinished"></translation> </message> -</context> -<context> - <name>message-description:</name> <message> - <location line="-26"/> - <source>sent</source> - <comment>For when someone else is the sender</comment> + <location line="+3"/> + <source>%1 sent an encrypted message</source> <translation type="unfinished"></translation> </message> </context> <context> - <name>message-description: </name> + <name>popups::UserMentions</name> <message> - <location line="-2"/> - <source>sent</source> - <comment>For when you are the sender</comment> + <location filename="../../src/popups/UserMentions.cpp" line="+61"/> + <source>This Room</source> <translation type="unfinished"></translation> </message> -</context> -<context> - <name>utils</name> <message> - <location filename="../../src/Utils.cpp" line="+46"/> - <location filename="../../src/Utils.h" line="+55"/> - <source>You</source> + <location line="+1"/> + <source>All Rooms</source> <translation type="unfinished"></translation> </message> +</context> +<context> + <name>utils</name> <message> - <location line="+219"/> + <location filename="../../src/Utils.cpp" line="+282"/> <source>sent a file.</source> <translation type="unfinished"></translation> </message> @@ -1146,7 +1210,7 @@ Mediagrootte: %2 <translation type="unfinished"></translation> </message> <message> - <location filename="../../src/Utils.h" line="-23"/> + <location filename="../../src/Utils.h" line="+4"/> <source>Unknown Message Type</source> <translation type="unfinished"></translation> </message> diff --git a/resources/langs/nheko_pl.ts b/resources/langs/nheko_pl.ts index f4f98dbbc72f946bd095189ebb48489e73d76f12..b7c3878d9fc97dfcd25a5da6258fc2f0b7fd7d77 100644 --- a/resources/langs/nheko_pl.ts +++ b/resources/langs/nheko_pl.ts @@ -1,38 +1,15 @@ <?xml version="1.0" encoding="utf-8"?> <!DOCTYPE TS> <TS version="2.1" language="pl"> -<context> - <name>AudioItem</name> - <message> - <location filename="../../src/timeline/widgets/AudioItem.cpp" line="+118"/> - <source>Save File</source> - <translation>Zapisz plik</translation> - </message> -</context> <context> <name>ChatPage</name> <message> - <location filename="../../src/ChatPage.cpp" line="+330"/> - <source>Failed to upload image. Please try again.</source> - <translation>Nie udaÅ‚o siÄ™ wysÅ‚ać obrazu. Spróbuj ponownie.</translation> - </message> - <message> - <location line="+45"/> - <source>Failed to upload file. Please try again.</source> - <translation>Nie udaÅ‚o siÄ™ wysÅ‚ać pliku. Spróbuj ponownie.</translation> - </message> - <message> - <location line="+43"/> - <source>Failed to upload audio. Please try again.</source> - <translation>Nie udaÅ‚o siÄ™ wysÅ‚ać pliku dźwiÄ™kowego. Spróbuj ponownie.</translation> - </message> - <message> - <location line="+42"/> - <source>Failed to upload video. Please try again.</source> - <translation>Nie udaÅ‚o siÄ™ wysÅ‚ać filmu. Spróbuj ponownie.</translation> + <location filename="../../src/ChatPage.cpp" line="+346"/> + <source>Failed to upload media. Please try again.</source> + <translation type="unfinished"></translation> </message> <message> - <location line="+380"/> + <location line="+389"/> <source>Failed to restore OLM account. Please login again.</source> <translation>Nie udaÅ‚o siÄ™ przywrócić konta OLM. Spróbuj zalogować siÄ™ ponownie.</translation> </message> @@ -42,18 +19,18 @@ <translation>Nie udaÅ‚o siÄ™ przywrócić zapisanych danych. Spróbuj zalogować siÄ™ ponownie.</translation> </message> <message> - <location line="+198"/> + <location line="+181"/> <source>Failed to setup encryption keys. Server response: %1 %2. Please try again later.</source> <translation type="unfinished"></translation> </message> <message> <location line="+51"/> - <location line="+153"/> + <location line="+155"/> <source>Please try to login again: %1</source> <translation>Spróbuj zalogować siÄ™ ponownie: %1</translation> </message> <message> - <location line="-45"/> + <location line="-47"/> <source>Room creation failed: %1</source> <translation>Tworzenie pokoju nie powiodÅ‚o siÄ™: %1</translation> </message> @@ -116,19 +93,11 @@ </message> </context> <context> - <name>FileItem</name> + <name>EncryptionIndicator</name> <message> - <location filename="../../src/timeline/widgets/FileItem.cpp" line="+107"/> - <source>Save File</source> - <translation>Zapisz plik</translation> - </message> -</context> -<context> - <name>ImageItem</name> - <message> - <location filename="../../src/timeline/widgets/ImageItem.cpp" line="+241"/> - <source>Save image</source> - <translation>Zapisz obraz</translation> + <location filename="../qml/EncryptionIndicator.qml" line="+11"/> + <source>Encrypted</source> + <translation type="unfinished"></translation> </message> </context> <context> @@ -200,7 +169,7 @@ <context> <name>MemberList</name> <message> - <location filename="../../src/dialogs/MemberList.cpp" line="+96"/> + <location filename="../../src/dialogs/MemberList.cpp" line="+89"/> <source>Room members</source> <translation>CzÅ‚onkowie pokoju</translation> </message> @@ -210,6 +179,27 @@ <translation type="unfinished"></translation> </message> </context> +<context> + <name>MessageDelegate</name> + <message> + <location filename="../qml/delegates/MessageDelegate.qml" line="+43"/> + <source>redacted</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+6"/> + <source>Encryption enabled</source> + <translation type="unfinished"></translation> + </message> +</context> +<context> + <name>Placeholder</name> + <message> + <location filename="../qml/delegates/Placeholder.qml" line="+4"/> + <source>unimplemented event: </source> + <translation type="unfinished"></translation> + </message> +</context> <context> <name>QuickSwitcher</name> <message> @@ -277,7 +267,7 @@ <context> <name>RoomInfo</name> <message> - <location filename="../../src/Cache.cpp" line="+2205"/> + <location filename="../../src/Cache.cpp" line="+2307"/> <source>no version stored</source> <translation type="unfinished"></translation> </message> @@ -285,12 +275,12 @@ <context> <name>RoomInfoListItem</name> <message> - <location filename="../../src/RoomInfoListItem.cpp" line="+93"/> + <location filename="../../src/RoomInfoListItem.cpp" line="+95"/> <source>Leave room</source> <translation>Opuść pokój</translation> </message> <message> - <location line="+181"/> + <location line="+161"/> <source>Accept</source> <translation>Akceptuj</translation> </message> @@ -331,36 +321,36 @@ <context> <name>StatusIndicator</name> <message> - <location filename="../../src/timeline/TimelineItem.cpp" line="+107"/> - <source>Encrypted</source> - <translation>Szyfrowana</translation> + <location filename="../qml/StatusIndicator.qml" line="+13"/> + <source>Failed</source> + <translation type="unfinished"></translation> </message> <message> - <location line="+3"/> - <source>Delivered</source> - <translation>Dostarczono</translation> + <location line="+1"/> + <source>Sent</source> + <translation type="unfinished"></translation> </message> <message> - <location line="+3"/> - <source>Seen</source> - <translation>WyÅ›wietlona</translation> + <location line="+1"/> + <source>Received</source> + <translation type="unfinished"></translation> </message> <message> - <location line="+3"/> - <source>Sent</source> - <translation>WysÅ‚ana</translation> + <location line="+1"/> + <source>Read</source> + <translation type="unfinished"></translation> </message> </context> <context> <name>TextInputWidget</name> <message> - <location filename="../../src/TextInputWidget.cpp" line="+507"/> + <location filename="../../src/TextInputWidget.cpp" line="+502"/> <source>Send a file</source> <translation>WyÅ›lij plik</translation> </message> <message> <location line="+13"/> - <location filename="../../src/TextInputWidget.h" line="+164"/> + <location filename="../../src/TextInputWidget.h" line="+161"/> <source>Write a message...</source> <translation>Napisz wiadomość…</translation> </message> @@ -375,7 +365,7 @@ <translation>Emoji</translation> </message> <message> - <location line="+75"/> + <location line="+72"/> <source>Select a file</source> <translation>Wybierz plik</translation> </message> @@ -391,32 +381,9 @@ </message> </context> <context> - <name>TimelineItem</name> - <message> - <location filename="../../src/timeline/TimelineItem.cpp" line="+85"/> - <source>Message redaction failed: %1</source> - <translation>Redagowanie wiadomoÅ›ci nie powiodÅ‚o siÄ™: %1</translation> - </message> - <message> - <location line="+39"/> - <source>Reply</source> - <translation type="unfinished"></translation> - </message> + <name>TimelineModel</name> <message> - <location line="+11"/> - <source>Options</source> - <translation type="unfinished"></translation> - </message> -</context> -<context> - <name>TimelineView</name> - <message> - <location filename="../../src/timeline/TimelineView.cpp" line="+245"/> - <source>Encryption is enabled</source> - <translation>Szyfrowanie jest wÅ‚Ä…czone</translation> - </message> - <message> - <location line="+65"/> + <location filename="../../src/timeline/TimelineModel.cpp" line="+835"/> <source>-- Encrypted Event (No keys found for decryption) --</source> <comment>Placeholder, when the message was not decrypted yet or can't be decrypted</comment> <translation type="unfinished"></translation> @@ -440,16 +407,87 @@ <translation type="unfinished"></translation> </message> <message> - <location line="+27"/> + <location line="+25"/> <source>-- Encrypted Event (Unknown event type) --</source> <comment>Placeholder, when the message was decrypted, but we couldn't parse it, because Nheko/mtxclient don't support that event type yet</comment> <translation type="unfinished"></translation> </message> + <message> + <location line="+54"/> + <source>Message redaction failed: %1</source> + <translation type="unfinished">Redagowanie wiadomoÅ›ci nie powiodÅ‚o siÄ™: %1</translation> + </message> + <message> + <location line="+453"/> + <source>Save image</source> + <translation type="unfinished">Zapisz obraz</translation> + </message> + <message> + <location line="+2"/> + <source>Save video</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+2"/> + <source>Save audio</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+2"/> + <source>Save file</source> + <translation type="unfinished"></translation> + </message> +</context> +<context> + <name>TimelineRow</name> + <message> + <location filename="../qml/TimelineRow.qml" line="+57"/> + <source>Reply</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+14"/> + <source>Options</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+12"/> + <source>Read receipts</source> + <translation type="unfinished">Potwierdzenia przeczytania</translation> + </message> + <message> + <location line="+4"/> + <source>Mark as read</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+3"/> + <source>View raw message</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+4"/> + <source>Redact message</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+5"/> + <source>Save as</source> + <translation type="unfinished"></translation> + </message> +</context> +<context> + <name>TimelineView</name> + <message> + <location filename="../qml/TimelineView.qml" line="+24"/> + <source>No room open</source> + <translation type="unfinished"></translation> + </message> </context> <context> <name>TopRoomBar</name> <message> - <location filename="../../src/TopRoomBar.cpp" line="+79"/> + <location filename="../../src/TopRoomBar.cpp" line="+78"/> <source>Room options</source> <translation>Ustawienia pokoju</translation> </message> @@ -516,7 +554,7 @@ <context> <name>UserSettingsPage</name> <message> - <location filename="../../src/UserSettingsPage.cpp" line="+166"/> + <location filename="../../src/UserSettingsPage.cpp" line="+171"/> <source>Minimize to tray</source> <translation>Zminimalizuj do paska zadaÅ„</translation> </message> @@ -530,6 +568,11 @@ <source>Group's sidebar</source> <translation>Pasek boczny grupy</translation> </message> + <message> + <location line="+9"/> + <source>Circular Avatars</source> + <translation type="unfinished"></translation> + </message> <message> <location line="+9"/> <source>Typing notifications</source> @@ -606,7 +649,7 @@ <translation>OGÓLNE</translation> </message> <message> - <location line="+156"/> + <location line="+161"/> <source>Open Sessions File</source> <translation type="unfinished"></translation> </message> @@ -826,7 +869,7 @@ Rozmiar multimediów: %2 <context> <name>dialogs::ReadReceipts</name> <message> - <location filename="../../src/dialogs/ReadReceipts.cpp" line="+121"/> + <location filename="../../src/dialogs/ReadReceipts.cpp" line="+117"/> <source>Read receipts</source> <translation>Potwierdzenia przeczytania</translation> </message> @@ -955,7 +998,7 @@ Rozmiar multimediów: %2 <translation>Nie udaÅ‚o siÄ™ wÅ‚Ä…czyć szyfrowania: %1</translation> </message> <message> - <location line="+149"/> + <location line="+148"/> <source>Select an avatar</source> <translation>Wybierz awatar</translation> </message> @@ -981,19 +1024,6 @@ Rozmiar multimediów: %2 <translation>Nie udaÅ‚o siÄ™ wysÅ‚ać obrazu: %s</translation> </message> </context> -<context> - <name>dialogs::UserMentions</name> - <message> - <location filename="../../src/dialogs/UserMentions.cpp" line="+53"/> - <source>This Room</source> - <translation type="unfinished"></translation> - </message> - <message> - <location line="+1"/> - <source>All Rooms</source> - <translation type="unfinished"></translation> - </message> -</context> <context> <name>dialogs::UserProfile</name> <message> @@ -1017,7 +1047,7 @@ Rozmiar multimediów: %2 <translation>Rozpocznij rozmowÄ™</translation> </message> <message> - <location line="+57"/> + <location line="+56"/> <source>Devices</source> <translation>UrzÄ…dzenia</translation> </message> @@ -1068,69 +1098,103 @@ Rozmiar multimediów: %2 <context> <name>message-description sent:</name> <message> - <location filename="../../src/Utils.h" line="+104"/> - <source>%1 an audio clip</source> + <location filename="../../src/Utils.h" line="+95"/> + <source>You sent an audio clip</source> <translation type="unfinished"></translation> </message> <message> <location line="+3"/> - <source>%1 an image</source> + <source>%1 sent an audio clip</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+5"/> + <source>You sent an image</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+3"/> + <source>%1 sent an image</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+5"/> + <source>You sent a file</source> <translation type="unfinished"></translation> </message> <message> <location line="+3"/> - <source>%1 a file</source> + <source>%1 sent a file</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+5"/> + <source>You sent a video</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+3"/> + <source>%1 sent a video</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+5"/> + <source>You sent a sticker</source> <translation type="unfinished"></translation> </message> <message> <location line="+3"/> - <source>%1 a video clip</source> + <source>%1 sent a sticker</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+5"/> + <source>You sent a notification</source> <translation type="unfinished"></translation> </message> <message> <location line="+3"/> - <source>%1 a sticker</source> + <source>%1 sent a notification</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+5"/> + <source>You: %1</source> <translation type="unfinished"></translation> </message> <message> <location line="+3"/> - <source>%1 a notification</source> + <source>%1: %2</source> <translation type="unfinished"></translation> </message> <message> <location line="+7"/> - <source>%1 an encrypted message</source> + <source>You sent an encrypted message</source> <translation type="unfinished"></translation> </message> -</context> -<context> - <name>message-description:</name> <message> - <location line="-26"/> - <source>sent</source> - <comment>For when someone else is the sender</comment> + <location line="+3"/> + <source>%1 sent an encrypted message</source> <translation type="unfinished"></translation> </message> </context> <context> - <name>message-description: </name> + <name>popups::UserMentions</name> <message> - <location line="-2"/> - <source>sent</source> - <comment>For when you are the sender</comment> + <location filename="../../src/popups/UserMentions.cpp" line="+61"/> + <source>This Room</source> <translation type="unfinished"></translation> </message> -</context> -<context> - <name>utils</name> <message> - <location filename="../../src/Utils.cpp" line="+46"/> - <location filename="../../src/Utils.h" line="+55"/> - <source>You</source> + <location line="+1"/> + <source>All Rooms</source> <translation type="unfinished"></translation> </message> +</context> +<context> + <name>utils</name> <message> - <location line="+219"/> + <location filename="../../src/Utils.cpp" line="+282"/> <source>sent a file.</source> <translation type="unfinished"></translation> </message> @@ -1150,7 +1214,7 @@ Rozmiar multimediów: %2 <translation type="unfinished"></translation> </message> <message> - <location filename="../../src/Utils.h" line="-23"/> + <location filename="../../src/Utils.h" line="+4"/> <source>Unknown Message Type</source> <translation type="unfinished"></translation> </message> diff --git a/resources/langs/nheko_ru.ts b/resources/langs/nheko_ru.ts index 04285c7298c9f853132b31fa9b681f2c3fb71759..3069cdadb3347b81f9199e33e65e13519825015b 100644 --- a/resources/langs/nheko_ru.ts +++ b/resources/langs/nheko_ru.ts @@ -1,38 +1,15 @@ <?xml version="1.0" encoding="utf-8"?> <!DOCTYPE TS> <TS version="2.1" language="ru"> -<context> - <name>AudioItem</name> - <message> - <location filename="../../src/timeline/widgets/AudioItem.cpp" line="+118"/> - <source>Save File</source> - <translation>Сохранить файл</translation> - </message> -</context> <context> <name>ChatPage</name> <message> - <location filename="../../src/ChatPage.cpp" line="+330"/> - <source>Failed to upload image. Please try again.</source> - <translation>Ðе удалоÑÑŒ загрузить изображение. ПожалуйÑта, попробуйте еще раз.</translation> - </message> - <message> - <location line="+45"/> - <source>Failed to upload file. Please try again.</source> - <translation>Ðе удалоÑÑŒ загрузить файл. ПожалуйÑта, попробуйте еще раз.</translation> - </message> - <message> - <location line="+43"/> - <source>Failed to upload audio. Please try again.</source> - <translation>Ðе удалоÑÑŒ загрузить аудио. ПожалуйÑта, попробуйте еще раз.</translation> - </message> - <message> - <location line="+42"/> - <source>Failed to upload video. Please try again.</source> - <translation>Ðе удалоÑÑŒ загрузить видео. ПожалуйÑта, попробуйте еще раз.</translation> + <location filename="../../src/ChatPage.cpp" line="+346"/> + <source>Failed to upload media. Please try again.</source> + <translation type="unfinished"></translation> </message> <message> - <location line="+380"/> + <location line="+389"/> <source>Failed to restore OLM account. Please login again.</source> <translation>Ðе удалоÑÑŒ воÑÑтановить учетную запиÑÑŒ OLM. ПожалуйÑта, войдите Ñнова.</translation> </message> @@ -42,18 +19,18 @@ <translation>Ðе удалоÑÑŒ воÑÑтановить Ñохраненные данные. ПожалуйÑта, войдите Ñнова.</translation> </message> <message> - <location line="+198"/> + <location line="+181"/> <source>Failed to setup encryption keys. Server response: %1 %2. Please try again later.</source> <translation>Ðе удалоÑÑŒ наÑтроить ключи шифрованиÑ. Ответ Ñервера:%1 %2. ПожалуйÑта, попробуйте позже.</translation> </message> <message> <location line="+51"/> - <location line="+153"/> + <location line="+155"/> <source>Please try to login again: %1</source> <translation>Повторите попытку входа: %1</translation> </message> <message> - <location line="-45"/> + <location line="-47"/> <source>Room creation failed: %1</source> <translation>Ðе удалоÑÑŒ Ñоздать комнату: %1</translation> </message> @@ -116,19 +93,11 @@ </message> </context> <context> - <name>FileItem</name> + <name>EncryptionIndicator</name> <message> - <location filename="../../src/timeline/widgets/FileItem.cpp" line="+107"/> - <source>Save File</source> - <translation>Сохранить файл</translation> - </message> -</context> -<context> - <name>ImageItem</name> - <message> - <location filename="../../src/timeline/widgets/ImageItem.cpp" line="+241"/> - <source>Save image</source> - <translation>Сохранить изображение</translation> + <location filename="../qml/EncryptionIndicator.qml" line="+11"/> + <source>Encrypted</source> + <translation type="unfinished"></translation> </message> </context> <context> @@ -200,7 +169,7 @@ <context> <name>MemberList</name> <message> - <location filename="../../src/dialogs/MemberList.cpp" line="+96"/> + <location filename="../../src/dialogs/MemberList.cpp" line="+89"/> <source>Room members</source> <translation>УчаÑтники комнаты</translation> </message> @@ -210,6 +179,27 @@ <translation type="unfinished"></translation> </message> </context> +<context> + <name>MessageDelegate</name> + <message> + <location filename="../qml/delegates/MessageDelegate.qml" line="+43"/> + <source>redacted</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+6"/> + <source>Encryption enabled</source> + <translation type="unfinished"></translation> + </message> +</context> +<context> + <name>Placeholder</name> + <message> + <location filename="../qml/delegates/Placeholder.qml" line="+4"/> + <source>unimplemented event: </source> + <translation type="unfinished"></translation> + </message> +</context> <context> <name>QuickSwitcher</name> <message> @@ -277,7 +267,7 @@ <context> <name>RoomInfo</name> <message> - <location filename="../../src/Cache.cpp" line="+2205"/> + <location filename="../../src/Cache.cpp" line="+2307"/> <source>no version stored</source> <translation type="unfinished"></translation> </message> @@ -285,12 +275,12 @@ <context> <name>RoomInfoListItem</name> <message> - <location filename="../../src/RoomInfoListItem.cpp" line="+93"/> + <location filename="../../src/RoomInfoListItem.cpp" line="+95"/> <source>Leave room</source> <translation>Покинуть комнату</translation> </message> <message> - <location line="+181"/> + <location line="+161"/> <source>Accept</source> <translation>ПринÑÑ‚ÑŒ</translation> </message> @@ -331,36 +321,36 @@ <context> <name>StatusIndicator</name> <message> - <location filename="../../src/timeline/TimelineItem.cpp" line="+107"/> - <source>Encrypted</source> - <translation>Зашифровано</translation> + <location filename="../qml/StatusIndicator.qml" line="+13"/> + <source>Failed</source> + <translation type="unfinished"></translation> </message> <message> - <location line="+3"/> - <source>Delivered</source> - <translation>ДоÑтавлено</translation> + <location line="+1"/> + <source>Sent</source> + <translation type="unfinished"></translation> </message> <message> - <location line="+3"/> - <source>Seen</source> - <translation>Прочитано</translation> + <location line="+1"/> + <source>Received</source> + <translation type="unfinished"></translation> </message> <message> - <location line="+3"/> - <source>Sent</source> - <translation>Отправлено</translation> + <location line="+1"/> + <source>Read</source> + <translation type="unfinished"></translation> </message> </context> <context> <name>TextInputWidget</name> <message> - <location filename="../../src/TextInputWidget.cpp" line="+507"/> + <location filename="../../src/TextInputWidget.cpp" line="+502"/> <source>Send a file</source> <translation>Отправить файл</translation> </message> <message> <location line="+13"/> - <location filename="../../src/TextInputWidget.h" line="+164"/> + <location filename="../../src/TextInputWidget.h" line="+161"/> <source>Write a message...</source> <translation>ÐапиÑать Ñообщение...</translation> </message> @@ -375,7 +365,7 @@ <translation type="unfinished"></translation> </message> <message> - <location line="+75"/> + <location line="+72"/> <source>Select a file</source> <translation>Выберите файл</translation> </message> @@ -391,32 +381,9 @@ </message> </context> <context> - <name>TimelineItem</name> - <message> - <location filename="../../src/timeline/TimelineItem.cpp" line="+85"/> - <source>Message redaction failed: %1</source> - <translation>Ошибка Ñ€ÐµÐ´Ð°ÐºÑ‚Ð¸Ñ€Ð¾Ð²Ð°Ð½Ð¸Ñ ÑообщениÑ: %1</translation> - </message> - <message> - <location line="+39"/> - <source>Reply</source> - <translation type="unfinished"></translation> - </message> + <name>TimelineModel</name> <message> - <location line="+11"/> - <source>Options</source> - <translation type="unfinished"></translation> - </message> -</context> -<context> - <name>TimelineView</name> - <message> - <location filename="../../src/timeline/TimelineView.cpp" line="+245"/> - <source>Encryption is enabled</source> - <translation>Шифрование включено</translation> - </message> - <message> - <location line="+65"/> + <location filename="../../src/timeline/TimelineModel.cpp" line="+835"/> <source>-- Encrypted Event (No keys found for decryption) --</source> <comment>Placeholder, when the message was not decrypted yet or can't be decrypted</comment> <translation type="unfinished"></translation> @@ -440,16 +407,87 @@ <translation type="unfinished"></translation> </message> <message> - <location line="+27"/> + <location line="+25"/> <source>-- Encrypted Event (Unknown event type) --</source> <comment>Placeholder, when the message was decrypted, but we couldn't parse it, because Nheko/mtxclient don't support that event type yet</comment> <translation type="unfinished"></translation> </message> + <message> + <location line="+54"/> + <source>Message redaction failed: %1</source> + <translation type="unfinished">Ошибка Ñ€ÐµÐ´Ð°ÐºÑ‚Ð¸Ñ€Ð¾Ð²Ð°Ð½Ð¸Ñ ÑообщениÑ: %1</translation> + </message> + <message> + <location line="+453"/> + <source>Save image</source> + <translation type="unfinished">Сохранить изображение</translation> + </message> + <message> + <location line="+2"/> + <source>Save video</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+2"/> + <source>Save audio</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+2"/> + <source>Save file</source> + <translation type="unfinished"></translation> + </message> +</context> +<context> + <name>TimelineRow</name> + <message> + <location filename="../qml/TimelineRow.qml" line="+57"/> + <source>Reply</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+14"/> + <source>Options</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+12"/> + <source>Read receipts</source> + <translation type="unfinished">Подтверждать прочтение</translation> + </message> + <message> + <location line="+4"/> + <source>Mark as read</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+3"/> + <source>View raw message</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+4"/> + <source>Redact message</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+5"/> + <source>Save as</source> + <translation type="unfinished"></translation> + </message> +</context> +<context> + <name>TimelineView</name> + <message> + <location filename="../qml/TimelineView.qml" line="+24"/> + <source>No room open</source> + <translation type="unfinished"></translation> + </message> </context> <context> <name>TopRoomBar</name> <message> - <location filename="../../src/TopRoomBar.cpp" line="+79"/> + <location filename="../../src/TopRoomBar.cpp" line="+78"/> <source>Room options</source> <translation>ÐаÑтройки комнаты</translation> </message> @@ -516,7 +554,7 @@ <context> <name>UserSettingsPage</name> <message> - <location filename="../../src/UserSettingsPage.cpp" line="+166"/> + <location filename="../../src/UserSettingsPage.cpp" line="+171"/> <source>Minimize to tray</source> <translation>Сворачивать в ÑиÑтемную панель</translation> </message> @@ -530,6 +568,11 @@ <source>Group's sidebar</source> <translation>Ð‘Ð¾ÐºÐ¾Ð²Ð°Ñ Ð¿Ð°Ð½ÐµÐ»ÑŒ групп</translation> </message> + <message> + <location line="+9"/> + <source>Circular Avatars</source> + <translation type="unfinished"></translation> + </message> <message> <location line="+9"/> <source>Typing notifications</source> @@ -606,7 +649,7 @@ <translation>ГЛÐÐ’ÐОЕ</translation> </message> <message> - <location line="+156"/> + <location line="+161"/> <source>Open Sessions File</source> <translation>Открыть файл ÑеанÑов</translation> </message> @@ -827,7 +870,7 @@ Media size: %2 <context> <name>dialogs::ReadReceipts</name> <message> - <location filename="../../src/dialogs/ReadReceipts.cpp" line="+121"/> + <location filename="../../src/dialogs/ReadReceipts.cpp" line="+117"/> <source>Read receipts</source> <translation>Подтверждать прочтение</translation> </message> @@ -954,7 +997,7 @@ Media size: %2 <translation>Ðе удалоÑÑŒ включить шифрование: %1</translation> </message> <message> - <location line="+149"/> + <location line="+148"/> <source>Select an avatar</source> <translation>Выберите аватар</translation> </message> @@ -980,19 +1023,6 @@ Media size: %2 <translation>Ðе удалоÑÑŒ загрузить изображение: %s</translation> </message> </context> -<context> - <name>dialogs::UserMentions</name> - <message> - <location filename="../../src/dialogs/UserMentions.cpp" line="+53"/> - <source>This Room</source> - <translation type="unfinished"></translation> - </message> - <message> - <location line="+1"/> - <source>All Rooms</source> - <translation type="unfinished"></translation> - </message> -</context> <context> <name>dialogs::UserProfile</name> <message> @@ -1016,7 +1046,7 @@ Media size: %2 <translation>Ðачать разговор</translation> </message> <message> - <location line="+57"/> + <location line="+56"/> <source>Devices</source> <translation>УÑтройÑтва</translation> </message> @@ -1067,69 +1097,103 @@ Media size: %2 <context> <name>message-description sent:</name> <message> - <location filename="../../src/Utils.h" line="+104"/> - <source>%1 an audio clip</source> + <location filename="../../src/Utils.h" line="+95"/> + <source>You sent an audio clip</source> <translation type="unfinished"></translation> </message> <message> <location line="+3"/> - <source>%1 an image</source> + <source>%1 sent an audio clip</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+5"/> + <source>You sent an image</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+3"/> + <source>%1 sent an image</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+5"/> + <source>You sent a file</source> <translation type="unfinished"></translation> </message> <message> <location line="+3"/> - <source>%1 a file</source> + <source>%1 sent a file</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+5"/> + <source>You sent a video</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+3"/> + <source>%1 sent a video</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+5"/> + <source>You sent a sticker</source> <translation type="unfinished"></translation> </message> <message> <location line="+3"/> - <source>%1 a video clip</source> + <source>%1 sent a sticker</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+5"/> + <source>You sent a notification</source> <translation type="unfinished"></translation> </message> <message> <location line="+3"/> - <source>%1 a sticker</source> + <source>%1 sent a notification</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+5"/> + <source>You: %1</source> <translation type="unfinished"></translation> </message> <message> <location line="+3"/> - <source>%1 a notification</source> + <source>%1: %2</source> <translation type="unfinished"></translation> </message> <message> <location line="+7"/> - <source>%1 an encrypted message</source> + <source>You sent an encrypted message</source> <translation type="unfinished"></translation> </message> -</context> -<context> - <name>message-description:</name> <message> - <location line="-26"/> - <source>sent</source> - <comment>For when someone else is the sender</comment> + <location line="+3"/> + <source>%1 sent an encrypted message</source> <translation type="unfinished"></translation> </message> </context> <context> - <name>message-description: </name> + <name>popups::UserMentions</name> <message> - <location line="-2"/> - <source>sent</source> - <comment>For when you are the sender</comment> + <location filename="../../src/popups/UserMentions.cpp" line="+61"/> + <source>This Room</source> <translation type="unfinished"></translation> </message> -</context> -<context> - <name>utils</name> <message> - <location filename="../../src/Utils.cpp" line="+46"/> - <location filename="../../src/Utils.h" line="+55"/> - <source>You</source> + <location line="+1"/> + <source>All Rooms</source> <translation type="unfinished"></translation> </message> +</context> +<context> + <name>utils</name> <message> - <location line="+219"/> + <location filename="../../src/Utils.cpp" line="+282"/> <source>sent a file.</source> <translation type="unfinished"></translation> </message> @@ -1149,7 +1213,7 @@ Media size: %2 <translation type="unfinished"></translation> </message> <message> - <location filename="../../src/Utils.h" line="-23"/> + <location filename="../../src/Utils.h" line="+4"/> <source>Unknown Message Type</source> <translation type="unfinished"></translation> </message> diff --git a/resources/langs/nheko_zh_CN.ts b/resources/langs/nheko_zh_CN.ts index 1e539e64f75d2a2a418e1349e6b642a1a68ab29a..31ca068cdfb9d48e64d89a575ad3d34719c453ed 100644 --- a/resources/langs/nheko_zh_CN.ts +++ b/resources/langs/nheko_zh_CN.ts @@ -1,38 +1,15 @@ <?xml version="1.0" encoding="utf-8"?> <!DOCTYPE TS> <TS version="2.1" language="zh_CN"> -<context> - <name>AudioItem</name> - <message> - <location filename="../../src/timeline/widgets/AudioItem.cpp" line="+118"/> - <source>Save File</source> - <translation>ä¿å˜æ–‡ä»¶</translation> - </message> -</context> <context> <name>ChatPage</name> <message> - <location filename="../../src/ChatPage.cpp" line="+330"/> - <source>Failed to upload image. Please try again.</source> - <translation>ä¸Šä¼ å›¾åƒå¤±è´¥ã€‚请é‡è¯•ã€‚</translation> - </message> - <message> - <location line="+45"/> - <source>Failed to upload file. Please try again.</source> - <translation>ä¸Šä¼ æ–‡ä»¶å¤±è´¥ï¼Œè¯·é‡è¯•ã€‚</translation> - </message> - <message> - <location line="+43"/> - <source>Failed to upload audio. Please try again.</source> - <translation>ä¸Šä¼ éŸ³é¢‘å¤±è´¥ã€‚è¯·é‡è¯•ã€‚</translation> - </message> - <message> - <location line="+42"/> - <source>Failed to upload video. Please try again.</source> - <translation>ä¸Šä¼ è§†é¢‘å¤±è´¥ã€‚è¯·é‡è¯•ã€‚</translation> + <location filename="../../src/ChatPage.cpp" line="+346"/> + <source>Failed to upload media. Please try again.</source> + <translation type="unfinished"></translation> </message> <message> - <location line="+380"/> + <location line="+389"/> <source>Failed to restore OLM account. Please login again.</source> <translation>æ¢å¤ OLM 账户失败。请é‡æ–°ç™»å½•ã€‚</translation> </message> @@ -42,18 +19,18 @@ <translation>æ¢å¤ä¿å˜çš„æ•°æ®å¤±è´¥ã€‚请é‡æ–°ç™»å½•ã€‚</translation> </message> <message> - <location line="+198"/> + <location line="+181"/> <source>Failed to setup encryption keys. Server response: %1 %2. Please try again later.</source> <translation type="unfinished"></translation> </message> <message> <location line="+51"/> - <location line="+153"/> + <location line="+155"/> <source>Please try to login again: %1</source> <translation>请å°è¯•å†æ¬¡ç™»å½•ï¼š%1</translation> </message> <message> - <location line="-45"/> + <location line="-47"/> <source>Room creation failed: %1</source> <translation>创建èŠå¤©å®¤å¤±è´¥ï¼š%1</translation> </message> @@ -116,19 +93,11 @@ </message> </context> <context> - <name>FileItem</name> + <name>EncryptionIndicator</name> <message> - <location filename="../../src/timeline/widgets/FileItem.cpp" line="+107"/> - <source>Save File</source> - <translation>ä¿å˜æ–‡ä»¶</translation> - </message> -</context> -<context> - <name>ImageItem</name> - <message> - <location filename="../../src/timeline/widgets/ImageItem.cpp" line="+241"/> - <source>Save image</source> - <translation>ä¿å˜å›¾åƒ</translation> + <location filename="../qml/EncryptionIndicator.qml" line="+11"/> + <source>Encrypted</source> + <translation type="unfinished"></translation> </message> </context> <context> @@ -200,7 +169,7 @@ <context> <name>MemberList</name> <message> - <location filename="../../src/dialogs/MemberList.cpp" line="+96"/> + <location filename="../../src/dialogs/MemberList.cpp" line="+89"/> <source>Room members</source> <translation>èŠå¤©å®¤æˆå‘˜</translation> </message> @@ -210,6 +179,27 @@ <translation type="unfinished"></translation> </message> </context> +<context> + <name>MessageDelegate</name> + <message> + <location filename="../qml/delegates/MessageDelegate.qml" line="+43"/> + <source>redacted</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+6"/> + <source>Encryption enabled</source> + <translation type="unfinished"></translation> + </message> +</context> +<context> + <name>Placeholder</name> + <message> + <location filename="../qml/delegates/Placeholder.qml" line="+4"/> + <source>unimplemented event: </source> + <translation type="unfinished"></translation> + </message> +</context> <context> <name>QuickSwitcher</name> <message> @@ -277,7 +267,7 @@ <context> <name>RoomInfo</name> <message> - <location filename="../../src/Cache.cpp" line="+2205"/> + <location filename="../../src/Cache.cpp" line="+2307"/> <source>no version stored</source> <translation type="unfinished"></translation> </message> @@ -285,12 +275,12 @@ <context> <name>RoomInfoListItem</name> <message> - <location filename="../../src/RoomInfoListItem.cpp" line="+93"/> + <location filename="../../src/RoomInfoListItem.cpp" line="+95"/> <source>Leave room</source> <translation>离开èŠå¤©å®¤</translation> </message> <message> - <location line="+181"/> + <location line="+161"/> <source>Accept</source> <translation>接å—</translation> </message> @@ -331,36 +321,36 @@ <context> <name>StatusIndicator</name> <message> - <location filename="../../src/timeline/TimelineItem.cpp" line="+107"/> - <source>Encrypted</source> - <translation>åŠ å¯†çš„</translation> + <location filename="../qml/StatusIndicator.qml" line="+13"/> + <source>Failed</source> + <translation type="unfinished"></translation> </message> <message> - <location line="+3"/> - <source>Delivered</source> - <translation>å·²é€è¾¾</translation> + <location line="+1"/> + <source>Sent</source> + <translation type="unfinished"></translation> </message> <message> - <location line="+3"/> - <source>Seen</source> - <translation>已阅读</translation> + <location line="+1"/> + <source>Received</source> + <translation type="unfinished"></translation> </message> <message> - <location line="+3"/> - <source>Sent</source> - <translation>å·²å‘é€</translation> + <location line="+1"/> + <source>Read</source> + <translation type="unfinished"></translation> </message> </context> <context> <name>TextInputWidget</name> <message> - <location filename="../../src/TextInputWidget.cpp" line="+507"/> + <location filename="../../src/TextInputWidget.cpp" line="+502"/> <source>Send a file</source> <translation>å‘é€ä¸€ä¸ªæ–‡ä»¶</translation> </message> <message> <location line="+13"/> - <location filename="../../src/TextInputWidget.h" line="+164"/> + <location filename="../../src/TextInputWidget.h" line="+161"/> <source>Write a message...</source> <translation>写一æ¡æ¶ˆæ¯...</translation> </message> @@ -375,7 +365,7 @@ <translation></translation> </message> <message> - <location line="+75"/> + <location line="+72"/> <source>Select a file</source> <translation>选择一个文件</translation> </message> @@ -391,32 +381,9 @@ </message> </context> <context> - <name>TimelineItem</name> - <message> - <location filename="../../src/timeline/TimelineItem.cpp" line="+85"/> - <source>Message redaction failed: %1</source> - <translation>åˆ é™¤æ¶ˆæ¯å¤±è´¥ï¼š%1</translation> - </message> - <message> - <location line="+39"/> - <source>Reply</source> - <translation type="unfinished"></translation> - </message> + <name>TimelineModel</name> <message> - <location line="+11"/> - <source>Options</source> - <translation type="unfinished"></translation> - </message> -</context> -<context> - <name>TimelineView</name> - <message> - <location filename="../../src/timeline/TimelineView.cpp" line="+245"/> - <source>Encryption is enabled</source> - <translation>åŠ å¯†å·²å¯ç”¨</translation> - </message> - <message> - <location line="+65"/> + <location filename="../../src/timeline/TimelineModel.cpp" line="+835"/> <source>-- Encrypted Event (No keys found for decryption) --</source> <comment>Placeholder, when the message was not decrypted yet or can't be decrypted</comment> <translation type="unfinished"></translation> @@ -440,16 +407,87 @@ <translation type="unfinished"></translation> </message> <message> - <location line="+27"/> + <location line="+25"/> <source>-- Encrypted Event (Unknown event type) --</source> <comment>Placeholder, when the message was decrypted, but we couldn't parse it, because Nheko/mtxclient don't support that event type yet</comment> <translation type="unfinished"></translation> </message> + <message> + <location line="+54"/> + <source>Message redaction failed: %1</source> + <translation type="unfinished">åˆ é™¤æ¶ˆæ¯å¤±è´¥ï¼š%1</translation> + </message> + <message> + <location line="+453"/> + <source>Save image</source> + <translation type="unfinished">ä¿å˜å›¾åƒ</translation> + </message> + <message> + <location line="+2"/> + <source>Save video</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+2"/> + <source>Save audio</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+2"/> + <source>Save file</source> + <translation type="unfinished"></translation> + </message> +</context> +<context> + <name>TimelineRow</name> + <message> + <location filename="../qml/TimelineRow.qml" line="+57"/> + <source>Reply</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+14"/> + <source>Options</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+12"/> + <source>Read receipts</source> + <translation type="unfinished">阅读回执</translation> + </message> + <message> + <location line="+4"/> + <source>Mark as read</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+3"/> + <source>View raw message</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+4"/> + <source>Redact message</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+5"/> + <source>Save as</source> + <translation type="unfinished"></translation> + </message> +</context> +<context> + <name>TimelineView</name> + <message> + <location filename="../qml/TimelineView.qml" line="+24"/> + <source>No room open</source> + <translation type="unfinished"></translation> + </message> </context> <context> <name>TopRoomBar</name> <message> - <location filename="../../src/TopRoomBar.cpp" line="+79"/> + <location filename="../../src/TopRoomBar.cpp" line="+78"/> <source>Room options</source> <translation>èŠå¤©å®¤é€‰é¡¹</translation> </message> @@ -514,7 +552,7 @@ <context> <name>UserSettingsPage</name> <message> - <location filename="../../src/UserSettingsPage.cpp" line="+166"/> + <location filename="../../src/UserSettingsPage.cpp" line="+171"/> <source>Minimize to tray</source> <translation>最å°åŒ–至托盘</translation> </message> @@ -528,6 +566,11 @@ <source>Group's sidebar</source> <translation>群组侧边æ </translation> </message> + <message> + <location line="+9"/> + <source>Circular Avatars</source> + <translation type="unfinished"></translation> + </message> <message> <location line="+9"/> <source>Typing notifications</source> @@ -604,7 +647,7 @@ <translation>通用</translation> </message> <message> - <location line="+156"/> + <location line="+161"/> <source>Open Sessions File</source> <translation>打开会è¯æ–‡ä»¶</translation> </message> @@ -824,7 +867,7 @@ Media size: %2 <context> <name>dialogs::ReadReceipts</name> <message> - <location filename="../../src/dialogs/ReadReceipts.cpp" line="+121"/> + <location filename="../../src/dialogs/ReadReceipts.cpp" line="+117"/> <source>Read receipts</source> <translation>阅读回执</translation> </message> @@ -951,7 +994,7 @@ Media size: %2 <translation>å¯ç”¨åŠ 密失败:%1</translation> </message> <message> - <location line="+149"/> + <location line="+148"/> <source>Select an avatar</source> <translation>选择一个头åƒ</translation> </message> @@ -977,19 +1020,6 @@ Media size: %2 <translation>ä¸Šä¼ å›¾åƒå¤±è´¥ï¼š%s</translation> </message> </context> -<context> - <name>dialogs::UserMentions</name> - <message> - <location filename="../../src/dialogs/UserMentions.cpp" line="+53"/> - <source>This Room</source> - <translation type="unfinished"></translation> - </message> - <message> - <location line="+1"/> - <source>All Rooms</source> - <translation type="unfinished"></translation> - </message> -</context> <context> <name>dialogs::UserProfile</name> <message> @@ -1013,7 +1043,7 @@ Media size: %2 <translation>开始一个èŠå¤©</translation> </message> <message> - <location line="+57"/> + <location line="+56"/> <source>Devices</source> <translation>设备</translation> </message> @@ -1072,69 +1102,103 @@ Media size: %2 <context> <name>message-description sent:</name> <message> - <location filename="../../src/Utils.h" line="+104"/> - <source>%1 an audio clip</source> + <location filename="../../src/Utils.h" line="+95"/> + <source>You sent an audio clip</source> <translation type="unfinished"></translation> </message> <message> <location line="+3"/> - <source>%1 an image</source> + <source>%1 sent an audio clip</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+5"/> + <source>You sent an image</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+3"/> + <source>%1 sent an image</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+5"/> + <source>You sent a file</source> <translation type="unfinished"></translation> </message> <message> <location line="+3"/> - <source>%1 a file</source> + <source>%1 sent a file</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+5"/> + <source>You sent a video</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+3"/> + <source>%1 sent a video</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+5"/> + <source>You sent a sticker</source> <translation type="unfinished"></translation> </message> <message> <location line="+3"/> - <source>%1 a video clip</source> + <source>%1 sent a sticker</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+5"/> + <source>You sent a notification</source> <translation type="unfinished"></translation> </message> <message> <location line="+3"/> - <source>%1 a sticker</source> + <source>%1 sent a notification</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+5"/> + <source>You: %1</source> <translation type="unfinished"></translation> </message> <message> <location line="+3"/> - <source>%1 a notification</source> + <source>%1: %2</source> <translation type="unfinished"></translation> </message> <message> <location line="+7"/> - <source>%1 an encrypted message</source> + <source>You sent an encrypted message</source> <translation type="unfinished"></translation> </message> -</context> -<context> - <name>message-description:</name> <message> - <location line="-26"/> - <source>sent</source> - <comment>For when someone else is the sender</comment> + <location line="+3"/> + <source>%1 sent an encrypted message</source> <translation type="unfinished"></translation> </message> </context> <context> - <name>message-description: </name> + <name>popups::UserMentions</name> <message> - <location line="-2"/> - <source>sent</source> - <comment>For when you are the sender</comment> + <location filename="../../src/popups/UserMentions.cpp" line="+61"/> + <source>This Room</source> <translation type="unfinished"></translation> </message> -</context> -<context> - <name>utils</name> <message> - <location filename="../../src/Utils.cpp" line="+46"/> - <location filename="../../src/Utils.h" line="+55"/> - <source>You</source> + <location line="+1"/> + <source>All Rooms</source> <translation type="unfinished"></translation> </message> +</context> +<context> + <name>utils</name> <message> - <location line="+219"/> + <location filename="../../src/Utils.cpp" line="+282"/> <source>sent a file.</source> <translation type="unfinished"></translation> </message> @@ -1154,7 +1218,7 @@ Media size: %2 <translation type="unfinished"></translation> </message> <message> - <location filename="../../src/Utils.h" line="-23"/> + <location filename="../../src/Utils.h" line="+4"/> <source>Unknown Message Type</source> <translation type="unfinished"></translation> </message> diff --git a/resources/qml/Avatar.qml b/resources/qml/Avatar.qml new file mode 100644 index 0000000000000000000000000000000000000000..a53f057b20d1be4c05efdb11c96a4ceeae82bc7b --- /dev/null +++ b/resources/qml/Avatar.qml @@ -0,0 +1,51 @@ +import QtQuick 2.6 +import QtGraphicalEffects 1.0 +import Qt.labs.settings 1.0 + +Rectangle { + id: avatar + width: 48 + height: 48 + radius: settings.avatar_circles ? height/2 : 3 + + Settings { + id: settings + category: "user" + property bool avatar_circles: true + } + + property alias url: img.source + property string displayName + + Text { + anchors.fill: parent + text: String.fromCodePoint(displayName.codePointAt(0)) + color: colors.text + font.pixelSize: avatar.height/2 + verticalAlignment: Text.AlignVCenter + horizontalAlignment: Text.AlignHCenter + } + + Image { + id: img + anchors.fill: parent + asynchronous: true + fillMode: Image.PreserveAspectCrop + mipmap: true + smooth: false + + sourceSize.width: avatar.width + sourceSize.height: avatar.height + + layer.enabled: true + layer.effect: OpacityMask { + maskSource: Rectangle { + anchors.fill: parent + width: avatar.width + height: avatar.height + radius: settings.avatar_circles ? height/2 : 3 + } + } + } + color: colors.dark +} diff --git a/resources/qml/EncryptionIndicator.qml b/resources/qml/EncryptionIndicator.qml new file mode 100644 index 0000000000000000000000000000000000000000..905cf9346ac34e2fb783e7ff664229451e9d68e2 --- /dev/null +++ b/resources/qml/EncryptionIndicator.qml @@ -0,0 +1,24 @@ +import QtQuick 2.5 +import QtQuick.Controls 2.1 +import im.nheko 1.0 + +Rectangle { + id: indicator + color: "transparent" + width: 16 + height: 16 + ToolTip.visible: ma.containsMouse && indicator.visible + ToolTip.text: qsTr("Encrypted") + MouseArea{ + id: ma + anchors.fill: parent + hoverEnabled: true + } + + Image { + id: stateImg + anchors.fill: parent + source: "image://colorimage/:/icons/icons/ui/lock.png?"+colors.buttonText + } +} + diff --git a/resources/qml/ImageButton.qml b/resources/qml/ImageButton.qml new file mode 100644 index 0000000000000000000000000000000000000000..dc576e183eb9aebddac9e912bc393a8118f92d82 --- /dev/null +++ b/resources/qml/ImageButton.qml @@ -0,0 +1,29 @@ +import QtQuick 2.3 +import QtQuick.Controls 2.3 + +Button { + property string image: undefined + + id: button + + flat: true + + // disable background, because we don't want a border on hover + background: Item { + } + + Image { + id: buttonImg + // Workaround, can't get icon.source working for now... + anchors.fill: parent + source: "image://colorimage/" + image + "?" + (button.hovered ? colors.highlight : colors.buttonText) + } + + MouseArea + { + id: mouseArea + anchors.fill: parent + onPressed: mouse.accepted = false + cursorShape: Qt.PointingHandCursor + } +} diff --git a/resources/qml/MatrixText.qml b/resources/qml/MatrixText.qml new file mode 100644 index 0000000000000000000000000000000000000000..46e7471179b13ad15fbae1fc2888f9686efbfed1 --- /dev/null +++ b/resources/qml/MatrixText.qml @@ -0,0 +1,33 @@ +import QtQuick 2.5 +import QtQuick.Controls 2.3 + +TextEdit { + textFormat: TextEdit.RichText + readOnly: true + wrapMode: Text.Wrap + selectByMouse: true + color: colors.text + + onLinkActivated: { + if (/^https:\/\/matrix.to\/#\/(@.*)$/.test(link)) chat.model.openUserProfile(/^https:\/\/matrix.to\/#\/(@.*)$/.exec(link)[1]) + else if (/^https:\/\/matrix.to\/#\/(![^\/]*)$/.test(link)) timelineManager.setHistoryView(/^https:\/\/matrix.to\/#\/(!.*)$/.exec(link)[1]) + else if (/^https:\/\/matrix.to\/#\/(![^\/]*)\/(\$.*)$/.test(link)) { + var match = /^https:\/\/matrix.to\/#\/(![^\/]*)\/(\$.*)$/.exec(link) + timelineManager.setHistoryView(match[1]) + chat.positionViewAtIndex(chat.model.idToIndex(match[2]), ListView.Contain) + } + else Qt.openUrlExternally(link) + } + MouseArea + { + anchors.fill: parent + onPressed: mouse.accepted = false + cursorShape: parent.hoveredLink ? Qt.PointingHandCursor : Qt.ArrowCursor + } + + ToolTip { + visible: parent.hoveredLink + text: parent.hoveredLink + palette: colors + } +} diff --git a/resources/qml/StatusIndicator.qml b/resources/qml/StatusIndicator.qml new file mode 100644 index 0000000000000000000000000000000000000000..91e8f76990f146a82ffc8326d131f65dd61c0f22 --- /dev/null +++ b/resources/qml/StatusIndicator.qml @@ -0,0 +1,38 @@ +import QtQuick 2.5 +import QtQuick.Controls 2.1 +import im.nheko 1.0 + +Rectangle { + id: indicator + property int state: 0 + color: "transparent" + width: 16 + height: 16 + ToolTip.visible: ma.containsMouse && state != MtxEvent.Empty + ToolTip.text: switch (state) { + case MtxEvent.Failed: return qsTr("Failed") + case MtxEvent.Sent: return qsTr("Sent") + case MtxEvent.Received: return qsTr("Received") + case MtxEvent.Read: return qsTr("Read") + default: return "" + } + MouseArea{ + id: ma + anchors.fill: parent + hoverEnabled: true + } + + Image { + id: stateImg + // Workaround, can't get icon.source working for now... + anchors.fill: parent + source: switch (indicator.state) { + case MtxEvent.Failed: return "image://colorimage/:/icons/icons/ui/remove-symbol.png?" + colors.buttonText + case MtxEvent.Sent: return "image://colorimage/:/icons/icons/ui/clock.png?" + colors.buttonText + case MtxEvent.Received: return "image://colorimage/:/icons/icons/ui/checkmark.png?" + colors.buttonText + case MtxEvent.Read: return "image://colorimage/:/icons/icons/ui/double-tick-indicator.png?" + colors.buttonText + default: return "" + } + } +} + diff --git a/resources/qml/TimelineRow.qml b/resources/qml/TimelineRow.qml new file mode 100644 index 0000000000000000000000000000000000000000..2c2ed02ad614fd90ce642e392aff7a45a51395d0 --- /dev/null +++ b/resources/qml/TimelineRow.qml @@ -0,0 +1,122 @@ +import QtQuick 2.6 +import QtQuick.Controls 2.3 +import QtQuick.Layouts 1.2 +import QtQuick.Window 2.2 + +import im.nheko 1.0 + +import "./delegates" + +RowLayout { + property var view: chat + + anchors.leftMargin: avatarSize + 4 + anchors.left: parent.left + anchors.right: parent.right + + height: Math.max(contentItem.height, 16) + + Column { + Layout.fillWidth: true + Layout.alignment: Qt.AlignTop + + //property var replyTo: model.replyTo + + //Text { + // property int idx: timelineManager.timeline.idToIndex(replyTo) + // text: "" + (idx != -1 ? timelineManager.timeline.data(timelineManager.timeline.index(idx, 0), 2) : "nothing") + //} + MessageDelegate { + id: contentItem + + width: parent.width + height: childrenRect.height + } + } + + StatusIndicator { + state: model.state + Layout.alignment: Qt.AlignRight | Qt.AlignTop + Layout.preferredHeight: 16 + } + + EncryptionIndicator { + visible: model.isEncrypted + Layout.alignment: Qt.AlignRight | Qt.AlignTop + Layout.preferredHeight: 16 + } + + ImageButton { + Layout.alignment: Qt.AlignRight | Qt.AlignTop + Layout.preferredHeight: 16 + id: replyButton + + image: ":/icons/icons/ui/mail-reply.png" + ToolTip { + visible: replyButton.hovered + text: qsTr("Reply") + palette: colors + } + + onClicked: view.model.replyAction(model.id) + } + ImageButton { + Layout.alignment: Qt.AlignRight | Qt.AlignTop + Layout.preferredHeight: 16 + id: optionsButton + + image: ":/icons/icons/ui/vertical-ellipsis.png" + ToolTip { + visible: optionsButton.hovered + text: qsTr("Options") + palette: colors + } + + onClicked: contextMenu.open() + + Menu { + y: optionsButton.height + id: contextMenu + palette: colors + + MenuItem { + text: qsTr("Read receipts") + onTriggered: view.model.readReceiptsAction(model.id) + } + MenuItem { + text: qsTr("Mark as read") + } + MenuItem { + text: qsTr("View raw message") + onTriggered: view.model.viewRawMessage(model.id) + } + MenuItem { + text: qsTr("Redact message") + onTriggered: view.model.redactEvent(model.id) + } + MenuItem { + visible: model.type == MtxEvent.ImageMessage || model.type == MtxEvent.VideoMessage || model.type == MtxEvent.AudioMessage || model.type == MtxEvent.FileMessage || model.type == MtxEvent.Sticker + text: qsTr("Save as") + onTriggered: timelineManager.timeline.saveMedia(model.id) + } + } + } + + Text { + Layout.alignment: Qt.AlignRight | Qt.AlignTop + text: model.timestamp.toLocaleTimeString("HH:mm") + color: inactiveColors.text + + MouseArea{ + id: ma + anchors.fill: parent + hoverEnabled: true + } + + ToolTip { + visible: ma.containsMouse + text: Qt.formatDateTime(model.timestamp, Qt.DefaultLocaleLongDate) + palette: colors + } + } +} diff --git a/resources/qml/TimelineView.qml b/resources/qml/TimelineView.qml new file mode 100644 index 0000000000000000000000000000000000000000..1a1900ad472e821138df5790558e5a3a894ee87f --- /dev/null +++ b/resources/qml/TimelineView.qml @@ -0,0 +1,185 @@ +import QtQuick 2.9 +import QtQuick.Controls 2.1 +import QtQuick.Layouts 1.2 +import QtGraphicalEffects 1.0 +import QtQuick.Window 2.2 + +import im.nheko 1.0 + +import "./delegates" + +Item { + property var colors: currentActivePalette + property var systemInactive: SystemPalette { colorGroup: SystemPalette.Disabled } + property var inactiveColors: currentInactivePalette ? currentInactivePalette : systemInactive + property int avatarSize: 40 + + Rectangle { + anchors.fill: parent + color: colors.window + + Text { + visible: !timelineManager.timeline && !timelineManager.isInitialSync + anchors.centerIn: parent + text: qsTr("No room open") + font.pointSize: 24 + color: colors.windowText + } + + BusyIndicator { + anchors.centerIn: parent + running: timelineManager.isInitialSync + height: 200 + width: 200 + } + + ListView { + id: chat + + cacheBuffer: 2000 + + visible: timelineManager.timeline != null + anchors.fill: parent + + anchors.leftMargin: 4 + anchors.rightMargin: scrollbar.width + + model: timelineManager.timeline + + boundsBehavior: Flickable.StopAtBounds + + onVerticalOvershootChanged: contentY = contentY - verticalOvershoot + + MouseArea { + anchors.fill: parent + acceptedButtons: Qt.NoButton + propagateComposedEvents: true + z: -1 + onWheel: { + if (wheel.angleDelta != 0) { + chat.contentY = chat.contentY - wheel.angleDelta.y + wheel.accepted = true + chat.forceLayout() + chat.updatePosition() + } + } + } + + onModelChanged: { + if (model) { + currentIndex = model.currentIndex + if (model.currentIndex == count - 1) { + positionViewAtEnd() + } else { + positionViewAtIndex(model.currentIndex, ListView.End) + } + } + } + + ScrollBar.vertical: ScrollBar { + id: scrollbar + parent: chat.parent + anchors.top: chat.top + anchors.left: chat.right + anchors.bottom: chat.bottom + onPressedChanged: if (!pressed) chat.updatePosition() + } + + property bool atBottom: false + onCountChanged: { + if (atBottom) { + var newIndex = count - 1 // last index + positionViewAtEnd() + currentIndex = newIndex + model.currentIndex = newIndex + } + + if (contentHeight < height && model) { + model.fetchHistory(); + } + } + + onAtYBeginningChanged: if (atYBeginning) { chat.model.currentIndex = 0; chat.currentIndex = 0; model.fetchHistory(); } + + function updatePosition() { + for (var y = chat.contentY + chat.height; y > chat.height; y -= 9) { + var i = chat.itemAt(100, y); + if (!i) continue; + if (!i.isFullyVisible()) continue; + chat.model.currentIndex = i.getIndex(); + chat.currentIndex = i.getIndex() + atBottom = i.getIndex() == count - 1; + break; + } + } + onMovementEnded: updatePosition() + + spacing: 4 + delegate: TimelineRow { + function isFullyVisible() { + return height > 1 && (y - chat.contentY - 1) + height < chat.height + } + function getIndex() { + return index; + } + } + + section { + property: "section" + delegate: Column { + topPadding: 4 + bottomPadding: 4 + spacing: 8 + + width: parent.width + height: (section.includes(" ") ? dateBubble.height + 8 + userName.height : userName.height) + 8 + + Label { + id: dateBubble + anchors.horizontalCenter: parent.horizontalCenter + visible: section.includes(" ") + text: chat.model.formatDateSeparator(new Date(Number(section.split(" ")[1]))) + color: colors.windowText + + height: contentHeight * 1.2 + width: contentWidth * 1.2 + horizontalAlignment: Text.AlignHCenter + background: Rectangle { + radius: parent.height / 2 + color: colors.dark + } + } + Row { + height: userName.height + spacing: 4 + Avatar { + width: avatarSize + height: avatarSize + url: chat.model.avatarUrl(section.split(" ")[0]).replace("mxc://", "image://MxcImage/") + displayName: chat.model.displayName(section.split(" ")[0]) + + MouseArea { + anchors.fill: parent + onClicked: chat.model.openUserProfile(section.split(" ")[0]) + cursorShape: Qt.PointingHandCursor + } + } + + Text { + id: userName + text: chat.model.escapeEmoji(chat.model.displayName(section.split(" ")[0])) + color: chat.model.userColor(section.split(" ")[0], colors.window) + textFormat: Text.RichText + + MouseArea { + anchors.fill: parent + onClicked: chat.model.openUserProfile(section.split(" ")[0]) + cursorShape: Qt.PointingHandCursor + } + } + } + } + } + } + } +} diff --git a/resources/qml/delegates/FileMessage.qml b/resources/qml/delegates/FileMessage.qml new file mode 100644 index 0000000000000000000000000000000000000000..2c911c5ee1cb3359d2cfecfc5f614dfdfa83b7db --- /dev/null +++ b/resources/qml/delegates/FileMessage.qml @@ -0,0 +1,57 @@ +import QtQuick 2.6 +import QtQuick.Layouts 1.2 + +Rectangle { + radius: 10 + color: colors.dark + height: row.height + 24 + width: parent ? parent.width : undefined + + RowLayout { + id: row + + anchors.centerIn: parent + width: parent.width - 24 + + spacing: 15 + + Rectangle { + id: button + color: colors.light + radius: 22 + height: 44 + width: 44 + Image { + id: img + anchors.centerIn: parent + + source: "qrc:/icons/icons/ui/arrow-pointing-down.png" + fillMode: Image.Pad + + } + MouseArea { + anchors.fill: parent + onClicked: timelineManager.timeline.saveMedia(model.id) + cursorShape: Qt.PointingHandCursor + } + } + ColumnLayout { + id: col + + Text { + Layout.fillWidth: true + text: model.body + textFormat: Text.PlainText + elide: Text.ElideRight + color: colors.text + } + Text { + Layout.fillWidth: true + text: model.filesize + textFormat: Text.PlainText + elide: Text.ElideRight + color: colors.text + } + } + } +} diff --git a/resources/qml/delegates/ImageMessage.qml b/resources/qml/delegates/ImageMessage.qml new file mode 100644 index 0000000000000000000000000000000000000000..1b6e572983825634d71f5a1c81a499f1f021dfa2 --- /dev/null +++ b/resources/qml/delegates/ImageMessage.qml @@ -0,0 +1,23 @@ +import QtQuick 2.6 + +import im.nheko 1.0 + +Item { + width: Math.min(parent ? parent.width : undefined, model.width) + height: width * model.proportionalHeight + + Image { + id: img + anchors.fill: parent + + source: model.url.replace("mxc://", "image://MxcImage/") + asynchronous: true + fillMode: Image.PreserveAspectFit + + MouseArea { + enabled: model.type == MtxEvent.ImageMessage + anchors.fill: parent + onClicked: timelineManager.openImageOverlay(model.url, model.id) + } + } +} diff --git a/resources/qml/delegates/MessageDelegate.qml b/resources/qml/delegates/MessageDelegate.qml new file mode 100644 index 0000000000000000000000000000000000000000..178dfd86054091dee556b9d66e47fd529c0e478d --- /dev/null +++ b/resources/qml/delegates/MessageDelegate.qml @@ -0,0 +1,55 @@ +import QtQuick 2.6 +import im.nheko 1.0 + +DelegateChooser { + //role: "type" //< not supported in our custom implementation, have to use roleValue + roleValue: model.type + + DelegateChoice { + roleValue: MtxEvent.TextMessage + TextMessage {} + } + DelegateChoice { + roleValue: MtxEvent.NoticeMessage + NoticeMessage {} + } + DelegateChoice { + roleValue: MtxEvent.EmoteMessage + TextMessage {} + } + DelegateChoice { + roleValue: MtxEvent.ImageMessage + ImageMessage {} + } + DelegateChoice { + roleValue: MtxEvent.Sticker + ImageMessage {} + } + DelegateChoice { + roleValue: MtxEvent.FileMessage + FileMessage {} + } + DelegateChoice { + roleValue: MtxEvent.VideoMessage + PlayableMediaMessage {} + } + DelegateChoice { + roleValue: MtxEvent.AudioMessage + PlayableMediaMessage {} + } + DelegateChoice { + roleValue: MtxEvent.Redacted + Pill { + text: qsTr("redacted") + } + } + DelegateChoice { + roleValue: MtxEvent.Encryption + Pill { + text: qsTr("Encryption enabled") + } + } + DelegateChoice { + Placeholder {} + } +} diff --git a/resources/qml/delegates/NoticeMessage.qml b/resources/qml/delegates/NoticeMessage.qml new file mode 100644 index 0000000000000000000000000000000000000000..a392eb5bb418ac0dd0ed456aeeff104f803be757 --- /dev/null +++ b/resources/qml/delegates/NoticeMessage.qml @@ -0,0 +1,8 @@ +import ".." + +MatrixText { + text: model.formattedBody + width: parent ? parent.width : undefined + font.italic: true + color: inactiveColors.text +} diff --git a/resources/qml/delegates/Pill.qml b/resources/qml/delegates/Pill.qml new file mode 100644 index 0000000000000000000000000000000000000000..53a9684e4949223648422ab028353c26a45f5716 --- /dev/null +++ b/resources/qml/delegates/Pill.qml @@ -0,0 +1,14 @@ +import QtQuick 2.5 +import QtQuick.Controls 2.1 + +Label { + color: inactiveColors.text + horizontalAlignment: Text.AlignHCenter + + height: contentHeight * 1.2 + width: contentWidth * 1.2 + background: Rectangle { + radius: parent.height / 2 + color: colors.dark + } +} diff --git a/resources/qml/delegates/Placeholder.qml b/resources/qml/delegates/Placeholder.qml new file mode 100644 index 0000000000000000000000000000000000000000..4c0e68c39b0db3e9b601d1efae00814a9999f88c --- /dev/null +++ b/resources/qml/delegates/Placeholder.qml @@ -0,0 +1,7 @@ +import ".." + +MatrixText { + text: qsTr("unimplemented event: ") + model.type + width: parent ? parent.width : undefined + color: inactiveColors.text +} diff --git a/resources/qml/delegates/PlayableMediaMessage.qml b/resources/qml/delegates/PlayableMediaMessage.qml new file mode 100644 index 0000000000000000000000000000000000000000..d0d4d7cb7dd23413f356af2eb18e9fc8644bac30 --- /dev/null +++ b/resources/qml/delegates/PlayableMediaMessage.qml @@ -0,0 +1,164 @@ +import QtQuick 2.6 +import QtQuick.Layouts 1.2 +import QtQuick.Controls 2.1 +import QtMultimedia 5.6 + +import im.nheko 1.0 + +Rectangle { + id: bg + radius: 10 + color: colors.dark + height: content.height + 24 + width: parent ? parent.width : undefined + + Column { + id: content + width: parent.width - 24 + anchors.centerIn: parent + + Rectangle { + id: videoContainer + visible: model.type == MtxEvent.VideoMessage + width: Math.min(parent.width, model.width ? model.width : 400) // some media has 0 as size... + height: width*model.proportionalHeight + Image { + anchors.fill: parent + source: model.thumbnailUrl.replace("mxc://", "image://MxcImage/") + asynchronous: true + fillMode: Image.PreserveAspectFit + + VideoOutput { + anchors.fill: parent + fillMode: VideoOutput.PreserveAspectFit + source: media + } + } + } + + RowLayout { + width: parent.width + Text { + id: positionText + text: "--:--:--" + color: colors.text + } + Slider { + Layout.fillWidth: true + id: progress + value: media.position + from: 0 + to: media.duration + + onMoved: media.seek(value) + //indeterminate: true + function updatePositionTexts() { + function formatTime(date) { + var hh = date.getUTCHours(); + var mm = date.getUTCMinutes(); + var ss = date.getSeconds(); + if (hh < 10) {hh = "0"+hh;} + if (mm < 10) {mm = "0"+mm;} + if (ss < 10) {ss = "0"+ss;} + return hh+":"+mm+":"+ss; + } + positionText.text = formatTime(new Date(media.position)) + durationText.text = formatTime(new Date(media.duration)) + } + onValueChanged: updatePositionTexts() + } + Text { + id: durationText + text: "--:--:--" + color: colors.text + } + } + + RowLayout { + width: parent.width + + spacing: 15 + + Rectangle { + id: button + color: colors.light + radius: 22 + height: 44 + width: 44 + Image { + id: img + anchors.centerIn: parent + + source: "qrc:/icons/icons/ui/arrow-pointing-down.png" + fillMode: Image.Pad + + } + MouseArea { + anchors.fill: parent + onClicked: { + switch (button.state) { + case "": timelineManager.timeline.cacheMedia(model.id); break; + case "stopped": + media.play(); console.log("play"); + button.state = "playing" + break + case "playing": + media.pause(); console.log("pause"); + button.state = "stopped" + break + } + } + cursorShape: Qt.PointingHandCursor + } + MediaPlayer { + id: media + onError: console.log(errorString) + onStatusChanged: if(status == MediaPlayer.Loaded) progress.updatePositionTexts() + onStopped: button.state = "stopped" + } + + Connections { + target: timelineManager.timeline + onMediaCached: { + if (mxcUrl == model.url) { + media.source = "file://" + cacheUrl + button.state = "stopped" + console.log("media loaded: " + mxcUrl + " at " + cacheUrl) + } + console.log("media cached: " + mxcUrl + " at " + cacheUrl) + } + } + + states: [ + State { + name: "stopped" + PropertyChanges { target: img; source: "qrc:/icons/icons/ui/play-sign.png" } + }, + State { + name: "playing" + PropertyChanges { target: img; source: "qrc:/icons/icons/ui/pause-symbol.png" } + } + ] + } + ColumnLayout { + id: col + + Text { + Layout.fillWidth: true + text: model.body + textFormat: Text.PlainText + elide: Text.ElideRight + color: colors.text + } + Text { + Layout.fillWidth: true + text: model.filesize + textFormat: Text.PlainText + elide: Text.ElideRight + color: colors.text + } + } + } + } +} + diff --git a/resources/qml/delegates/TextMessage.qml b/resources/qml/delegates/TextMessage.qml new file mode 100644 index 0000000000000000000000000000000000000000..f984b32f17428ad98de773c1eb69beaf48243d83 --- /dev/null +++ b/resources/qml/delegates/TextMessage.qml @@ -0,0 +1,6 @@ +import ".." + +MatrixText { + text: model.formattedBody.replace("<pre>", "<pre style='white-space: pre-wrap'>") + width: parent ? parent.width : undefined +} diff --git a/resources/res.qrc b/resources/res.qrc index ad27af5a437104533daa93b02f7cccf16b8523e0..53406c48ee73168e620524e7784f7a4f95e4701e 100644 --- a/resources/res.qrc +++ b/resources/res.qrc @@ -114,4 +114,21 @@ <file>styles/nheko.qss</file> <file>styles/nheko-dark.qss</file> </qresource> + <qresource prefix="/"> + <file>qml/TimelineView.qml</file> + <file>qml/Avatar.qml</file> + <file>qml/ImageButton.qml</file> + <file>qml/MatrixText.qml</file> + <file>qml/StatusIndicator.qml</file> + <file>qml/EncryptionIndicator.qml</file> + <file>qml/TimelineRow.qml</file> + <file>qml/delegates/MessageDelegate.qml</file> + <file>qml/delegates/TextMessage.qml</file> + <file>qml/delegates/NoticeMessage.qml</file> + <file>qml/delegates/ImageMessage.qml</file> + <file>qml/delegates/PlayableMediaMessage.qml</file> + <file>qml/delegates/FileMessage.qml</file> + <file>qml/delegates/Pill.qml</file> + <file>qml/delegates/Placeholder.qml</file> + </qresource> </RCC> diff --git a/src/AvatarProvider.cpp b/src/AvatarProvider.cpp index ec745c049f25475c7fe524e5002e3cf7a8925c6f..68b6901eedd00a9b77cb694a7cf1ce91745ab27d 100644 --- a/src/AvatarProvider.cpp +++ b/src/AvatarProvider.cpp @@ -43,7 +43,6 @@ resolve(const QString &avatarUrl, int size, QObject *receiver, AvatarCallback ca QPixmap pixmap; if (avatar_cache.find(cacheKey, &pixmap)) { - nhlog::net()->info("cached pixmap {}", avatarUrl.toStdString()); callback(pixmap); return; } @@ -52,7 +51,6 @@ resolve(const QString &avatarUrl, int size, QObject *receiver, AvatarCallback ca if (!data.isNull()) { pixmap.loadFromData(data); avatar_cache.insert(cacheKey, pixmap); - nhlog::net()->info("loaded pixmap from disk cache {}", avatarUrl.toStdString()); callback(pixmap); return; } @@ -69,8 +67,8 @@ resolve(const QString &avatarUrl, int size, QObject *receiver, AvatarCallback ca }); mtx::http::ThumbOpts opts; - opts.width = 256; - opts.height = 256; + opts.width = size; + opts.height = size; opts.mxc_url = avatarUrl.toStdString(); http::client()->get_thumbnail( @@ -86,8 +84,6 @@ resolve(const QString &avatarUrl, int size, QObject *receiver, AvatarCallback ca cache::client()->saveImage(opts.mxc_url, res); - nhlog::net()->info("downloaded pixmap {}", opts.mxc_url); - emit proxy->avatarDownloaded(QByteArray(res.data(), res.size())); }); } diff --git a/src/Cache.h b/src/Cache.h index 0da49793995bc5b3e4c600bef4a0e3c7b6a701bf..f5e1cfa08d32434c90407545abba66398adcb9ca 100644 --- a/src/Cache.h +++ b/src/Cache.h @@ -91,7 +91,6 @@ from_json(const json &j, ReadReceiptKey &key) struct DescInfo { QString event_id; - QString username; QString userid; QString body; QString timestamp; diff --git a/src/ChatPage.cpp b/src/ChatPage.cpp index 21ded4b3af4bccf43ead05cf19bd660a2a106618..d6f6940b60afc88299a2441a9e49998a6f3a39f3 100644 --- a/src/ChatPage.cpp +++ b/src/ChatPage.cpp @@ -54,6 +54,8 @@ constexpr int CHECK_CONNECTIVITY_INTERVAL = 15'000; constexpr int RETRY_TIMEOUT = 5'000; constexpr size_t MAX_ONETIME_KEYS = 50; +Q_DECLARE_METATYPE(boost::optional<mtx::crypto::EncryptedFile>) + ChatPage::ChatPage(QSharedPointer<UserSettings> userSettings, QWidget *parent) : QWidget(parent) , isConnected_(true) @@ -62,6 +64,9 @@ ChatPage::ChatPage(QSharedPointer<UserSettings> userSettings, QWidget *parent) { setObjectName("chatPage"); + qRegisterMetaType<boost::optional<mtx::crypto::EncryptedFile>>( + "boost::optional<mtx::crypto::EncryptedFile>"); + topLayout_ = new QHBoxLayout(this); topLayout_->setSpacing(0); topLayout_->setMargin(0); @@ -113,12 +118,7 @@ ChatPage::ChatPage(QSharedPointer<UserSettings> userSettings, QWidget *parent) view_manager_ = new TimelineViewManager(this); contentLayout_->addWidget(top_bar_); - contentLayout_->addWidget(view_manager_); - - connect(this, - &ChatPage::removeTimelineEvent, - view_manager_, - &TimelineViewManager::removeTimelineEvent); + contentLayout_->addWidget(view_manager_->getWidget()); // Splitter splitter->addWidget(sideBar_); @@ -304,9 +304,9 @@ ChatPage::ChatPage(QSharedPointer<UserSettings> userSettings, QWidget *parent) connect( text_input_, - &TextInputWidget::uploadImage, + &TextInputWidget::uploadMedia, this, - [this](QSharedPointer<QIODevice> dev, const QString &fn) { + [this](QSharedPointer<QIODevice> dev, QString mimeClass, const QString &fn) { QMimeDatabase db; QMimeType mime = db.mimeTypeForData(dev.data()); @@ -316,9 +316,18 @@ ChatPage::ChatPage(QSharedPointer<UserSettings> userSettings, QWidget *parent) return; } - auto bin = dev->peek(dev->size()); - auto payload = std::string(bin.data(), bin.size()); - auto dimensions = QImageReader(dev.data()).size(); + auto bin = dev->peek(dev->size()); + auto payload = std::string(bin.data(), bin.size()); + boost::optional<mtx::crypto::EncryptedFile> encryptedFile; + if (cache::client()->isRoomEncrypted(current_room_.toStdString())) { + mtx::crypto::BinaryBuf buf; + std::tie(buf, encryptedFile) = mtx::crypto::encrypt_file(payload); + payload = mtx::crypto::to_string(buf); + } + + QSize dimensions; + if (mimeClass == "image") + dimensions = QImageReader(dev.data()).size(); http::client()->upload( payload, @@ -327,193 +336,61 @@ ChatPage::ChatPage(QSharedPointer<UserSettings> userSettings, QWidget *parent) [this, room_id = current_room_, filename = fn, - mime = mime.name(), - size = payload.size(), + encryptedFile, + mimeClass, + mime = mime.name(), + size = payload.size(), dimensions](const mtx::responses::ContentURI &res, mtx::http::RequestErr err) { if (err) { emit uploadFailed( - tr("Failed to upload image. Please try again.")); - nhlog::net()->warn("failed to upload image: {} {} ({})", + tr("Failed to upload media. Please try again.")); + nhlog::net()->warn("failed to upload media: {} {} ({})", err->matrix_error.error, to_string(err->matrix_error.errcode), static_cast<int>(err->status_code)); return; } - emit imageUploaded(room_id, + emit mediaUploaded(room_id, filename, + encryptedFile, QString::fromStdString(res.content_uri), + mimeClass, mime, size, dimensions); }); }); - connect(text_input_, - &TextInputWidget::uploadFile, - this, - [this](QSharedPointer<QIODevice> dev, const QString &fn) { - QMimeDatabase db; - QMimeType mime = db.mimeTypeForData(dev.data()); - - if (!dev->open(QIODevice::ReadOnly)) { - emit uploadFailed( - QString("Error while reading media: %1").arg(dev->errorString())); - return; - } - - auto bin = dev->readAll(); - auto payload = std::string(bin.data(), bin.size()); - - http::client()->upload( - payload, - mime.name().toStdString(), - QFileInfo(fn).fileName().toStdString(), - [this, - room_id = current_room_, - filename = fn, - mime = mime.name(), - size = payload.size()](const mtx::responses::ContentURI &res, - mtx::http::RequestErr err) { - if (err) { - emit uploadFailed( - tr("Failed to upload file. Please try again.")); - nhlog::net()->warn("failed to upload file: {} ({})", - err->matrix_error.error, - static_cast<int>(err->status_code)); - return; - } - - emit fileUploaded(room_id, - filename, - QString::fromStdString(res.content_uri), - mime, - size); - }); - }); - - connect(text_input_, - &TextInputWidget::uploadAudio, - this, - [this](QSharedPointer<QIODevice> dev, const QString &fn) { - QMimeDatabase db; - QMimeType mime = db.mimeTypeForData(dev.data()); - - if (!dev->open(QIODevice::ReadOnly)) { - emit uploadFailed( - QString("Error while reading media: %1").arg(dev->errorString())); - return; - } - - auto bin = dev->readAll(); - auto payload = std::string(bin.data(), bin.size()); - - http::client()->upload( - payload, - mime.name().toStdString(), - QFileInfo(fn).fileName().toStdString(), - [this, - room_id = current_room_, - filename = fn, - mime = mime.name(), - size = payload.size()](const mtx::responses::ContentURI &res, - mtx::http::RequestErr err) { - if (err) { - emit uploadFailed( - tr("Failed to upload audio. Please try again.")); - nhlog::net()->warn("failed to upload audio: {} ({})", - err->matrix_error.error, - static_cast<int>(err->status_code)); - return; - } - - emit audioUploaded(room_id, - filename, - QString::fromStdString(res.content_uri), - mime, - size); - }); - }); - connect(text_input_, - &TextInputWidget::uploadVideo, - this, - [this](QSharedPointer<QIODevice> dev, const QString &fn) { - QMimeDatabase db; - QMimeType mime = db.mimeTypeForData(dev.data()); - - if (!dev->open(QIODevice::ReadOnly)) { - emit uploadFailed( - QString("Error while reading media: %1").arg(dev->errorString())); - return; - } - - auto bin = dev->readAll(); - auto payload = std::string(bin.data(), bin.size()); - - http::client()->upload( - payload, - mime.name().toStdString(), - QFileInfo(fn).fileName().toStdString(), - [this, - room_id = current_room_, - filename = fn, - mime = mime.name(), - size = payload.size()](const mtx::responses::ContentURI &res, - mtx::http::RequestErr err) { - if (err) { - emit uploadFailed( - tr("Failed to upload video. Please try again.")); - nhlog::net()->warn("failed to upload video: {} ({})", - err->matrix_error.error, - static_cast<int>(err->status_code)); - return; - } - - emit videoUploaded(room_id, - filename, - QString::fromStdString(res.content_uri), - mime, - size); - }); - }); - connect(this, &ChatPage::uploadFailed, this, [this](const QString &msg) { text_input_->hideUploadSpinner(); emit showNotification(msg); }); connect(this, - &ChatPage::imageUploaded, + &ChatPage::mediaUploaded, this, [this](QString roomid, QString filename, + boost::optional<mtx::crypto::EncryptedFile> encryptedFile, QString url, + QString mimeClass, QString mime, qint64 dsize, QSize dimensions) { text_input_->hideUploadSpinner(); - view_manager_->queueImageMessage( - roomid, filename, url, mime, dsize, dimensions); - }); - connect(this, - &ChatPage::fileUploaded, - this, - [this](QString roomid, QString filename, QString url, QString mime, qint64 dsize) { - text_input_->hideUploadSpinner(); - view_manager_->queueFileMessage(roomid, filename, url, mime, dsize); - }); - connect(this, - &ChatPage::audioUploaded, - this, - [this](QString roomid, QString filename, QString url, QString mime, qint64 dsize) { - text_input_->hideUploadSpinner(); - view_manager_->queueAudioMessage(roomid, filename, url, mime, dsize); - }); - connect(this, - &ChatPage::videoUploaded, - this, - [this](QString roomid, QString filename, QString url, QString mime, qint64 dsize) { - text_input_->hideUploadSpinner(); - view_manager_->queueVideoMessage(roomid, filename, url, mime, dsize); + + if (mimeClass == "image") + view_manager_->queueImageMessage( + roomid, filename, encryptedFile, url, mime, dsize, dimensions); + else if (mimeClass == "audio") + view_manager_->queueAudioMessage( + roomid, filename, encryptedFile, url, mime, dsize); + else if (mimeClass == "video") + view_manager_->queueVideoMessage( + roomid, filename, encryptedFile, url, mime, dsize); + else + view_manager_->queueFileMessage( + roomid, filename, encryptedFile, url, mime, dsize); }); connect(room_list_, &RoomList::roomAvatarChanged, this, &ChatPage::updateTopBarAvatar); @@ -566,7 +443,7 @@ ChatPage::ChatPage(QSharedPointer<UserSettings> userSettings, QWidget *parent) connect(this, &ChatPage::initializeViews, view_manager_, - [this](const mtx::responses::Rooms &rooms) { view_manager_->initialize(rooms); }); + [this](const mtx::responses::Rooms &rooms) { view_manager_->sync(rooms); }); connect(this, &ChatPage::initializeEmptyViews, view_manager_, @@ -582,7 +459,7 @@ ChatPage::ChatPage(QSharedPointer<UserSettings> userSettings, QWidget *parent) nhlog::db()->error("failed to retrieve invites: {}", e.what()); } - view_manager_->initialize(rooms); + view_manager_->sync(rooms); removeLeftRooms(rooms.leave); bool hasNotifications = false; diff --git a/src/ChatPage.h b/src/ChatPage.h index e41ae1ae4ac97fb72b1dad96e9db12360335a2fd..20e156af81aa9f018565bec39c658d8ac7bced47 100644 --- a/src/ChatPage.h +++ b/src/ChatPage.h @@ -18,7 +18,9 @@ #pragma once #include <atomic> +#include <boost/optional.hpp> #include <boost/variant.hpp> +#include <mtx/common.hpp> #include <mtx/responses.hpp> #include <QFrame> @@ -94,27 +96,14 @@ signals: const QPoint widgetPos); void uploadFailed(const QString &msg); - void imageUploaded(const QString &roomid, + void mediaUploaded(const QString &roomid, const QString &filename, + const boost::optional<mtx::crypto::EncryptedFile> &file, const QString &url, + const QString &mimeClass, const QString &mime, qint64 dsize, const QSize &dimensions); - void fileUploaded(const QString &roomid, - const QString &filename, - const QString &url, - const QString &mime, - qint64 dsize); - void audioUploaded(const QString &roomid, - const QString &filename, - const QString &url, - const QString &mime, - qint64 dsize); - void videoUploaded(const QString &roomid, - const QString &filename, - const QString &url, - const QString &mime, - qint64 dsize); void contentLoaded(); void closing(); @@ -125,8 +114,6 @@ signals: void showUserSettingsPage(); void showOverlayProgressBar(); - void removeTimelineEvent(const QString &room_id, const QString &event_id); - void ownProfileOk(); void setUserDisplayName(const QString &name); void setUserAvatar(const QString &avatar); diff --git a/src/ColorImageProvider.cpp b/src/ColorImageProvider.cpp new file mode 100644 index 0000000000000000000000000000000000000000..92e4732b73c3fe79b92cecedb63375661ea10218 --- /dev/null +++ b/src/ColorImageProvider.cpp @@ -0,0 +1,30 @@ +#include "ColorImageProvider.h" + +#include "Logging.h" +#include <QPainter> + +QPixmap +ColorImageProvider::requestPixmap(const QString &id, QSize *size, const QSize &) +{ + auto args = id.split('?'); + + nhlog::ui()->info("Loading {}, source is {}", id.toStdString(), args[0].toStdString()); + + QPixmap source(args[0]); + + if (size) + *size = QSize(source.width(), source.height()); + + if (args.size() < 2) + return source; + + QColor color(args[1]); + + QPixmap colorized = source; + QPainter painter(&colorized); + painter.setCompositionMode(QPainter::CompositionMode_SourceIn); + painter.fillRect(colorized.rect(), color); + painter.end(); + + return colorized; +} diff --git a/src/ColorImageProvider.h b/src/ColorImageProvider.h new file mode 100644 index 0000000000000000000000000000000000000000..21f36c126cd32a6427fa2d25448364cff0cb9ca1 --- /dev/null +++ b/src/ColorImageProvider.h @@ -0,0 +1,11 @@ +#include <QQuickImageProvider> + +class ColorImageProvider : public QQuickImageProvider +{ +public: + ColorImageProvider() + : QQuickImageProvider(QQuickImageProvider::Pixmap) + {} + + QPixmap requestPixmap(const QString &id, QSize *size, const QSize &requestedSize) override; +}; diff --git a/src/Logging.cpp b/src/Logging.cpp index 322875826f05af709140ec76288f4e5f7b1e3f4c..126b3781d910eb4712acf9fe5e2e5f28f3d29016 100644 --- a/src/Logging.cpp +++ b/src/Logging.cpp @@ -5,14 +5,43 @@ #include "spdlog/sinks/stdout_color_sinks.h" #include <iostream> +#include <QString> +#include <QtGlobal> + namespace { std::shared_ptr<spdlog::logger> db_logger = nullptr; std::shared_ptr<spdlog::logger> net_logger = nullptr; std::shared_ptr<spdlog::logger> crypto_logger = nullptr; std::shared_ptr<spdlog::logger> ui_logger = nullptr; +std::shared_ptr<spdlog::logger> qml_logger = nullptr; constexpr auto MAX_FILE_SIZE = 1024 * 1024 * 6; constexpr auto MAX_LOG_FILES = 3; + +void +qmlMessageHandler(QtMsgType type, const QMessageLogContext &context, const QString &msg) +{ + std::string localMsg = msg.toStdString(); + const char *file = context.file ? context.file : ""; + const char *function = context.function ? context.function : ""; + switch (type) { + case QtDebugMsg: + nhlog::qml()->debug("{} ({}:{}, {})", localMsg, file, context.line, function); + break; + case QtInfoMsg: + nhlog::qml()->info("{} ({}:{}, {})", localMsg, file, context.line, function); + break; + case QtWarningMsg: + nhlog::qml()->warn("{} ({}:{}, {})", localMsg, file, context.line, function); + break; + case QtCriticalMsg: + nhlog::qml()->critical("{} ({}:{}, {})", localMsg, file, context.line, function); + break; + case QtFatalMsg: + nhlog::qml()->critical("{} ({}:{}, {})", localMsg, file, context.line, function); + break; + } +} } namespace nhlog { @@ -35,12 +64,15 @@ init(const std::string &file_path) db_logger = std::make_shared<spdlog::logger>("db", std::begin(sinks), std::end(sinks)); crypto_logger = std::make_shared<spdlog::logger>("crypto", std::begin(sinks), std::end(sinks)); + qml_logger = std::make_shared<spdlog::logger>("qml", std::begin(sinks), std::end(sinks)); if (nheko::enable_debug_log) { db_logger->set_level(spdlog::level::trace); ui_logger->set_level(spdlog::level::trace); crypto_logger->set_level(spdlog::level::trace); } + + qInstallMessageHandler(qmlMessageHandler); } std::shared_ptr<spdlog::logger> @@ -66,4 +98,10 @@ crypto() { return crypto_logger; } + +std::shared_ptr<spdlog::logger> +qml() +{ + return qml_logger; +} } diff --git a/src/Logging.h b/src/Logging.h index e54f3c3f3b861062858bac8118b6669ab4108372..f572afae0d2a420731dcef7bfb26e921432ae4c2 100644 --- a/src/Logging.h +++ b/src/Logging.h @@ -19,5 +19,8 @@ db(); std::shared_ptr<spdlog::logger> crypto(); +std::shared_ptr<spdlog::logger> +qml(); + extern bool enable_debug_log_from_commandline; } diff --git a/src/MatrixClient.h b/src/MatrixClient.h index 2af57267e4d6da20d4b6eb68095ec76a83411139..c77b1183b9cf27ddda4ea4ec2337b2044c7dc858 100644 --- a/src/MatrixClient.h +++ b/src/MatrixClient.h @@ -20,16 +20,6 @@ Q_DECLARE_METATYPE(nlohmann::json) Q_DECLARE_METATYPE(std::vector<std::string>) Q_DECLARE_METATYPE(std::vector<QString>) -class MediaProxy : public QObject -{ - Q_OBJECT - -signals: - void imageDownloaded(const QPixmap &); - void imageSaved(const QString &, const QByteArray &); - void fileDownloaded(const QByteArray &); -}; - namespace http { mtx::http::Client * client(); diff --git a/src/MxcImageProvider.cpp b/src/MxcImageProvider.cpp new file mode 100644 index 0000000000000000000000000000000000000000..edf6ceb5cec2b5f04883616534706e946f339c36 --- /dev/null +++ b/src/MxcImageProvider.cpp @@ -0,0 +1,83 @@ +#include "MxcImageProvider.h" + +#include "Cache.h" + +void +MxcImageResponse::run() +{ + if (m_requestedSize.isValid() && !m_encryptionInfo) { + QString fileName = QString("%1_%2x%3_crop") + .arg(m_id) + .arg(m_requestedSize.width()) + .arg(m_requestedSize.height()); + + auto data = cache::client()->image(fileName); + if (!data.isNull() && m_image.loadFromData(data)) { + m_image = m_image.scaled(m_requestedSize, Qt::KeepAspectRatio); + m_image.setText("mxc url", "mxc://" + m_id); + emit finished(); + return; + } + + mtx::http::ThumbOpts opts; + opts.mxc_url = "mxc://" + m_id.toStdString(); + opts.width = m_requestedSize.width() > 0 ? m_requestedSize.width() : -1; + opts.height = m_requestedSize.height() > 0 ? m_requestedSize.height() : -1; + opts.method = "crop"; + http::client()->get_thumbnail( + opts, [this, fileName](const std::string &res, mtx::http::RequestErr err) { + if (err) { + nhlog::net()->error("Failed to download image {}", + m_id.toStdString()); + m_error = "Failed download"; + emit finished(); + + return; + } + + auto data = QByteArray(res.data(), res.size()); + cache::client()->saveImage(fileName, data); + m_image.loadFromData(data); + m_image.setText("mxc url", "mxc://" + m_id); + + emit finished(); + }); + } else { + auto data = cache::client()->image(m_id); + if (!data.isNull() && m_image.loadFromData(data)) { + m_image.setText("mxc url", "mxc://" + m_id); + emit finished(); + return; + } + + http::client()->download( + "mxc://" + m_id.toStdString(), + [this](const std::string &res, + const std::string &, + const std::string &originalFilename, + mtx::http::RequestErr err) { + if (err) { + nhlog::net()->error("Failed to download image {}", + m_id.toStdString()); + m_error = "Failed download"; + emit finished(); + + return; + } + + auto temp = res; + if (m_encryptionInfo) + temp = mtx::crypto::to_string( + mtx::crypto::decrypt_file(temp, m_encryptionInfo.value())); + + auto data = QByteArray(temp.data(), temp.size()); + m_image.loadFromData(data); + m_image.setText("original filename", + QString::fromStdString(originalFilename)); + m_image.setText("mxc url", "mxc://" + m_id); + cache::client()->saveImage(m_id, data); + + emit finished(); + }); + } +} diff --git a/src/MxcImageProvider.h b/src/MxcImageProvider.h new file mode 100644 index 0000000000000000000000000000000000000000..2c197a13c552fe944be896c6f3c487210007c4e3 --- /dev/null +++ b/src/MxcImageProvider.h @@ -0,0 +1,69 @@ +#pragma once + +#include <QQuickAsyncImageProvider> +#include <QQuickImageResponse> + +#include <QImage> +#include <QThreadPool> + +#include <mtx/common.hpp> + +#include <boost/optional.hpp> + +class MxcImageResponse + : public QQuickImageResponse + , public QRunnable +{ +public: + MxcImageResponse(const QString &id, + const QSize &requestedSize, + boost::optional<mtx::crypto::EncryptedFile> encryptionInfo) + : m_id(id) + , m_requestedSize(requestedSize) + , m_encryptionInfo(encryptionInfo) + { + setAutoDelete(false); + } + + QQuickTextureFactory *textureFactory() const override + { + return QQuickTextureFactory::textureFactoryForImage(m_image); + } + QString errorString() const override { return m_error; } + + void run() override; + + QString m_id, m_error; + QSize m_requestedSize; + QImage m_image; + boost::optional<mtx::crypto::EncryptedFile> m_encryptionInfo; +}; + +class MxcImageProvider + : public QObject + , public QQuickAsyncImageProvider +{ + Q_OBJECT +public slots: + QQuickImageResponse *requestImageResponse(const QString &id, + const QSize &requestedSize) override + { + boost::optional<mtx::crypto::EncryptedFile> info; + auto temp = infos.find("mxc://" + id); + if (temp != infos.end()) + info = *temp; + + MxcImageResponse *response = new MxcImageResponse(id, requestedSize, info); + pool.start(response); + return response; + } + + void addEncryptionInfo(mtx::crypto::EncryptedFile info) + { + infos.insert(QString::fromStdString(info.url), info); + } + +private: + QThreadPool pool; + QHash<QString, mtx::crypto::EncryptedFile> infos; +}; diff --git a/src/RoomInfoListItem.cpp b/src/RoomInfoListItem.cpp index 8aadbea219ad60797fd55b5c3e6c0d9331a3c1c4..8bebb0f50c46cc669679a09a62c7b058013f9703 100644 --- a/src/RoomInfoListItem.cpp +++ b/src/RoomInfoListItem.cpp @@ -118,7 +118,7 @@ RoomInfoListItem::RoomInfoListItem(QString room_id, RoomInfo info, QWidget *pare // so we can't use them for sorting. if (roomType_ == RoomType::Invited) lastMsgInfo_ = { - emptyEventId, "-", "-", "-", "-", QDateTime::currentDateTime().addYears(10)}; + emptyEventId, "-", "-", "-", QDateTime::currentDateTime().addYears(10)}; } void @@ -142,7 +142,7 @@ RoomInfoListItem::resizeEvent(QResizeEvent *) void RoomInfoListItem::paintEvent(QPaintEvent *event) { - bool rounded = QSettings().value("user/avatar/circles", true).toBool(); + bool rounded = QSettings().value("user/avatar_circles", true).toBool(); Q_UNUSED(event); @@ -210,33 +210,11 @@ RoomInfoListItem::paintEvent(QPaintEvent *event) p.setFont(QFont{}); p.setPen(subtitlePen); - // The limit is the space between the end of the avatar and the start of the - // timestamp. - int usernameLimit = - std::max(0, width() - 3 * wm.padding - msgStampWidth - wm.iconSize - 20); - auto userName = - metrics.elidedText(lastMsgInfo_.username, Qt::ElideRight, usernameLimit); - - p.setFont(QFont{}); - p.drawText(QPoint(2 * wm.padding + wm.iconSize, bottom_y), userName); - -#if QT_VERSION < QT_VERSION_CHECK(5, 11, 0) - int nameWidth = QFontMetrics(QFont{}).width(userName); -#else - int nameWidth = QFontMetrics(QFont{}).horizontalAdvance(userName); -#endif - p.setFont(QFont{}); - - // The limit is the space between the end of the username and the start of - // the timestamp. - int descriptionLimit = - std::max(0, - width() - 3 * wm.padding - bottomLineWidthLimit - wm.iconSize - - nameWidth - 5); + int descriptionLimit = std::max( + 0, width() - 3 * wm.padding - bottomLineWidthLimit - wm.iconSize); auto description = metrics.elidedText(lastMsgInfo_.body, Qt::ElideRight, descriptionLimit); - p.drawText(QPoint(2 * wm.padding + wm.iconSize + nameWidth, bottom_y), - description); + p.drawText(QPoint(2 * wm.padding + wm.iconSize, bottom_y), description); // We show the last message timestamp. p.save(); diff --git a/src/TextInputWidget.cpp b/src/TextInputWidget.cpp index f723c01ac92774977deb9aef7896a9d89a051a13..66700dbc879f973604b95cea4c244d519f485f9f 100644 --- a/src/TextInputWidget.cpp +++ b/src/TextInputWidget.cpp @@ -458,21 +458,16 @@ FilteredTextEdit::textChanged() } void -FilteredTextEdit::uploadData(const QByteArray data, const QString &media, const QString &filename) +FilteredTextEdit::uploadData(const QByteArray data, + const QString &mediaType, + const QString &filename) { QSharedPointer<QBuffer> buffer{new QBuffer{this}}; buffer->setData(data); emit startedUpload(); - if (media == "image") - emit image(buffer, filename); - else if (media == "audio") - emit audio(buffer, filename); - else if (media == "video") - emit video(buffer, filename); - else - emit file(buffer, filename); + emit media(buffer, mediaType, filename); } void @@ -580,10 +575,7 @@ TextInputWidget::TextInputWidget(QWidget *parent) connect(input_, &FilteredTextEdit::message, this, &TextInputWidget::sendTextMessage); connect(input_, &FilteredTextEdit::reply, this, &TextInputWidget::sendReplyMessage); connect(input_, &FilteredTextEdit::command, this, &TextInputWidget::command); - connect(input_, &FilteredTextEdit::image, this, &TextInputWidget::uploadImage); - connect(input_, &FilteredTextEdit::audio, this, &TextInputWidget::uploadAudio); - connect(input_, &FilteredTextEdit::video, this, &TextInputWidget::uploadVideo); - connect(input_, &FilteredTextEdit::file, this, &TextInputWidget::uploadFile); + connect(input_, &FilteredTextEdit::media, this, &TextInputWidget::uploadMedia); connect(emojiBtn_, SIGNAL(emojiSelected(const QString &)), this, @@ -642,14 +634,8 @@ TextInputWidget::openFileSelection() const auto format = mime.name().split("/")[0]; QSharedPointer<QFile> file{new QFile{fileName, this}}; - if (format == "image") - emit uploadImage(file, fileName); - else if (format == "audio") - emit uploadAudio(file, fileName); - else if (format == "video") - emit uploadVideo(file, fileName); - else - emit uploadFile(file, fileName); + + emit uploadMedia(file, format, fileName); showUploadSpinner(); } diff --git a/src/TextInputWidget.h b/src/TextInputWidget.h index 71f794d13e5be56343ffbcc88b1b709c7b89bbe5..d498be724ddfabd8be75622dcd38d053c45f78e3 100644 --- a/src/TextInputWidget.h +++ b/src/TextInputWidget.h @@ -63,10 +63,7 @@ signals: void message(QString); void reply(QString, const RelatedInfo &); void command(QString name, QString args); - void image(QSharedPointer<QIODevice> data, const QString &filename); - void audio(QSharedPointer<QIODevice> data, const QString &filename); - void video(QSharedPointer<QIODevice> data, const QString &filename); - void file(QSharedPointer<QIODevice> data, const QString &filename); + void media(QSharedPointer<QIODevice> data, QString mimeClass, const QString &filename); //! Trigger the suggestion popup. void showSuggestions(const QString &query); @@ -179,10 +176,9 @@ signals: void sendEmoteMessage(QString msg); void heightChanged(int height); - void uploadImage(const QSharedPointer<QIODevice> data, const QString &filename); - void uploadFile(const QSharedPointer<QIODevice> data, const QString &filename); - void uploadAudio(const QSharedPointer<QIODevice> data, const QString &filename); - void uploadVideo(const QSharedPointer<QIODevice> data, const QString &filename); + void uploadMedia(const QSharedPointer<QIODevice> data, + QString mimeClass, + const QString &filename); void sendJoinRoomRequest(const QString &room); diff --git a/src/UserSettingsPage.cpp b/src/UserSettingsPage.cpp index 9fd033e9a0bdbf8aa7633466da626e50a7d77fce..1caea4494ebbe16683f62b2abaff2b6f77212a7f 100644 --- a/src/UserSettingsPage.cpp +++ b/src/UserSettingsPage.cpp @@ -53,7 +53,7 @@ UserSettings::load() isReadReceiptsEnabled_ = settings.value("user/read_receipts", true).toBool(); theme_ = settings.value("user/theme", defaultTheme_).toString(); font_ = settings.value("user/font_family", "default").toString(); - avatarCircles_ = settings.value("user/avatar/circles", true).toBool(); + avatarCircles_ = settings.value("user/avatar_circles", true).toBool(); emojiFont_ = settings.value("user/emoji_font_family", "default").toString(); baseFontSize_ = settings.value("user/font_size", QFont().pointSizeF()).toDouble(); @@ -119,9 +119,7 @@ UserSettings::save() settings.setValue("start_in_tray", isStartInTrayEnabled_); settings.endGroup(); - settings.beginGroup("avatar"); - settings.setValue("circles", avatarCircles_); - settings.endGroup(); + settings.setValue("avatar_circles", avatarCircles_); settings.setValue("font_size", baseFontSize_); settings.setValue("typing_notifications", isTypingNotificationsEnabled_); diff --git a/src/Utils.cpp b/src/Utils.cpp index c60adb58a1367d2b6a65c34176096379e73f0543..3e59d9129152b430670834d073fb836d6248cf82 100644 --- a/src/Utils.cpp +++ b/src/Utils.cpp @@ -40,9 +40,8 @@ utils::replaceEmoji(const QString &body) for (auto &code : utf32_string) { // TODO: Be more precise here. if (code > 9000) - fmtBody += - QString("<span style=\"font-family: " + userFontFamily + ";\">") + - QString::fromUcs4(&code, 1) + "</span>"; + fmtBody += QString("<font face=\"" + userFontFamily + "\">") + + QString::fromUcs4(&code, 1) + "</font>"; else fmtBody += QString::fromUcs4(&code, 1); } @@ -147,11 +146,6 @@ utils::getMessageDescription(const TimelineEvent &event, const auto ts = QDateTime::fromMSecsSinceEpoch(msg.origin_server_ts); DescInfo info; - if (sender == localUser) - info.username = QCoreApplication::translate("utils", "You"); - else - info.username = username; - info.userid = sender; info.body = QString(" %1").arg(messageDescription<Encrypted>()); info.timestamp = utils::descriptiveTime(ts); @@ -324,19 +318,29 @@ utils::linkifyMessage(const QString &body) return doc; } -QByteArray escapeRawHtml(const QByteArray &data) { - QByteArray buffer; - const size_t length = data.size(); - buffer.reserve(length); - for(size_t pos = 0; pos != length; ++pos) { - switch(data.at(pos)) { - case '&': buffer.append("&"); break; - case '<': buffer.append("<"); break; - case '>': buffer.append(">"); break; - default: buffer.append(data.at(pos)); break; - } - } - return buffer; +QByteArray +escapeRawHtml(const QByteArray &data) +{ + QByteArray buffer; + const size_t length = data.size(); + buffer.reserve(length); + for (size_t pos = 0; pos != length; ++pos) { + switch (data.at(pos)) { + case '&': + buffer.append("&"); + break; + case '<': + buffer.append("<"); + break; + case '>': + buffer.append(">"); + break; + default: + buffer.append(data.at(pos)); + break; + } + } + return buffer; } QString @@ -362,7 +366,7 @@ utils::getFormattedQuoteBody(const RelatedInfo &related, const QString &html) { return QString("<mx-reply><blockquote><a " "href=\"https://matrix.to/#/%1/%2\">In reply " - "to</a>* <a href=\"https://matrix.to/#/%3\">%4</a><br " + "to</a> <a href=\"https://matrix.to/#/%3\">%4</a><br" "/>%5</blockquote></mx-reply>") .arg(related.room, QString::fromStdString(related.related_event), @@ -378,9 +382,6 @@ utils::getQuoteBody(const RelatedInfo &related) using MsgType = mtx::events::MessageType; switch (related.type) { - case MsgType::Text: { - return markdownToHtml(related.quoted_body); - } case MsgType::File: { return QString(QCoreApplication::translate("utils", "sent a file.")); } diff --git a/src/Utils.h b/src/Utils.h index 225754be4a5eb37ec0fbc2db261ee0cdc00d7fc7..bdb5184438fb82e39ab967ef0f5c3c4c97316c38 100644 --- a/src/Utils.h +++ b/src/Utils.h @@ -4,10 +4,6 @@ #include "Cache.h" #include "RoomInfoListItem.h" -#include "timeline/widgets/AudioItem.h" -#include "timeline/widgets/FileItem.h" -#include "timeline/widgets/ImageItem.h" -#include "timeline/widgets/VideoItem.h" #include <QCoreApplication> #include <QDateTime> @@ -94,38 +90,72 @@ messageDescription(const QString &username = "", using Video = mtx::events::RoomEvent<mtx::events::msg::Video>; using Encrypted = mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>; - // Sometimes the verb form of sent changes in some languages depending on the actor. - auto remoteSent = QCoreApplication::translate( - "message-description: ", "sent", "For when you are the sender"); - auto localSent = QCoreApplication::translate( - "message-description:", "sent", "For when someone else is the sender"); - QString sentVerb = isLocal ? localSent : remoteSent; - if (std::is_same<T, AudioItem>::value || std::is_same<T, Audio>::value) { - return QCoreApplication::translate("message-description sent:", "%1 an audio clip") - .arg(sentVerb); - } else if (std::is_same<T, ImageItem>::value || std::is_same<T, Image>::value) { - return QCoreApplication::translate("message-description sent:", "%1 an image") - .arg(sentVerb); - } else if (std::is_same<T, FileItem>::value || std::is_same<T, File>::value) { - return QCoreApplication::translate("message-description sent:", "%1 a file") - .arg(sentVerb); - } else if (std::is_same<T, VideoItem>::value || std::is_same<T, Video>::value) { - return QCoreApplication::translate("message-description sent:", "%1 a video clip") - .arg(sentVerb); - } else if (std::is_same<T, StickerItem>::value || std::is_same<T, Sticker>::value) { - return QCoreApplication::translate("message-description sent:", "%1 a sticker") - .arg(sentVerb); + if (std::is_same<T, Audio>::value) { + if (isLocal) + return QCoreApplication::translate("message-description sent:", + "You sent an audio clip"); + else + return QCoreApplication::translate("message-description sent:", + "%1 sent an audio clip") + .arg(username); + } else if (std::is_same<T, Image>::value) { + if (isLocal) + return QCoreApplication::translate("message-description sent:", + "You sent an image"); + else + return QCoreApplication::translate("message-description sent:", + "%1 sent an image") + .arg(username); + } else if (std::is_same<T, File>::value) { + if (isLocal) + return QCoreApplication::translate("message-description sent:", + "You sent a file"); + else + return QCoreApplication::translate("message-description sent:", + "%1 sent a file") + .arg(username); + } else if (std::is_same<T, Video>::value) { + if (isLocal) + return QCoreApplication::translate("message-description sent:", + "You sent a video"); + else + return QCoreApplication::translate("message-description sent:", + "%1 sent a video") + .arg(username); + } else if (std::is_same<T, Sticker>::value) { + if (isLocal) + return QCoreApplication::translate("message-description sent:", + "You sent a sticker"); + else + return QCoreApplication::translate("message-description sent:", + "%1 sent a sticker") + .arg(username); } else if (std::is_same<T, Notice>::value) { - return QCoreApplication::translate("message-description sent:", "%1 a notification") - .arg(sentVerb); + if (isLocal) + return QCoreApplication::translate("message-description sent:", + "You sent a notification"); + else + return QCoreApplication::translate("message-description sent:", + "%1 sent a notification") + .arg(username); } else if (std::is_same<T, Text>::value) { - return QString(": %1").arg(body); + if (isLocal) + return QCoreApplication::translate("message-description sent:", "You: %1") + .arg(body); + else + return QCoreApplication::translate("message-description sent:", "%1: %2") + .arg(username) + .arg(body); } else if (std::is_same<T, Emote>::value) { return QString("* %1 %2").arg(username).arg(body); } else if (std::is_same<T, Encrypted>::value) { - return QCoreApplication::translate("message-description sent:", - "%1 an encrypted message") - .arg(sentVerb); + if (isLocal) + return QCoreApplication::translate("message-description sent:", + "You sent an encrypted message"); + else + return QCoreApplication::translate("message-description sent:", + "%1 sent an encrypted message") + .arg(username); } else { return QCoreApplication::translate("utils", "Unknown Message Type"); } @@ -135,29 +165,19 @@ template<class T, class Event> DescInfo createDescriptionInfo(const Event &event, const QString &localUser, const QString &room_id) { - using Text = mtx::events::RoomEvent<mtx::events::msg::Text>; - using Emote = mtx::events::RoomEvent<mtx::events::msg::Emote>; - const auto msg = boost::get<T>(event); const auto sender = QString::fromStdString(msg.sender); const auto username = Cache::displayName(room_id, sender); const auto ts = QDateTime::fromMSecsSinceEpoch(msg.origin_server_ts); - bool isText = std::is_same<T, Text>::value; - bool isEmote = std::is_same<T, Emote>::value; - - return DescInfo{ - QString::fromStdString(msg.event_id), - isEmote ? "" - : (sender == localUser ? QCoreApplication::translate("utils", "You") : username), - sender, - (isText || isEmote) - ? messageDescription<T>( - username, QString::fromStdString(msg.content.body).trimmed(), sender == localUser) - : QString(" %1").arg(messageDescription<T>()), - utils::descriptiveTime(ts), - ts}; + return DescInfo{QString::fromStdString(msg.event_id), + sender, + messageDescription<T>(username, + QString::fromStdString(msg.content.body).trimmed(), + sender == localUser), + utils::descriptiveTime(ts), + ts}; } //! Scale down an image to fit to the given width & height limitations. diff --git a/src/dialogs/ImageOverlay.cpp b/src/dialogs/ImageOverlay.cpp index dd9cd03ab8d5367fded1e374979e43c20d8f4c95..cbdd351cb28d435a0d5145b65496c719f85823a9 100644 --- a/src/dialogs/ImageOverlay.cpp +++ b/src/dialogs/ImageOverlay.cpp @@ -41,7 +41,6 @@ ImageOverlay::ImageOverlay(QPixmap image, QWidget *parent) setAttribute(Qt::WA_DeleteOnClose, true); setWindowState(Qt::WindowFullScreen); - // Deprecated in 5.13: screen_ = QApplication::desktop()->availableGeometry(); screen_ = QGuiApplication::primaryScreen()->availableGeometry(); move(QApplication::desktop()->mapToGlobal(screen_.topLeft())); diff --git a/src/dialogs/MemberList.cpp b/src/dialogs/MemberList.cpp index 9e973efa5d715b032e2aa068b41f1faea990c5a6..f62cf9fe4ae6385e7cf2385b8f11b79eb9dfbfc0 100644 --- a/src/dialogs/MemberList.cpp +++ b/src/dialogs/MemberList.cpp @@ -1,4 +1,5 @@ #include <QAbstractSlider> +#include <QLabel> #include <QListWidgetItem> #include <QPainter> #include <QPushButton> diff --git a/src/dialogs/RoomSettings.cpp b/src/dialogs/RoomSettings.cpp index 00b034ccd71f747d34d41b3efd6c3e03cf34719f..25909cd86846f59d42b7fed55f5eaba669533665 100644 --- a/src/dialogs/RoomSettings.cpp +++ b/src/dialogs/RoomSettings.cpp @@ -488,7 +488,7 @@ RoomSettings::retrieveRoomInfo() usesEncryption_ = cache::client()->isRoomEncrypted(room_id_.toStdString()); info_ = cache::client()->singleRoomInfo(room_id_.toStdString()); setAvatar(); - } catch (const lmdb::error &e) { + } catch (const lmdb::error &) { nhlog::db()->warn("failed to retrieve room info from cache: {}", room_id_.toStdString()); } diff --git a/src/popups/UserMentions.cpp b/src/popups/UserMentions.cpp index 3480959ac79bd35d459f963c6aca195ebd2037e7..3be5c462ca356012afaf86d445162523e6565039 100644 --- a/src/popups/UserMentions.cpp +++ b/src/popups/UserMentions.cpp @@ -7,7 +7,7 @@ #include "ChatPage.h" #include "Logging.h" #include "UserMentions.h" -#include "timeline/TimelineItem.h" +//#include "timeline/TimelineItem.h" using namespace popups; @@ -116,39 +116,46 @@ UserMentions::pushItem(const QString &event_id, const QString &room_id, const QString ¤t_room_id) { - setUpdatesEnabled(false); - - // Add to the 'all' section - TimelineItem *view_item = new TimelineItem( - mtx::events::MessageType::Text, user_id, body, true, room_id, all_scroll_widget_); - view_item->setEventId(event_id); - view_item->hide(); - - all_scroll_layout_->addWidget(view_item); - QTimer::singleShot(0, this, [view_item, this]() { - view_item->show(); - view_item->adjustSize(); - setUpdatesEnabled(true); - }); - - // if it matches the current room... add it to the current room as well. - if (QString::compare(room_id, current_room_id, Qt::CaseInsensitive) == 0) { - // Add to the 'local' section - TimelineItem *local_view_item = new TimelineItem(mtx::events::MessageType::Text, - user_id, - body, - true, - room_id, - local_scroll_widget_); - local_view_item->setEventId(event_id); - local_view_item->hide(); - local_scroll_layout_->addWidget(local_view_item); - - QTimer::singleShot(0, this, [local_view_item]() { - local_view_item->show(); - local_view_item->adjustSize(); - }); - } + (void)event_id; + (void)user_id; + (void)body; + (void)room_id; + (void)current_room_id; + // setUpdatesEnabled(false); + // + // // Add to the 'all' section + // TimelineItem *view_item = new TimelineItem( + // mtx::events::MessageType::Text, user_id, body, true, room_id, + // all_scroll_widget_); + // view_item->setEventId(event_id); + // view_item->hide(); + // + // all_scroll_layout_->addWidget(view_item); + // QTimer::singleShot(0, this, [view_item, this]() { + // view_item->show(); + // view_item->adjustSize(); + // setUpdatesEnabled(true); + // }); + // + // // if it matches the current room... add it to the current room as well. + // if (QString::compare(room_id, current_room_id, Qt::CaseInsensitive) == 0) { + // // Add to the 'local' section + // TimelineItem *local_view_item = new + // TimelineItem(mtx::events::MessageType::Text, + // user_id, + // body, + // true, + // room_id, + // local_scroll_widget_); + // local_view_item->setEventId(event_id); + // local_view_item->hide(); + // local_scroll_layout_->addWidget(local_view_item); + // + // QTimer::singleShot(0, this, [local_view_item]() { + // local_view_item->show(); + // local_view_item->adjustSize(); + // }); + // } } void @@ -158,4 +165,4 @@ UserMentions::paintEvent(QPaintEvent *) opt.init(this); QPainter p(this); style()->drawPrimitive(QStyle::PE_Widget, &opt, &p, this); -} \ No newline at end of file +} diff --git a/src/timeline/.TimelineItem.cpp.swp b/src/timeline/.TimelineItem.cpp.swp deleted file mode 100644 index 75e03aebe8a2b20a05aec339ab9cab819ed7fe2e..0000000000000000000000000000000000000000 --- a/src/timeline/.TimelineItem.cpp.swp +++ /dev/null @@ -1,186 +0,0 @@ -b0VIM 8.1������€c]7B[�'"��nicolas���������������������������������gentoo-neko�����������������������������~nicolas/Dokumente/devel/open-source/nheko/src/timeline/TimelineItem.cpp�����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������utf-8 �3210����#"! U�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������tp�����������/����������������������������0�������1����������������������������a����������������������������x���������������������@�������~����������������������������¿�������������� ��������������Ó����������������������������à���������������������;�������ö���������������������B�������0��������������������-�������s���������������������������Ÿ���������������������������¿��������������������I�������Á��������������������3������� ��������������������;�������=������������� ������� -�������w��������������������<�������‚���������������������������¾���������������������������Ì���������������������������ß��������������������K�������ë���������������������������6������������� -�������4�������M��������������������9�������‚���������������������������º�����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������ad�� ��á �����/�������ý��¸��µ��m��%��à��¹��¶��s��1��ð ��À ��½ ��x ��/ ��+ �� �� ��÷��Û��Â��±��Ÿ��‹��Š��t��`��K��3��"����ó��Ù��Ø��µ����f��>������õ -��Ö -��Õ -�� -��d -��c -��á �� -��C -�� -��Ñ �� ��; ��9 ��8 ��3 ��þ��ü��Ð��Ï��¬��d��*��)��à��Þ��Ý��Ø����«��x��`��_��F������þ��ý��ã��¸��Ž��w��m��?����ÿ��Ð��¢��‹����V��"������×��À��¶��´��³��®��x��v��^��]��D����é��Ò��¥��x��a��8����ù��Ð��¨��‘��g��G��0��&��%�������������}� update();�� }� break;� setToolTip("");� case StatusIndicatorState::Empty:� break;� setToolTip(tr("Sent"));� case StatusIndicatorState::Sent:� break;� setToolTip(tr("Seen"));� case StatusIndicatorState::Read:� break;� setToolTip(tr("Delivered"));� case StatusIndicatorState::Received:� break;� setToolTip(tr("Encrypted"));� case StatusIndicatorState::Encrypted:� switch (state) {�� state_ = state;�{�StatusIndicator::setState(StatusIndicatorState state)�void��}� }� break;� case StatusIndicatorState::Empty:� }� break;� paintIcon(p, doubleCheckmarkIcon_);� case StatusIndicatorState::Read: {� }� break;� paintIcon(p, checkmarkIcon_);� case StatusIndicatorState::Received: {� break;� paintIcon(p, lockIcon_);� case StatusIndicatorState::Encrypted:� }� break;� paintIcon(p, clockIcon_);� case StatusIndicatorState::Sent: {� switch (state_) {�� p.setPen(iconColor_);�� PainterHighQualityEnabler hq(p);� Painter p(this);�� return;� if (state_ == StatusIndicatorState::Empty)�{�StatusIndicator::paintEvent(QPaintEvent *)�void��}� QIcon(pixmap).paint(&p, rect(), Qt::AlignCenter, QIcon::Normal);�� painter.fillRect(pixmap.rect(), p.pen().color());� painter.setCompositionMode(QPainter::CompositionMode_SourceIn);� QPainter painter(&pixmap);�� auto pixmap = icon.pixmap(width());�{�StatusIndicator::paintIcon(QPainter &p, QIcon &icon)�void��}� doubleCheckmarkIcon_.addFile(":/icons/StatusIndicator::StaStatusIndicator::StaStatusIndicator::StatusIndicator(QWidget *parent)�������úw.�������������?¬StatusIndicator::StatusIndicator(QWidget *parent)�������úw.�������������úw. �������"������úw.�������+������úw.��������constexpr int MSG_PADDING = 20;�������úw.�������constexpr int MSG_RIGHT_MARGIN = 7;�������úw.��������#include "mtx/identifiers.hpp"�#include "dialogs/RawMessage.h"��#include "timeline/widgets/VideoItem.h"�#include "timeline/widgets/ImageItem.h"�#include "timeline/widgets/FileItem.h"�#include "timeline/widgets/AudioItem.h"�#include "timeline/TimelineItem.h"��#include "ui/TextLabel.h"�#include "ui/Painter.h"�#include "ui/Avatar.h"�#include "Olm.h"�#include "MainWindow.h"�#include "Logging.h"�#include "Config.h"�#include "ChatPage.h"��#include <QtGlobal>�#include <QTimer>�#include <QMenu>�#include <QFontDatabase>�#include <QDesktopServices>�#include <QContextMenuEvent>��#include <functional>� */� * along with this program. If not, see <http://www.gnu.org/licenses/>.� * You should have received a copy of the GNU General Public License� *� * GNU General Public License for more details.� * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the� * but WITHOUT ANY WARRANTY; without even the implied warranty of� * This program is distributed in the hope that it will be useful,� *� * (at your option) any later version.� * the Free Software Foundation, either version 3 of the License, or� * it under the terms of the GNU General Public License as published by� * This program is free software: you can redistribute it and/or modify� *� * nheko Copyright (C) 2017 Konstantinos Sideris <siderisk@auth.gr>�/*�ad��x��°������������™��X��*��Ô��À��²��°��Ã��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������}� });� }� });� }� "failed to serialize event ({}, {})", room_id, event_id);� nhlog::net()->warn(� } catch (const nlohmann::json::exception &e) {� emit proxy->eventRetrieved(utils::serialize_event(res));�������úw. �������ad��ç��/�����K�������Þ����†��8����Ú��Ù��H����ã ��â ��œ ��o ��I ��" �� �� ��Î��¡��z��S��G��F��÷��«��Ÿ����œ��—������ç -��º -�� -��B -��@ -��? -��: -��± ��¯ ��‡ ��3 ��2 ��ù��Ð��Ï��‰��K��J�� ��Ò��Œ��‹��Y������ô����›��š��•��K��I����ç��æ��Ÿ��T����å��º��¸��·��²��/��Ô��������Ó��Ñ��Ð��Ë��B��@��&��î��ì��ë��æ��‚��€��R��:��ø��µ��³��²�������������void��}� style()->drawPrimitive(QStyle::PE_Widget, &opt, &p, this);� QPainter p(this);� ������?¬…±�������������?¬…±������� opt.init(this);� QStyleOptTimelineItem::setUseTimelineItem::setUserAvatar(const QString &userid)�������úw.�������������?TimelineItem::setUserAvatar(const QString &userid)�������úw.�������������?TimelineItem::setUserAvatar(const QString &userid)�������úw.�������������?TimelineItem::setUserAvatar(const QStrinTimelineItem::setUserAvatar(const QStrinTimelineItem::setUserAvatar(const QStrinTimelineItem::setUserAvatar(const QString &userid)�������úw.�������������?TimelineItem::setUserAvatar(const QString &userid)�������úw.���������� ���úw.�������#������úw.�������,������úw.�������void��}� 0);� 0,� conf::timeline::msgTopMargin,� QFontMetrics(f).height() * 2 + 2,� topLayout_->setContentsMargins(conf::timeline::msgLeftMargin +�� f.setPointSizeF(f.pointSizeF());� QFont f;� ������úw.�������������úw.�������{�TimelineItem::setupSimpleLayout()�������úw.�������������úw.�������void��}� mainLayout_->insertWidget(0, userName_, Qt::AlignTop | Qt::AlignLeft);� if (userName_)�� topLayout_->setAlignment(userAvatar_, Qt::AlignTop | Qt::AlignLeft);� topLayout_->insertWidget(0, userAvatar_);�� userAvatar_->setLetter(QChar(userName[1]).toUpper());� if (userName[0] == '@' && userName.size() > 1)� // TODO: The provided user name should be a UserId class�� userAvatar_->setLetter(QChar(userName[0]).toUpper());� userAvatar_ = new Avatar(this, QFontMetrics(f).height() * 2);�� f.setPointSizeF(f.pointSizeF());� QFont f;� ������úw.�������������úw.�������� conf::timeline::msgLeftMargin, conf::timeline::msgAvatarTopMargin, 0, 0);� topLayout_->setContentsMargins(�{�TimelineItem::setupAvatarLayout(const QString &userName)�������úw.�������������úw.�������'������úw.�������0������úw.�������void��}� QString("<span style=\"color: #999\"> %1 </span>").arg(time.toString("HH:mm")));� timestamp_->setText(� timestamp_->setFont(timestampFont_);� timestamp_ = new QLabel(this);�{�TimelineItem::generateTimestamp(const QDateTime &time)�������úw.�������������úw.�������'��� ���úw.�������2������úw.�������void��}� });� MainWindow::instance()->openUserProfile(user_id, room_id_);� connect(filter, &UserProfileFilter::clicked, this, [this, user_id]() {�� });� userName_->setFont(f);� f.setUnderline(false);� QFont f = userName_->font();� connect(filter, &UserProfileFilter::hoverOff, this, [this]() {�� });� userName_->setFont(f);� f.setUnderline(true);� QFont f = userName_->font();� connect(filter, &UserProfileFilter::hoverOn, this, [this]() {�� userName_->setCursor(Qt::PointingHandCursor);� userName_->installEventFilter(filter);� auto filter = new UserProfileFilter(user_id, userName_);�������úw.�������������úw.�������-������úw.�������6��� ���úw. -�������� refreshAuthorColor();� ������úw.������� // otherwise this will just set it.� // Set the user color asynchronously if it hasn't been generated yet,�#endif� QFontMetrics(userName_->font()).horizontalAdvance(userName_->text()));� userName_->setFixedWidth(�ad��õ��= ������������Ú��°��S��+��þ��Ï����<��ï ��k ��= ��› ��B ��; ��í��Á��£��¢��a��2��ü��û��µ��ˆ��b��;��/��.��ç -��º -��“ -��l -��` -��_ -�� -��Ä ��¸ ��¶ ��µ ��° ��y ��w ��P ��# �� ��«��©��¨��£��j��h��@��ì��ë��Ú��±��°��j��,��+��ê��³��m��l��:��í��ì��Õ��~��|��{��v��T��R��A������Ð��…��@����ë��é��è��ã��°��®��Š��r��q��@��>��=��8��ÿ��ý��ã��«��©��¨��£��{��y��_��G��-��ê��è��ç��â�� ����������������void��}� style()->drawPrimitive(QStyle::PE_Widget, &opt, &p, this);� QPainter p(this);� opt.init(this);� QStyleOption opt;�{�TimelineItem::paintEvent(QPaintEvent *)�void��}� contextMenu_->exec(event->globalPos());� if (contextMenu_)�{�TimelineItem::contextMenuEvent(QContextMenuEvent *event)�void��}� userAvatar_->setImage(room_id_, userid);�� return;� if (userAvatar_ == nullptr)�{�TimelineItem::setUserAvatar(const QString &userid)�void��}� 0);� 0,� conf::timeline::msgTopMargin,� QFontMetrics(f).height() * 2 + 2,� topLayout_->setContentsMargins(conf::timeline::msgLeftMargin +�� f.setPointSizeF(f.pointSizeF());� QFont f;�{�TimelineItem::setupSimpleLayout()�void��}� mainLayout_->insertWidget(0, userName_, Qt::AlignTop | Qt::AlignLeft);� if (userName_)�� topLayout_->setAlignment(userAvatar_, Qt::AlignTop | Qt::AlignLeft);� topLayout_->insertWidget(0, userAvatar_);�� userAvatar_->setLetter(QChar(userName[1]).toUpper());� if (userName[0] == '@' && userName.size() > 1)� // TODO: The provided user name should be a UserId class�� userAvatar_->setLetter(QChar(userName[0]).toUpper());� userAvatar_ = new Avatar(this, QFontMetrics(f).height() * 2);�� f.setPointSizeF(f.pointSizeF());� QFont f;�� conf::timeline::msgLeftMargin, conf::timeline::msgAvatarTopMargin, 0, 0);� topLayout_->setContentsMargins(�{�TimelineItem::setupAvatarLayout(const QString &userName)�void��}� QString("<span style=\"color: #999\"> %1 </span>").arg(time.toString("HH:mm")));� timestamp_->setText(� timestamp_->setFont(timestampFont_);� timestamp_ = new QLabel(this);�{�TimelineItem::generateTimestamp(const QDateTime &time)�void��}� });� MainWindow::instance()->openUserProfile(user_id, room_id_);� connect(filter, &UserProfileFilter::clicked, this, [this, user_id]() {�� });� userName_->setFont(f);� f.setUnderline(false);� QFont f = userName_->font();� connect(filter, &UserProfileFilter::hoverOff, this, [this]() {�� });� userName_->setFont(f);� f.setUnderline(true);� QFont f = userName_->font();� connect(filter, &UserProfileFilter::hoverOn, this, [this]() {�� userName_->setCursor(Qt::PointingHandCursor);� userName_->installEventFilter(filter);� auto filter = new UserProfileFilter(user_id, userName_);�� refreshAuthorColor();� // otherwise this will just set it.� // Set the user color asynchrono#else��������ZhÑø�������������oÁ€�#else��������ZhÑø�������������oÁ€������ userName_->setFixedWidth(QFontMetrics(userName_->font()).width(userName_->text()));����\���ZhÑø���������\���oÁ€������ // width deprecated in 5.13:����%���ZhÑø���������%���oÁ€������#if QT_VERSION < QT_VERSION_CHECK(5, 11, 0)���� -���úw. �������������úw. ������� userName_->setAlignment(Qt::AlignLeft | Qt::AlignTop);� userName_->setAttribute(Qt::WA_Hover);� userName_->setToolTipDuration(1500);� userName_->setToolTip(user_id);� userName_->setText(utils::replaceEmoji(fm.elidedText(sender, Qt::ElideRight, 500)));� userName_->setFont(usernameFont);� userName_ = new QLabel(this);�ad��Ù��©�����-�������À��e����ý��Ê��´��²��Ž����G��E��D��Ç ��m ��- ��Ò��~��j��7��!����û��ú��´��²��±��4��Ç -��† -��+ -��× ��à �� ��z ��x ��! ��û��ú��É��È��}��{��z��û��©��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������� const mtx::ev const mtx::ev const mtx::events::Sticker &event,�8������úw.����� const mtx::ev const mtx::ev const mtx::ev const mtx::events::Sticker &event,�8������úw.����� const mtx::ev const mtx::ev const mtx::ev const mtx::events::Sticker &event,�8������úw.����� const mtx::events::Sticker &event,�8������úw.�������TimelineItem::TimelineItem(StickerItem *image,�������úw.�������������úw. �������������úw.�������)������úw.��������}� addSaveImageAction(image);� ������úw.�������������úw.�������� markOwnMessagesAsReceived(event.sender);�� image, event, with_sender);� setupWidgetLayout<mtx::events::RoomEvent<mtx::events::msg::Image>, ImageItem>(�{� , room_id_{room_id}� , message_type_(mtx::events::MessageType::Image)� : QWidget(parent)� QWidget *parent)�������úw.�������%������úw.������� const QString &room_id,�"������úw.�������+������úw.������� bool with_sender,�!������úw.������� const mtx::events::RoomEvent<mtx::events::msg::Image> &event,�S������úw.�������TimelineItem::TimelineItem(ImageItem *image,�������úw.�������������úw. ���������� ���úw.�������'������úw.��������}� setupLocalWidgetLayout<VideoItem>(video, userid, withSender);�� init();� ������úw.�������{� , room_id_{room_id}� , message_type_(mtx::events::MessageType::Video)� : QWidget{parent}� QWidget *parent)�������úw.�������%������úw.������� const QString &room_id,�"������úw.�������+������úw.������� bool withSender,�!��� -���úw.������� const QString &userid,�"������úw.�������+������úw.�������TimelineItem::TimelineItem(VideoItem *video,�������úw.�������������úw. ���������� ���úw.�������'������úw.��������}� setupLocalWidgetLayout<AudioItem>(audio, userid, withSender);�� init();� ������úw.�������{� , room_id_{room_id}� , message_type_(mtx::events::MessageType::Audio)� : QWidget{parent}� QWidget *parent)�������úw.�������%������úw.������� const QString &room_id,�"������úw.�������+������úw.������� bool withSender,�!��� -���úw.�������ad��Q��u�����B�������²����Z��/��Ý��—��h��W��&��ú ��Ï ��¦ ��` ��1 ��' ��& ��æ��³��²��d��c��5��ò��Â��Á��r��a��3��ú -��ð -��ï -��¼ -��º -��¹ -��< -��â ��¢ ��G ��ó��ß��¬��–��”��p��o��)��(��Ý��Û��Ú��_����Å��j������Ð��º��¸��”��“��O��M��L��Ï��u��;��"��ö��â��¯��™��—��‡��†��@��>��=����Þ��²����S��?�� const QString const QString const QString &userid,�"������úw.�������"������?¬ const QString const QString const QString const QString const QString &userid,�"������úw.�������"������?¬ const QString const QString const QString const QString &userid,�"������úw.�������"������?¬ const QString const QString &userid,�"������úw.�������+������úw.�������TimelineItem::TimelineItem(AudioItem *audio,�������úw.�������������úw. ���������� ���úw.�������'������úw.��������}� setupLocalWidgetLayout<FileItem>(file, userid, withSender);�� init();� ������úw.�������{� , room_id_{room_id}� , message_type_(mtx::events::MessageType::File)� : QWidget{parent}� QWidget *parent)�������úw.�������%������úw.������� const QString &room_id,�"������úw.�������+������úw.������� bool withSender,�!��� -���úw.������� const QString &userid,�"������úw.�������+������úw.�������TimelineItem::TimelineItem(FileItem *file,�������úw.�������������úw. �������������úw.�������&������úw.��������}� addSaveImageAction(image);� ������úw.�������������úw.�������� setupLocalWidgetLayout<ImageItem>(image, userid, withSender);�� init();� ������úw.�������{� , room_id_{room_id}� , message_type_(mtx::events::MessageType::Image)� : QWidget{parent}� QWidget *parent)�������úw.�������%������úw.������� const QString &room_id,�"������úw.�������+������úw.������� bool withSender,�!��� -���úw.������� const QString &userid,�"������úw.�������+������úw.�������TimelineItem::TimelineItem(ImageItem *image,�������úw.�������������úw. ���������� ���úw.�������'������úw.��������}� adjustMessageLayout();� ������úw.�������� }� setupSimpleLayout();�������úw.������� generateBody(formatted_body);� } else {� setUserAvatar(userid);���� ���úw.�������������úw.�������� setupAvatarLayout(displayName);� generateBody(userid, displayName, formatted_body);� if (withSender) {� ��� -���úw.�������� generateTimestamp(timestamp);� ������úw.���������� ���úw.�������� formatted_body.replace("mx-reply", "div");� formatted_body = utils::linkifyMessage(formatted_body);�� }� timestamp};� utils::descriptiveTime(timestamp),� body,� userid,� "You: ",� descriptionMsg_ = {emptyEventId,� } else {� timestamp};� utils::descriptiveTime(timestamp),� QString("* %1 %2").arg(displayName).arg(body),� userid,� "",� descriptionMsg_ = {emptyEventId,� formatted_body = QString("<em>%1</em>").arg(formatted_body);�ad��‹��ÿ������������®��\����ä��ã��ª��„��ƒ��[�� ��ê ��¾ ��½ ��i ��E ��D �� ��î��³��B��A��ÿ��å��À��w��#��"��ö -��Æ -��‰ -�� -�� -��å �� ��q ��& ��ú��ù��°����J��I��ö��õ��¿��v��,��+��Ö��‘����Ž��‹��j��f��ò��˜��G����¬��l��X��B��,��*����Ø��×��‚�� ����Ú��‹��Š��T����ß��Þ��˜��—��b�������� if (ty == mtx::events::MessageType::Emote) {�� QString emptyEventId;� ������?¬…±�������������?¬…±�������� formatted_body = body.toHtmlEscaped();� if (formatted_body == body.trimmed().toHtmlEscaped())� // Escape html if the input is not formatted.�� auto formatted_body = utils::markdownToHtml(body);�������?¬…±������� // Generate the html body to be rendered.�� auto timestamp = QDateTime::currentDateTime();���� ���?¬…±���������� ���?¬…±�������'������?¬…±������� auto displayName = Cache::displayName(room_id_, userid);�������?¬…±�������� addReplyAction();� ������?¬…±������� init();� ������?¬…±�������{� , room_id_{room_id}� , message_type_(ty)� : QWidget(parent)� QWidget *parent)�%������?¬…±������� const QString &room_id,�"������?¬…±�������+������?¬…±������� bool withSender,�!��� -���?¬…±������� QString body,�������?¬…±�������$������?¬…±������� const QString &userid,�"������?¬…±�������+������?¬…±�������TimelineItem::TimelineItem(mtx::events::MessageType ty,�������?¬…±�������������?¬…± �������5������?¬…±������� */� * For messages created locally.�/*��}� setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Maximum);� parentWidget()->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Maximum);�� statusIndicator_->setFixedHeight(tsFm.height() - tsFm.leading());� statusIndicator_->setFixedWidth(tsFm.height() - tsFm.leading());� statusIndicator_ = new StatusIndicator(this);�� QFontMetrics tsFm(timestampFont_);� ������?¬…±�������������?¬…±�������� timestampFont_.setStyleHint(QFont::Monospace);� timestampFont_.setFamily("Monospace");� timestampFont_.setPointSizeF(timestampFont_.pointSizeF() * 0.9);�� contextBtn_->setMenu(contextMenu_);� contextBtn_->setIconSize(QSize(buttonSize_ / 2, buttonSize_ / 2));� contextBtn_->setIcon(context_icon);� context_icon.addFile(":/icons/icons/ui/vertical-ellipsis.png");� QIcon context_icon;� ������?¬…±�������� contextBtn_->setCornerRadius(buttonSize_ / 2);� ������?¬…± -�������������?¬…±�������&������?¬…±������� contextBtn_->setFixedSize(buttonSize_, buttonSize_);� contextBtn_->setToolTip(tr("Options"));� contextBtn_ = new FlatButton(this);�� connect(replyBtn_, &FlatButton:: QIcon reply_icon;� ������úw.� QIcon reply_icon;� ������úw.� QIcon reply_icon;� ������úw.������� ������?¬…±� QIcon reply_icon;� ������úw.� QIcon reply_icon;� ������úw.���������� -���úw.�������� replyBtn_->setCornerRadius(buttonSize_ / 2);� ��� ���úw. -�������������úw.�������$������úw.������� replyBtn_->setFixedSize(buttonSize_, buttonSize_);� replyBtn_->setToolTip(tr("Reply"));� replyBtn_ = new FlatButton(this);�� mainLayout_->setSpacing(0);� mainLayout_->setContentsMargins(conf::timeline::headerLeftMargin, 0, 0, 0);�� topLayout_->addLayout(mainLayout_);� topLayout_->setSpacing(0);� conf::timeline::msgLeftMargin, conf::timeline::msgTopMargin, 0, 0);� topLayout_->setContentsMargins(�� actionLayout_->setSpacing(0);� actionLayout_->setContentsMargins(13, 1, 13, 0);�� messageLayout_->setSpacing(MSG_PADDING);� messageLayout_->setContentsMargins(0, 0, MSG_RIGHT_MARGIN, 0);� actionLayout_ = new QHBoxLayout;� ��� ���úw. -�������������úw.������� messageLayout_ = new QHBoxLayout;� ������úw. -�������������úw.�������ad��/ ��{ ��������������D��â��á��£����M��7��þ ��ý ��Í ��{ ��e ��O �� ��×��Ö����w��v��N�����Ý��±��°��\��8��7�� ��á -��¦ -��q -��p -��V -�� -��ï ��¦ ��R ��Q ��% ��õ��¸����€��d����ð��¥��y��x��/�����É��È����œ��f����Ó��Ò��}��8��6��5��2���� ��Õ��£��z��N����ï��Û��Å��¯������ƒ��‚��A������Õ��š��™��c��%��î��í��Ï��Î��™��˜����������� if (ty == mtx::events::MessageType::Emote) {�� QString emptyEventId;�� formatted_body = body.toHtmlEscaped();� if (formatted_body == body.trimmed().toHtmlEscaped())� // Escape html if the input is not formatted.�� auto formatted_body = utils::markdownToHtml(body);� // Generate the html body to be rendered.�� auto timestamp = QDateTime::currentDateTime();� auto displayName = Cache::displayName(room_id_, userid);�� addReplyAction();� init();�{� , room_id_{room_id}� , message_type_(ty)� : QWidget(parent)� QWidget *parent)� const QString &room_id,� bool withSender,� QString body,� const QString &userid,�TimelineItem::TimelineItem(mtx::events::MessageType ty,� */� * For messages created locally.�/*��}� setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Maximum);� parentWidget()->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Maximum);�� statusIndicator_->setFixedHeight(tsFm.height() - tsFm.leading());� statusIndicator_->setFixedWidth(tsFm.height() - tsFm.leading());� statusIndicator_ = new StatusIndicator(this);�� QFontMetrics tsFm(timestampFont_);�� timestampFont_.setStyleHint(QFont::Monospace);� timestampFont_.setFamily("Monospace");� timestampFont_.setPointSizeF(timestampFont_.pointSizeF() * 0.9);�� contextBtn_->setMenu(contextMenu_);� contextBtn_->setIconSize(QSize(buttonSize_ / 2, buttonSize_ / 2));� contextBtn_->setIcon(context_icon);� context_icon.addFile(":/icons/icons/ui/vertical-ellipsis.png");� QIcon context_icon;�� contextBtn_->setCornerRadius(buttonSize_ / 2);� contextBtn_->setFixedSize(buttonSize_, buttonSize_);� contextBtn_->setToolTip(tr("Options"));� contextBtn_ = new FlatButton(this);�� connect(replyBtn_, &FlatButton::clicked, this, &TimelineItem::replyAction);� replyBtn_->setIconSize(QSize(buttonSize_ / 2, buttonSize_ / 2));� replyBtn_->setIcon(reply_icon);� reply_icon.addFile(":/icons/icons/ui/mail-reply.png");� QIcon reply_icon;�� replyBtn_->setCornerRadius(buttonSize_ / 2);� replyBtn_->setFixedSize(buttonSize_, buttonSize_);� replyBtn_->setToolTip(tr("Reply"));� replyBtn_ = new FlatButton(this);�� mainLayout_->setSpacing(0);� mainLayout_->setContentsMargins(conf::timeline::headerLeftMargin, 0, 0, 0);�� topLayout_->addLayout(mainLayout_);� topLayout_->setSpacing(0);� conf::timeline::msgLeftMargin, conf::timeline::msgTopMargin, 0, 0);� topLayout_->setContentsMargins(�� actionLayout_->setSpacing(0);� actionLayout_->setContentsMargins(13, 1, 13, 0);�� messageLayout_->setSpacing(MSG_PADDING);� messageLayout_->setContentsMargins(0, 0, MSG_RIGHT_MARGIN, 0);� mainLayout_ = new QVBoxLayout mainLayout_ = new QVBoxLayout;� ������úw. -�������������úw.������� topLayout_ = new QHBoxLayout(this);�� &TimelineItem::finishedGeneratingColor);� this,� &QFutureWatcher<QString>::finished,� connect(colorGenerating_,� colorGenerating_ = new QFutureWatcher<QString>(this);�� connect(viewRawMessage_, &QAction::triggered, this, &TimelineItem::openRawMessageViewer);� connect(markAsRead_, &QAction::triggered, this, &TimelineItem::sendReadReceipt);� ChatPage::instance(), &ChatPage::themeChanged, this, &TimelineItem::refreshAuthorColor);�ad��`��L�����4�������}��{��K��õ��Á��À��l��b��`��_��Z������á ��Ž ��X ��W ��ú��ð��î��í��è��¤��¢��z��b��a����í��·����F������} -��{ -��z -��u -��) -��' -��÷ ��’ ��\ ��[ �� ��´�� ��–��”��“��Ž��L�� -��% -�� -�� -��Ê ��— ��V ��U ��& ��% ��ý��ü��Ý��Û��Ú��Õ��¯����‹��H����µ��w��(��Ê��u����é��º��¸��·��²��‡��…��:������Î��n����ð��ä��ã��À����™��`����Ó��Ò��µ��‡��0����ú��ù��á��������������������� try {�� }� return;� "failed to retrieve event {} from {}", event_id, room_id);� nhlog::net()->warn(� if (err) {�� using namespace mtx::events;� const mtx::events::collections::TimelineEvents &res, mtx::http::RequestErr err) {� [event_id, room_id, proxy = std::move(proxy)](� event_id,� room_id,� http::client()->get_event(�� });� Q_UNUSED(dialog);� auto dialog = new dialogs::RawMessage{QString::fromStdString(obj.dump(4))};� connect(proxy.get(), &EventProxy::eventRetrieved, this, [](const nlohmann::json &obj) {� auto proxy = std::make_shared<EventProxy>();�� const auto room_id = room_id_.toStdString();� const auto event_id = event_id_.toStdString();�������?¬…±�������{�TimelineItem::openRawMessageViewer() const�void��}� });� }� TimelineItem::addAvatar()�������úw.�������������?¬…±���������� ���úw.�TimelineItem::addAvaTimelineItem::addAvaTimelineItem::addAvatar()�������úw.�TimelineItem::addAvaTimelineItem::addAvaTimelineItem::addAvatar()�������úw.�TimelineItem::addAvaTimelineItem::addAvatar()�������úw.�TimelineItem::addAvatar()�������úw.�������������?¬…±���������� ���úw.���������� ���?¬…±�TimelineItem::addAvatar()�������úw.�TimelineItem::addAvaTimelineItem::addAvaTimelineItem::addAvatar()�������úw.�TimelineItem::addAvatar()�������úw.���������� ���úw.�������void��}� }� });� olm::request_keys(room_id_.toStdString(), event_id_.toStdString());� connect(requestKeys, &QAction::triggered, this, [this]() {�� contextMenu_->addAction(requestKeys);� auto requestKeys = new QAction("Request encryption keys", this);�������úw.������� if (contextMenu_) {� ������úw. -�������{�TimelineItem::addKeyRequestAction()�������úw.�������������úw.�������void��}� emit ChatPage::instance()->messageReply(related);� ������úw. �������������úw.�������������úw.�������$������úw.�������1������úw.�������� related.room = room_id_;� related.related_event = eventId().toStdString();� related.quoted_user = descriptionMsg_.userid;� related.quoted_body = body_->toPlainText();� related.type = message_type_;� RelatedInfo related;� ������úw.�������������úw.�������� return;� if (!body_)�������úw. -�������{�TimelineItem::replyAction()�������úw.�������������úw.�������void��}� }� connect(replyAction, &QAction::triggered, this, &TimelineItem::replyAction);�� contextMenu_->addAction(replyAction);� auto replyAction = new QAction("Reply", this);�������úw.������� if (contextMenu_) {� ������úw. -�������{�TimelineItem::addReplyAction()�������úw.�������������úw.�������void��}� }� connect(saveImage, &QAction::triggered, image, &ImageItem::saveAs);�� contextMenu_->addAction(saveImage);� auto saveImage = new QAction("Save image", this);���� ���úw.������� if (contextMenu_) {� ������úw. -�������{�TimelineItem::addSaveImageAction(ImageItem *image)�������úw.�������������úw.�������"��� ���úw.�������-������úw.�������ad��‹ ��# -������������¿��d����ü��æ��ä��‹��Š��Y��X��5��3��2��· ��K �� - ��¯��[��G����ÿ��ý��¨��ƒ��‚��Q��O��N��Ñ -��d -��# -��r��T��@�� ��÷ -��õ -��ž -��x -��w -��F -��D -��C -�� -��½ �� ��] ��1 �� ��ê��Ô��Ò��{��U��T��#��!�� ����ò��î��”��g��4����ô��À��ª��¨��˜��~��}��L��K����¿��h��g�� -��¦��¥����>����è��ª��ƒ��‚��\��[��@��÷��ö��³��ƒ��‚��[��J����÷��í��ì��Í��Ë��Ê��Ç��Ú�����/*��}� adjustMessageLayout();�� }� setupSimpleLayout();� generateBody(formatted_body);� } else {� setUserAvatar(sender);�� setupAvatarLayout(displayName);� generateBody(sender, displayName, formatted_body);�� auto displayName = Cache::displayName(room_id_, sender);� if (with_sender) {�� generateTimestamp(timestamp);�� timestamp};� utils::descriptiveTime(timestamp),� " sent a notification",� sender,� Cache::displayName(room_id_, sender),� descriptionMsg_ = {event_id_,�� auto body = QString::fromStdString(event.content.body).trimmed().toHtmlEscaped();� auto formatted_body = utils::linkifyMessage(utils::getMessageBody(event).trimmed());�� const auto timestamp = QDateTime::fromMSecsSinceEpoch(event.origin_server_ts);� const auto sender = QString::fromStdString(event.sender);� event_id_ = QString::fromStdString(event.event_id);�� markOwnMessagesAsReceived(event.sender);�� addReplyAction();� init();�{� , room_id_{room_id}� , message_type_(mtx::events::MessageType::Notice)� : QWidget(parent)� QWidget *parent)� const QString &room_id,� bool with_sender,�TimelineItem::TimelineItem(const mtx::events::RoomEvent<mtx::events::msg::Notice> &event,� */� * Used to display remote notice messages.�/*��}� markOwnMessagesAsReceived(event.sender);�� video, event, with_sender);� bool with_sen bool with_sen bool with_sender,�!������úw.�������!������?¬…±�� bool with_sen bool with_sen bool with_sender,�!������úw.�������!������?¬…±�� bool with_sender,�!������úw.������� const mtx::events::RoomEvent<mtx::events::msg::Audio> &event,�S������úw.�������TimelineItem::TimelineItem(AudioItem *audio,�������úw.�������������úw. ���������� ���úw.�������'������úw.��������}� markOwnMessagesAsReceived(event.sender);�� file, event, with_sender);� setupWidgetLayout<mtx::events::RoomEvent<mtx::events::msg::File>, FileItem>(�{� , room_id_{room_id}� , message_type_(mtx::events::MessageType::File)� : QWidget(parent)� QWidget *parent)�������úw.�������%������úw.������� const QString &room_id,�"������úw.�������+������úw.������� bool with_sender,�!������úw.������� const mtx::events::RoomEvent<mtx::events::msg::File> &event,�R������úw.�������TimelineItem::TimelineItem(FileItem *file,�������úw.�������������úw. �������������úw.�������&������úw.��������}� addSaveImageAction(image);�� markOwnMessagesAsReceived(event.sender);�� setupWidgetLayout<mtx::events::Sticker, StickerItem>(image, event, with_sender);�{� , room_id_{room_id}� : QWidget(parent)� QWidget *parent)�������úw.�������%������úw.������� const QString &room_id,�"������úw.�������+������úw.������� bool with_sender,�!������úw.�������ad��;��§������������¼��º��¹��´��é��ç��°��¯��„��J����» ��± ��° ��l ��' ��ø��÷��¨��§���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������� QFontMetrics fm(usernameFont);�� QFontMetrics fm(usernameFont);� ������úw.������� ������?¬…±�������������úw.�������������?¬…±�������� QFontMetric� QFontMetrics fm(usernameFont);�� QFontMetrics fm(usernameFont);�� QFontMetrics fm(usernameFont);� ������úw.�������������úw.�������� usernameFont.setWeight(QFont::Medium);� usernameFont.setPointSizeF(usernameFont.pointSizeF() * 1.1);� QFont usernameFont;� ������úw.�������������úw.�������� }� sender = displayname.split(":")[0].split("@")[1];� if (displayname.split(":")[0].split("@").size() > 1)� // TODO: Fix this by using a UserId type.� if (displayname.startsWith("@")) {�� auto sender = displayname;�������úw.�������{�TimelineItem::generateUserName(const QString &user_id, const QString &displayname)�������úw.�������������úw.�������&������úw.�������/������úw.�������>������úw.�������G������úw.�������void��}� generateBody(body);� ������úw.�������������úw.�������ad�� ��T ����� -�������¼��m��Ñ��Ï��Î��É��ˆ��†��ì ��T ��Ì����˜��ó��ñ����¬��k��¦ -��™ -��Õ ��Ô ��¥ ��£ ��¢ ��ƒ ��~ ��Ö��Ô��”��4��3��×��–��8��,��*��)��$��Å��Ã��€��S��'��ò��è��§��v��7��ç��œ��r��_��^��Ö��Õ��‹��`����õ��š��ˆ��~��|��{��v��������9��Ñ����¿ QString userColor = colorGenerating_->result();� ������?¬…±���������� ���úw.���������� ���?¬…±������� nhlog::ui()->debug("finishedGeneratingColor for: {}", userName_->toolTip().toStdString());�{�TimelineItem::finishedGeneratingColor()�������úw.�������������?¬…±�������������?¬…±�������void��}� }� }� userName_->setStyleSheet("QLabel { color : " + userColor + "; }");� } else {� colorGenerating_->setFuture(QtConcurrent::run(generate));� if (userColor.isEmpty()) {� // If the color is empty, then generate it asynchronously�� QString userColor = Cache::userColor(userName_->toolTip());�������?¬…±���������� ���úw.���������� ���?¬…±�������� };� return userColor;� userName_->toolTip(), backgroundColor().name());� QString userColor = utils::generateContrastingHexColor(� std::function<QString()> generate = [this]() {� // generate user's unique color.� if (userName_) {� ��� ���úw. -������� ��� ���?¬…± -������� }� colorGenerating_->waitForFinished();� colorGenerating_->cancel();� if (colorGenerating_->isRunning()) {� // Cancel and wait if we are already generating the color.�{�TimelineItem::refreshAuthorColor()�������úw.�������������?¬…±�������������?¬…±�������void��}� });� ChatPage::instance()->currentRoom());� MainWindow::instance()->openUserProfile(user_id,� connect(body_, &TextLabel::userProfileTriggered, this, [](const QString &user_id) {�� body_->setTextInteractionFlags(Qt::TextSelectableByMouse | Qt::TextBrowserInteraction);� body_ = new TextLabel(utils::replaceEmoji(body), this);�{�TimelineItem::generateBody(const QString &body)�������úw.�������������?¬…±�������������?¬…±�������"������?¬…±�������+������úw.�������+������?¬…±�������void�// Only the body is displayed.��}� sendReadReceipt();� ������?¬…±�������� statusIndicator_->setState(StatusIndicatorState::Received);�������úw. -�������������?¬…± -�������#������?¬…±�������,������?¬…±�������B������úw.�������B������?¬…±������� else� statusIndicator_->setState(StatusIndicatorState::Encrypted);�������úw. -�������������?¬…± -�������#������?¬…±�������,������?¬…±�������B��� ���úw.�������B��� ���?¬…±������� if (isEncrypted)� ������úw.������� ������?¬…±�������� isReceived_ = true;� �� statusIndicator_->setState(StatusIndicatorState::Read);� statusIndicator_->setSta statusIndicator_->setState(StatusIndicatorState::Read);������� statusIndicator_->setSta statusIndicator_->setState(StatusIndicatorState::Read);������� statusIndicator_->setState(StatusIndicatorState::Read);������� statusIndicator_->setState(StatusIndicatorState::Read);�������úw. -�������#������úw.�������,������úw.�������B������úw.������� if (statusIndicator_->state() != StatusIndicatorState::Encrypted)� ������úw. -�������������úw.�������*������úw.�������@��� ���úw.�������{�TimelineItem::markRead()�������úw.�������������úw.�������void��}� statusIndicator_->setState(StatusIndicatorState::Received);�������úw. -�������#������úw.�������,������úw.�������B������úw.������� if (sender == settings.value("auth/user_id").toString().toStdString())� QSettings settings;� ��� ���úw.�������������úw.�������ad��-��Q������������¥��Q��™��Î��¸��¶��_��9��8��������7 ��¶��a��Þ��v��b��/������À -��š -��™ -��h -��f -��e -��b -��7 -��3 -��a �� ��‰��!�� ��Ù��Ã��Á��‰��G��F������Í��`��á��à��[��Ï��Î��¨��g��D����Ó��¬��«��…��„��A��Ð��Ï��Œ��\��[��4��#��õ��¨��ž����V��T��S��P��������������/*��}� adjustMessageLayout();� ������úw.������� ������?¬…±�������� }� setupSimpleLayout();�������úw.�������������?¬…±������� generateBody(formatted_body);� } else {� setUserAvatar(sender);�� setupAvatarLayout(displayName);� generateBody(sender, displayName, formatted_body);�� auto displayName = Cache::displayName(room_id_, sender);�������úw.�������������?¬…±������� if (with_sender) {� ������úw.������� ������?¬…±�������� generateTimestamp(timestamp);�� timestamp};� utils::descriptiveTime(timestamp),� " sent a notification",� sender,� Cache::displayName(room_id_, sender),� descriptionMsg_ = {event_id_,�� auto body = QString::fromStdString(event.content.body).trimmed().toHtmlEscaped();�������úw.�������������?¬…±������� auto formatted_body = utils::linkifyMessage(utils::getMessageBody(event).trimmed());�������úw.�������������?¬…±�������� const auto timestamp = QDateTime::fromMSecsSinceEpoch(event.origin_server_ts);���� ���úw.���������� ���?¬…±������� const auto sender = QString::fromStdString(event.sender);�������úw.�������������?¬…±������� event_id_ = QString::fromStdString(event.event_id);�� markOwnMessagesAsReceived(event.sender);�� addReplyAction();� ������úw.������� ������?¬…±������� init();� ������úw.������� ������?¬…±�������{� , room_id_{room_id}� , message_type_(mtx::events::MessageType::Notice)� : QWidget(parent)� QWidget *parent)�������?¬…±�������%������úw.�������%������?¬…±������� const QString &room_id,�"������úw.�������"������?¬…±�������+������úw.�������+������?¬…±������� bool with_sender,�!������úw.�������!������?¬…±�������TimelineItem::TimelineItem(const mtx::events::RoomEvent<mtx::events::msg::Notice> &event,�������úw.�������������?¬…±�������������úw.�������������?¬…±�������T������úw.�������T������?¬…±������� */� * Used to display remote notice messages.�/*��}� markOwnMessagesAsReceived(event.sender);�� video, event, with_sender);� setupWidgetLayout<mtx::events::RoomEvent<mtx::events::msg::Video>, VideoItem>(�{� , room_id_{room_id}� , message_type_(mtx::events::MessageType::Video)� : QWidget(parent)� QWidget *parent)�������?¬…±�������%������úw.�������%������?¬…±������� const QString &room_id,�"������úw.�������"������?¬…±�������+������úw.�������+������?¬…±������� bool with_sender,�!������úw.�������!������?¬…±������� const mtx::events::RoomEvent<mtx::events::msg::Video> &event,�S������úw.�������S������?¬…±�������TimelineItem::TimelineItem(VideoItem *video,�������úw.�������������?¬…±�������������úw. �������������?¬…± ���������� ���úw.���������� ���?¬…±�������'������úw.�������'������?¬…±��������}� markOwnMessagesAsReceived(event.sender);�� audio, event, with_sender);� setupWidgetLayout<mtx::events::RoomEvent<mtx::even QWidget *parent)�������úw.�������������?¬…±�������%������úw.������� QWidget *pare QWidget *pare QWidget *parent)�������úw.�������%������úw.������� const QString &room_id,�"������úw.�������+������úw.�������ad��r��Z�����3�������Ö��Ò��=��ü��¡��M��9����ð ��î ��Ê ��œ ��› ��j ��i ��% ��Ï��Î��]��å��ä��}��(��á -��à -��º -��› -��x -��. -��ð ��É ��È ��¢ ��¡ ��r ��/ ��ÿ��þ��×��Æ��˜��_��U��T��!��������ò��î��Z��’��ž��r��^��,��������ê��é��¸��·��s��1��0��Ó��o��n����Ú��Ù��©��ƒ��(����Ï��‘��j��i��C��B��'��ä��´��³��Œ��{��M��(������þ��ü��û��Ý��Û��·��Š��ˆ��‡��‚��i��g��'��%��$����Ü��Ú��í��������{�TimelineItem::markOwnMessagesAsReceived(const std::string &sender)�void��}� statusIndicator_->setState(StatusIndicatorState::Sent);�{�TimelineItem::markSent()�void��}� colorGenerating_->waitForFinished();� colorGenerating_->cancel();�{�TimelineItem::~TimelineItem()��}� adjustMessageLayout();�� }� setupSimpleLayout();� generateBody(formatted_body);� } else {� setUserAvatar(sender);�� setupAvatarLayout(displayName);� generateBody(sender, displayName, formatted_body);� if (with_sender) {�� generateTimestamp(timestamp);�� timestamp};� utils::descriptiveTime(timestamp),� QString(": %1").arg(body),� sender,� sender == settings.value("auth/user_id") ? "You" : displayName,� descriptionMsg_ = {event_id_,� QSettTimelineItem::TimelineItem(const mtx::events::RoomEvent<mtx:TimelineItem::TimeliTimelineItem::TimelineItem(const mtx::evTimelineItem::TimelineItem(const mtx::evTimelineItem::TimeliTimelineItem::TimeliTimelineItem::TimeliTimelineItem::TimeliTimelineItem::TimeliTimelineItem::TimeliTimelineItem::TimeliTimelineItem::TimeliTimelineItem::TimeliTimelineItem::TimeliTimelineItem::TimelineItem(const mtx::events::RoomEvent<mtx:TimelineItem::TimelineItem(const mtx::events::RoomEvent<mtx::events::msg::Text> &event,�������úw.�������������úw. �������R������úw.������� */� * Used to display remote text messages.�/*��}� adjustMessageLayout();� ������úw.�������� }� setupSimpleLayout();�������úw.������� generateBody(formatted_body);� } else {� setUserAvatar(sender);�� setupAvatarLayout(displayName);� generateBody(sender, displayName, formatted_body);� if (with_sender) {� ������úw.�������� generateTimestamp(timestamp);�� timestamp};� utils::descriptiveTime(timestamp),� QString("* %1 %2").arg(displayName).arg(body),� sender,� "",� descriptionMsg_ = {event_id_,�� formatted_body = QString("<em>%1</em>").arg(formatted_body);� auto displayName = Cache::displayName(room_id_, sender);�������úw.������� auto timestamp = QDateTime::fromMSecsSinceEpoch(event.origin_server_ts);���� ���úw.�������� auto body = QString::fromStdString(event.content.body).trimmed().toHtmlEscaped();�������úw.������� auto formatted_body = utils::linkifyMessage(utils::getMessageBody(event).trimmed());�������úw.�������� const auto sender = QString::fromStdString(event.sender);�������úw.������� event_id_ = QString::fromStdString(event.event_id);�� markOwnMessagesAsReceived(event.sender);�� addReplyAction();� ������úw.������� init();� ������úw.�������{� , room_id_{room_id}� , message_type_(mtx::events::MessageType::Emote)� : QWidget(parent)� QWidget *parent)�������úw.�������%������úw.������� const QString &room_id,�"������úw.�������+������úw.������� bool with_sender,�!������úw.�������TimelineItem::TimelineItem(const mtx::events::RoomEvent<mtx::events::msg::Emote> &event,�������úw.�������������úw.�������S������úw.������� */� * Used to display remote emote messages.�ad��¤��¬�����;�������¿��d����ü��Ê��´��²��Ž��`��_��.��-��é ��“ ��’ ��! ��©��¨��A��ì��ë��§����&����Í -�� -��h -��g -��A -��@ -�� -��Î ��ž �� ��v ��e ��7 ��þ��ô��ó��À��¾��½��c��a��=������ ����Ç��Å��5��3��2��-��®��¬������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������{�TimelineItem::mark{�TimelineItem::markOwnMessagesAsReceive{�TimelineItem::markOwnMessagesAsReceive{�TimelineItem::mark{�TimelineItem::mark{�TimelineItem::mark{�TimelineItem::mark{�TimelineItem::mark{�TimelineItem::mark{�TimelineItem::mark{�TimelineItem::markOwnMessagesAsReceive{�TimelineItem::mark{�TimelineItem::mark{�TimelineItem::mark{�TimelineItem::markOwnMessagesAsReceived(const std::string {�TimelineItem::markOwnMessagesAsReceive{�TimelineItem::markOwnMessagesAsReceived(const std::string &sender)�������úw.{�TimelineItem::markOwnMessagesAsReceived(const std::string {�TimelineItem::markOwnMessagesAsReceived(const std::string &sender)�������úw.�������������úw.�������<������úw.�������void��}� statusIndicator_->setState(StatusIndicatorState::Sent);� ������úw. -�������������úw.�������$������úw.�������:������úw.�������{�TimelineItem::markSent()�������úw.�������������úw.�������void��}� colorGenerating_->waitForFinished();� colorGenerating_->cancel();�{�TimelineItem::~TimelineItem()�������úw.�������������úw.�������������úw.��������}� adjustMessageLayout();� ������úw.�������� }� setupSimpleLayout();�������úw.������� generateBody(formatted_body);� } else {� setUserAvatar(sender);�� setupAvatarLayout(displayName);� generateBody(sender, displayName, formatted_body);� if (with_sender) {� ������úw.�������� generateTimestamp(timestamp);�� timestamp};� utils::descriptiveTime(timestamp),� QString(": %1").arg(body),� sender,� sender == settings.value("auth/user_id") ? "You" : displayName,� descriptionMsg_ = {event_id_,� QSettings settings;� ��� ���úw.�������������úw.�������� auto displayName = Cache::displayName(room_id_, sender);�������úw.������� auto timestamp = QDateTime::fromMSecsSinceEpoch(event.origin_server_ts);���� ���úw.�������� auto body = QString::fromStdString(event.content.body).trimmed().toHtmlEscaped();�������úw.������� auto formatted_body = utils::linkifyMessage(utils::getMessageBody(event).trimmed());�������úw.�������� const auto sender = QString::fromStdString(event.sender);�������úw.������� event_id_ = QString::fromStdString(event.event_id);�� markOwnMessagesAsReceived(event.sender);�� addReplyAction();� ������úw.������� init();� ������úw.�������{� , room_id_{room_id}� , message_type_(mtx::events::MessageType::Text)� : QWidget(parent)� QWidget *parent)�������úw.�������%������úw.������� const QString &room_id,�"������úw.�������+������úw.������� bool with_sender,�!������úw.�������ad��J��&�����0�������ì��ê��²��x��6��â��à��ß��Ú��-��+��ë ��ê ��Ÿ ��W �� �� ��Ó��Ñ��Ð��Ë��d��b��ó��Û��Ú��™��H��G��)��(��ú -��§ -��} -��f -��\ -�� -��Ý ��Æ ��o ��A ��* �� ��Í��™��‚��x��&��h��Á��¿��¾��¹��÷��õ����Œ��K��á��´����4����ð��‹��c��L��ç��¿��¨��B��T��ƒ��y��x case StatusIndicatorState::Empty:�������?¬…±�������$������úw.�������$������?¬…±������� break;� setToolTip(tr("Sent"));� case StatusIndicatorState::Sent:�������?¬…±�������$������úw.�������$������?¬…±������� break;� setToolTip(tr("Seen"));� case StatusIndicatorState::Read:�������?¬…±�������$������úw.�������$������?¬…±������� break;� setToolTip(tr("Delivered"));� case StatusIndicatorState::Received:�������?¬…±�������$������úw.�������$������?¬…±������� break;� setToolTip(tr("Encrypted"));� case StatusIndicatorState::Encrypted:�������?¬…±�������$��� ���úw.�������$��� ���?¬…±������� switch (state) {�������úw.�������������?¬…±�������� state_ = state;� ������úw. -������� ������?¬…± -�������������úw.�������������?¬…±�������{�StatusIndicator::setState(StatusIndicatorState state)�������úw.�������������?¬…±�������������úw.������ case StatusIndicatorState::Empty:�������úw.�������������?¬…±�������$������úw.������� case StatusIndicatorState::Empty:�������úw.�������������?¬…±�������$������úw.�������$������?¬…±����� case StatusI case StatusIndicatorState::Empty case StatusIndicatorState::Empty:�������úw.����� case StatusIndicatorState::Empty:�������úw.����� case StatusIndicatorState::Empty case StatusIndicatorState::Empty case StatusI case StatusIndicatorState::Empty case StatusIndicatorState::Empty case StatusIndicatorState::Empty case StatusIndicatorState::Empty case StatusIndicatorState::Empty case StatusIndicatorState::Empty:�������úw.�������$������úw.������� }� break;� paintIcon(p, doubleCheckmarkIcon_);� case StatusIndicatorState::Read: {�������úw.�������$������úw.������� }� break;� paintIcon(p, checkmarkIcon_);� case StatusIndicatorState::Received: {�������úw.�������$������úw.������� break;� paintIcon(p, lockIcon_);� case StatusIndicatorState::Encrypted:�������úw.�������$��� ���úw.������� }� break;� paintIcon(p, clockIcon_);� case StatusIndicatorState::Sent: {�������úw.�������$������úw.������� switch (state_) {�������úw. -�������� p.setPen(iconColor_);�� PainterHighQualityEnabler hq(p);� ������úw.�������#������úw.������� Painter p(this);� ������úw.�������������úw.�������� return;� if (state_ == StatusIndicatorState::Empty)� ������úw. -�������������úw.�������-������úw.�������{�StatusIndicator::paintEvent(QPaintEvent *)�������úw.���������� -���úw.�������������úw.�������void��}� QIcon(pixmap).paint(&p, rect(), Qt::AlignCenter, QIcon::Normal);�� painter.fillRect(pixmap.rect(), p.pen().color());� painter.setCompositionMode(QPainter::CompositionMode_SourceIn);� QPainter painter(&pixmap);� ������úw.�������������úw.�������� auto pixmap = icon.pixmap(width());�������úw.�������{�StatusIndicator::paintIcon(QPainter &p, QIcon &icon)�������úw.���������� ���úw.�������������úw.�������&������úw.�������)������úw.�������0������úw.�������void��}� doubleCheckmarkIcon_.addFile(":/icons/icons/ui/double-tick-indicator.png");� checkmarkIcon_.addFile(":/icons/icons/ui/checkmark.png");� clockIcon_.addFile(":/icons/icons/ui/clock.png");� lockIcon_.addFile(":/icons/icons/ui/lock.png");�{� : QWidget(parent)�ad��±��±�����9�������þ��Ñ��¹��¸��v����²��±��‚����Y��X��9��7��6��1��ã ��á ��¿ ��| ��8 ��é��«��\��þ��©��R����î -��ì -��ë -��æ -��“ -��‘ -��F -��ü ��û ��² ��R ��ö��À��´��³����}��i��0��Ò��£��¢��…��W�����Þ��Ê��É��±��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������� tr try {�� }� tr try {�� try {�� tr tr tr tr try {�� }� return;� "failed to retrieve event {} from {}", event_id, room_id);� nhlog::net()->warn(� if (err) {�� using namespace mtx::events;� const mtx::events::collections::TimelineEvents &res, mtx::http::RequestErr err) {� [event_id, room_id, proxy = std::move(proxy)](� event_id,� room_id,� http::client()->get_event(�� });� Q_UNUSED(dialog);�������úw. ������� auto dialog = new dialogs::RawMessage{QString::fromStdString(obj.dump(4))};� connect(proxy.get(), &EventProxy::eventRetrieved, this, [](const nlohmann::json &obj) {� auto proxy = std::make_shared<EventProxy>();�������úw.�������� const auto room_id = room_id_.toStdString();�������úw.������� const auto event_id = event_id_.toStdString();�������úw.�������{�TimelineItem::openRawMessageViewer() const�������úw.�������������úw.�������void��}� });� }� event_id_.toStdString());� room_id_.toStdString(),� "failed to read_event ({}, {})",� nhlog::net()->warn(� if (err) {� [this](mtx::http::RequestErr err) {� event_id_.toStdString(),� http::client()->read_event(room_id_.toStdString(),� if (!event_id_.isEmpty())�{�TimelineItem::sendReadReceipt() const�������úw.�������������úw.�������void��}� setUserAvatar(userid);�� setupAvatarLayout(displayName);�� generateUserName(userid, displayName);�� auto displayName = Cache::displayName(room_id_, userid);�������úw.������� auto userid = descriptionMsg_.userid;�������úw.�������������úw. -�������,������úw.������� // TODO: should be replaced with the proper event struct.�� return;� if (userAvatar_)� ������úw. -�������{�ad��Ò��î�����@�������û��¦��¤��o��B����á��¬��}��|��-��Ü ��– ��V �� �� ��â��à��ß��Ú��Ž��Œ��_��2����Ñ��œ��m��l����Ì -��† -��F -�� -�� -��Ò ��Ð ��Ï ��Ê �� ��‹ ��S �� ��ã��«��x��w��J�� -��Ë��ˆ��G����á��³��†��…��<����¿��³��²��V��î��N��U������Å��›��e��3�����¡��t����´��U��#��ÿ��þ��½��Ÿ��“��‚��©�������������������� connect(� });� });� emit eventRedacted(event_id_);�� }� return;� err->matrix_error.error)));� .arg(QString::fromStdString(� emit redactionFailed(tr("Message redaction failed: %1")� if (err) {� [this](const mtx::responses::EventId &, mtx::http::RequestErr err) emit ChatPage::instance( emit ChatPage::instance( emit ChatPage::instance( emit emit emit emit emit emit emit ChatPage::instance()->removeTimelineEvent(room_id_, event_id);�������úw. ������� connect(this, &TimelineItem::eventRedacted, this, [this](const QString &event_id) {�� });� MainWindow::instance()->openReadReceiptsDialog(event_id_);� if (!event_id_.isEmpty())� connect(showReadReceipts_, &QAction::triggered, this, [this]() {�� contextMenu_->addAction(redactMsg_);� contextMenu_->addAction(markAsRead_);� contextMenu_->addAction(viewRawMessage_);� contextMenu_->addAction(showReadReceipts_);� redactMsg_ = new QAction("Redact message", this);� viewRawMessage_ = new QAction("View raw message", this);� markAsRead_ = new QAction("Mark as read", this);� showReadReceipts_ = new QAction("Read receipts", this);� contextMenu_ = new QMenu(this);�� auto buttonSize_ = 32;�������úw.������� body_ = nullptr;� ������úw. -������� userName_ = nullptr;� ��� ���úw. -������� timestamp_ = nullptr;� ��� -���úw. -������� userAvatar_ = nullptr;� ������úw. -�������{�TimelineItem::init()�������úw.�������������úw.�������void��}� mainLayout_->addLayout(messageLayout_);�� messageLayout_->setAlignment(actionLayout_, Qt::AlignTop);� messageLayout_->setAlignment(timestamp_, Qt::AlignTop);� messageLayout_->setAlignment(statusIndicator_, Qt::AlignTop);� actionLayout_->setAlignment(contextBtn_, Qt::AlignTop | Qt::AlignRight);� actionLayout_->setAlignment(replyBtn_, Qt::AlignTop | Qt::AlignRight);�� messageLayout_->addWidget(timestamp_);� messageLayout_->addWidget(statusIndicator_);� messageLayout_->addLayout(actionLayout_);� actionLayout_->addWidget(contextBtn_);� actionLayout_->addWidget(replyBtn_);� messageLayout_->addWidget(body_, 1);�{�TimelineItem::adjustMessageLayout()�������úw.�������������úw.�������void��}� mainLayout_->addLayout(messageLayout_);�� messageLayout_->setAlignment(actionLayout_, Qt::AlignTop);� messageLayout_->setAlignment(timestamp_, Qt::AlignTop);� messageLayout_->setAlignment(statusIndicator_, Qt::AlignTop);� actionLayout_->setAlignment(contextBtn_, Qt::AlignTop | Qt::AlignRight);� actionLayout_->setAlignment(replyBtn_, Qt::AlignTop | Qt::AlignRight);�� messageLayout_->addWidget(timestamp_);� messageLayout_->addWidget(statusIndicator_);� messageLayout_->addLayout(actionLayout_);� actionLayout_->addWidget(contextBtn_);� actionLayout_->addWidget(replyBtn_);� messageLayout_->addLayout(widgetLayout_, 1);�{�TimelineItem::adjustMessageLayoutForWidget()�������úw.�������������úw.�������void�ad�� ��������������ô��Ÿ��I��=��û��Ñ��›��i��6��× ��ª ��4 ��Ö��w��E��!�� ��Ë����¡������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������ connect(� connect(� connect(� connect(� });� });� emit eventRedacted(event_id_);�#������úw. �������� }� return;� err->matrix_error.error)));� .arg(QString::fromStdString(� emit redactionFailed(tr("Message redaction failed: %1")�+������úw. ������� if (err) {� [this](const mtx::responses::EventId &, mtx::http::RequestErr err) {� event_id_.toStdString(),� room_id_.toStdString(),� http::client()->redact_event(� if (!event_id_.isEmpty())� connect(redactMsg_, &QAction::triggered, this, [this]() {� });� emit ChatPage::instance()->showNotification(msg);�������úw. ������� connect(this, &TimelineItem::redactionFailed, this, [](const QString &msg) {� });�ad��u��É������������ÿ��Û��Ž��F��õ��ã����†��„��@��;��7 ��5 ��É����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������� generateUserName(user_id, displayname);� ������úw.������� ������?¬…±�������������úw.�������������?¬…±�������#������úw.�������#������?¬…± generateUserName(user_id, displayname);� ������úw. generateUserName(user_id, displayname);� ������úw.�������������úw.�������#������úw.�������{�TimelineItem::generateBody(const QString &user_id, const QString &displayname, const QString &body)�������úw.�������������úw.�������"������úw.�������+������úw.�������:������úw.�������C������úw.�������V������úw.�������_������úw.�������void�// The username/timestamp is displayed along with the message body.�}� }� userName_->setStyleSheet("QLabel { color : " + userColor + "; }");� }� Cache::insertUserColor(userName_->toolTip(), userColor);� if (Cache::userColor(userName_->toolTip()).isEmpty()) {� // another TimelineItem might have inserted in the meantime.� if (!userColor.isEmpty()) {��ad��Æ��B ������������þ��Æ��®����|��z��y��t��ë��é��Ï��—��•��”����+��)��ç ��Ï �� ��J ��H ��G ��B ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������void��}� stylvoid��}� style()->drawPrimitive(QStyle::PE_Widget, &opt, &p, this);� void��}� style()->drawPrimitive(QStyle::PE_Widget, &ovoid��}� style()->drawPrimitive(Qvoid��}� style()->drawPrimitive(Qvoid��}� style()->drawPrimitive(QStyle::PE_Widget, &opt, &p, this);� QPainter p(this);� ������úw.�������������úw.������� opt.init(this);� QStyleOption opt;� ������úw.�������������úw.�������{�TimelineItem::paintEvent(QPaintEvent *)�������úw.���������� -���úw.�������������úw.�������void��}� contextMenu_->exec(event->globalPos());� if (contextMenu_)�{�TimelineItem::contextMenuEvent(QContextMenuEvent *event)�������úw.�������������úw.������� ������úw.�������3������úw.�������void��}� userAvatar_->setImage(room_id_, userid);�� return;� if (userAvatar_ == nullptr)� ������úw. -�������{�ad���������;�������Á��™��P��ü��û��Ï��Ÿ��b��ï ��î ��ª ��b ��6 ��ë��¿��¾��u��F������»��º��„��;��ñ -��ð -��› -��V -��T -��S -��P -��/ -��+ -��· ��] �� ��Ì��q���� ��ó��Ý��Û��·��‰��ˆ��3��¾��½��‹��<��;����Ç������I��H��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������� if (ty == mtx::events::MessageType::Emote) {�� if (ty == mtx::events::MessageTy if (ty == mtx::events::MessageTy if (ty == mtx::events::MessageType::Emote) {�� if (ty == mtx::events::MessageTy if (ty == mtx::events::MessageTy if (ty == mt if (ty == mtx::events::MessageTy if (ty == mtx::events::MessageTy if (ty == mt if (ty == mt if (ty == mt if (ty == mtx::events::MessageType::Emote) {�� if (ty == mt if (ty == mtx::events::MessageTy if (ty == mtx::events::MessageType::Emote) {�� QString emptyEventId;� ������úw.�������������úw.�������� formatted_body = body.toHtmlEscaped();� if (formatted_body == body.trimmed().toHtmlEscaped())� // Escape html if the input is not formatted.�� auto formatted_body = utils::markdownToHtml(body);�������úw.������� // Generate the html body to be rendered.�� auto timestamp = QDateTime::currentDateTime();���� ���úw.���������� ���úw.�������'������úw.������� auto displayName = Cache::displayName(room_id_, userid);�������úw.�������� addReplyAction();� ������úw.������� init();� ������úw.�������{� , room_id_{room_id}� , message_type_(ty)� : QWidget(parent)� QWidget *parent)�������úw.�������%������úw.������� const QString &room_id,�"������úw.�������+������úw.������� bool withSender,�!��� -���úw.������� QString body,�������úw.�������$������úw.������� const QString &userid,�"������úw.�������+������úw.�������TimelineItem::TimelineItem(mtx::events::MessageType ty,�������úw.�������������úw. �������5������úw.������� */� * For messages created locally.�/*��}� setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Maximum);� parentWidget()->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Maximum);�� statusIndicator_->setFixedHeight(tsFm.height() - tsFm.leading());� statusIndicator_->setFixedWidth(tsFm.height() - tsFm.leading());� statusIndicator_ = new StatusIndicator(this);�� QFontMetrics tsFm(timestampFont_);� ������úw.�������������úw.�������� timestampFont_.setStyleHint(QFont::Monospace);� timestampFont_.setFamily("Monospace");� timestampFont_.setPointSizeF(timestampFont_.pointSizeF() * 0.9);�� contextBtn_->setMenu(contextMenu_);� contextBtn_->setIconSize(QSize(buttonSize_ / 2, buttonSize_ / 2));� contextBtn_->setIcon(context_icon);� context_icon.addFile(":/icons/icons/ui/vertical-ellipsis.png");� QIcon context_icon;� ������úw.�������������úw.�������� contextBtn_->setCornerRadius(buttonSize_ / 2);� ������úw. -�������������úw.�������&������úw.������� contextBtn_->setFixedSize(buttonSize_, buttonSize_);� contextBtn_->setToolTip(tr("Options"));� contextBtn_ = new FlatButton(this);�� connect(replyBtn_, &FlatButton::clicked, this, &TimelineItem::replyAction);� replyBtn_->setIconSize(QSize(buttonSize_ / 2, buttonSize_ / 2));� replyBtn_->setIcon(reply_icon);� reply_icon.addFile(":/icons/icons/ui/mail-reply.png");�ad��Í��Ù�����<�������þ��ý��ø������]��\��/��’��…��é ��è ��¹ ��· ��¶ ��— ��’ �� �� ��Ð��p��o����Ò��t��h��f��e��`������Ð -��£ -��w -��B -��8 -�� -��Ú ��› ��K ��� ��Ö��Ã��Â��N��M����Ø��†��m�������ö��ô��ó��î��ž��œ��9��Ù����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������� QString userColor = colorGenerating_->result();� ��� QString user QString user QString userColor = colorGenerating_->result();� ������úw.������� ��� QString userColor = colorGenerating_->result();� ������úw.������� ��� QString user QString userColor = colorGenerating_->result();� ������úw.������� ��� QString userColor = colorGenerat QString user QString userColor = colorGenerat QString userColor = colorGenerat QString userColor = colorGenerat QString userColor = colorGenerating_->result();� ������úw.���������� ���úw.������� nhlog::ui()->debug("finishedGeneratingColor for: {}", userName_->toolTip().toStdString());�{�TimelineItem::finishedGeneratingColor()�������úw.�������������úw.�������void��}� }� }� userName_->setStyleSheet("QLabel { color : " + userColor + "; }");� } else {� colorGenerating_->setFuture(QtConcurrent::run(generate));� if (userColor.isEmpty()) {� // If the color is empty, then generate it asynchronously�� QString userColor = Cache::userColor(userName_->toolTip());�������úw.���������� ���úw.�������� };� return userColor;� userName_->toolTip(), backgroundColor().name());� QString userColor = utils::generateContrastingHexColor(� std::function<QString()> generate = [this]() {� // generate user's unique color.� if (userName_) {� ��� ���úw. -������� }� colorGenerating_->waitForFinished();� colorGenerating_->cancel();� if (colorGenerating_->isRunning()) {� // Cancel and wait if we are already generating the color.�{�TimelineItem::refreshAuthorColor()�������úw.�������������úw.�������void��}� });� ChatPage::instance()->currentRoom());� MainWindow::instance()->openUserProfile(user_id,� connect(body_, &TextLabel::userProfileTriggered, this, [](const QString &user_id) {�� body_->setTextInteractionFlags(Qt::TextSelectableByMouse | Qt::TextBrowserInteraction);� body_ = new TextLabel(utils::replaceEmoji(body), this);�{�TimelineItem::generateBody(const QString &body)�������úw.�������������úw.�������"������úw.�������+������úw.�������void�// Only the body is displayed.��}� sendReadReceipt();� ������úw.�������� statusIndicator_->setState(StatusIndicatorState::Received);�������úw. -�������#������úw.�������,������úw.�������B������úw.������� else� statusIndicator_->setState(StatusIndicatorState::Encrypted);�������úw. -�������#������úw.�������,������úw.�������B��� ���úw.������� if (isEncrypted)� ������úw.�������� isReceived_ = true;� ������úw. -�������{�TimelineItem::markReceived(bool isEncrypted)�������úw.�������������úw.�������!������úw.�������void��}�ad��q��©������������à��É��¿��¾��¬��ª��©������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������}� update();�� }� break;� setToolTip("");�ad����C�����I�������ì��¹��£��¡��J��$��#��ò��ð��ï��r����Ä ��i �� �� ��Î��¸��¶��_��9��8����������Ö��Ò��<��û -�� -��L -��8 -�� -��î ��ì ��È ��š ��™ ��h ��g �� ��Ç��\��[��ê��r��q��K�� -��ç��´��v��O��N��(��'��ø��›��š��W��'��&��ÿ��î��À��‡��}��|��I��G��F��C�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������/*��}� adjustMessageLayout();� ������úw.������� ������?¬…±�������� /*��}� adjust/*��}� adjust/*��}� adjustMessageLayout();� ��/*��}� adjustMessageLayout();� ��/*��}� adjustMessageLayout();� ������úw.������� ��/*��}� adjust/*��}� adjustMessageLayout();� ��/*��}� adjustMessageLayout();� ��/*��}� adjust/*��}� adjust/*��}� adjust/*��}� adjust/*��}� adjust/*��}� adjust/*��}� adjust/*��}� adjust/*��}� adjust/*��}� adjust/*��}� adjustMessageLayout();� ������úw.�������� }� setupSimpleLayout();�������úw.������� generateBody(formatted_body);� } else {� setUserAvatar(sender);�� setupAvatarLayout(displayName);� generateBody(sender, displayName, formatted_body);�� auto displayName = Cache::displayName(room_id_, sender);�������úw.������� if (with_sender) {� ������úw.�������� generateTimestamp(timestamp);�� timestamp};� utils::descriptiveTime(timestamp),� " sent a notification",� sender,� Cache::displayName(room_id_, sender),� descriptionMsg_ = {event_id_,�� auto body = QString::fromStdString(event.content.body).trimmed().toHtmlEscaped();�������úw.������� auto formatted_body = utils::linkifyMessage(utils::getMessageBody(event).trimmed());�������úw.�������� const auto timestamp = QDateTime::fromMSecsSinceEpoch(event.origin_server_ts);���� ���úw.������� const auto sender = QString::fromStdString(event.sender);�������úw.������� event_id_ = QString::fromStdString(event.event_id);�� markOwnMessagesAsReceived(event.sender);�� addReplyAction();� ������úw.������� init();� ������úw.�������{� , room_id_{room_id}� , message_type_(mtx::events::MessageType::Notice)� : QWidget(parent)� QWidget *parent)�������úw.�������%������úw.������� const QString &room_id,�"������úw.�������+������úw.������� bool with_sender,�!������úw.�������TimelineItem::TimelineItem(const mtx::events::RoomEvent<mtx::events::msg::Notice> &event,�������úw.�������������úw.�������T������úw.������� */� * Used to display remote notice messages.�/*��}� markOwnMessagesAsReceived(event.sender);�� video, event, with_sender);� setupWidgetLayout<mtx::events::RoomEvent<mtx::events::msg::Video>, VideoItem>(�{� , room_id_{room_id}� , message_type_(mtx::events::MessageType::Video)� : QWidget(parent)� QWidget *parent)�������úw.�������%������úw.������� const QString &room_id,�"������úw.�������+������úw.������� bool with_sender,�!������úw.������� const mtx::events::RoomEvent<mtx::events::msg::Video> &event,�S������úw.�������TimelineItem::TimelineItem(VideoItem *video,�������úw.�������������úw. ���������� ���úw.�������'������úw.��������}� markOwnMessagesAsReceived(event.sender);�� audio, event, with_sender);� setupWidgetLayout<mtx::events::RoomEvent<mtx::events::msg::Audio>, AudioItem>(�{� , room_id_{room_id}� , message_type_(mtx::events::MessageType::Audio)� : QWidget(parent)�ad��Ä��<������������é��ß��Ý��Ü��×��Q��O������á��‹��^��G��ò ��Å ��® ��] ��5 �� ��Í��¥��Ž��<�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������� case StatusIndicatorState::Empty:�������úw.�������������?¬…±����� case StatusIndicatorState::Empty case StatusI case StatusIndicatorState::Empty case StatusIndicatorState::Empty case StatusIndicatorState::Empty case StatusIndicatorState::Empty case StatusIndicatorState::Empty case StatusIndicatorState::Empty:�������úw.�������$������úw.������� break;� setToolTip(tr("Sent"));� case StatusIndicatorState::Sent:�������úw.�������$������úw.������� break;� setToolTip(tr("Seen"));� case StatusIndicatorState::Read:�������úw.�������$������úw.������� break;� setToolTip(tr("Delivered"));� case StatusIndicatorState::Received:�������úw.�������$������úw.������� break;� setToolTip(tr("Encrypted"));� case StatusIndicatorState::Encrypted:�������úw.�������$��� ���úw.������� switch (state) {�������úw.�������� state_ = state;� ������úw. -�������������úw.�������{�StatusIndicator::setState(StatusIndicatorState state)�������úw.�������������úw.�������������úw.�������0������úw.�������void��}� }� break;� \ No newline at end of file diff --git a/src/timeline/DelegateChooser.cpp b/src/timeline/DelegateChooser.cpp new file mode 100644 index 0000000000000000000000000000000000000000..632a2a646eeaa2cd7090bf0753268b0ffdc4a7e3 --- /dev/null +++ b/src/timeline/DelegateChooser.cpp @@ -0,0 +1,138 @@ +#include "DelegateChooser.h" + +#include "Logging.h" + +// uses private API, which moved between versions +#include <QQmlEngine> +#include <QtGlobal> + +QQmlComponent * +DelegateChoice::delegate() const +{ + return delegate_; +} + +void +DelegateChoice::setDelegate(QQmlComponent *delegate) +{ + if (delegate != delegate_) { + delegate_ = delegate; + emit delegateChanged(); + emit changed(); + } +} + +QVariant +DelegateChoice::roleValue() const +{ + return roleValue_; +} + +void +DelegateChoice::setRoleValue(const QVariant &value) +{ + if (value != roleValue_) { + roleValue_ = value; + emit roleValueChanged(); + emit changed(); + } +} + +QVariant +DelegateChooser::roleValue() const +{ + return roleValue_; +} + +void +DelegateChooser::setRoleValue(const QVariant &value) +{ + if (value != roleValue_) { + roleValue_ = value; + recalcChild(); + emit roleValueChanged(); + } +} + +QQmlListProperty<DelegateChoice> +DelegateChooser::choices() +{ + return QQmlListProperty<DelegateChoice>(this, + this, + &DelegateChooser::appendChoice, + &DelegateChooser::choiceCount, + &DelegateChooser::choice, + &DelegateChooser::clearChoices); +} + +void +DelegateChooser::appendChoice(QQmlListProperty<DelegateChoice> *p, DelegateChoice *c) +{ + DelegateChooser *dc = static_cast<DelegateChooser *>(p->object); + dc->choices_.append(c); +} + +int +DelegateChooser::choiceCount(QQmlListProperty<DelegateChoice> *p) +{ + return static_cast<DelegateChooser *>(p->object)->choices_.count(); +} +DelegateChoice * +DelegateChooser::choice(QQmlListProperty<DelegateChoice> *p, int index) +{ + return static_cast<DelegateChooser *>(p->object)->choices_.at(index); +} +void +DelegateChooser::clearChoices(QQmlListProperty<DelegateChoice> *p) +{ + static_cast<DelegateChooser *>(p->object)->choices_.clear(); +} + +void +DelegateChooser::recalcChild() +{ + for (const auto choice : choices_) { + auto choiceValue = choice->roleValue(); + if (!roleValue_.isValid() || !choiceValue.isValid() || choiceValue == roleValue_) { + if (child) { + child->setParentItem(nullptr); + child = nullptr; + } + + choice->delegate()->create(incubator, QQmlEngine::contextForObject(this)); + return; + } + } +} + +void +DelegateChooser::componentComplete() +{ + QQuickItem::componentComplete(); + recalcChild(); +} + +void +DelegateChooser::DelegateIncubator::statusChanged(QQmlIncubator::Status status) +{ + if (status == QQmlIncubator::Ready) { + chooser.child = dynamic_cast<QQuickItem *>(object()); + if (chooser.child == nullptr) { + nhlog::ui()->error("Delegate has to be derived of Item!"); + return; + } + + chooser.child->setParentItem(&chooser); + connect(chooser.child, &QQuickItem::heightChanged, &chooser, [this]() { + chooser.setHeight(chooser.child->height()); + }); + chooser.setHeight(chooser.child->height()); + QQmlEngine::setObjectOwnership(chooser.child, + QQmlEngine::ObjectOwnership::JavaScriptOwnership); + + } else if (status == QQmlIncubator::Error) { + for (const auto &e : errors()) + nhlog::ui()->error("Error instantiating delegate: {}", + e.toString().toStdString()); + } +} diff --git a/src/timeline/DelegateChooser.h b/src/timeline/DelegateChooser.h new file mode 100644 index 0000000000000000000000000000000000000000..68ebeb0418ae88291e8c35ee551a83d9fa18bdea --- /dev/null +++ b/src/timeline/DelegateChooser.h @@ -0,0 +1,82 @@ +// A DelegateChooser like the one, that was added to Qt5.12 (in labs), but compatible with older Qt +// versions see KDE/kquickitemviews see qtdeclarative/qqmldelagatecomponent + +#pragma once + +#include <QQmlComponent> +#include <QQmlIncubator> +#include <QQmlListProperty> +#include <QQuickItem> +#include <QtCore/QObject> +#include <QtCore/QVariant> + +class QQmlAdaptorModel; + +class DelegateChoice : public QObject +{ + Q_OBJECT + Q_CLASSINFO("DefaultProperty", "delegate") + +public: + Q_PROPERTY(QVariant roleValue READ roleValue WRITE setRoleValue NOTIFY roleValueChanged) + Q_PROPERTY(QQmlComponent *delegate READ delegate WRITE setDelegate NOTIFY delegateChanged) + + QQmlComponent *delegate() const; + void setDelegate(QQmlComponent *delegate); + + QVariant roleValue() const; + void setRoleValue(const QVariant &value); + +signals: + void delegateChanged(); + void roleValueChanged(); + void changed(); + +private: + QVariant roleValue_; + QQmlComponent *delegate_ = nullptr; +}; + +class DelegateChooser : public QQuickItem +{ + Q_OBJECT + Q_CLASSINFO("DefaultProperty", "choices") + +public: + Q_PROPERTY(QQmlListProperty<DelegateChoice> choices READ choices CONSTANT) + Q_PROPERTY(QVariant roleValue READ roleValue WRITE setRoleValue NOTIFY roleValueChanged) + + QQmlListProperty<DelegateChoice> choices(); + + QVariant roleValue() const; + void setRoleValue(const QVariant &value); + + void recalcChild(); + void componentComplete() override; + +signals: + void roleChanged(); + void roleValueChanged(); + +private: + struct DelegateIncubator : public QQmlIncubator + { + DelegateIncubator(DelegateChooser &parent) + : QQmlIncubator(QQmlIncubator::AsynchronousIfNested) + , chooser(parent) + {} + void statusChanged(QQmlIncubator::Status status) override; + + DelegateChooser &chooser; + }; + + QVariant roleValue_; + QList<DelegateChoice *> choices_; + QQuickItem *child = nullptr; + DelegateIncubator incubator{*this}; + + static void appendChoice(QQmlListProperty<DelegateChoice> *, DelegateChoice *); + static int choiceCount(QQmlListProperty<DelegateChoice> *); + static DelegateChoice *choice(QQmlListProperty<DelegateChoice> *, int index); + static void clearChoices(QQmlListProperty<DelegateChoice> *); +}; diff --git a/src/timeline/TimelineItem.cpp b/src/timeline/TimelineItem.cpp deleted file mode 100644 index 7916bd80c2efee1efef09b1d3f70772d3ad18802..0000000000000000000000000000000000000000 --- a/src/timeline/TimelineItem.cpp +++ /dev/null @@ -1,960 +0,0 @@ -/* - * nheko Copyright (C) 2017 Konstantinos Sideris <siderisk@auth.gr> - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - */ -#include <functional> - -#include <QContextMenuEvent> -#include <QDesktopServices> -#include <QFontDatabase> -#include <QMenu> -#include <QTimer> -#include <QtGlobal> - -#include "ChatPage.h" -#include "Config.h" -#include "Logging.h" -#include "MainWindow.h" -#include "Olm.h" -#include "ui/Avatar.h" -#include "ui/Painter.h" -#include "ui/TextLabel.h" - -#include "timeline/TimelineItem.h" -#include "timeline/widgets/AudioItem.h" -#include "timeline/widgets/FileItem.h" -#include "timeline/widgets/ImageItem.h" -#include "timeline/widgets/VideoItem.h" - -#include "dialogs/RawMessage.h" -#include "mtx/identifiers.hpp" - -constexpr int MSG_RIGHT_MARGIN = 7; -constexpr int MSG_PADDING = 20; - -StatusIndicator::StatusIndicator(QWidget *parent) - : QWidget(parent) -{ - lockIcon_.addFile(":/icons/icons/ui/lock.png"); - clockIcon_.addFile(":/icons/icons/ui/clock.png"); - checkmarkIcon_.addFile(":/icons/icons/ui/checkmark.png"); - doubleCheckmarkIcon_.addFile(":/icons/icons/ui/double-tick-indicator.png"); -} - -void -StatusIndicator::paintIcon(QPainter &p, QIcon &icon) -{ - auto pixmap = icon.pixmap(width()); - - QPainter painter(&pixmap); - painter.setCompositionMode(QPainter::CompositionMode_SourceIn); - painter.fillRect(pixmap.rect(), p.pen().color()); - - QIcon(pixmap).paint(&p, rect(), Qt::AlignCenter, QIcon::Normal); -} - -void -StatusIndicator::paintEvent(QPaintEvent *) -{ - if (state_ == StatusIndicatorState::Empty) - return; - - Painter p(this); - PainterHighQualityEnabler hq(p); - - p.setPen(iconColor_); - - switch (state_) { - case StatusIndicatorState::Sent: { - paintIcon(p, clockIcon_); - break; - } - case StatusIndicatorState::Encrypted: - paintIcon(p, lockIcon_); - break; - case StatusIndicatorState::Received: { - paintIcon(p, checkmarkIcon_); - break; - } - case StatusIndicatorState::Read: { - paintIcon(p, doubleCheckmarkIcon_); - break; - } - case StatusIndicatorState::Empty: - break; - } -} - -void -StatusIndicator::setState(StatusIndicatorState state) -{ - state_ = state; - - switch (state) { - case StatusIndicatorState::Encrypted: - setToolTip(tr("Encrypted")); - break; - case StatusIndicatorState::Received: - setToolTip(tr("Delivered")); - break; - case StatusIndicatorState::Read: - setToolTip(tr("Seen")); - break; - case StatusIndicatorState::Sent: - setToolTip(tr("Sent")); - break; - case StatusIndicatorState::Empty: - setToolTip(""); - break; - } - - update(); -} - -void -TimelineItem::adjustMessageLayoutForWidget() -{ - messageLayout_->addLayout(widgetLayout_, 1); - actionLayout_->addWidget(replyBtn_); - actionLayout_->addWidget(contextBtn_); - messageLayout_->addLayout(actionLayout_); - messageLayout_->addWidget(statusIndicator_); - messageLayout_->addWidget(timestamp_); - - actionLayout_->setAlignment(replyBtn_, Qt::AlignTop | Qt::AlignRight); - actionLayout_->setAlignment(contextBtn_, Qt::AlignTop | Qt::AlignRight); - messageLayout_->setAlignment(statusIndicator_, Qt::AlignTop); - messageLayout_->setAlignment(timestamp_, Qt::AlignTop); - messageLayout_->setAlignment(actionLayout_, Qt::AlignTop); - - mainLayout_->addLayout(messageLayout_); -} - -void -TimelineItem::adjustMessageLayout() -{ - messageLayout_->addWidget(body_, 1); - actionLayout_->addWidget(replyBtn_); - actionLayout_->addWidget(contextBtn_); - messageLayout_->addLayout(actionLayout_); - messageLayout_->addWidget(statusIndicator_); - messageLayout_->addWidget(timestamp_); - - actionLayout_->setAlignment(replyBtn_, Qt::AlignTop | Qt::AlignRight); - actionLayout_->setAlignment(contextBtn_, Qt::AlignTop | Qt::AlignRight); - messageLayout_->setAlignment(statusIndicator_, Qt::AlignTop); - messageLayout_->setAlignment(timestamp_, Qt::AlignTop); - messageLayout_->setAlignment(actionLayout_, Qt::AlignTop); - - mainLayout_->addLayout(messageLayout_); -} - -void -TimelineItem::init() -{ - userAvatar_ = nullptr; - timestamp_ = nullptr; - userName_ = nullptr; - body_ = nullptr; - auto buttonSize_ = 32; - - contextMenu_ = new QMenu(this); - showReadReceipts_ = new QAction("Read receipts", this); - markAsRead_ = new QAction("Mark as read", this); - viewRawMessage_ = new QAction("View raw message", this); - redactMsg_ = new QAction("Redact message", this); - contextMenu_->addAction(showReadReceipts_); - contextMenu_->addAction(viewRawMessage_); - contextMenu_->addAction(markAsRead_); - contextMenu_->addAction(redactMsg_); - - connect(showReadReceipts_, &QAction::triggered, this, [this]() { - if (!event_id_.isEmpty()) - MainWindow::instance()->openReadReceiptsDialog(event_id_); - }); - - connect(this, &TimelineItem::eventRedacted, this, [this](const QString &event_id) { - emit ChatPage::instance()->removeTimelineEvent(room_id_, event_id); - }); - connect(this, &TimelineItem::redactionFailed, this, [](const QString &msg) { - emit ChatPage::instance()->showNotification(msg); - }); - connect(redactMsg_, &QAction::triggered, this, [this]() { - if (!event_id_.isEmpty()) - http::client()->redact_event( - room_id_.toStdString(), - event_id_.toStdString(), - [this](const mtx::responses::EventId &, mtx::http::RequestErr err) { - if (err) { - emit redactionFailed(tr("Message redaction failed: %1") - .arg(QString::fromStdString( - err->matrix_error.error))); - return; - } - - emit eventRedacted(event_id_); - }); - }); - connect( - ChatPage::instance(), &ChatPage::themeChanged, this, &TimelineItem::refreshAuthorColor); - connect(markAsRead_, &QAction::triggered, this, &TimelineItem::sendReadReceipt); - connect(viewRawMessage_, &QAction::triggered, this, &TimelineItem::openRawMessageViewer); - - colorGenerating_ = new QFutureWatcher<QString>(this); - connect(colorGenerating_, - &QFutureWatcher<QString>::finished, - this, - &TimelineItem::finishedGeneratingColor); - - topLayout_ = new QHBoxLayout(this); - mainLayout_ = new QVBoxLayout; - messageLayout_ = new QHBoxLayout; - actionLayout_ = new QHBoxLayout; - messageLayout_->setContentsMargins(0, 0, MSG_RIGHT_MARGIN, 0); - messageLayout_->setSpacing(MSG_PADDING); - - actionLayout_->setContentsMargins(13, 1, 13, 0); - actionLayout_->setSpacing(0); - - topLayout_->setContentsMargins( - conf::timeline::msgLeftMargin, conf::timeline::msgTopMargin, 0, 0); - topLayout_->setSpacing(0); - topLayout_->addLayout(mainLayout_); - - mainLayout_->setContentsMargins(conf::timeline::headerLeftMargin, 0, 0, 0); - mainLayout_->setSpacing(0); - - replyBtn_ = new FlatButton(this); - replyBtn_->setToolTip(tr("Reply")); - replyBtn_->setFixedSize(buttonSize_, buttonSize_); - replyBtn_->setCornerRadius(buttonSize_ / 2); - - QIcon reply_icon; - reply_icon.addFile(":/icons/icons/ui/mail-reply.png"); - replyBtn_->setIcon(reply_icon); - replyBtn_->setIconSize(QSize(buttonSize_ / 2, buttonSize_ / 2)); - connect(replyBtn_, &FlatButton::clicked, this, &TimelineItem::replyAction); - - contextBtn_ = new FlatButton(this); - contextBtn_->setToolTip(tr("Options")); - contextBtn_->setFixedSize(buttonSize_, buttonSize_); - contextBtn_->setCornerRadius(buttonSize_ / 2); - - QIcon context_icon; - context_icon.addFile(":/icons/icons/ui/vertical-ellipsis.png"); - contextBtn_->setIcon(context_icon); - contextBtn_->setIconSize(QSize(buttonSize_ / 2, buttonSize_ / 2)); - contextBtn_->setMenu(contextMenu_); - - timestampFont_.setPointSizeF(timestampFont_.pointSizeF() * 0.9); - timestampFont_.setFamily("Monospace"); - timestampFont_.setStyleHint(QFont::Monospace); - - QFontMetrics tsFm(timestampFont_); - - statusIndicator_ = new StatusIndicator(this); - statusIndicator_->setFixedWidth(tsFm.height() - tsFm.leading()); - statusIndicator_->setFixedHeight(tsFm.height() - tsFm.leading()); - - parentWidget()->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Maximum); - setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Maximum); -} - -/* - * For messages created locally. - */ -TimelineItem::TimelineItem(mtx::events::MessageType ty, - const QString &userid, - QString body, - bool withSender, - const QString &room_id, - QWidget *parent) - : QWidget(parent) - , message_type_(ty) - , room_id_{room_id} -{ - init(); - addReplyAction(); - - auto displayName = Cache::displayName(room_id_, userid); - auto timestamp = QDateTime::currentDateTime(); - - // Generate the html body to be rendered. - auto formatted_body = utils::markdownToHtml(body); - - // Escape html if the input is not formatted. - if (formatted_body == body.trimmed().toHtmlEscaped()) - formatted_body = body.toHtmlEscaped(); - - QString emptyEventId; - - if (ty == mtx::events::MessageType::Emote) { - formatted_body = QString("<em>%1</em>").arg(formatted_body); - descriptionMsg_ = {emptyEventId, - "", - userid, - QString("* %1 %2").arg(displayName).arg(body), - utils::descriptiveTime(timestamp), - timestamp}; - } else { - descriptionMsg_ = {emptyEventId, - "You: ", - userid, - body, - utils::descriptiveTime(timestamp), - timestamp}; - } - - formatted_body = utils::linkifyMessage(formatted_body); - formatted_body.replace("mx-reply", "div"); - - generateTimestamp(timestamp); - - if (withSender) { - generateBody(userid, displayName, formatted_body); - setupAvatarLayout(displayName); - - setUserAvatar(userid); - } else { - generateBody(formatted_body); - setupSimpleLayout(); - } - - adjustMessageLayout(); -} - -TimelineItem::TimelineItem(ImageItem *image, - const QString &userid, - bool withSender, - const QString &room_id, - QWidget *parent) - : QWidget{parent} - , message_type_(mtx::events::MessageType::Image) - , room_id_{room_id} -{ - init(); - - setupLocalWidgetLayout<ImageItem>(image, userid, withSender); - - addSaveImageAction(image); -} - -TimelineItem::TimelineItem(FileItem *file, - const QString &userid, - bool withSender, - const QString &room_id, - QWidget *parent) - : QWidget{parent} - , message_type_(mtx::events::MessageType::File) - , room_id_{room_id} -{ - init(); - - setupLocalWidgetLayout<FileItem>(file, userid, withSender); -} - -TimelineItem::TimelineItem(AudioItem *audio, - const QString &userid, - bool withSender, - const QString &room_id, - QWidget *parent) - : QWidget{parent} - , message_type_(mtx::events::MessageType::Audio) - , room_id_{room_id} -{ - init(); - - setupLocalWidgetLayout<AudioItem>(audio, userid, withSender); -} - -TimelineItem::TimelineItem(VideoItem *video, - const QString &userid, - bool withSender, - const QString &room_id, - QWidget *parent) - : QWidget{parent} - , message_type_(mtx::events::MessageType::Video) - , room_id_{room_id} -{ - init(); - - setupLocalWidgetLayout<VideoItem>(video, userid, withSender); -} - -TimelineItem::TimelineItem(ImageItem *image, - const mtx::events::RoomEvent<mtx::events::msg::Image> &event, - bool with_sender, - const QString &room_id, - QWidget *parent) - : QWidget(parent) - , message_type_(mtx::events::MessageType::Image) - , room_id_{room_id} -{ - setupWidgetLayout<mtx::events::RoomEvent<mtx::events::msg::Image>, ImageItem>( - image, event, with_sender); - - markOwnMessagesAsReceived(event.sender); - - addSaveImageAction(image); -} - -TimelineItem::TimelineItem(StickerItem *image, - const mtx::events::Sticker &event, - bool with_sender, - const QString &room_id, - QWidget *parent) - : QWidget(parent) - , room_id_{room_id} -{ - setupWidgetLayout<mtx::events::Sticker, StickerItem>(image, event, with_sender); - - markOwnMessagesAsReceived(event.sender); - - addSaveImageAction(image); -} - -TimelineItem::TimelineItem(FileItem *file, - const mtx::events::RoomEvent<mtx::events::msg::File> &event, - bool with_sender, - const QString &room_id, - QWidget *parent) - : QWidget(parent) - , message_type_(mtx::events::MessageType::File) - , room_id_{room_id} -{ - setupWidgetLayout<mtx::events::RoomEvent<mtx::events::msg::File>, FileItem>( - file, event, with_sender); - - markOwnMessagesAsReceived(event.sender); -} - -TimelineItem::TimelineItem(AudioItem *audio, - const mtx::events::RoomEvent<mtx::events::msg::Audio> &event, - bool with_sender, - const QString &room_id, - QWidget *parent) - : QWidget(parent) - , message_type_(mtx::events::MessageType::Audio) - , room_id_{room_id} -{ - setupWidgetLayout<mtx::events::RoomEvent<mtx::events::msg::Audio>, AudioItem>( - audio, event, with_sender); - - markOwnMessagesAsReceived(event.sender); -} - -TimelineItem::TimelineItem(VideoItem *video, - const mtx::events::RoomEvent<mtx::events::msg::Video> &event, - bool with_sender, - const QString &room_id, - QWidget *parent) - : QWidget(parent) - , message_type_(mtx::events::MessageType::Video) - , room_id_{room_id} -{ - setupWidgetLayout<mtx::events::RoomEvent<mtx::events::msg::Video>, VideoItem>( - video, event, with_sender); - - markOwnMessagesAsReceived(event.sender); -} - -/* - * Used to display remote notice messages. - */ -TimelineItem::TimelineItem(const mtx::events::RoomEvent<mtx::events::msg::Notice> &event, - bool with_sender, - const QString &room_id, - QWidget *parent) - : QWidget(parent) - , message_type_(mtx::events::MessageType::Notice) - , room_id_{room_id} -{ - init(); - addReplyAction(); - - markOwnMessagesAsReceived(event.sender); - - event_id_ = QString::fromStdString(event.event_id); - const auto sender = QString::fromStdString(event.sender); - const auto timestamp = QDateTime::fromMSecsSinceEpoch(event.origin_server_ts); - - auto formatted_body = utils::linkifyMessage(utils::getMessageBody(event).trimmed()); - auto body = QString::fromStdString(event.content.body).trimmed().toHtmlEscaped(); - - descriptionMsg_ = {event_id_, - Cache::displayName(room_id_, sender), - sender, - " sent a notification", - utils::descriptiveTime(timestamp), - timestamp}; - - generateTimestamp(timestamp); - - if (with_sender) { - auto displayName = Cache::displayName(room_id_, sender); - - generateBody(sender, displayName, formatted_body); - setupAvatarLayout(displayName); - - setUserAvatar(sender); - } else { - generateBody(formatted_body); - setupSimpleLayout(); - } - - adjustMessageLayout(); -} - -/* - * Used to display remote emote messages. - */ -TimelineItem::TimelineItem(const mtx::events::RoomEvent<mtx::events::msg::Emote> &event, - bool with_sender, - const QString &room_id, - QWidget *parent) - : QWidget(parent) - , message_type_(mtx::events::MessageType::Emote) - , room_id_{room_id} -{ - init(); - addReplyAction(); - - markOwnMessagesAsReceived(event.sender); - - event_id_ = QString::fromStdString(event.event_id); - const auto sender = QString::fromStdString(event.sender); - - auto formatted_body = utils::linkifyMessage(utils::getMessageBody(event).trimmed()); - auto body = QString::fromStdString(event.content.body).trimmed().toHtmlEscaped(); - - auto timestamp = QDateTime::fromMSecsSinceEpoch(event.origin_server_ts); - auto displayName = Cache::displayName(room_id_, sender); - formatted_body = QString("<em>%1</em>").arg(formatted_body); - - descriptionMsg_ = {event_id_, - "", - sender, - QString("* %1 %2").arg(displayName).arg(body), - utils::descriptiveTime(timestamp), - timestamp}; - - generateTimestamp(timestamp); - - if (with_sender) { - generateBody(sender, displayName, formatted_body); - setupAvatarLayout(displayName); - - setUserAvatar(sender); - } else { - generateBody(formatted_body); - setupSimpleLayout(); - } - - adjustMessageLayout(); -} - -/* - * Used to display remote text messages. - */ -TimelineItem::TimelineItem(const mtx::events::RoomEvent<mtx::events::msg::Text> &event, - bool with_sender, - const QString &room_id, - QWidget *parent) - : QWidget(parent) - , message_type_(mtx::events::MessageType::Text) - , room_id_{room_id} -{ - init(); - addReplyAction(); - - markOwnMessagesAsReceived(event.sender); - - event_id_ = QString::fromStdString(event.event_id); - const auto sender = QString::fromStdString(event.sender); - - auto formatted_body = utils::linkifyMessage(utils::getMessageBody(event).trimmed()); - auto body = QString::fromStdString(event.content.body).trimmed().toHtmlEscaped(); - - auto timestamp = QDateTime::fromMSecsSinceEpoch(event.origin_server_ts); - auto displayName = Cache::displayName(room_id_, sender); - - QSettings settings; - descriptionMsg_ = {event_id_, - sender == settings.value("auth/user_id") ? "You" : displayName, - sender, - QString(": %1").arg(body), - utils::descriptiveTime(timestamp), - timestamp}; - - generateTimestamp(timestamp); - - if (with_sender) { - generateBody(sender, displayName, formatted_body); - setupAvatarLayout(displayName); - - setUserAvatar(sender); - } else { - generateBody(formatted_body); - setupSimpleLayout(); - } - - adjustMessageLayout(); -} - -TimelineItem::~TimelineItem() -{ - colorGenerating_->cancel(); - colorGenerating_->waitForFinished(); -} - -void -TimelineItem::markSent() -{ - statusIndicator_->setState(StatusIndicatorState::Sent); -} - -void -TimelineItem::markOwnMessagesAsReceived(const std::string &sender) -{ - QSettings settings; - if (sender == settings.value("auth/user_id").toString().toStdString()) - statusIndicator_->setState(StatusIndicatorState::Received); -} - -void -TimelineItem::markRead() -{ - if (statusIndicator_->state() != StatusIndicatorState::Encrypted) - statusIndicator_->setState(StatusIndicatorState::Read); -} - -void -TimelineItem::markReceived(bool isEncrypted) -{ - isReceived_ = true; - - if (isEncrypted) - statusIndicator_->setState(StatusIndicatorState::Encrypted); - else - statusIndicator_->setState(StatusIndicatorState::Received); - - sendReadReceipt(); -} - -// Only the body is displayed. -void -TimelineItem::generateBody(const QString &body) -{ - body_ = new TextLabel(utils::replaceEmoji(body), this); - body_->setTextInteractionFlags(Qt::TextSelectableByMouse | Qt::TextBrowserInteraction); - - connect(body_, &TextLabel::userProfileTriggered, this, [](const QString &user_id) { - MainWindow::instance()->openUserProfile(user_id, - ChatPage::instance()->currentRoom()); - }); -} - -void -TimelineItem::refreshAuthorColor() -{ - // Cancel and wait if we are already generating the color. - if (colorGenerating_->isRunning()) { - colorGenerating_->cancel(); - colorGenerating_->waitForFinished(); - } - if (userName_) { - // generate user's unique color. - std::function<QString()> generate = [this]() { - QString userColor = utils::generateContrastingHexColor( - userName_->toolTip(), backgroundColor().name()); - return userColor; - }; - - QString userColor = Cache::userColor(userName_->toolTip()); - - // If the color is empty, then generate it asynchronously - if (userColor.isEmpty()) { - colorGenerating_->setFuture(QtConcurrent::run(generate)); - } else { - userName_->setStyleSheet("QLabel { color : " + userColor + "; }"); - } - } -} - -void -TimelineItem::finishedGeneratingColor() -{ - nhlog::ui()->debug("finishedGeneratingColor for: {}", userName_->toolTip().toStdString()); - QString userColor = colorGenerating_->result(); - - if (!userColor.isEmpty()) { - // another TimelineItem might have inserted in the meantime. - if (Cache::userColor(userName_->toolTip()).isEmpty()) { - Cache::insertUserColor(userName_->toolTip(), userColor); - } - userName_->setStyleSheet("QLabel { color : " + userColor + "; }"); - } -} -// The username/timestamp is displayed along with the message body. -void -TimelineItem::generateBody(const QString &user_id, const QString &displayname, const QString &body) -{ - generateUserName(user_id, displayname); - generateBody(body); -} - -void -TimelineItem::generateUserName(const QString &user_id, const QString &displayname) -{ - auto sender = displayname; - - if (displayname.startsWith("@")) { - // TODO: Fix this by using a UserId type. - if (displayname.split(":")[0].split("@").size() > 1) - sender = displayname.split(":")[0].split("@")[1]; - } - - QFont usernameFont; - usernameFont.setPointSizeF(usernameFont.pointSizeF() * 1.1); - usernameFont.setWeight(QFont::Medium); - - QFontMetrics fm(usernameFont); - - userName_ = new QLabel(this); - userName_->setFont(usernameFont); - userName_->setText(utils::replaceEmoji(fm.elidedText(sender, Qt::ElideRight, 500))); - userName_->setToolTip(user_id); - userName_->setToolTipDuration(1500); - userName_->setAttribute(Qt::WA_Hover); - userName_->setAlignment(Qt::AlignLeft | Qt::AlignTop); -#if QT_VERSION < QT_VERSION_CHECK(5, 11, 0) - // width deprecated in 5.13: - userName_->setFixedWidth(QFontMetrics(userName_->font()).width(userName_->text())); -#else - userName_->setFixedWidth( - QFontMetrics(userName_->font()).horizontalAdvance(userName_->text())); -#endif - // Set the user color asynchronously if it hasn't been generated yet, - // otherwise this will just set it. - refreshAuthorColor(); - - auto filter = new UserProfileFilter(user_id, userName_); - userName_->installEventFilter(filter); - userName_->setCursor(Qt::PointingHandCursor); - - connect(filter, &UserProfileFilter::hoverOn, this, [this]() { - QFont f = userName_->font(); - f.setUnderline(true); - userName_->setFont(f); - }); - - connect(filter, &UserProfileFilter::hoverOff, this, [this]() { - QFont f = userName_->font(); - f.setUnderline(false); - userName_->setFont(f); - }); - - connect(filter, &UserProfileFilter::clicked, this, [this, user_id]() { - MainWindow::instance()->openUserProfile(user_id, room_id_); - }); -} - -void -TimelineItem::generateTimestamp(const QDateTime &time) -{ - timestamp_ = new QLabel(this); - timestamp_->setFont(timestampFont_); - timestamp_->setText( - QString("<span style=\"color: #999\"> %1 </span>").arg(time.toString("HH:mm"))); -} - -void -TimelineItem::setupAvatarLayout(const QString &userName) -{ - topLayout_->setContentsMargins( - conf::timeline::msgLeftMargin, conf::timeline::msgAvatarTopMargin, 0, 0); - - QFont f; - f.setPointSizeF(f.pointSizeF()); - - userAvatar_ = new Avatar(this, QFontMetrics(f).height() * 2); - userAvatar_->setLetter(QChar(userName[0]).toUpper()); - - // TODO: The provided user name should be a UserId class - if (userName[0] == '@' && userName.size() > 1) - userAvatar_->setLetter(QChar(userName[1]).toUpper()); - - topLayout_->insertWidget(0, userAvatar_); - topLayout_->setAlignment(userAvatar_, Qt::AlignTop | Qt::AlignLeft); - - if (userName_) - mainLayout_->insertWidget(0, userName_, Qt::AlignTop | Qt::AlignLeft); -} - -void -TimelineItem::setupSimpleLayout() -{ - QFont f; - f.setPointSizeF(f.pointSizeF()); - - topLayout_->setContentsMargins(conf::timeline::msgLeftMargin + - QFontMetrics(f).height() * 2 + 2, - conf::timeline::msgTopMargin, - 0, - 0); -} - -void -TimelineItem::setUserAvatar(const QString &userid) -{ - if (userAvatar_ == nullptr) - return; - - userAvatar_->setImage(room_id_, userid); -} - -void -TimelineItem::contextMenuEvent(QContextMenuEvent *event) -{ - if (contextMenu_) - contextMenu_->exec(event->globalPos()); -} - -void -TimelineItem::paintEvent(QPaintEvent *) -{ - QStyleOption opt; - opt.init(this); - QPainter p(this); - style()->drawPrimitive(QStyle::PE_Widget, &opt, &p, this); -} - -void -TimelineItem::addSaveImageAction(ImageItem *image) -{ - if (contextMenu_) { - auto saveImage = new QAction("Save image", this); - contextMenu_->addAction(saveImage); - - connect(saveImage, &QAction::triggered, image, &ImageItem::saveAs); - } -} - -void -TimelineItem::addReplyAction() -{ - if (contextMenu_) { - auto replyAction = new QAction("Reply", this); - contextMenu_->addAction(replyAction); - - connect(replyAction, &QAction::triggered, this, &TimelineItem::replyAction); - } -} - -void -TimelineItem::replyAction() -{ - if (!body_) - return; - - RelatedInfo related; - related.type = message_type_; - related.quoted_body = body_->toPlainText(); - related.quoted_user = descriptionMsg_.userid; - related.related_event = eventId().toStdString(); - related.room = room_id_; - - emit ChatPage::instance()->messageReply(related); -} - -void -TimelineItem::addKeyRequestAction() -{ - if (contextMenu_) { - auto requestKeys = new QAction("Request encryption keys", this); - contextMenu_->addAction(requestKeys); - - connect(requestKeys, &QAction::triggered, this, [this]() { - olm::request_keys(room_id_.toStdString(), event_id_.toStdString()); - }); - } -} - -void -TimelineItem::addAvatar() -{ - if (userAvatar_) - return; - - // TODO: should be replaced with the proper event struct. - auto userid = descriptionMsg_.userid; - auto displayName = Cache::displayName(room_id_, userid); - - generateUserName(userid, displayName); - - setupAvatarLayout(displayName); - - setUserAvatar(userid); -} - -void -TimelineItem::sendReadReceipt() const -{ - if (!event_id_.isEmpty()) - http::client()->read_event(room_id_.toStdString(), - event_id_.toStdString(), - [this](mtx::http::RequestErr err) { - if (err) { - nhlog::net()->warn( - "failed to read_event ({}, {})", - room_id_.toStdString(), - event_id_.toStdString()); - } - }); -} - -void -TimelineItem::openRawMessageViewer() const -{ - const auto event_id = event_id_.toStdString(); - const auto room_id = room_id_.toStdString(); - - auto proxy = std::make_shared<EventProxy>(); - connect(proxy.get(), &EventProxy::eventRetrieved, this, [](const nlohmann::json &obj) { - auto dialog = new dialogs::RawMessage{QString::fromStdString(obj.dump(4))}; - Q_UNUSED(dialog); - }); - - http::client()->get_event( - room_id, - event_id, - [event_id, room_id, proxy = std::move(proxy)]( - const mtx::events::collections::TimelineEvents &res, mtx::http::RequestErr err) { - using namespace mtx::events; - - if (err) { - nhlog::net()->warn( - "failed to retrieve event {} from {}", event_id, room_id); - return; - } - - try { - emit proxy->eventRetrieved(utils::serialize_event(res)); - } catch (const nlohmann::json::exception &e) { - nhlog::net()->warn( - "failed to serialize event ({}, {})", room_id, event_id); - } - }); -} diff --git a/src/timeline/TimelineItem.h b/src/timeline/TimelineItem.h deleted file mode 100644 index 356976e5931f140a6ec37d714f221b04af233619..0000000000000000000000000000000000000000 --- a/src/timeline/TimelineItem.h +++ /dev/null @@ -1,389 +0,0 @@ -/* - * nheko Copyright (C) 2017 Konstantinos Sideris <siderisk@auth.gr> - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - */ - -#pragma once - -#include <QApplication> -#include <QDateTime> -#include <QHBoxLayout> -#include <QLabel> -#include <QLayout> -#include <QPainter> -#include <QSettings> -#include <QTimer> - -#include <QtConcurrent> - -#include "mtx/events.hpp" - -#include "AvatarProvider.h" -#include "RoomInfoListItem.h" -#include "Utils.h" - -#include "Cache.h" -#include "MatrixClient.h" - -#include "ui/FlatButton.h" - -class ImageItem; -class StickerItem; -class AudioItem; -class VideoItem; -class FileItem; -class Avatar; -class TextLabel; - -enum class StatusIndicatorState -{ - //! The encrypted message was received by the server. - Encrypted, - //! The plaintext message was received by the server. - Received, - //! At least one of the participants has read the message. - Read, - //! The client sent the message. Not yet received. - Sent, - //! When the message is loaded from cache or backfill. - Empty, -}; - -//! -//! Used to notify the user about the status of a message. -//! -class StatusIndicator : public QWidget -{ - Q_OBJECT - -public: - explicit StatusIndicator(QWidget *parent); - void setState(StatusIndicatorState state); - StatusIndicatorState state() const { return state_; } - -protected: - void paintEvent(QPaintEvent *event) override; - -private: - void paintIcon(QPainter &p, QIcon &icon); - - QIcon lockIcon_; - QIcon clockIcon_; - QIcon checkmarkIcon_; - QIcon doubleCheckmarkIcon_; - - QColor iconColor_ = QColor("#999"); - - StatusIndicatorState state_ = StatusIndicatorState::Empty; - - static constexpr int MaxWidth = 24; -}; - -class EventProxy : public QObject -{ - Q_OBJECT - -signals: - void eventRetrieved(const nlohmann::json &); -}; - -class UserProfileFilter : public QObject -{ - Q_OBJECT - -public: - explicit UserProfileFilter(const QString &user_id, QLabel *parent) - : QObject(parent) - , user_id_{user_id} - {} - -signals: - void hoverOff(); - void hoverOn(); - void clicked(); - -protected: - bool eventFilter(QObject *obj, QEvent *event) - { - if (event->type() == QEvent::MouseButtonRelease) { - emit clicked(); - return true; - } else if (event->type() == QEvent::HoverLeave) { - emit hoverOff(); - return true; - } else if (event->type() == QEvent::HoverEnter) { - emit hoverOn(); - return true; - } - - return QObject::eventFilter(obj, event); - } - -private: - QString user_id_; -}; - -class TimelineItem : public QWidget -{ - Q_OBJECT - Q_PROPERTY(QColor backgroundColor READ backgroundColor WRITE setBackgroundColor) - -public: - TimelineItem(const mtx::events::RoomEvent<mtx::events::msg::Notice> &e, - bool with_sender, - const QString &room_id, - QWidget *parent = 0); - TimelineItem(const mtx::events::RoomEvent<mtx::events::msg::Text> &e, - bool with_sender, - const QString &room_id, - QWidget *parent = 0); - TimelineItem(const mtx::events::RoomEvent<mtx::events::msg::Emote> &e, - bool with_sender, - const QString &room_id, - QWidget *parent = 0); - - // For local messages. - // m.text & m.emote - TimelineItem(mtx::events::MessageType ty, - const QString &userid, - QString body, - bool withSender, - const QString &room_id, - QWidget *parent = 0); - // m.image - TimelineItem(ImageItem *item, - const QString &userid, - bool withSender, - const QString &room_id, - QWidget *parent = 0); - TimelineItem(FileItem *item, - const QString &userid, - bool withSender, - const QString &room_id, - QWidget *parent = 0); - TimelineItem(AudioItem *item, - const QString &userid, - bool withSender, - const QString &room_id, - QWidget *parent = 0); - TimelineItem(VideoItem *item, - const QString &userid, - bool withSender, - const QString &room_id, - QWidget *parent = 0); - - TimelineItem(ImageItem *img, - const mtx::events::RoomEvent<mtx::events::msg::Image> &e, - bool with_sender, - const QString &room_id, - QWidget *parent); - TimelineItem(StickerItem *img, - const mtx::events::Sticker &e, - bool with_sender, - const QString &room_id, - QWidget *parent); - TimelineItem(FileItem *file, - const mtx::events::RoomEvent<mtx::events::msg::File> &e, - bool with_sender, - const QString &room_id, - QWidget *parent); - TimelineItem(AudioItem *audio, - const mtx::events::RoomEvent<mtx::events::msg::Audio> &e, - bool with_sender, - const QString &room_id, - QWidget *parent); - TimelineItem(VideoItem *video, - const mtx::events::RoomEvent<mtx::events::msg::Video> &e, - bool with_sender, - const QString &room_id, - QWidget *parent); - - ~TimelineItem(); - - void setBackgroundColor(const QColor &color) { backgroundColor_ = color; } - QColor backgroundColor() const { return backgroundColor_; } - - void setUserAvatar(const QString &userid); - DescInfo descriptionMessage() const { return descriptionMsg_; } - QString eventId() const { return event_id_; } - void setEventId(const QString &event_id) { event_id_ = event_id; } - void markReceived(bool isEncrypted); - void markRead(); - void markSent(); - bool isReceived() { return isReceived_; }; - void setRoomId(QString room_id) { room_id_ = room_id; } - void sendReadReceipt() const; - void openRawMessageViewer() const; - void replyAction(); - - //! Add a user avatar for this event. - void addAvatar(); - void addKeyRequestAction(); - -signals: - void eventRedacted(const QString &event_id); - void redactionFailed(const QString &msg); - -public slots: - void refreshAuthorColor(); - void finishedGeneratingColor(); - -protected: - void paintEvent(QPaintEvent *event) override; - void contextMenuEvent(QContextMenuEvent *event) override; - -private: - //! If we are the sender of the message the event wil be marked as received by the server. - void markOwnMessagesAsReceived(const std::string &sender); - void init(); - //! Add a context menu option to save the image of the timeline item. - void addSaveImageAction(ImageItem *image); - //! Add the reply action in the context menu for widgets that support it. - void addReplyAction(); - - template<class Widget> - void setupLocalWidgetLayout(Widget *widget, const QString &userid, bool withSender); - - template<class Event, class Widget> - void setupWidgetLayout(Widget *widget, const Event &event, bool withSender); - - void generateBody(const QString &body); - void generateBody(const QString &user_id, const QString &displayname, const QString &body); - void generateTimestamp(const QDateTime &time); - void generateUserName(const QString &userid, const QString &displayname); - - void setupAvatarLayout(const QString &userName); - void setupSimpleLayout(); - - void adjustMessageLayout(); - void adjustMessageLayoutForWidget(); - - //! Whether or not the event associated with the widget - //! has been acknowledged by the server. - bool isReceived_ = false; - - QFutureWatcher<QString> *colorGenerating_; - - QString event_id_; - mtx::events::MessageType message_type_ = mtx::events::MessageType::Unknown; - QString room_id_; - - DescInfo descriptionMsg_; - - QMenu *contextMenu_; - QAction *showReadReceipts_; - QAction *markAsRead_; - QAction *redactMsg_; - QAction *viewRawMessage_; - QAction *replyMsg_; - - QHBoxLayout *topLayout_ = nullptr; - QHBoxLayout *messageLayout_ = nullptr; - QHBoxLayout *actionLayout_ = nullptr; - QVBoxLayout *mainLayout_ = nullptr; - QHBoxLayout *widgetLayout_ = nullptr; - - Avatar *userAvatar_; - - QFont timestampFont_; - - StatusIndicator *statusIndicator_; - - QLabel *timestamp_; - QLabel *userName_; - TextLabel *body_; - - QColor backgroundColor_; - - FlatButton *replyBtn_; - FlatButton *contextBtn_; -}; - -template<class Widget> -void -TimelineItem::setupLocalWidgetLayout(Widget *widget, const QString &userid, bool withSender) -{ - auto displayName = Cache::displayName(room_id_, userid); - auto timestamp = QDateTime::currentDateTime(); - - descriptionMsg_ = {"", // No event_id up until this point. - "You", - userid, - QString(" %1").arg(utils::messageDescription<Widget>()), - utils::descriptiveTime(timestamp), - timestamp}; - - generateTimestamp(timestamp); - - widgetLayout_ = new QHBoxLayout; - widgetLayout_->setContentsMargins(0, 2, 0, 2); - widgetLayout_->addWidget(widget); - widgetLayout_->addStretch(1); - - if (withSender) { - generateBody(userid, displayName, ""); - setupAvatarLayout(displayName); - - setUserAvatar(userid); - } else { - setupSimpleLayout(); - } - - adjustMessageLayoutForWidget(); -} - -template<class Event, class Widget> -void -TimelineItem::setupWidgetLayout(Widget *widget, const Event &event, bool withSender) -{ - init(); - - // if (event.type == mtx::events::EventType::RoomMessage) { - // message_type_ = mtx::events::getMessageType(event.content.msgtype); - //} - // TODO: Fix this. - message_type_ = mtx::events::MessageType::Unknown; - event_id_ = QString::fromStdString(event.event_id); - const auto sender = QString::fromStdString(event.sender); - - auto timestamp = QDateTime::fromMSecsSinceEpoch(event.origin_server_ts); - auto displayName = Cache::displayName(room_id_, sender); - - QSettings settings; - descriptionMsg_ = {event_id_, - sender == settings.value("auth/user_id") ? "You" : displayName, - sender, - QString(" %1").arg(utils::messageDescription<Widget>()), - utils::descriptiveTime(timestamp), - timestamp}; - - generateTimestamp(timestamp); - - widgetLayout_ = new QHBoxLayout(); - widgetLayout_->setContentsMargins(0, 2, 0, 2); - widgetLayout_->addWidget(widget); - widgetLayout_->addStretch(1); - - if (withSender) { - generateBody(sender, displayName, ""); - setupAvatarLayout(displayName); - - setUserAvatar(sender); - } else { - setupSimpleLayout(); - } - - adjustMessageLayoutForWidget(); -} diff --git a/src/timeline/TimelineModel.cpp b/src/timeline/TimelineModel.cpp new file mode 100644 index 0000000000000000000000000000000000000000..e3d87ae6aa455b41c728db6255a7ece2d446f30a --- /dev/null +++ b/src/timeline/TimelineModel.cpp @@ -0,0 +1,1541 @@ +#include "TimelineModel.h" + +#include <algorithm> +#include <type_traits> + +#include <QFileDialog> +#include <QMimeDatabase> +#include <QRegularExpression> +#include <QStandardPaths> + +#include "ChatPage.h" +#include "Logging.h" +#include "MainWindow.h" +#include "MxcImageProvider.h" +#include "Olm.h" +#include "TimelineViewManager.h" +#include "Utils.h" +#include "dialogs/RawMessage.h" + +Q_DECLARE_METATYPE(QModelIndex) + +namespace { +template<class T> +QString +eventId(const mtx::events::RoomEvent<T> &event) +{ + return QString::fromStdString(event.event_id); +} +template<class T> +QString +roomId(const mtx::events::Event<T> &event) +{ + return QString::fromStdString(event.room_id); +} +template<class T> +QString +senderId(const mtx::events::RoomEvent<T> &event) +{ + return QString::fromStdString(event.sender); +} + +template<class T> +QDateTime +eventTimestamp(const mtx::events::RoomEvent<T> &event) +{ + return QDateTime::fromMSecsSinceEpoch(event.origin_server_ts); +} + +template<class T> +std::string +eventMsgType(const mtx::events::Event<T> &) +{ + return ""; +} +template<class T> +auto +eventMsgType(const mtx::events::RoomEvent<T> &e) -> decltype(e.content.msgtype) +{ + return e.content.msgtype; +} + +template<class T> +QString +eventBody(const mtx::events::Event<T> &) +{ + return QString(""); +} +template<class T> +auto +eventBody(const mtx::events::RoomEvent<T> &e) + -> std::enable_if_t<std::is_same<decltype(e.content.body), std::string>::value, QString> +{ + return QString::fromStdString(e.content.body); +} + +template<class T> +QString +eventFormattedBody(const mtx::events::Event<T> &) +{ + return QString(""); +} +template<class T> +auto +eventFormattedBody(const mtx::events::RoomEvent<T> &e) + -> std::enable_if_t<std::is_same<decltype(e.content.formatted_body), std::string>::value, QString> +{ + auto temp = e.content.formatted_body; + if (!temp.empty()) { + return QString::fromStdString(temp); + } else { + return QString::fromStdString(e.content.body).toHtmlEscaped().replace("\n", "<br>"); + } +} + +template<class T> +boost::optional<mtx::crypto::EncryptedFile> +eventEncryptionInfo(const mtx::events::Event<T> &) +{ + return boost::none; +} + +template<class T> +auto +eventEncryptionInfo(const mtx::events::RoomEvent<T> &e) -> std::enable_if_t< + std::is_same<decltype(e.content.file), boost::optional<mtx::crypto::EncryptedFile>>::value, + boost::optional<mtx::crypto::EncryptedFile>> +{ + return e.content.file; +} + +template<class T> +QString +eventUrl(const mtx::events::Event<T> &) +{ + return ""; +} + +QString +eventUrl(const mtx::events::StateEvent<mtx::events::state::Avatar> &e) +{ + return QString::fromStdString(e.content.url); +} + +template<class T> +auto +eventUrl(const mtx::events::RoomEvent<T> &e) + -> std::enable_if_t<std::is_same<decltype(e.content.url), std::string>::value, QString> +{ + if (e.content.file) + return QString::fromStdString(e.content.file->url); + return QString::fromStdString(e.content.url); +} + +template<class T> +QString +eventThumbnailUrl(const mtx::events::Event<T> &) +{ + return ""; +} +template<class T> +auto +eventThumbnailUrl(const mtx::events::RoomEvent<T> &e) + -> std::enable_if_t<std::is_same<decltype(e.content.info.thumbnail_url), std::string>::value, + QString> +{ + return QString::fromStdString(e.content.info.thumbnail_url); +} + +template<class T> +QString +eventFilename(const mtx::events::Event<T> &) +{ + return ""; +} +QString +eventFilename(const mtx::events::RoomEvent<mtx::events::msg::Audio> &e) +{ + // body may be the original filename + return QString::fromStdString(e.content.body); +} +QString +eventFilename(const mtx::events::RoomEvent<mtx::events::msg::Video> &e) +{ + // body may be the original filename + return QString::fromStdString(e.content.body); +} +QString +eventFilename(const mtx::events::RoomEvent<mtx::events::msg::Image> &e) +{ + // body may be the original filename + return QString::fromStdString(e.content.body); +} +QString +eventFilename(const mtx::events::RoomEvent<mtx::events::msg::File> &e) +{ + // body may be the original filename + if (!e.content.filename.empty()) + return QString::fromStdString(e.content.filename); + return QString::fromStdString(e.content.body); +} + +template<class T> +auto +eventFilesize(const mtx::events::RoomEvent<T> &e) -> decltype(e.content.info.size) +{ + return e.content.info.size; +} + +template<class T> +int64_t +eventFilesize(const mtx::events::Event<T> &) +{ + return 0; +} + +template<class T> +QString +eventMimeType(const mtx::events::Event<T> &) +{ + return QString(); +} +template<class T> +auto +eventMimeType(const mtx::events::RoomEvent<T> &e) + -> std::enable_if_t<std::is_same<decltype(e.content.info.mimetype), std::string>::value, QString> +{ + return QString::fromStdString(e.content.info.mimetype); +} + +template<class T> +QString +eventRelatesTo(const mtx::events::Event<T> &) +{ + return QString(); +} +template<class T> +auto +eventRelatesTo(const mtx::events::RoomEvent<T> &e) -> std::enable_if_t< + std::is_same<decltype(e.content.relates_to.in_reply_to.event_id), std::string>::value, + QString> +{ + return QString::fromStdString(e.content.relates_to.in_reply_to.event_id); +} + +template<class T> +qml_mtx_events::EventType +toRoomEventType(const mtx::events::Event<T> &e) +{ + using mtx::events::EventType; + switch (e.type) { + case EventType::RoomKeyRequest: + return qml_mtx_events::EventType::KeyRequest; + case EventType::RoomAliases: + return qml_mtx_events::EventType::Aliases; + case EventType::RoomAvatar: + return qml_mtx_events::EventType::Avatar; + case EventType::RoomCanonicalAlias: + return qml_mtx_events::EventType::CanonicalAlias; + case EventType::RoomCreate: + return qml_mtx_events::EventType::Create; + case EventType::RoomEncrypted: + return qml_mtx_events::EventType::Encrypted; + case EventType::RoomEncryption: + return qml_mtx_events::EventType::Encryption; + case EventType::RoomGuestAccess: + return qml_mtx_events::EventType::GuestAccess; + case EventType::RoomHistoryVisibility: + return qml_mtx_events::EventType::HistoryVisibility; + case EventType::RoomJoinRules: + return qml_mtx_events::EventType::JoinRules; + case EventType::RoomMember: + return qml_mtx_events::EventType::Member; + case EventType::RoomMessage: + return qml_mtx_events::EventType::UnknownMessage; + case EventType::RoomName: + return qml_mtx_events::EventType::Name; + case EventType::RoomPowerLevels: + return qml_mtx_events::EventType::PowerLevels; + case EventType::RoomTopic: + return qml_mtx_events::EventType::Topic; + case EventType::RoomTombstone: + return qml_mtx_events::EventType::Tombstone; + case EventType::RoomRedaction: + return qml_mtx_events::EventType::Redaction; + case EventType::RoomPinnedEvents: + return qml_mtx_events::EventType::PinnedEvents; + case EventType::Sticker: + return qml_mtx_events::EventType::Sticker; + case EventType::Tag: + return qml_mtx_events::EventType::Tag; + case EventType::Unsupported: + default: + return qml_mtx_events::EventType::Unsupported; + } +} +qml_mtx_events::EventType +toRoomEventType(const mtx::events::Event<mtx::events::msg::Audio> &) +{ + return qml_mtx_events::EventType::AudioMessage; +} +qml_mtx_events::EventType +toRoomEventType(const mtx::events::Event<mtx::events::msg::Emote> &) +{ + return qml_mtx_events::EventType::EmoteMessage; +} +qml_mtx_events::EventType +toRoomEventType(const mtx::events::Event<mtx::events::msg::File> &) +{ + return qml_mtx_events::EventType::FileMessage; +} +qml_mtx_events::EventType +toRoomEventType(const mtx::events::Event<mtx::events::msg::Image> &) +{ + return qml_mtx_events::EventType::ImageMessage; +} +qml_mtx_events::EventType +toRoomEventType(const mtx::events::Event<mtx::events::msg::Notice> &) +{ + return qml_mtx_events::EventType::NoticeMessage; +} +qml_mtx_events::EventType +toRoomEventType(const mtx::events::Event<mtx::events::msg::Text> &) +{ + return qml_mtx_events::EventType::TextMessage; +} +qml_mtx_events::EventType +toRoomEventType(const mtx::events::Event<mtx::events::msg::Video> &) +{ + return qml_mtx_events::EventType::VideoMessage; +} + +qml_mtx_events::EventType +toRoomEventType(const mtx::events::Event<mtx::events::msg::Redacted> &) +{ + return qml_mtx_events::EventType::Redacted; +} +// ::EventType::Type toRoomEventType(const Event<mtx::events::msg::Location> &e) { return +// ::EventType::LocationMessage; } + +template<class T> +uint64_t +eventHeight(const mtx::events::Event<T> &) +{ + return -1; +} +template<class T> +auto +eventHeight(const mtx::events::RoomEvent<T> &e) -> decltype(e.content.info.h) +{ + return e.content.info.h; +} +template<class T> +uint64_t +eventWidth(const mtx::events::Event<T> &) +{ + return -1; +} +template<class T> +auto +eventWidth(const mtx::events::RoomEvent<T> &e) -> decltype(e.content.info.w) +{ + return e.content.info.w; +} + +template<class T> +double +eventPropHeight(const mtx::events::RoomEvent<T> &e) +{ + auto w = eventWidth(e); + if (w == 0) + w = 1; + + double prop = eventHeight(e) / (double)w; + + return prop > 0 ? prop : 1.; +} +} + +TimelineModel::TimelineModel(TimelineViewManager *manager, QString room_id, QObject *parent) + : QAbstractListModel(parent) + , room_id_(room_id) + , manager_(manager) +{ + connect( + this, &TimelineModel::oldMessagesRetrieved, this, &TimelineModel::addBackwardsEvents); + connect(this, &TimelineModel::messageFailed, this, [this](QString txn_id) { + pending.removeOne(txn_id); + failed.insert(txn_id); + int idx = idToIndex(txn_id); + if (idx < 0) { + nhlog::ui()->warn("Failed index out of range"); + return; + } + isProcessingPending = false; + emit dataChanged(index(idx, 0), index(idx, 0)); + }); + connect(this, &TimelineModel::messageSent, this, [this](QString txn_id, QString event_id) { + pending.removeOne(txn_id); + int idx = idToIndex(txn_id); + if (idx < 0) { + nhlog::ui()->warn("Sent index out of range"); + return; + } + eventOrder[idx] = event_id; + auto ev = events.value(txn_id); + ev = boost::apply_visitor( + [event_id](const auto &e) -> mtx::events::collections::TimelineEvents { + auto eventCopy = e; + eventCopy.event_id = event_id.toStdString(); + return eventCopy; + }, + ev); + events.remove(txn_id); + events.insert(event_id, ev); + + // mark our messages as read + readEvent(event_id.toStdString()); + + // ask to be notified for read receipts + cache::client()->addPendingReceipt(room_id_, event_id); + + isProcessingPending = false; + emit dataChanged(index(idx, 0), index(idx, 0)); + + if (pending.size() > 0) + emit nextPendingMessage(); + }); + connect(this, &TimelineModel::redactionFailed, this, [](const QString &msg) { + emit ChatPage::instance()->showNotification(msg); + }); + + connect( + this, &TimelineModel::nextPendingMessage, this, &TimelineModel::processOnePendingMessage); + connect(this, &TimelineModel::newMessageToSend, this, &TimelineModel::addPendingMessage); +} + +QHash<int, QByteArray> +TimelineModel::roleNames() const +{ + return { + {Section, "section"}, + {Type, "type"}, + {Body, "body"}, + {FormattedBody, "formattedBody"}, + {UserId, "userId"}, + {UserName, "userName"}, + {Timestamp, "timestamp"}, + {Url, "url"}, + {ThumbnailUrl, "thumbnailUrl"}, + {Filename, "filename"}, + {Filesize, "filesize"}, + {MimeType, "mimetype"}, + {Height, "height"}, + {Width, "width"}, + {ProportionalHeight, "proportionalHeight"}, + {Id, "id"}, + {State, "state"}, + {IsEncrypted, "isEncrypted"}, + {ReplyTo, "replyTo"}, + }; +} +int +TimelineModel::rowCount(const QModelIndex &parent) const +{ + Q_UNUSED(parent); + return (int)this->eventOrder.size(); +} + +QVariant +TimelineModel::data(const QModelIndex &index, int role) const +{ + if (index.row() < 0 && index.row() >= (int)eventOrder.size()) + return QVariant(); + + QString id = eventOrder[index.row()]; + + mtx::events::collections::TimelineEvents event = events.value(id); + + if (auto e = boost::get<mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>>(&event)) { + event = decryptEvent(*e).event; + } + + switch (role) { + case Section: { + QDateTime date = boost::apply_visitor( + [](const auto &e) -> QDateTime { return eventTimestamp(e); }, event); + date.setTime(QTime()); + + QString userId = + boost::apply_visitor([](const auto &e) -> QString { return senderId(e); }, event); + + for (int r = index.row() - 1; r > 0; r--) { + auto tempEv = events.value(eventOrder[r]); + QDateTime prevDate = boost::apply_visitor( + [](const auto &e) -> QDateTime { return eventTimestamp(e); }, tempEv); + prevDate.setTime(QTime()); + if (prevDate != date) + return QString("%2 %1").arg(date.toMSecsSinceEpoch()).arg(userId); + + QString prevUserId = boost::apply_visitor( + [](const auto &e) -> QString { return senderId(e); }, tempEv); + if (userId != prevUserId) + break; + } + + return QString("%1").arg(userId); + } + case UserId: + return QVariant(boost::apply_visitor( + [](const auto &e) -> QString { return senderId(e); }, event)); + case UserName: + return QVariant(displayName(boost::apply_visitor( + [](const auto &e) -> QString { return senderId(e); }, event))); + + case Timestamp: + return QVariant(boost::apply_visitor( + [](const auto &e) -> QDateTime { return eventTimestamp(e); }, event)); + case Type: + return QVariant(boost::apply_visitor( + [](const auto &e) -> qml_mtx_events::EventType { return toRoomEventType(e); }, + event)); + case Body: + return QVariant(utils::replaceEmoji(boost::apply_visitor( + [](const auto &e) -> QString { return eventBody(e); }, event))); + case FormattedBody: + return QVariant( + utils::replaceEmoji( + utils::linkifyMessage(boost::apply_visitor( + [](const auto &e) -> QString { return eventFormattedBody(e); }, event))) + .remove("<mx-reply>") + .remove("</mx-reply>")); + case Url: + return QVariant(boost::apply_visitor( + [](const auto &e) -> QString { return eventUrl(e); }, event)); + case ThumbnailUrl: + return QVariant(boost::apply_visitor( + [](const auto &e) -> QString { return eventThumbnailUrl(e); }, event)); + case Filename: + return QVariant(boost::apply_visitor( + [](const auto &e) -> QString { return eventFilename(e); }, event)); + case Filesize: + return QVariant(boost::apply_visitor( + [](const auto &e) -> QString { + return utils::humanReadableFileSize(eventFilesize(e)); + }, + event)); + case MimeType: + return QVariant(boost::apply_visitor( + [](const auto &e) -> QString { return eventMimeType(e); }, event)); + case Height: + return QVariant(boost::apply_visitor( + [](const auto &e) -> qulonglong { return eventHeight(e); }, event)); + case Width: + return QVariant(boost::apply_visitor( + [](const auto &e) -> qulonglong { return eventWidth(e); }, event)); + case ProportionalHeight: + return QVariant(boost::apply_visitor( + [](const auto &e) -> double { return eventPropHeight(e); }, event)); + case Id: + return id; + case State: + // only show read receipts for messages not from us + if (boost::apply_visitor([](const auto &e) -> QString { return senderId(e); }, + event) + .toStdString() != http::client()->user_id().to_string()) + return qml_mtx_events::Empty; + else if (failed.contains(id)) + return qml_mtx_events::Failed; + else if (pending.contains(id)) + return qml_mtx_events::Sent; + else if (read.contains(id) || + cache::client()->readReceipts(id, room_id_).size() > 1) + return qml_mtx_events::Read; + else + return qml_mtx_events::Received; + case IsEncrypted: { + auto tempEvent = events[id]; + return boost::get<mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>>( + &tempEvent) != nullptr; + } + case ReplyTo: { + QString evId = boost::apply_visitor( + [](const auto &e) -> QString { return eventRelatesTo(e); }, event); + return QVariant(evId); + } + default: + return QVariant(); + } +} + +void +TimelineModel::addEvents(const mtx::responses::Timeline &timeline) +{ + if (isInitialSync) { + prev_batch_token_ = QString::fromStdString(timeline.prev_batch); + isInitialSync = false; + } + + if (timeline.events.empty()) + return; + + std::vector<QString> ids = internalAddEvents(timeline.events); + + if (ids.empty()) + return; + + beginInsertRows(QModelIndex(), + static_cast<int>(this->eventOrder.size()), + static_cast<int>(this->eventOrder.size() + ids.size() - 1)); + this->eventOrder.insert(this->eventOrder.end(), ids.begin(), ids.end()); + endInsertRows(); + + updateLastMessage(); +} + +template<typename T> +auto +isMessage(const mtx::events::RoomEvent<T> &e) + -> std::enable_if_t<std::is_same<decltype(e.content.msgtype), std::string>::value, bool> +{ + return true; +} + +template<typename T> +auto +isMessage(const mtx::events::Event<T> &) +{ + return false; +} + +void +TimelineModel::updateLastMessage() +{ + for (auto it = eventOrder.rbegin(); it != eventOrder.rend(); ++it) { + auto event = events.value(*it); + if (auto e = boost::get<mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>>( + &event)) { + event = decryptEvent(*e).event; + } + + if (!boost::apply_visitor([](const auto &e) -> bool { return isMessage(e); }, + event)) + continue; + + auto description = utils::getMessageDescription( + event, QString::fromStdString(http::client()->user_id().to_string()), room_id_); + emit manager_->updateRoomsLastMessage(room_id_, description); + return; + } +} + +std::vector<QString> +TimelineModel::internalAddEvents( + const std::vector<mtx::events::collections::TimelineEvents> &timeline) +{ + std::vector<QString> ids; + for (const auto &e : timeline) { + QString id = + boost::apply_visitor([](const auto &e) -> QString { return eventId(e); }, e); + + if (this->events.contains(id)) { + this->events.insert(id, e); + int idx = idToIndex(id); + emit dataChanged(index(idx, 0), index(idx, 0)); + continue; + } + + if (auto redaction = + boost::get<mtx::events::RedactionEvent<mtx::events::msg::Redaction>>(&e)) { + QString redacts = QString::fromStdString(redaction->redacts); + auto redacted = std::find(eventOrder.begin(), eventOrder.end(), redacts); + + if (redacted != eventOrder.end()) { + auto redactedEvent = boost::apply_visitor( + [](const auto &ev) + -> mtx::events::RoomEvent<mtx::events::msg::Redacted> { + mtx::events::RoomEvent<mtx::events::msg::Redacted> + replacement = {}; + replacement.event_id = ev.event_id; + replacement.room_id = ev.room_id; + replacement.sender = ev.sender; + replacement.origin_server_ts = ev.origin_server_ts; + replacement.type = ev.type; + return replacement; + }, + e); + events.insert(redacts, redactedEvent); + + int row = (int)std::distance(eventOrder.begin(), redacted); + emit dataChanged(index(row, 0), index(row, 0)); + } + + continue; // don't insert redaction into timeline + } + + if (auto event = + boost::get<mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>>(&e)) { + auto temp = decryptEvent(*event).event; + auto encInfo = boost::apply_visitor( + [](const auto &ev) -> boost::optional<mtx::crypto::EncryptedFile> { + return eventEncryptionInfo(ev); + }, + temp); + + if (encInfo) + emit newEncryptedImage(encInfo.value()); + } + + this->events.insert(id, e); + ids.push_back(id); + } + return ids; +} + +void +TimelineModel::fetchHistory() +{ + if (paginationInProgress) { + nhlog::ui()->warn("Already loading older messages"); + return; + } + + paginationInProgress = true; + mtx::http::MessagesOpts opts; + opts.room_id = room_id_.toStdString(); + opts.from = prev_batch_token_.toStdString(); + + nhlog::ui()->info("Paginationg room {}", opts.room_id); + + http::client()->messages( + opts, [this, opts](const mtx::responses::Messages &res, mtx::http::RequestErr err) { + if (err) { + nhlog::net()->error("failed to call /messages ({}): {} - {}", + opts.room_id, + mtx::errors::to_string(err->matrix_error.errcode), + err->matrix_error.error); + paginationInProgress = false; + return; + } + + emit oldMessagesRetrieved(std::move(res)); + paginationInProgress = false; + }); +} + +void +TimelineModel::setCurrentIndex(int index) +{ + auto oldIndex = idToIndex(currentId); + currentId = indexToId(index); + emit currentIndexChanged(index); + + if (oldIndex < index && !pending.contains(currentId) && + ChatPage::instance()->isActiveWindow()) { + readEvent(currentId.toStdString()); + } +} + +void +TimelineModel::readEvent(const std::string &id) +{ + http::client()->read_event(room_id_.toStdString(), id, [this](mtx::http::RequestErr err) { + if (err) { + nhlog::net()->warn("failed to read_event ({}, {})", + room_id_.toStdString(), + currentId.toStdString()); + } + }); +} + +void +TimelineModel::addBackwardsEvents(const mtx::responses::Messages &msgs) +{ + std::vector<QString> ids = internalAddEvents(msgs.chunk); + + if (!ids.empty()) { + beginInsertRows(QModelIndex(), 0, static_cast<int>(ids.size() - 1)); + this->eventOrder.insert(this->eventOrder.begin(), ids.rbegin(), ids.rend()); + endInsertRows(); + } + + prev_batch_token_ = QString::fromStdString(msgs.end); +} + +QColor +TimelineModel::userColor(QString id, QColor background) +{ + if (!userColors.contains(id)) + userColors.insert( + id, QColor(utils::generateContrastingHexColor(id, background.name()))); + return userColors.value(id); +} + +QString +TimelineModel::displayName(QString id) const +{ + return Cache::displayName(room_id_, id); +} + +QString +TimelineModel::avatarUrl(QString id) const +{ + return Cache::avatarUrl(room_id_, id); +} + +QString +TimelineModel::formatDateSeparator(QDate date) const +{ + auto now = QDateTime::currentDateTime(); + + QString fmt = QLocale::system().dateFormat(QLocale::LongFormat); + + if (now.date().year() == date.year()) { + QRegularExpression rx("[^a-zA-Z]*y+[^a-zA-Z]*"); + fmt = fmt.remove(rx); + } + + return date.toString(fmt); +} + +QString +TimelineModel::escapeEmoji(QString str) const +{ + return utils::replaceEmoji(str); +} + +void +TimelineModel::viewRawMessage(QString id) const +{ + std::string ev = utils::serialize_event(events.value(id)).dump(4); + auto dialog = new dialogs::RawMessage(QString::fromStdString(ev)); + Q_UNUSED(dialog); +} + +void + +TimelineModel::openUserProfile(QString userid) const +{ + MainWindow::instance()->openUserProfile(userid, room_id_); +} + +DecryptionResult +TimelineModel::decryptEvent(const mtx::events::EncryptedEvent<mtx::events::msg::Encrypted> &e) const +{ + MegolmSessionIndex index; + index.room_id = room_id_.toStdString(); + index.session_id = e.content.session_id; + index.sender_key = e.content.sender_key; + + mtx::events::RoomEvent<mtx::events::msg::Notice> dummy; + dummy.origin_server_ts = e.origin_server_ts; + dummy.event_id = e.event_id; + dummy.sender = e.sender; + dummy.content.body = + tr("-- Encrypted Event (No keys found for decryption) --", + "Placeholder, when the message was not decrypted yet or can't be decrypted") + .toStdString(); + + try { + if (!cache::client()->inboundMegolmSessionExists(index)) { + nhlog::crypto()->info("Could not find inbound megolm session ({}, {}, {})", + index.room_id, + index.session_id, + e.sender); + // TODO: request megolm session_id & session_key from the sender. + return {dummy, false}; + } + } catch (const lmdb::error &e) { + nhlog::db()->critical("failed to check megolm session's existence: {}", e.what()); + dummy.content.body = tr("-- Decryption Error (failed to communicate with DB) --", + "Placeholder, when the message can't be decrypted, because " + "the DB access failed when trying to lookup the session.") + .toStdString(); + return {dummy, false}; + } + + std::string msg_str; + try { + auto session = cache::client()->getInboundMegolmSession(index); + auto res = olm::client()->decrypt_group_message(session, e.content.ciphertext); + msg_str = std::string((char *)res.data.data(), res.data.size()); + } catch (const lmdb::error &e) { + nhlog::db()->critical("failed to retrieve megolm session with index ({}, {}, {})", + index.room_id, + index.session_id, + index.sender_key, + e.what()); + dummy.content.body = + tr("-- Decryption Error (failed to retrieve megolm keys from db) --", + "Placeholder, when the message can't be decrypted, because the DB access " + "failed.") + .toStdString(); + return {dummy, false}; + } catch (const mtx::crypto::olm_exception &e) { + nhlog::crypto()->critical("failed to decrypt message with index ({}, {}, {}): {}", + index.room_id, + index.session_id, + index.sender_key, + e.what()); + dummy.content.body = + tr("-- Decryption Error (%1) --", + "Placeholder, when the message can't be decrypted. In this case, the Olm " + "decrytion returned an error, which is passed ad %1") + .arg(e.what()) + .toStdString(); + return {dummy, false}; + } + + // Add missing fields for the event. + json body = json::parse(msg_str); + body["event_id"] = e.event_id; + body["sender"] = e.sender; + body["origin_server_ts"] = e.origin_server_ts; + body["unsigned"] = e.unsigned_data; + + json event_array = json::array(); + event_array.push_back(body); + + std::vector<mtx::events::collections::TimelineEvents> temp_events; + mtx::responses::utils::parse_timeline_events(event_array, temp_events); + + if (temp_events.size() == 1) + return {temp_events.at(0), true}; + + dummy.content.body = + tr("-- Encrypted Event (Unknown event type) --", + "Placeholder, when the message was decrypted, but we couldn't parse it, because " + "Nheko/mtxclient don't support that event type yet") + .toStdString(); + return {dummy, false}; +} + +void +TimelineModel::replyAction(QString id) +{ + auto event = events.value(id); + if (auto e = boost::get<mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>>(&event)) { + event = decryptEvent(*e).event; + } + + RelatedInfo related = boost::apply_visitor( + [](const auto &ev) -> RelatedInfo { + RelatedInfo related_ = {}; + related_.quoted_user = QString::fromStdString(ev.sender); + related_.related_event = ev.event_id; + return related_; + }, + event); + related.type = mtx::events::getMessageType(boost::apply_visitor( + [](const auto &e) -> std::string { return eventMsgType(e); }, event)); + related.quoted_body = boost::apply_visitor( + [](const auto &e) -> QString { return eventFormattedBody(e); }, event); + related.quoted_body.remove(QRegularExpression( + "<mx-reply>.*</mx-reply>", QRegularExpression::DotMatchesEverythingOption)); + nhlog::ui()->debug("after replacement: {}", related.quoted_body.toStdString()); + related.room = room_id_; + + if (related.quoted_body.isEmpty()) + return; + + ChatPage::instance()->messageReply(related); +} + +void +TimelineModel::readReceiptsAction(QString id) const +{ + MainWindow::instance()->openReadReceiptsDialog(id); +} + +void +TimelineModel::redactEvent(QString id) +{ + if (!id.isEmpty()) + http::client()->redact_event( + room_id_.toStdString(), + id.toStdString(), + [this, id](const mtx::responses::EventId &, mtx::http::RequestErr err) { + if (err) { + emit redactionFailed( + tr("Message redaction failed: %1") + .arg(QString::fromStdString(err->matrix_error.error))); + return; + } + + emit eventRedacted(id); + }); +} + +int +TimelineModel::idToIndex(QString id) const +{ + if (id.isEmpty()) + return -1; + for (int i = 0; i < (int)eventOrder.size(); i++) + if (id == eventOrder[i]) + return i; + return -1; +} + +QString +TimelineModel::indexToId(int index) const +{ + if (index < 0 || index >= (int)eventOrder.size()) + return ""; + return eventOrder[index]; +} + +// Note: this will only be called for our messages +void +TimelineModel::markEventsAsRead(const std::vector<QString> &event_ids) +{ + for (const auto &id : event_ids) { + read.insert(id); + int idx = idToIndex(id); + if (idx < 0) { + nhlog::ui()->warn("Read index out of range"); + return; + } + emit dataChanged(index(idx, 0), index(idx, 0)); + } +} + +void +TimelineModel::sendEncryptedMessage(const std::string &txn_id, nlohmann::json content) +{ + const auto room_id = room_id_.toStdString(); + + using namespace mtx::events; + using namespace mtx::identifiers; + + json doc{{"type", "m.room.message"}, {"content", content}, {"room_id", room_id}}; + + try { + // Check if we have already an outbound megolm session then we can use. + if (cache::client()->outboundMegolmSessionExists(room_id)) { + auto data = olm::encrypt_group_message( + room_id, http::client()->device_id(), doc.dump()); + + http::client()->send_room_message<msg::Encrypted, EventType::RoomEncrypted>( + room_id, + txn_id, + data, + [this, txn_id](const mtx::responses::EventId &res, + mtx::http::RequestErr err) { + if (err) { + const int status_code = + static_cast<int>(err->status_code); + nhlog::net()->warn("[{}] failed to send message: {} {}", + txn_id, + err->matrix_error.error, + status_code); + emit messageFailed(QString::fromStdString(txn_id)); + } + emit messageSent( + QString::fromStdString(txn_id), + QString::fromStdString(res.event_id.to_string())); + }); + return; + } + + nhlog::ui()->debug("creating new outbound megolm session"); + + // Create a new outbound megolm session. + auto outbound_session = olm::client()->init_outbound_group_session(); + const auto session_id = mtx::crypto::session_id(outbound_session.get()); + const auto session_key = mtx::crypto::session_key(outbound_session.get()); + + // TODO: needs to be moved in the lib. + auto megolm_payload = json{{"algorithm", "m.megolm.v1.aes-sha2"}, + {"room_id", room_id}, + {"session_id", session_id}, + {"session_key", session_key}}; + + // Saving the new megolm session. + // TODO: Maybe it's too early to save. + OutboundGroupSessionData session_data; + session_data.session_id = session_id; + session_data.session_key = session_key; + session_data.message_index = 0; // TODO Update me + cache::client()->saveOutboundMegolmSession( + room_id, session_data, std::move(outbound_session)); + + const auto members = cache::client()->roomMembers(room_id); + nhlog::ui()->info("retrieved {} members for {}", members.size(), room_id); + + auto keeper = + std::make_shared<StateKeeper>([megolm_payload, room_id, doc, txn_id, this]() { + try { + auto data = olm::encrypt_group_message( + room_id, http::client()->device_id(), doc.dump()); + + http::client() + ->send_room_message<msg::Encrypted, EventType::RoomEncrypted>( + room_id, + txn_id, + data, + [this, txn_id](const mtx::responses::EventId &res, + mtx::http::RequestErr err) { + if (err) { + const int status_code = + static_cast<int>(err->status_code); + nhlog::net()->warn( + "[{}] failed to send message: {} {}", + txn_id, + err->matrix_error.error, + status_code); + emit messageFailed( + QString::fromStdString(txn_id)); + } + emit messageSent( + QString::fromStdString(txn_id), + QString::fromStdString(res.event_id.to_string())); + }); + } catch (const lmdb::error &e) { + nhlog::db()->critical( + "failed to save megolm outbound session: {}", e.what()); + emit messageFailed(QString::fromStdString(txn_id)); + } + }); + + mtx::requests::QueryKeys req; + for (const auto &member : members) + req.device_keys[member] = {}; + + http::client()->query_keys( + req, + [keeper = std::move(keeper), megolm_payload, txn_id, this]( + const mtx::responses::QueryKeys &res, mtx::http::RequestErr err) { + if (err) { + nhlog::net()->warn("failed to query device keys: {} {}", + err->matrix_error.error, + static_cast<int>(err->status_code)); + // TODO: Mark the event as failed. Communicate with the UI. + emit messageFailed(QString::fromStdString(txn_id)); + return; + } + + for (const auto &user : res.device_keys) { + // Mapping from a device_id with valid identity keys to the + // generated room_key event used for sharing the megolm session. + std::map<std::string, std::string> room_key_msgs; + std::map<std::string, DevicePublicKeys> deviceKeys; + + room_key_msgs.clear(); + deviceKeys.clear(); + + for (const auto &dev : user.second) { + const auto user_id = ::UserId(dev.second.user_id); + const auto device_id = DeviceId(dev.second.device_id); + + const auto device_keys = dev.second.keys; + const auto curveKey = "curve25519:" + device_id.get(); + const auto edKey = "ed25519:" + device_id.get(); + + if ((device_keys.find(curveKey) == device_keys.end()) || + (device_keys.find(edKey) == device_keys.end())) { + nhlog::net()->debug( + "ignoring malformed keys for device {}", + device_id.get()); + continue; + } + + DevicePublicKeys pks; + pks.ed25519 = device_keys.at(edKey); + pks.curve25519 = device_keys.at(curveKey); + + try { + if (!mtx::crypto::verify_identity_signature( + json(dev.second), device_id, user_id)) { + nhlog::crypto()->warn( + "failed to verify identity keys: {}", + json(dev.second).dump(2)); + continue; + } + } catch (const json::exception &e) { + nhlog::crypto()->warn( + "failed to parse device key json: {}", + e.what()); + continue; + } catch (const mtx::crypto::olm_exception &e) { + nhlog::crypto()->warn( + "failed to verify device key json: {}", + e.what()); + continue; + } + + auto room_key = olm::client() + ->create_room_key_event( + user_id, pks.ed25519, megolm_payload) + .dump(); + + room_key_msgs.emplace(device_id, room_key); + deviceKeys.emplace(device_id, pks); + } + + std::vector<std::string> valid_devices; + valid_devices.reserve(room_key_msgs.size()); + for (auto const &d : room_key_msgs) { + valid_devices.push_back(d.first); + + nhlog::net()->info("{}", d.first); + nhlog::net()->info(" curve25519 {}", + deviceKeys.at(d.first).curve25519); + nhlog::net()->info(" ed25519 {}", + deviceKeys.at(d.first).ed25519); + } + + nhlog::net()->info( + "sending claim request for user {} with {} devices", + user.first, + valid_devices.size()); + + http::client()->claim_keys( + user.first, + valid_devices, + std::bind(&TimelineModel::handleClaimedKeys, + this, + keeper, + room_key_msgs, + deviceKeys, + user.first, + std::placeholders::_1, + std::placeholders::_2)); + + // TODO: Wait before sending the next batch of requests. + std::this_thread::sleep_for(std::chrono::milliseconds(500)); + } + }); + + // TODO: Let the user know about the errors. + } catch (const lmdb::error &e) { + nhlog::db()->critical( + "failed to open outbound megolm session ({}): {}", room_id, e.what()); + emit messageFailed(QString::fromStdString(txn_id)); + } catch (const mtx::crypto::olm_exception &e) { + nhlog::crypto()->critical( + "failed to open outbound megolm session ({}): {}", room_id, e.what()); + emit messageFailed(QString::fromStdString(txn_id)); + } +} + +void +TimelineModel::handleClaimedKeys(std::shared_ptr<StateKeeper> keeper, + const std::map<std::string, std::string> &room_keys, + const std::map<std::string, DevicePublicKeys> &pks, + const std::string &user_id, + const mtx::responses::ClaimKeys &res, + mtx::http::RequestErr err) +{ + if (err) { + nhlog::net()->warn("claim keys error: {} {} {}", + err->matrix_error.error, + err->parse_error, + static_cast<int>(err->status_code)); + return; + } + + nhlog::net()->debug("claimed keys for {}", user_id); + + if (res.one_time_keys.size() == 0) { + nhlog::net()->debug("no one-time keys found for user_id: {}", user_id); + return; + } + + if (res.one_time_keys.find(user_id) == res.one_time_keys.end()) { + nhlog::net()->debug("no one-time keys found for user_id: {}", user_id); + return; + } + + auto retrieved_devices = res.one_time_keys.at(user_id); + + // Payload with all the to_device message to be sent. + json body; + body["messages"][user_id] = json::object(); + + for (const auto &rd : retrieved_devices) { + const auto device_id = rd.first; + nhlog::net()->debug("{} : \n {}", device_id, rd.second.dump(2)); + + // TODO: Verify signatures + auto otk = rd.second.begin()->at("key"); + + if (pks.find(device_id) == pks.end()) { + nhlog::net()->critical("couldn't find public key for device: {}", + device_id); + continue; + } + + auto id_key = pks.at(device_id).curve25519; + auto s = olm::client()->create_outbound_session(id_key, otk); + + if (room_keys.find(device_id) == room_keys.end()) { + nhlog::net()->critical("couldn't find m.room_key for device: {}", + device_id); + continue; + } + + auto device_msg = olm::client()->create_olm_encrypted_content( + s.get(), room_keys.at(device_id), pks.at(device_id).curve25519); + + try { + cache::client()->saveOlmSession(id_key, std::move(s)); + } catch (const lmdb::error &e) { + nhlog::db()->critical("failed to save outbound olm session: {}", e.what()); + } catch (const mtx::crypto::olm_exception &e) { + nhlog::crypto()->critical("failed to pickle outbound olm session: {}", + e.what()); + } + + body["messages"][user_id][device_id] = device_msg; + } + + nhlog::net()->info("send_to_device: {}", user_id); + + http::client()->send_to_device( + "m.room.encrypted", body, [keeper](mtx::http::RequestErr err) { + if (err) { + nhlog::net()->warn("failed to send " + "send_to_device " + "message: {}", + err->matrix_error.error); + } + + (void)keeper; + }); +} + +struct SendMessageVisitor +{ + SendMessageVisitor(const QString &txn_id, TimelineModel *model) + : txn_id_qstr_(txn_id) + , model_(model) + {} + + template<typename T> + void operator()(const mtx::events::Event<T> &) + {} + + template<typename T, + std::enable_if_t<std::is_same<decltype(T::msgtype), std::string>::value, int> = 0> + void operator()(const mtx::events::RoomEvent<T> &msg) + + { + if (cache::client()->isRoomEncrypted(model_->room_id_.toStdString())) { + model_->sendEncryptedMessage(txn_id_qstr_.toStdString(), + nlohmann::json(msg.content)); + } else { + QString txn_id_qstr = txn_id_qstr_; + TimelineModel *model = model_; + http::client()->send_room_message<T, mtx::events::EventType::RoomMessage>( + model->room_id_.toStdString(), + txn_id_qstr.toStdString(), + msg.content, + [txn_id_qstr, model](const mtx::responses::EventId &res, + mtx::http::RequestErr err) { + if (err) { + const int status_code = + static_cast<int>(err->status_code); + nhlog::net()->warn("[{}] failed to send message: {} {}", + txn_id_qstr.toStdString(), + err->matrix_error.error, + status_code); + emit model->messageFailed(txn_id_qstr); + } + emit model->messageSent( + txn_id_qstr, QString::fromStdString(res.event_id.to_string())); + }); + } + } + + QString txn_id_qstr_; + TimelineModel *model_; +}; + +void +TimelineModel::processOnePendingMessage() +{ + if (isProcessingPending || pending.isEmpty()) + return; + + isProcessingPending = true; + + QString txn_id_qstr = pending.first(); + + auto event = events.value(txn_id_qstr); + boost::apply_visitor(SendMessageVisitor{txn_id_qstr, this}, event); +} + +void +TimelineModel::addPendingMessage(mtx::events::collections::TimelineEvents event) +{ + internalAddEvents({event}); + + QString txn_id_qstr = + boost::apply_visitor([](const auto &e) -> QString { return eventId(e); }, event); + beginInsertRows(QModelIndex(), + static_cast<int>(this->eventOrder.size()), + static_cast<int>(this->eventOrder.size())); + pending.push_back(txn_id_qstr); + this->eventOrder.insert(this->eventOrder.end(), txn_id_qstr); + endInsertRows(); + updateLastMessage(); + + if (!isProcessingPending) + emit nextPendingMessage(); +} + +void +TimelineModel::saveMedia(QString eventId) const +{ + mtx::events::collections::TimelineEvents event = events.value(eventId); + + if (auto e = boost::get<mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>>(&event)) { + event = decryptEvent(*e).event; + } + + QString mxcUrl = + boost::apply_visitor([](const auto &e) -> QString { return eventUrl(e); }, event); + QString originalFilename = + boost::apply_visitor([](const auto &e) -> QString { return eventFilename(e); }, event); + QString mimeType = + boost::apply_visitor([](const auto &e) -> QString { return eventMimeType(e); }, event); + + using EncF = boost::optional<mtx::crypto::EncryptedFile>; + EncF encryptionInfo = + boost::apply_visitor([](const auto &e) -> EncF { return eventEncryptionInfo(e); }, event); + + qml_mtx_events::EventType eventType = boost::apply_visitor( + [](const auto &e) -> qml_mtx_events::EventType { return toRoomEventType(e); }, event); + + QString dialogTitle; + if (eventType == qml_mtx_events::EventType::ImageMessage) { + dialogTitle = tr("Save image"); + } else if (eventType == qml_mtx_events::EventType::VideoMessage) { + dialogTitle = tr("Save video"); + } else if (eventType == qml_mtx_events::EventType::AudioMessage) { + dialogTitle = tr("Save audio"); + } else { + dialogTitle = tr("Save file"); + } + + QString filterString = QMimeDatabase().mimeTypeForName(mimeType).filterString(); + + auto filename = QFileDialog::getSaveFileName( + manager_->getWidget(), dialogTitle, originalFilename, filterString); + + if (filename.isEmpty()) + return; + + const auto url = mxcUrl.toStdString(); + + http::client()->download( + url, + [filename, url, encryptionInfo](const std::string &data, + const std::string &, + const std::string &, + mtx::http::RequestErr err) { + if (err) { + nhlog::net()->warn("failed to retrieve image {}: {} {}", + url, + err->matrix_error.error, + static_cast<int>(err->status_code)); + return; + } + + try { + auto temp = data; + if (encryptionInfo) + temp = mtx::crypto::to_string( + mtx::crypto::decrypt_file(temp, encryptionInfo.value())); + + QFile file(filename); + + if (!file.open(QIODevice::WriteOnly)) + return; + + file.write(QByteArray(temp.data(), (int)temp.size())); + file.close(); + } catch (const std::exception &e) { + nhlog::ui()->warn("Error while saving file to: {}", e.what()); + } + }); +} + +void +TimelineModel::cacheMedia(QString eventId) +{ + mtx::events::collections::TimelineEvents event = events.value(eventId); + + if (auto e = boost::get<mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>>(&event)) { + event = decryptEvent(*e).event; + } + + QString mxcUrl = + boost::apply_visitor([](const auto &e) -> QString { return eventUrl(e); }, event); + QString mimeType = + boost::apply_visitor([](const auto &e) -> QString { return eventMimeType(e); }, event); + + using EncF = boost::optional<mtx::crypto::EncryptedFile>; + EncF encryptionInfo = + boost::apply_visitor([](const auto &e) -> EncF { return eventEncryptionInfo(e); }, event); + + // If the message is a link to a non mxcUrl, don't download it + if (!mxcUrl.startsWith("mxc://")) { + emit mediaCached(mxcUrl, mxcUrl); + return; + } + + QString suffix = QMimeDatabase().mimeTypeForName(mimeType).preferredSuffix(); + + const auto url = mxcUrl.toStdString(); + QFileInfo filename(QString("%1/media_cache/%2.%3") + .arg(QStandardPaths::writableLocation(QStandardPaths::CacheLocation)) + .arg(QString(mxcUrl).remove("mxc://")) + .arg(suffix)); + if (QDir::cleanPath(filename.path()) != filename.path()) { + nhlog::net()->warn("mxcUrl '{}' is not safe, not downloading file", url); + return; + } + + QDir().mkpath(filename.path()); + + if (filename.isReadable()) { + emit mediaCached(mxcUrl, filename.filePath()); + return; + } + + http::client()->download( + url, + [this, mxcUrl, filename, url, encryptionInfo](const std::string &data, + const std::string &, + const std::string &, + mtx::http::RequestErr err) { + if (err) { + nhlog::net()->warn("failed to retrieve image {}: {} {}", + url, + err->matrix_error.error, + static_cast<int>(err->status_code)); + return; + } + + try { + auto temp = data; + if (encryptionInfo) + temp = mtx::crypto::to_string( + mtx::crypto::decrypt_file(temp, encryptionInfo.value())); + + QFile file(filename.filePath()); + + if (!file.open(QIODevice::WriteOnly)) + return; + + file.write(QByteArray(temp.data(), temp.size())); + file.close(); + } catch (const std::exception &e) { + nhlog::ui()->warn("Error while saving file to: {}", e.what()); + } + + emit mediaCached(mxcUrl, filename.filePath()); + }); +} diff --git a/src/timeline/TimelineModel.h b/src/timeline/TimelineModel.h new file mode 100644 index 0000000000000000000000000000000000000000..06c64acf5475aaedd5be7630bae52013b752f9fb --- /dev/null +++ b/src/timeline/TimelineModel.h @@ -0,0 +1,242 @@ +#pragma once + +#include <QAbstractListModel> +#include <QColor> +#include <QDate> +#include <QHash> +#include <QSet> + +#include <mtx/common.hpp> +#include <mtx/responses.hpp> + +#include "Cache.h" +#include "Logging.h" +#include "MatrixClient.h" + +namespace qml_mtx_events { +Q_NAMESPACE + +enum EventType +{ + // Unsupported event + Unsupported, + /// m.room_key_request + KeyRequest, + /// m.room.aliases + Aliases, + /// m.room.avatar + Avatar, + /// m.room.canonical_alias + CanonicalAlias, + /// m.room.create + Create, + /// m.room.encrypted. + Encrypted, + /// m.room.encryption. + Encryption, + /// m.room.guest_access + GuestAccess, + /// m.room.history_visibility + HistoryVisibility, + /// m.room.join_rules + JoinRules, + /// m.room.member + Member, + /// m.room.name + Name, + /// m.room.power_levels + PowerLevels, + /// m.room.tombstone + Tombstone, + /// m.room.topic + Topic, + /// m.room.redaction + Redaction, + /// m.room.pinned_events + PinnedEvents, + // m.sticker + Sticker, + // m.tag + Tag, + /// m.room.message + AudioMessage, + EmoteMessage, + FileMessage, + ImageMessage, + LocationMessage, + NoticeMessage, + TextMessage, + VideoMessage, + Redacted, + UnknownMessage, +}; +Q_ENUM_NS(EventType) + +enum EventState +{ + //! The plaintext message was received by the server. + Received, + //! At least one of the participants has read the message. + Read, + //! The client sent the message. Not yet received. + Sent, + //! When the message is loaded from cache or backfill. + Empty, + //! When the message failed to send + Failed, +}; +Q_ENUM_NS(EventState) +} + +class StateKeeper +{ +public: + StateKeeper(std::function<void()> &&fn) + : fn_(std::move(fn)) + {} + + ~StateKeeper() { fn_(); } + +private: + std::function<void()> fn_; +}; + +struct DecryptionResult +{ + //! The decrypted content as a normal plaintext event. + mtx::events::collections::TimelineEvents event; + //! Whether or not the decryption was successful. + bool isDecrypted = false; +}; + +class TimelineViewManager; + +class TimelineModel : public QAbstractListModel +{ + Q_OBJECT + Q_PROPERTY( + int currentIndex READ currentIndex WRITE setCurrentIndex NOTIFY currentIndexChanged) + +public: + explicit TimelineModel(TimelineViewManager *manager, QString room_id, QObject *parent = 0); + + enum Roles + { + Section, + Type, + Body, + FormattedBody, + UserId, + UserName, + Timestamp, + Url, + ThumbnailUrl, + Filename, + Filesize, + MimeType, + Height, + Width, + ProportionalHeight, + Id, + State, + IsEncrypted, + ReplyTo, + }; + + QHash<int, QByteArray> roleNames() const override; + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + + Q_INVOKABLE QColor userColor(QString id, QColor background); + Q_INVOKABLE QString displayName(QString id) const; + Q_INVOKABLE QString avatarUrl(QString id) const; + Q_INVOKABLE QString formatDateSeparator(QDate date) const; + + Q_INVOKABLE QString escapeEmoji(QString str) const; + Q_INVOKABLE void viewRawMessage(QString id) const; + Q_INVOKABLE void openUserProfile(QString userid) const; + Q_INVOKABLE void replyAction(QString id); + Q_INVOKABLE void readReceiptsAction(QString id) const; + Q_INVOKABLE void redactEvent(QString id); + Q_INVOKABLE int idToIndex(QString id) const; + Q_INVOKABLE QString indexToId(int index) const; + Q_INVOKABLE void cacheMedia(QString eventId); + Q_INVOKABLE void saveMedia(QString eventId) const; + + void addEvents(const mtx::responses::Timeline &events); + template<class T> + void sendMessage(const T &msg); + +public slots: + void fetchHistory(); + void setCurrentIndex(int index); + int currentIndex() const { return idToIndex(currentId); } + void markEventsAsRead(const std::vector<QString> &event_ids); + +private slots: + // Add old events at the top of the timeline. + void addBackwardsEvents(const mtx::responses::Messages &msgs); + void processOnePendingMessage(); + void addPendingMessage(mtx::events::collections::TimelineEvents event); + +signals: + void oldMessagesRetrieved(const mtx::responses::Messages &res); + void messageFailed(QString txn_id); + void messageSent(QString txn_id, QString event_id); + void currentIndexChanged(int index); + void redactionFailed(QString id); + void eventRedacted(QString id); + void nextPendingMessage(); + void newMessageToSend(mtx::events::collections::TimelineEvents event); + void mediaCached(QString mxcUrl, QString cacheUrl); + void newEncryptedImage(mtx::crypto::EncryptedFile encryptionInfo); + +private: + DecryptionResult decryptEvent( + const mtx::events::EncryptedEvent<mtx::events::msg::Encrypted> &e) const; + std::vector<QString> internalAddEvents( + const std::vector<mtx::events::collections::TimelineEvents> &timeline); + void sendEncryptedMessage(const std::string &txn_id, nlohmann::json content); + void handleClaimedKeys(std::shared_ptr<StateKeeper> keeper, + const std::map<std::string, std::string> &room_key, + const std::map<std::string, DevicePublicKeys> &pks, + const std::string &user_id, + const mtx::responses::ClaimKeys &res, + mtx::http::RequestErr err); + void updateLastMessage(); + void readEvent(const std::string &id); + + QHash<QString, mtx::events::collections::TimelineEvents> events; + QSet<QString> failed, read; + QList<QString> pending; + std::vector<QString> eventOrder; + + QString room_id_; + QString prev_batch_token_; + + bool isInitialSync = true; + bool paginationInProgress = false; + bool isProcessingPending = false; + + QHash<QString, QColor> userColors; + QString currentId; + + TimelineViewManager *manager_; + + friend struct SendMessageVisitor; +}; + +template<class T> +void +TimelineModel::sendMessage(const T &msg) +{ + auto txn_id = http::client()->generate_txn_id(); + mtx::events::RoomEvent<T> msgCopy = {}; + msgCopy.content = msg; + msgCopy.type = mtx::events::EventType::RoomMessage; + msgCopy.event_id = txn_id; + msgCopy.sender = http::client()->user_id().to_string(); + msgCopy.origin_server_ts = QDateTime::currentMSecsSinceEpoch(); + + emit newMessageToSend(msgCopy); +} diff --git a/src/timeline/TimelineView.cpp b/src/timeline/TimelineView.cpp deleted file mode 100644 index ed783e9013e392bb6d75b1ea252d78d4d1daa3e2..0000000000000000000000000000000000000000 --- a/src/timeline/TimelineView.cpp +++ /dev/null @@ -1,1627 +0,0 @@ -/* - * nheko Copyright (C) 2017 Konstantinos Sideris <siderisk@auth.gr> - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - */ - -#include <boost/variant.hpp> - -#include <QApplication> -#include <QFileInfo> -#include <QTimer> -#include <QtConcurrent> - -#include "Cache.h" -#include "ChatPage.h" -#include "Config.h" -#include "Logging.h" -#include "Olm.h" -#include "UserSettingsPage.h" -#include "Utils.h" -#include "ui/FloatingButton.h" -#include "ui/InfoMessage.h" - -#include "timeline/TimelineView.h" -#include "timeline/widgets/AudioItem.h" -#include "timeline/widgets/FileItem.h" -#include "timeline/widgets/ImageItem.h" -#include "timeline/widgets/VideoItem.h" - -using TimelineEvent = mtx::events::collections::TimelineEvents; - -//! Maximum number of widgets to keep in the timeline layout. -constexpr int MAX_RETAINED_WIDGETS = 100; -constexpr int MIN_SCROLLBAR_HANDLE = 60; - -//! Retrieve the timestamp of the event represented by the given widget. -QDateTime -getDate(QWidget *widget) -{ - auto item = qobject_cast<TimelineItem *>(widget); - if (item) - return item->descriptionMessage().datetime; - - auto infoMsg = qobject_cast<InfoMessage *>(widget); - if (infoMsg) - return infoMsg->datetime(); - - return QDateTime(); -} - -TimelineView::TimelineView(const mtx::responses::Timeline &timeline, - const QString &room_id, - QWidget *parent) - : QWidget(parent) - , room_id_{room_id} -{ - init(); - addEvents(timeline); -} - -TimelineView::TimelineView(const QString &room_id, QWidget *parent) - : QWidget(parent) - , room_id_{room_id} -{ - init(); - getMessages(); -} - -void -TimelineView::sliderRangeChanged(int min, int max) -{ - Q_UNUSED(min); - - if (!scroll_area_->verticalScrollBar()->isVisible()) { - scroll_area_->verticalScrollBar()->setValue(max); - return; - } - - // If the scrollbar is close to the bottom and a new message - // is added we move the scrollbar. - if (max - scroll_area_->verticalScrollBar()->value() < SCROLL_BAR_GAP) { - scroll_area_->verticalScrollBar()->setValue(max); - return; - } - - int currentHeight = scroll_widget_->size().height(); - int diff = currentHeight - oldHeight_; - int newPosition = oldPosition_ + diff; - - // Keep the scroll bar to the bottom if it hasn't been activated yet. - if (oldPosition_ == 0 && !scroll_area_->verticalScrollBar()->isVisible()) - newPosition = max; - - if (lastMessageDirection_ == TimelineDirection::Top) - scroll_area_->verticalScrollBar()->setValue(newPosition); -} - -void -TimelineView::fetchHistory() -{ - if (!isScrollbarActivated() && !isTimelineFinished) { - if (!isVisible()) - return; - - isPaginationInProgress_ = true; - getMessages(); - paginationTimer_->start(2000); - - return; - } - - paginationTimer_->stop(); -} - -void -TimelineView::scrollDown() -{ - int current = scroll_area_->verticalScrollBar()->value(); - int max = scroll_area_->verticalScrollBar()->maximum(); - - // The first time we enter the room move the scroll bar to the bottom. - if (!isInitialized) { - scroll_area_->verticalScrollBar()->setValue(max); - isInitialized = true; - return; - } - - // If the gap is small enough move the scroll bar down. e.g when a new - // message appears. - if (max - current < SCROLL_BAR_GAP) - scroll_area_->verticalScrollBar()->setValue(max); -} - -void -TimelineView::sliderMoved(int position) -{ - if (!scroll_area_->verticalScrollBar()->isVisible()) - return; - - toggleScrollDownButton(); - - // The scrollbar is high enough so we can start retrieving old events. - if (position < SCROLL_BAR_GAP) { - if (isTimelineFinished) - return; - - // Prevent user from moving up when there is pagination in - // progress. - if (isPaginationInProgress_) - return; - - isPaginationInProgress_ = true; - - getMessages(); - } -} - -bool -TimelineView::isStartOfTimeline(const mtx::responses::Messages &msgs) -{ - return (msgs.chunk.size() == 0 && (msgs.end.empty() || msgs.end == msgs.start)); -} - -void -TimelineView::addBackwardsEvents(const mtx::responses::Messages &msgs) -{ - // We've reached the start of the timline and there're no more messages. - if (isStartOfTimeline(msgs)) { - nhlog::ui()->info("[{}] start of timeline reached, no more messages to fetch", - room_id_.toStdString()); - isTimelineFinished = true; - return; - } - - isTimelineFinished = false; - - // Queue incoming messages to be rendered later. - topMessages_.insert(topMessages_.end(), - std::make_move_iterator(msgs.chunk.begin()), - std::make_move_iterator(msgs.chunk.end())); - - // The RoomList message preview will be updated only if this - // is the first batch of messages received through /messages - // i.e there are no other messages currently present. - if (!topMessages_.empty() && scroll_layout_->count() == 0) - notifyForLastEvent(findFirstViewableEvent(topMessages_)); - - if (isVisible()) { - renderTopEvents(topMessages_); - - // Free up space for new messages. - topMessages_.clear(); - - // Send a read receipt for the last event. - if (isActiveWindow()) - readLastEvent(); - } - - prev_batch_token_ = QString::fromStdString(msgs.end); - isPaginationInProgress_ = false; -} - -QWidget * -TimelineView::parseMessageEvent(const mtx::events::collections::TimelineEvents &event, - TimelineDirection direction) -{ - using namespace mtx::events; - - using AudioEvent = RoomEvent<msg::Audio>; - using EmoteEvent = RoomEvent<msg::Emote>; - using FileEvent = RoomEvent<msg::File>; - using ImageEvent = RoomEvent<msg::Image>; - using NoticeEvent = RoomEvent<msg::Notice>; - using TextEvent = RoomEvent<msg::Text>; - using VideoEvent = RoomEvent<msg::Video>; - - if (boost::get<RedactionEvent<msg::Redaction>>(&event) != nullptr) { - auto redaction_event = boost::get<RedactionEvent<msg::Redaction>>(event); - const auto event_id = QString::fromStdString(redaction_event.redacts); - - QTimer::singleShot(0, this, [event_id, this]() { - if (eventIds_.contains(event_id)) - removeEvent(event_id); - }); - - return nullptr; - } else if (boost::get<StateEvent<state::Encryption>>(&event) != nullptr) { - auto msg = boost::get<StateEvent<state::Encryption>>(event); - auto event_id = QString::fromStdString(msg.event_id); - - if (eventIds_.contains(event_id)) - return nullptr; - - auto item = new InfoMessage(tr("Encryption is enabled"), this); - item->saveDatetime(QDateTime::fromMSecsSinceEpoch(msg.origin_server_ts)); - eventIds_[event_id] = item; - - // Force the next message to have avatar by not providing the current username. - saveMessageInfo("", msg.origin_server_ts, direction); - - return item; - } else if (boost::get<RoomEvent<msg::Audio>>(&event) != nullptr) { - auto audio = boost::get<RoomEvent<msg::Audio>>(event); - return processMessageEvent<AudioEvent, AudioItem>(audio, direction); - } else if (boost::get<RoomEvent<msg::Emote>>(&event) != nullptr) { - auto emote = boost::get<RoomEvent<msg::Emote>>(event); - return processMessageEvent<EmoteEvent>(emote, direction); - } else if (boost::get<RoomEvent<msg::File>>(&event) != nullptr) { - auto file = boost::get<RoomEvent<msg::File>>(event); - return processMessageEvent<FileEvent, FileItem>(file, direction); - } else if (boost::get<RoomEvent<msg::Image>>(&event) != nullptr) { - auto image = boost::get<RoomEvent<msg::Image>>(event); - return processMessageEvent<ImageEvent, ImageItem>(image, direction); - } else if (boost::get<RoomEvent<msg::Notice>>(&event) != nullptr) { - auto notice = boost::get<RoomEvent<msg::Notice>>(event); - return processMessageEvent<NoticeEvent>(notice, direction); - } else if (boost::get<RoomEvent<msg::Text>>(&event) != nullptr) { - auto text = boost::get<RoomEvent<msg::Text>>(event); - return processMessageEvent<TextEvent>(text, direction); - } else if (boost::get<RoomEvent<msg::Video>>(&event) != nullptr) { - auto video = boost::get<RoomEvent<msg::Video>>(event); - return processMessageEvent<VideoEvent, VideoItem>(video, direction); - } else if (boost::get<Sticker>(&event) != nullptr) { - return processMessageEvent<Sticker, StickerItem>(boost::get<Sticker>(event), - direction); - } else if (boost::get<EncryptedEvent<msg::Encrypted>>(&event) != nullptr) { - auto res = parseEncryptedEvent(boost::get<EncryptedEvent<msg::Encrypted>>(event)); - auto widget = parseMessageEvent(res.event, direction); - - if (widget == nullptr) - return nullptr; - - auto item = qobject_cast<TimelineItem *>(widget); - - if (item && res.isDecrypted) - item->markReceived(true); - else if (item && !res.isDecrypted) - item->addKeyRequestAction(); - - return widget; - } - - return nullptr; -} - -DecryptionResult -TimelineView::parseEncryptedEvent(const mtx::events::EncryptedEvent<mtx::events::msg::Encrypted> &e) -{ - MegolmSessionIndex index; - index.room_id = room_id_.toStdString(); - index.session_id = e.content.session_id; - index.sender_key = e.content.sender_key; - - mtx::events::RoomEvent<mtx::events::msg::Notice> dummy; - dummy.origin_server_ts = e.origin_server_ts; - dummy.event_id = e.event_id; - dummy.sender = e.sender; - dummy.content.body = - tr("-- Encrypted Event (No keys found for decryption) --", - "Placeholder, when the message was not decrypted yet or can't be decrypted") - .toStdString(); - - try { - if (!cache::client()->inboundMegolmSessionExists(index)) { - nhlog::crypto()->info("Could not find inbound megolm session ({}, {}, {})", - index.room_id, - index.session_id, - e.sender); - // TODO: request megolm session_id & session_key from the sender. - return {dummy, false}; - } - } catch (const lmdb::error &e) { - nhlog::db()->critical("failed to check megolm session's existence: {}", e.what()); - dummy.content.body = tr("-- Decryption Error (failed to communicate with DB) --", - "Placeholder, when the message can't be decrypted, because " - "the DB access failed when trying to lookup the session.") - .toStdString(); - return {dummy, false}; - } - - std::string msg_str; - try { - auto session = cache::client()->getInboundMegolmSession(index); - auto res = olm::client()->decrypt_group_message(session, e.content.ciphertext); - msg_str = std::string((char *)res.data.data(), res.data.size()); - } catch (const lmdb::error &e) { - nhlog::db()->critical("failed to retrieve megolm session with index ({}, {}, {})", - index.room_id, - index.session_id, - index.sender_key, - e.what()); - dummy.content.body = - tr("-- Decryption Error (failed to retrieve megolm keys from db) --", - "Placeholder, when the message can't be decrypted, because the DB access " - "failed.") - .toStdString(); - return {dummy, false}; - } catch (const mtx::crypto::olm_exception &e) { - nhlog::crypto()->critical("failed to decrypt message with index ({}, {}, {}): {}", - index.room_id, - index.session_id, - index.sender_key, - e.what()); - dummy.content.body = - tr("-- Decryption Error (%1) --", - "Placeholder, when the message can't be decrypted. In this case, the Olm " - "decrytion returned an error, which is passed ad %1") - .arg(e.what()) - .toStdString(); - return {dummy, false}; - } - - // Add missing fields for the event. - json body = json::parse(msg_str); - body["event_id"] = e.event_id; - body["sender"] = e.sender; - body["origin_server_ts"] = e.origin_server_ts; - body["unsigned"] = e.unsigned_data; - - nhlog::crypto()->debug("decrypted event: {}", e.event_id); - - json event_array = json::array(); - event_array.push_back(body); - - std::vector<TimelineEvent> events; - mtx::responses::utils::parse_timeline_events(event_array, events); - - if (events.size() == 1) - return {events.at(0), true}; - - dummy.content.body = - tr("-- Encrypted Event (Unknown event type) --", - "Placeholder, when the message was decrypted, but we couldn't parse it, because " - "Nheko/mtxclient don't support that event type yet") - .toStdString(); - return {dummy, false}; -} - -void -TimelineView::displayReadReceipts(std::vector<TimelineEvent> events) -{ - QtConcurrent::run( - [events = std::move(events), room_id = room_id_, local_user = local_user_, this]() { - std::vector<QString> event_ids; - - for (const auto &e : events) { - if (utils::event_sender(e) == local_user) - event_ids.emplace_back( - QString::fromStdString(utils::event_id(e))); - } - - auto readEvents = - cache::client()->filterReadEvents(room_id, event_ids, local_user.toStdString()); - - if (!readEvents.empty()) - emit markReadEvents(readEvents); - }); -} - -void -TimelineView::renderBottomEvents(const std::vector<TimelineEvent> &events) -{ - int counter = 0; - - for (const auto &event : events) { - QWidget *item = parseMessageEvent(event, TimelineDirection::Bottom); - - if (item != nullptr) { - addTimelineItem(item, TimelineDirection::Bottom); - counter++; - - // Prevent blocking of the event-loop - // by calling processEvents every 10 items we render. - if (counter % 4 == 0) - QApplication::processEvents(); - } - } - - lastMessageDirection_ = TimelineDirection::Bottom; - - displayReadReceipts(events); - - QApplication::processEvents(); -} - -void -TimelineView::renderTopEvents(const std::vector<TimelineEvent> &events) -{ - std::vector<QWidget *> items; - - // Reset the sender of the first message in the timeline - // cause we're about to insert a new one. - firstSender_.clear(); - firstMsgTimestamp_ = QDateTime(); - - // Parse in reverse order to determine where we should not show sender's name. - for (auto it = events.rbegin(); it != events.rend(); ++it) { - auto item = parseMessageEvent(*it, TimelineDirection::Top); - - if (item != nullptr) - items.push_back(item); - } - - // Reverse again to render them. - std::reverse(items.begin(), items.end()); - - oldPosition_ = scroll_area_->verticalScrollBar()->value(); - oldHeight_ = scroll_widget_->size().height(); - - for (const auto &item : items) - addTimelineItem(item, TimelineDirection::Top); - - lastMessageDirection_ = TimelineDirection::Top; - - QApplication::processEvents(); - - displayReadReceipts(events); - - // If this batch is the first being rendered (i.e the first and the last - // events originate from this batch), set the last sender. - if (lastSender_.isEmpty() && !items.empty()) { - for (const auto &w : items) { - auto timelineItem = qobject_cast<TimelineItem *>(w); - if (timelineItem) { - saveLastMessageInfo(timelineItem->descriptionMessage().userid, - timelineItem->descriptionMessage().datetime); - break; - } - } - } -} - -void -TimelineView::addEvents(const mtx::responses::Timeline &timeline) -{ - if (isInitialSync) { - prev_batch_token_ = QString::fromStdString(timeline.prev_batch); - isInitialSync = false; - } - - bottomMessages_.insert(bottomMessages_.end(), - std::make_move_iterator(timeline.events.begin()), - std::make_move_iterator(timeline.events.end())); - - if (!bottomMessages_.empty()) - notifyForLastEvent(findLastViewableEvent(bottomMessages_)); - - // If the current timeline is open and there are messages to be rendered. - if (isVisible() && !bottomMessages_.empty()) { - renderBottomEvents(bottomMessages_); - - // Free up space for new messages. - bottomMessages_.clear(); - - // Send a read receipt for the last event. - if (isActiveWindow()) - readLastEvent(); - } -} - -void -TimelineView::init() -{ - local_user_ = utils::localUser(); - - QIcon icon; - icon.addFile(":/icons/icons/ui/angle-arrow-down.png"); - scrollDownBtn_ = new FloatingButton(icon, this); - scrollDownBtn_->hide(); - - connect(scrollDownBtn_, &QPushButton::clicked, this, [this]() { - const int max = scroll_area_->verticalScrollBar()->maximum(); - scroll_area_->verticalScrollBar()->setValue(max); - }); - top_layout_ = new QVBoxLayout(this); - top_layout_->setSpacing(0); - top_layout_->setMargin(0); - - scroll_area_ = new QScrollArea(this); - scroll_area_->setWidgetResizable(true); - scroll_area_->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); - - scroll_widget_ = new QWidget(this); - scroll_widget_->setObjectName("scroll_widget"); - - // Height of the typing display. - QFont f; - f.setPointSizeF(f.pointSizeF() * 0.9); - const int bottomMargin = QFontMetrics(f).height() + 6; - - scroll_layout_ = new QVBoxLayout(scroll_widget_); - scroll_layout_->setContentsMargins(4, 0, 15, bottomMargin); - scroll_layout_->setSpacing(0); - scroll_layout_->setObjectName("timelinescrollarea"); - - scroll_area_->setWidget(scroll_widget_); - scroll_area_->setAlignment(Qt::AlignBottom); - - top_layout_->addWidget(scroll_area_); - - setLayout(top_layout_); - - paginationTimer_ = new QTimer(this); - connect(paginationTimer_, &QTimer::timeout, this, &TimelineView::fetchHistory); - - connect(this, &TimelineView::messagesRetrieved, this, &TimelineView::addBackwardsEvents); - - connect(this, &TimelineView::messageFailed, this, &TimelineView::handleFailedMessage); - connect(this, &TimelineView::messageSent, this, &TimelineView::updatePendingMessage); - - connect( - this, &TimelineView::markReadEvents, this, [this](const std::vector<QString> &event_ids) { - for (const auto &event : event_ids) { - if (eventIds_.contains(event)) { - auto widget = eventIds_[event]; - if (!widget) - return; - - auto item = qobject_cast<TimelineItem *>(widget); - if (!item) - return; - - item->markRead(); - } - } - }); - - connect(scroll_area_->verticalScrollBar(), - SIGNAL(valueChanged(int)), - this, - SLOT(sliderMoved(int))); - connect(scroll_area_->verticalScrollBar(), - SIGNAL(rangeChanged(int, int)), - this, - SLOT(sliderRangeChanged(int, int))); -} - -void -TimelineView::getMessages() -{ - mtx::http::MessagesOpts opts; - opts.room_id = room_id_.toStdString(); - opts.from = prev_batch_token_.toStdString(); - - http::client()->messages( - opts, [this, opts](const mtx::responses::Messages &res, mtx::http::RequestErr err) { - if (err) { - nhlog::net()->error("failed to call /messages ({}): {} - {}", - opts.room_id, - mtx::errors::to_string(err->matrix_error.errcode), - err->matrix_error.error); - return; - } - - emit messagesRetrieved(std::move(res)); - }); -} - -void -TimelineView::updateLastSender(const QString &user_id, TimelineDirection direction) -{ - if (direction == TimelineDirection::Bottom) - lastSender_ = user_id; - else - firstSender_ = user_id; -} - -bool -TimelineView::isSenderRendered(const QString &user_id, - uint64_t origin_server_ts, - TimelineDirection direction) -{ - if (direction == TimelineDirection::Bottom) { - return (lastSender_ != user_id) || - isDateDifference(lastMsgTimestamp_, - QDateTime::fromMSecsSinceEpoch(origin_server_ts)); - } else { - return (firstSender_ != user_id) || - isDateDifference(firstMsgTimestamp_, - QDateTime::fromMSecsSinceEpoch(origin_server_ts)); - } -} - -void -TimelineView::addTimelineItem(QWidget *item, TimelineDirection direction) -{ - const auto newDate = getDate(item); - - if (direction == TimelineDirection::Bottom) { - QWidget *lastItem = nullptr; - int lastItemPosition = 0; - - if (scroll_layout_->count() > 0) { - lastItemPosition = scroll_layout_->count() - 1; - lastItem = scroll_layout_->itemAt(lastItemPosition)->widget(); - } - - if (lastItem) { - const auto oldDate = getDate(lastItem); - - if (oldDate.daysTo(newDate) != 0) { - auto separator = new DateSeparator(newDate, this); - - if (separator) - pushTimelineItem(separator, direction); - } - } - - pushTimelineItem(item, direction); - } else { - if (scroll_layout_->count() > 0) { - const auto firstItem = scroll_layout_->itemAt(0)->widget(); - - if (firstItem) { - const auto oldDate = getDate(firstItem); - - if (newDate.daysTo(oldDate) != 0) { - auto separator = new DateSeparator(oldDate); - - if (separator) - pushTimelineItem(separator, direction); - } - } - } - - pushTimelineItem(item, direction); - } -} - -void -TimelineView::updatePendingMessage(const std::string &txn_id, const QString &event_id) -{ - nhlog::ui()->debug("[{}] message was received by the server", txn_id); - if (!pending_msgs_.isEmpty() && - pending_msgs_.head().txn_id == txn_id) { // We haven't received it yet - auto msg = pending_msgs_.dequeue(); - msg.event_id = event_id; - - if (msg.widget) { - msg.widget->setEventId(event_id); - eventIds_[event_id] = msg.widget; - - // If the response comes after we have received the event from sync - // we've already marked the widget as received. - if (!msg.widget->isReceived()) { - msg.widget->markReceived(msg.is_encrypted); - cache::client()->addPendingReceipt(room_id_, event_id); - pending_sent_msgs_.append(msg); - } - } else { - nhlog::ui()->warn("[{}] received message response for invalid widget", - txn_id); - } - } - - sendNextPendingMessage(); -} - -void -TimelineView::addUserMessage(mtx::events::MessageType ty, - const QString &body, - const RelatedInfo &related = RelatedInfo()) -{ - auto with_sender = (lastSender_ != local_user_) || isDateDifference(lastMsgTimestamp_); - - QString full_body; - if (related.related_event.empty()) { - full_body = body; - } else { - full_body = utils::getFormattedQuoteBody(related, body); - } - TimelineItem *view_item = - new TimelineItem(ty, local_user_, full_body, with_sender, room_id_, scroll_widget_); - - PendingMessage message; - message.ty = ty; - message.txn_id = http::client()->generate_txn_id(); - message.body = body; - message.related = related; - message.widget = view_item; - - try { - message.is_encrypted = cache::client()->isRoomEncrypted(room_id_.toStdString()); - } catch (const lmdb::error &e) { - nhlog::db()->critical("failed to check encryption status of room {}", e.what()); - view_item->deleteLater(); - - // TODO: Send a notification to the user. - - return; - } - - addTimelineItem(view_item); - - lastMessageDirection_ = TimelineDirection::Bottom; - - saveLastMessageInfo(local_user_, QDateTime::currentDateTime()); - handleNewUserMessage(message); -} - -void -TimelineView::addUserMessage(mtx::events::MessageType ty, const QString &body) -{ - addUserMessage(ty, body, RelatedInfo()); -} - -void -TimelineView::handleNewUserMessage(PendingMessage msg) -{ - pending_msgs_.enqueue(msg); - if (pending_msgs_.size() == 1 && pending_sent_msgs_.isEmpty()) - sendNextPendingMessage(); -} - -void -TimelineView::sendNextPendingMessage() -{ - if (pending_msgs_.size() == 0) - return; - - using namespace mtx::events; - - PendingMessage &m = pending_msgs_.head(); - - nhlog::ui()->debug("[{}] sending next queued message", m.txn_id); - - if (m.widget) - m.widget->markSent(); - - if (m.is_encrypted) { - nhlog::ui()->debug("[{}] sending encrypted event", m.txn_id); - prepareEncryptedMessage(std::move(m)); - return; - } - - switch (m.ty) { - case mtx::events::MessageType::Audio: { - http::client()->send_room_message<msg::Audio, EventType::RoomMessage>( - room_id_.toStdString(), - m.txn_id, - toRoomMessage<msg::Audio>(m), - std::bind(&TimelineView::sendRoomMessageHandler, - this, - m.txn_id, - std::placeholders::_1, - std::placeholders::_2)); - - break; - } - case mtx::events::MessageType::Image: { - http::client()->send_room_message<msg::Image, EventType::RoomMessage>( - room_id_.toStdString(), - m.txn_id, - toRoomMessage<msg::Image>(m), - std::bind(&TimelineView::sendRoomMessageHandler, - this, - m.txn_id, - std::placeholders::_1, - std::placeholders::_2)); - - break; - } - case mtx::events::MessageType::Video: { - http::client()->send_room_message<msg::Video, EventType::RoomMessage>( - room_id_.toStdString(), - m.txn_id, - toRoomMessage<msg::Video>(m), - std::bind(&TimelineView::sendRoomMessageHandler, - this, - m.txn_id, - std::placeholders::_1, - std::placeholders::_2)); - - break; - } - case mtx::events::MessageType::File: { - http::client()->send_room_message<msg::File, EventType::RoomMessage>( - room_id_.toStdString(), - m.txn_id, - toRoomMessage<msg::File>(m), - std::bind(&TimelineView::sendRoomMessageHandler, - this, - m.txn_id, - std::placeholders::_1, - std::placeholders::_2)); - - break; - } - case mtx::events::MessageType::Text: { - http::client()->send_room_message<msg::Text, EventType::RoomMessage>( - room_id_.toStdString(), - m.txn_id, - toRoomMessage<msg::Text>(m), - std::bind(&TimelineView::sendRoomMessageHandler, - this, - m.txn_id, - std::placeholders::_1, - std::placeholders::_2)); - - break; - } - case mtx::events::MessageType::Emote: { - http::client()->send_room_message<msg::Emote, EventType::RoomMessage>( - room_id_.toStdString(), - m.txn_id, - toRoomMessage<msg::Emote>(m), - std::bind(&TimelineView::sendRoomMessageHandler, - this, - m.txn_id, - std::placeholders::_1, - std::placeholders::_2)); - break; - } - default: - nhlog::ui()->warn("cannot send unknown message type: {}", m.body.toStdString()); - break; - } -} - -void -TimelineView::notifyForLastEvent() -{ - if (scroll_layout_->count() == 0) { - nhlog::ui()->error("notifyForLastEvent called with empty timeline"); - return; - } - - auto lastItem = scroll_layout_->itemAt(scroll_layout_->count() - 1); - - if (!lastItem) - return; - - auto *lastTimelineItem = qobject_cast<TimelineItem *>(lastItem->widget()); - - if (lastTimelineItem) - emit updateLastTimelineMessage(room_id_, lastTimelineItem->descriptionMessage()); - else - nhlog::ui()->warn("cast to TimelineItem failed: {}", room_id_.toStdString()); -} - -void -TimelineView::notifyForLastEvent(const TimelineEvent &event) -{ - auto descInfo = utils::getMessageDescription(event, local_user_, room_id_); - - if (!descInfo.timestamp.isEmpty()) - emit updateLastTimelineMessage(room_id_, descInfo); -} - -bool -TimelineView::isPendingMessage(const std::string &txn_id, - const QString &sender, - const QString &local_userid) -{ - if (sender != local_userid) - return false; - - auto match_txnid = [txn_id](const auto &msg) -> bool { return msg.txn_id == txn_id; }; - - return std::any_of(pending_msgs_.cbegin(), pending_msgs_.cend(), match_txnid) || - std::any_of(pending_sent_msgs_.cbegin(), pending_sent_msgs_.cend(), match_txnid); -} - -void -TimelineView::removePendingMessage(const std::string &txn_id) -{ - if (txn_id.empty()) - return; - - for (auto it = pending_sent_msgs_.begin(); it != pending_sent_msgs_.end(); ++it) { - if (it->txn_id == txn_id) { - int index = std::distance(pending_sent_msgs_.begin(), it); - pending_sent_msgs_.removeAt(index); - - if (pending_sent_msgs_.isEmpty()) - sendNextPendingMessage(); - - nhlog::ui()->debug("[{}] removed message with sync", txn_id); - } - } - for (auto it = pending_msgs_.begin(); it != pending_msgs_.end(); ++it) { - if (it->txn_id == txn_id) { - if (it->widget) { - it->widget->markReceived(it->is_encrypted); - - // TODO: update when a solution for encrypted messages is available. - if (!it->is_encrypted) - cache::client()->addPendingReceipt(room_id_, it->event_id); - } - - nhlog::ui()->debug("[{}] received sync before message response", txn_id); - return; - } - } -} - -void -TimelineView::handleFailedMessage(const std::string &txn_id) -{ - Q_UNUSED(txn_id); - // Note: We do this even if the message has already been echoed. - QTimer::singleShot(2000, this, SLOT(sendNextPendingMessage())); -} - -void -TimelineView::paintEvent(QPaintEvent *) -{ - QStyleOption opt; - opt.init(this); - QPainter p(this); - style()->drawPrimitive(QStyle::PE_Widget, &opt, &p, this); -} - -void -TimelineView::readLastEvent() const -{ - if (!ChatPage::instance()->userSettings()->isReadReceiptsEnabled()) - return; - - const auto eventId = getLastEventId(); - - if (!eventId.isEmpty()) - http::client()->read_event(room_id_.toStdString(), - eventId.toStdString(), - [this, eventId](mtx::http::RequestErr err) { - if (err) { - nhlog::net()->warn( - "failed to read event ({}, {})", - room_id_.toStdString(), - eventId.toStdString()); - } - }); -} - -QString -TimelineView::getLastEventId() const -{ - auto index = scroll_layout_->count(); - - // Search backwards for the first event that has a valid event id. - while (index > 0) { - --index; - - auto lastItem = scroll_layout_->itemAt(index); - auto *lastTimelineItem = qobject_cast<TimelineItem *>(lastItem->widget()); - - if (lastTimelineItem && !lastTimelineItem->eventId().isEmpty()) - return lastTimelineItem->eventId(); - } - - return QString(""); -} - -void -TimelineView::showEvent(QShowEvent *event) -{ - if (!topMessages_.empty()) { - renderTopEvents(topMessages_); - topMessages_.clear(); - } - - if (!bottomMessages_.empty()) { - renderBottomEvents(bottomMessages_); - bottomMessages_.clear(); - scrollDown(); - } - - toggleScrollDownButton(); - - readLastEvent(); - - QWidget::showEvent(event); -} - -void -TimelineView::hideEvent(QHideEvent *event) -{ - const auto handleHeight = scroll_area_->verticalScrollBar()->sizeHint().height(); - const auto widgetsNum = scroll_layout_->count(); - - // Remove widgets from the timeline to reduce the memory footprint. - if (handleHeight < MIN_SCROLLBAR_HANDLE && widgetsNum > MAX_RETAINED_WIDGETS) - clearTimeline(); - - QWidget::hideEvent(event); -} - -bool -TimelineView::event(QEvent *event) -{ - if (event->type() == QEvent::WindowActivate) - readLastEvent(); - - return QWidget::event(event); -} - -void -TimelineView::clearTimeline() -{ - // Delete all widgets. - QLayoutItem *item; - while ((item = scroll_layout_->takeAt(0)) != nullptr) { - delete item->widget(); - delete item; - } - - // The next call to /messages will be without a prev token. - prev_batch_token_.clear(); - eventIds_.clear(); - - // Clear queues with pending messages to be rendered. - bottomMessages_.clear(); - topMessages_.clear(); - - firstSender_.clear(); - lastSender_.clear(); -} - -void -TimelineView::toggleScrollDownButton() -{ - const int maxScroll = scroll_area_->verticalScrollBar()->maximum(); - const int currentScroll = scroll_area_->verticalScrollBar()->value(); - - if (maxScroll - currentScroll > SCROLL_BAR_GAP) { - scrollDownBtn_->show(); - scrollDownBtn_->raise(); - } else { - scrollDownBtn_->hide(); - } -} - -void -TimelineView::removeEvent(const QString &event_id) -{ - if (!eventIds_.contains(event_id)) { - nhlog::ui()->warn("cannot remove widget with unknown event_id: {}", - event_id.toStdString()); - return; - } - - auto removedItem = eventIds_[event_id]; - - // Find the next and the previous widgets in the timeline - auto prevWidget = relativeWidget(removedItem, -1); - auto nextWidget = relativeWidget(removedItem, 1); - - // See if they are timeline items - auto prevItem = qobject_cast<TimelineItem *>(prevWidget); - auto nextItem = qobject_cast<TimelineItem *>(nextWidget); - - // ... or a date separator - auto prevLabel = qobject_cast<DateSeparator *>(prevWidget); - - // If it's a TimelineItem add an avatar. - if (prevItem) { - prevItem->addAvatar(); - } - - if (nextItem) { - nextItem->addAvatar(); - } else if (prevLabel) { - // If there's no chat message after this, and we have a label before us, delete the - // label. - prevLabel->deleteLater(); - } - - // If we deleted the last item in the timeline... - if (!nextItem && prevItem) - saveLastMessageInfo(prevItem->descriptionMessage().userid, - prevItem->descriptionMessage().datetime); - - // If we deleted the first item in the timeline... - if (!prevItem && nextItem) - saveFirstMessageInfo(nextItem->descriptionMessage().userid, - nextItem->descriptionMessage().datetime); - - // If we deleted the only item in the timeline... - if (!prevItem && !nextItem) { - firstSender_.clear(); - firstMsgTimestamp_ = QDateTime(); - lastSender_.clear(); - lastMsgTimestamp_ = QDateTime(); - } - - // Finally remove the event. - removedItem->deleteLater(); - eventIds_.remove(event_id); - - // Update the room list with a view of the last message after - // all events have been processed. - QTimer::singleShot(0, this, [this]() { notifyForLastEvent(); }); -} - -QWidget * -TimelineView::relativeWidget(QWidget *item, int dt) const -{ - int pos = scroll_layout_->indexOf(item); - - if (pos == -1) - return nullptr; - - pos = pos + dt; - - bool isOutOfBounds = (pos < 0 || pos > scroll_layout_->count() - 1); - - return isOutOfBounds ? nullptr : scroll_layout_->itemAt(pos)->widget(); -} - -TimelineEvent -TimelineView::findFirstViewableEvent(const std::vector<TimelineEvent> &events) -{ - auto it = std::find_if(events.begin(), events.end(), [](const auto &event) { - return mtx::events::EventType::RoomMessage == utils::event_type(event); - }); - - return (it == std::end(events)) ? events.front() : *it; -} - -TimelineEvent -TimelineView::findLastViewableEvent(const std::vector<TimelineEvent> &events) -{ - auto it = std::find_if(events.rbegin(), events.rend(), [](const auto &event) { - return (mtx::events::EventType::RoomMessage == utils::event_type(event)) || - (mtx::events::EventType::RoomEncrypted == utils::event_type(event)); - }); - - return (it == std::rend(events)) ? events.back() : *it; -} - -void -TimelineView::saveMessageInfo(const QString &sender, - uint64_t origin_server_ts, - TimelineDirection direction) -{ - updateLastSender(sender, direction); - - if (direction == TimelineDirection::Bottom) - lastMsgTimestamp_ = QDateTime::fromMSecsSinceEpoch(origin_server_ts); - else - firstMsgTimestamp_ = QDateTime::fromMSecsSinceEpoch(origin_server_ts); -} - -bool -TimelineView::isDateDifference(const QDateTime &first, const QDateTime &second) const -{ - // Check if the dates are in a different day. - if (std::abs(first.daysTo(second)) != 0) - return true; - - const uint64_t diffInSeconds = std::abs(first.msecsTo(second)) / 1000; - constexpr uint64_t fifteenMins = 15 * 60; - - return diffInSeconds > fifteenMins; -} - -void -TimelineView::sendRoomMessageHandler(const std::string &txn_id, - const mtx::responses::EventId &res, - mtx::http::RequestErr err) -{ - if (err) { - const int status_code = static_cast<int>(err->status_code); - nhlog::net()->warn("[{}] failed to send message: {} {}", - txn_id, - err->matrix_error.error, - status_code); - emit messageFailed(txn_id); - return; - } - - emit messageSent(txn_id, QString::fromStdString(res.event_id.to_string())); -} - -template<> -mtx::events::msg::Audio -toRoomMessage<mtx::events::msg::Audio>(const PendingMessage &m) -{ - mtx::events::msg::Audio audio; - audio.info.mimetype = m.mime.toStdString(); - audio.info.size = m.media_size; - audio.body = m.filename.toStdString(); - audio.url = m.body.toStdString(); - return audio; -} - -template<> -mtx::events::msg::Image -toRoomMessage<mtx::events::msg::Image>(const PendingMessage &m) -{ - mtx::events::msg::Image image; - image.info.mimetype = m.mime.toStdString(); - image.info.size = m.media_size; - image.body = m.filename.toStdString(); - image.url = m.body.toStdString(); - image.info.h = m.dimensions.height(); - image.info.w = m.dimensions.width(); - return image; -} - -template<> -mtx::events::msg::Video -toRoomMessage<mtx::events::msg::Video>(const PendingMessage &m) -{ - mtx::events::msg::Video video; - video.info.mimetype = m.mime.toStdString(); - video.info.size = m.media_size; - video.body = m.filename.toStdString(); - video.url = m.body.toStdString(); - return video; -} - -template<> -mtx::events::msg::Emote -toRoomMessage<mtx::events::msg::Emote>(const PendingMessage &m) -{ - auto html = utils::markdownToHtml(m.body); - - mtx::events::msg::Emote emote; - emote.body = m.body.trimmed().toStdString(); - - if (html != m.body.trimmed().toHtmlEscaped()) - emote.formatted_body = html.toStdString(); - - return emote; -} - -template<> -mtx::events::msg::File -toRoomMessage<mtx::events::msg::File>(const PendingMessage &m) -{ - mtx::events::msg::File file; - file.info.mimetype = m.mime.toStdString(); - file.info.size = m.media_size; - file.body = m.filename.toStdString(); - file.url = m.body.toStdString(); - return file; -} - -template<> -mtx::events::msg::Text -toRoomMessage<mtx::events::msg::Text>(const PendingMessage &m) -{ - auto html = utils::markdownToHtml(m.body); - - mtx::events::msg::Text text; - - text.body = m.body.trimmed().toStdString(); - - if (html != m.body.trimmed().toHtmlEscaped()) { - if (!m.related.quoted_body.isEmpty()) { - text.formatted_body = - utils::getFormattedQuoteBody(m.related, html).toStdString(); - } else { - text.formatted_body = html.toStdString(); - } - } - - if (!m.related.related_event.empty()) { - text.relates_to.in_reply_to.event_id = m.related.related_event; - } - - return text; -} - -void -TimelineView::prepareEncryptedMessage(const PendingMessage &msg) -{ - const auto room_id = room_id_.toStdString(); - - using namespace mtx::events; - using namespace mtx::identifiers; - - json content; - - // Serialize the message to the plaintext that will be encrypted. - switch (msg.ty) { - case MessageType::Audio: { - content = json(toRoomMessage<msg::Audio>(msg)); - break; - } - case MessageType::Emote: { - content = json(toRoomMessage<msg::Emote>(msg)); - break; - } - case MessageType::File: { - content = json(toRoomMessage<msg::File>(msg)); - break; - } - case MessageType::Image: { - content = json(toRoomMessage<msg::Image>(msg)); - break; - } - case MessageType::Text: { - content = json(toRoomMessage<msg::Text>(msg)); - break; - } - case MessageType::Video: { - content = json(toRoomMessage<msg::Video>(msg)); - break; - } - default: - break; - } - - json doc{{"type", "m.room.message"}, {"content", content}, {"room_id", room_id}}; - - try { - // Check if we have already an outbound megolm session then we can use. - if (cache::client()->outboundMegolmSessionExists(room_id)) { - auto data = olm::encrypt_group_message( - room_id, http::client()->device_id(), doc.dump()); - - http::client()->send_room_message<msg::Encrypted, EventType::RoomEncrypted>( - room_id, - msg.txn_id, - data, - std::bind(&TimelineView::sendRoomMessageHandler, - this, - msg.txn_id, - std::placeholders::_1, - std::placeholders::_2)); - return; - } - - nhlog::ui()->debug("creating new outbound megolm session"); - - // Create a new outbound megolm session. - auto outbound_session = olm::client()->init_outbound_group_session(); - const auto session_id = mtx::crypto::session_id(outbound_session.get()); - const auto session_key = mtx::crypto::session_key(outbound_session.get()); - - // TODO: needs to be moved in the lib. - auto megolm_payload = json{{"algorithm", "m.megolm.v1.aes-sha2"}, - {"room_id", room_id}, - {"session_id", session_id}, - {"session_key", session_key}}; - - // Saving the new megolm session. - // TODO: Maybe it's too early to save. - OutboundGroupSessionData session_data; - session_data.session_id = session_id; - session_data.session_key = session_key; - session_data.message_index = 0; // TODO Update me - cache::client()->saveOutboundMegolmSession( - room_id, session_data, std::move(outbound_session)); - - const auto members = cache::client()->roomMembers(room_id); - nhlog::ui()->info("retrieved {} members for {}", members.size(), room_id); - - auto keeper = std::make_shared<StateKeeper>( - [megolm_payload, room_id, doc, txn_id = msg.txn_id, this]() { - try { - auto data = olm::encrypt_group_message( - room_id, http::client()->device_id(), doc.dump()); - - http::client() - ->send_room_message<msg::Encrypted, EventType::RoomEncrypted>( - room_id, - txn_id, - data, - std::bind(&TimelineView::sendRoomMessageHandler, - this, - txn_id, - std::placeholders::_1, - std::placeholders::_2)); - - } catch (const lmdb::error &e) { - nhlog::db()->critical( - "failed to save megolm outbound session: {}", e.what()); - } - }); - - mtx::requests::QueryKeys req; - for (const auto &member : members) - req.device_keys[member] = {}; - - http::client()->query_keys( - req, - [keeper = std::move(keeper), megolm_payload, this]( - const mtx::responses::QueryKeys &res, mtx::http::RequestErr err) { - if (err) { - nhlog::net()->warn("failed to query device keys: {} {}", - err->matrix_error.error, - static_cast<int>(err->status_code)); - // TODO: Mark the event as failed. Communicate with the UI. - return; - } - - for (const auto &user : res.device_keys) { - // Mapping from a device_id with valid identity keys to the - // generated room_key event used for sharing the megolm session. - std::map<std::string, std::string> room_key_msgs; - std::map<std::string, DevicePublicKeys> deviceKeys; - - room_key_msgs.clear(); - deviceKeys.clear(); - - for (const auto &dev : user.second) { - const auto user_id = UserId(dev.second.user_id); - const auto device_id = DeviceId(dev.second.device_id); - - const auto device_keys = dev.second.keys; - const auto curveKey = "curve25519:" + device_id.get(); - const auto edKey = "ed25519:" + device_id.get(); - - if ((device_keys.find(curveKey) == device_keys.end()) || - (device_keys.find(edKey) == device_keys.end())) { - nhlog::net()->debug( - "ignoring malformed keys for device {}", - device_id.get()); - continue; - } - - DevicePublicKeys pks; - pks.ed25519 = device_keys.at(edKey); - pks.curve25519 = device_keys.at(curveKey); - - try { - if (!mtx::crypto::verify_identity_signature( - json(dev.second), device_id, user_id)) { - nhlog::crypto()->warn( - "failed to verify identity keys: {}", - json(dev.second).dump(2)); - continue; - } - } catch (const json::exception &e) { - nhlog::crypto()->warn( - "failed to parse device key json: {}", - e.what()); - continue; - } catch (const mtx::crypto::olm_exception &e) { - nhlog::crypto()->warn( - "failed to verify device key json: {}", - e.what()); - continue; - } - - auto room_key = olm::client() - ->create_room_key_event( - user_id, pks.ed25519, megolm_payload) - .dump(); - - room_key_msgs.emplace(device_id, room_key); - deviceKeys.emplace(device_id, pks); - } - - std::vector<std::string> valid_devices; - valid_devices.reserve(room_key_msgs.size()); - for (auto const &d : room_key_msgs) { - valid_devices.push_back(d.first); - - nhlog::net()->info("{}", d.first); - nhlog::net()->info(" curve25519 {}", - deviceKeys.at(d.first).curve25519); - nhlog::net()->info(" ed25519 {}", - deviceKeys.at(d.first).ed25519); - } - - nhlog::net()->info( - "sending claim request for user {} with {} devices", - user.first, - valid_devices.size()); - - http::client()->claim_keys( - user.first, - valid_devices, - std::bind(&TimelineView::handleClaimedKeys, - this, - keeper, - room_key_msgs, - deviceKeys, - user.first, - std::placeholders::_1, - std::placeholders::_2)); - - // TODO: Wait before sending the next batch of requests. - std::this_thread::sleep_for(std::chrono::milliseconds(500)); - } - }); - - // TODO: Let the user know about the errors. - } catch (const lmdb::error &e) { - nhlog::db()->critical( - "failed to open outbound megolm session ({}): {}", room_id, e.what()); - } catch (const mtx::crypto::olm_exception &e) { - nhlog::crypto()->critical( - "failed to open outbound megolm session ({}): {}", room_id, e.what()); - } -} - -void -TimelineView::handleClaimedKeys(std::shared_ptr<StateKeeper> keeper, - const std::map<std::string, std::string> &room_keys, - const std::map<std::string, DevicePublicKeys> &pks, - const std::string &user_id, - const mtx::responses::ClaimKeys &res, - mtx::http::RequestErr err) -{ - if (err) { - nhlog::net()->warn("claim keys error: {} {} {}", - err->matrix_error.error, - err->parse_error, - static_cast<int>(err->status_code)); - return; - } - - nhlog::net()->debug("claimed keys for {}", user_id); - - if (res.one_time_keys.size() == 0) { - nhlog::net()->debug("no one-time keys found for user_id: {}", user_id); - return; - } - - if (res.one_time_keys.find(user_id) == res.one_time_keys.end()) { - nhlog::net()->debug("no one-time keys found for user_id: {}", user_id); - return; - } - - auto retrieved_devices = res.one_time_keys.at(user_id); - - // Payload with all the to_device message to be sent. - json body; - body["messages"][user_id] = json::object(); - - for (const auto &rd : retrieved_devices) { - const auto device_id = rd.first; - nhlog::net()->debug("{} : \n {}", device_id, rd.second.dump(2)); - - // TODO: Verify signatures - auto otk = rd.second.begin()->at("key"); - - if (pks.find(device_id) == pks.end()) { - nhlog::net()->critical("couldn't find public key for device: {}", - device_id); - continue; - } - - auto id_key = pks.at(device_id).curve25519; - auto s = olm::client()->create_outbound_session(id_key, otk); - - if (room_keys.find(device_id) == room_keys.end()) { - nhlog::net()->critical("couldn't find m.room_key for device: {}", - device_id); - continue; - } - - auto device_msg = olm::client()->create_olm_encrypted_content( - s.get(), room_keys.at(device_id), pks.at(device_id).curve25519); - - try { - cache::client()->saveOlmSession(id_key, std::move(s)); - } catch (const lmdb::error &e) { - nhlog::db()->critical("failed to save outbound olm session: {}", e.what()); - } catch (const mtx::crypto::olm_exception &e) { - nhlog::crypto()->critical("failed to pickle outbound olm session: {}", - e.what()); - } - - body["messages"][user_id][device_id] = device_msg; - } - - nhlog::net()->info("send_to_device: {}", user_id); - - http::client()->send_to_device( - "m.room.encrypted", body, [keeper](mtx::http::RequestErr err) { - if (err) { - nhlog::net()->warn("failed to send " - "send_to_device " - "message: {}", - err->matrix_error.error); - } - - (void)keeper; - }); -} diff --git a/src/timeline/TimelineView.h b/src/timeline/TimelineView.h deleted file mode 100644 index 35796efd154a1ce5970d501082df1c5114de3d4c..0000000000000000000000000000000000000000 --- a/src/timeline/TimelineView.h +++ /dev/null @@ -1,449 +0,0 @@ -/* - * nheko Copyright (C) 2017 Konstantinos Sideris <siderisk@auth.gr> - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - */ - -#pragma once - -#include <QApplication> -#include <QLayout> -#include <QList> -#include <QQueue> -#include <QScrollArea> -#include <QScrollBar> -#include <QStyle> -#include <QStyleOption> -#include <QTimer> - -#include <mtx/events.hpp> -#include <mtx/responses/messages.hpp> - -#include "../Utils.h" -#include "MatrixClient.h" -#include "timeline/TimelineItem.h" - -class StateKeeper -{ -public: - StateKeeper(std::function<void()> &&fn) - : fn_(std::move(fn)) - {} - - ~StateKeeper() { fn_(); } - -private: - std::function<void()> fn_; -}; - -struct DecryptionResult -{ - //! The decrypted content as a normal plaintext event. - utils::TimelineEvent event; - //! Whether or not the decryption was successful. - bool isDecrypted = false; -}; - -class FloatingButton; -struct DescInfo; - -// Contains info about a message shown in the history view -// but not yet confirmed by the homeserver through sync. -struct PendingMessage -{ - mtx::events::MessageType ty; - std::string txn_id; - RelatedInfo related; - QString body; - QString filename; - QString mime; - uint64_t media_size; - QString event_id; - TimelineItem *widget; - QSize dimensions; - bool is_encrypted = false; -}; - -template<class MessageT> -MessageT -toRoomMessage(const PendingMessage &) = delete; - -template<> -mtx::events::msg::Audio -toRoomMessage<mtx::events::msg::Audio>(const PendingMessage &m); - -template<> -mtx::events::msg::Emote -toRoomMessage<mtx::events::msg::Emote>(const PendingMessage &m); - -template<> -mtx::events::msg::File -toRoomMessage<mtx::events::msg::File>(const PendingMessage &); - -template<> -mtx::events::msg::Image -toRoomMessage<mtx::events::msg::Image>(const PendingMessage &m); - -template<> -mtx::events::msg::Text -toRoomMessage<mtx::events::msg::Text>(const PendingMessage &); - -template<> -mtx::events::msg::Video -toRoomMessage<mtx::events::msg::Video>(const PendingMessage &m); - -// In which place new TimelineItems should be inserted. -enum class TimelineDirection -{ - Top, - Bottom, -}; - -class TimelineView : public QWidget -{ - Q_OBJECT - -public: - TimelineView(const mtx::responses::Timeline &timeline, - const QString &room_id, - QWidget *parent = 0); - TimelineView(const QString &room_id, QWidget *parent = 0); - - // Add new events at the end of the timeline. - void addEvents(const mtx::responses::Timeline &timeline); - void addUserMessage(mtx::events::MessageType ty, - const QString &body, - const RelatedInfo &related); - void addUserMessage(mtx::events::MessageType ty, const QString &msg); - - template<class Widget, mtx::events::MessageType MsgType> - void addUserMessage(const QString &url, - const QString &filename, - const QString &mime, - uint64_t size, - const QSize &dimensions = QSize()); - void updatePendingMessage(const std::string &txn_id, const QString &event_id); - void scrollDown(); - - //! Remove an item from the timeline with the given Event ID. - void removeEvent(const QString &event_id); - void setPrevBatchToken(const QString &token) { prev_batch_token_ = token; } - -public slots: - void sliderRangeChanged(int min, int max); - void sliderMoved(int position); - void fetchHistory(); - - // Add old events at the top of the timeline. - void addBackwardsEvents(const mtx::responses::Messages &msgs); - - // Whether or not the initial batch has been loaded. - bool hasLoaded() { return scroll_layout_->count() > 0 || isTimelineFinished; } - - void handleFailedMessage(const std::string &txn_id); - -private slots: - void sendNextPendingMessage(); - -signals: - void updateLastTimelineMessage(const QString &user, const DescInfo &info); - void messagesRetrieved(const mtx::responses::Messages &res); - void messageFailed(const std::string &txn_id); - void messageSent(const std::string &txn_id, const QString &event_id); - void markReadEvents(const std::vector<QString> &event_ids); - -protected: - void paintEvent(QPaintEvent *event) override; - void showEvent(QShowEvent *event) override; - void hideEvent(QHideEvent *event) override; - bool event(QEvent *event) override; - -private: - using TimelineEvent = mtx::events::collections::TimelineEvents; - - //! Mark our own widgets as read if they have more than one receipt. - void displayReadReceipts(std::vector<TimelineEvent> events); - //! Determine if the start of the timeline is reached from the response of /messages. - bool isStartOfTimeline(const mtx::responses::Messages &msgs); - - QWidget *relativeWidget(QWidget *item, int dt) const; - - DecryptionResult parseEncryptedEvent( - const mtx::events::EncryptedEvent<mtx::events::msg::Encrypted> &e); - - void handleClaimedKeys(std::shared_ptr<StateKeeper> keeper, - const std::map<std::string, std::string> &room_key, - const std::map<std::string, DevicePublicKeys> &pks, - const std::string &user_id, - const mtx::responses::ClaimKeys &res, - mtx::http::RequestErr err); - - //! Callback for all message sending. - void sendRoomMessageHandler(const std::string &txn_id, - const mtx::responses::EventId &res, - mtx::http::RequestErr err); - void prepareEncryptedMessage(const PendingMessage &msg); - - //! Call the /messages endpoint to fill the timeline. - void getMessages(); - //! HACK: Fixing layout flickering when adding to the bottom - //! of the timeline. - void pushTimelineItem(QWidget *item, TimelineDirection dir) - { - setUpdatesEnabled(false); - item->hide(); - - if (dir == TimelineDirection::Top) - scroll_layout_->insertWidget(0, item); - else - scroll_layout_->addWidget(item); - - QTimer::singleShot(0, this, [item, this]() { - item->show(); - item->adjustSize(); - setUpdatesEnabled(true); - }); - } - - //! Decides whether or not to show or hide the scroll down button. - void toggleScrollDownButton(); - void init(); - void addTimelineItem(QWidget *item, - TimelineDirection direction = TimelineDirection::Bottom); - void updateLastSender(const QString &user_id, TimelineDirection direction); - void notifyForLastEvent(); - void notifyForLastEvent(const TimelineEvent &event); - //! Keep track of the sender and the timestamp of the current message. - void saveLastMessageInfo(const QString &sender, const QDateTime &datetime) - { - lastSender_ = sender; - lastMsgTimestamp_ = datetime; - } - void saveFirstMessageInfo(const QString &sender, const QDateTime &datetime) - { - firstSender_ = sender; - firstMsgTimestamp_ = datetime; - } - //! Keep track of the sender and the timestamp of the current message. - void saveMessageInfo(const QString &sender, - uint64_t origin_server_ts, - TimelineDirection direction); - - TimelineEvent findFirstViewableEvent(const std::vector<TimelineEvent> &events); - TimelineEvent findLastViewableEvent(const std::vector<TimelineEvent> &events); - - //! Mark the last event as read. - void readLastEvent() const; - //! Whether or not the scrollbar is visible (non-zero height). - bool isScrollbarActivated() { return scroll_area_->verticalScrollBar()->value() != 0; } - //! Retrieve the event id of the last item. - QString getLastEventId() const; - - template<class Event, class Widget> - TimelineItem *processMessageEvent(const Event &event, TimelineDirection direction); - - // TODO: Remove this eventually. - template<class Event> - TimelineItem *processMessageEvent(const Event &event, TimelineDirection direction); - - // For events with custom display widgets. - template<class Event, class Widget> - TimelineItem *createTimelineItem(const Event &event, bool withSender); - - // For events without custom display widgets. - // TODO: All events should have custom widgets. - template<class Event> - TimelineItem *createTimelineItem(const Event &event, bool withSender); - - // Used to determine whether or not we should prefix a message with the - // sender's name. - bool isSenderRendered(const QString &user_id, - uint64_t origin_server_ts, - TimelineDirection direction); - - bool isPendingMessage(const std::string &txn_id, - const QString &sender, - const QString &userid); - void removePendingMessage(const std::string &txn_id); - - bool isDuplicate(const QString &event_id) { return eventIds_.contains(event_id); } - - void handleNewUserMessage(PendingMessage msg); - bool isDateDifference(const QDateTime &first, - const QDateTime &second = QDateTime::currentDateTime()) const; - - // Return nullptr if the event couldn't be parsed. - QWidget *parseMessageEvent(const mtx::events::collections::TimelineEvents &event, - TimelineDirection direction); - - //! Store the event id associated with the given widget. - void saveEventId(QWidget *widget); - //! Remove all widgets from the timeline layout. - void clearTimeline(); - - QVBoxLayout *top_layout_; - QVBoxLayout *scroll_layout_; - - QScrollArea *scroll_area_; - QWidget *scroll_widget_; - - QString firstSender_; - QDateTime firstMsgTimestamp_; - QString lastSender_; - QDateTime lastMsgTimestamp_; - - QString room_id_; - QString prev_batch_token_; - QString local_user_; - - bool isPaginationInProgress_ = false; - - // Keeps track whether or not the user has visited the view. - bool isInitialized = false; - bool isTimelineFinished = false; - bool isInitialSync = true; - - const int SCROLL_BAR_GAP = 200; - - QTimer *paginationTimer_; - - int scroll_height_ = 0; - int previous_max_height_ = 0; - - int oldPosition_; - int oldHeight_; - - FloatingButton *scrollDownBtn_; - - TimelineDirection lastMessageDirection_; - - //! Messages received by sync not added to the timeline. - std::vector<TimelineEvent> bottomMessages_; - //! Messages received by /messages not added to the timeline. - std::vector<TimelineEvent> topMessages_; - - //! Render the given timeline events to the bottom of the timeline. - void renderBottomEvents(const std::vector<TimelineEvent> &events); - //! Render the given timeline events to the top of the timeline. - void renderTopEvents(const std::vector<TimelineEvent> &events); - - // The events currently rendered. Used for duplicate detection. - QMap<QString, QWidget *> eventIds_; - QQueue<PendingMessage> pending_msgs_; - QList<PendingMessage> pending_sent_msgs_; -}; - -template<class Widget, mtx::events::MessageType MsgType> -void -TimelineView::addUserMessage(const QString &url, - const QString &filename, - const QString &mime, - uint64_t size, - const QSize &dimensions) -{ - auto with_sender = (lastSender_ != local_user_) || isDateDifference(lastMsgTimestamp_); - auto trimmed = QFileInfo{filename}.fileName(); // Trim file path. - - auto widget = new Widget(url, trimmed, size, this); - - TimelineItem *view_item = - new TimelineItem(widget, local_user_, with_sender, room_id_, scroll_widget_); - - addTimelineItem(view_item); - - lastMessageDirection_ = TimelineDirection::Bottom; - - // Keep track of the sender and the timestamp of the current message. - saveLastMessageInfo(local_user_, QDateTime::currentDateTime()); - - PendingMessage message; - message.ty = MsgType; - message.txn_id = http::client()->generate_txn_id(); - message.body = url; - message.filename = trimmed; - message.mime = mime; - message.media_size = size; - message.widget = view_item; - message.dimensions = dimensions; - - handleNewUserMessage(message); -} - -template<class Event> -TimelineItem * -TimelineView::createTimelineItem(const Event &event, bool withSender) -{ - TimelineItem *item = new TimelineItem(event, withSender, room_id_, scroll_widget_); - return item; -} - -template<class Event, class Widget> -TimelineItem * -TimelineView::createTimelineItem(const Event &event, bool withSender) -{ - auto eventWidget = new Widget(event); - auto item = new TimelineItem(eventWidget, event, withSender, room_id_, scroll_widget_); - - return item; -} - -template<class Event> -TimelineItem * -TimelineView::processMessageEvent(const Event &event, TimelineDirection direction) -{ - const auto event_id = QString::fromStdString(event.event_id); - const auto sender = QString::fromStdString(event.sender); - - const auto txn_id = event.unsigned_data.transaction_id; - if ((!txn_id.empty() && isPendingMessage(txn_id, sender, local_user_)) || - isDuplicate(event_id)) { - removePendingMessage(txn_id); - return nullptr; - } - - auto with_sender = isSenderRendered(sender, event.origin_server_ts, direction); - - saveMessageInfo(sender, event.origin_server_ts, direction); - - auto item = createTimelineItem<Event>(event, with_sender); - - eventIds_[event_id] = item; - - return item; -} - -template<class Event, class Widget> -TimelineItem * -TimelineView::processMessageEvent(const Event &event, TimelineDirection direction) -{ - const auto event_id = QString::fromStdString(event.event_id); - const auto sender = QString::fromStdString(event.sender); - - const auto txn_id = event.unsigned_data.transaction_id; - if ((!txn_id.empty() && isPendingMessage(txn_id, sender, local_user_)) || - isDuplicate(event_id)) { - removePendingMessage(txn_id); - return nullptr; - } - - auto with_sender = isSenderRendered(sender, event.origin_server_ts, direction); - - saveMessageInfo(sender, event.origin_server_ts, direction); - - auto item = createTimelineItem<Event, Widget>(event, with_sender); - - eventIds_[event_id] = item; - - return item; -} diff --git a/src/timeline/TimelineViewManager.cpp b/src/timeline/TimelineViewManager.cpp index 8650548175c814165364689eec8f8ea6b1956c23..6e18d111f1692760ff630ccfbc51eecd57a05521 100644 --- a/src/timeline/TimelineViewManager.cpp +++ b/src/timeline/TimelineViewManager.cpp @@ -1,340 +1,292 @@ -/* - * nheko Copyright (C) 2017 Konstantinos Sideris <siderisk@auth.gr> - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - */ +#include "TimelineViewManager.h" -#include <random> +#include <QMetaType> +#include <QPalette> +#include <QQmlContext> -#include <QApplication> -#include <QFileInfo> -#include <QSettings> - -#include "Cache.h" +#include "ChatPage.h" +#include "ColorImageProvider.h" +#include "DelegateChooser.h" #include "Logging.h" -#include "Utils.h" -#include "timeline/TimelineView.h" -#include "timeline/TimelineViewManager.h" -#include "timeline/widgets/AudioItem.h" -#include "timeline/widgets/FileItem.h" -#include "timeline/widgets/ImageItem.h" -#include "timeline/widgets/VideoItem.h" - -TimelineViewManager::TimelineViewManager(QWidget *parent) - : QStackedWidget(parent) -{} +#include "MxcImageProvider.h" +#include "UserSettingsPage.h" +#include "dialogs/ImageOverlay.h" void -TimelineViewManager::updateReadReceipts(const QString &room_id, - const std::vector<QString> &event_ids) +TimelineViewManager::updateColorPalette() { - if (timelineViewExists(room_id)) { - auto view = views_[room_id]; - if (view) - emit view->markReadEvents(event_ids); + UserSettings settings; + if (settings.theme() == "light") { + QPalette lightActive(/*windowText*/ QColor("#333"), + /*button*/ QColor("#333"), + /*light*/ QColor(), + /*dark*/ QColor(220, 220, 220, 120), + /*mid*/ QColor(), + /*text*/ QColor("#333"), + /*bright_text*/ QColor(), + /*base*/ QColor("white"), + /*window*/ QColor("white")); + view->rootContext()->setContextProperty("currentActivePalette", lightActive); + view->rootContext()->setContextProperty("currentInactivePalette", lightActive); + } else if (settings.theme() == "dark") { + QPalette darkActive(/*windowText*/ QColor("#caccd1"), + /*button*/ QColor("#caccd1"), + /*light*/ QColor(), + /*dark*/ QColor(45, 49, 57, 120), + /*mid*/ QColor(), + /*text*/ QColor("#caccd1"), + /*bright_text*/ QColor(), + /*base*/ QColor("#202228"), + /*window*/ QColor("#202228")); + darkActive.setColor(QPalette::Highlight, QColor("#e7e7e9")); + view->rootContext()->setContextProperty("currentActivePalette", darkActive); + view->rootContext()->setContextProperty("currentInactivePalette", darkActive); + } else { + view->rootContext()->setContextProperty("currentActivePalette", QPalette()); + view->rootContext()->setContextProperty("currentInactivePalette", nullptr); } } -void -TimelineViewManager::removeTimelineEvent(const QString &room_id, const QString &event_id) -{ - auto view = views_[room_id]; - - if (view) - view->removeEvent(event_id); -} - -void -TimelineViewManager::queueTextMessage(const QString &msg) -{ - if (active_room_.isEmpty()) - return; - - auto room_id = active_room_; - auto view = views_[room_id]; - - view->addUserMessage(mtx::events::MessageType::Text, msg); -} - -void -TimelineViewManager::queueEmoteMessage(const QString &msg) -{ - if (active_room_.isEmpty()) - return; - - auto room_id = active_room_; - auto view = views_[room_id]; - - view->addUserMessage(mtx::events::MessageType::Emote, msg); -} - -void -TimelineViewManager::queueReplyMessage(const QString &reply, const RelatedInfo &related) +TimelineViewManager::TimelineViewManager(QWidget *parent) + : imgProvider(new MxcImageProvider()) + , colorImgProvider(new ColorImageProvider()) { - if (active_room_.isEmpty()) - return; - - auto room_id = active_room_; - auto view = views_[room_id]; - - view->addUserMessage(mtx::events::MessageType::Text, reply, related); + qmlRegisterUncreatableMetaObject(qml_mtx_events::staticMetaObject, + "im.nheko", + 1, + 0, + "MtxEvent", + "Can't instantiate enum!"); + qmlRegisterType<DelegateChoice>("im.nheko", 1, 0, "DelegateChoice"); + qmlRegisterType<DelegateChooser>("im.nheko", 1, 0, "DelegateChooser"); + +#ifdef USE_QUICK_VIEW + view = new QQuickView(); + container = QWidget::createWindowContainer(view, parent); +#else + view = new QQuickWidget(parent); + container = view; + view->setResizeMode(QQuickWidget::SizeRootObjectToView); + container->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); + + connect(view, &QQuickWidget::statusChanged, this, [](QQuickWidget::Status status) { + nhlog::ui()->debug("Status changed to {}", status); + }); +#endif + container->setMinimumSize(200, 200); + view->rootContext()->setContextProperty("timelineManager", this); + updateColorPalette(); + view->engine()->addImageProvider("MxcImage", imgProvider); + view->engine()->addImageProvider("colorimage", colorImgProvider); + view->setSource(QUrl("qrc:///qml/TimelineView.qml")); + + connect(dynamic_cast<ChatPage *>(parent), + &ChatPage::themeChanged, + this, + &TimelineViewManager::updateColorPalette); } void -TimelineViewManager::queueImageMessage(const QString &roomid, - const QString &filename, - const QString &url, - const QString &mime, - uint64_t size, - const QSize &dimensions) +TimelineViewManager::sync(const mtx::responses::Rooms &rooms) { - if (!timelineViewExists(roomid)) { - nhlog::ui()->warn("Cannot send m.image message to a non-managed view"); - return; + for (auto it = rooms.join.cbegin(); it != rooms.join.cend(); ++it) { + // addRoom will only add the room, if it doesn't exist + addRoom(QString::fromStdString(it->first)); + models.value(QString::fromStdString(it->first))->addEvents(it->second.timeline); } - auto view = views_[roomid]; - - view->addUserMessage<ImageItem, mtx::events::MessageType::Image>( - url, filename, mime, size, dimensions); + this->isInitialSync_ = false; + emit initialSyncChanged(false); } void -TimelineViewManager::queueFileMessage(const QString &roomid, - const QString &filename, - const QString &url, - const QString &mime, - uint64_t size) +TimelineViewManager::addRoom(const QString &room_id) { - if (!timelineViewExists(roomid)) { - nhlog::ui()->warn("cannot send m.file message to a non-managed view"); - return; + if (!models.contains(room_id)) { + QSharedPointer<TimelineModel> newRoom(new TimelineModel(this, room_id)); + connect(newRoom.data(), + &TimelineModel::newEncryptedImage, + imgProvider, + &MxcImageProvider::addEncryptionInfo); + models.insert(room_id, std::move(newRoom)); } - - auto view = views_[roomid]; - - view->addUserMessage<FileItem, mtx::events::MessageType::File>(url, filename, mime, size); } void -TimelineViewManager::queueAudioMessage(const QString &roomid, - const QString &filename, - const QString &url, - const QString &mime, - uint64_t size) +TimelineViewManager::setHistoryView(const QString &room_id) { - if (!timelineViewExists(roomid)) { - nhlog::ui()->warn("cannot send m.audio message to a non-managed view"); - return; - } - - auto view = views_[roomid]; + nhlog::ui()->info("Trying to activate room {}", room_id.toStdString()); - view->addUserMessage<AudioItem, mtx::events::MessageType::Audio>(url, filename, mime, size); + auto room = models.find(room_id); + if (room != models.end()) { + timeline_ = room.value().data(); + emit activeTimelineChanged(timeline_); + nhlog::ui()->info("Activated room {}", room_id.toStdString()); + } } void -TimelineViewManager::queueVideoMessage(const QString &roomid, - const QString &filename, - const QString &url, - const QString &mime, - uint64_t size) +TimelineViewManager::openImageOverlay(QString mxcUrl, QString eventId) const { - if (!timelineViewExists(roomid)) { - nhlog::ui()->warn("cannot send m.video message to a non-managed view"); - return; - } - - auto view = views_[roomid]; + QQuickImageResponse *imgResponse = + imgProvider->requestImageResponse(mxcUrl.remove("mxc://"), QSize()); + connect(imgResponse, &QQuickImageResponse::finished, this, [this, eventId, imgResponse]() { + if (!imgResponse->errorString().isEmpty()) { + nhlog::ui()->error("Error when retrieving image for overlay: {}", + imgResponse->errorString().toStdString()); + return; + } + auto pixmap = QPixmap::fromImage(imgResponse->textureFactory()->image()); - view->addUserMessage<VideoItem, mtx::events::MessageType::Video>(url, filename, mime, size); + auto imgDialog = new dialogs::ImageOverlay(pixmap); + imgDialog->show(); + connect(imgDialog, &dialogs::ImageOverlay::saving, timeline_, [this, eventId]() { + timeline_->saveMedia(eventId); + }); + }); } void -TimelineViewManager::initialize(const mtx::responses::Rooms &rooms) +TimelineViewManager::updateReadReceipts(const QString &room_id, + const std::vector<QString> &event_ids) { - for (auto it = rooms.join.cbegin(); it != rooms.join.cend(); ++it) { - addRoom(it->second, QString::fromStdString(it->first)); + auto room = models.find(room_id); + if (room != models.end()) { + room.value()->markEventsAsRead(event_ids); } - - sync(rooms); } void TimelineViewManager::initWithMessages(const std::map<QString, mtx::responses::Timeline> &msgs) { - for (auto it = msgs.cbegin(); it != msgs.cend(); ++it) { - if (timelineViewExists(it->first)) - return; + for (const auto &e : msgs) { + addRoom(e.first); - // Create a history view with the room events. - TimelineView *view = new TimelineView(it->second, it->first); - views_.emplace(it->first, QSharedPointer<TimelineView>(view)); - - connect(view, - &TimelineView::updateLastTimelineMessage, - this, - &TimelineViewManager::updateRoomsLastMessage); - - // Add the view in the widget stack. - addWidget(view); + models.value(e.first)->addEvents(e.second); } } void -TimelineViewManager::initialize(const std::vector<std::string> &rooms) +TimelineViewManager::queueTextMessage(const QString &msg) { - for (const auto &roomid : rooms) - addRoom(QString::fromStdString(roomid)); + mtx::events::msg::Text text = {}; + text.body = msg.trimmed().toStdString(); + text.format = "org.matrix.custom.html"; + text.formatted_body = utils::markdownToHtml(msg).toStdString(); + + if (timeline_) + timeline_->sendMessage(text); } void -TimelineViewManager::addRoom(const mtx::responses::JoinedRoom &room, const QString &room_id) +TimelineViewManager::queueReplyMessage(const QString &reply, const RelatedInfo &related) { - if (timelineViewExists(room_id)) - return; - - // Create a history view with the room events. - TimelineView *view = new TimelineView(room.timeline, room_id); - views_.emplace(room_id, QSharedPointer<TimelineView>(view)); + mtx::events::msg::Text text = {}; + + QString body; + bool firstLine = true; + for (const auto &line : related.quoted_body.split("\n")) { + if (firstLine) { + firstLine = false; + body = QString("> <%1> %2\n").arg(related.quoted_user).arg(line); + } else { + body = QString("%1\n> %2\n").arg(body).arg(line); + } + } - connect(view, - &TimelineView::updateLastTimelineMessage, - this, - &TimelineViewManager::updateRoomsLastMessage); + text.body = QString("%1\n%2").arg(body).arg(reply).toStdString(); + text.format = "org.matrix.custom.html"; + text.formatted_body = + utils::getFormattedQuoteBody(related, utils::markdownToHtml(reply)).toStdString(); + text.relates_to.in_reply_to.event_id = related.related_event; - // Add the view in the widget stack. - addWidget(view); + if (timeline_) + timeline_->sendMessage(text); } void -TimelineViewManager::addRoom(const QString &room_id) +TimelineViewManager::queueEmoteMessage(const QString &msg) { - if (timelineViewExists(room_id)) - return; + auto html = utils::markdownToHtml(msg); - // Create a history view without any events. - TimelineView *view = new TimelineView(room_id); - views_.emplace(room_id, QSharedPointer<TimelineView>(view)); + mtx::events::msg::Emote emote; + emote.body = msg.trimmed().toStdString(); - connect(view, - &TimelineView::updateLastTimelineMessage, - this, - &TimelineViewManager::updateRoomsLastMessage); + if (html != msg.trimmed().toHtmlEscaped()) + emote.formatted_body = html.toStdString(); - // Add the view in the widget stack. - addWidget(view); + if (timeline_) + timeline_->sendMessage(emote); } void -TimelineViewManager::sync(const mtx::responses::Rooms &rooms) +TimelineViewManager::queueImageMessage(const QString &roomid, + const QString &filename, + const boost::optional<mtx::crypto::EncryptedFile> &file, + const QString &url, + const QString &mime, + uint64_t dsize, + const QSize &dimensions) { - for (const auto &room : rooms.join) { - auto roomid = QString::fromStdString(room.first); - - if (!timelineViewExists(roomid)) { - nhlog::ui()->warn("ignoring event from unknown room: {}", - roomid.toStdString()); - continue; - } - - auto view = views_.at(roomid); - - view->addEvents(room.second.timeline); - } + mtx::events::msg::Image image; + image.info.mimetype = mime.toStdString(); + image.info.size = dsize; + image.body = filename.toStdString(); + image.url = url.toStdString(); + image.info.h = dimensions.height(); + image.info.w = dimensions.width(); + image.file = file; + models.value(roomid)->sendMessage(image); } void -TimelineViewManager::setHistoryView(const QString &room_id) +TimelineViewManager::queueFileMessage( + const QString &roomid, + const QString &filename, + const boost::optional<mtx::crypto::EncryptedFile> &encryptedFile, + const QString &url, + const QString &mime, + uint64_t dsize) { - if (!timelineViewExists(room_id)) { - nhlog::ui()->warn("room from RoomList is not present in ViewManager: {}", - room_id.toStdString()); - return; - } - - active_room_ = room_id; - auto view = views_.at(room_id); - - setCurrentWidget(view.data()); - - view->fetchHistory(); - view->scrollDown(); + mtx::events::msg::File file; + file.info.mimetype = mime.toStdString(); + file.info.size = dsize; + file.body = filename.toStdString(); + file.url = url.toStdString(); + file.file = encryptedFile; + models.value(roomid)->sendMessage(file); } -QString -TimelineViewManager::chooseRandomColor() +void +TimelineViewManager::queueAudioMessage(const QString &roomid, + const QString &filename, + const boost::optional<mtx::crypto::EncryptedFile> &file, + const QString &url, + const QString &mime, + uint64_t dsize) { - std::random_device random_device; - std::mt19937 engine{random_device()}; - std::uniform_real_distribution<float> dist(0, 1); - - float hue = dist(engine); - float saturation = 0.9; - float value = 0.7; - - int hue_i = hue * 6; - - float f = hue * 6 - hue_i; - - float p = value * (1 - saturation); - float q = value * (1 - f * saturation); - float t = value * (1 - (1 - f) * saturation); - - float r = 0; - float g = 0; - float b = 0; - - if (hue_i == 0) { - r = value; - g = t; - b = p; - } else if (hue_i == 1) { - r = q; - g = value; - b = p; - } else if (hue_i == 2) { - r = p; - g = value; - b = t; - } else if (hue_i == 3) { - r = p; - g = q; - b = value; - } else if (hue_i == 4) { - r = t; - g = p; - b = value; - } else if (hue_i == 5) { - r = value; - g = p; - b = q; - } - - int ri = r * 256; - int gi = g * 256; - int bi = b * 256; - - QColor color(ri, gi, bi); - - return color.name(); + mtx::events::msg::Audio audio; + audio.info.mimetype = mime.toStdString(); + audio.info.size = dsize; + audio.body = filename.toStdString(); + audio.url = url.toStdString(); + audio.file = file; + models.value(roomid)->sendMessage(audio); } -bool -TimelineViewManager::hasLoaded() const +void +TimelineViewManager::queueVideoMessage(const QString &roomid, + const QString &filename, + const boost::optional<mtx::crypto::EncryptedFile> &file, + const QString &url, + const QString &mime, + uint64_t dsize) { - return std::all_of(views_.cbegin(), views_.cend(), [](const auto &view) { - return view.second->hasLoaded(); - }); + mtx::events::msg::Video video; + video.info.mimetype = mime.toStdString(); + video.info.size = dsize; + video.body = filename.toStdString(); + video.url = url.toStdString(); + video.file = file; + models.value(roomid)->sendMessage(video); } diff --git a/src/timeline/TimelineViewManager.h b/src/timeline/TimelineViewManager.h index b52136d97a3112fe2778b2b10b602ecedf247424..9e8de616c08974abaf51a320136047ac4e12dd2d 100644 --- a/src/timeline/TimelineViewManager.h +++ b/src/timeline/TimelineViewManager.h @@ -1,98 +1,97 @@ -/* - * nheko Copyright (C) 2017 Konstantinos Sideris <siderisk@auth.gr> - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - */ - #pragma once +#include <QQuickView> +#include <QQuickWidget> #include <QSharedPointer> -#include <QStackedWidget> +#include <QWidget> -#include <mtx.hpp> +#include <mtx/common.hpp> +#include <mtx/responses.hpp> +#include "Cache.h" +#include "Logging.h" +#include "TimelineModel.h" #include "Utils.h" -class QFile; - -class RoomInfoListItem; -class TimelineView; -struct DescInfo; -struct SavedMessages; +class MxcImageProvider; +class ColorImageProvider; -class TimelineViewManager : public QStackedWidget +class TimelineViewManager : public QObject { Q_OBJECT -public: - TimelineViewManager(QWidget *parent); + Q_PROPERTY( + TimelineModel *timeline MEMBER timeline_ READ activeTimeline NOTIFY activeTimelineChanged) + Q_PROPERTY( + bool isInitialSync MEMBER isInitialSync_ READ isInitialSync NOTIFY initialSyncChanged) - // Initialize with timeline events. - void initialize(const mtx::responses::Rooms &rooms); - // Empty initialization. - void initialize(const std::vector<std::string> &rooms); - - void addRoom(const mtx::responses::JoinedRoom &room, const QString &room_id); - void addRoom(const QString &room_id); +public: + TimelineViewManager(QWidget *parent = 0); + QWidget *getWidget() const { return container; } void sync(const mtx::responses::Rooms &rooms); - void clearAll() { views_.clear(); } + void addRoom(const QString &room_id); - // Check if all the timelines have been loaded. - bool hasLoaded() const; + void clearAll() { models.clear(); } - static QString chooseRandomColor(); + Q_INVOKABLE TimelineModel *activeTimeline() const { return timeline_; } + Q_INVOKABLE bool isInitialSync() const { return isInitialSync_; } + Q_INVOKABLE void openImageOverlay(QString mxcUrl, QString eventId) const; signals: void clearRoomMessageCount(QString roomid); - void updateRoomsLastMessage(const QString &user, const DescInfo &info); + void updateRoomsLastMessage(QString roomid, const DescInfo &info); + void activeTimelineChanged(TimelineModel *timeline); + void initialSyncChanged(bool isInitialSync); public slots: void updateReadReceipts(const QString &room_id, const std::vector<QString> &event_ids); - void removeTimelineEvent(const QString &room_id, const QString &event_id); void initWithMessages(const std::map<QString, mtx::responses::Timeline> &msgs); void setHistoryView(const QString &room_id); + void updateColorPalette(); + void queueTextMessage(const QString &msg); void queueReplyMessage(const QString &reply, const RelatedInfo &related); void queueEmoteMessage(const QString &msg); void queueImageMessage(const QString &roomid, const QString &filename, + const boost::optional<mtx::crypto::EncryptedFile> &file, const QString &url, const QString &mime, uint64_t dsize, const QSize &dimensions); void queueFileMessage(const QString &roomid, const QString &filename, + const boost::optional<mtx::crypto::EncryptedFile> &file, const QString &url, const QString &mime, uint64_t dsize); void queueAudioMessage(const QString &roomid, const QString &filename, + const boost::optional<mtx::crypto::EncryptedFile> &file, const QString &url, const QString &mime, uint64_t dsize); void queueVideoMessage(const QString &roomid, const QString &filename, + const boost::optional<mtx::crypto::EncryptedFile> &file, const QString &url, const QString &mime, uint64_t dsize); private: - //! Check if the given room id is managed by a TimelineView. - bool timelineViewExists(const QString &id) { return views_.find(id) != views_.end(); } - - QString active_room_; - std::map<QString, QSharedPointer<TimelineView>> views_; +#ifdef USE_QUICK_VIEW + QQuickView *view; +#else + QQuickWidget *view; +#endif + QWidget *container; + + MxcImageProvider *imgProvider; + ColorImageProvider *colorImgProvider; + + QHash<QString, QSharedPointer<TimelineModel>> models; + TimelineModel *timeline_ = nullptr; + bool isInitialSync_ = true; }; diff --git a/src/timeline/widgets/AudioItem.cpp b/src/timeline/widgets/AudioItem.cpp deleted file mode 100644 index 5d6431ee4c881b4fffe398b908747459d9dc4dda..0000000000000000000000000000000000000000 --- a/src/timeline/widgets/AudioItem.cpp +++ /dev/null @@ -1,236 +0,0 @@ -/* - * nheko Copyright (C) 2017 Konstantinos Sideris <siderisk@auth.gr> - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - */ - -#include <QBrush> -#include <QDesktopServices> -#include <QFile> -#include <QFileDialog> -#include <QPainter> -#include <QPixmap> -#include <QtGlobal> - -#include "Logging.h" -#include "MatrixClient.h" -#include "Utils.h" - -#include "timeline/widgets/AudioItem.h" - -constexpr int MaxWidth = 400; -constexpr int Height = 70; -constexpr int IconRadius = 22; -constexpr int IconDiameter = IconRadius * 2; -constexpr int HorizontalPadding = 12; -constexpr int TextPadding = 15; -constexpr int ActionIconRadius = IconRadius - 4; - -constexpr double VerticalPadding = Height - 2 * IconRadius; -constexpr double IconYCenter = Height / 2; -constexpr double IconXCenter = HorizontalPadding + IconRadius; - -void -AudioItem::init() -{ - setMouseTracking(true); - setCursor(Qt::PointingHandCursor); - setAttribute(Qt::WA_Hover, true); - - playIcon_.addFile(":/icons/icons/ui/play-sign.png"); - pauseIcon_.addFile(":/icons/icons/ui/pause-symbol.png"); - - player_ = new QMediaPlayer; - player_->setMedia(QUrl(url_)); - player_->setVolume(100); - player_->setNotifyInterval(1000); - - connect(player_, &QMediaPlayer::stateChanged, this, [this](QMediaPlayer::State state) { - if (state == QMediaPlayer::StoppedState) { - state_ = AudioState::Play; - player_->setMedia(QUrl(url_)); - update(); - } - }); - - setFixedHeight(Height); -} - -AudioItem::AudioItem(const mtx::events::RoomEvent<mtx::events::msg::Audio> &event, QWidget *parent) - : QWidget(parent) - , url_{QUrl(QString::fromStdString(event.content.url))} - , text_{QString::fromStdString(event.content.body)} - , event_{event} -{ - readableFileSize_ = utils::humanReadableFileSize(event.content.info.size); - - init(); -} - -AudioItem::AudioItem(const QString &url, const QString &filename, uint64_t size, QWidget *parent) - : QWidget(parent) - , url_{url} - , text_{filename} -{ - readableFileSize_ = utils::humanReadableFileSize(size); - - init(); -} - -QSize -AudioItem::sizeHint() const -{ - return QSize(MaxWidth, Height); -} - -void -AudioItem::mousePressEvent(QMouseEvent *event) -{ - if (event->button() != Qt::LeftButton) - return; - - auto point = event->pos(); - - // Click on the download icon. - if (QRect(HorizontalPadding, VerticalPadding / 2, IconDiameter, IconDiameter) - .contains(point)) { - if (state_ == AudioState::Play) { - state_ = AudioState::Pause; - player_->play(); - } else { - state_ = AudioState::Play; - player_->pause(); - } - - update(); - } else { - filenameToSave_ = QFileDialog::getSaveFileName(this, tr("Save File"), text_); - - if (filenameToSave_.isEmpty()) - return; - - auto proxy = std::make_shared<MediaProxy>(); - connect(proxy.get(), &MediaProxy::fileDownloaded, this, &AudioItem::fileDownloaded); - - http::client()->download( - url_.toString().toStdString(), - [proxy = std::move(proxy), url = url_](const std::string &data, - const std::string &, - const std::string &, - mtx::http::RequestErr err) { - if (err) { - nhlog::net()->info("failed to retrieve m.audio content: {}", - url.toString().toStdString()); - return; - } - - emit proxy->fileDownloaded(QByteArray(data.data(), data.size())); - }); - } -} - -void -AudioItem::fileDownloaded(const QByteArray &data) -{ - try { - QFile file(filenameToSave_); - - if (!file.open(QIODevice::WriteOnly)) - return; - - file.write(data); - file.close(); - } catch (const std::exception &e) { - nhlog::ui()->warn("error while saving file: {}", e.what()); - } -} - -void -AudioItem::resizeEvent(QResizeEvent *event) -{ - QFont font; - font.setWeight(QFont::Medium); - - QFontMetrics fm(font); -#if QT_VERSION < QT_VERSION_CHECK(5, 11, 0) - const int computedWidth = std::min( - fm.width(text_) + 2 * IconRadius + VerticalPadding * 2 + TextPadding, (double)MaxWidth); -#else - const int computedWidth = - std::min(fm.horizontalAdvance(text_) + 2 * IconRadius + VerticalPadding * 2 + TextPadding, - (double)MaxWidth); -#endif - resize(computedWidth, Height); - - event->accept(); -} - -void -AudioItem::paintEvent(QPaintEvent *event) -{ - Q_UNUSED(event); - - QPainter painter(this); - painter.setRenderHint(QPainter::Antialiasing); - - QFont font; - font.setWeight(QFont::Medium); - - QFontMetrics fm(font); - - QPainterPath path; - path.addRoundedRect(QRectF(0, 0, width(), height()), 10, 10); - - painter.setPen(Qt::NoPen); - painter.fillPath(path, backgroundColor_); - painter.drawPath(path); - - QPainterPath circle; - circle.addEllipse(QPoint(IconXCenter, IconYCenter), IconRadius, IconRadius); - - painter.setPen(Qt::NoPen); - painter.fillPath(circle, iconColor_); - painter.drawPath(circle); - - QIcon icon_; - if (state_ == AudioState::Play) - icon_ = playIcon_; - else - icon_ = pauseIcon_; - - icon_.paint(&painter, - QRect(IconXCenter - ActionIconRadius / 2, - IconYCenter - ActionIconRadius / 2, - ActionIconRadius, - ActionIconRadius), - Qt::AlignCenter, - QIcon::Normal); - - const int textStartX = HorizontalPadding + 2 * IconRadius + TextPadding; - const int textStartY = VerticalPadding + fm.ascent() / 2; - - // Draw the filename. - QString elidedText = fm.elidedText( - text_, Qt::ElideRight, width() - HorizontalPadding * 2 - TextPadding - 2 * IconRadius); - - painter.setFont(font); - painter.setPen(QPen(textColor_)); - painter.drawText(QPoint(textStartX, textStartY), elidedText); - - // Draw the filesize. - font.setWeight(QFont::Normal); - painter.setFont(font); - painter.setPen(QPen(textColor_)); - painter.drawText(QPoint(textStartX, textStartY + 1.5 * fm.ascent()), readableFileSize_); -} diff --git a/src/timeline/widgets/AudioItem.h b/src/timeline/widgets/AudioItem.h deleted file mode 100644 index c32b773137a8310037045847ced825b4d9f7ebaa..0000000000000000000000000000000000000000 --- a/src/timeline/widgets/AudioItem.h +++ /dev/null @@ -1,104 +0,0 @@ -/* - * nheko Copyright (C) 2017 Konstantinos Sideris <siderisk@auth.gr> - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - */ - -#pragma once - -#include <QEvent> -#include <QIcon> -#include <QMediaPlayer> -#include <QMouseEvent> -#include <QSharedPointer> -#include <QWidget> - -#include <mtx.hpp> - -class AudioItem : public QWidget -{ - Q_OBJECT - - Q_PROPERTY(QColor textColor WRITE setTextColor READ textColor) - Q_PROPERTY(QColor iconColor WRITE setIconColor READ iconColor) - Q_PROPERTY(QColor backgroundColor WRITE setBackgroundColor READ backgroundColor) - - Q_PROPERTY(QColor durationBackgroundColor WRITE setDurationBackgroundColor READ - durationBackgroundColor) - Q_PROPERTY(QColor durationForegroundColor WRITE setDurationForegroundColor READ - durationForegroundColor) - -public: - AudioItem(const mtx::events::RoomEvent<mtx::events::msg::Audio> &event, - QWidget *parent = nullptr); - - AudioItem(const QString &url, - const QString &filename, - uint64_t size, - QWidget *parent = nullptr); - - QSize sizeHint() const override; - - void setTextColor(const QColor &color) { textColor_ = color; } - void setIconColor(const QColor &color) { iconColor_ = color; } - void setBackgroundColor(const QColor &color) { backgroundColor_ = color; } - - void setDurationBackgroundColor(const QColor &color) { durationBgColor_ = color; } - void setDurationForegroundColor(const QColor &color) { durationFgColor_ = color; } - - QColor textColor() const { return textColor_; } - QColor iconColor() const { return iconColor_; } - QColor backgroundColor() const { return backgroundColor_; } - - QColor durationBackgroundColor() const { return durationBgColor_; } - QColor durationForegroundColor() const { return durationFgColor_; } - -protected: - void paintEvent(QPaintEvent *event) override; - void resizeEvent(QResizeEvent *event) override; - void mousePressEvent(QMouseEvent *event) override; - -private slots: - void fileDownloaded(const QByteArray &data); - -private: - void init(); - - enum class AudioState - { - Play, - Pause, - }; - - AudioState state_ = AudioState::Play; - - QUrl url_; - QString text_; - QString readableFileSize_; - QString filenameToSave_; - - mtx::events::RoomEvent<mtx::events::msg::Audio> event_; - - QMediaPlayer *player_; - - QIcon playIcon_; - QIcon pauseIcon_; - - QColor textColor_ = QColor("white"); - QColor iconColor_ = QColor("#38A3D8"); - QColor backgroundColor_ = QColor("#333"); - - QColor durationBgColor_ = QColor("black"); - QColor durationFgColor_ = QColor("blue"); -}; diff --git a/src/timeline/widgets/FileItem.cpp b/src/timeline/widgets/FileItem.cpp deleted file mode 100644 index 1a555d1cf099f0f9a7f260b8182fe3eeeb953855..0000000000000000000000000000000000000000 --- a/src/timeline/widgets/FileItem.cpp +++ /dev/null @@ -1,221 +0,0 @@ -/* - * nheko Copyright (C) 2017 Konstantinos Sideris <siderisk@auth.gr> - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - */ - -#include <QBrush> -#include <QDesktopServices> -#include <QFile> -#include <QFileDialog> -#include <QPainter> -#include <QPixmap> -#include <QtGlobal> - -#include "Logging.h" -#include "MatrixClient.h" -#include "Utils.h" - -#include "timeline/widgets/FileItem.h" - -constexpr int MaxWidth = 400; -constexpr int Height = 70; -constexpr int IconRadius = 22; -constexpr int IconDiameter = IconRadius * 2; -constexpr int HorizontalPadding = 12; -constexpr int TextPadding = 15; -constexpr int DownloadIconRadius = IconRadius - 4; - -constexpr double VerticalPadding = Height - 2 * IconRadius; -constexpr double IconYCenter = Height / 2; -constexpr double IconXCenter = HorizontalPadding + IconRadius; - -void -FileItem::init() -{ - setMouseTracking(true); - setCursor(Qt::PointingHandCursor); - setAttribute(Qt::WA_Hover, true); - - icon_.addFile(":/icons/icons/ui/arrow-pointing-down.png"); - - setFixedHeight(Height); -} - -FileItem::FileItem(const mtx::events::RoomEvent<mtx::events::msg::File> &event, QWidget *parent) - : QWidget(parent) - , url_{QString::fromStdString(event.content.url)} - , text_{QString::fromStdString(event.content.body)} - , event_{event} -{ - readableFileSize_ = utils::humanReadableFileSize(event.content.info.size); - - init(); -} - -FileItem::FileItem(const QString &url, const QString &filename, uint64_t size, QWidget *parent) - : QWidget(parent) - , url_{url} - , text_{filename} -{ - readableFileSize_ = utils::humanReadableFileSize(size); - - init(); -} - -void -FileItem::openUrl() -{ - if (url_.toString().isEmpty()) - return; - - auto urlToOpen = utils::mxcToHttp( - url_, QString::fromStdString(http::client()->server()), http::client()->port()); - - if (!QDesktopServices::openUrl(urlToOpen)) - nhlog::ui()->warn("Could not open url: {}", urlToOpen.toStdString()); -} - -QSize -FileItem::sizeHint() const -{ - return QSize(MaxWidth, Height); -} - -void -FileItem::mousePressEvent(QMouseEvent *event) -{ - if (event->button() != Qt::LeftButton) - return; - - auto point = event->pos(); - - // Click on the download icon. - if (QRect(HorizontalPadding, VerticalPadding / 2, IconDiameter, IconDiameter) - .contains(point)) { - filenameToSave_ = QFileDialog::getSaveFileName(this, tr("Save File"), text_); - - if (filenameToSave_.isEmpty()) - return; - - auto proxy = std::make_shared<MediaProxy>(); - connect(proxy.get(), &MediaProxy::fileDownloaded, this, &FileItem::fileDownloaded); - - http::client()->download( - url_.toString().toStdString(), - [proxy = std::move(proxy), url = url_](const std::string &data, - const std::string &, - const std::string &, - mtx::http::RequestErr err) { - if (err) { - nhlog::ui()->warn("failed to retrieve m.file content: {}", - url.toString().toStdString()); - return; - } - - emit proxy->fileDownloaded(QByteArray(data.data(), data.size())); - }); - } else { - openUrl(); - } -} - -void -FileItem::fileDownloaded(const QByteArray &data) -{ - try { - QFile file(filenameToSave_); - - if (!file.open(QIODevice::WriteOnly)) - return; - - file.write(data); - file.close(); - } catch (const std::exception &e) { - nhlog::ui()->warn("Error while saving file to: {}", e.what()); - } -} - -void -FileItem::resizeEvent(QResizeEvent *event) -{ - QFont font; - font.setWeight(QFont::Medium); - - QFontMetrics fm(font); -#if QT_VERSION < QT_VERSION_CHECK(5, 11, 0) - const int computedWidth = std::min( - fm.width(text_) + 2 * IconRadius + VerticalPadding * 2 + TextPadding, (double)MaxWidth); -#else - const int computedWidth = - std::min(fm.horizontalAdvance(text_) + 2 * IconRadius + VerticalPadding * 2 + TextPadding, - (double)MaxWidth); -#endif - resize(computedWidth, Height); - - event->accept(); -} - -void -FileItem::paintEvent(QPaintEvent *event) -{ - Q_UNUSED(event); - - QPainter painter(this); - painter.setRenderHint(QPainter::Antialiasing); - - QFont font; - font.setWeight(QFont::Medium); - - QFontMetrics fm(font); - - QPainterPath path; - path.addRoundedRect(QRectF(0, 0, width(), height()), 10, 10); - - painter.setPen(Qt::NoPen); - painter.fillPath(path, backgroundColor_); - painter.drawPath(path); - - QPainterPath circle; - circle.addEllipse(QPoint(IconXCenter, IconYCenter), IconRadius, IconRadius); - - painter.setPen(Qt::NoPen); - painter.fillPath(circle, iconColor_); - painter.drawPath(circle); - - icon_.paint(&painter, - QRect(IconXCenter - DownloadIconRadius / 2, - IconYCenter - DownloadIconRadius / 2, - DownloadIconRadius, - DownloadIconRadius), - Qt::AlignCenter, - QIcon::Normal); - - const int textStartX = HorizontalPadding + 2 * IconRadius + TextPadding; - const int textStartY = VerticalPadding + fm.ascent() / 2; - - // Draw the filename. - QString elidedText = fm.elidedText( - text_, Qt::ElideRight, width() - HorizontalPadding * 2 - TextPadding - 2 * IconRadius); - - painter.setFont(font); - painter.setPen(QPen(textColor_)); - painter.drawText(QPoint(textStartX, textStartY), elidedText); - - // Draw the filesize. - font.setWeight(QFont::Normal); - painter.setFont(font); - painter.setPen(QPen(textColor_)); - painter.drawText(QPoint(textStartX, textStartY + 1.5 * fm.ascent()), readableFileSize_); -} diff --git a/src/timeline/widgets/FileItem.h b/src/timeline/widgets/FileItem.h deleted file mode 100644 index d63cce8895c5c7f0ca32c299f1d07947a01dbad3..0000000000000000000000000000000000000000 --- a/src/timeline/widgets/FileItem.h +++ /dev/null @@ -1,79 +0,0 @@ -/* - * nheko Copyright (C) 2017 Konstantinos Sideris <siderisk@auth.gr> - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - */ - -#pragma once - -#include <QEvent> -#include <QIcon> -#include <QMouseEvent> -#include <QSharedPointer> -#include <QWidget> - -#include <mtx.hpp> - -class FileItem : public QWidget -{ - Q_OBJECT - - Q_PROPERTY(QColor textColor WRITE setTextColor READ textColor) - Q_PROPERTY(QColor iconColor WRITE setIconColor READ iconColor) - Q_PROPERTY(QColor backgroundColor WRITE setBackgroundColor READ backgroundColor) - -public: - FileItem(const mtx::events::RoomEvent<mtx::events::msg::File> &event, - QWidget *parent = nullptr); - - FileItem(const QString &url, - const QString &filename, - uint64_t size, - QWidget *parent = nullptr); - - QSize sizeHint() const override; - - void setTextColor(const QColor &color) { textColor_ = color; } - void setIconColor(const QColor &color) { iconColor_ = color; } - void setBackgroundColor(const QColor &color) { backgroundColor_ = color; } - - QColor textColor() const { return textColor_; } - QColor iconColor() const { return iconColor_; } - QColor backgroundColor() const { return backgroundColor_; } - -protected: - void paintEvent(QPaintEvent *event) override; - void mousePressEvent(QMouseEvent *event) override; - void resizeEvent(QResizeEvent *event) override; - -private slots: - void fileDownloaded(const QByteArray &data); - -private: - void openUrl(); - void init(); - - QUrl url_; - QString text_; - QString readableFileSize_; - QString filenameToSave_; - - mtx::events::RoomEvent<mtx::events::msg::File> event_; - - QIcon icon_; - - QColor textColor_ = QColor("white"); - QColor iconColor_ = QColor("#38A3D8"); - QColor backgroundColor_ = QColor("#333"); -}; diff --git a/src/timeline/widgets/ImageItem.cpp b/src/timeline/widgets/ImageItem.cpp deleted file mode 100644 index 26c569d7e8dce0ec87eb39951392dfe2e824ec35..0000000000000000000000000000000000000000 --- a/src/timeline/widgets/ImageItem.cpp +++ /dev/null @@ -1,267 +0,0 @@ -/* - * nheko Copyright (C) 2017 Konstantinos Sideris <siderisk@auth.gr> - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - */ - -#include <QBrush> -#include <QDesktopServices> -#include <QFileDialog> -#include <QFileInfo> -#include <QPainter> -#include <QPixmap> -#include <QUuid> -#include <QtGlobal> - -#include "Config.h" -#include "ImageItem.h" -#include "Logging.h" -#include "MatrixClient.h" -#include "Utils.h" -#include "dialogs/ImageOverlay.h" - -void -ImageItem::downloadMedia(const QUrl &url) -{ - auto proxy = std::make_shared<MediaProxy>(); - connect(proxy.get(), &MediaProxy::imageDownloaded, this, &ImageItem::setImage); - - http::client()->download(url.toString().toStdString(), - [proxy = std::move(proxy), url](const std::string &data, - const std::string &, - const std::string &, - mtx::http::RequestErr err) { - if (err) { - nhlog::net()->warn( - "failed to retrieve image {}: {} {}", - url.toString().toStdString(), - err->matrix_error.error, - static_cast<int>(err->status_code)); - return; - } - - QPixmap img; - img.loadFromData(QByteArray(data.data(), data.size())); - - emit proxy->imageDownloaded(img); - }); -} - -void -ImageItem::saveImage(const QString &filename, const QByteArray &data) -{ - try { - QFile file(filename); - - if (!file.open(QIODevice::WriteOnly)) - return; - - file.write(data); - file.close(); - } catch (const std::exception &e) { - nhlog::ui()->warn("Error while saving file to: {}", e.what()); - } -} - -void -ImageItem::init() -{ - setMouseTracking(true); - setCursor(Qt::PointingHandCursor); - setAttribute(Qt::WA_Hover, true); - - downloadMedia(url_); -} - -ImageItem::ImageItem(const mtx::events::RoomEvent<mtx::events::msg::Image> &event, QWidget *parent) - : QWidget(parent) - , event_{event} -{ - url_ = QString::fromStdString(event.content.url); - text_ = QString::fromStdString(event.content.body); - - init(); -} - -ImageItem::ImageItem(const QString &url, const QString &filename, uint64_t size, QWidget *parent) - : QWidget(parent) - , url_{url} - , text_{filename} -{ - Q_UNUSED(size); - init(); -} - -void -ImageItem::openUrl() -{ - if (url_.toString().isEmpty()) - return; - - auto urlToOpen = utils::mxcToHttp( - url_, QString::fromStdString(http::client()->server()), http::client()->port()); - - if (!QDesktopServices::openUrl(urlToOpen)) - nhlog::ui()->warn("could not open url: {}", urlToOpen.toStdString()); -} - -QSize -ImageItem::sizeHint() const -{ - if (image_.isNull()) - return QSize(max_width_, bottom_height_); - - return QSize(width_, height_); -} - -void -ImageItem::setImage(const QPixmap &image) -{ - image_ = image; - scaled_image_ = utils::scaleDown(max_width_, max_height_, image_); - - width_ = scaled_image_.width(); - height_ = scaled_image_.height(); - - setFixedSize(width_, height_); - update(); -} - -void -ImageItem::mousePressEvent(QMouseEvent *event) -{ - if (!isInteractive_) { - event->accept(); - return; - } - - if (event->button() != Qt::LeftButton) - return; - - if (image_.isNull()) { - openUrl(); - return; - } - - if (textRegion_.contains(event->pos())) { - openUrl(); - } else { - auto imgDialog = new dialogs::ImageOverlay(image_); - imgDialog->show(); - connect(imgDialog, &dialogs::ImageOverlay::saving, this, &ImageItem::saveAs); - } -} - -void -ImageItem::resizeEvent(QResizeEvent *event) -{ - if (!image_) - return QWidget::resizeEvent(event); - - scaled_image_ = utils::scaleDown(max_width_, max_height_, image_); - - width_ = scaled_image_.width(); - height_ = scaled_image_.height(); - - setFixedSize(width_, height_); -} - -void -ImageItem::paintEvent(QPaintEvent *event) -{ - Q_UNUSED(event); - - QPainter painter(this); - painter.setRenderHint(QPainter::Antialiasing); - - QFont font; - - QFontMetrics metrics(font); - const int fontHeight = metrics.height() + metrics.ascent(); - - if (image_.isNull()) { - QString elidedText = metrics.elidedText(text_, Qt::ElideRight, max_width_ - 10); -#if QT_VERSION < QT_VERSION_CHECK(5, 11, 0) - setFixedSize(metrics.width(elidedText), fontHeight); -#else - setFixedSize(metrics.horizontalAdvance(elidedText), fontHeight); -#endif - painter.setFont(font); - painter.setPen(QPen(QColor(66, 133, 244))); - painter.drawText(QPoint(0, fontHeight / 2), elidedText); - - return; - } - - imageRegion_ = QRectF(0, 0, width_, height_); - - QPainterPath path; - path.addRoundedRect(imageRegion_, 5, 5); - - painter.setPen(Qt::NoPen); - painter.fillPath(path, scaled_image_); - painter.drawPath(path); - - // Bottom text section - if (isInteractive_ && underMouse()) { - const int textBoxHeight = fontHeight / 2 + 6; - - textRegion_ = QRectF(0, height_ - textBoxHeight, width_, textBoxHeight); - - QPainterPath textPath; - textPath.addRoundedRect(textRegion_, 0, 0); - - painter.fillPath(textPath, QColor(40, 40, 40, 140)); - - QString elidedText = metrics.elidedText(text_, Qt::ElideRight, width_ - 10); - - font.setWeight(QFont::Medium); - painter.setFont(font); - painter.setPen(QPen(QColor(Qt::white))); - - textRegion_.adjust(5, 0, 5, 0); - painter.drawText(textRegion_, Qt::AlignVCenter, elidedText); - } -} - -void -ImageItem::saveAs() -{ - auto filename = QFileDialog::getSaveFileName(this, tr("Save image"), text_); - - if (filename.isEmpty()) - return; - - const auto url = url_.toString().toStdString(); - - auto proxy = std::make_shared<MediaProxy>(); - connect(proxy.get(), &MediaProxy::imageSaved, this, &ImageItem::saveImage); - - http::client()->download( - url, - [proxy = std::move(proxy), filename, url](const std::string &data, - const std::string &, - const std::string &, - mtx::http::RequestErr err) { - if (err) { - nhlog::net()->warn("failed to retrieve image {}: {} {}", - url, - err->matrix_error.error, - static_cast<int>(err->status_code)); - return; - } - - emit proxy->imageSaved(filename, QByteArray(data.data(), data.size())); - }); -} diff --git a/src/timeline/widgets/ImageItem.h b/src/timeline/widgets/ImageItem.h deleted file mode 100644 index 65bd962de528bbb4b9f2a4302eb01a9542687c15..0000000000000000000000000000000000000000 --- a/src/timeline/widgets/ImageItem.h +++ /dev/null @@ -1,104 +0,0 @@ -/* - * nheko Copyright (C) 2017 Konstantinos Sideris <siderisk@auth.gr> - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - */ - -#pragma once - -#include <QEvent> -#include <QMouseEvent> -#include <QSharedPointer> -#include <QWidget> - -#include <mtx.hpp> - -namespace dialogs { -class ImageOverlay; -} - -class ImageItem : public QWidget -{ - Q_OBJECT -public: - ImageItem(const mtx::events::RoomEvent<mtx::events::msg::Image> &event, - QWidget *parent = nullptr); - - ImageItem(const QString &url, - const QString &filename, - uint64_t size, - QWidget *parent = nullptr); - - QSize sizeHint() const override; - -public slots: - //! Show a save as dialog for the image. - void saveAs(); - void setImage(const QPixmap &image); - void saveImage(const QString &filename, const QByteArray &data); - -protected: - void paintEvent(QPaintEvent *event) override; - void mousePressEvent(QMouseEvent *event) override; - void resizeEvent(QResizeEvent *event) override; - - //! Whether the user can interact with the displayed image. - bool isInteractive_ = true; - -private: - void init(); - void openUrl(); - void downloadMedia(const QUrl &url); - - int max_width_ = 500; - int max_height_ = 300; - - int width_; - int height_; - - QPixmap scaled_image_; - QPixmap image_; - - QUrl url_; - QString text_; - - int bottom_height_ = 30; - - QRectF textRegion_; - QRectF imageRegion_; - - mtx::events::RoomEvent<mtx::events::msg::Image> event_; -}; - -class StickerItem : public ImageItem -{ - Q_OBJECT - -public: - StickerItem(const mtx::events::Sticker &event, QWidget *parent = nullptr) - : ImageItem{QString::fromStdString(event.content.url), - QString::fromStdString(event.content.body), - event.content.info.size, - parent} - , event_{event} - { - isInteractive_ = false; - setCursor(Qt::ArrowCursor); - setMouseTracking(false); - setAttribute(Qt::WA_Hover, false); - } - -private: - mtx::events::Sticker event_; -}; diff --git a/src/timeline/widgets/VideoItem.cpp b/src/timeline/widgets/VideoItem.cpp deleted file mode 100644 index 4b5dc02274b52110b1711cd437c2610554d18534..0000000000000000000000000000000000000000 --- a/src/timeline/widgets/VideoItem.cpp +++ /dev/null @@ -1,65 +0,0 @@ -/* - * nheko Copyright (C) 2017 Konstantinos Sideris <siderisk@auth.gr> - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - */ - -#include <QLabel> -#include <QVBoxLayout> - -#include "Config.h" -#include "MatrixClient.h" -#include "Utils.h" -#include "timeline/widgets/VideoItem.h" - -void -VideoItem::init() -{ - url_ = utils::mxcToHttp( - url_, QString::fromStdString(http::client()->server()), http::client()->port()); -} - -VideoItem::VideoItem(const mtx::events::RoomEvent<mtx::events::msg::Video> &event, QWidget *parent) - : QWidget(parent) - , url_{QString::fromStdString(event.content.url)} - , text_{QString::fromStdString(event.content.body)} - , event_{event} -{ - readableFileSize_ = utils::humanReadableFileSize(event.content.info.size); - - init(); - - auto layout = new QVBoxLayout(this); - layout->setMargin(0); - layout->setSpacing(0); - - QString link = QString("<a href=%1>%2</a>").arg(url_.toString()).arg(text_); - - label_ = new QLabel(link, this); - label_->setMargin(0); - label_->setTextInteractionFlags(Qt::TextSelectableByMouse | Qt::TextBrowserInteraction); - label_->setOpenExternalLinks(true); - - layout->addWidget(label_); -} - -VideoItem::VideoItem(const QString &url, const QString &filename, uint64_t size, QWidget *parent) - : QWidget(parent) - , url_{url} - , text_{filename} -{ - readableFileSize_ = utils::humanReadableFileSize(size); - - init(); -} diff --git a/src/timeline/widgets/VideoItem.h b/src/timeline/widgets/VideoItem.h deleted file mode 100644 index 26fa1c35ba6e152f34f26001041add81d3342862..0000000000000000000000000000000000000000 --- a/src/timeline/widgets/VideoItem.h +++ /dev/null @@ -1,51 +0,0 @@ -/* - * nheko Copyright (C) 2017 Konstantinos Sideris <siderisk@auth.gr> - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - */ - -#pragma once - -#include <QEvent> -#include <QLabel> -#include <QSharedPointer> -#include <QUrl> -#include <QWidget> - -#include <mtx.hpp> - -class VideoItem : public QWidget -{ - Q_OBJECT - -public: - VideoItem(const mtx::events::RoomEvent<mtx::events::msg::Video> &event, - QWidget *parent = nullptr); - - VideoItem(const QString &url, - const QString &filename, - uint64_t size, - QWidget *parent = nullptr); - -private: - void init(); - - QUrl url_; - QString text_; - QString readableFileSize_; - - QLabel *label_; - - mtx::events::RoomEvent<mtx::events::msg::Video> event_; -}; diff --git a/src/ui/Avatar.cpp b/src/ui/Avatar.cpp index 501a8968bd37a7ad78eafc35d2591840a57f066b..e4a90f81d9b7c36a4379f5b0d72929072bf31e03 100644 --- a/src/ui/Avatar.cpp +++ b/src/ui/Avatar.cpp @@ -101,7 +101,7 @@ Avatar::setIcon(const QIcon &icon) void Avatar::paintEvent(QPaintEvent *) { - bool rounded = QSettings().value("user/avatar/circles", true).toBool(); + bool rounded = QSettings().value("user/avatar_circles", true).toBool(); QPainter painter(this); painter.setRenderHint(QPainter::Antialiasing);