Merge pull request #91 from Nheko-Reborn/qml-timeline

Qml timeline
This commit is contained in:
Joseph Donofry 2019-11-29 14:51:24 -05:00 committed by GitHub
commit 4047a90302
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
75 changed files with 5159 additions and 6118 deletions

View File

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

View File

@ -54,5 +54,7 @@ if [ "$TRAVIS_OS_NAME" = "linux" ]; then
qt${QT_PKG}tools \
qt${QT_PKG}svg \
qt${QT_PKG}multimedia \
qt${QT_PKG}quickcontrols2 \
qt${QT_PKG}graphicaleffects \
liblmdb-dev
fi

View File

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

View File

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

9
.gitignore vendored
View File

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

View File

@ -15,7 +15,8 @@ matrix:
include:
- os: osx
compiler: clang
osx_image: xcode9.2
# Use the default osx image, because that one is actually tested to work with homebrew and probably the oldest supported version
# osx_image: xcode9
env:
- DEPLOYMENT=1
- USE_BUNDLED_BOOST=0
@ -42,8 +43,8 @@ matrix:
env:
- CXX_COMPILER=g++-8
- C_COMPILER=gcc-8
- QT_VERSION=571
- QT_PKG=57
- QT_VERSION=58
- QT_PKG=58
- USE_BUNDLED_BOOST=1
- USE_BUNDLED_CMARK=1
- USE_BUNDLED_JSON=1

View File

