Merge branch '0.7.0-dev' of ssh://github.com/Nheko-Reborn/nheko into 0.7.0-dev

This commit is contained in:
Joseph Donofry 2019-12-07 21:39:47 -05:00
commit e79ae4ea09
No known key found for this signature in database
GPG Key ID: E8A1D78EF044B0CB
82 changed files with 5502 additions and 6557 deletions

View File

@ -11,5 +11,7 @@ FILES=$(find src -type f -type f \( -iname "*.cpp" -o -iname "*.h" \))
for f in $FILES for f in $FILES
do do
clang-format -i "$f" && git diff --exit-code clang-format -i "$f"
done; done;
git diff --exit-code

View File

@ -31,8 +31,8 @@ if [ "$TRAVIS_OS_NAME" = "linux" ]; then
QT_PKG="59" QT_PKG="59"
fi fi
wget https://cmake.org/files/v3.12/cmake-3.12.2-Linux-x86_64.sh wget https://cmake.org/files/v3.15/cmake-3.15.5-Linux-x86_64.sh
sudo sh cmake-3.12.2-Linux-x86_64.sh --skip-license --prefix=/usr/local sudo sh cmake-3.15.5-Linux-x86_64.sh --skip-license --prefix=/usr/local
mkdir -p build-libsodium mkdir -p build-libsodium
( cd build-libsodium ( cd build-libsodium
@ -54,5 +54,7 @@ if [ "$TRAVIS_OS_NAME" = "linux" ]; then
qt${QT_PKG}tools \ qt${QT_PKG}tools \
qt${QT_PKG}svg \ qt${QT_PKG}svg \
qt${QT_PKG}multimedia \ qt${QT_PKG}multimedia \
qt${QT_PKG}quickcontrols2 \
qt${QT_PKG}graphicaleffects \
liblmdb-dev liblmdb-dev
fi fi

View File

@ -44,8 +44,7 @@ do
linuxdeployqt=$res linuxdeployqt=$res
done 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 -bundle-non-qt-libs -qmldir=./resources/qml -appimage
./"$linuxdeployqt" ${DIR}/usr/share/applications/*.desktop -unsupported-allow-new-glibc -appimage
chmod +x nheko-*x86_64.AppImage chmod +x nheko-*x86_64.AppImage

View File

@ -16,7 +16,7 @@ PATH=/usr/local/opt/qt/bin/:${PATH}
mkdir -p nheko.app/Contents/Frameworks mkdir -p nheko.app/Contents/Frameworks
find "${ICU_LIB}" -type l -name "*.dylib" -exec cp -a -n {} nheko.app/Contents/Frameworks/ \; || true 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) user=$(id -nu)
sudo chown "${user}" nheko.dmg sudo chown "${user}" nheko.dmg

View File

@ -13,6 +13,9 @@ if [ "$TRAVIS_OS_NAME" = "linux" ]; then
sudo update-alternatives --set gcc "/usr/bin/${C_COMPILER}" sudo update-alternatives --set gcc "/usr/bin/${C_COMPILER}"
sudo update-alternatives --set g++ "/usr/bin/${CXX_COMPILER}" sudo update-alternatives --set g++ "/usr/bin/${CXX_COMPILER}"
export PATH="/usr/local/bin/:${PATH}"
cmake --version
fi fi
if [ "$TRAVIS_OS_NAME" = "linux" ]; then if [ "$TRAVIS_OS_NAME" = "linux" ]; then
@ -35,7 +38,8 @@ cmake --build .deps
# Build nheko # Build nheko
cmake -GNinja -H. -Bbuild \ cmake -GNinja -H. -Bbuild \
-DCMAKE_BUILD_TYPE=RelWithDebInfo \ -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 cmake --build build
if [ "$TRAVIS_OS_NAME" = "osx" ]; then if [ "$TRAVIS_OS_NAME" = "osx" ]; then

9
.gitignore vendored
View File

@ -1,8 +1,11 @@
build /build*
tags tags
cscope* cscope*
.clang_complete .clang_complete
*wintoastlib* *wintoastlib*
/.ccls-cache
/.exrc
.gdb_history
# GTAGS # GTAGS
GTAGS GTAGS
@ -49,6 +52,10 @@ ui_*.h
*.qmlproject.user *.qmlproject.user
*.qmlproject.user.* *.qmlproject.user.*
# Vim
*.swp
*.swo
#####=== CMake ===##### #####=== CMake ===#####
CMakeCache.txt CMakeCache.txt

View File

@ -15,7 +15,8 @@ matrix:
include: include:
- os: osx - os: osx
compiler: clang 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: env:
- DEPLOYMENT=1 - DEPLOYMENT=1
- USE_BUNDLED_BOOST=0 - USE_BUNDLED_BOOST=0
@ -42,8 +43,8 @@ matrix:
env: env:
- CXX_COMPILER=g++-8 - CXX_COMPILER=g++-8
- C_COMPILER=gcc-8 - C_COMPILER=gcc-8
- QT_VERSION=571 - QT_VERSION=592
- QT_PKG=57 - QT_PKG=59
- USE_BUNDLED_BOOST=1 - USE_BUNDLED_BOOST=1
- USE_BUNDLED_CMARK=1 - USE_BUNDLED_CMARK=1
- USE_BUNDLED_JSON=1 - USE_BUNDLED_JSON=1

View File

@ -69,7 +69,8 @@ include(LMDB)
# #
# Discover Qt dependencies. # 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) find_package(Qt5DBus)
if (APPLE) if (APPLE)
@ -192,12 +193,8 @@ set(SRC_FILES
# Timeline # Timeline
src/timeline/TimelineViewManager.cpp src/timeline/TimelineViewManager.cpp
src/timeline/TimelineItem.cpp src/timeline/TimelineModel.cpp
src/timeline/TimelineView.cpp src/timeline/DelegateChooser.cpp
src/timeline/widgets/AudioItem.cpp
src/timeline/widgets/FileItem.cpp
src/timeline/widgets/ImageItem.cpp
src/timeline/widgets/VideoItem.cpp
# UI components # UI components
src/ui/Avatar.cpp src/ui/Avatar.cpp
@ -229,6 +226,8 @@ set(SRC_FILES
src/Logging.cpp src/Logging.cpp
src/MainWindow.cpp src/MainWindow.cpp
src/MatrixClient.cpp src/MatrixClient.cpp
src/MxcImageProvider.cpp
src/ColorImageProvider.cpp
src/QuickSwitcher.cpp src/QuickSwitcher.cpp
src/Olm.cpp src/Olm.cpp
src/RegisterPage.cpp src/RegisterPage.cpp
@ -260,7 +259,7 @@ include(FeatureSummary)
set(Boost_USE_STATIC_LIBS OFF) set(Boost_USE_STATIC_LIBS OFF)
set(Boost_USE_STATIC_RUNTIME OFF) set(Boost_USE_STATIC_RUNTIME OFF)
set(Boost_USE_MULTITHREADED ON) set(Boost_USE_MULTITHREADED ON)
find_package(Boost 1.66 REQUIRED find_package(Boost 1.70 REQUIRED
COMPONENTS atomic COMPONENTS atomic
chrono chrono
date_time date_time
@ -333,13 +332,9 @@ qt5_wrap_cpp(MOC_HEADERS
src/emoji/PickButton.h src/emoji/PickButton.h
# Timeline # Timeline
src/timeline/TimelineItem.h
src/timeline/TimelineView.h
src/timeline/TimelineViewManager.h src/timeline/TimelineViewManager.h
src/timeline/widgets/AudioItem.h src/timeline/TimelineModel.h
src/timeline/widgets/FileItem.h src/timeline/DelegateChooser.h
src/timeline/widgets/ImageItem.h
src/timeline/widgets/VideoItem.h
# UI components # UI components
src/ui/Avatar.h src/ui/Avatar.h
@ -370,7 +365,7 @@ qt5_wrap_cpp(MOC_HEADERS
src/CommunitiesList.h src/CommunitiesList.h
src/LoginPage.h src/LoginPage.h
src/MainWindow.h src/MainWindow.h
src/MatrixClient.h src/MxcImageProvider.h
src/InviteeItem.h src/InviteeItem.h
src/QuickSwitcher.h src/QuickSwitcher.h
src/RegisterPage.h src/RegisterPage.h
@ -405,6 +400,9 @@ set(COMMON_LIBS
Qt5::Svg Qt5::Svg
Qt5::Concurrent Qt5::Concurrent
Qt5::Multimedia Qt5::Multimedia
Qt5::Qml
Qt5::QuickControls2
Qt5::QuickWidgets
nlohmann_json::nlohmann_json) nlohmann_json::nlohmann_json)
if(APPVEYOR_BUILD) if(APPVEYOR_BUILD)
@ -448,6 +446,7 @@ if(APPLE)
target_link_libraries (nheko ${NHEKO_LIBS} Qt5::MacExtras) target_link_libraries (nheko ${NHEKO_LIBS} Qt5::MacExtras)
elseif(WIN32) elseif(WIN32)
add_executable (nheko ${OS_BUNDLE} ${ICON_FILE} ${NHEKO_DEPS}) 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) target_link_libraries (nheko ${NTDLIB} ${NHEKO_LIBS} Qt5::WinMain)
else() else()
add_executable (nheko ${OS_BUNDLE} ${NHEKO_DEPS}) add_executable (nheko ${OS_BUNDLE} ${NHEKO_DEPS})

View File

@ -7,7 +7,7 @@ RUN \
add-apt-repository -y ppa:ubuntu-toolchain-r/test && \ add-apt-repository -y ppa:ubuntu-toolchain-r/test && \
apt-get update -qq && \ apt-get update -qq && \
apt-get install -y \ apt-get install -y \
qt510base qt510tools qt510svg qt510multimedia \ qt510base qt510tools qt510svg qt510multimedia qt510quickcontrols2 qt510graphicaleffects \
gcc-5 g++-5 gcc-5 g++-5
RUN \ RUN \

View File

@ -68,7 +68,7 @@ update-translations:
-locations relative \ -locations relative \
-Iinclude/dialogs \ -Iinclude/dialogs \
-Iinclude \ -Iinclude \
src/ -ts resources/langs/nheko_*.ts -no-obsolete src/ resources/qml/ -ts resources/langs/nheko_*.ts -no-obsolete
clean: clean:
rm -rf build rm -rf build

View File

@ -11,6 +11,11 @@ nheko
The motivation behind the project is to provide a native desktop app for [Matrix] that 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. feels more like a mainstream chat app ([Riot], Telegram etc) and less like an IRC client.
### Translations ###
[![Translation status](http://weblate.nheko.im/widgets/nheko/-/nheko-master/svg-badge.svg)](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 ### Note regarding End-to-End encryption
Currently the implementation is at best a **proof of concept** and it should only be used for 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 ### Build Requirements
- Qt5 (5.7 or greater). Qt 5.7 adds support for color font rendering with - Qt5 (5.9 or greater). Qt 5.7 adds support for color font rendering with
Freetype, which is essential to properly support emoji. Freetype, which is essential to properly support emoji, 5.8 adds some features
- CMake 3.1 or greater. 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) - [mtxclient](https://github.com/Nheko-Reborn/mtxclient)
- [LMDB](https://symas.com/lightning-memory-mapped-database/) - [LMDB](https://symas.com/lightning-memory-mapped-database/)
- [cmark](https://github.com/commonmark/cmark) - [cmark](https://github.com/commonmark/cmark)
- Boost 1.66 or greater. - Boost 1.70 or greater.
- [libolm](https://git.matrix.org/git/olm) - [libolm](https://git.matrix.org/git/olm)
- [libsodium](https://github.com/jedisct1/libsodium) - [libsodium](https://github.com/jedisct1/libsodium)
- [spdlog](https://github.com/gabime/spdlog) - [spdlog](https://github.com/gabime/spdlog)
@ -126,7 +132,7 @@ sudo pacman -S qt5-base \
##### Gentoo Linux ##### Gentoo Linux
```bash ```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) ##### Ubuntu (e.g 14.04)

View File

@ -34,6 +34,7 @@ install:
lmdb:%PLATFORM%-windows lmdb:%PLATFORM%-windows
openssl:%PLATFORM%-windows openssl:%PLATFORM%-windows
zlib:%PLATFORM%-windows zlib:%PLATFORM%-windows
- vcpkg upgrade --no-dry-run
build_script: build_script:
# VERSION format: branch-master/branch-1.2 # VERSION format: branch-master/branch-1.2

View File

@ -21,4 +21,8 @@ if(NOT EXISTS ${_qrc})
endif() endif()
qt5_add_resources(LANG_QRC ${_qrc}) 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()

12
deps/CMakeLists.txt vendored
View File

@ -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) option(MTX_STATIC "Compile / link bundled mtx client statically" OFF)
if(USE_BUNDLED_BOOST) if(USE_BUNDLED_BOOST)
# bundled boost is 1.68, which requires CMake 3.12 or greater. # bundled boost is 1.70, which requires CMake 3.15 or greater.
cmake_minimum_required(VERSION 3.12) cmake_minimum_required(VERSION 3.15)
endif() endif()
include(ExternalProject) include(ExternalProject)
set(BOOST_URL 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 set(BOOST_SHA256
8f32d4617390d1c2d16f26a27ab60d97807b35440d45891fa340fc2648b04406) 430ae8354789de4fd19ee52f3b1f739e1fba576f0aded0897c3c2bc00fb38778)
set( set(
MTXCLIENT_URL MTXCLIENT_URL
https://github.com/Nheko-Reborn/mtxclient/archive/6eee767cc25a9db9f125843e584656cde1ebb6c5.tar.gz https://github.com/Nheko-Reborn/mtxclient/archive/64182a84e35378113f7d3a80f3073894416480e7.zip
) )
set(MTXCLIENT_HASH set(MTXCLIENT_HASH
72fe77da4fed98b3cf069299f66092c820c900359a27ec26070175f9ad208a03) c9973501920046f04c72983472451736343d00e7a40f4d4a12181191093a5fab)
set( set(
TWEENY_URL TWEENY_URL
https://github.com/mobius3/tweeny/archive/b94ce07cfb02a0eb8ac8aaf66137dabdaea857cf.tar.gz https://github.com/mobius3/tweeny/archive/b94ce07cfb02a0eb8ac8aaf66137dabdaea857cf.tar.gz

View File

@ -3,6 +3,10 @@ if(WIN32)
return() return()
endif() endif()
include(BoostToolsetId)
set(BOOST_TOOLSET "gcc")
Boost_Get_ToolsetId(BOOST_TOOLSET)
ExternalProject_Add( ExternalProject_Add(
Boost Boost
@ -16,6 +20,7 @@ ExternalProject_Add(
CONFIGURE_COMMAND ${DEPS_BUILD_DIR}/boost/bootstrap.sh CONFIGURE_COMMAND ${DEPS_BUILD_DIR}/boost/bootstrap.sh
--with-libraries=random,thread,system,iostreams,atomic,chrono,date_time,regex --with-libraries=random,thread,system,iostreams,atomic,chrono,date_time,regex
--prefix=${DEPS_INSTALL_DIR} --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 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 INSTALL_COMMAND ${DEPS_BUILD_DIR}/boost/b2 -d0 install
) )

35
deps/cmake/BoostToolsetId.cmake vendored Normal file
View File

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

View File

@ -1,38 +1,15 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE TS> <!DOCTYPE TS>
<TS version="2.1" language="de"> <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> <context>
<name>ChatPage</name> <name>ChatPage</name>
<message> <message>
<location filename="../../src/ChatPage.cpp" line="+330"/> <location filename="../../src/ChatPage.cpp" line="+346"/>
<source>Failed to upload image. Please try again.</source> <source>Failed to upload media. Please try again.</source>
<translation>Hochladen des Bildes fehlgeschlagen. Bitte versuche es erneut.</translation> <translation>Medienupload fehlgeschlagen. Bitte versuche es erneut.</translation>
</message> </message>
<message> <message>
<location line="+45"/> <location line="+389"/>
<source>Failed to upload file. Please try again.</source>
<translation>Hochladen der Datei 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"/>
<source>Failed to restore OLM account. Please login again.</source> <source>Failed to restore OLM account. Please login again.</source>
<translation>Wiederherstellung des OLM Accounts fehlgeschlagen. Bitte logge dich erneut ein.</translation> <translation>Wiederherstellung des OLM Accounts fehlgeschlagen. Bitte logge dich erneut ein.</translation>
</message> </message>
@ -42,18 +19,18 @@
<translation>Gespeicherte Nachrichten konnten nicht wiederhergestellt werden. Bitte melde Dich erneut an.</translation> <translation>Gespeicherte Nachrichten konnten nicht wiederhergestellt werden. Bitte melde Dich erneut an.</translation>
</message> </message>
<message> <message>
<location line="+198"/> <location line="+181"/>
<source>Failed to setup encryption keys. Server response: %1 %2. Please try again later.</source> <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> <translation>Fehler beim Setup der Verschlüsselungsschlüssel. Servermeldung: %1 %2. Bitte versuche es später erneut.</translation>
</message> </message>
<message> <message>
<location line="+51"/> <location line="+51"/>
<location line="+153"/> <location line="+155"/>
<source>Please try to login again: %1</source> <source>Please try to login again: %1</source>
<translation>Bitte melde dich erneut an: %1</translation> <translation>Bitte melde dich erneut an: %1</translation>
</message> </message>
<message> <message>
<location line="-45"/> <location line="-47"/>
<source>Room creation failed: %1</source> <source>Room creation failed: %1</source>
<translation>Raum konnte nicht erstellt werden: %1</translation> <translation>Raum konnte nicht erstellt werden: %1</translation>
</message> </message>
@ -116,19 +93,11 @@
</message> </message>
</context> </context>
<context> <context>
<name>FileItem</name> <name>EncryptionIndicator</name>
<message> <message>
<location filename="../../src/timeline/widgets/FileItem.cpp" line="+107"/> <location filename="../qml/EncryptionIndicator.qml" line="+11"/>
<source>Save File</source> <source>Encrypted</source>
<translation>Datei speichern</translation> <translation>Verschlüsselt</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>
</message> </message>
</context> </context>
<context> <context>
@ -200,7 +169,7 @@
<context> <context>
<name>MemberList</name> <name>MemberList</name>
<message> <message>
<location filename="../../src/dialogs/MemberList.cpp" line="+96"/> <location filename="../../src/dialogs/MemberList.cpp" line="+89"/>
<source>Room members</source> <source>Room members</source>
<translation>Teilnehmerliste</translation> <translation>Teilnehmerliste</translation>
</message> </message>
@ -210,6 +179,27 @@
<translation>OK</translation> <translation>OK</translation>
</message> </message>
</context> </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> <context>
<name>QuickSwitcher</name> <name>QuickSwitcher</name>
<message> <message>
@ -277,7 +267,7 @@
<context> <context>
<name>RoomInfo</name> <name>RoomInfo</name>
<message> <message>
<location filename="../../src/Cache.cpp" line="+2205"/> <location filename="../../src/Cache.cpp" line="+2307"/>
<source>no version stored</source> <source>no version stored</source>
<translation>keine Version gespeichert</translation> <translation>keine Version gespeichert</translation>
</message> </message>
@ -285,12 +275,12 @@
<context> <context>
<name>RoomInfoListItem</name> <name>RoomInfoListItem</name>
<message> <message>
<location filename="../../src/RoomInfoListItem.cpp" line="+93"/> <location filename="../../src/RoomInfoListItem.cpp" line="+95"/>
<source>Leave room</source> <source>Leave room</source>
<translation>Raum verlassen</translation> <translation>Raum verlassen</translation>
</message> </message>
<message> <message>
<location line="+181"/> <location line="+161"/>
<source>Accept</source> <source>Accept</source>
<translation>Akzeptieren</translation> <translation>Akzeptieren</translation>
</message> </message>
@ -331,36 +321,36 @@
<context> <context>
<name>StatusIndicator</name> <name>StatusIndicator</name>
<message> <message>
<location filename="../../src/timeline/TimelineItem.cpp" line="+107"/> <location filename="../qml/StatusIndicator.qml" line="+13"/>
<source>Encrypted</source> <source>Failed</source>
<translation>Verschlüsselt</translation> <translation>Fehlgeschlagen</translation>
</message> </message>
<message> <message>
<location line="+3"/> <location line="+1"/>
<source>Delivered</source>
<translation>Erhalten</translation>
</message>
<message>
<location line="+3"/>
<source>Seen</source>
<translation>Gelesen</translation>
</message>
<message>
<location line="+3"/>
<source>Sent</source> <source>Sent</source>
<translation>Gesendet</translation> <translation>Gesendet</translation>
</message> </message>
<message>
<location line="+1"/>
<source>Received</source>
<translation>Empfangen</translation>
</message>
<message>
<location line="+1"/>
<source>Read</source>
<translation>Gelesen</translation>
</message>
</context> </context>
<context> <context>
<name>TextInputWidget</name> <name>TextInputWidget</name>
<message> <message>
<location filename="../../src/TextInputWidget.cpp" line="+507"/> <location filename="../../src/TextInputWidget.cpp" line="+502"/>
<source>Send a file</source> <source>Send a file</source>
<translation>Versende Datei</translation> <translation>Versende Datei</translation>
</message> </message>
<message> <message>
<location line="+13"/> <location line="+13"/>
<location filename="../../src/TextInputWidget.h" line="+164"/> <location filename="../../src/TextInputWidget.h" line="+161"/>
<source>Write a message...</source> <source>Write a message...</source>
<translation>Schreibe eine Nachricht</translation> <translation>Schreibe eine Nachricht</translation>
</message> </message>
@ -375,7 +365,7 @@
<translation>Emoji</translation> <translation>Emoji</translation>
</message> </message>
<message> <message>
<location line="+75"/> <location line="+72"/>
<source>Select a file</source> <source>Select a file</source>
<translation>Datei auswählen</translation> <translation>Datei auswählen</translation>
</message> </message>
@ -391,32 +381,9 @@
</message> </message>
</context> </context>
<context> <context>
<name>TimelineItem</name> <name>TimelineModel</name>
<message> <message>
<location filename="../../src/timeline/TimelineItem.cpp" line="+85"/> <location filename="../../src/timeline/TimelineModel.cpp" line="+835"/>
<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>
<message>
<location line="+65"/>
<source>-- Encrypted Event (No keys found for decryption) --</source> <source>-- Encrypted Event (No keys found for decryption) --</source>
<comment>Placeholder, when the message was not decrypted yet or can&apos;t be decrypted</comment> <comment>Placeholder, when the message was not decrypted yet or can&apos;t be decrypted</comment>
<translation>-- verschlüsselter Event (keine Schlüssel zur Entschlüsselung gefunden) --</translation> <translation>-- verschlüsselter Event (keine Schlüssel zur Entschlüsselung gefunden) --</translation>
@ -440,16 +407,87 @@
<translation>-- Entschlüsselungsfehler (%1) --</translation> <translation>-- Entschlüsselungsfehler (%1) --</translation>
</message> </message>
<message> <message>
<location line="+27"/> <location line="+25"/>
<source>-- Encrypted Event (Unknown event type) --</source> <source>-- Encrypted Event (Unknown event type) --</source>
<comment>Placeholder, when the message was decrypted, but we couldn&apos;t parse it, because Nheko/mtxclient don&apos;t support that event type yet</comment> <comment>Placeholder, when the message was decrypted, but we couldn&apos;t parse it, because Nheko/mtxclient don&apos;t support that event type yet</comment>
<translation>-- verschlüsselter Event (Unbekannter Eventtyp) --</translation> <translation>-- verschlüsselter Event (Unbekannter Eventtyp) --</translation>
</message> </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>
<context> <context>
<name>TopRoomBar</name> <name>TopRoomBar</name>
<message> <message>
<location filename="../../src/TopRoomBar.cpp" line="+79"/> <location filename="../../src/TopRoomBar.cpp" line="+78"/>
<source>Room options</source> <source>Room options</source>
<translation>Raumoptionen</translation> <translation>Raumoptionen</translation>
</message> </message>
@ -515,7 +553,7 @@
<context> <context>
<name>UserSettingsPage</name> <name>UserSettingsPage</name>
<message> <message>
<location filename="../../src/UserSettingsPage.cpp" line="+166"/> <location filename="../../src/UserSettingsPage.cpp" line="+171"/>
<source>Minimize to tray</source> <source>Minimize to tray</source>
<translation>Ins Benachrichtigungsfeld minimieren</translation> <translation>Ins Benachrichtigungsfeld minimieren</translation>
</message> </message>
@ -529,6 +567,11 @@
<source>Group&apos;s sidebar</source> <source>Group&apos;s sidebar</source>
<translation>Gruppen-Seitenleiste</translation> <translation>Gruppen-Seitenleiste</translation>
</message> </message>
<message>
<location line="+9"/>
<source>Circular Avatars</source>
<translation>Runde Profilbilder</translation>
</message>
<message> <message>
<location line="+9"/> <location line="+9"/>
<source>Typing notifications</source> <source>Typing notifications</source>
@ -605,7 +648,7 @@
<translation>ALLGEMEINES</translation> <translation>ALLGEMEINES</translation>
</message> </message>
<message> <message>
<location line="+156"/> <location line="+161"/>
<source>Open Sessions File</source> <source>Open Sessions File</source>
<translation>Öffne Sessions Datei</translation> <translation>Öffne Sessions Datei</translation>
</message> </message>
@ -825,7 +868,7 @@ Medien-Größe: %2
<context> <context>
<name>dialogs::ReadReceipts</name> <name>dialogs::ReadReceipts</name>
<message> <message>
<location filename="../../src/dialogs/ReadReceipts.cpp" line="+121"/> <location filename="../../src/dialogs/ReadReceipts.cpp" line="+117"/>
<source>Read receipts</source> <source>Read receipts</source>
<translation>Lesebestätigungen</translation> <translation>Lesebestätigungen</translation>
</message> </message>
@ -951,7 +994,7 @@ Medien-Größe: %2
<translation>Aktivierung der Verschlüsselung fehlgeschlagen: %1</translation> <translation>Aktivierung der Verschlüsselung fehlgeschlagen: %1</translation>
</message> </message>
<message> <message>
<location line="+149"/> <location line="+148"/>
<source>Select an avatar</source> <source>Select an avatar</source>
<translation>Wähle einen Avatar</translation> <translation>Wähle einen Avatar</translation>
</message> </message>
@ -977,19 +1020,6 @@ Medien-Größe: %2
<translation>Hochladen der Bilddatei fehlgeschlagen: %s</translation> <translation>Hochladen der Bilddatei fehlgeschlagen: %s</translation>
</message> </message>
</context> </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> <context>
<name>dialogs::UserProfile</name> <name>dialogs::UserProfile</name>
<message> <message>
@ -1013,7 +1043,7 @@ Medien-Größe: %2
<translation>Gespräch beginnen</translation> <translation>Gespräch beginnen</translation>
</message> </message>
<message> <message>
<location line="+57"/> <location line="+56"/>
<source>Devices</source> <source>Devices</source>
<translation>Geräte</translation> <translation>Geräte</translation>
</message> </message>
@ -1064,69 +1094,103 @@ Medien-Größe: %2
<context> <context>
<name>message-description sent:</name> <name>message-description sent:</name>
<message> <message>
<location filename="../../src/Utils.h" line="+104"/> <location filename="../../src/Utils.h" line="+95"/>
<source>%1 an audio clip</source> <source>You sent an audio clip</source>
<translation>%1 einen Audioclip</translation> <translation>Du hast eine Audiodatei gesendet.</translation>
</message> </message>
<message> <message>
<location line="+3"/> <location line="+3"/>
<source>%1 an image</source> <source>%1 sent an audio clip</source>
<translation>%1 ein Bild</translation> <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>
<message> <message>
<location line="+3"/> <location line="+3"/>
<source>%1 a file</source> <source>%1 sent an image</source>
<translation>%1 eine Datei</translation> <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>
<message> <message>
<location line="+3"/> <location line="+3"/>
<source>%1 a video clip</source> <source>%1 sent a file</source>
<translation>%1 einen Videoclip</translation> <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>
<message> <message>
<location line="+3"/> <location line="+3"/>
<source>%1 a sticker</source> <source>%1 sent a video</source>
<translation>%1 einen Sticker</translation> <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>
<message> <message>
<location line="+3"/> <location line="+3"/>
<source>%1 a notification</source> <source>%1 sent a sticker</source>
<translation>1% eine Benachrichtigung</translation> <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 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: %2</source>
<translation>%1: %2</translation>
</message> </message>
<message> <message>
<location line="+7"/> <location line="+7"/>
<source>%1 an encrypted message</source> <source>You sent an encrypted message</source>
<translation>1% eine verschüsselte Nachricht</translation> <translation>Du hast eine verschlüsselte Nachricht gesendet.</translation>
</message>
<message>
<location line="+3"/>
<source>%1 sent an encrypted message</source>
<translation>%1 hat eine verschlüsselte Nachricht gesendet.</translation>
</message> </message>
</context> </context>
<context> <context>
<name>message-description:</name> <name>popups::UserMentions</name>
<message> <message>
<location line="-26"/> <location filename="../../src/popups/UserMentions.cpp" line="+61"/>
<source>sent</source> <source>This Room</source>
<comment>For when someone else is the sender</comment> <translation>Dieser Raum</translation>
<translation type="unfinished"></translation>
</message> </message>
</context>
<context>
<name>message-description: </name>
<message> <message>
<location line="-2"/> <location line="+1"/>
<source>sent</source> <source>All Rooms</source>
<comment>For when you are the sender</comment> <translation>Alle Räume</translation>
<translation type="unfinished"></translation>
</message> </message>
</context> </context>
<context> <context>
<name>utils</name> <name>utils</name>
<message> <message>
<location filename="../../src/Utils.cpp" line="+46"/> <location filename="../../src/Utils.cpp" line="+282"/>
<location filename="../../src/Utils.h" line="+55"/>
<source>You</source>
<translation>Du</translation>
</message>
<message>
<location line="+219"/>
<source>sent a file.</source> <source>sent a file.</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
@ -1146,7 +1210,7 @@ Medien-Größe: %2
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location filename="../../src/Utils.h" line="-23"/> <location filename="../../src/Utils.h" line="+4"/>
<source>Unknown Message Type</source> <source>Unknown Message Type</source>
<translation>Unbekannter Nachrichtentyp</translation> <translation>Unbekannter Nachrichtentyp</translation>
</message> </message>

View File

@ -1,38 +1,15 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE TS> <!DOCTYPE TS>
<TS version="2.1" language="el"> <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> <context>
<name>ChatPage</name> <name>ChatPage</name>
<message> <message>
<location filename="../../src/ChatPage.cpp" line="+330"/> <location filename="../../src/ChatPage.cpp" line="+346"/>
<source>Failed to upload image. Please try again.</source> <source>Failed to upload media. Please try again.</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location line="+45"/> <location line="+389"/>
<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>
<translation type="unfinished"></translation>
</message>
<message>
<location line="+380"/>
<source>Failed to restore OLM account. Please login again.</source> <source>Failed to restore OLM account. Please login again.</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
@ -42,18 +19,18 @@
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location line="+198"/> <location line="+181"/>
<source>Failed to setup encryption keys. Server response: %1 %2. Please try again later.</source> <source>Failed to setup encryption keys. Server response: %1 %2. Please try again later.</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location line="+51"/> <location line="+51"/>
<location line="+153"/> <location line="+155"/>
<source>Please try to login again: %1</source> <source>Please try to login again: %1</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location line="-45"/> <location line="-47"/>
<source>Room creation failed: %1</source> <source>Room creation failed: %1</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
@ -116,19 +93,11 @@
</message> </message>
</context> </context>
<context> <context>
<name>FileItem</name> <name>EncryptionIndicator</name>
<message> <message>
<location filename="../../src/timeline/widgets/FileItem.cpp" line="+107"/> <location filename="../qml/EncryptionIndicator.qml" line="+11"/>
<source>Save File</source> <source>Encrypted</source>
<translation>Αποθήκευση</translation> <translation type="unfinished"></translation>
</message>
</context>
<context>
<name>ImageItem</name>
<message>
<location filename="../../src/timeline/widgets/ImageItem.cpp" line="+241"/>
<source>Save image</source>
<translation>Αποθήκευση Εικόνας</translation>
</message> </message>
</context> </context>
<context> <context>
@ -200,7 +169,7 @@
<context> <context>
<name>MemberList</name> <name>MemberList</name>
<message> <message>
<location filename="../../src/dialogs/MemberList.cpp" line="+96"/> <location filename="../../src/dialogs/MemberList.cpp" line="+89"/>
<source>Room members</source> <source>Room members</source>
<translation>Μέλη</translation> <translation>Μέλη</translation>
</message> </message>
@ -210,6 +179,27 @@
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
</context> </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> <context>
<name>QuickSwitcher</name> <name>QuickSwitcher</name>
<message> <message>
@ -277,7 +267,7 @@
<context> <context>
<name>RoomInfo</name> <name>RoomInfo</name>
<message> <message>
<location filename="../../src/Cache.cpp" line="+2205"/> <location filename="../../src/Cache.cpp" line="+2307"/>
<source>no version stored</source> <source>no version stored</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
@ -285,12 +275,12 @@
<context> <context>
<name>RoomInfoListItem</name> <name>RoomInfoListItem</name>
<message> <message>
<location filename="../../src/RoomInfoListItem.cpp" line="+93"/> <location filename="../../src/RoomInfoListItem.cpp" line="+95"/>
<source>Leave room</source> <source>Leave room</source>
<translation>Βγές</translation> <translation>Βγές</translation>
</message> </message>
<message> <message>
<location line="+181"/> <location line="+161"/>
<source>Accept</source> <source>Accept</source>
<translation>Αποδοχή</translation> <translation>Αποδοχή</translation>
</message> </message>
@ -331,36 +321,36 @@
<context> <context>
<name>StatusIndicator</name> <name>StatusIndicator</name>
<message> <message>
<location filename="../../src/timeline/TimelineItem.cpp" line="+107"/> <location filename="../qml/StatusIndicator.qml" line="+13"/>
<source>Encrypted</source> <source>Failed</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location line="+3"/> <location line="+1"/>
<source>Delivered</source>
<translation type="unfinished"></translation>
</message>
<message>
<location line="+3"/>
<source>Seen</source>
<translation type="unfinished"></translation>
</message>
<message>
<location line="+3"/>
<source>Sent</source> <source>Sent</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message>
<location line="+1"/>
<source>Received</source>
<translation type="unfinished"></translation>
</message>
<message>
<location line="+1"/>
<source>Read</source>
<translation type="unfinished"></translation>
</message>
</context> </context>
<context> <context>
<name>TextInputWidget</name> <name>TextInputWidget</name>
<message> <message>
<location filename="../../src/TextInputWidget.cpp" line="+507"/> <location filename="../../src/TextInputWidget.cpp" line="+502"/>
<source>Send a file</source> <source>Send a file</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location line="+13"/> <location line="+13"/>
<location filename="../../src/TextInputWidget.h" line="+164"/> <location filename="../../src/TextInputWidget.h" line="+161"/>
<source>Write a message...</source> <source>Write a message...</source>
<translation>Γράψε ένα μήνυμα...</translation> <translation>Γράψε ένα μήνυμα...</translation>
</message> </message>
@ -375,7 +365,7 @@
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location line="+75"/> <location line="+72"/>
<source>Select a file</source> <source>Select a file</source>
<translation>Διάλεξε ένα αρχείο</translation> <translation>Διάλεξε ένα αρχείο</translation>
</message> </message>
@ -391,32 +381,9 @@
</message> </message>
</context> </context>
<context> <context>
<name>TimelineItem</name> <name>TimelineModel</name>
<message> <message>
<location filename="../../src/timeline/TimelineItem.cpp" line="+85"/> <location filename="../../src/timeline/TimelineModel.cpp" line="+835"/>
<source>Message redaction failed: %1</source>
<translation type="unfinished"></translation>
</message>
<message>
<location line="+39"/>
<source>Reply</source>
<translation type="unfinished"></translation>
</message>
<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 type="unfinished"></translation>
</message>
<message>
<location line="+65"/>
<source>-- Encrypted Event (No keys found for decryption) --</source> <source>-- Encrypted Event (No keys found for decryption) --</source>
<comment>Placeholder, when the message was not decrypted yet or can&apos;t be decrypted</comment> <comment>Placeholder, when the message was not decrypted yet or can&apos;t be decrypted</comment>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
@ -440,16 +407,87 @@
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location line="+27"/> <location line="+25"/>
<source>-- Encrypted Event (Unknown event type) --</source> <source>-- Encrypted Event (Unknown event type) --</source>
<comment>Placeholder, when the message was decrypted, but we couldn&apos;t parse it, because Nheko/mtxclient don&apos;t support that event type yet</comment> <comment>Placeholder, when the message was decrypted, but we couldn&apos;t parse it, because Nheko/mtxclient don&apos;t support that event type yet</comment>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message>
<location line="+54"/>
<source>Message redaction failed: %1</source>
<translation type="unfinished"></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>
<context> <context>
<name>TopRoomBar</name> <name>TopRoomBar</name>
<message> <message>
<location filename="../../src/TopRoomBar.cpp" line="+79"/> <location filename="../../src/TopRoomBar.cpp" line="+78"/>
<source>Room options</source> <source>Room options</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
@ -515,7 +553,7 @@
<context> <context>
<name>UserSettingsPage</name> <name>UserSettingsPage</name>
<message> <message>
<location filename="../../src/UserSettingsPage.cpp" line="+166"/> <location filename="../../src/UserSettingsPage.cpp" line="+171"/>
<source>Minimize to tray</source> <source>Minimize to tray</source>
<translation>Ελαχιστοποίηση</translation> <translation>Ελαχιστοποίηση</translation>
</message> </message>
@ -529,6 +567,11 @@
<source>Group&apos;s sidebar</source> <source>Group&apos;s sidebar</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message>
<location line="+9"/>
<source>Circular Avatars</source>
<translation type="unfinished"></translation>
</message>
<message> <message>
<location line="+9"/> <location line="+9"/>
<source>Typing notifications</source> <source>Typing notifications</source>
@ -605,7 +648,7 @@
<translation>ΓΕΝΙΚΑ</translation> <translation>ΓΕΝΙΚΑ</translation>
</message> </message>
<message> <message>
<location line="+156"/> <location line="+161"/>
<source>Open Sessions File</source> <source>Open Sessions File</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
@ -823,7 +866,7 @@ Media size: %2
<context> <context>
<name>dialogs::ReadReceipts</name> <name>dialogs::ReadReceipts</name>
<message> <message>
<location filename="../../src/dialogs/ReadReceipts.cpp" line="+121"/> <location filename="../../src/dialogs/ReadReceipts.cpp" line="+117"/>
<source>Read receipts</source> <source>Read receipts</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
@ -949,7 +992,7 @@ Media size: %2
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location line="+149"/> <location line="+148"/>
<source>Select an avatar</source> <source>Select an avatar</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
@ -975,19 +1018,6 @@ Media size: %2
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
</context> </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> <context>
<name>dialogs::UserProfile</name> <name>dialogs::UserProfile</name>
<message> <message>
@ -1011,7 +1041,7 @@ Media size: %2
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location line="+57"/> <location line="+56"/>
<source>Devices</source> <source>Devices</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
@ -1062,69 +1092,103 @@ Media size: %2
<context> <context>
<name>message-description sent:</name> <name>message-description sent:</name>
<message> <message>
<location filename="../../src/Utils.h" line="+104"/> <location filename="../../src/Utils.h" line="+95"/>
<source>%1 an audio clip</source> <source>You sent an audio clip</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location line="+3"/> <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> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location line="+3"/> <location line="+3"/>
<source>%1 a file</source> <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> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location line="+3"/> <location line="+3"/>
<source>%1 a video clip</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> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location line="+3"/> <location line="+3"/>
<source>%1 a sticker</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> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location line="+3"/> <location line="+3"/>
<source>%1 a notification</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 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: %2</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location line="+7"/> <location line="+7"/>
<source>%1 an encrypted message</source> <source>You sent an encrypted message</source>
<translation type="unfinished"></translation>
</message>
<message>
<location line="+3"/>
<source>%1 sent an encrypted message</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
</context> </context>
<context> <context>
<name>message-description:</name> <name>popups::UserMentions</name>
<message> <message>
<location line="-26"/> <location filename="../../src/popups/UserMentions.cpp" line="+61"/>
<source>sent</source> <source>This Room</source>
<comment>For when someone else is the sender</comment>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
</context>
<context>
<name>message-description: </name>
<message> <message>
<location line="-2"/> <location line="+1"/>
<source>sent</source> <source>All Rooms</source>
<comment>For when you are the sender</comment>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
</context> </context>
<context> <context>
<name>utils</name> <name>utils</name>
<message> <message>
<location filename="../../src/Utils.cpp" line="+46"/> <location filename="../../src/Utils.cpp" line="+282"/>
<location filename="../../src/Utils.h" line="+55"/>
<source>You</source>
<translation type="unfinished"></translation>
</message>
<message>
<location line="+219"/>
<source>sent a file.</source> <source>sent a file.</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
@ -1144,7 +1208,7 @@ Media size: %2
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location filename="../../src/Utils.h" line="-23"/> <location filename="../../src/Utils.h" line="+4"/>
<source>Unknown Message Type</source> <source>Unknown Message Type</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>

View File

@ -1,38 +1,15 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE TS> <!DOCTYPE TS>
<TS version="2.1" language="en"> <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> <context>
<name>ChatPage</name> <name>ChatPage</name>
<message> <message>
<location filename="../../src/ChatPage.cpp" line="+330"/> <location filename="../../src/ChatPage.cpp" line="+346"/>
<source>Failed to upload image. Please try again.</source> <source>Failed to upload media. Please try again.</source>
<translation>Failed to upload image. Please try again.</translation> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location line="+45"/> <location line="+389"/>
<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>
</message>
<message>
<location line="+380"/>
<source>Failed to restore OLM account. Please login again.</source> <source>Failed to restore OLM account. Please login again.</source>
<translation>Failed to restore OLM account. Please login again.</translation> <translation>Failed to restore OLM account. Please login again.</translation>
</message> </message>
@ -42,18 +19,18 @@
<translation>Failed to restore save data. Please login again.</translation> <translation>Failed to restore save data. Please login again.</translation>
</message> </message>
<message> <message>
<location line="+198"/> <location line="+181"/>
<source>Failed to setup encryption keys. Server response: %1 %2. Please try again later.</source> <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> <translation>Failed to setup encryption keys. Server response: %1 %2. Please try again later.</translation>
</message> </message>
<message> <message>
<location line="+51"/> <location line="+51"/>
<location line="+153"/> <location line="+155"/>
<source>Please try to login again: %1</source> <source>Please try to login again: %1</source>
<translation>Please try to login again: %1</translation> <translation>Please try to login again: %1</translation>
</message> </message>
<message> <message>
<location line="-45"/> <location line="-47"/>
<source>Room creation failed: %1</source> <source>Room creation failed: %1</source>
<translation>Room creation failed: %1</translation> <translation>Room creation failed: %1</translation>
</message> </message>
@ -116,19 +93,11 @@
</message> </message>
</context> </context>
<context> <context>
<name>FileItem</name> <name>EncryptionIndicator</name>
<message> <message>
<location filename="../../src/timeline/widgets/FileItem.cpp" line="+107"/> <location filename="../qml/EncryptionIndicator.qml" line="+11"/>
<source>Save File</source> <source>Encrypted</source>
<translation>Save File</translation> <translation type="unfinished"></translation>
</message>
</context>
<context>
<name>ImageItem</name>
<message>
<location filename="../../src/timeline/widgets/ImageItem.cpp" line="+241"/>
<source>Save image</source>
<translation>Save image</translation>
</message> </message>
</context> </context>
<context> <context>
@ -200,7 +169,7 @@
<context> <context>
<name>MemberList</name> <name>MemberList</name>
<message> <message>
<location filename="../../src/dialogs/MemberList.cpp" line="+96"/> <location filename="../../src/dialogs/MemberList.cpp" line="+89"/>
<source>Room members</source> <source>Room members</source>
<translation>Room members</translation> <translation>Room members</translation>
</message> </message>
@ -210,6 +179,27 @@
<translation>OK</translation> <translation>OK</translation>
</message> </message>
</context> </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> <context>
<name>QuickSwitcher</name> <name>QuickSwitcher</name>
<message> <message>
@ -277,7 +267,7 @@
<context> <context>
<name>RoomInfo</name> <name>RoomInfo</name>
<message> <message>
<location filename="../../src/Cache.cpp" line="+2205"/> <location filename="../../src/Cache.cpp" line="+2307"/>
<source>no version stored</source> <source>no version stored</source>
<translation>no version stored</translation> <translation>no version stored</translation>
</message> </message>
@ -285,12 +275,12 @@
<context> <context>
<name>RoomInfoListItem</name> <name>RoomInfoListItem</name>
<message> <message>
<location filename="../../src/RoomInfoListItem.cpp" line="+93"/> <location filename="../../src/RoomInfoListItem.cpp" line="+95"/>
<source>Leave room</source> <source>Leave room</source>
<translation>Leave room</translation> <translation>Leave room</translation>
</message> </message>
<message> <message>
<location line="+181"/> <location line="+161"/>
<source>Accept</source> <source>Accept</source>
<translation>Accept</translation> <translation>Accept</translation>
</message> </message>
@ -331,36 +321,36 @@
<context> <context>
<name>StatusIndicator</name> <name>StatusIndicator</name>
<message> <message>
<location filename="../../src/timeline/TimelineItem.cpp" line="+107"/> <location filename="../qml/StatusIndicator.qml" line="+13"/>
<source>Encrypted</source> <source>Failed</source>
<translation>Encrypted</translation> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location line="+3"/> <location line="+1"/>
<source>Delivered</source>
<translation>Delivered</translation>
</message>
<message>
<location line="+3"/>
<source>Seen</source>
<translation>Seen</translation>
</message>
<message>
<location line="+3"/>
<source>Sent</source> <source>Sent</source>
<translation>Sent</translation> <translation type="unfinished"></translation>
</message>
<message>
<location line="+1"/>
<source>Received</source>
<translation type="unfinished"></translation>
</message>
<message>
<location line="+1"/>
<source>Read</source>
<translation type="unfinished"></translation>
</message> </message>
</context> </context>
<context> <context>
<name>TextInputWidget</name> <name>TextInputWidget</name>
<message> <message>
<location filename="../../src/TextInputWidget.cpp" line="+507"/> <location filename="../../src/TextInputWidget.cpp" line="+502"/>
<source>Send a file</source> <source>Send a file</source>
<translation>Send a file</translation> <translation>Send a file</translation>
</message> </message>
<message> <message>
<location line="+13"/> <location line="+13"/>
<location filename="../../src/TextInputWidget.h" line="+164"/> <location filename="../../src/TextInputWidget.h" line="+161"/>
<source>Write a message...</source> <source>Write a message...</source>
<translation>Write a message</translation> <translation>Write a message</translation>
</message> </message>
@ -375,7 +365,7 @@
<translation>Emoji</translation> <translation>Emoji</translation>
</message> </message>
<message> <message>
<location line="+75"/> <location line="+72"/>
<source>Select a file</source> <source>Select a file</source>
<translation>Select a file</translation> <translation>Select a file</translation>
</message> </message>
@ -391,65 +381,113 @@
</message> </message>
</context> </context>
<context> <context>
<name>TimelineItem</name> <name>TimelineModel</name>
<message> <message>
<location filename="../../src/timeline/TimelineItem.cpp" line="+85"/> <location filename="../../src/timeline/TimelineModel.cpp" line="+835"/>
<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>
<message>
<location filename="../../src/timeline/TimelineView.cpp" line="+245"/>
<source>Encryption is enabled</source>
<translation>Encryption is enabled</translation>
</message>
<message>
<location line="+65"/>
<source>-- Encrypted Event (No keys found for decryption) --</source> <source>-- Encrypted Event (No keys found for decryption) --</source>
<comment>Placeholder, when the message was not decrypted yet or can&apos;t be decrypted</comment> <comment>Placeholder, when the message was not decrypted yet or can&apos;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>
<message> <message>
<location line="+15"/> <location line="+15"/>
<source>-- Decryption Error (failed to communicate with DB) --</source> <source>-- Decryption Error (failed to communicate with DB) --</source>
<comment>Placeholder, when the message can&apos;t be decrypted, because the DB access failed when trying to lookup the session.</comment> <comment>Placeholder, when the message can&apos;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>
<message> <message>
<location line="+19"/> <location line="+19"/>
<source>-- Decryption Error (failed to retrieve megolm keys from db) --</source> <source>-- Decryption Error (failed to retrieve megolm keys from db) --</source>
<comment>Placeholder, when the message can&apos;t be decrypted, because the DB access failed.</comment> <comment>Placeholder, when the message can&apos;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>
<message> <message>
<location line="+12"/> <location line="+12"/>
<source>-- Decryption Error (%1) --</source> <source>-- Decryption Error (%1) --</source>
<comment>Placeholder, when the message can&apos;t be decrypted. In this case, the Olm decrytion returned an error, which is passed ad %1</comment> <comment>Placeholder, when the message can&apos;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>
<message> <message>
<location line="+27"/> <location line="+25"/>
<source>-- Encrypted Event (Unknown event type) --</source> <source>-- Encrypted Event (Unknown event type) --</source>
<comment>Placeholder, when the message was decrypted, but we couldn&apos;t parse it, because Nheko/mtxclient don&apos;t support that event type yet</comment> <comment>Placeholder, when the message was decrypted, but we couldn&apos;t parse it, because Nheko/mtxclient don&apos;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> </message>
</context> </context>
<context> <context>
<name>TopRoomBar</name> <name>TopRoomBar</name>
<message> <message>
<location filename="../../src/TopRoomBar.cpp" line="+79"/> <location filename="../../src/TopRoomBar.cpp" line="+78"/>
<source>Room options</source> <source>Room options</source>
<translation>Room options</translation> <translation>Room options</translation>
</message> </message>
@ -515,7 +553,7 @@
<context> <context>
<name>UserSettingsPage</name> <name>UserSettingsPage</name>
<message> <message>
<location filename="../../src/UserSettingsPage.cpp" line="+166"/> <location filename="../../src/UserSettingsPage.cpp" line="+171"/>
<source>Minimize to tray</source> <source>Minimize to tray</source>
<translation>Minimize to tray</translation> <translation>Minimize to tray</translation>
</message> </message>
@ -529,6 +567,11 @@
<source>Group&apos;s sidebar</source> <source>Group&apos;s sidebar</source>
<translation>Group&apos;s sidebar</translation> <translation>Group&apos;s sidebar</translation>
</message> </message>
<message>
<location line="+9"/>
<source>Circular Avatars</source>
<translation type="unfinished"></translation>
</message>
<message> <message>
<location line="+9"/> <location line="+9"/>
<source>Typing notifications</source> <source>Typing notifications</source>
@ -605,7 +648,7 @@
<translation>GENERAL</translation> <translation>GENERAL</translation>
</message> </message>
<message> <message>
<location line="+156"/> <location line="+161"/>
<source>Open Sessions File</source> <source>Open Sessions File</source>
<translation>Open Sessions File</translation> <translation>Open Sessions File</translation>
</message> </message>
@ -825,7 +868,7 @@ Media size: %2
<context> <context>
<name>dialogs::ReadReceipts</name> <name>dialogs::ReadReceipts</name>
<message> <message>
<location filename="../../src/dialogs/ReadReceipts.cpp" line="+121"/> <location filename="../../src/dialogs/ReadReceipts.cpp" line="+117"/>
<source>Read receipts</source> <source>Read receipts</source>
<translation>Read receipts</translation> <translation>Read receipts</translation>
</message> </message>
@ -953,7 +996,7 @@ Media size: %2
<translation>Failed to enable encryption: %1</translation> <translation>Failed to enable encryption: %1</translation>
</message> </message>
<message> <message>
<location line="+149"/> <location line="+148"/>
<source>Select an avatar</source> <source>Select an avatar</source>
<translation>Select an avatar</translation> <translation>Select an avatar</translation>
</message> </message>
@ -979,19 +1022,6 @@ Media size: %2
<translation>Failed to upload image: %s</translation> <translation>Failed to upload image: %s</translation>
</message> </message>
</context> </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> <context>
<name>dialogs::UserProfile</name> <name>dialogs::UserProfile</name>
<message> <message>
@ -1015,7 +1045,7 @@ Media size: %2
<translation>Start a conversation</translation> <translation>Start a conversation</translation>
</message> </message>
<message> <message>
<location line="+57"/> <location line="+56"/>
<source>Devices</source> <source>Devices</source>
<translation>Devices</translation> <translation>Devices</translation>
</message> </message>
@ -1066,69 +1096,103 @@ Media size: %2
<context> <context>
<name>message-description sent:</name> <name>message-description sent:</name>
<message> <message>
<location filename="../../src/Utils.h" line="+104"/> <location filename="../../src/Utils.h" line="+95"/>
<source>%1 an audio clip</source> <source>You sent an audio clip</source>
<translation>%1 an audio clip</translation> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location line="+3"/> <location line="+3"/>
<source>%1 an image</source> <source>%1 sent an audio clip</source>
<translation>%1 an image</translation> <translation type="unfinished"></translation>
</message>
<message>
<location line="+5"/>
<source>You sent an image</source>
<translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location line="+3"/> <location line="+3"/>
<source>%1 a file</source> <source>%1 sent an image</source>
<translation>%1 a file</translation> <translation type="unfinished"></translation>
</message>
<message>
<location line="+5"/>
<source>You sent a file</source>
<translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location line="+3"/> <location line="+3"/>
<source>%1 a video clip</source> <source>%1 sent a file</source>
<translation>%1 a video clip</translation> <translation type="unfinished"></translation>
</message>
<message>
<location line="+5"/>
<source>You sent a video</source>
<translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location line="+3"/> <location line="+3"/>
<source>%1 a sticker</source> <source>%1 sent a video</source>
<translation>%1 a sticker</translation> <translation type="unfinished"></translation>
</message>
<message>
<location line="+5"/>
<source>You sent a sticker</source>
<translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location line="+3"/> <location line="+3"/>
<source>%1 a notification</source> <source>%1 sent a sticker</source>
<translation>%1 a notification</translation> <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 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: %2</source>
<translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location line="+7"/> <location line="+7"/>
<source>%1 an encrypted message</source> <source>You sent an encrypted message</source>
<translation>%1 an encrypted message</translation> <translation type="unfinished"></translation>
</message>
<message>
<location line="+3"/>
<source>%1 sent an encrypted message</source>
<translation type="unfinished"></translation>
</message> </message>
</context> </context>
<context> <context>
<name>message-description:</name> <name>popups::UserMentions</name>
<message> <message>
<location line="-26"/> <location filename="../../src/popups/UserMentions.cpp" line="+61"/>
<source>sent</source> <source>This Room</source>
<comment>For when someone else is the sender</comment> <translation type="unfinished">This Room</translation>
<translation>sent</translation>
</message> </message>
</context>
<context>
<name>message-description: </name>
<message> <message>
<location line="-2"/> <location line="+1"/>
<source>sent</source> <source>All Rooms</source>
<comment>For when you are the sender</comment> <translation type="unfinished">All Rooms</translation>
<translation>sent</translation>
</message> </message>
</context> </context>
<context> <context>
<name>utils</name> <name>utils</name>
<message> <message>
<location filename="../../src/Utils.cpp" line="+46"/> <location filename="../../src/Utils.cpp" line="+282"/>
<location filename="../../src/Utils.h" line="+55"/>
<source>You</source>
<translation>You</translation>
</message>
<message>
<location line="+219"/>
<source>sent a file.</source> <source>sent a file.</source>
<translation>sent a file.</translation> <translation>sent a file.</translation>
</message> </message>
@ -1148,7 +1212,7 @@ Media size: %2
<translation>sent a video.</translation> <translation>sent a video.</translation>
</message> </message>
<message> <message>
<location filename="../../src/Utils.h" line="-23"/> <location filename="../../src/Utils.h" line="+4"/>
<source>Unknown Message Type</source> <source>Unknown Message Type</source>
<translation>Unknown Message Type</translation> <translation>Unknown Message Type</translation>
</message> </message>

View File

@ -1,38 +1,15 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE TS> <!DOCTYPE TS>
<TS version="2.1" language="fi"> <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> <context>
<name>ChatPage</name> <name>ChatPage</name>
<message> <message>
<location filename="../../src/ChatPage.cpp" line="+330"/> <location filename="../../src/ChatPage.cpp" line="+346"/>
<source>Failed to upload image. Please try again.</source> <source>Failed to upload media. Please try again.</source>
<translation>Kuvan lähettäminen epäonnistui. Ole hyvä ja yritä uudelleen.</translation> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location line="+45"/> <location line="+389"/>
<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>
</message>
<message>
<location line="+380"/>
<source>Failed to restore OLM account. Please login again.</source> <source>Failed to restore OLM account. Please login again.</source>
<translation>OLM-tilin palauttaminen epäonnistui. Ole hyvä ja kirjaudu sisään uudelleen.</translation> <translation>OLM-tilin palauttaminen epäonnistui. Ole hyvä ja kirjaudu sisään uudelleen.</translation>
</message> </message>
@ -42,18 +19,18 @@
<translation>Tallennettujen tietojen palauttaminen epäonnistui. Ole hyvä ja kirjaudu sisään uudelleen.</translation> <translation>Tallennettujen tietojen palauttaminen epäonnistui. Ole hyvä ja kirjaudu sisään uudelleen.</translation>
</message> </message>
<message> <message>
<location line="+198"/> <location line="+181"/>
<source>Failed to setup encryption keys. Server response: %1 %2. Please try again later.</source> <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> <translation>Salausavainten lähetys epäonnistui. Palvelimen vastaus: %1 %2. Ole hyvä ja yritä uudelleen myöhemmin.</translation>
</message> </message>
<message> <message>
<location line="+51"/> <location line="+51"/>
<location line="+153"/> <location line="+155"/>
<source>Please try to login again: %1</source> <source>Please try to login again: %1</source>
<translation>Ole hyvä ja yritä kirjautua sisään uudelleen: %1</translation> <translation>Ole hyvä ja yritä kirjautua sisään uudelleen: %1</translation>
</message> </message>
<message> <message>
<location line="-45"/> <location line="-47"/>
<source>Room creation failed: %1</source> <source>Room creation failed: %1</source>
<translation>Huoneen luominen epäonnistui: %1</translation> <translation>Huoneen luominen epäonnistui: %1</translation>
</message> </message>
@ -116,19 +93,11 @@
</message> </message>
</context> </context>
<context> <context>
<name>FileItem</name> <name>EncryptionIndicator</name>
<message> <message>
<location filename="../../src/timeline/widgets/FileItem.cpp" line="+107"/> <location filename="../qml/EncryptionIndicator.qml" line="+11"/>
<source>Save File</source> <source>Encrypted</source>
<translation>Tallenna tiedosto</translation> <translation type="unfinished"></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>
</message> </message>
</context> </context>
<context> <context>
@ -200,7 +169,7 @@
<context> <context>
<name>MemberList</name> <name>MemberList</name>
<message> <message>
<location filename="../../src/dialogs/MemberList.cpp" line="+96"/> <location filename="../../src/dialogs/MemberList.cpp" line="+89"/>
<source>Room members</source> <source>Room members</source>
<translation>Huoneen jäsenet</translation> <translation>Huoneen jäsenet</translation>
</message> </message>
@ -210,6 +179,27 @@
<translation>OK</translation> <translation>OK</translation>
</message> </message>
</context> </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> <context>
<name>QuickSwitcher</name> <name>QuickSwitcher</name>
<message> <message>
@ -277,7 +267,7 @@
<context> <context>
<name>RoomInfo</name> <name>RoomInfo</name>
<message> <message>
<location filename="../../src/Cache.cpp" line="+2205"/> <location filename="../../src/Cache.cpp" line="+2307"/>
<source>no version stored</source> <source>no version stored</source>
<translation>ei tallennettua versiota</translation> <translation>ei tallennettua versiota</translation>
</message> </message>
@ -285,12 +275,12 @@
<context> <context>
<name>RoomInfoListItem</name> <name>RoomInfoListItem</name>
<message> <message>
<location filename="../../src/RoomInfoListItem.cpp" line="+93"/> <location filename="../../src/RoomInfoListItem.cpp" line="+95"/>
<source>Leave room</source> <source>Leave room</source>
<translation>Poistu huoneesta</translation> <translation>Poistu huoneesta</translation>
</message> </message>
<message> <message>
<location line="+181"/> <location line="+161"/>
<source>Accept</source> <source>Accept</source>
<translation>Hyväksy</translation> <translation>Hyväksy</translation>
</message> </message>
@ -331,36 +321,36 @@
<context> <context>
<name>StatusIndicator</name> <name>StatusIndicator</name>
<message> <message>
<location filename="../../src/timeline/TimelineItem.cpp" line="+107"/> <location filename="../qml/StatusIndicator.qml" line="+13"/>
<source>Encrypted</source> <source>Failed</source>
<translation>Salattu</translation> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location line="+3"/> <location line="+1"/>
<source>Delivered</source>
<translation>Toimitettu</translation>
</message>
<message>
<location line="+3"/>
<source>Seen</source>
<translation>Luettu</translation>
</message>
<message>
<location line="+3"/>
<source>Sent</source> <source>Sent</source>
<translation>Lähetetty</translation> <translation type="unfinished"></translation>
</message>
<message>
<location line="+1"/>
<source>Received</source>
<translation type="unfinished"></translation>
</message>
<message>
<location line="+1"/>
<source>Read</source>
<translation type="unfinished"></translation>
</message> </message>
</context> </context>
<context> <context>
<name>TextInputWidget</name> <name>TextInputWidget</name>
<message> <message>
<location filename="../../src/TextInputWidget.cpp" line="+507"/> <location filename="../../src/TextInputWidget.cpp" line="+502"/>
<source>Send a file</source> <source>Send a file</source>
<translation>Lähetä tiedosto</translation> <translation>Lähetä tiedosto</translation>
</message> </message>
<message> <message>
<location line="+13"/> <location line="+13"/>
<location filename="../../src/TextInputWidget.h" line="+164"/> <location filename="../../src/TextInputWidget.h" line="+161"/>
<source>Write a message...</source> <source>Write a message...</source>
<translation>Kirjoita viesti</translation> <translation>Kirjoita viesti</translation>
</message> </message>
@ -375,7 +365,7 @@
<translation>Emoji</translation> <translation>Emoji</translation>
</message> </message>
<message> <message>
<location line="+75"/> <location line="+72"/>
<source>Select a file</source> <source>Select a file</source>
<translation>Valitse tiedosto</translation> <translation>Valitse tiedosto</translation>
</message> </message>
@ -391,65 +381,113 @@
</message> </message>
</context> </context>
<context> <context>
<name>TimelineItem</name> <name>TimelineModel</name>
<message> <message>
<location filename="../../src/timeline/TimelineItem.cpp" line="+85"/> <location filename="../../src/timeline/TimelineModel.cpp" line="+835"/>
<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>
<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"/>
<source>-- Encrypted Event (No keys found for decryption) --</source> <source>-- Encrypted Event (No keys found for decryption) --</source>
<comment>Placeholder, when the message was not decrypted yet or can&apos;t be decrypted</comment> <comment>Placeholder, when the message was not decrypted yet or can&apos;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>
<message> <message>
<location line="+15"/> <location line="+15"/>
<source>-- Decryption Error (failed to communicate with DB) --</source> <source>-- Decryption Error (failed to communicate with DB) --</source>
<comment>Placeholder, when the message can&apos;t be decrypted, because the DB access failed when trying to lookup the session.</comment> <comment>Placeholder, when the message can&apos;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>
<message> <message>
<location line="+19"/> <location line="+19"/>
<source>-- Decryption Error (failed to retrieve megolm keys from db) --</source> <source>-- Decryption Error (failed to retrieve megolm keys from db) --</source>
<comment>Placeholder, when the message can&apos;t be decrypted, because the DB access failed.</comment> <comment>Placeholder, when the message can&apos;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>
<message> <message>
<location line="+12"/> <location line="+12"/>
<source>-- Decryption Error (%1) --</source> <source>-- Decryption Error (%1) --</source>
<comment>Placeholder, when the message can&apos;t be decrypted. In this case, the Olm decrytion returned an error, which is passed ad %1</comment> <comment>Placeholder, when the message can&apos;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>
<message> <message>
<location line="+27"/> <location line="+25"/>
<source>-- Encrypted Event (Unknown event type) --</source> <source>-- Encrypted Event (Unknown event type) --</source>
<comment>Placeholder, when the message was decrypted, but we couldn&apos;t parse it, because Nheko/mtxclient don&apos;t support that event type yet</comment> <comment>Placeholder, when the message was decrypted, but we couldn&apos;t parse it, because Nheko/mtxclient don&apos;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> </message>
</context> </context>
<context> <context>
<name>TopRoomBar</name> <name>TopRoomBar</name>
<message> <message>
<location filename="../../src/TopRoomBar.cpp" line="+79"/> <location filename="../../src/TopRoomBar.cpp" line="+78"/>
<source>Room options</source> <source>Room options</source>
<translation>Huonevaihtoehdot</translation> <translation>Huonevaihtoehdot</translation>
</message> </message>
@ -515,7 +553,7 @@
<context> <context>
<name>UserSettingsPage</name> <name>UserSettingsPage</name>
<message> <message>
<location filename="../../src/UserSettingsPage.cpp" line="+166"/> <location filename="../../src/UserSettingsPage.cpp" line="+171"/>
<source>Minimize to tray</source> <source>Minimize to tray</source>
<translation>Pienennä ilmoitusalueelle</translation> <translation>Pienennä ilmoitusalueelle</translation>
</message> </message>
@ -529,6 +567,11 @@
<source>Group&apos;s sidebar</source> <source>Group&apos;s sidebar</source>
<translation>Ryhmäsivupalkki</translation> <translation>Ryhmäsivupalkki</translation>
</message> </message>
<message>
<location line="+9"/>
<source>Circular Avatars</source>
<translation type="unfinished"></translation>
</message>
<message> <message>
<location line="+9"/> <location line="+9"/>
<source>Typing notifications</source> <source>Typing notifications</source>
@ -605,7 +648,7 @@
<translation>YLEISET ASETUKSET</translation> <translation>YLEISET ASETUKSET</translation>
</message> </message>
<message> <message>
<location line="+156"/> <location line="+161"/>
<source>Open Sessions File</source> <source>Open Sessions File</source>
<translation>Avaa Istuntoavaintiedosto</translation> <translation>Avaa Istuntoavaintiedosto</translation>
</message> </message>
@ -825,7 +868,7 @@ Median koko: %2
<context> <context>
<name>dialogs::ReadReceipts</name> <name>dialogs::ReadReceipts</name>
<message> <message>
<location filename="../../src/dialogs/ReadReceipts.cpp" line="+121"/> <location filename="../../src/dialogs/ReadReceipts.cpp" line="+117"/>
<source>Read receipts</source> <source>Read receipts</source>
<translation>Lukukuittaukset</translation> <translation>Lukukuittaukset</translation>
</message> </message>
@ -953,7 +996,7 @@ Median koko: %2
<translation>Salauksen aktivointi epäonnistui: %1</translation> <translation>Salauksen aktivointi epäonnistui: %1</translation>
</message> </message>
<message> <message>
<location line="+149"/> <location line="+148"/>
<source>Select an avatar</source> <source>Select an avatar</source>
<translation>Valitse profiilikuva</translation> <translation>Valitse profiilikuva</translation>
</message> </message>
@ -979,19 +1022,6 @@ Median koko: %2
<translation>Kuvan lähetys epäonnistui: %s</translation> <translation>Kuvan lähetys epäonnistui: %s</translation>
</message> </message>
</context> </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> <context>
<name>dialogs::UserProfile</name> <name>dialogs::UserProfile</name>
<message> <message>
@ -1015,7 +1045,7 @@ Median koko: %2
<translation>Aloita keskustelu</translation> <translation>Aloita keskustelu</translation>
</message> </message>
<message> <message>
<location line="+57"/> <location line="+56"/>
<source>Devices</source> <source>Devices</source>
<translation>Laitteet</translation> <translation>Laitteet</translation>
</message> </message>
@ -1066,69 +1096,103 @@ Median koko: %2
<context> <context>
<name>message-description sent:</name> <name>message-description sent:</name>
<message> <message>
<location filename="../../src/Utils.h" line="+104"/> <location filename="../../src/Utils.h" line="+95"/>
<source>%1 an audio clip</source> <source>You sent an audio clip</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location line="+3"/> <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> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location line="+3"/> <location line="+3"/>
<source>%1 a file</source> <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> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location line="+3"/> <location line="+3"/>
<source>%1 a video clip</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> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location line="+3"/> <location line="+3"/>
<source>%1 a sticker</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> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location line="+3"/> <location line="+3"/>
<source>%1 a notification</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 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: %2</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location line="+7"/> <location line="+7"/>
<source>%1 an encrypted message</source> <source>You sent an encrypted message</source>
<translation type="unfinished"></translation>
</message>
<message>
<location line="+3"/>
<source>%1 sent an encrypted message</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
</context> </context>
<context> <context>
<name>message-description:</name> <name>popups::UserMentions</name>
<message> <message>
<location line="-26"/> <location filename="../../src/popups/UserMentions.cpp" line="+61"/>
<source>sent</source> <source>This Room</source>
<comment>For when someone else is the sender</comment>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
</context>
<context>
<name>message-description: </name>
<message> <message>
<location line="-2"/> <location line="+1"/>
<source>sent</source> <source>All Rooms</source>
<comment>For when you are the sender</comment>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
</context> </context>
<context> <context>
<name>utils</name> <name>utils</name>
<message> <message>
<location filename="../../src/Utils.cpp" line="+46"/> <location filename="../../src/Utils.cpp" line="+282"/>
<location filename="../../src/Utils.h" line="+55"/>
<source>You</source>
<translation>Sinä</translation>
</message>
<message>
<location line="+219"/>
<source>sent a file.</source> <source>sent a file.</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
@ -1148,7 +1212,7 @@ Median koko: %2
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location filename="../../src/Utils.h" line="-23"/> <location filename="../../src/Utils.h" line="+4"/>
<source>Unknown Message Type</source> <source>Unknown Message Type</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>

View File

@ -1,38 +1,15 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE TS> <!DOCTYPE TS>
<TS version="2.1" language="fr"> <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> <context>
<name>ChatPage</name> <name>ChatPage</name>
<message> <message>
<location filename="../../src/ChatPage.cpp" line="+330"/> <location filename="../../src/ChatPage.cpp" line="+346"/>
<source>Failed to upload image. Please try again.</source> <source>Failed to upload media. Please try again.</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location line="+45"/> <location line="+389"/>
<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>
<translation type="unfinished"></translation>
</message>
<message>
<location line="+380"/>
<source>Failed to restore OLM account. Please login again.</source> <source>Failed to restore OLM account. Please login again.</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
@ -42,18 +19,18 @@
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location line="+198"/> <location line="+181"/>
<source>Failed to setup encryption keys. Server response: %1 %2. Please try again later.</source> <source>Failed to setup encryption keys. Server response: %1 %2. Please try again later.</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location line="+51"/> <location line="+51"/>
<location line="+153"/> <location line="+155"/>
<source>Please try to login again: %1</source> <source>Please try to login again: %1</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location line="-45"/> <location line="-47"/>
<source>Room creation failed: %1</source> <source>Room creation failed: %1</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
@ -116,19 +93,11 @@
</message> </message>
</context> </context>
<context> <context>
<name>FileItem</name> <name>EncryptionIndicator</name>
<message> <message>
<location filename="../../src/timeline/widgets/FileItem.cpp" line="+107"/> <location filename="../qml/EncryptionIndicator.qml" line="+11"/>
<source>Save File</source> <source>Encrypted</source>
<translation>Enregistrer le fichier</translation> <translation type="unfinished"></translation>
</message>
</context>
<context>
<name>ImageItem</name>
<message>
<location filename="../../src/timeline/widgets/ImageItem.cpp" line="+241"/>
<source>Save image</source>
<translation>Enregistrer l&apos;image</translation>
</message> </message>
</context> </context>
<context> <context>
@ -200,7 +169,7 @@
<context> <context>
<name>MemberList</name> <name>MemberList</name>
<message> <message>
<location filename="../../src/dialogs/MemberList.cpp" line="+96"/> <location filename="../../src/dialogs/MemberList.cpp" line="+89"/>
<source>Room members</source> <source>Room members</source>
<translation>Membres du salon</translation> <translation>Membres du salon</translation>
</message> </message>
@ -210,6 +179,27 @@
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
</context> </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> <context>
<name>QuickSwitcher</name> <name>QuickSwitcher</name>
<message> <message>
@ -278,7 +268,7 @@
<context> <context>
<name>RoomInfo</name> <name>RoomInfo</name>
<message> <message>
<location filename="../../src/Cache.cpp" line="+2205"/> <location filename="../../src/Cache.cpp" line="+2307"/>
<source>no version stored</source> <source>no version stored</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
@ -286,12 +276,12 @@
<context> <context>
<name>RoomInfoListItem</name> <name>RoomInfoListItem</name>
<message> <message>
<location filename="../../src/RoomInfoListItem.cpp" line="+93"/> <location filename="../../src/RoomInfoListItem.cpp" line="+95"/>
<source>Leave room</source> <source>Leave room</source>
<translation>Quitter le salon</translation> <translation>Quitter le salon</translation>
</message> </message>
<message> <message>
<location line="+181"/> <location line="+161"/>
<source>Accept</source> <source>Accept</source>
<translation>Accepter</translation> <translation>Accepter</translation>
</message> </message>
@ -332,36 +322,36 @@
<context> <context>
<name>StatusIndicator</name> <name>StatusIndicator</name>
<message> <message>
<location filename="../../src/timeline/TimelineItem.cpp" line="+107"/> <location filename="../qml/StatusIndicator.qml" line="+13"/>
<source>Encrypted</source> <source>Failed</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location line="+3"/> <location line="+1"/>
<source>Delivered</source>
<translation type="unfinished"></translation>
</message>
<message>
<location line="+3"/>
<source>Seen</source>
<translation type="unfinished"></translation>
</message>
<message>
<location line="+3"/>
<source>Sent</source> <source>Sent</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message>
<location line="+1"/>
<source>Received</source>
<translation type="unfinished"></translation>
</message>
<message>
<location line="+1"/>
<source>Read</source>
<translation type="unfinished"></translation>
</message>
</context> </context>
<context> <context>
<name>TextInputWidget</name> <name>TextInputWidget</name>
<message> <message>
<location filename="../../src/TextInputWidget.cpp" line="+507"/> <location filename="../../src/TextInputWidget.cpp" line="+502"/>
<source>Send a file</source> <source>Send a file</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location line="+13"/> <location line="+13"/>
<location filename="../../src/TextInputWidget.h" line="+164"/> <location filename="../../src/TextInputWidget.h" line="+161"/>
<source>Write a message...</source> <source>Write a message...</source>
<translation>Écrivez un message...</translation> <translation>Écrivez un message...</translation>
</message> </message>
@ -376,7 +366,7 @@
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location line="+75"/> <location line="+72"/>
<source>Select a file</source> <source>Select a file</source>
<translation>Sélectionnez un fichier</translation> <translation>Sélectionnez un fichier</translation>
</message> </message>
@ -392,32 +382,9 @@
</message> </message>
</context> </context>
<context> <context>
<name>TimelineItem</name> <name>TimelineModel</name>
<message> <message>
<location filename="../../src/timeline/TimelineItem.cpp" line="+85"/> <location filename="../../src/timeline/TimelineModel.cpp" line="+835"/>
<source>Message redaction failed: %1</source>
<translation type="unfinished"></translation>
</message>
<message>
<location line="+39"/>
<source>Reply</source>
<translation type="unfinished"></translation>
</message>
<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 type="unfinished"></translation>
</message>
<message>
<location line="+65"/>
<source>-- Encrypted Event (No keys found for decryption) --</source> <source>-- Encrypted Event (No keys found for decryption) --</source>
<comment>Placeholder, when the message was not decrypted yet or can&apos;t be decrypted</comment> <comment>Placeholder, when the message was not decrypted yet or can&apos;t be decrypted</comment>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
@ -441,16 +408,87 @@
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location line="+27"/> <location line="+25"/>
<source>-- Encrypted Event (Unknown event type) --</source> <source>-- Encrypted Event (Unknown event type) --</source>
<comment>Placeholder, when the message was decrypted, but we couldn&apos;t parse it, because Nheko/mtxclient don&apos;t support that event type yet</comment> <comment>Placeholder, when the message was decrypted, but we couldn&apos;t parse it, because Nheko/mtxclient don&apos;t support that event type yet</comment>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message>
<location line="+54"/>
<source>Message redaction failed: %1</source>
<translation type="unfinished"></translation>
</message>
<message>
<location line="+453"/>
<source>Save image</source>
<translation type="unfinished">Enregistrer l&apos;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">Accusés de lecture</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>
<context> <context>
<name>TopRoomBar</name> <name>TopRoomBar</name>
<message> <message>
<location filename="../../src/TopRoomBar.cpp" line="+79"/> <location filename="../../src/TopRoomBar.cpp" line="+78"/>
<source>Room options</source> <source>Room options</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
@ -516,7 +554,7 @@
<context> <context>
<name>UserSettingsPage</name> <name>UserSettingsPage</name>
<message> <message>
<location filename="../../src/UserSettingsPage.cpp" line="+166"/> <location filename="../../src/UserSettingsPage.cpp" line="+171"/>
<source>Minimize to tray</source> <source>Minimize to tray</source>
<translation>Réduire à la barre des tâches</translation> <translation>Réduire à la barre des tâches</translation>
</message> </message>
@ -530,6 +568,11 @@
<source>Group&apos;s sidebar</source> <source>Group&apos;s sidebar</source>
<translation>Barre latérale des groupes</translation> <translation>Barre latérale des groupes</translation>
</message> </message>
<message>
<location line="+9"/>
<source>Circular Avatars</source>
<translation type="unfinished"></translation>
</message>
<message> <message>
<location line="+9"/> <location line="+9"/>
<source>Typing notifications</source> <source>Typing notifications</source>
@ -606,7 +649,7 @@
<translation>GÉNÉRAL</translation> <translation>GÉNÉRAL</translation>
</message> </message>
<message> <message>
<location line="+156"/> <location line="+161"/>
<source>Open Sessions File</source> <source>Open Sessions File</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
@ -826,7 +869,7 @@ Taille du média : %2
<context> <context>
<name>dialogs::ReadReceipts</name> <name>dialogs::ReadReceipts</name>
<message> <message>
<location filename="../../src/dialogs/ReadReceipts.cpp" line="+121"/> <location filename="../../src/dialogs/ReadReceipts.cpp" line="+117"/>
<source>Read receipts</source> <source>Read receipts</source>
<translation>Accusés de lecture</translation> <translation>Accusés de lecture</translation>
</message> </message>
@ -952,7 +995,7 @@ Taille du média : %2
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location line="+149"/> <location line="+148"/>
<source>Select an avatar</source> <source>Select an avatar</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
@ -978,19 +1021,6 @@ Taille du média : %2
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
</context> </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> <context>
<name>dialogs::UserProfile</name> <name>dialogs::UserProfile</name>
<message> <message>
@ -1014,7 +1044,7 @@ Taille du média : %2
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location line="+57"/> <location line="+56"/>
<source>Devices</source> <source>Devices</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
@ -1065,69 +1095,103 @@ Taille du média : %2
<context> <context>
<name>message-description sent:</name> <name>message-description sent:</name>
<message> <message>
<location filename="../../src/Utils.h" line="+104"/> <location filename="../../src/Utils.h" line="+95"/>
<source>%1 an audio clip</source> <source>You sent an audio clip</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location line="+3"/> <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> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location line="+3"/> <location line="+3"/>
<source>%1 a file</source> <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> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location line="+3"/> <location line="+3"/>
<source>%1 a video clip</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> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location line="+3"/> <location line="+3"/>
<source>%1 a sticker</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> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location line="+3"/> <location line="+3"/>
<source>%1 a notification</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 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: %2</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location line="+7"/> <location line="+7"/>
<source>%1 an encrypted message</source> <source>You sent an encrypted message</source>
<translation type="unfinished"></translation>
</message>
<message>
<location line="+3"/>
<source>%1 sent an encrypted message</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
</context> </context>
<context> <context>
<name>message-description:</name> <name>popups::UserMentions</name>
<message> <message>
<location line="-26"/> <location filename="../../src/popups/UserMentions.cpp" line="+61"/>
<source>sent</source> <source>This Room</source>
<comment>For when someone else is the sender</comment>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
</context>
<context>
<name>message-description: </name>
<message> <message>
<location line="-2"/> <location line="+1"/>
<source>sent</source> <source>All Rooms</source>
<comment>For when you are the sender</comment>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
</context> </context>
<context> <context>
<name>utils</name> <name>utils</name>
<message> <message>
<location filename="../../src/Utils.cpp" line="+46"/> <location filename="../../src/Utils.cpp" line="+282"/>
<location filename="../../src/Utils.h" line="+55"/>
<source>You</source>
<translation type="unfinished"></translation>
</message>
<message>
<location line="+219"/>
<source>sent a file.</source> <source>sent a file.</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
@ -1147,7 +1211,7 @@ Taille du média : %2
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location filename="../../src/Utils.h" line="-23"/> <location filename="../../src/Utils.h" line="+4"/>
<source>Unknown Message Type</source> <source>Unknown Message Type</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>

View File

@ -1,38 +1,15 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE TS> <!DOCTYPE TS>
<TS version="2.1" language="nl_NL"> <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> <context>
<name>ChatPage</name> <name>ChatPage</name>
<message> <message>
<location filename="../../src/ChatPage.cpp" line="+330"/> <location filename="../../src/ChatPage.cpp" line="+346"/>
<source>Failed to upload image. Please try again.</source> <source>Failed to upload media. Please try again.</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location line="+45"/> <location line="+389"/>
<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>
<translation type="unfinished"></translation>
</message>
<message>
<location line="+380"/>
<source>Failed to restore OLM account. Please login again.</source> <source>Failed to restore OLM account. Please login again.</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
@ -42,18 +19,18 @@
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location line="+198"/> <location line="+181"/>
<source>Failed to setup encryption keys. Server response: %1 %2. Please try again later.</source> <source>Failed to setup encryption keys. Server response: %1 %2. Please try again later.</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location line="+51"/> <location line="+51"/>
<location line="+153"/> <location line="+155"/>
<source>Please try to login again: %1</source> <source>Please try to login again: %1</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location line="-45"/> <location line="-47"/>
<source>Room creation failed: %1</source> <source>Room creation failed: %1</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
@ -116,19 +93,11 @@
</message> </message>
</context> </context>
<context> <context>
<name>FileItem</name> <name>EncryptionIndicator</name>
<message> <message>
<location filename="../../src/timeline/widgets/FileItem.cpp" line="+107"/> <location filename="../qml/EncryptionIndicator.qml" line="+11"/>
<source>Save File</source> <source>Encrypted</source>
<translation>Bestand opslaan</translation> <translation type="unfinished"></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>
</message> </message>
</context> </context>
<context> <context>
@ -200,7 +169,7 @@
<context> <context>
<name>MemberList</name> <name>MemberList</name>
<message> <message>
<location filename="../../src/dialogs/MemberList.cpp" line="+96"/> <location filename="../../src/dialogs/MemberList.cpp" line="+89"/>
<source>Room members</source> <source>Room members</source>
<translation>Kamerleden</translation> <translation>Kamerleden</translation>
</message> </message>
@ -210,6 +179,27 @@
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
</context> </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> <context>
<name>QuickSwitcher</name> <name>QuickSwitcher</name>
<message> <message>
@ -277,7 +267,7 @@
<context> <context>
<name>RoomInfo</name> <name>RoomInfo</name>
<message> <message>
<location filename="../../src/Cache.cpp" line="+2205"/> <location filename="../../src/Cache.cpp" line="+2307"/>
<source>no version stored</source> <source>no version stored</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
@ -285,12 +275,12 @@
<context> <context>
<name>RoomInfoListItem</name> <name>RoomInfoListItem</name>
<message> <message>
<location filename="../../src/RoomInfoListItem.cpp" line="+93"/> <location filename="../../src/RoomInfoListItem.cpp" line="+95"/>
<source>Leave room</source> <source>Leave room</source>
<translation>Kamer verlaten</translation> <translation>Kamer verlaten</translation>
</message> </message>
<message> <message>
<location line="+181"/> <location line="+161"/>
<source>Accept</source> <source>Accept</source>
<translation>Accepteren</translation> <translation>Accepteren</translation>
</message> </message>
@ -331,36 +321,36 @@
<context> <context>
<name>StatusIndicator</name> <name>StatusIndicator</name>
<message> <message>
<location filename="../../src/timeline/TimelineItem.cpp" line="+107"/> <location filename="../qml/StatusIndicator.qml" line="+13"/>
<source>Encrypted</source> <source>Failed</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location line="+3"/> <location line="+1"/>
<source>Delivered</source>
<translation type="unfinished"></translation>
</message>
<message>
<location line="+3"/>
<source>Seen</source>
<translation type="unfinished"></translation>
</message>
<message>
<location line="+3"/>
<source>Sent</source> <source>Sent</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message>
<location line="+1"/>
<source>Received</source>
<translation type="unfinished"></translation>
</message>
<message>
<location line="+1"/>
<source>Read</source>
<translation type="unfinished"></translation>
</message>
</context> </context>
<context> <context>
<name>TextInputWidget</name> <name>TextInputWidget</name>
<message> <message>
<location filename="../../src/TextInputWidget.cpp" line="+507"/> <location filename="../../src/TextInputWidget.cpp" line="+502"/>
<source>Send a file</source> <source>Send a file</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location line="+13"/> <location line="+13"/>
<location filename="../../src/TextInputWidget.h" line="+164"/> <location filename="../../src/TextInputWidget.h" line="+161"/>
<source>Write a message...</source> <source>Write a message...</source>
<translation>Typ een bericht...</translation> <translation>Typ een bericht...</translation>
</message> </message>
@ -375,7 +365,7 @@
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location line="+75"/> <location line="+72"/>
<source>Select a file</source> <source>Select a file</source>
<translation>Kies een bestand</translation> <translation>Kies een bestand</translation>
</message> </message>
@ -391,32 +381,9 @@
</message> </message>
</context> </context>
<context> <context>
<name>TimelineItem</name> <name>TimelineModel</name>
<message> <message>
<location filename="../../src/timeline/TimelineItem.cpp" line="+85"/> <location filename="../../src/timeline/TimelineModel.cpp" line="+835"/>
<source>Message redaction failed: %1</source>
<translation type="unfinished"></translation>
</message>
<message>
<location line="+39"/>
<source>Reply</source>
<translation type="unfinished"></translation>
</message>
<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 type="unfinished"></translation>
</message>
<message>
<location line="+65"/>
<source>-- Encrypted Event (No keys found for decryption) --</source> <source>-- Encrypted Event (No keys found for decryption) --</source>
<comment>Placeholder, when the message was not decrypted yet or can&apos;t be decrypted</comment> <comment>Placeholder, when the message was not decrypted yet or can&apos;t be decrypted</comment>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
@ -440,16 +407,87 @@
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location line="+27"/> <location line="+25"/>
<source>-- Encrypted Event (Unknown event type) --</source> <source>-- Encrypted Event (Unknown event type) --</source>
<comment>Placeholder, when the message was decrypted, but we couldn&apos;t parse it, because Nheko/mtxclient don&apos;t support that event type yet</comment> <comment>Placeholder, when the message was decrypted, but we couldn&apos;t parse it, because Nheko/mtxclient don&apos;t support that event type yet</comment>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message>
<location line="+54"/>
<source>Message redaction failed: %1</source>
<translation type="unfinished"></translation>
</message>
<message>
<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="+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">Leesbevestigingen</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>
<context> <context>
<name>TopRoomBar</name> <name>TopRoomBar</name>
<message> <message>
<location filename="../../src/TopRoomBar.cpp" line="+79"/> <location filename="../../src/TopRoomBar.cpp" line="+78"/>
<source>Room options</source> <source>Room options</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
@ -515,7 +553,7 @@
<context> <context>
<name>UserSettingsPage</name> <name>UserSettingsPage</name>
<message> <message>
<location filename="../../src/UserSettingsPage.cpp" line="+166"/> <location filename="../../src/UserSettingsPage.cpp" line="+171"/>
<source>Minimize to tray</source> <source>Minimize to tray</source>
<translation>Minimaliseren naar systeemvak</translation> <translation>Minimaliseren naar systeemvak</translation>
</message> </message>
@ -529,6 +567,11 @@
<source>Group&apos;s sidebar</source> <source>Group&apos;s sidebar</source>
<translation>Zijbalk van groep</translation> <translation>Zijbalk van groep</translation>
</message> </message>
<message>
<location line="+9"/>
<source>Circular Avatars</source>
<translation type="unfinished"></translation>
</message>
<message> <message>
<location line="+9"/> <location line="+9"/>
<source>Typing notifications</source> <source>Typing notifications</source>
@ -605,7 +648,7 @@
<translation>ALGEMEEN</translation> <translation>ALGEMEEN</translation>
</message> </message>
<message> <message>
<location line="+156"/> <location line="+161"/>
<source>Open Sessions File</source> <source>Open Sessions File</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
@ -825,7 +868,7 @@ Mediagrootte: %2
<context> <context>
<name>dialogs::ReadReceipts</name> <name>dialogs::ReadReceipts</name>
<message> <message>
<location filename="../../src/dialogs/ReadReceipts.cpp" line="+121"/> <location filename="../../src/dialogs/ReadReceipts.cpp" line="+117"/>
<source>Read receipts</source> <source>Read receipts</source>
<translation>Leesbevestigingen</translation> <translation>Leesbevestigingen</translation>
</message> </message>
@ -951,7 +994,7 @@ Mediagrootte: %2
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location line="+149"/> <location line="+148"/>
<source>Select an avatar</source> <source>Select an avatar</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
@ -977,19 +1020,6 @@ Mediagrootte: %2
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
</context> </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> <context>
<name>dialogs::UserProfile</name> <name>dialogs::UserProfile</name>
<message> <message>
@ -1013,7 +1043,7 @@ Mediagrootte: %2
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location line="+57"/> <location line="+56"/>
<source>Devices</source> <source>Devices</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
@ -1064,69 +1094,103 @@ Mediagrootte: %2
<context> <context>
<name>message-description sent:</name> <name>message-description sent:</name>
<message> <message>
<location filename="../../src/Utils.h" line="+104"/> <location filename="../../src/Utils.h" line="+95"/>
<source>%1 an audio clip</source> <source>You sent an audio clip</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location line="+3"/> <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> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location line="+3"/> <location line="+3"/>
<source>%1 a file</source> <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> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location line="+3"/> <location line="+3"/>
<source>%1 a video clip</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> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location line="+3"/> <location line="+3"/>
<source>%1 a sticker</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> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location line="+3"/> <location line="+3"/>
<source>%1 a notification</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 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: %2</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location line="+7"/> <location line="+7"/>
<source>%1 an encrypted message</source> <source>You sent an encrypted message</source>
<translation type="unfinished"></translation>
</message>
<message>
<location line="+3"/>
<source>%1 sent an encrypted message</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
</context> </context>
<context> <context>
<name>message-description:</name> <name>popups::UserMentions</name>
<message> <message>
<location line="-26"/> <location filename="../../src/popups/UserMentions.cpp" line="+61"/>
<source>sent</source> <source>This Room</source>
<comment>For when someone else is the sender</comment>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
</context>
<context>
<name>message-description: </name>
<message> <message>
<location line="-2"/> <location line="+1"/>
<source>sent</source> <source>All Rooms</source>
<comment>For when you are the sender</comment>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
</context> </context>
<context> <context>
<name>utils</name> <name>utils</name>
<message> <message>
<location filename="../../src/Utils.cpp" line="+46"/> <location filename="../../src/Utils.cpp" line="+282"/>
<location filename="../../src/Utils.h" line="+55"/>
<source>You</source>
<translation type="unfinished"></translation>
</message>
<message>
<location line="+219"/>
<source>sent a file.</source> <source>sent a file.</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
@ -1146,7 +1210,7 @@ Mediagrootte: %2
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location filename="../../src/Utils.h" line="-23"/> <location filename="../../src/Utils.h" line="+4"/>
<source>Unknown Message Type</source> <source>Unknown Message Type</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>

View File

@ -1,38 +1,15 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE TS> <!DOCTYPE TS>
<TS version="2.1" language="pl"> <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> <context>
<name>ChatPage</name> <name>ChatPage</name>
<message> <message>
<location filename="../../src/ChatPage.cpp" line="+330"/> <location filename="../../src/ChatPage.cpp" line="+346"/>
<source>Failed to upload image. Please try again.</source> <source>Failed to upload media. Please try again.</source>
<translation>Nie udało się wysłać obrazu. Spróbuj ponownie.</translation> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location line="+45"/> <location line="+389"/>
<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>
</message>
<message>
<location line="+380"/>
<source>Failed to restore OLM account. Please login again.</source> <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> <translation>Nie udało się przywrócić konta OLM. Spróbuj zalogować się ponownie.</translation>
</message> </message>
@ -42,18 +19,18 @@
<translation>Nie udało się przywrócić zapisanych danych. Spróbuj zalogować się ponownie.</translation> <translation>Nie udało się przywrócić zapisanych danych. Spróbuj zalogować się ponownie.</translation>
</message> </message>
<message> <message>
<location line="+198"/> <location line="+181"/>
<source>Failed to setup encryption keys. Server response: %1 %2. Please try again later.</source> <source>Failed to setup encryption keys. Server response: %1 %2. Please try again later.</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location line="+51"/> <location line="+51"/>
<location line="+153"/> <location line="+155"/>
<source>Please try to login again: %1</source> <source>Please try to login again: %1</source>
<translation>Spróbuj zalogować się ponownie: %1</translation> <translation>Spróbuj zalogować się ponownie: %1</translation>
</message> </message>
<message> <message>
<location line="-45"/> <location line="-47"/>
<source>Room creation failed: %1</source> <source>Room creation failed: %1</source>
<translation>Tworzenie pokoju nie powiodło się: %1</translation> <translation>Tworzenie pokoju nie powiodło się: %1</translation>
</message> </message>
@ -116,19 +93,11 @@
</message> </message>
</context> </context>
<context> <context>
<name>FileItem</name> <name>EncryptionIndicator</name>
<message> <message>
<location filename="../../src/timeline/widgets/FileItem.cpp" line="+107"/> <location filename="../qml/EncryptionIndicator.qml" line="+11"/>
<source>Save File</source> <source>Encrypted</source>
<translation>Zapisz plik</translation> <translation type="unfinished"></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>
</message> </message>
</context> </context>
<context> <context>
@ -200,7 +169,7 @@
<context> <context>
<name>MemberList</name> <name>MemberList</name>
<message> <message>
<location filename="../../src/dialogs/MemberList.cpp" line="+96"/> <location filename="../../src/dialogs/MemberList.cpp" line="+89"/>
<source>Room members</source> <source>Room members</source>
<translation>Członkowie pokoju</translation> <translation>Członkowie pokoju</translation>
</message> </message>
@ -210,6 +179,27 @@
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
</context> </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> <context>
<name>QuickSwitcher</name> <name>QuickSwitcher</name>
<message> <message>
@ -277,7 +267,7 @@
<context> <context>
<name>RoomInfo</name> <name>RoomInfo</name>
<message> <message>
<location filename="../../src/Cache.cpp" line="+2205"/> <location filename="../../src/Cache.cpp" line="+2307"/>
<source>no version stored</source> <source>no version stored</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
@ -285,12 +275,12 @@
<context> <context>
<name>RoomInfoListItem</name> <name>RoomInfoListItem</name>
<message> <message>
<location filename="../../src/RoomInfoListItem.cpp" line="+93"/> <location filename="../../src/RoomInfoListItem.cpp" line="+95"/>
<source>Leave room</source> <source>Leave room</source>
<translation>Opuść pokój</translation> <translation>Opuść pokój</translation>
</message> </message>
<message> <message>
<location line="+181"/> <location line="+161"/>
<source>Accept</source> <source>Accept</source>
<translation>Akceptuj</translation> <translation>Akceptuj</translation>
</message> </message>
@ -331,36 +321,36 @@
<context> <context>
<name>StatusIndicator</name> <name>StatusIndicator</name>
<message> <message>
<location filename="../../src/timeline/TimelineItem.cpp" line="+107"/> <location filename="../qml/StatusIndicator.qml" line="+13"/>
<source>Encrypted</source> <source>Failed</source>
<translation>Szyfrowana</translation> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location line="+3"/> <location line="+1"/>
<source>Delivered</source>
<translation>Dostarczono</translation>
</message>
<message>
<location line="+3"/>
<source>Seen</source>
<translation>Wyświetlona</translation>
</message>
<message>
<location line="+3"/>
<source>Sent</source> <source>Sent</source>
<translation>Wysłana</translation> <translation type="unfinished"></translation>
</message>
<message>
<location line="+1"/>
<source>Received</source>
<translation type="unfinished"></translation>
</message>
<message>
<location line="+1"/>
<source>Read</source>
<translation type="unfinished"></translation>
</message> </message>
</context> </context>
<context> <context>
<name>TextInputWidget</name> <name>TextInputWidget</name>
<message> <message>
<location filename="../../src/TextInputWidget.cpp" line="+507"/> <location filename="../../src/TextInputWidget.cpp" line="+502"/>
<source>Send a file</source> <source>Send a file</source>
<translation>Wyślij plik</translation> <translation>Wyślij plik</translation>
</message> </message>
<message> <message>
<location line="+13"/> <location line="+13"/>
<location filename="../../src/TextInputWidget.h" line="+164"/> <location filename="../../src/TextInputWidget.h" line="+161"/>
<source>Write a message...</source> <source>Write a message...</source>
<translation>Napisz wiadomość</translation> <translation>Napisz wiadomość</translation>
</message> </message>
@ -375,7 +365,7 @@
<translation>Emoji</translation> <translation>Emoji</translation>
</message> </message>
<message> <message>
<location line="+75"/> <location line="+72"/>
<source>Select a file</source> <source>Select a file</source>
<translation>Wybierz plik</translation> <translation>Wybierz plik</translation>
</message> </message>
@ -391,32 +381,9 @@
</message> </message>
</context> </context>
<context> <context>
<name>TimelineItem</name> <name>TimelineModel</name>
<message> <message>
<location filename="../../src/timeline/TimelineItem.cpp" line="+85"/> <location filename="../../src/timeline/TimelineModel.cpp" line="+835"/>
<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>
<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"/>
<source>-- Encrypted Event (No keys found for decryption) --</source> <source>-- Encrypted Event (No keys found for decryption) --</source>
<comment>Placeholder, when the message was not decrypted yet or can&apos;t be decrypted</comment> <comment>Placeholder, when the message was not decrypted yet or can&apos;t be decrypted</comment>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
@ -440,16 +407,87 @@
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location line="+27"/> <location line="+25"/>
<source>-- Encrypted Event (Unknown event type) --</source> <source>-- Encrypted Event (Unknown event type) --</source>
<comment>Placeholder, when the message was decrypted, but we couldn&apos;t parse it, because Nheko/mtxclient don&apos;t support that event type yet</comment> <comment>Placeholder, when the message was decrypted, but we couldn&apos;t parse it, because Nheko/mtxclient don&apos;t support that event type yet</comment>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </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>
<context> <context>
<name>TopRoomBar</name> <name>TopRoomBar</name>
<message> <message>
<location filename="../../src/TopRoomBar.cpp" line="+79"/> <location filename="../../src/TopRoomBar.cpp" line="+78"/>
<source>Room options</source> <source>Room options</source>
<translation>Ustawienia pokoju</translation> <translation>Ustawienia pokoju</translation>
</message> </message>
@ -516,7 +554,7 @@
<context> <context>
<name>UserSettingsPage</name> <name>UserSettingsPage</name>
<message> <message>
<location filename="../../src/UserSettingsPage.cpp" line="+166"/> <location filename="../../src/UserSettingsPage.cpp" line="+171"/>
<source>Minimize to tray</source> <source>Minimize to tray</source>
<translation>Zminimalizuj do paska zadań</translation> <translation>Zminimalizuj do paska zadań</translation>
</message> </message>
@ -530,6 +568,11 @@
<source>Group&apos;s sidebar</source> <source>Group&apos;s sidebar</source>
<translation>Pasek boczny grupy</translation> <translation>Pasek boczny grupy</translation>
</message> </message>
<message>
<location line="+9"/>
<source>Circular Avatars</source>
<translation type="unfinished"></translation>
</message>
<message> <message>
<location line="+9"/> <location line="+9"/>
<source>Typing notifications</source> <source>Typing notifications</source>
@ -606,7 +649,7 @@
<translation>OGÓLNE</translation> <translation>OGÓLNE</translation>
</message> </message>
<message> <message>
<location line="+156"/> <location line="+161"/>
<source>Open Sessions File</source> <source>Open Sessions File</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
@ -826,7 +869,7 @@ Rozmiar multimediów: %2
<context> <context>
<name>dialogs::ReadReceipts</name> <name>dialogs::ReadReceipts</name>
<message> <message>
<location filename="../../src/dialogs/ReadReceipts.cpp" line="+121"/> <location filename="../../src/dialogs/ReadReceipts.cpp" line="+117"/>
<source>Read receipts</source> <source>Read receipts</source>
<translation>Potwierdzenia przeczytania</translation> <translation>Potwierdzenia przeczytania</translation>
</message> </message>
@ -955,7 +998,7 @@ Rozmiar multimediów: %2
<translation>Nie udało się włączyć szyfrowania: %1</translation> <translation>Nie udało się włączyć szyfrowania: %1</translation>
</message> </message>
<message> <message>
<location line="+149"/> <location line="+148"/>
<source>Select an avatar</source> <source>Select an avatar</source>
<translation>Wybierz awatar</translation> <translation>Wybierz awatar</translation>
</message> </message>
@ -981,19 +1024,6 @@ Rozmiar multimediów: %2
<translation>Nie udało się wysłać obrazu: %s</translation> <translation>Nie udało się wysłać obrazu: %s</translation>
</message> </message>
</context> </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> <context>
<name>dialogs::UserProfile</name> <name>dialogs::UserProfile</name>
<message> <message>
@ -1017,7 +1047,7 @@ Rozmiar multimediów: %2
<translation>Rozpocznij rozmowę</translation> <translation>Rozpocznij rozmowę</translation>
</message> </message>
<message> <message>
<location line="+57"/> <location line="+56"/>
<source>Devices</source> <source>Devices</source>
<translation>Urządzenia</translation> <translation>Urządzenia</translation>
</message> </message>
@ -1068,69 +1098,103 @@ Rozmiar multimediów: %2
<context> <context>
<name>message-description sent:</name> <name>message-description sent:</name>
<message> <message>
<location filename="../../src/Utils.h" line="+104"/> <location filename="../../src/Utils.h" line="+95"/>
<source>%1 an audio clip</source> <source>You sent an audio clip</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location line="+3"/> <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> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location line="+3"/> <location line="+3"/>
<source>%1 a file</source> <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> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location line="+3"/> <location line="+3"/>
<source>%1 a video clip</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> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location line="+3"/> <location line="+3"/>
<source>%1 a sticker</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> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location line="+3"/> <location line="+3"/>
<source>%1 a notification</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 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: %2</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location line="+7"/> <location line="+7"/>
<source>%1 an encrypted message</source> <source>You sent an encrypted message</source>
<translation type="unfinished"></translation>
</message>
<message>
<location line="+3"/>
<source>%1 sent an encrypted message</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
</context> </context>
<context> <context>
<name>message-description:</name> <name>popups::UserMentions</name>
<message> <message>
<location line="-26"/> <location filename="../../src/popups/UserMentions.cpp" line="+61"/>
<source>sent</source> <source>This Room</source>
<comment>For when someone else is the sender</comment>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
</context>
<context>
<name>message-description: </name>
<message> <message>
<location line="-2"/> <location line="+1"/>
<source>sent</source> <source>All Rooms</source>
<comment>For when you are the sender</comment>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
</context> </context>
<context> <context>
<name>utils</name> <name>utils</name>
<message> <message>
<location filename="../../src/Utils.cpp" line="+46"/> <location filename="../../src/Utils.cpp" line="+282"/>
<location filename="../../src/Utils.h" line="+55"/>
<source>You</source>
<translation type="unfinished"></translation>
</message>
<message>
<location line="+219"/>
<source>sent a file.</source> <source>sent a file.</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
@ -1150,7 +1214,7 @@ Rozmiar multimediów: %2
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location filename="../../src/Utils.h" line="-23"/> <location filename="../../src/Utils.h" line="+4"/>
<source>Unknown Message Type</source> <source>Unknown Message Type</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>

View File

@ -1,38 +1,15 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE TS> <!DOCTYPE TS>
<TS version="2.1" language="ru"> <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> <context>
<name>ChatPage</name> <name>ChatPage</name>
<message> <message>
<location filename="../../src/ChatPage.cpp" line="+330"/> <location filename="../../src/ChatPage.cpp" line="+346"/>
<source>Failed to upload image. Please try again.</source> <source>Failed to upload media. Please try again.</source>
<translation>Не удалось загрузить изображение. Пожалуйста, попробуйте еще раз.</translation> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location line="+45"/> <location line="+389"/>
<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>
</message>
<message>
<location line="+380"/>
<source>Failed to restore OLM account. Please login again.</source> <source>Failed to restore OLM account. Please login again.</source>
<translation>Не удалось восстановить учетную запись OLM. Пожалуйста, войдите снова.</translation> <translation>Не удалось восстановить учетную запись OLM. Пожалуйста, войдите снова.</translation>
</message> </message>
@ -42,18 +19,18 @@
<translation>Не удалось восстановить сохраненные данные. Пожалуйста, войдите снова.</translation> <translation>Не удалось восстановить сохраненные данные. Пожалуйста, войдите снова.</translation>
</message> </message>
<message> <message>
<location line="+198"/> <location line="+181"/>
<source>Failed to setup encryption keys. Server response: %1 %2. Please try again later.</source> <source>Failed to setup encryption keys. Server response: %1 %2. Please try again later.</source>
<translation>Не удалось настроить ключи шифрования. Ответ сервера:%1 %2. Пожалуйста, попробуйте позже.</translation> <translation>Не удалось настроить ключи шифрования. Ответ сервера:%1 %2. Пожалуйста, попробуйте позже.</translation>
</message> </message>
<message> <message>
<location line="+51"/> <location line="+51"/>
<location line="+153"/> <location line="+155"/>
<source>Please try to login again: %1</source> <source>Please try to login again: %1</source>
<translation>Повторите попытку входа: %1</translation> <translation>Повторите попытку входа: %1</translation>
</message> </message>
<message> <message>
<location line="-45"/> <location line="-47"/>
<source>Room creation failed: %1</source> <source>Room creation failed: %1</source>
<translation>Не удалось создать комнату: %1</translation> <translation>Не удалось создать комнату: %1</translation>
</message> </message>
@ -116,19 +93,11 @@
</message> </message>
</context> </context>
<context> <context>
<name>FileItem</name> <name>EncryptionIndicator</name>
<message> <message>
<location filename="../../src/timeline/widgets/FileItem.cpp" line="+107"/> <location filename="../qml/EncryptionIndicator.qml" line="+11"/>
<source>Save File</source> <source>Encrypted</source>
<translation>Сохранить файл</translation> <translation type="unfinished"></translation>
</message>
</context>
<context>
<name>ImageItem</name>
<message>
<location filename="../../src/timeline/widgets/ImageItem.cpp" line="+241"/>
<source>Save image</source>
<translation>Сохранить изображение</translation>
</message> </message>
</context> </context>
<context> <context>
@ -200,7 +169,7 @@
<context> <context>
<name>MemberList</name> <name>MemberList</name>
<message> <message>
<location filename="../../src/dialogs/MemberList.cpp" line="+96"/> <location filename="../../src/dialogs/MemberList.cpp" line="+89"/>
<source>Room members</source> <source>Room members</source>
<translation>Участники комнаты</translation> <translation>Участники комнаты</translation>
</message> </message>
@ -210,6 +179,27 @@
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
</context> </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> <context>
<name>QuickSwitcher</name> <name>QuickSwitcher</name>
<message> <message>
@ -277,7 +267,7 @@
<context> <context>
<name>RoomInfo</name> <name>RoomInfo</name>
<message> <message>
<location filename="../../src/Cache.cpp" line="+2205"/> <location filename="../../src/Cache.cpp" line="+2307"/>
<source>no version stored</source> <source>no version stored</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
@ -285,12 +275,12 @@
<context> <context>
<name>RoomInfoListItem</name> <name>RoomInfoListItem</name>
<message> <message>
<location filename="../../src/RoomInfoListItem.cpp" line="+93"/> <location filename="../../src/RoomInfoListItem.cpp" line="+95"/>
<source>Leave room</source> <source>Leave room</source>
<translation>Покинуть комнату</translation> <translation>Покинуть комнату</translation>
</message> </message>
<message> <message>
<location line="+181"/> <location line="+161"/>
<source>Accept</source> <source>Accept</source>
<translation>Принять</translation> <translation>Принять</translation>
</message> </message>
@ -331,36 +321,36 @@
<context> <context>
<name>StatusIndicator</name> <name>StatusIndicator</name>
<message> <message>
<location filename="../../src/timeline/TimelineItem.cpp" line="+107"/> <location filename="../qml/StatusIndicator.qml" line="+13"/>
<source>Encrypted</source> <source>Failed</source>
<translation>Зашифровано</translation> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location line="+3"/> <location line="+1"/>
<source>Delivered</source>
<translation>Доставлено</translation>
</message>
<message>
<location line="+3"/>
<source>Seen</source>
<translation>Прочитано</translation>
</message>
<message>
<location line="+3"/>
<source>Sent</source> <source>Sent</source>
<translation>Отправлено</translation> <translation type="unfinished"></translation>
</message>
<message>
<location line="+1"/>
<source>Received</source>
<translation type="unfinished"></translation>
</message>
<message>
<location line="+1"/>
<source>Read</source>
<translation type="unfinished"></translation>
</message> </message>
</context> </context>
<context> <context>
<name>TextInputWidget</name> <name>TextInputWidget</name>
<message> <message>
<location filename="../../src/TextInputWidget.cpp" line="+507"/> <location filename="../../src/TextInputWidget.cpp" line="+502"/>
<source>Send a file</source> <source>Send a file</source>
<translation>Отправить файл</translation> <translation>Отправить файл</translation>
</message> </message>
<message> <message>
<location line="+13"/> <location line="+13"/>
<location filename="../../src/TextInputWidget.h" line="+164"/> <location filename="../../src/TextInputWidget.h" line="+161"/>
<source>Write a message...</source> <source>Write a message...</source>
<translation>Написать сообщение...</translation> <translation>Написать сообщение...</translation>
</message> </message>
@ -375,7 +365,7 @@
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location line="+75"/> <location line="+72"/>
<source>Select a file</source> <source>Select a file</source>
<translation>Выберите файл</translation> <translation>Выберите файл</translation>
</message> </message>
@ -391,32 +381,9 @@
</message> </message>
</context> </context>
<context> <context>
<name>TimelineItem</name> <name>TimelineModel</name>
<message> <message>
<location filename="../../src/timeline/TimelineItem.cpp" line="+85"/> <location filename="../../src/timeline/TimelineModel.cpp" line="+835"/>
<source>Message redaction failed: %1</source>
<translation>Ошибка редактирования сообщения: %1</translation>
</message>
<message>
<location line="+39"/>
<source>Reply</source>
<translation type="unfinished"></translation>
</message>
<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"/>
<source>-- Encrypted Event (No keys found for decryption) --</source> <source>-- Encrypted Event (No keys found for decryption) --</source>
<comment>Placeholder, when the message was not decrypted yet or can&apos;t be decrypted</comment> <comment>Placeholder, when the message was not decrypted yet or can&apos;t be decrypted</comment>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
@ -440,16 +407,87 @@
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location line="+27"/> <location line="+25"/>
<source>-- Encrypted Event (Unknown event type) --</source> <source>-- Encrypted Event (Unknown event type) --</source>
<comment>Placeholder, when the message was decrypted, but we couldn&apos;t parse it, because Nheko/mtxclient don&apos;t support that event type yet</comment> <comment>Placeholder, when the message was decrypted, but we couldn&apos;t parse it, because Nheko/mtxclient don&apos;t support that event type yet</comment>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </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>
<context> <context>
<name>TopRoomBar</name> <name>TopRoomBar</name>
<message> <message>
<location filename="../../src/TopRoomBar.cpp" line="+79"/> <location filename="../../src/TopRoomBar.cpp" line="+78"/>
<source>Room options</source> <source>Room options</source>
<translation>Настройки комнаты</translation> <translation>Настройки комнаты</translation>
</message> </message>
@ -516,7 +554,7 @@
<context> <context>
<name>UserSettingsPage</name> <name>UserSettingsPage</name>
<message> <message>
<location filename="../../src/UserSettingsPage.cpp" line="+166"/> <location filename="../../src/UserSettingsPage.cpp" line="+171"/>
<source>Minimize to tray</source> <source>Minimize to tray</source>
<translation>Сворачивать в системную панель</translation> <translation>Сворачивать в системную панель</translation>
</message> </message>
@ -530,6 +568,11 @@
<source>Group&apos;s sidebar</source> <source>Group&apos;s sidebar</source>
<translation>Боковая панель групп</translation> <translation>Боковая панель групп</translation>
</message> </message>
<message>
<location line="+9"/>
<source>Circular Avatars</source>
<translation type="unfinished"></translation>
</message>
<message> <message>
<location line="+9"/> <location line="+9"/>
<source>Typing notifications</source> <source>Typing notifications</source>
@ -606,7 +649,7 @@
<translation>ГЛАВНОЕ</translation> <translation>ГЛАВНОЕ</translation>
</message> </message>
<message> <message>
<location line="+156"/> <location line="+161"/>
<source>Open Sessions File</source> <source>Open Sessions File</source>
<translation>Открыть файл сеансов</translation> <translation>Открыть файл сеансов</translation>
</message> </message>
@ -827,7 +870,7 @@ Media size: %2
<context> <context>
<name>dialogs::ReadReceipts</name> <name>dialogs::ReadReceipts</name>
<message> <message>
<location filename="../../src/dialogs/ReadReceipts.cpp" line="+121"/> <location filename="../../src/dialogs/ReadReceipts.cpp" line="+117"/>
<source>Read receipts</source> <source>Read receipts</source>
<translation>Подтверждать прочтение</translation> <translation>Подтверждать прочтение</translation>
</message> </message>
@ -954,7 +997,7 @@ Media size: %2
<translation>Не удалось включить шифрование: %1</translation> <translation>Не удалось включить шифрование: %1</translation>
</message> </message>
<message> <message>
<location line="+149"/> <location line="+148"/>
<source>Select an avatar</source> <source>Select an avatar</source>
<translation>Выберите аватар</translation> <translation>Выберите аватар</translation>
</message> </message>
@ -980,19 +1023,6 @@ Media size: %2
<translation>Не удалось загрузить изображение: %s</translation> <translation>Не удалось загрузить изображение: %s</translation>
</message> </message>
</context> </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> <context>
<name>dialogs::UserProfile</name> <name>dialogs::UserProfile</name>
<message> <message>
@ -1016,7 +1046,7 @@ Media size: %2
<translation>Начать разговор</translation> <translation>Начать разговор</translation>
</message> </message>
<message> <message>
<location line="+57"/> <location line="+56"/>
<source>Devices</source> <source>Devices</source>
<translation>Устройства</translation> <translation>Устройства</translation>
</message> </message>
@ -1067,69 +1097,103 @@ Media size: %2
<context> <context>
<name>message-description sent:</name> <name>message-description sent:</name>
<message> <message>
<location filename="../../src/Utils.h" line="+104"/> <location filename="../../src/Utils.h" line="+95"/>
<source>%1 an audio clip</source> <source>You sent an audio clip</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location line="+3"/> <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> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location line="+3"/> <location line="+3"/>
<source>%1 a file</source> <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> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location line="+3"/> <location line="+3"/>
<source>%1 a video clip</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> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location line="+3"/> <location line="+3"/>
<source>%1 a sticker</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> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location line="+3"/> <location line="+3"/>
<source>%1 a notification</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 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: %2</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location line="+7"/> <location line="+7"/>
<source>%1 an encrypted message</source> <source>You sent an encrypted message</source>
<translation type="unfinished"></translation>
</message>
<message>
<location line="+3"/>
<source>%1 sent an encrypted message</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
</context> </context>
<context> <context>
<name>message-description:</name> <name>popups::UserMentions</name>
<message> <message>
<location line="-26"/> <location filename="../../src/popups/UserMentions.cpp" line="+61"/>
<source>sent</source> <source>This Room</source>
<comment>For when someone else is the sender</comment>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
</context>
<context>
<name>message-description: </name>
<message> <message>
<location line="-2"/> <location line="+1"/>
<source>sent</source> <source>All Rooms</source>
<comment>For when you are the sender</comment>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
</context> </context>
<context> <context>
<name>utils</name> <name>utils</name>
<message> <message>
<location filename="../../src/Utils.cpp" line="+46"/> <location filename="../../src/Utils.cpp" line="+282"/>
<location filename="../../src/Utils.h" line="+55"/>
<source>You</source>
<translation type="unfinished"></translation>
</message>
<message>
<location line="+219"/>
<source>sent a file.</source> <source>sent a file.</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
@ -1149,7 +1213,7 @@ Media size: %2
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location filename="../../src/Utils.h" line="-23"/> <location filename="../../src/Utils.h" line="+4"/>
<source>Unknown Message Type</source> <source>Unknown Message Type</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>

View File

@ -1,38 +1,15 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE TS> <!DOCTYPE TS>
<TS version="2.1" language="zh_CN"> <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> <context>
<name>ChatPage</name> <name>ChatPage</name>
<message> <message>
<location filename="../../src/ChatPage.cpp" line="+330"/> <location filename="../../src/ChatPage.cpp" line="+346"/>
<source>Failed to upload image. Please try again.</source> <source>Failed to upload media. Please try again.</source>
<translation></translation> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location line="+45"/> <location line="+389"/>
<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>
</message>
<message>
<location line="+380"/>
<source>Failed to restore OLM account. Please login again.</source> <source>Failed to restore OLM account. Please login again.</source>
<translation> OLM </translation> <translation> OLM </translation>
</message> </message>
@ -42,18 +19,18 @@
<translation></translation> <translation></translation>
</message> </message>
<message> <message>
<location line="+198"/> <location line="+181"/>
<source>Failed to setup encryption keys. Server response: %1 %2. Please try again later.</source> <source>Failed to setup encryption keys. Server response: %1 %2. Please try again later.</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location line="+51"/> <location line="+51"/>
<location line="+153"/> <location line="+155"/>
<source>Please try to login again: %1</source> <source>Please try to login again: %1</source>
<translation>%1</translation> <translation>%1</translation>
</message> </message>
<message> <message>
<location line="-45"/> <location line="-47"/>
<source>Room creation failed: %1</source> <source>Room creation failed: %1</source>
<translation>%1</translation> <translation>%1</translation>
</message> </message>
@ -116,19 +93,11 @@
</message> </message>
</context> </context>
<context> <context>
<name>FileItem</name> <name>EncryptionIndicator</name>
<message> <message>
<location filename="../../src/timeline/widgets/FileItem.cpp" line="+107"/> <location filename="../qml/EncryptionIndicator.qml" line="+11"/>
<source>Save File</source> <source>Encrypted</source>
<translation></translation> <translation type="unfinished"></translation>
</message>
</context>
<context>
<name>ImageItem</name>
<message>
<location filename="../../src/timeline/widgets/ImageItem.cpp" line="+241"/>
<source>Save image</source>
<translation></translation>
</message> </message>
</context> </context>
<context> <context>
@ -200,7 +169,7 @@
<context> <context>
<name>MemberList</name> <name>MemberList</name>
<message> <message>
<location filename="../../src/dialogs/MemberList.cpp" line="+96"/> <location filename="../../src/dialogs/MemberList.cpp" line="+89"/>
<source>Room members</source> <source>Room members</source>
<translation></translation> <translation></translation>
</message> </message>
@ -210,6 +179,27 @@
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
</context> </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> <context>
<name>QuickSwitcher</name> <name>QuickSwitcher</name>
<message> <message>
@ -277,7 +267,7 @@
<context> <context>
<name>RoomInfo</name> <name>RoomInfo</name>
<message> <message>
<location filename="../../src/Cache.cpp" line="+2205"/> <location filename="../../src/Cache.cpp" line="+2307"/>
<source>no version stored</source> <source>no version stored</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
@ -285,12 +275,12 @@
<context> <context>
<name>RoomInfoListItem</name> <name>RoomInfoListItem</name>
<message> <message>
<location filename="../../src/RoomInfoListItem.cpp" line="+93"/> <location filename="../../src/RoomInfoListItem.cpp" line="+95"/>
<source>Leave room</source> <source>Leave room</source>
<translation></translation> <translation></translation>
</message> </message>
<message> <message>
<location line="+181"/> <location line="+161"/>
<source>Accept</source> <source>Accept</source>
<translation></translation> <translation></translation>
</message> </message>
@ -331,36 +321,36 @@
<context> <context>
<name>StatusIndicator</name> <name>StatusIndicator</name>
<message> <message>
<location filename="../../src/timeline/TimelineItem.cpp" line="+107"/> <location filename="../qml/StatusIndicator.qml" line="+13"/>
<source>Encrypted</source> <source>Failed</source>
<translation></translation> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location line="+3"/> <location line="+1"/>
<source>Delivered</source>
<translation></translation>
</message>
<message>
<location line="+3"/>
<source>Seen</source>
<translation></translation>
</message>
<message>
<location line="+3"/>
<source>Sent</source> <source>Sent</source>
<translation></translation> <translation type="unfinished"></translation>
</message>
<message>
<location line="+1"/>
<source>Received</source>
<translation type="unfinished"></translation>
</message>
<message>
<location line="+1"/>
<source>Read</source>
<translation type="unfinished"></translation>
</message> </message>
</context> </context>
<context> <context>
<name>TextInputWidget</name> <name>TextInputWidget</name>
<message> <message>
<location filename="../../src/TextInputWidget.cpp" line="+507"/> <location filename="../../src/TextInputWidget.cpp" line="+502"/>
<source>Send a file</source> <source>Send a file</source>
<translation></translation> <translation></translation>
</message> </message>
<message> <message>
<location line="+13"/> <location line="+13"/>
<location filename="../../src/TextInputWidget.h" line="+164"/> <location filename="../../src/TextInputWidget.h" line="+161"/>
<source>Write a message...</source> <source>Write a message...</source>
<translation>...</translation> <translation>...</translation>
</message> </message>
@ -375,7 +365,7 @@
<translation></translation> <translation></translation>
</message> </message>
<message> <message>
<location line="+75"/> <location line="+72"/>
<source>Select a file</source> <source>Select a file</source>
<translation></translation> <translation></translation>
</message> </message>
@ -391,32 +381,9 @@
</message> </message>
</context> </context>
<context> <context>
<name>TimelineItem</name> <name>TimelineModel</name>
<message> <message>
<location filename="../../src/timeline/TimelineItem.cpp" line="+85"/> <location filename="../../src/timeline/TimelineModel.cpp" line="+835"/>
<source>Message redaction failed: %1</source>
<translation>%1</translation>
</message>
<message>
<location line="+39"/>
<source>Reply</source>
<translation type="unfinished"></translation>
</message>
<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"/>
<source>-- Encrypted Event (No keys found for decryption) --</source> <source>-- Encrypted Event (No keys found for decryption) --</source>
<comment>Placeholder, when the message was not decrypted yet or can&apos;t be decrypted</comment> <comment>Placeholder, when the message was not decrypted yet or can&apos;t be decrypted</comment>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
@ -440,16 +407,87 @@
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location line="+27"/> <location line="+25"/>
<source>-- Encrypted Event (Unknown event type) --</source> <source>-- Encrypted Event (Unknown event type) --</source>
<comment>Placeholder, when the message was decrypted, but we couldn&apos;t parse it, because Nheko/mtxclient don&apos;t support that event type yet</comment> <comment>Placeholder, when the message was decrypted, but we couldn&apos;t parse it, because Nheko/mtxclient don&apos;t support that event type yet</comment>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </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>
<context> <context>
<name>TopRoomBar</name> <name>TopRoomBar</name>
<message> <message>
<location filename="../../src/TopRoomBar.cpp" line="+79"/> <location filename="../../src/TopRoomBar.cpp" line="+78"/>
<source>Room options</source> <source>Room options</source>
<translation></translation> <translation></translation>
</message> </message>
@ -514,7 +552,7 @@
<context> <context>
<name>UserSettingsPage</name> <name>UserSettingsPage</name>
<message> <message>
<location filename="../../src/UserSettingsPage.cpp" line="+166"/> <location filename="../../src/UserSettingsPage.cpp" line="+171"/>
<source>Minimize to tray</source> <source>Minimize to tray</source>
<translation></translation> <translation></translation>
</message> </message>
@ -528,6 +566,11 @@
<source>Group&apos;s sidebar</source> <source>Group&apos;s sidebar</source>
<translation></translation> <translation></translation>
</message> </message>
<message>
<location line="+9"/>
<source>Circular Avatars</source>
<translation type="unfinished"></translation>
</message>
<message> <message>
<location line="+9"/> <location line="+9"/>
<source>Typing notifications</source> <source>Typing notifications</source>
@ -604,7 +647,7 @@
<translation></translation> <translation></translation>
</message> </message>
<message> <message>
<location line="+156"/> <location line="+161"/>
<source>Open Sessions File</source> <source>Open Sessions File</source>
<translation></translation> <translation></translation>
</message> </message>
@ -824,7 +867,7 @@ Media size: %2
<context> <context>
<name>dialogs::ReadReceipts</name> <name>dialogs::ReadReceipts</name>
<message> <message>
<location filename="../../src/dialogs/ReadReceipts.cpp" line="+121"/> <location filename="../../src/dialogs/ReadReceipts.cpp" line="+117"/>
<source>Read receipts</source> <source>Read receipts</source>
<translation></translation> <translation></translation>
</message> </message>
@ -951,7 +994,7 @@ Media size: %2
<translation>%1</translation> <translation>%1</translation>
</message> </message>
<message> <message>
<location line="+149"/> <location line="+148"/>
<source>Select an avatar</source> <source>Select an avatar</source>
<translation></translation> <translation></translation>
</message> </message>
@ -977,19 +1020,6 @@ Media size: %2
<translation>%s</translation> <translation>%s</translation>
</message> </message>
</context> </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> <context>
<name>dialogs::UserProfile</name> <name>dialogs::UserProfile</name>
<message> <message>
@ -1013,7 +1043,7 @@ Media size: %2
<translation></translation> <translation></translation>
</message> </message>
<message> <message>
<location line="+57"/> <location line="+56"/>
<source>Devices</source> <source>Devices</source>
<translation></translation> <translation></translation>
</message> </message>
@ -1072,69 +1102,103 @@ Media size: %2
<context> <context>
<name>message-description sent:</name> <name>message-description sent:</name>
<message> <message>
<location filename="../../src/Utils.h" line="+104"/> <location filename="../../src/Utils.h" line="+95"/>
<source>%1 an audio clip</source> <source>You sent an audio clip</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location line="+3"/> <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> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location line="+3"/> <location line="+3"/>
<source>%1 a file</source> <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> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location line="+3"/> <location line="+3"/>
<source>%1 a video clip</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> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location line="+3"/> <location line="+3"/>
<source>%1 a sticker</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> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location line="+3"/> <location line="+3"/>
<source>%1 a notification</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 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: %2</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location line="+7"/> <location line="+7"/>
<source>%1 an encrypted message</source> <source>You sent an encrypted message</source>
<translation type="unfinished"></translation>
</message>
<message>
<location line="+3"/>
<source>%1 sent an encrypted message</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
</context> </context>
<context> <context>
<name>message-description:</name> <name>popups::UserMentions</name>
<message> <message>
<location line="-26"/> <location filename="../../src/popups/UserMentions.cpp" line="+61"/>
<source>sent</source> <source>This Room</source>
<comment>For when someone else is the sender</comment>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
</context>
<context>
<name>message-description: </name>
<message> <message>
<location line="-2"/> <location line="+1"/>
<source>sent</source> <source>All Rooms</source>
<comment>For when you are the sender</comment>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
</context> </context>
<context> <context>
<name>utils</name> <name>utils</name>
<message> <message>
<location filename="../../src/Utils.cpp" line="+46"/> <location filename="../../src/Utils.cpp" line="+282"/>
<location filename="../../src/Utils.h" line="+55"/>
<source>You</source>
<translation type="unfinished"></translation>
</message>
<message>
<location line="+219"/>
<source>sent a file.</source> <source>sent a file.</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
@ -1154,7 +1218,7 @@ Media size: %2
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location filename="../../src/Utils.h" line="-23"/> <location filename="../../src/Utils.h" line="+4"/>
<source>Unknown Message Type</source> <source>Unknown Message Type</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>

51
resources/qml/Avatar.qml Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,8 @@
import ".."
MatrixText {
text: model.formattedBody
width: parent ? parent.width : undefined
font.italic: true
color: inactiveColors.text
}

View File

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

View File

@ -0,0 +1,7 @@
import ".."
MatrixText {
text: qsTr("unimplemented event: ") + model.type
width: parent ? parent.width : undefined
color: inactiveColors.text
}

View File

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

View File

@ -0,0 +1,6 @@
import ".."
MatrixText {
text: model.formattedBody.replace("<pre>", "<pre style='white-space: pre-wrap'>")
width: parent ? parent.width : undefined
}

View File

@ -114,4 +114,21 @@
<file>styles/nheko.qss</file> <file>styles/nheko.qss</file>
<file>styles/nheko-dark.qss</file> <file>styles/nheko-dark.qss</file>
</qresource> </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> </RCC>

View File

@ -43,7 +43,6 @@ resolve(const QString &avatarUrl, int size, QObject *receiver, AvatarCallback ca
QPixmap pixmap; QPixmap pixmap;
if (avatar_cache.find(cacheKey, &pixmap)) { if (avatar_cache.find(cacheKey, &pixmap)) {
nhlog::net()->info("cached pixmap {}", avatarUrl.toStdString());
callback(pixmap); callback(pixmap);
return; return;
} }
@ -52,7 +51,6 @@ resolve(const QString &avatarUrl, int size, QObject *receiver, AvatarCallback ca
if (!data.isNull()) { if (!data.isNull()) {
pixmap.loadFromData(data); pixmap.loadFromData(data);
avatar_cache.insert(cacheKey, pixmap); avatar_cache.insert(cacheKey, pixmap);
nhlog::net()->info("loaded pixmap from disk cache {}", avatarUrl.toStdString());
callback(pixmap); callback(pixmap);
return; return;
} }
@ -69,8 +67,8 @@ resolve(const QString &avatarUrl, int size, QObject *receiver, AvatarCallback ca
}); });
mtx::http::ThumbOpts opts; mtx::http::ThumbOpts opts;
opts.width = 256; opts.width = size;
opts.height = 256; opts.height = size;
opts.mxc_url = avatarUrl.toStdString(); opts.mxc_url = avatarUrl.toStdString();
http::client()->get_thumbnail( 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); cache::client()->saveImage(opts.mxc_url, res);
nhlog::net()->info("downloaded pixmap {}", opts.mxc_url);
emit proxy->avatarDownloaded(QByteArray(res.data(), res.size())); emit proxy->avatarDownloaded(QByteArray(res.data(), res.size()));
}); });
} }

View File

@ -91,7 +91,6 @@ from_json(const json &j, ReadReceiptKey &key)
struct DescInfo struct DescInfo
{ {
QString event_id; QString event_id;
QString username;
QString userid; QString userid;
QString body; QString body;
QString timestamp; QString timestamp;

View File

@ -54,6 +54,8 @@ constexpr int CHECK_CONNECTIVITY_INTERVAL = 15'000;
constexpr int RETRY_TIMEOUT = 5'000; constexpr int RETRY_TIMEOUT = 5'000;
constexpr size_t MAX_ONETIME_KEYS = 50; constexpr size_t MAX_ONETIME_KEYS = 50;
Q_DECLARE_METATYPE(boost::optional<mtx::crypto::EncryptedFile>)
ChatPage::ChatPage(QSharedPointer<UserSettings> userSettings, QWidget *parent) ChatPage::ChatPage(QSharedPointer<UserSettings> userSettings, QWidget *parent)
: QWidget(parent) : QWidget(parent)
, isConnected_(true) , isConnected_(true)
@ -62,6 +64,9 @@ ChatPage::ChatPage(QSharedPointer<UserSettings> userSettings, QWidget *parent)
{ {
setObjectName("chatPage"); setObjectName("chatPage");
qRegisterMetaType<boost::optional<mtx::crypto::EncryptedFile>>(
"boost::optional<mtx::crypto::EncryptedFile>");
topLayout_ = new QHBoxLayout(this); topLayout_ = new QHBoxLayout(this);
topLayout_->setSpacing(0); topLayout_->setSpacing(0);
topLayout_->setMargin(0); topLayout_->setMargin(0);
@ -113,12 +118,7 @@ ChatPage::ChatPage(QSharedPointer<UserSettings> userSettings, QWidget *parent)
view_manager_ = new TimelineViewManager(this); view_manager_ = new TimelineViewManager(this);
contentLayout_->addWidget(top_bar_); contentLayout_->addWidget(top_bar_);
contentLayout_->addWidget(view_manager_); contentLayout_->addWidget(view_manager_->getWidget());
connect(this,
&ChatPage::removeTimelineEvent,
view_manager_,
&TimelineViewManager::removeTimelineEvent);
// Splitter // Splitter
splitter->addWidget(sideBar_); splitter->addWidget(sideBar_);
@ -304,9 +304,9 @@ ChatPage::ChatPage(QSharedPointer<UserSettings> userSettings, QWidget *parent)
connect( connect(
text_input_, text_input_,
&TextInputWidget::uploadImage, &TextInputWidget::uploadMedia,
this, this,
[this](QSharedPointer<QIODevice> dev, const QString &fn) { [this](QSharedPointer<QIODevice> dev, QString mimeClass, const QString &fn) {
QMimeDatabase db; QMimeDatabase db;
QMimeType mime = db.mimeTypeForData(dev.data()); QMimeType mime = db.mimeTypeForData(dev.data());
@ -316,9 +316,18 @@ ChatPage::ChatPage(QSharedPointer<UserSettings> userSettings, QWidget *parent)
return; return;
} }
auto bin = dev->peek(dev->size()); auto bin = dev->peek(dev->size());
auto payload = std::string(bin.data(), bin.size()); auto payload = std::string(bin.data(), bin.size());
auto dimensions = QImageReader(dev.data()).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( http::client()->upload(
payload, payload,
@ -327,193 +336,61 @@ ChatPage::ChatPage(QSharedPointer<UserSettings> userSettings, QWidget *parent)
[this, [this,
room_id = current_room_, room_id = current_room_,
filename = fn, filename = fn,
mime = mime.name(), encryptedFile,
size = payload.size(), mimeClass,
mime = mime.name(),
size = payload.size(),
dimensions](const mtx::responses::ContentURI &res, mtx::http::RequestErr err) { dimensions](const mtx::responses::ContentURI &res, mtx::http::RequestErr err) {
if (err) { if (err) {
emit uploadFailed( emit uploadFailed(
tr("Failed to upload image. Please try again.")); tr("Failed to upload media. Please try again."));
nhlog::net()->warn("failed to upload image: {} {} ({})", nhlog::net()->warn("failed to upload media: {} {} ({})",
err->matrix_error.error, err->matrix_error.error,
to_string(err->matrix_error.errcode), to_string(err->matrix_error.errcode),
static_cast<int>(err->status_code)); static_cast<int>(err->status_code));
return; return;
} }
emit imageUploaded(room_id, emit mediaUploaded(room_id,
filename, filename,
encryptedFile,
QString::fromStdString(res.content_uri), QString::fromStdString(res.content_uri),
mimeClass,
mime, mime,
size, size,
dimensions); 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) { connect(this, &ChatPage::uploadFailed, this, [this](const QString &msg) {
text_input_->hideUploadSpinner(); text_input_->hideUploadSpinner();
emit showNotification(msg); emit showNotification(msg);
}); });
connect(this, connect(this,
&ChatPage::imageUploaded, &ChatPage::mediaUploaded,
this, this,
[this](QString roomid, [this](QString roomid,
QString filename, QString filename,
boost::optional<mtx::crypto::EncryptedFile> encryptedFile,
QString url, QString url,
QString mimeClass,
QString mime, QString mime,
qint64 dsize, qint64 dsize,
QSize dimensions) { QSize dimensions) {
text_input_->hideUploadSpinner(); text_input_->hideUploadSpinner();
view_manager_->queueImageMessage(
roomid, filename, url, mime, dsize, dimensions); if (mimeClass == "image")
}); view_manager_->queueImageMessage(
connect(this, roomid, filename, encryptedFile, url, mime, dsize, dimensions);
&ChatPage::fileUploaded, else if (mimeClass == "audio")
this, view_manager_->queueAudioMessage(
[this](QString roomid, QString filename, QString url, QString mime, qint64 dsize) { roomid, filename, encryptedFile, url, mime, dsize);
text_input_->hideUploadSpinner(); else if (mimeClass == "video")
view_manager_->queueFileMessage(roomid, filename, url, mime, dsize); view_manager_->queueVideoMessage(
}); roomid, filename, encryptedFile, url, mime, dsize);
connect(this, else
&ChatPage::audioUploaded, view_manager_->queueFileMessage(
this, roomid, filename, encryptedFile, url, mime, dsize);
[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);
}); });
connect(room_list_, &RoomList::roomAvatarChanged, this, &ChatPage::updateTopBarAvatar); connect(room_list_, &RoomList::roomAvatarChanged, this, &ChatPage::updateTopBarAvatar);
@ -566,7 +443,7 @@ ChatPage::ChatPage(QSharedPointer<UserSettings> userSettings, QWidget *parent)
connect(this, connect(this,
&ChatPage::initializeViews, &ChatPage::initializeViews,
view_manager_, view_manager_,
[this](const mtx::responses::Rooms &rooms) { view_manager_->initialize(rooms); }); [this](const mtx::responses::Rooms &rooms) { view_manager_->sync(rooms); });
connect(this, connect(this,
&ChatPage::initializeEmptyViews, &ChatPage::initializeEmptyViews,
view_manager_, view_manager_,
@ -582,7 +459,7 @@ ChatPage::ChatPage(QSharedPointer<UserSettings> userSettings, QWidget *parent)
nhlog::db()->error("failed to retrieve invites: {}", e.what()); nhlog::db()->error("failed to retrieve invites: {}", e.what());
} }
view_manager_->initialize(rooms); view_manager_->sync(rooms);
removeLeftRooms(rooms.leave); removeLeftRooms(rooms.leave);
bool hasNotifications = false; bool hasNotifications = false;

View File

@ -18,7 +18,9 @@
#pragma once #pragma once
#include <atomic> #include <atomic>
#include <boost/optional.hpp>
#include <boost/variant.hpp> #include <boost/variant.hpp>
#include <mtx/common.hpp>
#include <mtx/responses.hpp> #include <mtx/responses.hpp>
#include <QFrame> #include <QFrame>
@ -94,27 +96,14 @@ signals:
const QPoint widgetPos); const QPoint widgetPos);
void uploadFailed(const QString &msg); void uploadFailed(const QString &msg);
void imageUploaded(const QString &roomid, void mediaUploaded(const QString &roomid,
const QString &filename, const QString &filename,
const boost::optional<mtx::crypto::EncryptedFile> &file,
const QString &url, const QString &url,
const QString &mimeClass,
const QString &mime, const QString &mime,
qint64 dsize, qint64 dsize,
const QSize &dimensions); 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 contentLoaded();
void closing(); void closing();
@ -125,8 +114,6 @@ signals:
void showUserSettingsPage(); void showUserSettingsPage();
void showOverlayProgressBar(); void showOverlayProgressBar();
void removeTimelineEvent(const QString &room_id, const QString &event_id);
void ownProfileOk(); void ownProfileOk();
void setUserDisplayName(const QString &name); void setUserDisplayName(const QString &name);
void setUserAvatar(const QString &avatar); void setUserAvatar(const QString &avatar);

View File

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

11
src/ColorImageProvider.h Normal file
View File

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

View File

@ -5,14 +5,43 @@
#include "spdlog/sinks/stdout_color_sinks.h" #include "spdlog/sinks/stdout_color_sinks.h"
#include <iostream> #include <iostream>
#include <QString>
#include <QtGlobal>
namespace { namespace {
std::shared_ptr<spdlog::logger> db_logger = nullptr; std::shared_ptr<spdlog::logger> db_logger = nullptr;
std::shared_ptr<spdlog::logger> net_logger = nullptr; std::shared_ptr<spdlog::logger> net_logger = nullptr;
std::shared_ptr<spdlog::logger> crypto_logger = nullptr; std::shared_ptr<spdlog::logger> crypto_logger = nullptr;
std::shared_ptr<spdlog::logger> ui_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_FILE_SIZE = 1024 * 1024 * 6;
constexpr auto MAX_LOG_FILES = 3; 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 { 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)); db_logger = std::make_shared<spdlog::logger>("db", std::begin(sinks), std::end(sinks));
crypto_logger = crypto_logger =
std::make_shared<spdlog::logger>("crypto", std::begin(sinks), std::end(sinks)); 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) { if (nheko::enable_debug_log) {
db_logger->set_level(spdlog::level::trace); db_logger->set_level(spdlog::level::trace);
ui_logger->set_level(spdlog::level::trace); ui_logger->set_level(spdlog::level::trace);
crypto_logger->set_level(spdlog::level::trace); crypto_logger->set_level(spdlog::level::trace);
} }
qInstallMessageHandler(qmlMessageHandler);
} }
std::shared_ptr<spdlog::logger> std::shared_ptr<spdlog::logger>
@ -66,4 +98,10 @@ crypto()
{ {
return crypto_logger; return crypto_logger;
} }
std::shared_ptr<spdlog::logger>
qml()
{
return qml_logger;
}
} }

View File

@ -19,5 +19,8 @@ db();
std::shared_ptr<spdlog::logger> std::shared_ptr<spdlog::logger>
crypto(); crypto();
std::shared_ptr<spdlog::logger>
qml();
extern bool enable_debug_log_from_commandline; extern bool enable_debug_log_from_commandline;
} }

View File

@ -20,16 +20,6 @@ Q_DECLARE_METATYPE(nlohmann::json)
Q_DECLARE_METATYPE(std::vector<std::string>) Q_DECLARE_METATYPE(std::vector<std::string>)
Q_DECLARE_METATYPE(std::vector<QString>) 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 { namespace http {
mtx::http::Client * mtx::http::Client *
client(); client();

83
src/MxcImageProvider.cpp Normal file
View File

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

69
src/MxcImageProvider.h Normal file
View File

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

View File

@ -118,7 +118,7 @@ RoomInfoListItem::RoomInfoListItem(QString room_id, RoomInfo info, QWidget *pare
// so we can't use them for sorting. // so we can't use them for sorting.
if (roomType_ == RoomType::Invited) if (roomType_ == RoomType::Invited)
lastMsgInfo_ = { lastMsgInfo_ = {
emptyEventId, "-", "-", "-", "-", QDateTime::currentDateTime().addYears(10)}; emptyEventId, "-", "-", "-", QDateTime::currentDateTime().addYears(10)};
} }
void void
@ -142,7 +142,7 @@ RoomInfoListItem::resizeEvent(QResizeEvent *)
void void
RoomInfoListItem::paintEvent(QPaintEvent *event) 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); Q_UNUSED(event);
@ -210,33 +210,11 @@ RoomInfoListItem::paintEvent(QPaintEvent *event)
p.setFont(QFont{}); p.setFont(QFont{});
p.setPen(subtitlePen); p.setPen(subtitlePen);
// The limit is the space between the end of the avatar and the start of the int descriptionLimit = std::max(
// timestamp. 0, width() - 3 * wm.padding - bottomLineWidthLimit - wm.iconSize);
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);
auto description = auto description =
metrics.elidedText(lastMsgInfo_.body, Qt::ElideRight, descriptionLimit); metrics.elidedText(lastMsgInfo_.body, Qt::ElideRight, descriptionLimit);
p.drawText(QPoint(2 * wm.padding + wm.iconSize + nameWidth, bottom_y), p.drawText(QPoint(2 * wm.padding + wm.iconSize, bottom_y), description);
description);
// We show the last message timestamp. // We show the last message timestamp.
p.save(); p.save();

View File

@ -458,21 +458,16 @@ FilteredTextEdit::textChanged()
} }
void 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}}; QSharedPointer<QBuffer> buffer{new QBuffer{this}};
buffer->setData(data); buffer->setData(data);
emit startedUpload(); emit startedUpload();
if (media == "image") emit media(buffer, mediaType, filename);
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);
} }
void void
@ -580,10 +575,7 @@ TextInputWidget::TextInputWidget(QWidget *parent)
connect(input_, &FilteredTextEdit::message, this, &TextInputWidget::sendTextMessage); connect(input_, &FilteredTextEdit::message, this, &TextInputWidget::sendTextMessage);
connect(input_, &FilteredTextEdit::reply, this, &TextInputWidget::sendReplyMessage); connect(input_, &FilteredTextEdit::reply, this, &TextInputWidget::sendReplyMessage);
connect(input_, &FilteredTextEdit::command, this, &TextInputWidget::command); connect(input_, &FilteredTextEdit::command, this, &TextInputWidget::command);
connect(input_, &FilteredTextEdit::image, this, &TextInputWidget::uploadImage); connect(input_, &FilteredTextEdit::media, this, &TextInputWidget::uploadMedia);
connect(input_, &FilteredTextEdit::audio, this, &TextInputWidget::uploadAudio);
connect(input_, &FilteredTextEdit::video, this, &TextInputWidget::uploadVideo);
connect(input_, &FilteredTextEdit::file, this, &TextInputWidget::uploadFile);
connect(emojiBtn_, connect(emojiBtn_,
SIGNAL(emojiSelected(const QString &)), SIGNAL(emojiSelected(const QString &)),
this, this,
@ -642,14 +634,8 @@ TextInputWidget::openFileSelection()
const auto format = mime.name().split("/")[0]; const auto format = mime.name().split("/")[0];
QSharedPointer<QFile> file{new QFile{fileName, this}}; QSharedPointer<QFile> file{new QFile{fileName, this}};
if (format == "image")
emit uploadImage(file, fileName); emit uploadMedia(file, format, fileName);
else if (format == "audio")
emit uploadAudio(file, fileName);
else if (format == "video")
emit uploadVideo(file, fileName);
else
emit uploadFile(file, fileName);
showUploadSpinner(); showUploadSpinner();
} }

View File

@ -63,10 +63,7 @@ signals:
void message(QString); void message(QString);
void reply(QString, const RelatedInfo &); void reply(QString, const RelatedInfo &);
void command(QString name, QString args); void command(QString name, QString args);
void image(QSharedPointer<QIODevice> data, const QString &filename); void media(QSharedPointer<QIODevice> data, QString mimeClass, 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);
//! Trigger the suggestion popup. //! Trigger the suggestion popup.
void showSuggestions(const QString &query); void showSuggestions(const QString &query);
@ -179,10 +176,9 @@ signals:
void sendEmoteMessage(QString msg); void sendEmoteMessage(QString msg);
void heightChanged(int height); void heightChanged(int height);
void uploadImage(const QSharedPointer<QIODevice> data, const QString &filename); void uploadMedia(const QSharedPointer<QIODevice> data,
void uploadFile(const QSharedPointer<QIODevice> data, const QString &filename); QString mimeClass,
void uploadAudio(const QSharedPointer<QIODevice> data, const QString &filename); const QString &filename);
void uploadVideo(const QSharedPointer<QIODevice> data, const QString &filename);
void sendJoinRoomRequest(const QString &room); void sendJoinRoomRequest(const QString &room);

View File

@ -53,7 +53,7 @@ UserSettings::load()
isReadReceiptsEnabled_ = settings.value("user/read_receipts", true).toBool(); isReadReceiptsEnabled_ = settings.value("user/read_receipts", true).toBool();
theme_ = settings.value("user/theme", defaultTheme_).toString(); theme_ = settings.value("user/theme", defaultTheme_).toString();
font_ = settings.value("user/font_family", "default").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(); emojiFont_ = settings.value("user/emoji_font_family", "default").toString();
baseFontSize_ = settings.value("user/font_size", QFont().pointSizeF()).toDouble(); baseFontSize_ = settings.value("user/font_size", QFont().pointSizeF()).toDouble();
@ -119,9 +119,7 @@ UserSettings::save()
settings.setValue("start_in_tray", isStartInTrayEnabled_); settings.setValue("start_in_tray", isStartInTrayEnabled_);
settings.endGroup(); settings.endGroup();
settings.beginGroup("avatar"); settings.setValue("avatar_circles", avatarCircles_);
settings.setValue("circles", avatarCircles_);
settings.endGroup();
settings.setValue("font_size", baseFontSize_); settings.setValue("font_size", baseFontSize_);
settings.setValue("typing_notifications", isTypingNotificationsEnabled_); settings.setValue("typing_notifications", isTypingNotificationsEnabled_);

View File

@ -40,9 +40,8 @@ utils::replaceEmoji(const QString &body)
for (auto &code : utf32_string) { for (auto &code : utf32_string) {
// TODO: Be more precise here. // TODO: Be more precise here.
if (code > 9000) if (code > 9000)
fmtBody += fmtBody += QString("<font face=\"" + userFontFamily + "\">") +
QString("<span style=\"font-family: " + userFontFamily + ";\">") + QString::fromUcs4(&code, 1) + "</font>";
QString::fromUcs4(&code, 1) + "</span>";
else else
fmtBody += QString::fromUcs4(&code, 1); fmtBody += QString::fromUcs4(&code, 1);
} }
@ -147,11 +146,6 @@ utils::getMessageDescription(const TimelineEvent &event,
const auto ts = QDateTime::fromMSecsSinceEpoch(msg.origin_server_ts); const auto ts = QDateTime::fromMSecsSinceEpoch(msg.origin_server_ts);
DescInfo info; DescInfo info;
if (sender == localUser)
info.username = QCoreApplication::translate("utils", "You");
else
info.username = username;
info.userid = sender; info.userid = sender;
info.body = QString(" %1").arg(messageDescription<Encrypted>()); info.body = QString(" %1").arg(messageDescription<Encrypted>());
info.timestamp = utils::descriptiveTime(ts); info.timestamp = utils::descriptiveTime(ts);
@ -324,19 +318,29 @@ utils::linkifyMessage(const QString &body)
return doc; return doc;
} }
QByteArray escapeRawHtml(const QByteArray &data) { QByteArray
QByteArray buffer; escapeRawHtml(const QByteArray &data)
const size_t length = data.size(); {
buffer.reserve(length); QByteArray buffer;
for(size_t pos = 0; pos != length; ++pos) { const size_t length = data.size();
switch(data.at(pos)) { buffer.reserve(length);
case '&': buffer.append("&amp;"); break; for (size_t pos = 0; pos != length; ++pos) {
case '<': buffer.append("&lt;"); break; switch (data.at(pos)) {
case '>': buffer.append("&gt;"); break; case '&':
default: buffer.append(data.at(pos)); break; buffer.append("&amp;");
} break;
} case '<':
return buffer; buffer.append("&lt;");
break;
case '>':
buffer.append("&gt;");
break;
default:
buffer.append(data.at(pos));
break;
}
}
return buffer;
} }
QString QString
@ -362,7 +366,7 @@ utils::getFormattedQuoteBody(const RelatedInfo &related, const QString &html)
{ {
return QString("<mx-reply><blockquote><a " return QString("<mx-reply><blockquote><a "
"href=\"https://matrix.to/#/%1/%2\">In reply " "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>") "/>%5</blockquote></mx-reply>")
.arg(related.room, .arg(related.room,
QString::fromStdString(related.related_event), QString::fromStdString(related.related_event),
@ -378,9 +382,6 @@ utils::getQuoteBody(const RelatedInfo &related)
using MsgType = mtx::events::MessageType; using MsgType = mtx::events::MessageType;
switch (related.type) { switch (related.type) {
case MsgType::Text: {
return markdownToHtml(related.quoted_body);
}
case MsgType::File: { case MsgType::File: {
return QString(QCoreApplication::translate("utils", "sent a file.")); return QString(QCoreApplication::translate("utils", "sent a file."));
} }

View File

@ -4,10 +4,6 @@
#include "Cache.h" #include "Cache.h"
#include "RoomInfoListItem.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 <QCoreApplication>
#include <QDateTime> #include <QDateTime>
@ -94,38 +90,72 @@ messageDescription(const QString &username = "",
using Video = mtx::events::RoomEvent<mtx::events::msg::Video>; using Video = mtx::events::RoomEvent<mtx::events::msg::Video>;
using Encrypted = mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>; using Encrypted = mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>;
// Sometimes the verb form of sent changes in some languages depending on the actor. if (std::is_same<T, Audio>::value) {
auto remoteSent = QCoreApplication::translate( if (isLocal)
"message-description: ", "sent", "For when you are the sender"); return QCoreApplication::translate("message-description sent:",
auto localSent = QCoreApplication::translate( "You sent an audio clip");
"message-description:", "sent", "For when someone else is the sender"); else
QString sentVerb = isLocal ? localSent : remoteSent; return QCoreApplication::translate("message-description sent:",
if (std::is_same<T, AudioItem>::value || std::is_same<T, Audio>::value) { "%1 sent an audio clip")
return QCoreApplication::translate("message-description sent:", "%1 an audio clip") .arg(username);
.arg(sentVerb); } else if (std::is_same<T, Image>::value) {
} else if (std::is_same<T, ImageItem>::value || std::is_same<T, Image>::value) { if (isLocal)
return QCoreApplication::translate("message-description sent:", "%1 an image") return QCoreApplication::translate("message-description sent:",
.arg(sentVerb); "You sent an image");
} else if (std::is_same<T, FileItem>::value || std::is_same<T, File>::value) { else
return QCoreApplication::translate("message-description sent:", "%1 a file") return QCoreApplication::translate("message-description sent:",
.arg(sentVerb); "%1 sent an image")
} else if (std::is_same<T, VideoItem>::value || std::is_same<T, Video>::value) { .arg(username);
return QCoreApplication::translate("message-description sent:", "%1 a video clip") } else if (std::is_same<T, File>::value) {
.arg(sentVerb); if (isLocal)
} else if (std::is_same<T, StickerItem>::value || std::is_same<T, Sticker>::value) { return QCoreApplication::translate("message-description sent:",
return QCoreApplication::translate("message-description sent:", "%1 a sticker") "You sent a file");
.arg(sentVerb); 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) { } else if (std::is_same<T, Notice>::value) {
return QCoreApplication::translate("message-description sent:", "%1 a notification") if (isLocal)
.arg(sentVerb); 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) { } 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) { } else if (std::is_same<T, Emote>::value) {
return QString("* %1 %2").arg(username).arg(body); return QString("* %1 %2").arg(username).arg(body);
} else if (std::is_same<T, Encrypted>::value) { } else if (std::is_same<T, Encrypted>::value) {
return QCoreApplication::translate("message-description sent:", if (isLocal)
"%1 an encrypted message") return QCoreApplication::translate("message-description sent:",
.arg(sentVerb); "You sent an encrypted message");
else
return QCoreApplication::translate("message-description sent:",
"%1 sent an encrypted message")
.arg(username);
} else { } else {
return QCoreApplication::translate("utils", "Unknown Message Type"); return QCoreApplication::translate("utils", "Unknown Message Type");
} }
@ -135,29 +165,19 @@ template<class T, class Event>
DescInfo DescInfo
createDescriptionInfo(const Event &event, const QString &localUser, const QString &room_id) 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 msg = boost::get<T>(event);
const auto sender = QString::fromStdString(msg.sender); const auto sender = QString::fromStdString(msg.sender);
const auto username = Cache::displayName(room_id, sender); const auto username = Cache::displayName(room_id, sender);
const auto ts = QDateTime::fromMSecsSinceEpoch(msg.origin_server_ts); const auto ts = QDateTime::fromMSecsSinceEpoch(msg.origin_server_ts);
bool isText = std::is_same<T, Text>::value; return DescInfo{QString::fromStdString(msg.event_id),
bool isEmote = std::is_same<T, Emote>::value; sender,
messageDescription<T>(username,
return DescInfo{ QString::fromStdString(msg.content.body).trimmed(),
QString::fromStdString(msg.event_id), sender == localUser),
isEmote ? "" utils::descriptiveTime(ts),
: (sender == localUser ? QCoreApplication::translate("utils", "You") : username), ts};
sender,
(isText || isEmote)
? messageDescription<T>(
username, QString::fromStdString(msg.content.body).trimmed(), sender == localUser)
: QString(" %1").arg(messageDescription<T>()),
utils::descriptiveTime(ts),
ts};
} }
//! Scale down an image to fit to the given width & height limitations. //! Scale down an image to fit to the given width & height limitations.

View File

@ -41,7 +41,6 @@ ImageOverlay::ImageOverlay(QPixmap image, QWidget *parent)
setAttribute(Qt::WA_DeleteOnClose, true); setAttribute(Qt::WA_DeleteOnClose, true);
setWindowState(Qt::WindowFullScreen); setWindowState(Qt::WindowFullScreen);
// Deprecated in 5.13: screen_ = QApplication::desktop()->availableGeometry();
screen_ = QGuiApplication::primaryScreen()->availableGeometry(); screen_ = QGuiApplication::primaryScreen()->availableGeometry();
move(QApplication::desktop()->mapToGlobal(screen_.topLeft())); move(QApplication::desktop()->mapToGlobal(screen_.topLeft()));

View File

@ -1,4 +1,5 @@
#include <QAbstractSlider> #include <QAbstractSlider>
#include <QLabel>
#include <QListWidgetItem> #include <QListWidgetItem>
#include <QPainter> #include <QPainter>
#include <QPushButton> #include <QPushButton>

View File

@ -488,7 +488,7 @@ RoomSettings::retrieveRoomInfo()
usesEncryption_ = cache::client()->isRoomEncrypted(room_id_.toStdString()); usesEncryption_ = cache::client()->isRoomEncrypted(room_id_.toStdString());
info_ = cache::client()->singleRoomInfo(room_id_.toStdString()); info_ = cache::client()->singleRoomInfo(room_id_.toStdString());
setAvatar(); setAvatar();
} catch (const lmdb::error &e) { } catch (const lmdb::error &) {
nhlog::db()->warn("failed to retrieve room info from cache: {}", nhlog::db()->warn("failed to retrieve room info from cache: {}",
room_id_.toStdString()); room_id_.toStdString());
} }

View File

@ -7,7 +7,7 @@
#include "ChatPage.h" #include "ChatPage.h"
#include "Logging.h" #include "Logging.h"
#include "UserMentions.h" #include "UserMentions.h"
#include "timeline/TimelineItem.h" //#include "timeline/TimelineItem.h"
using namespace popups; using namespace popups;
@ -116,39 +116,46 @@ UserMentions::pushItem(const QString &event_id,
const QString &room_id, const QString &room_id,
const QString &current_room_id) const QString &current_room_id)
{ {
setUpdatesEnabled(false); (void)event_id;
(void)user_id;
// Add to the 'all' section (void)body;
TimelineItem *view_item = new TimelineItem( (void)room_id;
mtx::events::MessageType::Text, user_id, body, true, room_id, all_scroll_widget_); (void)current_room_id;
view_item->setEventId(event_id); // setUpdatesEnabled(false);
view_item->hide(); //
// // Add to the 'all' section
all_scroll_layout_->addWidget(view_item); // TimelineItem *view_item = new TimelineItem(
QTimer::singleShot(0, this, [view_item, this]() { // mtx::events::MessageType::Text, user_id, body, true, room_id,
view_item->show(); // all_scroll_widget_);
view_item->adjustSize(); // view_item->setEventId(event_id);
setUpdatesEnabled(true); // view_item->hide();
}); //
// all_scroll_layout_->addWidget(view_item);
// if it matches the current room... add it to the current room as well. // QTimer::singleShot(0, this, [view_item, this]() {
if (QString::compare(room_id, current_room_id, Qt::CaseInsensitive) == 0) { // view_item->show();
// Add to the 'local' section // view_item->adjustSize();
TimelineItem *local_view_item = new TimelineItem(mtx::events::MessageType::Text, // setUpdatesEnabled(true);
user_id, // });
body, //
true, // // if it matches the current room... add it to the current room as well.
room_id, // if (QString::compare(room_id, current_room_id, Qt::CaseInsensitive) == 0) {
local_scroll_widget_); // // Add to the 'local' section
local_view_item->setEventId(event_id); // TimelineItem *local_view_item = new
local_view_item->hide(); // TimelineItem(mtx::events::MessageType::Text,
local_scroll_layout_->addWidget(local_view_item); // user_id,
// body,
QTimer::singleShot(0, this, [local_view_item]() { // true,
local_view_item->show(); // room_id,
local_view_item->adjustSize(); // 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 void

Binary file not shown.

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

@ -1,340 +1,292 @@
/* #include "TimelineViewManager.h"
* 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 <random> #include <QMetaType>
#include <QPalette>
#include <QQmlContext>
#include <QApplication> #include "ChatPage.h"
#include <QFileInfo> #include "ColorImageProvider.h"
#include <QSettings> #include "DelegateChooser.h"
#include "Cache.h"
#include "Logging.h" #include "Logging.h"
#include "Utils.h" #include "MxcImageProvider.h"
#include "timeline/TimelineView.h" #include "UserSettingsPage.h"
#include "timeline/TimelineViewManager.h" #include "dialogs/ImageOverlay.h"
#include "timeline/widgets/AudioItem.h"
#include "timeline/widgets/FileItem.h" void
#include "timeline/widgets/ImageItem.h" TimelineViewManager::updateColorPalette()
#include "timeline/widgets/VideoItem.h" {
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);
}
}
TimelineViewManager::TimelineViewManager(QWidget *parent) TimelineViewManager::TimelineViewManager(QWidget *parent)
: QStackedWidget(parent) : imgProvider(new MxcImageProvider())
{} , colorImgProvider(new ColorImageProvider())
void
TimelineViewManager::updateReadReceipts(const QString &room_id,
const std::vector<QString> &event_ids)
{ {
if (timelineViewExists(room_id)) { qmlRegisterUncreatableMetaObject(qml_mtx_events::staticMetaObject,
auto view = views_[room_id]; "im.nheko",
if (view) 1,
emit view->markReadEvents(event_ids); 0,
} "MtxEvent",
} "Can't instantiate enum!");
qmlRegisterType<DelegateChoice>("im.nheko", 1, 0, "DelegateChoice");
qmlRegisterType<DelegateChooser>("im.nheko", 1, 0, "DelegateChooser");
void #ifdef USE_QUICK_VIEW
TimelineViewManager::removeTimelineEvent(const QString &room_id, const QString &event_id) view = new QQuickView();
{ container = QWidget::createWindowContainer(view, parent);
auto view = views_[room_id]; #else
view = new QQuickWidget(parent);
container = view;
view->setResizeMode(QQuickWidget::SizeRootObjectToView);
container->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
if (view) connect(view, &QQuickWidget::statusChanged, this, [](QQuickWidget::Status status) {
view->removeEvent(event_id); 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"));
void connect(dynamic_cast<ChatPage *>(parent),
TimelineViewManager::queueTextMessage(const QString &msg) &ChatPage::themeChanged,
{
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)
{
if (active_room_.isEmpty())
return;
auto room_id = active_room_;
auto view = views_[room_id];
view->addUserMessage(mtx::events::MessageType::Text, reply, related);
}
void
TimelineViewManager::queueImageMessage(const QString &roomid,
const QString &filename,
const QString &url,
const QString &mime,
uint64_t size,
const QSize &dimensions)
{
if (!timelineViewExists(roomid)) {
nhlog::ui()->warn("Cannot send m.image message to a non-managed view");
return;
}
auto view = views_[roomid];
view->addUserMessage<ImageItem, mtx::events::MessageType::Image>(
url, filename, mime, size, dimensions);
}
void
TimelineViewManager::queueFileMessage(const QString &roomid,
const QString &filename,
const QString &url,
const QString &mime,
uint64_t size)
{
if (!timelineViewExists(roomid)) {
nhlog::ui()->warn("cannot send m.file message to a non-managed view");
return;
}
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)
{
if (!timelineViewExists(roomid)) {
nhlog::ui()->warn("cannot send m.audio message to a non-managed view");
return;
}
auto view = views_[roomid];
view->addUserMessage<AudioItem, mtx::events::MessageType::Audio>(url, filename, mime, size);
}
void
TimelineViewManager::queueVideoMessage(const QString &roomid,
const QString &filename,
const QString &url,
const QString &mime,
uint64_t size)
{
if (!timelineViewExists(roomid)) {
nhlog::ui()->warn("cannot send m.video message to a non-managed view");
return;
}
auto view = views_[roomid];
view->addUserMessage<VideoItem, mtx::events::MessageType::Video>(url, filename, mime, size);
}
void
TimelineViewManager::initialize(const mtx::responses::Rooms &rooms)
{
for (auto it = rooms.join.cbegin(); it != rooms.join.cend(); ++it) {
addRoom(it->second, QString::fromStdString(it->first));
}
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;
// 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);
}
}
void
TimelineViewManager::initialize(const std::vector<std::string> &rooms)
{
for (const auto &roomid : rooms)
addRoom(QString::fromStdString(roomid));
}
void
TimelineViewManager::addRoom(const mtx::responses::JoinedRoom &room, const QString &room_id)
{
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));
connect(view,
&TimelineView::updateLastTimelineMessage,
this, this,
&TimelineViewManager::updateRoomsLastMessage); &TimelineViewManager::updateColorPalette);
// Add the view in the widget stack.
addWidget(view);
}
void
TimelineViewManager::addRoom(const QString &room_id)
{
if (timelineViewExists(room_id))
return;
// Create a history view without any events.
TimelineView *view = new TimelineView(room_id);
views_.emplace(room_id, QSharedPointer<TimelineView>(view));
connect(view,
&TimelineView::updateLastTimelineMessage,
this,
&TimelineViewManager::updateRoomsLastMessage);
// Add the view in the widget stack.
addWidget(view);
} }
void void
TimelineViewManager::sync(const mtx::responses::Rooms &rooms) TimelineViewManager::sync(const mtx::responses::Rooms &rooms)
{ {
for (const auto &room : rooms.join) { for (auto it = rooms.join.cbegin(); it != rooms.join.cend(); ++it) {
auto roomid = QString::fromStdString(room.first); // 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);
}
if (!timelineViewExists(roomid)) { this->isInitialSync_ = false;
nhlog::ui()->warn("ignoring event from unknown room: {}", emit initialSyncChanged(false);
roomid.toStdString()); }
continue;
}
auto view = views_.at(roomid); void
TimelineViewManager::addRoom(const QString &room_id)
view->addEvents(room.second.timeline); {
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));
} }
} }
void void
TimelineViewManager::setHistoryView(const QString &room_id) TimelineViewManager::setHistoryView(const QString &room_id)
{ {
if (!timelineViewExists(room_id)) { nhlog::ui()->info("Trying to activate room {}", room_id.toStdString());
nhlog::ui()->warn("room from RoomList is not present in ViewManager: {}",
room_id.toStdString()); auto room = models.find(room_id);
return; if (room != models.end()) {
timeline_ = room.value().data();
emit activeTimelineChanged(timeline_);
nhlog::ui()->info("Activated room {}", room_id.toStdString());
} }
active_room_ = room_id;
auto view = views_.at(room_id);
setCurrentWidget(view.data());
view->fetchHistory();
view->scrollDown();
} }
QString void
TimelineViewManager::chooseRandomColor() TimelineViewManager::openImageOverlay(QString mxcUrl, QString eventId) const
{ {
std::random_device random_device; QQuickImageResponse *imgResponse =
std::mt19937 engine{random_device()}; imgProvider->requestImageResponse(mxcUrl.remove("mxc://"), QSize());
std::uniform_real_distribution<float> dist(0, 1); 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());
float hue = dist(engine); auto imgDialog = new dialogs::ImageOverlay(pixmap);
float saturation = 0.9; imgDialog->show();
float value = 0.7; connect(imgDialog, &dialogs::ImageOverlay::saving, timeline_, [this, eventId]() {
timeline_->saveMedia(eventId);
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();
}
bool
TimelineViewManager::hasLoaded() const
{
return std::all_of(views_.cbegin(), views_.cend(), [](const auto &view) {
return view.second->hasLoaded();
}); });
} }
void
TimelineViewManager::updateReadReceipts(const QString &room_id,
const std::vector<QString> &event_ids)
{
auto room = models.find(room_id);
if (room != models.end()) {
room.value()->markEventsAsRead(event_ids);
}
}
void
TimelineViewManager::initWithMessages(const std::map<QString, mtx::responses::Timeline> &msgs)
{
for (const auto &e : msgs) {
addRoom(e.first);
models.value(e.first)->addEvents(e.second);
}
}
void
TimelineViewManager::queueTextMessage(const QString &msg)
{
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::queueReplyMessage(const QString &reply, const RelatedInfo &related)
{
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);
}
}
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;
if (timeline_)
timeline_->sendMessage(text);
}
void
TimelineViewManager::queueEmoteMessage(const QString &msg)
{
auto html = utils::markdownToHtml(msg);
mtx::events::msg::Emote emote;
emote.body = msg.trimmed().toStdString();
if (html != msg.trimmed().toHtmlEscaped())
emote.formatted_body = html.toStdString();
if (timeline_)
timeline_->sendMessage(emote);
}
void
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)
{
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::queueFileMessage(
const QString &roomid,
const QString &filename,
const boost::optional<mtx::crypto::EncryptedFile> &encryptedFile,
const QString &url,
const QString &mime,
uint64_t dsize)
{
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);
}
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)
{
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);
}
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)
{
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);
}

View File

@ -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 #pragma once
#include <QQuickView>
#include <QQuickWidget>
#include <QSharedPointer> #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" #include "Utils.h"
class QFile; class MxcImageProvider;
class ColorImageProvider;
class RoomInfoListItem; class TimelineViewManager : public QObject
class TimelineView;
struct DescInfo;
struct SavedMessages;
class TimelineViewManager : public QStackedWidget
{ {
Q_OBJECT Q_OBJECT
Q_PROPERTY(
TimelineModel *timeline MEMBER timeline_ READ activeTimeline NOTIFY activeTimelineChanged)
Q_PROPERTY(
bool isInitialSync MEMBER isInitialSync_ READ isInitialSync NOTIFY initialSyncChanged)
public: public:
TimelineViewManager(QWidget *parent); TimelineViewManager(QWidget *parent = 0);
QWidget *getWidget() const { return container; }
// 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);
void sync(const mtx::responses::Rooms &rooms); 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. void clearAll() { models.clear(); }
bool hasLoaded() const;
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: signals:
void clearRoomMessageCount(QString roomid); 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: public slots:
void updateReadReceipts(const QString &room_id, const std::vector<QString> &event_ids); 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 initWithMessages(const std::map<QString, mtx::responses::Timeline> &msgs);
void setHistoryView(const QString &room_id); void setHistoryView(const QString &room_id);
void updateColorPalette();
void queueTextMessage(const QString &msg); void queueTextMessage(const QString &msg);
void queueReplyMessage(const QString &reply, const RelatedInfo &related); void queueReplyMessage(const QString &reply, const RelatedInfo &related);
void queueEmoteMessage(const QString &msg); void queueEmoteMessage(const QString &msg);
void queueImageMessage(const QString &roomid, void queueImageMessage(const QString &roomid,
const QString &filename, const QString &filename,
const boost::optional<mtx::crypto::EncryptedFile> &file,
const QString &url, const QString &url,
const QString &mime, const QString &mime,
uint64_t dsize, uint64_t dsize,
const QSize &dimensions); const QSize &dimensions);
void queueFileMessage(const QString &roomid, void queueFileMessage(const QString &roomid,
const QString &filename, const QString &filename,
const boost::optional<mtx::crypto::EncryptedFile> &file,
const QString &url, const QString &url,
const QString &mime, const QString &mime,
uint64_t dsize); uint64_t dsize);
void queueAudioMessage(const QString &roomid, void queueAudioMessage(const QString &roomid,
const QString &filename, const QString &filename,
const boost::optional<mtx::crypto::EncryptedFile> &file,
const QString &url, const QString &url,
const QString &mime, const QString &mime,
uint64_t dsize); uint64_t dsize);
void queueVideoMessage(const QString &roomid, void queueVideoMessage(const QString &roomid,
const QString &filename, const QString &filename,
const boost::optional<mtx::crypto::EncryptedFile> &file,
const QString &url, const QString &url,
const QString &mime, const QString &mime,
uint64_t dsize); uint64_t dsize);
private: private:
//! Check if the given room id is managed by a TimelineView. #ifdef USE_QUICK_VIEW
bool timelineViewExists(const QString &id) { return views_.find(id) != views_.end(); } QQuickView *view;
#else
QQuickWidget *view;
#endif
QWidget *container;
QString active_room_; MxcImageProvider *imgProvider;
std::map<QString, QSharedPointer<TimelineView>> views_; ColorImageProvider *colorImgProvider;
QHash<QString, QSharedPointer<TimelineModel>> models;
TimelineModel *timeline_ = nullptr;
bool isInitialSync_ = true;
}; };

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -101,7 +101,7 @@ Avatar::setIcon(const QIcon &icon)
void void
Avatar::paintEvent(QPaintEvent *) Avatar::paintEvent(QPaintEvent *)
{ {
bool rounded = QSettings().value("user/avatar/circles", true).toBool(); bool rounded = QSettings().value("user/avatar_circles", true).toBool();
QPainter painter(this); QPainter painter(this);
painter.setRenderHint(QPainter::Antialiasing); painter.setRenderHint(QPainter::Antialiasing);