@ -69,7 +69,8 @@ include(LMDB)
#
# Discover Qt dependencies.
#
find_package(Qt5 COMPONENTS Core Widgets LinguistTools Concurrent Svg Multimedia REQUIRED)
find_package(Qt5 COMPONENTS Core Widgets LinguistTools Concurrent Svg Multimedia Qml QuickControls2 QuickWidgets REQUIRED)
find_package(Qt5QuickCompiler)
find_package(Qt5DBus)
if (APPLE)
@ -192,12 +193,8 @@ set(SRC_FILES
# Timeline
src/timeline/TimelineViewManager.cpp
src/timeline/TimelineItem.cpp
src/timeline/TimelineView.cpp
src/timeline/widgets/AudioItem.cpp
src/timeline/widgets/FileItem.cpp
src/timeline/widgets/ImageItem.cpp
src/timeline/widgets/VideoItem.cpp
src/timeline/TimelineModel.cpp
src/timeline/DelegateChooser.cpp
# UI components
src/ui/Avatar.cpp
@ -229,6 +226,8 @@ set(SRC_FILES
src/Logging.cpp
src/MainWindow.cpp
src/MatrixClient.cpp
src/MxcImageProvider.cpp
src/ColorImageProvider.cpp
src/QuickSwitcher.cpp
src/Olm.cpp
src/RegisterPage.cpp
@ -333,13 +332,9 @@ qt5_wrap_cpp(MOC_HEADERS
src/emoji/PickButton.h
# Timeline
src/timeline/TimelineItem.h
src/timeline/TimelineView.h
src/timeline/TimelineViewManager.h
src/timeline/widgets/AudioItem.h
src/timeline/widgets/FileItem.h
src/timeline/widgets/ImageItem.h
src/timeline/widgets/VideoItem.h
src/timeline/TimelineModel.h
src/timeline/DelegateChooser.h
# UI components
src/ui/Avatar.h
@ -370,7 +365,6 @@ qt5_wrap_cpp(MOC_HEADERS
src/CommunitiesList.h
src/LoginPage.h
src/MainWindow.h
src/MatrixClient.h
src/InviteeItem.h
src/QuickSwitcher.h
src/RegisterPage.h
@ -405,6 +399,9 @@ set(COMMON_LIBS
Qt5::Svg
Qt5::Concurrent
Qt5::Multimedia
Qt5::Qml
Qt5::QuickControls2
Qt5::QuickWidgets
nlohmann_json::nlohmann_json)
if(APPVEYOR_BUILD)
@ -448,6 +445,7 @@ if(APPLE)
target_link_libraries (nheko ${NHEKO_LIBS} Qt5::MacExtras)
elseif(WIN32)
add_executable (nheko ${OS_BUNDLE} ${ICON_FILE} ${NHEKO_DEPS})
target_compile_definitions(nheko PRIVATE WIN32_LEAN_AND_MEAN)
target_link_libraries (nheko ${NTDLIB} ${NHEKO_LIBS} Qt5::WinMain)
else()
add_executable (nheko ${OS_BUNDLE} ${NHEKO_DEPS})

View File

@ -7,7 +7,7 @@ RUN \
add-apt-repository -y ppa:ubuntu-toolchain-r/test && \
apt-get update -qq && \
apt-get install -y \
qt510base qt510tools qt510svg qt510multimedia \
qt510base qt510tools qt510svg qt510multimedia qt510quickcontrols2 qt510graphicaleffects \
gcc-5 g++-5
RUN \
@ -44,4 +44,4 @@ ENV PATH=/opt/qt510/bin:$PATH
RUN mkdir /build
WORKDIR /build
WORKDIR /build

View File

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

View File

@ -89,8 +89,9 @@ sudo port install nheko
### Build Requirements
- Qt5 (5.7 or greater). Qt 5.7 adds support for color font rendering with
Freetype, which is essential to properly support emoji.
- Qt5 (5.8 or greater). Qt 5.7 adds support for color font rendering with
Freetype, which is essential to properly support emoji, 5.8 adds some features
to make interopability with Qml easier.
- CMake 3.1 or greater.
- [mtxclient](https://github.com/Nheko-Reborn/mtxclient)
- [LMDB](https://symas.com/lightning-memory-mapped-database/)

View File

@ -21,4 +21,9 @@ if(NOT EXISTS ${_qrc})
endif()
qt5_add_resources(LANG_QRC ${_qrc})
qt5_add_resources(QRC resources/res.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()

View File

@ -1,14 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE TS>
<TS version="2.1" language="de">
<context>
<name>AudioItem</name>
<message>
<location filename="../../src/timeline/widgets/AudioItem.cpp" line="+118"/>
<source>Save File</source>
<translation>In Datei speichern</translation>
</message>
</context>
<context>
<name>ChatPage</name>
<message>
@ -32,7 +24,7 @@
<translation>Hochladen der Videodatei fehlgeschlagen. Bitte versuche es erneut.</translation>
</message>
<message>
<location line="+380"/>
<location line="+393"/>
<source>Failed to restore OLM account. Please login again.</source>
<translation>Wiederherstellung des OLM Accounts fehlgeschlagen. Bitte logge dich erneut ein.</translation>
</message>
@ -42,18 +34,18 @@
<translation>Gespeicherte Nachrichten konnten nicht wiederhergestellt werden. Bitte melde Dich erneut an.</translation>
</message>
<message>
<location line="+198"/>
<location line="+181"/>
<source>Failed to setup encryption keys. Server response: %1 %2. Please try again later.</source>
<translation>Fehler beim Setup der Verschlüsselungsschlüssel. Servermeldung: %1 %2. Bitte versuche es später erneut.</translation>
</message>
<message>
<location line="+51"/>
<location line="+153"/>
<location line="+155"/>
<source>Please try to login again: %1</source>
<translation>Bitte melde dich erneut an: %1</translation>
</message>
<message>
<location line="-45"/>
<location line="-47"/>
<source>Room creation failed: %1</source>
<translation>Raum konnte nicht erstellt werden: %1</translation>
</message>
@ -116,19 +108,11 @@
</message>
</context>
<context>
<name>FileItem</name>
<name>EncryptionIndicator</name>
<message>
<location filename="../../src/timeline/widgets/FileItem.cpp" line="+107"/>
<source>Save File</source>
<translation>Datei speichern</translation>
</message>
</context>
<context>
<name>ImageItem</name>
<message>
<location filename="../../src/timeline/widgets/ImageItem.cpp" line="+241"/>
<source>Save image</source>
<translation>Bild speichern</translation>
<location filename="../qml/EncryptionIndicator.qml" line="+11"/>
<source>Encrypted</source>
<translation>Verschlüsselt</translation>
</message>
</context>
<context>
@ -200,7 +184,7 @@
<context>
<name>MemberList</name>
<message>
<location filename="../../src/dialogs/MemberList.cpp" line="+96"/>
<location filename="../../src/dialogs/MemberList.cpp" line="+89"/>
<source>Room members</source>
<translation>Teilnehmerliste</translation>
</message>
@ -210,6 +194,14 @@
<translation>OK</translation>
</message>
</context>
<context>
<name>Placeholder</name>
<message>
<location filename="../qml/delegates/Placeholder.qml" line="+4"/>
<source>unimplemented event: </source>
<translation>unimplementiertes event: </translation>
</message>
</context>
<context>
<name>QuickSwitcher</name>
<message>
@ -218,6 +210,14 @@
<translation>Raum suchen</translation>
</message>
</context>
<context>
<name>Redacted</name>
<message>
<location filename="../qml/delegates/Redacted.qml" line="+5"/>
<source>redacted</source>
<translation>gelöscht</translation>
</message>
</context>
<context>
<name>RegisterPage</name>
<message>
@ -277,7 +277,7 @@
<context>
<name>RoomInfo</name>
<message>
<location filename="../../src/Cache.cpp" line="+2205"/>
<location filename="../../src/Cache.cpp" line="+2307"/>
<source>no version stored</source>
<translation>keine Version gespeichert</translation>
</message>
@ -285,12 +285,12 @@
<context>
<name>RoomInfoListItem</name>
<message>
<location filename="../../src/RoomInfoListItem.cpp" line="+93"/>
<location filename="../../src/RoomInfoListItem.cpp" line="+95"/>
<source>Leave room</source>
<translation>Raum verlassen</translation>
</message>
<message>
<location line="+181"/>
<location line="+161"/>
<source>Accept</source>
<translation>Akzeptieren</translation>
</message>
@ -331,25 +331,25 @@
<context>
<name>StatusIndicator</name>
<message>
<location filename="../../src/timeline/TimelineItem.cpp" line="+107"/>
<source>Encrypted</source>
<translation>Verschlüsselt</translation>
<location filename="../qml/StatusIndicator.qml" line="+13"/>
<source>Failed</source>
<translation>Fehlgeschlagen</translation>
</message>
<message>
<location line="+3"/>
<source>Delivered</source>
<translation>Erhalten</translation>
</message>
<message>
<location line="+3"/>
<source>Seen</source>
<translation>Gelesen</translation>
</message>
<message>
<location line="+3"/>
<location line="+1"/>
<source>Sent</source>
<translation>Gesendet</translation>
</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>
<name>TextInputWidget</name>
@ -391,32 +391,9 @@
</message>
</context>
<context>
<name>TimelineItem</name>
<name>TimelineModel</name>
<message>
<location filename="../../src/timeline/TimelineItem.cpp" line="+85"/>
<source>Message redaction failed: %1</source>
<translation>Nachricht zurückziehen fehlgeschlagen: %1</translation>
</message>
<message>
<location line="+39"/>
<source>Reply</source>
<translation>Antworten</translation>
</message>
<message>
<location line="+11"/>
<source>Options</source>
<translation>Optionen</translation>
</message>
</context>
<context>
<name>TimelineView</name>
<message>
<location filename="../../src/timeline/TimelineView.cpp" line="+245"/>
<source>Encryption is enabled</source>
<translation>Verschlüsselung aktiv</translation>
</message>
<message>
<location line="+65"/>
<location filename="../../src/timeline/TimelineModel.cpp" line="+780"/>
<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>
<translation>-- verschlüsselter Event (keine Schlüssel zur Entschlüsselung gefunden) --</translation>
@ -440,16 +417,90 @@
<translation>-- Entschlüsselungsfehler (%1) --</translation>
</message>
<message>
<location line="+27"/>
<location line="+25"/>
<source>-- Encrypted Event (Unknown event type) --</source>
<comment>Placeholder, when the message was decrypted, but we couldn&apos;t parse it, because Nheko/mtxclient don&apos;t support that event type yet</comment>
<translation>-- verschlüsselter Event (Unbekannter Eventtyp) --</translation>
</message>
<message>
<location line="+50"/>
<source>Message redaction failed: %1</source>
<translation>Nachricht zurückziehen fehlgeschlagen: %1</translation>
</message>
</context>
<context>
<name>TimelineRow</name>
<message>
<location filename="../qml/TimelineRow.qml" line="+57"/>
<source>Reply</source>
<translation>Antworten</translation>
</message>
<message>
<location line="+14"/>
<source>Options</source>
<translation>Optionen</translation>
</message>
<message>
<location line="+12"/>
<source>Read receipts</source>
<translation>Lesebestätigungen</translation>
</message>
<message>
<location line="+4"/>
<source>Mark as read</source>
<translation>Als gelesen markieren</translation>
</message>
<message>
<location line="+3"/>
<source>View raw message</source>
<translation>Zeige rohen Nachrichteninhalt</translation>
</message>
<message>
<location line="+4"/>
<source>Redact message</source>
<translation>Nachricht löschen</translation>
</message>
<message>
<location line="+5"/>
<source>Save as</source>
<translation>Speichern als...</translation>
</message>
</context>
<context>
<name>TimelineView</name>
<message>
<location filename="../qml/TimelineView.qml" line="+24"/>
<source>No room open</source>
<translation>Kein Raum geöffnet</translation>
</message>
</context>
<context>
<name>TimelineViewManager</name>
<message>
<location filename="../../src/timeline/TimelineViewManager.cpp" line="+161"/>
<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>TopRoomBar</name>
<message>
<location filename="../../src/TopRoomBar.cpp" line="+79"/>
<location filename="../../src/TopRoomBar.cpp" line="+78"/>
<source>Room options</source>
<translation>Raumoptionen</translation>
</message>
@ -515,7 +566,7 @@
<context>
<name>UserSettingsPage</name>
<message>
<location filename="../../src/UserSettingsPage.cpp" line="+166"/>
<location filename="../../src/UserSettingsPage.cpp" line="+171"/>
<source>Minimize to tray</source>
<translation>Ins Benachrichtigungsfeld minimieren</translation>
</message>
@ -529,6 +580,11 @@
<source>Group&apos;s sidebar</source>
<translation>Gruppen-Seitenleiste</translation>
</message>
<message>
<location line="+9"/>
<source>Circular Avatars</source>
<translation>Runde Profilbilder</translation>
</message>
<message>
<location line="+9"/>
<source>Typing notifications</source>
@ -605,7 +661,7 @@
<translation>ALLGEMEINES</translation>
</message>
<message>
<location line="+156"/>
<location line="+161"/>
<source>Open Sessions File</source>
<translation>Öffne Sessions Datei</translation>
</message>
@ -825,7 +881,7 @@ Medien-Größe: %2
<context>
<name>dialogs::ReadReceipts</name>
<message>
<location filename="../../src/dialogs/ReadReceipts.cpp" line="+121"/>
<location filename="../../src/dialogs/ReadReceipts.cpp" line="+117"/>
<source>Read receipts</source>
<translation>Lesebestätigungen</translation>
</message>
@ -951,7 +1007,7 @@ Medien-Größe: %2
<translation>Aktivierung der Verschlüsselung fehlgeschlagen: %1</translation>
</message>
<message>
<location line="+149"/>
<location line="+148"/>
<source>Select an avatar</source>
<translation>Wähle einen Avatar</translation>
</message>
@ -977,19 +1033,6 @@ Medien-Größe: %2
<translation>Hochladen der Bilddatei fehlgeschlagen: %s</translation>
</message>
</context>
<context>
<name>dialogs::UserMentions</name>
<message>
<location filename="../../src/dialogs/UserMentions.cpp" line="+53"/>
<source>This Room</source>
<translation>Dieser Raum</translation>
</message>
<message>
<location line="+1"/>
<source>All Rooms</source>
<translation>Alle Räume</translation>
</message>
</context>
<context>
<name>dialogs::UserProfile</name>
<message>
@ -1013,7 +1056,7 @@ Medien-Größe: %2
<translation>Gespräch beginnen</translation>
</message>
<message>
<location line="+57"/>
<location line="+56"/>
<source>Devices</source>
<translation>Geräte</translation>
</message>
@ -1064,69 +1107,103 @@ Medien-Größe: %2
<context>
<name>message-description sent:</name>
<message>
<location filename="../../src/Utils.h" line="+104"/>
<source>%1 an audio clip</source>
<translation>%1 einen Audioclip</translation>
<location filename="../../src/Utils.h" line="+95"/>
<source>You sent an audio clip</source>
<translation>Du hast eine Audiodatei gesendet.</translation>
</message>
<message>
<location line="+3"/>
<source>%1 an image</source>
<translation>%1 ein Bild</translation>
<source>%1 sent an audio clip</source>
<translation>%1 hat eine Audiodatei gesendet.</translation>
</message>
<message>
<location line="+5"/>
<source>You sent an image</source>
<translation>Du hast ein Bild gesendet.</translation>
</message>
<message>
<location line="+3"/>
<source>%1 a file</source>
<translation>%1 eine Datei</translation>
<source>%1 sent an image</source>
<translation>%1 hat ein Bild gesendet.</translation>
</message>
<message>
<location line="+5"/>
<source>You sent a file</source>
<translation>Du hast eine Datei gesendet.</translation>
</message>
<message>
<location line="+3"/>
<source>%1 a video clip</source>
<translation>%1 einen Videoclip</translation>
<source>%1 sent a file</source>
<translation>%1 hat eine Datei gesendet.</translation>
</message>
<message>
<location line="+5"/>
<source>You sent a video</source>
<translation>Du hast ein Video gesendet.</translation>
</message>
<message>
<location line="+3"/>
<source>%1 a sticker</source>
<translation>%1 einen Sticker</translation>
<source>%1 sent a video</source>
<translation>%1 hat ein Video gesendet.</translation>
</message>
<message>
<location line="+5"/>
<source>You sent a sticker</source>
<translation>Du hast einen Sticker gesendet.</translation>
</message>
<message>
<location line="+3"/>
<source>%1 a notification</source>
<translation>1% eine Benachrichtigung</translation>
<source>%1 sent a sticker</source>
<translation>%1 hat einen Sticker gesendet.</translation>
</message>
<message>
<location line="+5"/>
<source>You sent a notification</source>
<translation>Du hast eine Benachrichtigung gesendet.</translation>
</message>
<message>
<location line="+3"/>
<source>%1 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>
<location line="+7"/>
<source>%1 an encrypted message</source>
<translation>1% eine verschüsselte Nachricht</translation>
<source>You sent an encrypted message</source>
<translation>Du hast eine verschlüsselte Nachricht gesendet.</translation>
</message>
<message>
<location line="+3"/>
<source>%1 sent an encrypted message</source>
<translation>%1 hat eine verschlüsselte Nachricht gesendet.</translation>
</message>
</context>
<context>
<name>message-description:</name>
<name>popups::UserMentions</name>
<message>
<location line="-26"/>
<source>sent</source>
<comment>For when someone else is the sender</comment>
<translation type="unfinished"></translation>
<location filename="../../src/popups/UserMentions.cpp" line="+61"/>
<source>This Room</source>
<translation>Dieser Raum</translation>
</message>
</context>
<context>
<name>message-description: </name>
<message>
<location line="-2"/>
<source>sent</source>
<comment>For when you are the sender</comment>
<translation type="unfinished"></translation>
<location line="+1"/>
<source>All Rooms</source>
<translation>Alle Räume</translation>
</message>
</context>
<context>
<name>utils</name>
<message>
<location filename="../../src/Utils.cpp" line="+46"/>
<location filename="../../src/Utils.h" line="+55"/>
<source>You</source>
<translation>Du</translation>
</message>
<message>
<location line="+219"/>
<location filename="../../src/Utils.cpp" line="+282"/>
<source>sent a file.</source>
<translation type="unfinished"></translation>
</message>
@ -1146,7 +1223,7 @@ Medien-Größe: %2
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../../src/Utils.h" line="-23"/>
<location filename="../../src/Utils.h" line="+4"/>
<source>Unknown Message Type</source>
<translation>Unbekannter Nachrichtentyp</translation>
</message>

View File

@ -1,14 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE TS>
<TS version="2.1" language="el">
<context>
<name>AudioItem</name>
<message>
<location filename="../../src/timeline/widgets/AudioItem.cpp" line="+118"/>
<source>Save File</source>
<translation>Αποθήκευση</translation>
</message>
</context>
<context>
<name>ChatPage</name>
<message>
@ -32,7 +24,7 @@
<translation type="unfinished"></translation>
</message>
<message>
<location line="+380"/>
<location line="+393"/>
<source>Failed to restore OLM account. Please login again.</source>
<translation type="unfinished"></translation>
</message>
@ -42,18 +34,18 @@
<translation type="unfinished"></translation>
</message>
<message>
<location line="+198"/>
<location line="+181"/>
<source>Failed to setup encryption keys. Server response: %1 %2. Please try again later.</source>
<translation type="unfinished"></translation>
</message>
<message>
<location line="+51"/>
<location line="+153"/>
<location line="+155"/>
<source>Please try to login again: %1</source>
<translation type="unfinished"></translation>
</message>
<message>
<location line="-45"/>
<location line="-47"/>
<source>Room creation failed: %1</source>
<translation type="unfinished"></translation>
</message>
@ -116,19 +108,11 @@
</message>
</context>
<context>
<name>FileItem</name>
<name>EncryptionIndicator</name>
<message>
<location filename="../../src/timeline/widgets/FileItem.cpp" line="+107"/>
<source>Save File</source>
<translation>Αποθήκευση</translation>
</message>
</context>
<context>
<name>ImageItem</name>
<message>
<location filename="../../src/timeline/widgets/ImageItem.cpp" line="+241"/>
<source>Save image</source>
<translation>Αποθήκευση Εικόνας</translation>
<location filename="../qml/EncryptionIndicator.qml" line="+11"/>
<source>Encrypted</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
@ -200,7 +184,7 @@
<context>
<name>MemberList</name>
<message>
<location filename="../../src/dialogs/MemberList.cpp" line="+96"/>
<location filename="../../src/dialogs/MemberList.cpp" line="+89"/>
<source>Room members</source>
<translation>Μέλη</translation>
</message>
@ -210,6 +194,14 @@
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>Placeholder</name>
<message>
<location filename="../qml/delegates/Placeholder.qml" line="+4"/>
<source>unimplemented event: </source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>QuickSwitcher</name>
<message>
@ -218,6 +210,14 @@
<translation>Αναζήτηση συνομιλίας...</translation>
</message>
</context>
<context>
<name>Redacted</name>
<message>
<location filename="../qml/delegates/Redacted.qml" line="+5"/>
<source>redacted</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>RegisterPage</name>
<message>
@ -277,7 +277,7 @@
<context>
<name>RoomInfo</name>
<message>
<location filename="../../src/Cache.cpp" line="+2205"/>
<location filename="../../src/Cache.cpp" line="+2307"/>
<source>no version stored</source>
<translation type="unfinished"></translation>
</message>
@ -285,12 +285,12 @@
<context>
<name>RoomInfoListItem</name>
<message>
<location filename="../../src/RoomInfoListItem.cpp" line="+93"/>
<location filename="../../src/RoomInfoListItem.cpp" line="+95"/>
<source>Leave room</source>
<translation>Βγές</translation>
</message>
<message>
<location line="+181"/>
<location line="+161"/>
<source>Accept</source>
<translation>Αποδοχή</translation>
</message>
@ -331,25 +331,25 @@
<context>
<name>StatusIndicator</name>
<message>
<location filename="../../src/timeline/TimelineItem.cpp" line="+107"/>
<source>Encrypted</source>
<location filename="../qml/StatusIndicator.qml" line="+13"/>
<source>Failed</source>
<translation type="unfinished"></translation>
</message>
<message>
<location line="+3"/>
<source>Delivered</source>
<translation type="unfinished"></translation>
</message>
<message>
<location line="+3"/>
<source>Seen</source>
<translation type="unfinished"></translation>
</message>
<message>
<location line="+3"/>
<location line="+1"/>
<source>Sent</source>
<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>
</context>
<context>
<name>TextInputWidget</name>
@ -391,32 +391,9 @@
</message>
</context>
<context>
<name>TimelineItem</name>
<name>TimelineModel</name>
<message>
<location filename="../../src/timeline/TimelineItem.cpp" line="+85"/>
<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"/>
<location filename="../../src/timeline/TimelineModel.cpp" line="+780"/>
<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>
<translation type="unfinished"></translation>
@ -440,16 +417,90 @@
<translation type="unfinished"></translation>
</message>
<message>
<location line="+27"/>
<location line="+25"/>
<source>-- Encrypted Event (Unknown event type) --</source>
<comment>Placeholder, when the message was decrypted, but we couldn&apos;t parse it, because Nheko/mtxclient don&apos;t support that event type yet</comment>
<translation type="unfinished"></translation>
</message>
<message>
<location line="+50"/>
<source>Message redaction failed: %1</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>TimelineRow</name>
<message>
<location filename="../qml/TimelineRow.qml" line="+57"/>
<source>Reply</source>
<translation type="unfinished"></translation>
</message>
<message>
<location line="+14"/>
<source>Options</source>
<translation type="unfinished"></translation>
</message>
<message>
<location line="+12"/>
<source>Read receipts</source>
<translation type="unfinished"></translation>
</message>
<message>
<location line="+4"/>
<source>Mark as read</source>
<translation type="unfinished"></translation>
</message>
<message>
<location line="+3"/>
<source>View raw message</source>
<translation type="unfinished"></translation>
</message>
<message>
<location line="+4"/>
<source>Redact message</source>
<translation type="unfinished"></translation>
</message>
<message>
<location line="+5"/>
<source>Save as</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>TimelineView</name>
<message>
<location filename="../qml/TimelineView.qml" line="+24"/>
<source>No room open</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>TimelineViewManager</name>
<message>
<location filename="../../src/timeline/TimelineViewManager.cpp" line="+161"/>
<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>TopRoomBar</name>
<message>
<location filename="../../src/TopRoomBar.cpp" line="+79"/>
<location filename="../../src/TopRoomBar.cpp" line="+78"/>
<source>Room options</source>
<translation type="unfinished"></translation>
</message>
@ -515,7 +566,7 @@
<context>
<name>UserSettingsPage</name>
<message>
<location filename="../../src/UserSettingsPage.cpp" line="+166"/>
<location filename="../../src/UserSettingsPage.cpp" line="+171"/>
<source>Minimize to tray</source>
<translation>Ελαχιστοποίηση</translation>
</message>
@ -529,6 +580,11 @@
<source>Group&apos;s sidebar</source>
<translation type="unfinished"></translation>
</message>
<message>
<location line="+9"/>
<source>Circular Avatars</source>
<translation type="unfinished"></translation>
</message>
<message>
<location line="+9"/>
<source>Typing notifications</source>
@ -605,7 +661,7 @@
<translation>ΓΕΝΙΚΑ</translation>
</message>
<message>
<location line="+156"/>
<location line="+161"/>
<source>Open Sessions File</source>
<translation type="unfinished"></translation>
</message>
@ -823,7 +879,7 @@ Media size: %2
<context>
<name>dialogs::ReadReceipts</name>
<message>
<location filename="../../src/dialogs/ReadReceipts.cpp" line="+121"/>
<location filename="../../src/dialogs/ReadReceipts.cpp" line="+117"/>
<source>Read receipts</source>
<translation type="unfinished"></translation>
</message>
@ -949,7 +1005,7 @@ Media size: %2
<translation type="unfinished"></translation>
</message>
<message>
<location line="+149"/>
<location line="+148"/>
<source>Select an avatar</source>
<translation type="unfinished"></translation>
</message>
@ -975,19 +1031,6 @@ Media size: %2
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>dialogs::UserMentions</name>
<message>
<location filename="../../src/dialogs/UserMentions.cpp" line="+53"/>
<source>This Room</source>
<translation type="unfinished"></translation>
</message>
<message>
<location line="+1"/>
<source>All Rooms</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>dialogs::UserProfile</name>
<message>
@ -1011,7 +1054,7 @@ Media size: %2
<translation type="unfinished"></translation>
</message>
<message>
<location line="+57"/>
<location line="+56"/>
<source>Devices</source>
<translation type="unfinished"></translation>
</message>
@ -1062,69 +1105,103 @@ Media size: %2
<context>
<name>message-description sent:</name>
<message>
<location filename="../../src/Utils.h" line="+104"/>
<source>%1 an audio clip</source>
<location filename="../../src/Utils.h" line="+95"/>
<source>You sent an audio clip</source>
<translation type="unfinished"></translation>
</message>
<message>
<location line="+3"/>
<source>%1 an image</source>
<source>%1 sent an audio clip</source>
<translation type="unfinished"></translation>
</message>
<message>
<location line="+5"/>
<source>You sent an image</source>
<translation type="unfinished"></translation>
</message>
<message>
<location line="+3"/>
<source>%1 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>
</message>
<message>
<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>
</message>
<message>
<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>
</message>
<message>
<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>
</message>
<message>
<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>
</message>
</context>
<context>
<name>message-description:</name>
<name>popups::UserMentions</name>
<message>
<location line="-26"/>
<source>sent</source>
<comment>For when someone else is the sender</comment>
<location filename="../../src/popups/UserMentions.cpp" line="+61"/>
<source>This Room</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>message-description: </name>
<message>
<location line="-2"/>
<source>sent</source>
<comment>For when you are the sender</comment>
<location line="+1"/>
<source>All Rooms</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>utils</name>
<message>
<location filename="../../src/Utils.cpp" line="+46"/>
<location filename="../../src/Utils.h" line="+55"/>
<source>You</source>
<translation type="unfinished"></translation>
</message>
<message>
<location line="+219"/>
<location filename="../../src/Utils.cpp" line="+282"/>
<source>sent a file.</source>
<translation type="unfinished"></translation>
</message>
@ -1144,7 +1221,7 @@ Media size: %2
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../../src/Utils.h" line="-23"/>
<location filename="../../src/Utils.h" line="+4"/>
<source>Unknown Message Type</source>
<translation type="unfinished"></translation>
</message>

View File

@ -1,14 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE TS>
<TS version="2.1" language="en">
<context>
<name>AudioItem</name>
<message>
<location filename="../../src/timeline/widgets/AudioItem.cpp" line="+118"/>
<source>Save File</source>
<translation>Save File</translation>
</message>
</context>
<context>
<name>ChatPage</name>
<message>
@ -32,7 +24,7 @@
<translation>Failed to upload video. Please try again.</translation>
</message>
<message>
<location line="+380"/>
<location line="+393"/>
<source>Failed to restore OLM account. Please login again.</source>
<translation>Failed to restore OLM account. Please login again.</translation>
</message>
@ -42,18 +34,18 @@
<translation>Failed to restore save data. Please login again.</translation>
</message>
<message>
<location line="+198"/>
<location line="+181"/>
<source>Failed to setup encryption keys. Server response: %1 %2. Please try again later.</source>
<translation>Failed to setup encryption keys. Server response: %1 %2. Please try again later.</translation>
</message>
<message>
<location line="+51"/>
<location line="+153"/>
<location line="+155"/>
<source>Please try to login again: %1</source>
<translation>Please try to login again: %1</translation>
</message>
<message>
<location line="-45"/>
<location line="-47"/>
<source>Room creation failed: %1</source>
<translation>Room creation failed: %1</translation>
</message>
@ -116,19 +108,11 @@
</message>
</context>
<context>
<name>FileItem</name>
<name>EncryptionIndicator</name>
<message>
<location filename="../../src/timeline/widgets/FileItem.cpp" line="+107"/>
<source>Save File</source>
<translation>Save File</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>
<location filename="../qml/EncryptionIndicator.qml" line="+11"/>
<source>Encrypted</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
@ -200,7 +184,7 @@
<context>
<name>MemberList</name>
<message>
<location filename="../../src/dialogs/MemberList.cpp" line="+96"/>
<location filename="../../src/dialogs/MemberList.cpp" line="+89"/>
<source>Room members</source>
<translation>Room members</translation>
</message>
@ -210,6 +194,14 @@
<translation>OK</translation>
</message>
</context>
<context>
<name>Placeholder</name>
<message>
<location filename="../qml/delegates/Placeholder.qml" line="+4"/>
<source>unimplemented event: </source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>QuickSwitcher</name>
<message>
@ -218,6 +210,14 @@
<translation>Search for a room</translation>
</message>
</context>
<context>
<name>Redacted</name>
<message>
<location filename="../qml/delegates/Redacted.qml" line="+5"/>
<source>redacted</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>RegisterPage</name>
<message>
@ -277,7 +277,7 @@
<context>
<name>RoomInfo</name>
<message>
<location filename="../../src/Cache.cpp" line="+2205"/>
<location filename="../../src/Cache.cpp" line="+2307"/>
<source>no version stored</source>
<translation>no version stored</translation>
</message>
@ -285,12 +285,12 @@
<context>
<name>RoomInfoListItem</name>
<message>
<location filename="../../src/RoomInfoListItem.cpp" line="+93"/>
<location filename="../../src/RoomInfoListItem.cpp" line="+95"/>
<source>Leave room</source>
<translation>Leave room</translation>
</message>
<message>
<location line="+181"/>
<location line="+161"/>
<source>Accept</source>
<translation>Accept</translation>
</message>
@ -331,24 +331,24 @@
<context>
<name>StatusIndicator</name>
<message>
<location filename="../../src/timeline/TimelineItem.cpp" line="+107"/>
<source>Encrypted</source>
<translation>Encrypted</translation>
<location filename="../qml/StatusIndicator.qml" line="+13"/>
<source>Failed</source>
<translation type="unfinished"></translation>
</message>
<message>
<location line="+3"/>
<source>Delivered</source>
<translation>Delivered</translation>
</message>
<message>
<location line="+3"/>
<source>Seen</source>
<translation>Seen</translation>
</message>
<message>
<location line="+3"/>
<location line="+1"/>
<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>
</context>
<context>
@ -391,65 +391,116 @@
</message>
</context>
<context>
<name>TimelineItem</name>
<name>TimelineModel</name>
<message>
<location filename="../../src/timeline/TimelineItem.cpp" line="+85"/>
<source>Message redaction failed: %1</source>
<translation>Message redaction failed: %1</translation>
</message>
<message>
<location line="+39"/>
<source>Reply</source>
<translation>Reply</translation>
</message>
<message>
<location line="+11"/>
<source>Options</source>
<translation>Options</translation>
</message>
</context>
<context>
<name>TimelineView</name>
<message>
<location filename="../../src/timeline/TimelineView.cpp" line="+245"/>
<source>Encryption is enabled</source>
<translation>Encryption is enabled</translation>
</message>
<message>
<location line="+65"/>
<location filename="../../src/timeline/TimelineModel.cpp" line="+780"/>
<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>
<translation>-- Encrypted Event (No keys found for decryption) --</translation>
<translation type="unfinished">-- Encrypted Event (No keys found for decryption) --</translation>
</message>
<message>
<location line="+15"/>
<source>-- Decryption Error (failed to communicate with DB) --</source>
<comment>Placeholder, when the message can&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>
<location line="+19"/>
<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>
<translation>-- Decryption Error (failed to retrieve megolm keys from db) --</translation>
<translation type="unfinished">-- Decryption Error (failed to retrieve megolm keys from db) --</translation>
</message>
<message>
<location line="+12"/>
<source>-- Decryption Error (%1) --</source>
<comment>Placeholder, when the message can&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>
<location line="+27"/>
<location line="+25"/>
<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>
<translation>-- Encrypted Event (Unknown event type) --</translation>
<translation type="unfinished">-- Encrypted Event (Unknown event type) --</translation>
</message>
<message>
<location line="+50"/>
<source>Message redaction failed: %1</source>
<translation type="unfinished">Message redaction failed: %1</translation>
</message>
</context>
<context>
<name>TimelineRow</name>
<message>
<location filename="../qml/TimelineRow.qml" line="+57"/>
<source>Reply</source>
<translation type="unfinished"></translation>
</message>
<message>
<location line="+14"/>
<source>Options</source>
<translation type="unfinished"></translation>
</message>
<message>
<location line="+12"/>
<source>Read receipts</source>
<translation type="unfinished">Read receipts</translation>
</message>
<message>
<location line="+4"/>
<source>Mark as read</source>
<translation type="unfinished"></translation>
</message>
<message>
<location line="+3"/>
<source>View raw message</source>
<translation type="unfinished"></translation>
</message>
<message>
<location line="+4"/>
<source>Redact message</source>
<translation type="unfinished"></translation>
</message>
<message>
<location line="+5"/>
<source>Save as</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>TimelineView</name>
<message>
<location filename="../qml/TimelineView.qml" line="+24"/>
<source>No room open</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>TimelineViewManager</name>
<message>
<location filename="../../src/timeline/TimelineViewManager.cpp" line="+161"/>
<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>TopRoomBar</name>
<message>
<location filename="../../src/TopRoomBar.cpp" line="+79"/>
<location filename="../../src/TopRoomBar.cpp" line="+78"/>
<source>Room options</source>
<translation>Room options</translation>
</message>
@ -515,7 +566,7 @@
<context>
<name>UserSettingsPage</name>
<message>
<location filename="../../src/UserSettingsPage.cpp" line="+166"/>
<location filename="../../src/UserSettingsPage.cpp" line="+171"/>
<source>Minimize to tray</source>
<translation>Minimize to tray</translation>
</message>
@ -529,6 +580,11 @@
<source>Group&apos;s sidebar</source>
<translation>Group&apos;s sidebar</translation>
</message>
<message>
<location line="+9"/>
<source>Circular Avatars</source>
<translation type="unfinished"></translation>
</message>
<message>
<location line="+9"/>
<source>Typing notifications</source>
@ -605,7 +661,7 @@
<translation>GENERAL</translation>
</message>
<message>
<location line="+156"/>
<location line="+161"/>
<source>Open Sessions File</source>
<translation>Open Sessions File</translation>
</message>
@ -825,7 +881,7 @@ Media size: %2
<context>
<name>dialogs::ReadReceipts</name>
<message>
<location filename="../../src/dialogs/ReadReceipts.cpp" line="+121"/>
<location filename="../../src/dialogs/ReadReceipts.cpp" line="+117"/>
<source>Read receipts</source>
<translation>Read receipts</translation>
</message>
@ -953,7 +1009,7 @@ Media size: %2
<translation>Failed to enable encryption: %1</translation>
</message>
<message>
<location line="+149"/>
<location line="+148"/>
<source>Select an avatar</source>
<translation>Select an avatar</translation>
</message>
@ -979,19 +1035,6 @@ Media size: %2
<translation>Failed to upload image: %s</translation>
</message>
</context>
<context>
<name>dialogs::UserMentions</name>
<message>
<location filename="../../src/dialogs/UserMentions.cpp" line="+53"/>
<source>This Room</source>
<translation>This Room</translation>
</message>
<message>
<location line="+1"/>
<source>All Rooms</source>
<translation>All Rooms</translation>
</message>
</context>
<context>
<name>dialogs::UserProfile</name>
<message>
@ -1015,7 +1058,7 @@ Media size: %2
<translation>Start a conversation</translation>
</message>
<message>
<location line="+57"/>
<location line="+56"/>
<source>Devices</source>
<translation>Devices</translation>
</message>
@ -1066,69 +1109,103 @@ Media size: %2
<context>
<name>message-description sent:</name>
<message>
<location filename="../../src/Utils.h" line="+104"/>
<source>%1 an audio clip</source>
<translation>%1 an audio clip</translation>
<location filename="../../src/Utils.h" line="+95"/>
<source>You sent an audio clip</source>
<translation type="unfinished"></translation>
</message>
<message>
<location line="+3"/>
<source>%1 an image</source>
<translation>%1 an image</translation>
<source>%1 sent an audio clip</source>
<translation type="unfinished"></translation>
</message>
<message>
<location line="+5"/>
<source>You sent an image</source>
<translation type="unfinished"></translation>
</message>
<message>
<location line="+3"/>
<source>%1 a file</source>
<translation>%1 a file</translation>
<source>%1 sent an image</source>
<translation type="unfinished"></translation>
</message>
<message>
<location line="+5"/>
<source>You sent a file</source>
<translation type="unfinished"></translation>
</message>
<message>
<location line="+3"/>
<source>%1 a video clip</source>
<translation>%1 a video clip</translation>
<source>%1 sent a file</source>
<translation type="unfinished"></translation>
</message>
<message>
<location line="+5"/>
<source>You sent a video</source>
<translation type="unfinished"></translation>
</message>
<message>
<location line="+3"/>
<source>%1 a sticker</source>
<translation>%1 a sticker</translation>
<source>%1 sent a video</source>
<translation type="unfinished"></translation>
</message>
<message>
<location line="+5"/>
<source>You sent a sticker</source>
<translation type="unfinished"></translation>
</message>
<message>
<location line="+3"/>
<source>%1 a notification</source>
<translation>%1 a notification</translation>
<source>%1 sent a sticker</source>
<translation type="unfinished"></translation>
</message>
<message>
<location line="+5"/>
<source>You sent a notification</source>
<translation type="unfinished"></translation>
</message>
<message>
<location line="+3"/>
<source>%1 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>
<location line="+7"/>
<source>%1 an encrypted message</source>
<translation>%1 an encrypted message</translation>
<source>You sent an encrypted message</source>
<translation type="unfinished"></translation>
</message>
<message>
<location line="+3"/>
<source>%1 sent an encrypted message</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>message-description:</name>
<name>popups::UserMentions</name>
<message>
<location line="-26"/>
<source>sent</source>
<comment>For when someone else is the sender</comment>
<translation>sent</translation>
<location filename="../../src/popups/UserMentions.cpp" line="+61"/>
<source>This Room</source>
<translation type="unfinished">This Room</translation>
</message>
</context>
<context>
<name>message-description: </name>
<message>
<location line="-2"/>
<source>sent</source>
<comment>For when you are the sender</comment>
<translation>sent</translation>
<location line="+1"/>
<source>All Rooms</source>
<translation type="unfinished">All Rooms</translation>
</message>
</context>
<context>
<name>utils</name>
<message>
<location filename="../../src/Utils.cpp" line="+46"/>
<location filename="../../src/Utils.h" line="+55"/>
<source>You</source>
<translation>You</translation>
</message>
<message>
<location line="+219"/>
<location filename="../../src/Utils.cpp" line="+282"/>
<source>sent a file.</source>
<translation>sent a file.</translation>
</message>
@ -1148,7 +1225,7 @@ Media size: %2
<translation>sent a video.</translation>
</message>
<message>
<location filename="../../src/Utils.h" line="-23"/>
<location filename="../../src/Utils.h" line="+4"/>
<source>Unknown Message Type</source>
<translation>Unknown Message Type</translation>
</message>

View File

@ -1,14 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE TS>
<TS version="2.1" language="fi">
<context>
<name>AudioItem</name>
<message>
<location filename="../../src/timeline/widgets/AudioItem.cpp" line="+118"/>
<source>Save File</source>
<translation>Tallenna tiedosto</translation>
</message>
</context>
<context>
<name>ChatPage</name>
<message>
@ -32,7 +24,7 @@
<translation>Videon lähettäminen epäonnistui. Ole hyvä ja yritä uudelleen.</translation>
</message>
<message>
<location line="+380"/>
<location line="+393"/>
<source>Failed to restore OLM account. Please login again.</source>
<translation>OLM-tilin palauttaminen epäonnistui. Ole hyvä ja kirjaudu sisään uudelleen.</translation>
</message>
@ -42,18 +34,18 @@
<translation>Tallennettujen tietojen palauttaminen epäonnistui. Ole hyvä ja kirjaudu sisään uudelleen.</translation>
</message>
<message>
<location line="+198"/>
<location line="+181"/>
<source>Failed to setup encryption keys. Server response: %1 %2. Please try again later.</source>
<translation>Salausavainten lähetys epäonnistui. Palvelimen vastaus: %1 %2. Ole hyvä ja yritä uudelleen myöhemmin.</translation>
</message>
<message>
<location line="+51"/>
<location line="+153"/>
<location line="+155"/>
<source>Please try to login again: %1</source>
<translation>Ole hyvä ja yritä kirjautua sisään uudelleen: %1</translation>
</message>
<message>
<location line="-45"/>
<location line="-47"/>
<source>Room creation failed: %1</source>
<translation>Huoneen luominen epäonnistui: %1</translation>
</message>
@ -116,19 +108,11 @@
</message>
</context>
<context>
<name>FileItem</name>
<name>EncryptionIndicator</name>
<message>
<location filename="../../src/timeline/widgets/FileItem.cpp" line="+107"/>
<source>Save File</source>
<translation>Tallenna tiedosto</translation>
</message>
</context>
<context>
<name>ImageItem</name>
<message>
<location filename="../../src/timeline/widgets/ImageItem.cpp" line="+241"/>
<source>Save image</source>
<translation>Tallenna kuva</translation>
<location filename="../qml/EncryptionIndicator.qml" line="+11"/>
<source>Encrypted</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
@ -200,7 +184,7 @@
<context>
<name>MemberList</name>
<message>
<location filename="../../src/dialogs/MemberList.cpp" line="+96"/>
<location filename="../../src/dialogs/MemberList.cpp" line="+89"/>
<source>Room members</source>
<translation>Huoneen jäsenet</translation>
</message>
@ -210,6 +194,14 @@
<translation>OK</translation>
</message>
</context>
<context>
<name>Placeholder</name>
<message>
<location filename="../qml/delegates/Placeholder.qml" line="+4"/>
<source>unimplemented event: </source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>QuickSwitcher</name>
<message>
@ -218,6 +210,14 @@
<translation>Etsi huonetta</translation>
</message>
</context>
<context>
<name>Redacted</name>
<message>
<location filename="../qml/delegates/Redacted.qml" line="+5"/>
<source>redacted</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>RegisterPage</name>
<message>
@ -277,7 +277,7 @@
<context>
<name>RoomInfo</name>
<message>
<location filename="../../src/Cache.cpp" line="+2205"/>
<location filename="../../src/Cache.cpp" line="+2307"/>
<source>no version stored</source>
<translation>ei tallennettua versiota</translation>
</message>
@ -285,12 +285,12 @@
<context>
<name>RoomInfoListItem</name>
<message>
<location filename="../../src/RoomInfoListItem.cpp" line="+93"/>
<location filename="../../src/RoomInfoListItem.cpp" line="+95"/>
<source>Leave room</source>
<translation>Poistu huoneesta</translation>
</message>
<message>
<location line="+181"/>
<location line="+161"/>
<source>Accept</source>
<translation>Hyväksy</translation>
</message>
@ -331,24 +331,24 @@
<context>
<name>StatusIndicator</name>
<message>
<location filename="../../src/timeline/TimelineItem.cpp" line="+107"/>
<source>Encrypted</source>
<translation>Salattu</translation>
<location filename="../qml/StatusIndicator.qml" line="+13"/>
<source>Failed</source>
<translation type="unfinished"></translation>
</message>
<message>
<location line="+3"/>
<source>Delivered</source>
<translation>Toimitettu</translation>
</message>
<message>
<location line="+3"/>
<source>Seen</source>
<translation>Luettu</translation>
</message>
<message>
<location line="+3"/>
<location line="+1"/>
<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>
</context>
<context>
@ -391,65 +391,116 @@
</message>
</context>
<context>
<name>TimelineItem</name>
<name>TimelineModel</name>
<message>
<location filename="../../src/timeline/TimelineItem.cpp" line="+85"/>
<source>Message redaction failed: %1</source>
<translation>Viestin poisto epäonnistui: %1</translation>
</message>
<message>
<location line="+39"/>
<source>Reply</source>
<translation>Vastaa</translation>
</message>
<message>
<location line="+11"/>
<source>Options</source>
<translation>Asetukset</translation>
</message>
</context>
<context>
<name>TimelineView</name>
<message>
<location filename="../../src/timeline/TimelineView.cpp" line="+245"/>
<source>Encryption is enabled</source>
<translation>Salaus on käytössä</translation>
</message>
<message>
<location line="+65"/>
<location filename="../../src/timeline/TimelineModel.cpp" line="+780"/>
<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>
<translation>-- Salattu viesti (salauksen purkuavaimia ei löydetty) --</translation>
<translation type="unfinished">-- Salattu viesti (salauksen purkuavaimia ei löydetty) --</translation>
</message>
<message>
<location line="+15"/>
<source>-- Decryption Error (failed to communicate with DB) --</source>
<comment>Placeholder, when the message can&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>
<location line="+19"/>
<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>
<translation>-- Virhe purkaessa salausta (megolm-avaimien hakeminen tietokannasta epäonnistui) --</translation>
<translation type="unfinished">-- Virhe purkaessa salausta (megolm-avaimien hakeminen tietokannasta epäonnistui) --</translation>
</message>
<message>
<location line="+12"/>
<source>-- Decryption Error (%1) --</source>
<comment>Placeholder, when the message can&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>
<location line="+27"/>
<location line="+25"/>
<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>
<translation>-- Salattu viesti (tuntematon viestityyppi) --</translation>
<translation type="unfinished">-- Salattu viesti (tuntematon viestityyppi) --</translation>
</message>
<message>
<location line="+50"/>
<source>Message redaction failed: %1</source>
<translation type="unfinished">Viestin poisto epäonnistui: %1</translation>
</message>
</context>
<context>
<name>TimelineRow</name>
<message>
<location filename="../qml/TimelineRow.qml" line="+57"/>
<source>Reply</source>
<translation type="unfinished"></translation>
</message>
<message>
<location line="+14"/>
<source>Options</source>
<translation type="unfinished"></translation>
</message>
<message>
<location line="+12"/>
<source>Read receipts</source>
<translation type="unfinished">Lukukuittaukset</translation>
</message>
<message>
<location line="+4"/>
<source>Mark as read</source>
<translation type="unfinished"></translation>
</message>
<message>
<location line="+3"/>
<source>View raw message</source>
<translation type="unfinished"></translation>
</message>
<message>
<location line="+4"/>
<source>Redact message</source>
<translation type="unfinished"></translation>
</message>
<message>
<location line="+5"/>
<source>Save as</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>TimelineView</name>
<message>
<location filename="../qml/TimelineView.qml" line="+24"/>
<source>No room open</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>TimelineViewManager</name>
<message>
<location filename="../../src/timeline/TimelineViewManager.cpp" line="+161"/>
<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>TopRoomBar</name>
<message>
<location filename="../../src/TopRoomBar.cpp" line="+79"/>
<location filename="../../src/TopRoomBar.cpp" line="+78"/>
<source>Room options</source>
<translation>Huonevaihtoehdot</translation>
</message>
@ -515,7 +566,7 @@
<context>
<name>UserSettingsPage</name>
<message>
<location filename="../../src/UserSettingsPage.cpp" line="+166"/>
<location filename="../../src/UserSettingsPage.cpp" line="+171"/>
<source>Minimize to tray</source>
<translation>Pienennä ilmoitusalueelle</translation>
</message>
@ -529,6 +580,11 @@
<source>Group&apos;s sidebar</source>
<translation>Ryhmäsivupalkki</translation>
</message>
<message>
<location line="+9"/>
<source>Circular Avatars</source>
<translation type="unfinished"></translation>
</message>
<message>
<location line="+9"/>
<source>Typing notifications</source>
@ -605,7 +661,7 @@
<translation>YLEISET ASETUKSET</translation>
</message>
<message>
<location line="+156"/>
<location line="+161"/>
<source>Open Sessions File</source>
<translation>Avaa Istuntoavaintiedosto</translation>
</message>
@ -825,7 +881,7 @@ Median koko: %2
<context>
<name>dialogs::ReadReceipts</name>
<message>
<location filename="../../src/dialogs/ReadReceipts.cpp" line="+121"/>
<location filename="../../src/dialogs/ReadReceipts.cpp" line="+117"/>
<source>Read receipts</source>
<translation>Lukukuittaukset</translation>
</message>
@ -953,7 +1009,7 @@ Median koko: %2
<translation>Salauksen aktivointi epäonnistui: %1</translation>
</message>
<message>
<location line="+149"/>
<location line="+148"/>
<source>Select an avatar</source>
<translation>Valitse profiilikuva</translation>
</message>
@ -979,19 +1035,6 @@ Median koko: %2
<translation>Kuvan lähetys epäonnistui: %s</translation>
</message>
</context>
<context>
<name>dialogs::UserMentions</name>
<message>
<location filename="../../src/dialogs/UserMentions.cpp" line="+53"/>
<source>This Room</source>
<translation type="unfinished"></translation>
</message>
<message>
<location line="+1"/>
<source>All Rooms</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>dialogs::UserProfile</name>
<message>
@ -1015,7 +1058,7 @@ Median koko: %2
<translation>Aloita keskustelu</translation>
</message>
<message>
<location line="+57"/>
<location line="+56"/>
<source>Devices</source>
<translation>Laitteet</translation>
</message>
@ -1066,69 +1109,103 @@ Median koko: %2
<context>
<name>message-description sent:</name>
<message>
<location filename="../../src/Utils.h" line="+104"/>
<source>%1 an audio clip</source>
<location filename="../../src/Utils.h" line="+95"/>
<source>You sent an audio clip</source>
<translation type="unfinished"></translation>
</message>
<message>
<location line="+3"/>
<source>%1 an image</source>
<source>%1 sent an audio clip</source>
<translation type="unfinished"></translation>
</message>
<message>
<location line="+5"/>
<source>You sent an image</source>
<translation type="unfinished"></translation>
</message>
<message>
<location line="+3"/>
<source>%1 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>
</message>
<message>
<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>
</message>
<message>
<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>
</message>
<message>
<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>
</message>
<message>
<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>
</message>
</context>
<context>
<name>message-description:</name>
<name>popups::UserMentions</name>
<message>
<location line="-26"/>
<source>sent</source>
<comment>For when someone else is the sender</comment>
<location filename="../../src/popups/UserMentions.cpp" line="+61"/>
<source>This Room</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>message-description: </name>
<message>
<location line="-2"/>
<source>sent</source>
<comment>For when you are the sender</comment>
<location line="+1"/>
<source>All Rooms</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>utils</name>
<message>
<location filename="../../src/Utils.cpp" line="+46"/>
<location filename="../../src/Utils.h" line="+55"/>
<source>You</source>
<translation>Sinä</translation>
</message>
<message>
<location line="+219"/>
<location filename="../../src/Utils.cpp" line="+282"/>
<source>sent a file.</source>
<translation type="unfinished"></translation>
</message>
@ -1148,7 +1225,7 @@ Median koko: %2
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../../src/Utils.h" line="-23"/>
<location filename="../../src/Utils.h" line="+4"/>
<source>Unknown Message Type</source>
<translation type="unfinished"></translation>
</message>

View File

@ -1,14 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE TS>
<TS version="2.1" language="fr">
<context>
<name>AudioItem</name>
<message>
<location filename="../../src/timeline/widgets/AudioItem.cpp" line="+118"/>
<source>Save File</source>
<translation>Enregistrer le fichier</translation>
</message>
</context>
<context>
<name>ChatPage</name>
<message>
@ -32,7 +24,7 @@
<translation type="unfinished"></translation>
</message>
<message>
<location line="+380"/>
<location line="+393"/>
<source>Failed to restore OLM account. Please login again.</source>
<translation type="unfinished"></translation>
</message>
@ -42,18 +34,18 @@
<translation type="unfinished"></translation>
</message>
<message>
<location line="+198"/>
<location line="+181"/>
<source>Failed to setup encryption keys. Server response: %1 %2. Please try again later.</source>
<translation type="unfinished"></translation>
</message>
<message>
<location line="+51"/>
<location line="+153"/>
<location line="+155"/>
<source>Please try to login again: %1</source>
<translation type="unfinished"></translation>
</message>
<message>
<location line="-45"/>
<location line="-47"/>
<source>Room creation failed: %1</source>
<translation type="unfinished"></translation>
</message>
@ -116,19 +108,11 @@
</message>
</context>
<context>
<name>FileItem</name>
<name>EncryptionIndicator</name>
<message>
<location filename="../../src/timeline/widgets/FileItem.cpp" line="+107"/>
<source>Save File</source>
<translation>Enregistrer le fichier</translation>
</message>
</context>
<context>
<name>ImageItem</name>
<message>
<location filename="../../src/timeline/widgets/ImageItem.cpp" line="+241"/>
<source>Save image</source>
<translation>Enregistrer l&apos;image</translation>
<location filename="../qml/EncryptionIndicator.qml" line="+11"/>
<source>Encrypted</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
@ -200,7 +184,7 @@
<context>
<name>MemberList</name>
<message>
<location filename="../../src/dialogs/MemberList.cpp" line="+96"/>
<location filename="../../src/dialogs/MemberList.cpp" line="+89"/>
<source>Room members</source>
<translation>Membres du salon</translation>
</message>
@ -210,6 +194,14 @@
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>Placeholder</name>
<message>
<location filename="../qml/delegates/Placeholder.qml" line="+4"/>
<source>unimplemented event: </source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>QuickSwitcher</name>
<message>
@ -218,6 +210,14 @@
<translation>Chercher un salon</translation>
</message>
</context>
<context>
<name>Redacted</name>
<message>
<location filename="../qml/delegates/Redacted.qml" line="+5"/>
<source>redacted</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>RegisterPage</name>
<message>
@ -278,7 +278,7 @@
<context>
<name>RoomInfo</name>
<message>
<location filename="../../src/Cache.cpp" line="+2205"/>
<location filename="../../src/Cache.cpp" line="+2307"/>
<source>no version stored</source>
<translation type="unfinished"></translation>
</message>
@ -286,12 +286,12 @@
<context>
<name>RoomInfoListItem</name>
<message>
<location filename="../../src/RoomInfoListItem.cpp" line="+93"/>
<location filename="../../src/RoomInfoListItem.cpp" line="+95"/>
<source>Leave room</source>
<translation>Quitter le salon</translation>
</message>
<message>
<location line="+181"/>
<location line="+161"/>
<source>Accept</source>
<translation>Accepter</translation>
</message>
@ -332,25 +332,25 @@
<context>
<name>StatusIndicator</name>
<message>
<location filename="../../src/timeline/TimelineItem.cpp" line="+107"/>
<source>Encrypted</source>
<location filename="../qml/StatusIndicator.qml" line="+13"/>
<source>Failed</source>
<translation type="unfinished"></translation>
</message>
<message>
<location line="+3"/>
<source>Delivered</source>
<translation type="unfinished"></translation>
</message>
<message>
<location line="+3"/>
<source>Seen</source>
<translation type="unfinished"></translation>
</message>
<message>
<location line="+3"/>
<location line="+1"/>
<source>Sent</source>
<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>
</context>
<context>
<name>TextInputWidget</name>
@ -392,32 +392,9 @@
</message>
</context>
<context>
<name>TimelineItem</name>
<name>TimelineModel</name>
<message>
<location filename="../../src/timeline/TimelineItem.cpp" line="+85"/>
<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"/>
<location filename="../../src/timeline/TimelineModel.cpp" line="+780"/>
<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>
<translation type="unfinished"></translation>
@ -441,16 +418,90 @@
<translation type="unfinished"></translation>
</message>
<message>
<location line="+27"/>
<location line="+25"/>
<source>-- Encrypted Event (Unknown event type) --</source>
<comment>Placeholder, when the message was decrypted, but we couldn&apos;t parse it, because Nheko/mtxclient don&apos;t support that event type yet</comment>
<translation type="unfinished"></translation>
</message>
<message>
<location line="+50"/>
<source>Message redaction failed: %1</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>
<name>TimelineViewManager</name>
<message>
<location filename="../../src/timeline/TimelineViewManager.cpp" line="+161"/>
<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>TopRoomBar</name>
<message>
<location filename="../../src/TopRoomBar.cpp" line="+79"/>
<location filename="../../src/TopRoomBar.cpp" line="+78"/>
<source>Room options</source>
<translation type="unfinished"></translation>
</message>
@ -516,7 +567,7 @@
<context>
<name>UserSettingsPage</name>
<message>
<location filename="../../src/UserSettingsPage.cpp" line="+166"/>
<location filename="../../src/UserSettingsPage.cpp" line="+171"/>
<source>Minimize to tray</source>
<translation>Réduire à la barre des tâches</translation>
</message>
@ -530,6 +581,11 @@
<source>Group&apos;s sidebar</source>
<translation>Barre latérale des groupes</translation>
</message>
<message>
<location line="+9"/>
<source>Circular Avatars</source>
<translation type="unfinished"></translation>
</message>
<message>
<location line="+9"/>
<source>Typing notifications</source>
@ -606,7 +662,7 @@
<translation>GÉNÉRAL</translation>
</message>
<message>
<location line="+156"/>
<location line="+161"/>
<source>Open Sessions File</source>
<translation type="unfinished"></translation>
</message>
@ -826,7 +882,7 @@ Taille du média : %2
<context>
<name>dialogs::ReadReceipts</name>
<message>
<location filename="../../src/dialogs/ReadReceipts.cpp" line="+121"/>
<location filename="../../src/dialogs/ReadReceipts.cpp" line="+117"/>
<source>Read receipts</source>
<translation>Accusés de lecture</translation>
</message>
@ -952,7 +1008,7 @@ Taille du média : %2
<translation type="unfinished"></translation>
</message>
<message>
<location line="+149"/>
<location line="+148"/>
<source>Select an avatar</source>
<translation type="unfinished"></translation>
</message>
@ -978,19 +1034,6 @@ Taille du média : %2
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>dialogs::UserMentions</name>
<message>
<location filename="../../src/dialogs/UserMentions.cpp" line="+53"/>
<source>This Room</source>
<translation type="unfinished"></translation>
</message>
<message>
<location line="+1"/>
<source>All Rooms</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>dialogs::UserProfile</name>
<message>
@ -1014,7 +1057,7 @@ Taille du média : %2
<translation type="unfinished"></translation>
</message>
<message>
<location line="+57"/>
<location line="+56"/>
<source>Devices</source>
<translation type="unfinished"></translation>
</message>
@ -1065,69 +1108,103 @@ Taille du média : %2
<context>
<name>message-description sent:</name>
<message>
<location filename="../../src/Utils.h" line="+104"/>
<source>%1 an audio clip</source>
<location filename="../../src/Utils.h" line="+95"/>
<source>You sent an audio clip</source>
<translation type="unfinished"></translation>
</message>
<message>
<location line="+3"/>
<source>%1 an image</source>
<source>%1 sent an audio clip</source>
<translation type="unfinished"></translation>
</message>
<message>
<location line="+5"/>
<source>You sent an image</source>
<translation type="unfinished"></translation>
</message>
<message>
<location line="+3"/>
<source>%1 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>
</message>
<message>
<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>
</message>
<message>
<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>
</message>
<message>
<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>
</message>
<message>
<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>
</message>
</context>
<context>
<name>message-description:</name>
<name>popups::UserMentions</name>
<message>
<location line="-26"/>
<source>sent</source>
<comment>For when someone else is the sender</comment>
<location filename="../../src/popups/UserMentions.cpp" line="+61"/>
<source>This Room</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>message-description: </name>
<message>
<location line="-2"/>
<source>sent</source>
<comment>For when you are the sender</comment>
<location line="+1"/>
<source>All Rooms</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>utils</name>
<message>
<location filename="../../src/Utils.cpp" line="+46"/>
<location filename="../../src/Utils.h" line="+55"/>
<source>You</source>
<translation type="unfinished"></translation>
</message>
<message>
<location line="+219"/>
<location filename="../../src/Utils.cpp" line="+282"/>
<source>sent a file.</source>
<translation type="unfinished"></translation>
</message>
@ -1147,7 +1224,7 @@ Taille du média : %2
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../../src/Utils.h" line="-23"/>
<location filename="../../src/Utils.h" line="+4"/>
<source>Unknown Message Type</source>
<translation type="unfinished"></translation>
</message>

View File

@ -1,14 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE TS>
<TS version="2.1" language="nl_NL">
<context>
<name>AudioItem</name>
<message>
<location filename="../../src/timeline/widgets/AudioItem.cpp" line="+118"/>
<source>Save File</source>
<translation>Bestand opslaan</translation>
</message>
</context>
<context>
<name>ChatPage</name>
<message>
@ -32,7 +24,7 @@
<translation type="unfinished"></translation>
</message>
<message>
<location line="+380"/>
<location line="+393"/>
<source>Failed to restore OLM account. Please login again.</source>
<translation type="unfinished"></translation>
</message>
@ -42,18 +34,18 @@
<translation type="unfinished"></translation>
</message>
<message>
<location line="+198"/>
<location line="+181"/>
<source>Failed to setup encryption keys. Server response: %1 %2. Please try again later.</source>
<translation type="unfinished"></translation>
</message>
<message>
<location line="+51"/>
<location line="+153"/>
<location line="+155"/>
<source>Please try to login again: %1</source>
<translation type="unfinished"></translation>
</message>
<message>
<location line="-45"/>
<location line="-47"/>
<source>Room creation failed: %1</source>
<translation type="unfinished"></translation>
</message>
@ -116,19 +108,11 @@
</message>
</context>
<context>
<name>FileItem</name>
<name>EncryptionIndicator</name>
<message>
<location filename="../../src/timeline/widgets/FileItem.cpp" line="+107"/>
<source>Save File</source>
<translation>Bestand opslaan</translation>
</message>
</context>
<context>
<name>ImageItem</name>
<message>
<location filename="../../src/timeline/widgets/ImageItem.cpp" line="+241"/>
<source>Save image</source>
<translation>Afbeelding opslaan</translation>
<location filename="../qml/EncryptionIndicator.qml" line="+11"/>
<source>Encrypted</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
@ -200,7 +184,7 @@
<context>
<name>MemberList</name>
<message>
<location filename="../../src/dialogs/MemberList.cpp" line="+96"/>
<location filename="../../src/dialogs/MemberList.cpp" line="+89"/>
<source>Room members</source>
<translation>Kamerleden</translation>
</message>
@ -210,6 +194,14 @@
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>Placeholder</name>
<message>
<location filename="../qml/delegates/Placeholder.qml" line="+4"/>
<source>unimplemented event: </source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>QuickSwitcher</name>
<message>
@ -218,6 +210,14 @@
<translation>Zoek een kamer...</translation>
</message>
</context>
<context>
<name>Redacted</name>
<message>
<location filename="../qml/delegates/Redacted.qml" line="+5"/>
<source>redacted</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>RegisterPage</name>
<message>
@ -277,7 +277,7 @@
<context>
<name>RoomInfo</name>
<message>
<location filename="../../src/Cache.cpp" line="+2205"/>
<location filename="../../src/Cache.cpp" line="+2307"/>
<source>no version stored</source>
<translation type="unfinished"></translation>
</message>
@ -285,12 +285,12 @@
<context>
<name>RoomInfoListItem</name>
<message>
<location filename="../../src/RoomInfoListItem.cpp" line="+93"/>
<location filename="../../src/RoomInfoListItem.cpp" line="+95"/>
<source>Leave room</source>
<translation>Kamer verlaten</translation>
</message>
<message>
<location line="+181"/>
<location line="+161"/>
<source>Accept</source>
<translation>Accepteren</translation>
</message>
@ -331,25 +331,25 @@
<context>
<name>StatusIndicator</name>
<message>
<location filename="../../src/timeline/TimelineItem.cpp" line="+107"/>
<source>Encrypted</source>
<location filename="../qml/StatusIndicator.qml" line="+13"/>
<source>Failed</source>
<translation type="unfinished"></translation>
</message>
<message>
<location line="+3"/>
<source>Delivered</source>
<translation type="unfinished"></translation>
</message>
<message>
<location line="+3"/>
<source>Seen</source>
<translation type="unfinished"></translation>
</message>
<message>
<location line="+3"/>
<location line="+1"/>
<source>Sent</source>
<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>
</context>
<context>
<name>TextInputWidget</name>
@ -391,32 +391,9 @@
</message>
</context>
<context>
<name>TimelineItem</name>
<name>TimelineModel</name>
<message>
<location filename="../../src/timeline/TimelineItem.cpp" line="+85"/>
<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"/>
<location filename="../../src/timeline/TimelineModel.cpp" line="+780"/>
<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>
<translation type="unfinished"></translation>
@ -440,16 +417,90 @@
<translation type="unfinished"></translation>
</message>
<message>
<location line="+27"/>
<location line="+25"/>
<source>-- Encrypted Event (Unknown event type) --</source>
<comment>Placeholder, when the message was decrypted, but we couldn&apos;t parse it, because Nheko/mtxclient don&apos;t support that event type yet</comment>
<translation type="unfinished"></translation>
</message>
<message>
<location line="+50"/>
<source>Message redaction failed: %1</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>
<name>TimelineViewManager</name>
<message>
<location filename="../../src/timeline/TimelineViewManager.cpp" line="+161"/>
<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>TopRoomBar</name>
<message>
<location filename="../../src/TopRoomBar.cpp" line="+79"/>
<location filename="../../src/TopRoomBar.cpp" line="+78"/>
<source>Room options</source>
<translation type="unfinished"></translation>
</message>
@ -515,7 +566,7 @@
<context>
<name>UserSettingsPage</name>
<message>
<location filename="../../src/UserSettingsPage.cpp" line="+166"/>
<location filename="../../src/UserSettingsPage.cpp" line="+171"/>
<source>Minimize to tray</source>
<translation>Minimaliseren naar systeemvak</translation>
</message>
@ -529,6 +580,11 @@
<source>Group&apos;s sidebar</source>
<translation>Zijbalk van groep</translation>
</message>
<message>
<location line="+9"/>
<source>Circular Avatars</source>
<translation type="unfinished"></translation>
</message>
<message>
<location line="+9"/>
<source>Typing notifications</source>
@ -605,7 +661,7 @@
<translation>ALGEMEEN</translation>
</message>
<message>
<location line="+156"/>
<location line="+161"/>
<source>Open Sessions File</source>
<translation type="unfinished"></translation>
</message>
@ -825,7 +881,7 @@ Mediagrootte: %2
<context>
<name>dialogs::ReadReceipts</name>
<message>
<location filename="../../src/dialogs/ReadReceipts.cpp" line="+121"/>
<location filename="../../src/dialogs/ReadReceipts.cpp" line="+117"/>
<source>Read receipts</source>
<translation>Leesbevestigingen</translation>
</message>
@ -951,7 +1007,7 @@ Mediagrootte: %2
<translation type="unfinished"></translation>
</message>
<message>
<location line="+149"/>
<location line="+148"/>
<source>Select an avatar</source>
<translation type="unfinished"></translation>
</message>
@ -977,19 +1033,6 @@ Mediagrootte: %2
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>dialogs::UserMentions</name>
<message>
<location filename="../../src/dialogs/UserMentions.cpp" line="+53"/>
<source>This Room</source>
<translation type="unfinished"></translation>
</message>
<message>
<location line="+1"/>
<source>All Rooms</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>dialogs::UserProfile</name>
<message>
@ -1013,7 +1056,7 @@ Mediagrootte: %2
<translation type="unfinished"></translation>
</message>
<message>
<location line="+57"/>
<location line="+56"/>
<source>Devices</source>
<translation type="unfinished"></translation>
</message>
@ -1064,69 +1107,103 @@ Mediagrootte: %2
<context>
<name>message-description sent:</name>
<message>
<location filename="../../src/Utils.h" line="+104"/>
<source>%1 an audio clip</source>
<location filename="../../src/Utils.h" line="+95"/>
<source>You sent an audio clip</source>
<translation type="unfinished"></translation>
</message>
<message>
<location line="+3"/>
<source>%1 an image</source>
<source>%1 sent an audio clip</source>
<translation type="unfinished"></translation>
</message>
<message>
<location line="+5"/>
<source>You sent an image</source>
<translation type="unfinished"></translation>
</message>
<message>
<location line="+3"/>
<source>%1 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>
</message>
<message>
<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>
</message>
<message>
<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>
</message>
<message>
<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>
</message>
<message>
<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>
</message>
</context>
<context>
<name>message-description:</name>
<name>popups::UserMentions</name>
<message>
<location line="-26"/>
<source>sent</source>
<comment>For when someone else is the sender</comment>
<location filename="../../src/popups/UserMentions.cpp" line="+61"/>
<source>This Room</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>message-description: </name>
<message>
<location line="-2"/>
<source>sent</source>
<comment>For when you are the sender</comment>
<location line="+1"/>
<source>All Rooms</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>utils</name>
<message>
<location filename="../../src/Utils.cpp" line="+46"/>
<location filename="../../src/Utils.h" line="+55"/>
<source>You</source>
<translation type="unfinished"></translation>
</message>
<message>
<location line="+219"/>
<location filename="../../src/Utils.cpp" line="+282"/>
<source>sent a file.</source>
<translation type="unfinished"></translation>
</message>
@ -1146,7 +1223,7 @@ Mediagrootte: %2
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../../src/Utils.h" line="-23"/>
<location filename="../../src/Utils.h" line="+4"/>
<source>Unknown Message Type</source>
<translation type="unfinished"></translation>
</message>

View File

@ -1,14 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE TS>
<TS version="2.1" language="pl">
<context>
<name>AudioItem</name>
<message>
<location filename="../../src/timeline/widgets/AudioItem.cpp" line="+118"/>
<source>Save File</source>
<translation>Zapisz plik</translation>
</message>
</context>
<context>
<name>ChatPage</name>
<message>
@ -32,7 +24,7 @@
<translation>Nie udało się wysłać filmu. Spróbuj ponownie.</translation>
</message>
<message>
<location line="+380"/>
<location line="+393"/>
<source>Failed to restore OLM account. Please login again.</source>
<translation>Nie udało się przywrócić konta OLM. Spróbuj zalogować się ponownie.</translation>
</message>
@ -42,18 +34,18 @@
<translation>Nie udało się przywrócić zapisanych danych. Spróbuj zalogować się ponownie.</translation>
</message>
<message>
<location line="+198"/>
<location line="+181"/>
<source>Failed to setup encryption keys. Server response: %1 %2. Please try again later.</source>
<translation type="unfinished"></translation>
</message>
<message>
<location line="+51"/>
<location line="+153"/>
<location line="+155"/>
<source>Please try to login again: %1</source>
<translation>Spróbuj zalogować się ponownie: %1</translation>
</message>
<message>
<location line="-45"/>
<location line="-47"/>
<source>Room creation failed: %1</source>
<translation>Tworzenie pokoju nie powiodło się: %1</translation>
</message>
@ -116,19 +108,11 @@
</message>
</context>
<context>
<name>FileItem</name>
<name>EncryptionIndicator</name>
<message>
<location filename="../../src/timeline/widgets/FileItem.cpp" line="+107"/>
<source>Save File</source>
<translation>Zapisz plik</translation>
</message>
</context>
<context>
<name>ImageItem</name>
<message>
<location filename="../../src/timeline/widgets/ImageItem.cpp" line="+241"/>
<source>Save image</source>
<translation>Zapisz obraz</translation>
<location filename="../qml/EncryptionIndicator.qml" line="+11"/>
<source>Encrypted</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
@ -200,7 +184,7 @@
<context>
<name>MemberList</name>
<message>
<location filename="../../src/dialogs/MemberList.cpp" line="+96"/>
<location filename="../../src/dialogs/MemberList.cpp" line="+89"/>
<source>Room members</source>
<translation>Członkowie pokoju</translation>
</message>
@ -210,6 +194,14 @@
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>Placeholder</name>
<message>
<location filename="../qml/delegates/Placeholder.qml" line="+4"/>
<source>unimplemented event: </source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>QuickSwitcher</name>
<message>
@ -218,6 +210,14 @@
<translation>Wyszukaj pokoju</translation>
</message>
</context>
<context>
<name>Redacted</name>
<message>
<location filename="../qml/delegates/Redacted.qml" line="+5"/>
<source>redacted</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>RegisterPage</name>
<message>
@ -277,7 +277,7 @@
<context>
<name>RoomInfo</name>
<message>
<location filename="../../src/Cache.cpp" line="+2205"/>
<location filename="../../src/Cache.cpp" line="+2307"/>
<source>no version stored</source>
<translation type="unfinished"></translation>
</message>
@ -285,12 +285,12 @@
<context>
<name>RoomInfoListItem</name>
<message>
<location filename="../../src/RoomInfoListItem.cpp" line="+93"/>
<location filename="../../src/RoomInfoListItem.cpp" line="+95"/>
<source>Leave room</source>
<translation>Opuść pokój</translation>
</message>
<message>
<location line="+181"/>
<location line="+161"/>
<source>Accept</source>
<translation>Akceptuj</translation>
</message>
@ -331,24 +331,24 @@
<context>
<name>StatusIndicator</name>
<message>
<location filename="../../src/timeline/TimelineItem.cpp" line="+107"/>
<source>Encrypted</source>
<translation>Szyfrowana</translation>
<location filename="../qml/StatusIndicator.qml" line="+13"/>
<source>Failed</source>
<translation type="unfinished"></translation>
</message>
<message>
<location line="+3"/>
<source>Delivered</source>
<translation>Dostarczono</translation>
</message>
<message>
<location line="+3"/>
<source>Seen</source>
<translation>Wyświetlona</translation>
</message>
<message>
<location line="+3"/>
<location line="+1"/>
<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>
</context>
<context>
@ -391,32 +391,9 @@
</message>
</context>
<context>
<name>TimelineItem</name>
<name>TimelineModel</name>
<message>
<location filename="../../src/timeline/TimelineItem.cpp" line="+85"/>
<source>Message redaction failed: %1</source>
<translation>Redagowanie wiadomości nie powiodło się: %1</translation>
</message>
<message>
<location line="+39"/>
<source>Reply</source>
<translation type="unfinished"></translation>
</message>
<message>
<location line="+11"/>
<source>Options</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>TimelineView</name>
<message>
<location filename="../../src/timeline/TimelineView.cpp" line="+245"/>
<source>Encryption is enabled</source>
<translation>Szyfrowanie jest włączone</translation>
</message>
<message>
<location line="+65"/>
<location filename="../../src/timeline/TimelineModel.cpp" line="+780"/>
<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>
<translation type="unfinished"></translation>
@ -440,16 +417,90 @@
<translation type="unfinished"></translation>
</message>
<message>
<location line="+27"/>
<location line="+25"/>
<source>-- Encrypted Event (Unknown event type) --</source>
<comment>Placeholder, when the message was decrypted, but we couldn&apos;t parse it, because Nheko/mtxclient don&apos;t support that event type yet</comment>
<translation type="unfinished"></translation>
</message>
<message>
<location line="+50"/>
<source>Message redaction failed: %1</source>
<translation type="unfinished">Redagowanie wiadomości nie powiodło się: %1</translation>
</message>
</context>
<context>
<name>TimelineRow</name>
<message>
<location filename="../qml/TimelineRow.qml" line="+57"/>
<source>Reply</source>
<translation type="unfinished"></translation>
</message>
<message>
<location line="+14"/>
<source>Options</source>
<translation type="unfinished"></translation>
</message>
<message>
<location line="+12"/>
<source>Read receipts</source>
<translation type="unfinished">Potwierdzenia przeczytania</translation>
</message>
<message>
<location line="+4"/>
<source>Mark as read</source>
<translation type="unfinished"></translation>
</message>
<message>
<location line="+3"/>
<source>View raw message</source>
<translation type="unfinished"></translation>
</message>
<message>
<location line="+4"/>
<source>Redact message</source>
<translation type="unfinished"></translation>
</message>
<message>
<location line="+5"/>
<source>Save as</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>TimelineView</name>
<message>
<location filename="../qml/TimelineView.qml" line="+24"/>
<source>No room open</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>TimelineViewManager</name>
<message>
<location filename="../../src/timeline/TimelineViewManager.cpp" line="+161"/>
<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>TopRoomBar</name>
<message>
<location filename="../../src/TopRoomBar.cpp" line="+79"/>
<location filename="../../src/TopRoomBar.cpp" line="+78"/>
<source>Room options</source>
<translation>Ustawienia pokoju</translation>
</message>
@ -516,7 +567,7 @@
<context>
<name>UserSettingsPage</name>
<message>
<location filename="../../src/UserSettingsPage.cpp" line="+166"/>
<location filename="../../src/UserSettingsPage.cpp" line="+171"/>
<source>Minimize to tray</source>
<translation>Zminimalizuj do paska zadań</translation>
</message>
@ -530,6 +581,11 @@
<source>Group&apos;s sidebar</source>
<translation>Pasek boczny grupy</translation>
</message>
<message>
<location line="+9"/>
<source>Circular Avatars</source>
<translation type="unfinished"></translation>
</message>
<message>
<location line="+9"/>
<source>Typing notifications</source>
@ -606,7 +662,7 @@
<translation>OGÓLNE</translation>
</message>
<message>
<location line="+156"/>
<location line="+161"/>
<source>Open Sessions File</source>
<translation type="unfinished"></translation>
</message>
@ -826,7 +882,7 @@ Rozmiar multimediów: %2
<context>
<name>dialogs::ReadReceipts</name>
<message>
<location filename="../../src/dialogs/ReadReceipts.cpp" line="+121"/>
<location filename="../../src/dialogs/ReadReceipts.cpp" line="+117"/>
<source>Read receipts</source>
<translation>Potwierdzenia przeczytania</translation>
</message>
@ -955,7 +1011,7 @@ Rozmiar multimediów: %2
<translation>Nie udało się włączyć szyfrowania: %1</translation>
</message>
<message>
<location line="+149"/>
<location line="+148"/>
<source>Select an avatar</source>
<translation>Wybierz awatar</translation>
</message>
@ -981,19 +1037,6 @@ Rozmiar multimediów: %2
<translation>Nie udało się wysłać obrazu: %s</translation>
</message>
</context>
<context>
<name>dialogs::UserMentions</name>
<message>
<location filename="../../src/dialogs/UserMentions.cpp" line="+53"/>
<source>This Room</source>
<translation type="unfinished"></translation>
</message>
<message>
<location line="+1"/>
<source>All Rooms</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>dialogs::UserProfile</name>
<message>
@ -1017,7 +1060,7 @@ Rozmiar multimediów: %2
<translation>Rozpocznij rozmowę</translation>
</message>
<message>
<location line="+57"/>
<location line="+56"/>
<source>Devices</source>
<translation>Urządzenia</translation>
</message>
@ -1068,69 +1111,103 @@ Rozmiar multimediów: %2
<context>
<name>message-description sent:</name>
<message>
<location filename="../../src/Utils.h" line="+104"/>
<source>%1 an audio clip</source>
<location filename="../../src/Utils.h" line="+95"/>
<source>You sent an audio clip</source>
<translation type="unfinished"></translation>
</message>
<message>
<location line="+3"/>
<source>%1 an image</source>
<source>%1 sent an audio clip</source>
<translation type="unfinished"></translation>
</message>
<message>
<location line="+5"/>
<source>You sent an image</source>
<translation type="unfinished"></translation>
</message>
<message>
<location line="+3"/>
<source>%1 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>
</message>
<message>
<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>
</message>
<message>
<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>
</message>
<message>
<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>
</message>
<message>
<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>
</message>
</context>
<context>
<name>message-description:</name>
<name>popups::UserMentions</name>
<message>
<location line="-26"/>
<source>sent</source>
<comment>For when someone else is the sender</comment>
<location filename="../../src/popups/UserMentions.cpp" line="+61"/>
<source>This Room</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>message-description: </name>
<message>
<location line="-2"/>
<source>sent</source>
<comment>For when you are the sender</comment>
<location line="+1"/>
<source>All Rooms</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>utils</name>
<message>
<location filename="../../src/Utils.cpp" line="+46"/>
<location filename="../../src/Utils.h" line="+55"/>
<source>You</source>
<translation type="unfinished"></translation>
</message>
<message>
<location line="+219"/>
<location filename="../../src/Utils.cpp" line="+282"/>
<source>sent a file.</source>
<translation type="unfinished"></translation>
</message>
@ -1150,7 +1227,7 @@ Rozmiar multimediów: %2
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../../src/Utils.h" line="-23"/>
<location filename="../../src/Utils.h" line="+4"/>
<source>Unknown Message Type</source>
<translation type="unfinished"></translation>
</message>

View File

@ -1,14 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE TS>
<TS version="2.1" language="ru">
<context>
<name>AudioItem</name>
<message>
<location filename="../../src/timeline/widgets/AudioItem.cpp" line="+118"/>
<source>Save File</source>
<translation>Сохранить файл</translation>
</message>
</context>
<context>
<name>ChatPage</name>
<message>
@ -32,7 +24,7 @@
<translation>Не удалось загрузить видео. Пожалуйста, попробуйте еще раз.</translation>
</message>
<message>
<location line="+380"/>
<location line="+393"/>
<source>Failed to restore OLM account. Please login again.</source>
<translation>Не удалось восстановить учетную запись OLM. Пожалуйста, войдите снова.</translation>
</message>
@ -42,18 +34,18 @@
<translation>Не удалось восстановить сохраненные данные. Пожалуйста, войдите снова.</translation>
</message>
<message>
<location line="+198"/>
<location line="+181"/>
<source>Failed to setup encryption keys. Server response: %1 %2. Please try again later.</source>
<translation>Не удалось настроить ключи шифрования. Ответ сервера:%1 %2. Пожалуйста, попробуйте позже.</translation>
</message>
<message>
<location line="+51"/>
<location line="+153"/>
<location line="+155"/>
<source>Please try to login again: %1</source>
<translation>Повторите попытку входа: %1</translation>
</message>
<message>
<location line="-45"/>
<location line="-47"/>
<source>Room creation failed: %1</source>
<translation>Не удалось создать комнату: %1</translation>
</message>
@ -116,19 +108,11 @@
</message>
</context>
<context>
<name>FileItem</name>
<name>EncryptionIndicator</name>
<message>
<location filename="../../src/timeline/widgets/FileItem.cpp" line="+107"/>
<source>Save File</source>
<translation>Сохранить файл</translation>
</message>
</context>
<context>
<name>ImageItem</name>
<message>
<location filename="../../src/timeline/widgets/ImageItem.cpp" line="+241"/>
<source>Save image</source>
<translation>Сохранить изображение</translation>
<location filename="../qml/EncryptionIndicator.qml" line="+11"/>
<source>Encrypted</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
@ -200,7 +184,7 @@
<context>
<name>MemberList</name>
<message>
<location filename="../../src/dialogs/MemberList.cpp" line="+96"/>
<location filename="../../src/dialogs/MemberList.cpp" line="+89"/>
<source>Room members</source>
<translation>Участники комнаты</translation>
</message>
@ -210,6 +194,14 @@
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>Placeholder</name>
<message>
<location filename="../qml/delegates/Placeholder.qml" line="+4"/>
<source>unimplemented event: </source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>QuickSwitcher</name>
<message>
@ -218,6 +210,14 @@
<translation>Поиск комнаты...</translation>
</message>
</context>
<context>
<name>Redacted</name>
<message>
<location filename="../qml/delegates/Redacted.qml" line="+5"/>
<source>redacted</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>RegisterPage</name>
<message>
@ -277,7 +277,7 @@
<context>
<name>RoomInfo</name>
<message>
<location filename="../../src/Cache.cpp" line="+2205"/>
<location filename="../../src/Cache.cpp" line="+2307"/>
<source>no version stored</source>
<translation type="unfinished"></translation>
</message>
@ -285,12 +285,12 @@
<context>
<name>RoomInfoListItem</name>
<message>
<location filename="../../src/RoomInfoListItem.cpp" line="+93"/>
<location filename="../../src/RoomInfoListItem.cpp" line="+95"/>
<source>Leave room</source>
<translation>Покинуть комнату</translation>
</message>
<message>
<location line="+181"/>
<location line="+161"/>
<source>Accept</source>
<translation>Принять</translation>
</message>
@ -331,24 +331,24 @@
<context>
<name>StatusIndicator</name>
<message>
<location filename="../../src/timeline/TimelineItem.cpp" line="+107"/>
<source>Encrypted</source>
<translation>Зашифровано</translation>
<location filename="../qml/StatusIndicator.qml" line="+13"/>
<source>Failed</source>
<translation type="unfinished"></translation>
</message>
<message>
<location line="+3"/>
<source>Delivered</source>
<translation>Доставлено</translation>
</message>
<message>
<location line="+3"/>
<source>Seen</source>
<translation>Прочитано</translation>
</message>
<message>
<location line="+3"/>
<location line="+1"/>
<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>
</context>
<context>
@ -391,32 +391,9 @@
</message>
</context>
<context>
<name>TimelineItem</name>
<name>TimelineModel</name>
<message>
<location filename="../../src/timeline/TimelineItem.cpp" line="+85"/>
<source>Message redaction failed: %1</source>
<translation>Ошибка редактирования сообщения: %1</translation>
</message>
<message>
<location line="+39"/>
<source>Reply</source>
<translation type="unfinished"></translation>
</message>
<message>
<location line="+11"/>
<source>Options</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>TimelineView</name>
<message>
<location filename="../../src/timeline/TimelineView.cpp" line="+245"/>
<source>Encryption is enabled</source>
<translation>Шифрование включено</translation>
</message>
<message>
<location line="+65"/>
<location filename="../../src/timeline/TimelineModel.cpp" line="+780"/>
<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>
<translation type="unfinished"></translation>
@ -440,16 +417,90 @@
<translation type="unfinished"></translation>
</message>
<message>
<location line="+27"/>
<location line="+25"/>
<source>-- Encrypted Event (Unknown event type) --</source>
<comment>Placeholder, when the message was decrypted, but we couldn&apos;t parse it, because Nheko/mtxclient don&apos;t support that event type yet</comment>
<translation type="unfinished"></translation>
</message>
<message>
<location line="+50"/>
<source>Message redaction failed: %1</source>
<translation type="unfinished">Ошибка редактирования сообщения: %1</translation>
</message>
</context>
<context>
<name>TimelineRow</name>
<message>
<location filename="../qml/TimelineRow.qml" line="+57"/>
<source>Reply</source>
<translation type="unfinished"></translation>
</message>
<message>
<location line="+14"/>
<source>Options</source>
<translation type="unfinished"></translation>
</message>
<message>
<location line="+12"/>
<source>Read receipts</source>
<translation type="unfinished">Подтверждать прочтение</translation>
</message>
<message>
<location line="+4"/>
<source>Mark as read</source>
<translation type="unfinished"></translation>
</message>
<message>
<location line="+3"/>
<source>View raw message</source>
<translation type="unfinished"></translation>
</message>
<message>
<location line="+4"/>
<source>Redact message</source>
<translation type="unfinished"></translation>
</message>
<message>
<location line="+5"/>
<source>Save as</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>TimelineView</name>
<message>
<location filename="../qml/TimelineView.qml" line="+24"/>
<source>No room open</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>TimelineViewManager</name>
<message>
<location filename="../../src/timeline/TimelineViewManager.cpp" line="+161"/>
<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>TopRoomBar</name>
<message>
<location filename="../../src/TopRoomBar.cpp" line="+79"/>
<location filename="../../src/TopRoomBar.cpp" line="+78"/>
<source>Room options</source>
<translation>Настройки комнаты</translation>
</message>
@ -516,7 +567,7 @@
<context>
<name>UserSettingsPage</name>
<message>
<location filename="../../src/UserSettingsPage.cpp" line="+166"/>
<location filename="../../src/UserSettingsPage.cpp" line="+171"/>
<source>Minimize to tray</source>
<translation>Сворачивать в системную панель</translation>
</message>
@ -530,6 +581,11 @@
<source>Group&apos;s sidebar</source>
<translation>Боковая панель групп</translation>
</message>
<message>
<location line="+9"/>
<source>Circular Avatars</source>
<translation type="unfinished"></translation>
</message>
<message>
<location line="+9"/>
<source>Typing notifications</source>
@ -606,7 +662,7 @@
<translation>ГЛАВНОЕ</translation>
</message>
<message>
<location line="+156"/>
<location line="+161"/>
<source>Open Sessions File</source>
<translation>Открыть файл сеансов</translation>
</message>
@ -827,7 +883,7 @@ Media size: %2
<context>
<name>dialogs::ReadReceipts</name>
<message>
<location filename="../../src/dialogs/ReadReceipts.cpp" line="+121"/>
<location filename="../../src/dialogs/ReadReceipts.cpp" line="+117"/>
<source>Read receipts</source>
<translation>Подтверждать прочтение</translation>
</message>
@ -954,7 +1010,7 @@ Media size: %2
<translation>Не удалось включить шифрование: %1</translation>
</message>
<message>
<location line="+149"/>
<location line="+148"/>
<source>Select an avatar</source>
<translation>Выберите аватар</translation>
</message>
@ -980,19 +1036,6 @@ Media size: %2
<translation>Не удалось загрузить изображение: %s</translation>
</message>
</context>
<context>
<name>dialogs::UserMentions</name>
<message>
<location filename="../../src/dialogs/UserMentions.cpp" line="+53"/>
<source>This Room</source>
<translation type="unfinished"></translation>
</message>
<message>
<location line="+1"/>
<source>All Rooms</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>dialogs::UserProfile</name>
<message>
@ -1016,7 +1059,7 @@ Media size: %2
<translation>Начать разговор</translation>
</message>
<message>
<location line="+57"/>
<location line="+56"/>
<source>Devices</source>
<translation>Устройства</translation>
</message>
@ -1067,69 +1110,103 @@ Media size: %2
<context>
<name>message-description sent:</name>
<message>
<location filename="../../src/Utils.h" line="+104"/>
<source>%1 an audio clip</source>
<location filename="../../src/Utils.h" line="+95"/>
<source>You sent an audio clip</source>
<translation type="unfinished"></translation>
</message>
<message>
<location line="+3"/>
<source>%1 an image</source>
<source>%1 sent an audio clip</source>
<translation type="unfinished"></translation>
</message>
<message>
<location line="+5"/>
<source>You sent an image</source>
<translation type="unfinished"></translation>
</message>
<message>
<location line="+3"/>
<source>%1 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>
</message>
<message>
<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>
</message>
<message>
<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>
</message>
<message>
<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>
</message>
<message>
<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>
</message>
</context>
<context>
<name>message-description:</name>
<name>popups::UserMentions</name>
<message>
<location line="-26"/>
<source>sent</source>
<comment>For when someone else is the sender</comment>
<location filename="../../src/popups/UserMentions.cpp" line="+61"/>
<source>This Room</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>message-description: </name>
<message>
<location line="-2"/>
<source>sent</source>
<comment>For when you are the sender</comment>
<location line="+1"/>
<source>All Rooms</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>utils</name>
<message>
<location filename="../../src/Utils.cpp" line="+46"/>
<location filename="../../src/Utils.h" line="+55"/>
<source>You</source>
<translation type="unfinished"></translation>
</message>
<message>
<location line="+219"/>
<location filename="../../src/Utils.cpp" line="+282"/>
<source>sent a file.</source>
<translation type="unfinished"></translation>
</message>
@ -1149,7 +1226,7 @@ Media size: %2
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../../src/Utils.h" line="-23"/>
<location filename="../../src/Utils.h" line="+4"/>
<source>Unknown Message Type</source>
<translation type="unfinished"></translation>
</message>

View File

@ -1,14 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE TS>
<TS version="2.1" language="zh_CN">
<context>
<name>AudioItem</name>
<message>
<location filename="../../src/timeline/widgets/AudioItem.cpp" line="+118"/>
<source>Save File</source>
<translation></translation>
</message>
</context>
<context>
<name>ChatPage</name>
<message>
@ -32,7 +24,7 @@
<translation></translation>
</message>
<message>
<location line="+380"/>
<location line="+393"/>
<source>Failed to restore OLM account. Please login again.</source>
<translation> OLM </translation>
</message>
@ -42,18 +34,18 @@
<translation></translation>
</message>
<message>
<location line="+198"/>
<location line="+181"/>
<source>Failed to setup encryption keys. Server response: %1 %2. Please try again later.</source>
<translation type="unfinished"></translation>
</message>
<message>
<location line="+51"/>
<location line="+153"/>
<location line="+155"/>
<source>Please try to login again: %1</source>
<translation>%1</translation>
</message>
<message>
<location line="-45"/>
<location line="-47"/>
<source>Room creation failed: %1</source>
<translation>%1</translation>
</message>
@ -116,19 +108,11 @@
</message>
</context>
<context>
<name>FileItem</name>
<name>EncryptionIndicator</name>
<message>
<location filename="../../src/timeline/widgets/FileItem.cpp" line="+107"/>
<source>Save File</source>
<translation></translation>
</message>
</context>
<context>
<name>ImageItem</name>
<message>
<location filename="../../src/timeline/widgets/ImageItem.cpp" line="+241"/>
<source>Save image</source>
<translation></translation>
<location filename="../qml/EncryptionIndicator.qml" line="+11"/>
<source>Encrypted</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
@ -200,7 +184,7 @@
<context>
<name>MemberList</name>
<message>
<location filename="../../src/dialogs/MemberList.cpp" line="+96"/>
<location filename="../../src/dialogs/MemberList.cpp" line="+89"/>
<source>Room members</source>
<translation></translation>
</message>
@ -210,6 +194,14 @@
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>Placeholder</name>
<message>
<location filename="../qml/delegates/Placeholder.qml" line="+4"/>
<source>unimplemented event: </source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>QuickSwitcher</name>
<message>
@ -218,6 +210,14 @@
<translation>...</translation>
</message>
</context>
<context>
<name>Redacted</name>
<message>
<location filename="../qml/delegates/Redacted.qml" line="+5"/>
<source>redacted</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>RegisterPage</name>
<message>
@ -277,7 +277,7 @@
<context>
<name>RoomInfo</name>
<message>
<location filename="../../src/Cache.cpp" line="+2205"/>
<location filename="../../src/Cache.cpp" line="+2307"/>
<source>no version stored</source>
<translation type="unfinished"></translation>
</message>
@ -285,12 +285,12 @@
<context>
<name>RoomInfoListItem</name>
<message>
<location filename="../../src/RoomInfoListItem.cpp" line="+93"/>
<location filename="../../src/RoomInfoListItem.cpp" line="+95"/>
<source>Leave room</source>
<translation></translation>
</message>
<message>
<location line="+181"/>
<location line="+161"/>
<source>Accept</source>
<translation></translation>
</message>
@ -331,24 +331,24 @@
<context>
<name>StatusIndicator</name>
<message>
<location filename="../../src/timeline/TimelineItem.cpp" line="+107"/>
<source>Encrypted</source>
<translation></translation>
<location filename="../qml/StatusIndicator.qml" line="+13"/>
<source>Failed</source>
<translation type="unfinished"></translation>
</message>
<message>
<location line="+3"/>
<source>Delivered</source>
<translation></translation>
</message>
<message>
<location line="+3"/>
<source>Seen</source>
<translation></translation>
</message>
<message>
<location line="+3"/>
<location line="+1"/>
<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>
</context>
<context>
@ -391,32 +391,9 @@
</message>
</context>
<context>
<name>TimelineItem</name>
<name>TimelineModel</name>
<message>
<location filename="../../src/timeline/TimelineItem.cpp" line="+85"/>
<source>Message redaction failed: %1</source>
<translation>%1</translation>
</message>
<message>
<location line="+39"/>
<source>Reply</source>
<translation type="unfinished"></translation>
</message>
<message>
<location line="+11"/>
<source>Options</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>TimelineView</name>
<message>
<location filename="../../src/timeline/TimelineView.cpp" line="+245"/>
<source>Encryption is enabled</source>
<translation></translation>
</message>
<message>
<location line="+65"/>
<location filename="../../src/timeline/TimelineModel.cpp" line="+780"/>
<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>
<translation type="unfinished"></translation>
@ -440,16 +417,90 @@
<translation type="unfinished"></translation>
</message>
<message>
<location line="+27"/>
<location line="+25"/>
<source>-- Encrypted Event (Unknown event type) --</source>
<comment>Placeholder, when the message was decrypted, but we couldn&apos;t parse it, because Nheko/mtxclient don&apos;t support that event type yet</comment>
<translation type="unfinished"></translation>
</message>
<message>
<location line="+50"/>
<source>Message redaction failed: %1</source>
<translation type="unfinished">%1</translation>
</message>
</context>
<context>
<name>TimelineRow</name>
<message>
<location filename="../qml/TimelineRow.qml" line="+57"/>
<source>Reply</source>
<translation type="unfinished"></translation>
</message>
<message>
<location line="+14"/>
<source>Options</source>
<translation type="unfinished"></translation>
</message>
<message>
<location line="+12"/>
<source>Read receipts</source>
<translation type="unfinished"></translation>
</message>
<message>
<location line="+4"/>
<source>Mark as read</source>
<translation type="unfinished"></translation>
</message>
<message>
<location line="+3"/>
<source>View raw message</source>
<translation type="unfinished"></translation>
</message>
<message>
<location line="+4"/>
<source>Redact message</source>
<translation type="unfinished"></translation>
</message>
<message>
<location line="+5"/>
<source>Save as</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>TimelineView</name>
<message>
<location filename="../qml/TimelineView.qml" line="+24"/>
<source>No room open</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>TimelineViewManager</name>
<message>
<location filename="../../src/timeline/TimelineViewManager.cpp" line="+161"/>
<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>TopRoomBar</name>
<message>
<location filename="../../src/TopRoomBar.cpp" line="+79"/>
<location filename="../../src/TopRoomBar.cpp" line="+78"/>
<source>Room options</source>
<translation></translation>
</message>
@ -514,7 +565,7 @@
<context>
<name>UserSettingsPage</name>
<message>
<location filename="../../src/UserSettingsPage.cpp" line="+166"/>
<location filename="../../src/UserSettingsPage.cpp" line="+171"/>
<source>Minimize to tray</source>
<translation></translation>
</message>
@ -528,6 +579,11 @@
<source>Group&apos;s sidebar</source>
<translation></translation>
</message>
<message>
<location line="+9"/>
<source>Circular Avatars</source>
<translation type="unfinished"></translation>
</message>
<message>
<location line="+9"/>
<source>Typing notifications</source>
@ -604,7 +660,7 @@
<translation></translation>
</message>
<message>
<location line="+156"/>
<location line="+161"/>
<source>Open Sessions File</source>
<translation></translation>
</message>
@ -824,7 +880,7 @@ Media size: %2
<context>
<name>dialogs::ReadReceipts</name>
<message>
<location filename="../../src/dialogs/ReadReceipts.cpp" line="+121"/>
<location filename="../../src/dialogs/ReadReceipts.cpp" line="+117"/>
<source>Read receipts</source>
<translation></translation>
</message>
@ -951,7 +1007,7 @@ Media size: %2
<translation>%1</translation>
</message>
<message>
<location line="+149"/>
<location line="+148"/>
<source>Select an avatar</source>
<translation></translation>
</message>
@ -977,19 +1033,6 @@ Media size: %2
<translation>%s</translation>
</message>
</context>
<context>
<name>dialogs::UserMentions</name>
<message>
<location filename="../../src/dialogs/UserMentions.cpp" line="+53"/>
<source>This Room</source>
<translation type="unfinished"></translation>
</message>
<message>
<location line="+1"/>
<source>All Rooms</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>dialogs::UserProfile</name>
<message>
@ -1013,7 +1056,7 @@ Media size: %2
<translation></translation>
</message>
<message>
<location line="+57"/>
<location line="+56"/>
<source>Devices</source>
<translation></translation>
</message>
@ -1072,69 +1115,103 @@ Media size: %2
<context>
<name>message-description sent:</name>
<message>
<location filename="../../src/Utils.h" line="+104"/>
<source>%1 an audio clip</source>
<location filename="../../src/Utils.h" line="+95"/>
<source>You sent an audio clip</source>
<translation type="unfinished"></translation>
</message>
<message>
<location line="+3"/>
<source>%1 an image</source>
<source>%1 sent an audio clip</source>
<translation type="unfinished"></translation>
</message>
<message>
<location line="+5"/>
<source>You sent an image</source>
<translation type="unfinished"></translation>
</message>
<message>
<location line="+3"/>
<source>%1 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>
</message>
<message>
<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>
</message>
<message>
<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>
</message>
<message>
<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>
</message>
<message>
<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>
</message>
</context>
<context>
<name>message-description:</name>
<name>popups::UserMentions</name>
<message>
<location line="-26"/>
<source>sent</source>
<comment>For when someone else is the sender</comment>
<location filename="../../src/popups/UserMentions.cpp" line="+61"/>
<source>This Room</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>message-description: </name>
<message>
<location line="-2"/>
<source>sent</source>
<comment>For when you are the sender</comment>
<location line="+1"/>
<source>All Rooms</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>utils</name>
<message>
<location filename="../../src/Utils.cpp" line="+46"/>
<location filename="../../src/Utils.h" line="+55"/>
<source>You</source>
<translation type="unfinished"></translation>
</message>
<message>
<location line="+219"/>
<location filename="../../src/Utils.cpp" line="+282"/>
<source>sent a file.</source>
<translation type="unfinished"></translation>
</message>
@ -1154,7 +1231,7 @@ Media size: %2
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../../src/Utils.h" line="-23"/>
<location filename="../../src/Utils.h" line="+4"/>
<source>Unknown Message Type</source>
<translation type="unfinished"></translation>
</message>

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 com.github.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 com.github.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 com.github.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.saveMedia(model.url, model.filename, model.mimetype, model.type)
}
}
}
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,165 @@
import QtQuick 2.6
import QtQuick.Controls 2.1
import QtQuick.Layouts 1.2
import QtGraphicalEffects 1.0
import QtQuick.Window 2.2
import com.github.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
onModelChanged: {
if (model) {
currentIndex = model.currentIndex
if (model.currentIndex == count - 1) {
positionViewAtEnd()
} else {
positionViewAtIndex(model.currentIndex, ListView.End)
}
}
}
ScrollBar.vertical: ScrollBar {
id: scrollbar
anchors.top: parent.top
anchors.left: parent.right
anchors.bottom: parent.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.saveMedia(model.url, model.filename, model.mimetype, model.type)
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 com.github.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.filename, model.mimetype, model.type)
}
}
}

View File

@ -0,0 +1,55 @@
import QtQuick 2.6
import com.github.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 com.github.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.cacheMedia(model.url, model.mimetype); 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
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-dark.qss</file>
</qresource>
<qresource prefix="/">
<file>qml/TimelineView.qml</file>
<file>qml/Avatar.qml</file>
<file>qml/ImageButton.qml</file>
<file>qml/MatrixText.qml</file>
<file>qml/StatusIndicator.qml</file>
<file>qml/EncryptionIndicator.qml</file>
<file>qml/TimelineRow.qml</file>
<file>qml/delegates/MessageDelegate.qml</file>
<file>qml/delegates/TextMessage.qml</file>
<file>qml/delegates/NoticeMessage.qml</file>
<file>qml/delegates/ImageMessage.qml</file>
<file>qml/delegates/PlayableMediaMessage.qml</file>
<file>qml/delegates/FileMessage.qml</file>
<file>qml/delegates/Pill.qml</file>
<file>qml/delegates/Placeholder.qml</file>
</qresource>
</RCC>

View File

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

View File

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

View File

@ -113,12 +113,7 @@ ChatPage::ChatPage(QSharedPointer<UserSettings> userSettings, QWidget *parent)
view_manager_ = new TimelineViewManager(this);
contentLayout_->addWidget(top_bar_);
contentLayout_->addWidget(view_manager_);
connect(this,
&ChatPage::removeTimelineEvent,
view_manager_,
&TimelineViewManager::removeTimelineEvent);
contentLayout_->addWidget(view_manager_->getWidget());
// Splitter
splitter->addWidget(sideBar_);
@ -566,7 +561,7 @@ ChatPage::ChatPage(QSharedPointer<UserSettings> userSettings, QWidget *parent)
connect(this,
&ChatPage::initializeViews,
view_manager_,
[this](const mtx::responses::Rooms &rooms) { view_manager_->initialize(rooms); });
[this](const mtx::responses::Rooms &rooms) { view_manager_->sync(rooms); });
connect(this,
&ChatPage::initializeEmptyViews,
view_manager_,
@ -582,7 +577,7 @@ ChatPage::ChatPage(QSharedPointer<UserSettings> userSettings, QWidget *parent)
nhlog::db()->error("failed to retrieve invites: {}", e.what());
}
view_manager_->initialize(rooms);
view_manager_->sync(rooms);
removeLeftRooms(rooms.leave);
bool hasNotifications = false;

View File

@ -125,8 +125,6 @@ signals:
void showUserSettingsPage();
void showOverlayProgressBar();
void removeTimelineEvent(const QString &room_id, const QString &event_id);
void ownProfileOk();
void setUserDisplayName(const QString &name);
void setUserAvatar(const QString &avatar);

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 <iostream>
#include <QString>
#include <QtGlobal>
namespace {
std::shared_ptr<spdlog::logger> db_logger = nullptr;
std::shared_ptr<spdlog::logger> net_logger = nullptr;
std::shared_ptr<spdlog::logger> crypto_logger = nullptr;
std::shared_ptr<spdlog::logger> ui_logger = nullptr;
std::shared_ptr<spdlog::logger> qml_logger = nullptr;
constexpr auto MAX_FILE_SIZE = 1024 * 1024 * 6;
constexpr auto MAX_LOG_FILES = 3;
void
qmlMessageHandler(QtMsgType type, const QMessageLogContext &context, const QString &msg)
{
std::string localMsg = msg.toStdString();
const char *file = context.file ? context.file : "";
const char *function = context.function ? context.function : "";
switch (type) {
case QtDebugMsg:
nhlog::qml()->debug("{} ({}:{}, {})", localMsg, file, context.line, function);
break;
case QtInfoMsg:
nhlog::qml()->info("{} ({}:{}, {})", localMsg, file, context.line, function);
break;
case QtWarningMsg:
nhlog::qml()->warn("{} ({}:{}, {})", localMsg, file, context.line, function);
break;
case QtCriticalMsg:
nhlog::qml()->critical("{} ({}:{}, {})", localMsg, file, context.line, function);
break;
case QtFatalMsg:
nhlog::qml()->critical("{} ({}:{}, {})", localMsg, file, context.line, function);
break;
}
}
}
namespace nhlog {
@ -35,12 +64,15 @@ init(const std::string &file_path)
db_logger = std::make_shared<spdlog::logger>("db", std::begin(sinks), std::end(sinks));
crypto_logger =
std::make_shared<spdlog::logger>("crypto", std::begin(sinks), std::end(sinks));
qml_logger = std::make_shared<spdlog::logger>("qml", std::begin(sinks), std::end(sinks));
if (nheko::enable_debug_log) {
db_logger->set_level(spdlog::level::trace);
ui_logger->set_level(spdlog::level::trace);
crypto_logger->set_level(spdlog::level::trace);
}
qInstallMessageHandler(qmlMessageHandler);
}
std::shared_ptr<spdlog::logger>
@ -66,4 +98,10 @@ crypto()
{
return crypto_logger;
}
std::shared_ptr<spdlog::logger>
qml()
{
return qml_logger;
}
}

View File

@ -19,5 +19,8 @@ db();
std::shared_ptr<spdlog::logger>
crypto();
std::shared_ptr<spdlog::logger>
qml();
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<QString>)
class MediaProxy : public QObject
{
Q_OBJECT
signals:
void imageDownloaded(const QPixmap &);
void imageSaved(const QString &, const QByteArray &);
void fileDownloaded(const QByteArray &);
};
namespace http {
mtx::http::Client *
client();

78
src/MxcImageProvider.cpp Normal file
View File

@ -0,0 +1,78 @@
#include "MxcImageProvider.h"
#include "Cache.h"
void
MxcImageResponse::run()
{
if (m_requestedSize.isValid()) {
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 data = QByteArray(res.data(), res.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();
});
}
}

47
src/MxcImageProvider.h Normal file
View File

@ -0,0 +1,47 @@
#pragma once
#include <QQuickAsyncImageProvider>
#include <QQuickImageResponse>
#include <QImage>
#include <QThreadPool>
class MxcImageResponse
: public QQuickImageResponse
, public QRunnable
{
public:
MxcImageResponse(const QString &id, const QSize &requestedSize)
: m_id(id)
, m_requestedSize(requestedSize)
{
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;
};
class MxcImageProvider : public QQuickAsyncImageProvider
{
public:
QQuickImageResponse *requestImageResponse(const QString &id,
const QSize &requestedSize) override
{
MxcImageResponse *response = new MxcImageResponse(id, requestedSize);
pool.start(response);
return response;
}
private:
QThreadPool pool;
};

View File

@ -118,7 +118,7 @@ RoomInfoListItem::RoomInfoListItem(QString room_id, RoomInfo info, QWidget *pare
// so we can't use them for sorting.
if (roomType_ == RoomType::Invited)
lastMsgInfo_ = {
emptyEventId, "-", "-", "-", "-", QDateTime::currentDateTime().addYears(10)};
emptyEventId, "-", "-", "-", QDateTime::currentDateTime().addYears(10)};
}
void
@ -142,7 +142,7 @@ RoomInfoListItem::resizeEvent(QResizeEvent *)
void
RoomInfoListItem::paintEvent(QPaintEvent *event)
{
bool rounded = QSettings().value("user/avatar/circles", true).toBool();
bool rounded = QSettings().value("user/avatar_circles", true).toBool();
Q_UNUSED(event);
@ -210,33 +210,11 @@ RoomInfoListItem::paintEvent(QPaintEvent *event)
p.setFont(QFont{});
p.setPen(subtitlePen);
// The limit is the space between the end of the avatar and the start of the
// timestamp.
int usernameLimit =
std::max(0, width() - 3 * wm.padding - msgStampWidth - wm.iconSize - 20);
auto userName =
metrics.elidedText(lastMsgInfo_.username, Qt::ElideRight, usernameLimit);
p.setFont(QFont{});
p.drawText(QPoint(2 * wm.padding + wm.iconSize, bottom_y), userName);
#if QT_VERSION < QT_VERSION_CHECK(5, 11, 0)
int nameWidth = QFontMetrics(QFont{}).width(userName);
#else
int nameWidth = QFontMetrics(QFont{}).horizontalAdvance(userName);
#endif
p.setFont(QFont{});
// The limit is the space between the end of the username and the start of
// the timestamp.
int descriptionLimit =
std::max(0,
width() - 3 * wm.padding - bottomLineWidthLimit - wm.iconSize -
nameWidth - 5);
int descriptionLimit = std::max(
0, width() - 3 * wm.padding - bottomLineWidthLimit - wm.iconSize);
auto description =
metrics.elidedText(lastMsgInfo_.body, Qt::ElideRight, descriptionLimit);
p.drawText(QPoint(2 * wm.padding + wm.iconSize + nameWidth, bottom_y),
description);
p.drawText(QPoint(2 * wm.padding + wm.iconSize, bottom_y), description);
// We show the last message timestamp.
p.save();

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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,237 @@
#pragma once
#include <QAbstractListModel>
#include <QColor>
#include <QDate>
#include <QHash>
#include <QSet>
#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;
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);
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,94 +1,342 @@
/*
* nheko Copyright (C) 2017 Konstantinos Sideris <siderisk@auth.gr>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include "TimelineViewManager.h"
#include <random>
#include <QFileDialog>
#include <QMetaType>
#include <QMimeDatabase>
#include <QPalette>
#include <QQmlContext>
#include <QStandardPaths>
#include <QApplication>
#include <QFileInfo>
#include <QSettings>
#include "Cache.h"
#include "ChatPage.h"
#include "ColorImageProvider.h"
#include "DelegateChooser.h"
#include "Logging.h"
#include "Utils.h"
#include "timeline/TimelineView.h"
#include "timeline/TimelineViewManager.h"
#include "timeline/widgets/AudioItem.h"
#include "timeline/widgets/FileItem.h"
#include "timeline/widgets/ImageItem.h"
#include "timeline/widgets/VideoItem.h"
#include "MxcImageProvider.h"
#include "UserSettingsPage.h"
#include "dialogs/ImageOverlay.h"
void
TimelineViewManager::updateColorPalette()
{
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)
: QStackedWidget(parent)
{}
: imgProvider(new MxcImageProvider())
, colorImgProvider(new ColorImageProvider())
{
qmlRegisterUncreatableMetaObject(qml_mtx_events::staticMetaObject,
"com.github.nheko",
1,
0,
"MtxEvent",
"Can't instantiate enum!");
qmlRegisterType<DelegateChoice>("com.github.nheko", 1, 0, "DelegateChoice");
qmlRegisterType<DelegateChooser>("com.github.nheko", 1, 0, "DelegateChooser");
#ifdef USE_QUICK_VIEW
view = new QQuickView();
container = QWidget::createWindowContainer(view, parent);
#else
view = new QQuickWidget(parent);
container = view;
view->setResizeMode(QQuickWidget::SizeRootObjectToView);
container->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
connect(view, &QQuickWidget::statusChanged, this, [](QQuickWidget::Status status) {
nhlog::ui()->debug("Status changed to {}", status);
});
#endif
container->setMinimumSize(200, 200);
view->rootContext()->setContextProperty("timelineManager", this);
updateColorPalette();
view->engine()->addImageProvider("MxcImage", imgProvider);
view->engine()->addImageProvider("colorimage", colorImgProvider);
view->setSource(QUrl("qrc:///qml/TimelineView.qml"));
connect(dynamic_cast<ChatPage *>(parent),
&ChatPage::themeChanged,
this,
&TimelineViewManager::updateColorPalette);
}
void
TimelineViewManager::sync(const mtx::responses::Rooms &rooms)
{
for (auto it = rooms.join.cbegin(); it != rooms.join.cend(); ++it) {
// addRoom will only add the room, if it doesn't exist
addRoom(QString::fromStdString(it->first));
models.value(QString::fromStdString(it->first))->addEvents(it->second.timeline);
}
this->isInitialSync_ = false;
emit initialSyncChanged(false);
}
void
TimelineViewManager::addRoom(const QString &room_id)
{
if (!models.contains(room_id))
models.insert(room_id,
QSharedPointer<TimelineModel>(new TimelineModel(this, room_id)));
}
void
TimelineViewManager::setHistoryView(const QString &room_id)
{
nhlog::ui()->info("Trying to activate room {}", room_id.toStdString());
auto room = models.find(room_id);
if (room != models.end()) {
timeline_ = room.value().data();
emit activeTimelineChanged(timeline_);
nhlog::ui()->info("Activated room {}", room_id.toStdString());
}
}
void
TimelineViewManager::openImageOverlay(QString mxcUrl,
QString originalFilename,
QString mimeType,
qml_mtx_events::EventType eventType) const
{
QQuickImageResponse *imgResponse =
imgProvider->requestImageResponse(mxcUrl.remove("mxc://"), QSize());
connect(imgResponse,
&QQuickImageResponse::finished,
this,
[this, mxcUrl, originalFilename, mimeType, eventType, 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());
auto imgDialog = new dialogs::ImageOverlay(pixmap);
imgDialog->show();
connect(imgDialog,
&dialogs::ImageOverlay::saving,
this,
[this, mxcUrl, originalFilename, mimeType, eventType]() {
saveMedia(mxcUrl, originalFilename, mimeType, eventType);
});
});
}
void
TimelineViewManager::saveMedia(QString mxcUrl,
QString originalFilename,
QString mimeType,
qml_mtx_events::EventType eventType) const
{
QString dialogTitle;
if (eventType == qml_mtx_events::EventType::ImageMessage) {
dialogTitle = tr("Save image");
} else if (eventType == qml_mtx_events::EventType::VideoMessage) {
dialogTitle = tr("Save video");
} else if (eventType == qml_mtx_events::EventType::AudioMessage) {
dialogTitle = tr("Save audio");
} else {
dialogTitle = tr("Save file");
}
QString filterString = QMimeDatabase().mimeTypeForName(mimeType).filterString();
auto filename =
QFileDialog::getSaveFileName(container, dialogTitle, originalFilename, filterString);
if (filename.isEmpty())
return;
const auto url = mxcUrl.toStdString();
http::client()->download(
url,
[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;
}
try {
QFile file(filename);
if (!file.open(QIODevice::WriteOnly))
return;
file.write(QByteArray(data.data(), (int)data.size()));
file.close();
} catch (const std::exception &e) {
nhlog::ui()->warn("Error while saving file to: {}", e.what());
}
});
}
void
TimelineViewManager::cacheMedia(QString mxcUrl, QString mimeType)
{
// If the message is a link to a non mxcUrl, don't download it
if (!mxcUrl.startsWith("mxc://")) {
emit mediaCached(mxcUrl, mxcUrl);
return;
}
QString suffix = QMimeDatabase().mimeTypeForName(mimeType).preferredSuffix();
const auto url = mxcUrl.toStdString();
QFileInfo filename(QString("%1/media_cache/%2.%3")
.arg(QStandardPaths::writableLocation(QStandardPaths::CacheLocation))
.arg(QString(mxcUrl).remove("mxc://"))
.arg(suffix));
if (QDir::cleanPath(filename.path()) != filename.path()) {
nhlog::net()->warn("mxcUrl '{}' is not safe, not downloading file", url);
return;
}
QDir().mkpath(filename.path());
if (filename.isReadable()) {
emit mediaCached(mxcUrl, filename.filePath());
return;
}
http::client()->download(
url,
[this, mxcUrl, filename, url](const std::string &data,
const std::string &,
const std::string &,
mtx::http::RequestErr err) {
if (err) {
nhlog::net()->warn("failed to retrieve image {}: {} {}",
url,
err->matrix_error.error,
static_cast<int>(err->status_code));
return;
}
try {
QFile file(filename.filePath());
if (!file.open(QIODevice::WriteOnly))
return;
file.write(QByteArray(data.data(), data.size()));
file.close();
} catch (const std::exception &e) {
nhlog::ui()->warn("Error while saving file to: {}", e.what());
}
emit mediaCached(mxcUrl, filename.filePath());
});
}
void
TimelineViewManager::updateReadReceipts(const QString &room_id,
const std::vector<QString> &event_ids)
{
if (timelineViewExists(room_id)) {
auto view = views_[room_id];
if (view)
emit view->markReadEvents(event_ids);
auto room = models.find(room_id);
if (room != models.end()) {
room.value()->markEventsAsRead(event_ids);
}
}
void
TimelineViewManager::removeTimelineEvent(const QString &room_id, const QString &event_id)
TimelineViewManager::initWithMessages(const std::map<QString, mtx::responses::Timeline> &msgs)
{
auto view = views_[room_id];
for (const auto &e : msgs) {
addRoom(e.first);
if (view)
view->removeEvent(event_id);
models.value(e.first)->addEvents(e.second);
}
}
void
TimelineViewManager::queueTextMessage(const QString &msg)
{
if (active_room_.isEmpty())
return;
mtx::events::msg::Text text = {};
text.body = msg.trimmed().toStdString();
text.format = "org.matrix.custom.html";
text.formatted_body = utils::markdownToHtml(msg).toStdString();
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);
if (timeline_)
timeline_->sendMessage(text);
}
void
TimelineViewManager::queueReplyMessage(const QString &reply, const RelatedInfo &related)
{
if (active_room_.isEmpty())
return;
mtx::events::msg::Text text = {};
auto room_id = active_room_;
auto view = views_[room_id];
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);
}
}
view->addUserMessage(mtx::events::MessageType::Text, reply, related);
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
@ -96,18 +344,17 @@ TimelineViewManager::queueImageMessage(const QString &roomid,
const QString &filename,
const QString &url,
const QString &mime,
uint64_t size,
uint64_t dsize,
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);
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();
models.value(roomid)->sendMessage(image);
}
void
@ -115,16 +362,14 @@ TimelineViewManager::queueFileMessage(const QString &roomid,
const QString &filename,
const QString &url,
const QString &mime,
uint64_t size)
uint64_t dsize)
{
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);
mtx::events::msg::File file;
file.info.mimetype = mime.toStdString();
file.info.size = dsize;
file.body = filename.toStdString();
file.url = url.toStdString();
models.value(roomid)->sendMessage(file);
}
void
@ -132,16 +377,14 @@ TimelineViewManager::queueAudioMessage(const QString &roomid,
const QString &filename,
const QString &url,
const QString &mime,
uint64_t size)
uint64_t dsize)
{
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);
mtx::events::msg::Audio audio;
audio.info.mimetype = mime.toStdString();
audio.info.size = dsize;
audio.body = filename.toStdString();
audio.url = url.toStdString();
models.value(roomid)->sendMessage(audio);
}
void
@ -149,192 +392,12 @@ TimelineViewManager::queueVideoMessage(const QString &roomid,
const QString &filename,
const QString &url,
const QString &mime,
uint64_t size)
uint64_t dsize)
{
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,
&TimelineViewManager::updateRoomsLastMessage);
// 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
TimelineViewManager::sync(const mtx::responses::Rooms &rooms)
{
for (const auto &room : rooms.join) {
auto roomid = QString::fromStdString(room.first);
if (!timelineViewExists(roomid)) {
nhlog::ui()->warn("ignoring event from unknown room: {}",
roomid.toStdString());
continue;
}
auto view = views_.at(roomid);
view->addEvents(room.second.timeline);
}
}
void
TimelineViewManager::setHistoryView(const QString &room_id)
{
if (!timelineViewExists(room_id)) {
nhlog::ui()->warn("room from RoomList is not present in ViewManager: {}",
room_id.toStdString());
return;
}
active_room_ = room_id;
auto view = views_.at(room_id);
setCurrentWidget(view.data());
view->fetchHistory();
view->scrollDown();
}
QString
TimelineViewManager::chooseRandomColor()
{
std::random_device random_device;
std::mt19937 engine{random_device()};
std::uniform_real_distribution<float> dist(0, 1);
float hue = dist(engine);
float saturation = 0.9;
float value = 0.7;
int hue_i = hue * 6;
float f = hue * 6 - hue_i;
float p = value * (1 - saturation);
float q = value * (1 - f * saturation);
float t = value * (1 - (1 - f) * saturation);
float r = 0;
float g = 0;
float b = 0;
if (hue_i == 0) {
r = value;
g = t;
b = p;
} else if (hue_i == 1) {
r = q;
g = value;
b = p;
} else if (hue_i == 2) {
r = p;
g = value;
b = t;
} else if (hue_i == 3) {
r = p;
g = q;
b = value;
} else if (hue_i == 4) {
r = t;
g = p;
b = value;
} else if (hue_i == 5) {
r = value;
g = p;
b = q;
}
int ri = r * 256;
int gi = g * 256;
int bi = b * 256;
QColor color(ri, gi, bi);
return color.name();
}
bool
TimelineViewManager::hasLoaded() const
{
return std::all_of(views_.cbegin(), views_.cend(), [](const auto &view) {
return view.second->hasLoaded();
});
mtx::events::msg::Video video;
video.info.mimetype = mime.toStdString();
video.info.size = dsize;
video.body = filename.toStdString();
video.url = url.toStdString();
models.value(roomid)->sendMessage(video);
}

View File

@ -1,69 +1,80 @@
/*
* nheko Copyright (C) 2017 Konstantinos Sideris <siderisk@auth.gr>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#pragma once
#include <QQuickView>
#include <QQuickWidget>
#include <QSharedPointer>
#include <QStackedWidget>
#include <QWidget>
#include <mtx.hpp>
#include <mtx/responses.hpp>
#include "Cache.h"
#include "Logging.h"
#include "TimelineModel.h"
#include "Utils.h"
class QFile;
class MxcImageProvider;
class ColorImageProvider;
class RoomInfoListItem;
class TimelineView;
struct DescInfo;
struct SavedMessages;
class TimelineViewManager : public QStackedWidget
class TimelineViewManager : public QObject
{
Q_OBJECT
Q_PROPERTY(
TimelineModel *timeline MEMBER timeline_ READ activeTimeline NOTIFY activeTimelineChanged)
Q_PROPERTY(
bool isInitialSync MEMBER isInitialSync_ READ isInitialSync NOTIFY initialSyncChanged)
public:
TimelineViewManager(QWidget *parent);
// 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);
TimelineViewManager(QWidget *parent = 0);
QWidget *getWidget() const { return container; }
void sync(const mtx::responses::Rooms &rooms);
void clearAll() { views_.clear(); }
void addRoom(const QString &room_id);
// Check if all the timelines have been loaded.
bool hasLoaded() const;
void clearAll() { models.clear(); }
static QString chooseRandomColor();
Q_INVOKABLE TimelineModel *activeTimeline() const { return timeline_; }
Q_INVOKABLE bool isInitialSync() const { return isInitialSync_; }
void openImageOverlay(QString mxcUrl,
QString originalFilename,
QString mimeType,
qml_mtx_events::EventType eventType) const;
void saveMedia(QString mxcUrl,
QString originalFilename,
QString mimeType,
qml_mtx_events::EventType eventType) const;
Q_INVOKABLE void cacheMedia(QString mxcUrl, QString mimeType);
// Qml can only pass enum as int
Q_INVOKABLE void openImageOverlay(QString mxcUrl,
QString originalFilename,
QString mimeType,
int eventType) const
{
openImageOverlay(
mxcUrl, originalFilename, mimeType, (qml_mtx_events::EventType)eventType);
}
Q_INVOKABLE void saveMedia(QString mxcUrl,
QString originalFilename,
QString mimeType,
int eventType) const
{
saveMedia(mxcUrl, originalFilename, mimeType, (qml_mtx_events::EventType)eventType);
}
signals:
void clearRoomMessageCount(QString roomid);
void updateRoomsLastMessage(const QString &user, const DescInfo &info);
void updateRoomsLastMessage(QString roomid, const DescInfo &info);
void activeTimelineChanged(TimelineModel *timeline);
void initialSyncChanged(bool isInitialSync);
void mediaCached(QString mxcUrl, QString cacheUrl);
public slots:
void updateReadReceipts(const QString &room_id, const std::vector<QString> &event_ids);
void removeTimelineEvent(const QString &room_id, const QString &event_id);
void initWithMessages(const std::map<QString, mtx::responses::Timeline> &msgs);
void setHistoryView(const QString &room_id);
void updateColorPalette();
void queueTextMessage(const QString &msg);
void queueReplyMessage(const QString &reply, const RelatedInfo &related);
void queueEmoteMessage(const QString &msg);
@ -90,9 +101,17 @@ public slots:
uint64_t dsize);
private:
//! Check if the given room id is managed by a TimelineView.
bool timelineViewExists(const QString &id) { return views_.find(id) != views_.end(); }
#ifdef USE_QUICK_VIEW
QQuickView *view;
#else
QQuickWidget *view;
#endif
QWidget *container;
QString active_room_;
std::map<QString, QSharedPointer<TimelineView>> views_;
MxcImageProvider *imgProvider;
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
Avatar::paintEvent(QPaintEvent *)
{
bool rounded = QSettings().value("user/avatar/circles", true).toBool();
bool rounded = QSettings().value("user/avatar_circles", true).toBool();
QPainter painter(this);
painter.setRenderHint(QPainter::Antialiasing);