diff --git a/.ci/format.sh b/.ci/format.sh index 2d922ee1..cc4a3b82 100755 --- a/.ci/format.sh +++ b/.ci/format.sh @@ -14,17 +14,4 @@ do clang-format -i "$f" done; -QMLFORMAT_PATH=$(command -v qmlformat || true) - -if [ -n "$QMLFORMAT_PATH" ]; then - QML_FILES=$(find resources -type f -iname "*.qml") - - for f in $QML_FILES - do - $QMLFORMAT_PATH -i "$f" - done; -else - echo "qmlformat not found; skipping qml formatting" -fi - git diff --exit-code diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 7ff92c17..cea6be7b 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -52,14 +52,14 @@ build-macos: stage: build tags: [macos] before_script: - - brew update - - brew reinstall --force python3 - - brew bundle --file=./.ci/macos/Brewfile --force --cleanup + #- brew update + #- brew reinstall --force python3 + #- brew bundle --file=./.ci/macos/Brewfile --force --cleanup - pip3 install dmgbuild - rm -rf ../.hunter && mv .hunter ../.hunter || true script: - - export PATH=/usr/local/opt/qt/bin/:${PATH} - - export CMAKE_PREFIX_PATH=/usr/local/opt/qt5 + - export PATH=/usr/local/opt/qt@5/bin/:${PATH} + - export CMAKE_PREFIX_PATH=/usr/local/opt/qt@5 - cmake -GNinja -H. -Bbuild -DCMAKE_BUILD_TYPE=RelWithDebInfo -DCMAKE_INSTALL_PREFIX=.deps/usr @@ -91,7 +91,9 @@ build-flatpak-amd64: #image: 'registry.gitlab.gnome.org/gnome/gnome-runtime-images/gnome:master' tags: [docker] before_script: - - apt-get update && apt-get -y install flatpak-builder git python curl python3-aiohttp python3-tenacity gir1.2-ostree-1.0 + # need flatpak 1.11.1 at least + - apt-get update && apt-get install -y software-properties-common + - add-apt-repository ppa:alexlarsson/flatpak && apt-get update && apt-get -y install flatpak-builder git python curl python3-aiohttp python3-tenacity gir1.2-ostree-1.0 - flatpak remote-add --user --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo - flatpak --noninteractive install --user flathub org.kde.Platform//5.15 - flatpak --noninteractive install --user flathub org.kde.Sdk//5.15 @@ -119,7 +121,9 @@ build-flatpak-arm64: #image: 'registry.gitlab.gnome.org/gnome/gnome-runtime-images/gnome:master' tags: [docker-arm64] before_script: - - apt-get update && apt-get -y install flatpak-builder git python curl python3-aiohttp python3-tenacity gir1.2-ostree-1.0 + # need flatpak 1.11.1 at least + - apt-get update && apt-get install -y software-properties-common + - add-apt-repository ppa:alexlarsson/flatpak && apt-get update && apt-get -y install flatpak-builder git python curl python3-aiohttp python3-tenacity gir1.2-ostree-1.0 - flatpak remote-add --user --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo - flatpak --noninteractive install --user flathub org.kde.Platform//5.15 - flatpak --noninteractive install --user flathub org.kde.Sdk//5.15 diff --git a/CMakeLists.txt b/CMakeLists.txt index 6b26b2e5..049ed8a3 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -281,14 +281,11 @@ set(SRC_FILES src/dialogs/CreateRoom.cpp src/dialogs/FallbackAuth.cpp src/dialogs/ImageOverlay.cpp - src/dialogs/InviteUsers.cpp src/dialogs/JoinRoom.cpp src/dialogs/LeaveRoom.cpp src/dialogs/Logout.cpp - src/dialogs/MemberList.cpp src/dialogs/PreviewUploadOverlay.cpp src/dialogs/ReCaptcha.cpp - src/dialogs/ReadReceipts.cpp # Emoji src/emoji/EmojiModel.cpp @@ -307,7 +304,6 @@ set(SRC_FILES src/timeline/RoomlistModel.cpp # UI components - src/ui/Avatar.cpp src/ui/Badge.cpp src/ui/DropShadow.cpp src/ui/FlatButton.cpp @@ -346,16 +342,20 @@ set(SRC_FILES src/CompletionProxyModel.cpp src/DeviceVerificationFlow.cpp src/EventAccessors.cpp - src/InviteeItem.cpp + src/InviteesModel.cpp src/Logging.cpp src/LoginPage.cpp src/MainWindow.cpp src/MatrixClient.cpp + src/MemberList.cpp src/MxcImageProvider.cpp src/Olm.cpp + src/ReadReceiptsModel.cpp src/RegisterPage.cpp src/SSOHandler.cpp - src/ImagePackModel.cpp + src/CombinedImagePackModel.cpp + src/SingleImagePackModel.cpp + src/ImagePackListModel.cpp src/TrayIcon.cpp src/UserSettingsPage.cpp src/UsersModel.cpp @@ -381,7 +381,7 @@ if(USE_BUNDLED_MTXCLIENT) FetchContent_Declare( MatrixClient GIT_REPOSITORY https://github.com/Nheko-Reborn/mtxclient.git - GIT_TAG 1c277e9ac69aafdaf6888ce595b21dc86e970f28 + GIT_TAG bcf363cb5e6c423f40c96123e227bc8c5f6d6f80 ) set(BUILD_LIB_EXAMPLES OFF CACHE INTERNAL "") set(BUILD_LIB_TESTS OFF CACHE INTERNAL "") @@ -492,15 +492,11 @@ qt5_wrap_cpp(MOC_HEADERS src/dialogs/CreateRoom.h src/dialogs/FallbackAuth.h src/dialogs/ImageOverlay.h - src/dialogs/InviteUsers.h src/dialogs/JoinRoom.h src/dialogs/LeaveRoom.h src/dialogs/Logout.h - src/dialogs/MemberList.h src/dialogs/PreviewUploadOverlay.h - src/dialogs/RawMessage.h src/dialogs/ReCaptcha.h - src/dialogs/ReadReceipts.h # Emoji src/emoji/EmojiModel.h @@ -518,7 +514,6 @@ qt5_wrap_cpp(MOC_HEADERS src/timeline/RoomlistModel.h # UI components - src/ui/Avatar.h src/ui/Badge.h src/ui/FlatButton.h src/ui/FloatingButton.h @@ -546,28 +541,33 @@ qt5_wrap_cpp(MOC_HEADERS src/AvatarProvider.h src/BlurhashProvider.h - src/Cache_p.h src/CacheCryptoStructs.h + src/Cache_p.h src/CallDevices.h src/CallManager.h src/ChatPage.h src/Clipboard.h + src/CombinedImagePackModel.h src/CompletionProxyModel.h src/DeviceVerificationFlow.h - src/InviteeItem.h + src/ImagePackListModel.h + src/InviteesModel.h src/LoginPage.h src/MainWindow.h + src/MemberList.h src/MxcImageProvider.h + src/Olm.h src/RegisterPage.h + src/RoomsModel.h src/SSOHandler.h - src/ImagePackModel.h + src/SingleImagePackModel.h src/TrayIcon.h src/UserSettingsPage.h src/UsersModel.h - src/RoomsModel.h src/WebRTCSession.h src/WelcomePage.h - ) + src/ReadReceiptsModel.h +) # # Bundle translations. diff --git a/io.github.NhekoReborn.Nheko.yaml b/io.github.NhekoReborn.Nheko.yaml index b6f468db..a0e57b09 100644 --- a/io.github.NhekoReborn.Nheko.yaml +++ b/io.github.NhekoReborn.Nheko.yaml @@ -19,6 +19,8 @@ finish-args: - --talk-name=org.freedesktop.secrets - --talk-name=org.freedesktop.StatusNotifierItem - --talk-name=org.kde.* + # needed for SingleApplication to work + - --allow=per-app-dev-shm cleanup: - /include - /bin/mdb* @@ -161,7 +163,7 @@ modules: buildsystem: cmake-ninja name: mtxclient sources: - - commit: 1c277e9ac69aafdaf6888ce595b21dc86e970f28 + - commit: bcf363cb5e6c423f40c96123e227bc8c5f6d6f80 type: git url: https://github.com/Nheko-Reborn/mtxclient.git - config-opts: diff --git a/nheko-nightly.flatpakref b/nheko-nightly.flatpakref index 7d27bdfe..74e47ecd 100644 --- a/nheko-nightly.flatpakref +++ b/nheko-nightly.flatpakref @@ -3,6 +3,7 @@ Title=Nheko Nightly Name=io.github.NhekoReborn.Nheko Branch=master Url=https://flatpak.neko.dev/repo/nightly +SuggestRemoteName=nheko-nightlies Homepage=https://nheko-reborn.github.io/ Icon=https://nheko.im/nheko-reborn/nheko/-/raw/master/resources/nheko.svg RuntimeRepo=https://dl.flathub.org/repo/flathub.flatpakrepo diff --git a/nheko-nightly.flatpakrepo b/nheko-nightly.flatpakrepo index 4fb1bc55..680558af 100644 --- a/nheko-nightly.flatpakrepo +++ b/nheko-nightly.flatpakrepo @@ -1,6 +1,7 @@ [Flatpak Repo] Title=Nheko Nightly Url=https://flatpak.neko.dev/repo/nightly +SuggestRemoteName=nheko-nightlies Homepage=https://nheko.im/ Comment=Nheko nightly release repository Description=Nheko nightly release repository diff --git a/resources/langs/nheko_cs.ts b/resources/langs/nheko_cs.ts index c544bf6d..99813f81 100644 --- a/resources/langs/nheko_cs.ts +++ b/resources/langs/nheko_cs.ts @@ -38,7 +38,7 @@ AwaitingVerificationConfirmation - + Awaiting Confirmation @@ -48,7 +48,7 @@ - + Cancel @@ -125,7 +125,7 @@ ChatPage - + Failed to invite user: %1 @@ -157,12 +157,12 @@ - + Confirm invite - + Do you really want to invite %1 (%2)? @@ -227,12 +227,12 @@ - + Do you really want to start a private chat with %1? - + Cache migration failed! @@ -352,7 +352,7 @@ CrossSigningSecrets - + Decrypt secrets @@ -426,12 +426,12 @@ EmojiPicker - + Search - + People @@ -604,10 +604,48 @@ + + ImagePackSettingsDialog + + + Image pack settings + + + + + Private pack + + + + + Pack from this room + + + + + Globally enabled pack + + + + + Enable globally + + + + + Enables this pack to be used in all rooms + + + + + Close + + + InputBar - + Select a file @@ -617,16 +655,42 @@ - + Failed to upload media. Please try again. - InviteeItem + InviteDialog - - Remove + + Invite users to %1 + + + + + User ID to invite + + + + + @joe:matrix.org + Example user id. The name 'joe' can be localized however you want. + + + + + Add + + + + + Invite + + + + + Cancel @@ -740,28 +804,15 @@ Example: https://server.my:8787 - - MemberList - - - Room members - - - - - OK - - - MessageDelegate - + Encryption enabled - + room name changed to: %1 @@ -771,7 +822,7 @@ Example: https://server.my:8787 - + topic changed to: %1 @@ -781,17 +832,17 @@ Example: https://server.my:8787 - + %1 changed the room avatar - + %1 created and configured room: %2 - + %1 placed a voice call. @@ -806,23 +857,23 @@ Example: https://server.my:8787 - + Negotiating call... - + %1 answered the call. - + removed - + %1 ended the call. @@ -830,7 +881,7 @@ Example: https://server.my:8787 MessageInput - + Hang up @@ -851,6 +902,11 @@ Example: https://server.my:8787 + Stickers + + + + Emoji @@ -868,17 +924,17 @@ Example: https://server.my:8787 MessageView - + Edit - + React - + Reply @@ -888,7 +944,7 @@ Example: https://server.my:8787 - + &Copy @@ -1096,7 +1152,7 @@ Example: https://server.my:8787 Placeholder - + unimplemented event: @@ -1216,7 +1272,7 @@ Example: https://server.my:8787 ReplyPopup - + Close @@ -1229,7 +1285,7 @@ Example: https://server.my:8787 RoomInfo - + no version stored @@ -1237,7 +1293,7 @@ Example: https://server.my:8787 RoomList - + New tag @@ -1246,6 +1302,16 @@ Example: https://server.my:8787 Enter the tag you want to use: + + + Leave Room + + + + + Are you sure you want to leave this room? + + Leave room @@ -1277,17 +1343,7 @@ Example: https://server.my:8787 - - Accept - - - - - Decline - - - - + Status Message @@ -1337,20 +1393,43 @@ Example: https://server.my:8787 + + RoomMembers + + + Members of %1 + + + + + %n people in %1 + Summary above list of members + + + + + + + + + Invite more people + + + RoomSettings - + Room Settings - + %1 member(s) - + SETTINGS @@ -1406,19 +1485,22 @@ Example: https://server.my:8787 - - Respond to key requests + + Sticker & Emote Settings - - Whether or not the client should respond automatically with the session keys - upon request. Use with caution, this is a temporary measure to test the - E2E implementation until device verification is completed. + + Change - + + Change what packs are enabled, remove packs or create new ones + + + + INFO @@ -1433,12 +1515,7 @@ Example: https://server.my:8787 - - OK - - - - + Failed to enable encryption: %1 @@ -1469,6 +1546,24 @@ Example: https://server.my:8787 + + RoomlistModel + + + Pending invite. + + + + + Previewing this room + + + + + No preview available + + + ScreenShare @@ -1526,7 +1621,7 @@ Example: https://server.my:8787 StatusIndicator - + Failed @@ -1546,6 +1641,14 @@ Example: https://server.my:8787 + + StickerPicker + + + Search + + + Success @@ -1567,7 +1670,7 @@ Example: https://server.my:8787 TimelineModel - + Message redaction failed: %1 @@ -1578,7 +1681,7 @@ Example: https://server.my:8787 - + Save image @@ -1713,12 +1816,12 @@ Example: https://server.my:8787 - + You joined this room. - + %1 has changed their avatar and changed their display name to %2. @@ -1747,7 +1850,7 @@ Example: https://server.my:8787 TimelineRow - + Edited @@ -1755,17 +1858,32 @@ Example: https://server.my:8787 TimelineView - + No room open - + %1 member(s) - + + join the conversation + + + + + accept invite + + + + + decline invite + + + + Back to room list @@ -1773,7 +1891,7 @@ Example: https://server.my:8787 TimelineViewManager - + No encrypted private chat found with this user. Create an encrypted private chat with this user and try again. @@ -1781,18 +1899,17 @@ Example: https://server.my:8787 TopBar - + Back to room list - - + No room selected - + Room options @@ -1833,7 +1950,7 @@ Example: https://server.my:8787 UserProfile - + Global User Profile @@ -1843,7 +1960,7 @@ Example: https://server.my:8787 - + Verify @@ -1892,7 +2009,7 @@ Example: https://server.my:8787 UserSettings - + Default @@ -1901,7 +2018,7 @@ Example: https://server.my:8787 UserSettingsPage - + Minimize to tray @@ -2347,7 +2464,7 @@ This usually causes the application icon in the task bar to animate in some fash Waiting - + Waiting for other party… @@ -2398,7 +2515,7 @@ This usually causes the application icon in the task bar to animate in some fash descriptiveTime - + Yesterday @@ -2469,19 +2586,6 @@ This usually causes the application icon in the task bar to animate in some fash - - dialogs::InviteUsers - - - Cancel - - - - - User ID to invite - - - dialogs::JoinRoom @@ -2593,7 +2697,7 @@ Media size: %2 message-description sent: - + You sent an audio clip diff --git a/resources/langs/nheko_de.ts b/resources/langs/nheko_de.ts index 58c209a2..cb5b54fb 100644 --- a/resources/langs/nheko_de.ts +++ b/resources/langs/nheko_de.ts @@ -38,7 +38,7 @@ AwaitingVerificationConfirmation - + Awaiting Confirmation Auf Bestätigung warten @@ -48,7 +48,7 @@ Wartet darauf, dass die andere Seite die Verifizierung abschließt. - + Cancel Abbrechen @@ -125,7 +125,7 @@ ChatPage - + Failed to invite user: %1 Nutzer konnte nicht eingeladen werden: %1 @@ -157,12 +157,12 @@ - + Confirm invite Einladung bestätigen - + Do you really want to invite %1 (%2)? Nutzer %1 (%2) wirklich einladen? @@ -227,12 +227,12 @@ Verbannung aufgehoben: %1 - + Do you really want to start a private chat with %1? Möchtest du wirklich eine private Konversation mit %1 beginnen? - + Cache migration failed! Migration des Caches fehlgeschlagen! @@ -352,7 +352,7 @@ CrossSigningSecrets - + Decrypt secrets Geheimnisse entschlüsseln @@ -426,12 +426,12 @@ EmojiPicker - + Search Suche - + People Leute @@ -604,10 +604,48 @@ Nachricht weiterleiten + + ImagePackSettingsDialog + + + Image pack settings + + + + + Private pack + + + + + Pack from this room + + + + + Globally enabled pack + + + + + Enable globally + + + + + Enables this pack to be used in all rooms + + + + + Close + Schließen + + InputBar - + Select a file Datei auswählen @@ -617,17 +655,43 @@ Alle Dateien (*) - + Failed to upload media. Please try again. Medienupload fehlgeschlagen. Bitte versuche es erneut. - InviteeItem + InviteDialog - - Remove - Löschen + + Invite users to %1 + Lade Benutzer in %1 ein + + + + User ID to invite + Benutzer-ID, die eingeladen werden soll + + + + @joe:matrix.org + Example user id. The name 'joe' can be localized however you want. + @joe:matrix.org + + + + Add + Hinzufügen + + + + Invite + Einladen + + + + Cancel + Abbrechen @@ -744,23 +808,10 @@ Beispiel: https://mein.server:8787 SSO Anmeldung fehlgeschlagen - - MemberList - - - Room members - Teilnehmerliste - - - - OK - OK - - MessageDelegate - + removed entfernt @@ -771,7 +822,7 @@ Beispiel: https://mein.server:8787 Verschlüsselung aktiviert - + room name changed to: %1 Raumname wurde gändert auf: %1 @@ -781,7 +832,7 @@ Beispiel: https://mein.server:8787 Raumname wurde entfernt - + topic changed to: %1 Raumthema wurde geändert auf: %1 @@ -791,17 +842,17 @@ Beispiel: https://mein.server:8787 Thema wurde entfernt - + %1 changed the room avatar %1 hat dem Raumavatar geändert - + %1 created and configured room: %2 %1 hat den Raum erstellt: %2 - + %1 placed a voice call. %1 hat einen Sprachanruf gestartet. @@ -816,17 +867,17 @@ Beispiel: https://mein.server:8787 %1 hat angerufen. - + %1 answered the call. %1 hat den Anruf angenommen. - + %1 ended the call. %1 hat den Anruf beendet. - + Negotiating call... Wählt… @@ -834,7 +885,7 @@ Beispiel: https://mein.server:8787 MessageInput - + Hang up Auflegen @@ -855,6 +906,11 @@ Beispiel: https://mein.server:8787 + Stickers + Sticker + + + Emoji Emoji @@ -872,17 +928,17 @@ Beispiel: https://mein.server:8787 MessageView - + Edit Bearbeiten - + React Reaktion senden - + Reply Antworten @@ -892,7 +948,7 @@ Beispiel: https://mein.server:8787 Optionen - + &Copy &Kopieren @@ -1100,7 +1156,7 @@ Beispiel: https://mein.server:8787 Placeholder - + unimplemented event: Unimplementiertes Event: @@ -1220,7 +1276,7 @@ Beispiel: https://mein.server:8787 ReplyPopup - + Close Schließen @@ -1233,7 +1289,7 @@ Beispiel: https://mein.server:8787 RoomInfo - + no version stored keine Version gespeichert @@ -1241,7 +1297,7 @@ Beispiel: https://mein.server:8787 RoomList - + New tag Neuer Tag @@ -1250,6 +1306,16 @@ Beispiel: https://mein.server:8787 Enter the tag you want to use: Gib den Tag, den du verwenden willst, ein: + + + Leave Room + + + + + Are you sure you want to leave this room? + + Leave room @@ -1281,17 +1347,7 @@ Beispiel: https://mein.server:8787 Neuen Tag erstellen... - - Accept - Akzeptieren - - - - Decline - Ablehnen - - - + Status Message Statusnachricht @@ -1341,20 +1397,42 @@ Beispiel: https://mein.server:8787 Benutzereinstellungen + + RoomMembers + + + Members of %1 + Teilnehmer in %1 + + + + %n people in %1 + Summary above list of members + + %n Person in %1 + %n Personen in %1 + + + + + Invite more people + Lade mehr Leute ein + + RoomSettings - + Room Settings Raumeinstellungen - + %1 member(s) %1 Teilnehmer - + SETTINGS EINSTELLUNGEN @@ -1410,19 +1488,22 @@ Beispiel: https://mein.server:8787 Verschlüsselung ist derzeit experimentell und könnte unerwartete Probleme verursachen.<br>Bitte beachte, dass dies später nicht mehr deaktiviert werden kann. - - Respond to key requests - Auf Schlüsselanfragen antworten + + Sticker & Emote Settings + - - Whether or not the client should respond automatically with the session keys - upon request. Use with caution, this is a temporary measure to test the - E2E implementation until device verification is completed. - Soll der Client automatisch mit den Sitzungsschlüsseln auf Anfragen antworten oder nicht? Bitte mit Vorsicht benutzen, dies ist eine vorübergehende Maßnahme, um die Ende-zu-Ende-Implementierung zu testen, bis die Geräteverifizierung abgeschlossen ist. + + Change + - + + Change what packs are enabled, remove packs or create new ones + + + + INFO INFO @@ -1437,12 +1518,7 @@ Beispiel: https://mein.server:8787 Raumversion - - OK - OK - - - + Failed to enable encryption: %1 Aktivierung der Verschlüsselung fehlgeschlagen: %1 @@ -1473,6 +1549,24 @@ Beispiel: https://mein.server:8787 Hochladen des Bildes fehlgeschlagen: %s + + RoomlistModel + + + Pending invite. + Offene Einladung. + + + + Previewing this room + Vorschau dieses Raums + + + + No preview available + Keine Vorschau verfügbar + + ScreenShare @@ -1530,7 +1624,7 @@ Beispiel: https://mein.server:8787 StatusIndicator - + Failed Fehlgeschlagen @@ -1550,6 +1644,14 @@ Beispiel: https://mein.server:8787 Gelesen + + StickerPicker + + + Search + Suche + + Success @@ -1571,7 +1673,7 @@ Beispiel: https://mein.server:8787 TimelineModel - + Message redaction failed: %1 Nachricht zurückziehen fehlgeschlagen: %1 @@ -1582,7 +1684,7 @@ Beispiel: https://mein.server:8787 Event konnte nicht verschlüsselt werden, senden wurde abgebrochen! - + Save image Bild speichern @@ -1716,12 +1818,12 @@ Beispiel: https://mein.server:8787 %1 hat das Anklopfen zurückgezogen. - + You joined this room. Du bist dem Raum beigetreten. - + %1 has changed their avatar and changed their display name to %2. %1 hat den eigenen Avatar und Namen geändert zu %2. @@ -1750,7 +1852,7 @@ Beispiel: https://mein.server:8787 TimelineRow - + Edited Bearbeitet @@ -1758,17 +1860,32 @@ Beispiel: https://mein.server:8787 TimelineView - + No room open Kein Raum geöffnet - + %1 member(s) %1 Teilnehmer - + + join the conversation + An der Unterhaltung teilnehmen + + + + accept invite + Einladung annehmen + + + + decline invite + Einladung ablehnen + + + Back to room list Zurück zur Raumliste @@ -1776,7 +1893,7 @@ Beispiel: https://mein.server:8787 TimelineViewManager - + No encrypted private chat found with this user. Create an encrypted private chat with this user and try again. Keinen verschlüsselten Chat mit diesem User gefunden. Erstelle einen verschlüsselten 1:1 Chat mit diesem Nutzer und versuche es erneut. @@ -1784,18 +1901,17 @@ Beispiel: https://mein.server:8787 TopBar - + Back to room list Zurück zur Raumliste - - + No room selected Kein Raum ausgewählt - + Room options Raumoptionen @@ -1836,7 +1952,7 @@ Beispiel: https://mein.server:8787 UserProfile - + Global User Profile Globales Nutzerprofil @@ -1846,7 +1962,7 @@ Beispiel: https://mein.server:8787 Raumspezifisches Nutzerprofil - + Verify Verifizieren @@ -1895,7 +2011,7 @@ Beispiel: https://mein.server:8787 UserSettings - + Default Standard @@ -1904,7 +2020,7 @@ Beispiel: https://mein.server:8787 UserSettingsPage - + Minimize to tray Ins Benachrichtigungsfeld minimieren @@ -2360,7 +2476,7 @@ Normalerweise animiert das den Taskbaricon oder färbt das Fenster orange ein. Waiting - + Waiting for other party… Auf Gegenseite warten… @@ -2411,7 +2527,7 @@ Normalerweise animiert das den Taskbaricon oder färbt das Fenster orange ein. descriptiveTime - + Yesterday Gestern @@ -2482,19 +2598,6 @@ Normalerweise animiert das den Taskbaricon oder färbt das Fenster orange ein.Öffne das Fallback, folge den Anweisungen und bestätige nach Abschluss via "Bestätigen". - - dialogs::InviteUsers - - - Cancel - Abbrechen - - - - User ID to invite - Benutzer-ID, die eingeladen werden soll - - dialogs::JoinRoom @@ -2608,7 +2711,7 @@ Medien-Größe: %2 message-description sent: - + You sent an audio clip Du hast eine Audiodatei gesendet diff --git a/resources/langs/nheko_el.ts b/resources/langs/nheko_el.ts index d5d5f323..d40a6433 100644 --- a/resources/langs/nheko_el.ts +++ b/resources/langs/nheko_el.ts @@ -38,7 +38,7 @@ AwaitingVerificationConfirmation - + Awaiting Confirmation @@ -48,7 +48,7 @@ - + Cancel Άκυρο @@ -125,7 +125,7 @@ ChatPage - + Failed to invite user: %1 @@ -157,12 +157,12 @@ - + Confirm invite - + Do you really want to invite %1 (%2)? @@ -227,12 +227,12 @@ - + Do you really want to start a private chat with %1? - + Cache migration failed! @@ -352,7 +352,7 @@ CrossSigningSecrets - + Decrypt secrets @@ -426,12 +426,12 @@ EmojiPicker - + Search - + People @@ -604,10 +604,48 @@ + + ImagePackSettingsDialog + + + Image pack settings + + + + + Private pack + + + + + Pack from this room + + + + + Globally enabled pack + + + + + Enable globally + + + + + Enables this pack to be used in all rooms + + + + + Close + + + InputBar - + Select a file Διάλεξε ένα αρχείο @@ -617,18 +655,44 @@ Όλα τα αρχεία (*) - + Failed to upload media. Please try again. - InviteeItem + InviteDialog - - Remove + + Invite users to %1 + + + User ID to invite + Όνομα χρήστη + + + + @joe:matrix.org + Example user id. The name 'joe' can be localized however you want. + + + + + Add + + + + + Invite + + + + + Cancel + Άκυρο + LoginPage @@ -740,23 +804,10 @@ Example: https://server.my:8787 - - MemberList - - - Room members - Μέλη - - - - OK - - - MessageDelegate - + removed @@ -767,7 +818,7 @@ Example: https://server.my:8787 - + room name changed to: %1 @@ -777,7 +828,7 @@ Example: https://server.my:8787 - + topic changed to: %1 @@ -787,17 +838,17 @@ Example: https://server.my:8787 - + %1 changed the room avatar - + %1 created and configured room: %2 - + %1 placed a voice call. @@ -812,17 +863,17 @@ Example: https://server.my:8787 - + %1 answered the call. - + %1 ended the call. - + Negotiating call... @@ -830,7 +881,7 @@ Example: https://server.my:8787 MessageInput - + Hang up @@ -851,6 +902,11 @@ Example: https://server.my:8787 + Stickers + + + + Emoji @@ -868,17 +924,17 @@ Example: https://server.my:8787 MessageView - + Edit - + React - + Reply @@ -888,7 +944,7 @@ Example: https://server.my:8787 - + &Copy @@ -1096,7 +1152,7 @@ Example: https://server.my:8787 Placeholder - + unimplemented event: @@ -1216,7 +1272,7 @@ Example: https://server.my:8787 ReplyPopup - + Close @@ -1229,7 +1285,7 @@ Example: https://server.my:8787 RoomInfo - + no version stored @@ -1237,7 +1293,7 @@ Example: https://server.my:8787 RoomList - + New tag @@ -1246,6 +1302,16 @@ Example: https://server.my:8787 Enter the tag you want to use: + + + Leave Room + + + + + Are you sure you want to leave this room? + + Leave room @@ -1277,17 +1343,7 @@ Example: https://server.my:8787 - - Accept - Αποδοχή - - - - Decline - Απόρριψη - - - + Status Message @@ -1337,20 +1393,42 @@ Example: https://server.my:8787 + + RoomMembers + + + Members of %1 + + + + + %n people in %1 + Summary above list of members + + + + + + + + Invite more people + + + RoomSettings - + Room Settings - + %1 member(s) - + SETTINGS @@ -1406,19 +1484,22 @@ Example: https://server.my:8787 - - Respond to key requests + + Sticker & Emote Settings - - Whether or not the client should respond automatically with the session keys - upon request. Use with caution, this is a temporary measure to test the - E2E implementation until device verification is completed. + + Change - + + Change what packs are enabled, remove packs or create new ones + + + + INFO @@ -1433,12 +1514,7 @@ Example: https://server.my:8787 - - OK - - - - + Failed to enable encryption: %1 @@ -1469,6 +1545,24 @@ Example: https://server.my:8787 + + RoomlistModel + + + Pending invite. + + + + + Previewing this room + + + + + No preview available + + + ScreenShare @@ -1526,7 +1620,7 @@ Example: https://server.my:8787 StatusIndicator - + Failed @@ -1546,6 +1640,14 @@ Example: https://server.my:8787 + + StickerPicker + + + Search + + + Success @@ -1567,7 +1669,7 @@ Example: https://server.my:8787 TimelineModel - + Message redaction failed: %1 @@ -1578,7 +1680,7 @@ Example: https://server.my:8787 - + Save image Αποθήκευση Εικόνας @@ -1712,12 +1814,12 @@ Example: https://server.my:8787 - + You joined this room. - + %1 has changed their avatar and changed their display name to %2. @@ -1746,7 +1848,7 @@ Example: https://server.my:8787 TimelineRow - + Edited @@ -1754,17 +1856,32 @@ Example: https://server.my:8787 TimelineView - + No room open - + %1 member(s) - + + join the conversation + + + + + accept invite + + + + + decline invite + + + + Back to room list @@ -1772,7 +1889,7 @@ Example: https://server.my:8787 TimelineViewManager - + No encrypted private chat found with this user. Create an encrypted private chat with this user and try again. @@ -1780,18 +1897,17 @@ Example: https://server.my:8787 TopBar - + Back to room list - - + No room selected - + Room options @@ -1832,7 +1948,7 @@ Example: https://server.my:8787 UserProfile - + Global User Profile @@ -1842,7 +1958,7 @@ Example: https://server.my:8787 - + Verify @@ -1891,7 +2007,7 @@ Example: https://server.my:8787 UserSettings - + Default @@ -1900,7 +2016,7 @@ Example: https://server.my:8787 UserSettingsPage - + Minimize to tray Ελαχιστοποίηση @@ -2346,7 +2462,7 @@ This usually causes the application icon in the task bar to animate in some fash Waiting - + Waiting for other party… @@ -2397,7 +2513,7 @@ This usually causes the application icon in the task bar to animate in some fash descriptiveTime - + Yesterday @@ -2468,19 +2584,6 @@ This usually causes the application icon in the task bar to animate in some fash - - dialogs::InviteUsers - - - Cancel - Άκυρο - - - - User ID to invite - Όνομα χρήστη - - dialogs::JoinRoom @@ -2592,7 +2695,7 @@ Media size: %2 message-description sent: - + You sent an audio clip diff --git a/resources/langs/nheko_en.ts b/resources/langs/nheko_en.ts index 7d3f8276..1851fff1 100644 --- a/resources/langs/nheko_en.ts +++ b/resources/langs/nheko_en.ts @@ -38,7 +38,7 @@ AwaitingVerificationConfirmation - + Awaiting Confirmation Awaiting Confirmation @@ -48,7 +48,7 @@ Waiting for other side to complete verification. - + Cancel Cancel @@ -125,7 +125,7 @@ ChatPage - + Failed to invite user: %1 Failed to invite user: %1 @@ -157,12 +157,12 @@ - + Confirm invite Confirm invite - + Do you really want to invite %1 (%2)? Do you really want to invite %1 (%2)? @@ -227,12 +227,12 @@ Unbanned user: %1 - + Do you really want to start a private chat with %1? Do you really want to start a private chat with %1? - + Cache migration failed! Cache migration failed! @@ -352,7 +352,7 @@ CrossSigningSecrets - + Decrypt secrets Decrypt secrets @@ -426,12 +426,12 @@ EmojiPicker - + Search Search - + People People @@ -604,10 +604,48 @@ Forward Message + + ImagePackSettingsDialog + + + Image pack settings + + + + + Private pack + + + + + Pack from this room + + + + + Globally enabled pack + + + + + Enable globally + + + + + Enables this pack to be used in all rooms + + + + + Close + Close + + InputBar - + Select a file Select a file @@ -617,17 +655,43 @@ All Files (*) - + Failed to upload media. Please try again. Failed to upload media. Please try again. - InviteeItem + InviteDialog - - Remove - Remove + + Invite users to %1 + Invite users to %1 + + + + User ID to invite + User ID to invite + + + + @joe:matrix.org + Example user id. The name 'joe' can be localized however you want. + @joe:matrix.org + + + + Add + Add + + + + Invite + Invite + + + + Cancel + Cancel @@ -744,28 +808,15 @@ Example: https://server.my:8787 SSO login failed - - MemberList - - - Room members - Room members - - - - OK - OK - - MessageDelegate - + Encryption enabled Encryption enabled - + room name changed to: %1 room name changed to: %1 @@ -775,7 +826,7 @@ Example: https://server.my:8787 removed room name - + topic changed to: %1 topic changed to: %1 @@ -785,17 +836,17 @@ Example: https://server.my:8787 removed topic - + %1 changed the room avatar %1 changed the room avatar - + %1 created and configured room: %2 %1 created and configured room: %2 - + %1 placed a voice call. %1 placed a voice call. @@ -810,23 +861,23 @@ Example: https://server.my:8787 %1 placed a call. - + Negotiating call... Negotiating call… - + %1 answered the call. %1 answered the call. - + removed removed - + %1 ended the call. %1 ended the call. @@ -834,7 +885,7 @@ Example: https://server.my:8787 MessageInput - + Hang up Hang up @@ -855,6 +906,11 @@ Example: https://server.my:8787 + Stickers + Stickers + + + Emoji Emoji @@ -872,17 +928,17 @@ Example: https://server.my:8787 MessageView - + Edit Edit - + React React - + Reply Reply @@ -892,7 +948,7 @@ Example: https://server.my:8787 Options - + &Copy &Copy @@ -1100,7 +1156,7 @@ Example: https://server.my:8787 Placeholder - + unimplemented event: unimplemented event: @@ -1220,7 +1276,7 @@ Example: https://server.my:8787 ReplyPopup - + Close Close @@ -1233,7 +1289,7 @@ Example: https://server.my:8787 RoomInfo - + no version stored no version stored @@ -1241,7 +1297,7 @@ Example: https://server.my:8787 RoomList - + New tag New tag @@ -1250,6 +1306,16 @@ Example: https://server.my:8787 Enter the tag you want to use: Enter the tag you want to use: + + + Leave Room + + + + + Are you sure you want to leave this room? + + Leave room @@ -1281,17 +1347,7 @@ Example: https://server.my:8787 Create new tag... - - Accept - Accept - - - - Decline - Decline - - - + Status Message Status Message @@ -1341,20 +1397,42 @@ Example: https://server.my:8787 User settings + + RoomMembers + + + Members of %1 + Members of %1 + + + + %n people in %1 + Summary above list of members + + %n person in %1 + %n people in %1 + + + + + Invite more people + Invite more people + + RoomSettings - + Room Settings Room Settings - + %1 member(s) %1 member(s) - + SETTINGS SETTINGS @@ -1410,21 +1488,22 @@ Example: https://server.my:8787 Encryption is currently experimental and things might break unexpectedly.<br>Please take note that it can't be disabled afterwards. - - Respond to key requests - Respond to key requests + + Sticker & Emote Settings + - - Whether or not the client should respond automatically with the session keys - upon request. Use with caution, this is a temporary measure to test the - E2E implementation until device verification is completed. - Whether or not the client should respond automatically with the session keys -upon request. Use with caution, this is a temporary measure to test the -E2E implementation until device verification is completed. + + Change + - + + Change what packs are enabled, remove packs or create new ones + + + + INFO INFO @@ -1439,12 +1518,7 @@ E2E implementation until device verification is completed. Room Version - - OK - OK - - - + Failed to enable encryption: %1 Failed to enable encryption: %1 @@ -1475,6 +1549,24 @@ E2E implementation until device verification is completed. Failed to upload image: %s + + RoomlistModel + + + Pending invite. + Pending invite. + + + + Previewing this room + Previewing this room + + + + No preview available + No preview available + + ScreenShare @@ -1532,7 +1624,7 @@ E2E implementation until device verification is completed. StatusIndicator - + Failed Failed @@ -1552,6 +1644,14 @@ E2E implementation until device verification is completed. Read + + StickerPicker + + + Search + Search + + Success @@ -1573,7 +1673,7 @@ E2E implementation until device verification is completed. TimelineModel - + Message redaction failed: %1 Message redaction failed: %1 @@ -1584,7 +1684,7 @@ E2E implementation until device verification is completed. Failed to encrypt event, sending aborted! - + Save image Save image @@ -1718,12 +1818,12 @@ E2E implementation until device verification is completed. %1 redacted their knock. - + You joined this room. You joined this room. - + %1 has changed their avatar and changed their display name to %2. %1 has changed their avatar and changed their display name to %2. @@ -1752,7 +1852,7 @@ E2E implementation until device verification is completed. TimelineRow - + Edited Edited @@ -1760,17 +1860,32 @@ E2E implementation until device verification is completed. TimelineView - + No room open No room open - + %1 member(s) %1 member(s) - + + join the conversation + join the conversation + + + + accept invite + accept invite + + + + decline invite + decline invite + + + Back to room list Back to room list @@ -1778,7 +1893,7 @@ E2E implementation until device verification is completed. TimelineViewManager - + No encrypted private chat found with this user. Create an encrypted private chat with this user and try again. No encrypted private chat found with this user. Create an encrypted private chat with this user and try again. @@ -1786,18 +1901,17 @@ E2E implementation until device verification is completed. TopBar - + Back to room list Back to room list - - + No room selected No room selected - + Room options Room options @@ -1838,7 +1952,7 @@ E2E implementation until device verification is completed. UserProfile - + Global User Profile Global User Profile @@ -1848,7 +1962,7 @@ E2E implementation until device verification is completed. Room User Profile - + Verify Verify @@ -1897,7 +2011,7 @@ E2E implementation until device verification is completed. UserSettings - + Default Default @@ -1906,7 +2020,7 @@ E2E implementation until device verification is completed. UserSettingsPage - + Minimize to tray Minimize to tray @@ -2363,7 +2477,7 @@ This usually causes the application icon in the task bar to animate in some fash Waiting - + Waiting for other party… Waiting for other party… @@ -2414,7 +2528,7 @@ This usually causes the application icon in the task bar to animate in some fash descriptiveTime - + Yesterday Yesterday @@ -2485,19 +2599,6 @@ This usually causes the application icon in the task bar to animate in some fash Open the fallback, follow the steps and confirm after completing them. - - dialogs::InviteUsers - - - Cancel - Cancel - - - - User ID to invite - User ID to invite - - dialogs::JoinRoom @@ -2611,7 +2712,7 @@ Media size: %2 message-description sent: - + You sent an audio clip You sent an audio clip diff --git a/resources/langs/nheko_eo.ts b/resources/langs/nheko_eo.ts index 26a67d49..a77c5c25 100644 --- a/resources/langs/nheko_eo.ts +++ b/resources/langs/nheko_eo.ts @@ -38,7 +38,7 @@ AwaitingVerificationConfirmation - + Awaiting Confirmation Atendante konfirmon @@ -48,7 +48,7 @@ Atendante kontrolon venontan de la alia flanko. - + Cancel Nuligi @@ -125,7 +125,7 @@ ChatPage - + Failed to invite user: %1 Malsukcesis inviti uzanton: %1 @@ -158,12 +158,12 @@ - + Confirm invite Konfirmu inviton - + Do you really want to invite %1 (%2)? Ĉu vi certe volas inviti uzanton %1 (%2)? @@ -228,12 +228,12 @@ Malforbaris uzanton: %1 - + Do you really want to start a private chat with %1? Ĉu vi certe volas komenci privatan babilon kun %1? - + Cache migration failed! Malsukcesis migrado de kaŝmemoro! @@ -353,7 +353,7 @@ CrossSigningSecrets - + Decrypt secrets Malĉifri sekretojn @@ -427,12 +427,12 @@ EmojiPicker - + Search Serĉu - + People Homoj @@ -605,10 +605,48 @@ + + ImagePackSettingsDialog + + + Image pack settings + + + + + Private pack + + + + + Pack from this room + + + + + Globally enabled pack + + + + + Enable globally + + + + + Enables this pack to be used in all rooms + + + + + Close + Fermi + + InputBar - + Select a file Elektu dosieron @@ -618,17 +656,43 @@ Ĉiuj dosieroj (*) - + Failed to upload media. Please try again. Malsukcesis alŝuti vidaŭdaĵojn. Bonvolu reprovi. - InviteeItem + InviteDialog - - Remove - Forigi + + Invite users to %1 + + + + + User ID to invite + + + + + @joe:matrix.org + Example user id. The name 'joe' can be localized however you want. + + + + + Add + + + + + Invite + + + + + Cancel + Nuligi @@ -747,23 +811,10 @@ Ekzemplo: https://servilo.mia:8787 Malsukcesis ununura saluto - - MemberList - - - Room members - Membroj de la ĉambro - - - - OK - Bone - - MessageDelegate - + removed forigita @@ -774,7 +825,7 @@ Ekzemplo: https://servilo.mia:8787 - + room name changed to: %1 Nomo da ĉambro ŝanĝiĝis al: %1 @@ -784,7 +835,7 @@ Ekzemplo: https://servilo.mia:8787 - + topic changed to: %1 @@ -794,17 +845,17 @@ Ekzemplo: https://servilo.mia:8787 - + %1 changed the room avatar - + %1 created and configured room: %2 - + %1 placed a voice call. %1 metis voĉvokon. @@ -819,17 +870,17 @@ Ekzemplo: https://servilo.mia:8787 %1 metis vokon. - + %1 answered the call. %1 respondis la vokon. - + %1 ended the call. %1 finis la vokon. - + Negotiating call... Traktante vokon… @@ -837,7 +888,7 @@ Ekzemplo: https://servilo.mia:8787 MessageInput - + Hang up @@ -858,6 +909,11 @@ Ekzemplo: https://servilo.mia:8787 + Stickers + + + + Emoji Bildosignoj @@ -875,17 +931,17 @@ Ekzemplo: https://servilo.mia:8787 MessageView - + Edit Redakti - + React Reagi - + Reply Respondi @@ -895,7 +951,7 @@ Ekzemplo: https://servilo.mia:8787 Elektebloj - + &Copy @@ -1103,7 +1159,7 @@ Ekzemplo: https://servilo.mia:8787 Placeholder - + unimplemented event: neprogramita okazo: @@ -1223,7 +1279,7 @@ Ekzemplo: https://servilo.mia:8787 ReplyPopup - + Close Fermi @@ -1236,7 +1292,7 @@ Ekzemplo: https://servilo.mia:8787 RoomInfo - + no version stored @@ -1244,7 +1300,7 @@ Ekzemplo: https://servilo.mia:8787 RoomList - + New tag @@ -1253,6 +1309,16 @@ Ekzemplo: https://servilo.mia:8787 Enter the tag you want to use: + + + Leave Room + + + + + Are you sure you want to leave this room? + + Leave room @@ -1284,17 +1350,7 @@ Ekzemplo: https://servilo.mia:8787 - - Accept - Akcepti - - - - Decline - Rifuzi - - - + Status Message @@ -1344,20 +1400,42 @@ Ekzemplo: https://servilo.mia:8787 Agordoj de uzanto + + RoomMembers + + + Members of %1 + + + + + %n people in %1 + Summary above list of members + + + + + + + + Invite more people + + + RoomSettings - + Room Settings Agordoj de ĉambro - + %1 member(s) %1 ĉambrano(j) - + SETTINGS AGORDOJ @@ -1413,19 +1491,22 @@ Ekzemplo: https://servilo.mia:8787 Ĉifrado nun estas eksperimenta kaj povus rompiĝi neatendite.<br>Bonvole sciu, ke ne eblas ĝin malŝalti poste. - - Respond to key requests - Respondi al petoj de ŝlosiloj - - - - Whether or not the client should respond automatically with the session keys - upon request. Use with caution, this is a temporary measure to test the - E2E implementation until device verification is completed. + + Sticker & Emote Settings - + + Change + + + + + Change what packs are enabled, remove packs or create new ones + + + + INFO INFORMOJ @@ -1440,12 +1521,7 @@ Ekzemplo: https://servilo.mia:8787 Versio de ĉambro - - OK - Bone - - - + Failed to enable encryption: %1 Malsukcesis ŝalti ĉifradon: %1 @@ -1476,6 +1552,24 @@ Ekzemplo: https://servilo.mia:8787 Malsukcesis alŝuti bildon: %s + + RoomlistModel + + + Pending invite. + + + + + Previewing this room + + + + + No preview available + + + ScreenShare @@ -1533,7 +1627,7 @@ Ekzemplo: https://servilo.mia:8787 StatusIndicator - + Failed Estas malsukcesa @@ -1554,6 +1648,14 @@ Ekzemplo: https://servilo.mia:8787 Estas legita + + StickerPicker + + + Search + Serĉu + + Success @@ -1575,7 +1677,7 @@ Ekzemplo: https://servilo.mia:8787 TimelineModel - + Message redaction failed: %1 @@ -1586,7 +1688,7 @@ Ekzemplo: https://servilo.mia:8787 - + Save image Konservi bildon @@ -1722,12 +1824,12 @@ Ekzemplo: https://servilo.mia:8787 - + You joined this room. Vi aliĝis ĉi tiun ĉambron. - + %1 has changed their avatar and changed their display name to %2. @@ -1756,7 +1858,7 @@ Ekzemplo: https://servilo.mia:8787 TimelineRow - + Edited Redaktita @@ -1764,17 +1866,32 @@ Ekzemplo: https://servilo.mia:8787 TimelineView - + No room open - + %1 member(s) %1 ĉambrano(j) - + + join the conversation + + + + + accept invite + + + + + decline invite + + + + Back to room list @@ -1782,7 +1899,7 @@ Ekzemplo: https://servilo.mia:8787 TimelineViewManager - + No encrypted private chat found with this user. Create an encrypted private chat with this user and try again. @@ -1790,18 +1907,17 @@ Ekzemplo: https://servilo.mia:8787 TopBar - + Back to room list - - + No room selected - + Room options @@ -1842,7 +1958,7 @@ Ekzemplo: https://servilo.mia:8787 UserProfile - + Global User Profile @@ -1852,7 +1968,7 @@ Ekzemplo: https://servilo.mia:8787 - + Verify @@ -1901,7 +2017,7 @@ Ekzemplo: https://servilo.mia:8787 UserSettings - + Default @@ -1910,7 +2026,7 @@ Ekzemplo: https://servilo.mia:8787 UserSettingsPage - + Minimize to tray @@ -2373,7 +2489,7 @@ This usually causes the application icon in the task bar to animate in some fash Waiting - + Waiting for other party… @@ -2425,7 +2541,7 @@ This usually causes the application icon in the task bar to animate in some fash descriptiveTime - + Yesterday Hieraŭ @@ -2496,19 +2612,6 @@ This usually causes the application icon in the task bar to animate in some fash - - dialogs::InviteUsers - - - Cancel - Nuligi - - - - User ID to invite - - - dialogs::JoinRoom @@ -2620,7 +2723,7 @@ Media size: %2 message-description sent: - + You sent an audio clip diff --git a/resources/langs/nheko_es.ts b/resources/langs/nheko_es.ts index 6318b9c4..922e0500 100644 --- a/resources/langs/nheko_es.ts +++ b/resources/langs/nheko_es.ts @@ -38,7 +38,7 @@ AwaitingVerificationConfirmation - + Awaiting Confirmation Esperando confirmación @@ -48,7 +48,7 @@ Esperando a que la otra parte complete la verificación. - + Cancel Cancelar @@ -125,7 +125,7 @@ ChatPage - + Failed to invite user: %1 No se pudo invitar al usuario: %1 @@ -157,12 +157,12 @@ - + Confirm invite Confirmar invitación - + Do you really want to invite %1 (%2)? @@ -227,12 +227,12 @@ - + Do you really want to start a private chat with %1? - + Cache migration failed! @@ -352,7 +352,7 @@ CrossSigningSecrets - + Decrypt secrets @@ -426,12 +426,12 @@ EmojiPicker - + Search - + People @@ -604,10 +604,48 @@ + + ImagePackSettingsDialog + + + Image pack settings + + + + + Private pack + + + + + Pack from this room + + + + + Globally enabled pack + + + + + Enable globally + + + + + Enables this pack to be used in all rooms + + + + + Close + + + InputBar - + Select a file @@ -617,18 +655,44 @@ - + Failed to upload media. Please try again. - InviteeItem + InviteDialog - - Remove + + Invite users to %1 + + + User ID to invite + + + + + @joe:matrix.org + Example user id. The name 'joe' can be localized however you want. + + + + + Add + + + + + Invite + + + + + Cancel + Cancelar + LoginPage @@ -740,28 +804,15 @@ Example: https://server.my:8787 - - MemberList - - - Room members - - - - - OK - - - MessageDelegate - + Encryption enabled - + room name changed to: %1 @@ -771,7 +822,7 @@ Example: https://server.my:8787 - + topic changed to: %1 @@ -781,17 +832,17 @@ Example: https://server.my:8787 - + %1 changed the room avatar - + %1 created and configured room: %2 - + %1 placed a voice call. @@ -806,23 +857,23 @@ Example: https://server.my:8787 - + Negotiating call... - + %1 answered the call. - + removed - + %1 ended the call. @@ -830,7 +881,7 @@ Example: https://server.my:8787 MessageInput - + Hang up @@ -851,6 +902,11 @@ Example: https://server.my:8787 + Stickers + + + + Emoji @@ -868,17 +924,17 @@ Example: https://server.my:8787 MessageView - + Edit - + React - + Reply @@ -888,7 +944,7 @@ Example: https://server.my:8787 - + &Copy @@ -1096,7 +1152,7 @@ Example: https://server.my:8787 Placeholder - + unimplemented event: @@ -1216,7 +1272,7 @@ Example: https://server.my:8787 ReplyPopup - + Close @@ -1229,7 +1285,7 @@ Example: https://server.my:8787 RoomInfo - + no version stored @@ -1237,7 +1293,7 @@ Example: https://server.my:8787 RoomList - + New tag @@ -1246,6 +1302,16 @@ Example: https://server.my:8787 Enter the tag you want to use: + + + Leave Room + + + + + Are you sure you want to leave this room? + + Leave room @@ -1277,17 +1343,7 @@ Example: https://server.my:8787 - - Accept - Aceptar - - - - Decline - Rechazar - - - + Status Message @@ -1337,20 +1393,42 @@ Example: https://server.my:8787 + + RoomMembers + + + Members of %1 + + + + + %n people in %1 + Summary above list of members + + + + + + + + Invite more people + + + RoomSettings - + Room Settings - + %1 member(s) - + SETTINGS @@ -1406,19 +1484,22 @@ Example: https://server.my:8787 - - Respond to key requests + + Sticker & Emote Settings - - Whether or not the client should respond automatically with the session keys - upon request. Use with caution, this is a temporary measure to test the - E2E implementation until device verification is completed. + + Change - + + Change what packs are enabled, remove packs or create new ones + + + + INFO @@ -1433,12 +1514,7 @@ Example: https://server.my:8787 - - OK - - - - + Failed to enable encryption: %1 @@ -1469,6 +1545,24 @@ Example: https://server.my:8787 + + RoomlistModel + + + Pending invite. + + + + + Previewing this room + + + + + No preview available + + + ScreenShare @@ -1526,7 +1620,7 @@ Example: https://server.my:8787 StatusIndicator - + Failed @@ -1546,6 +1640,14 @@ Example: https://server.my:8787 + + StickerPicker + + + Search + + + Success @@ -1567,7 +1669,7 @@ Example: https://server.my:8787 TimelineModel - + Message redaction failed: %1 @@ -1578,7 +1680,7 @@ Example: https://server.my:8787 - + Save image @@ -1722,12 +1824,12 @@ Example: https://server.my:8787 - + You joined this room. Te has unido a esta sala. - + Rejected the knock from %1. @@ -1746,7 +1848,7 @@ Example: https://server.my:8787 TimelineRow - + Edited @@ -1754,17 +1856,32 @@ Example: https://server.my:8787 TimelineView - + No room open - + %1 member(s) - + + join the conversation + + + + + accept invite + + + + + decline invite + + + + Back to room list @@ -1772,7 +1889,7 @@ Example: https://server.my:8787 TimelineViewManager - + No encrypted private chat found with this user. Create an encrypted private chat with this user and try again. @@ -1780,18 +1897,17 @@ Example: https://server.my:8787 TopBar - + Back to room list - - + No room selected - + Room options @@ -1832,7 +1948,7 @@ Example: https://server.my:8787 UserProfile - + Global User Profile @@ -1842,7 +1958,7 @@ Example: https://server.my:8787 - + Verify @@ -1891,7 +2007,7 @@ Example: https://server.my:8787 UserSettings - + Default @@ -1900,7 +2016,7 @@ Example: https://server.my:8787 UserSettingsPage - + Minimize to tray @@ -2346,7 +2462,7 @@ This usually causes the application icon in the task bar to animate in some fash Waiting - + Waiting for other party… @@ -2397,7 +2513,7 @@ This usually causes the application icon in the task bar to animate in some fash descriptiveTime - + Yesterday @@ -2468,19 +2584,6 @@ This usually causes the application icon in the task bar to animate in some fash - - dialogs::InviteUsers - - - Cancel - Cancelar - - - - User ID to invite - - - dialogs::JoinRoom @@ -2592,7 +2695,7 @@ Media size: %2 message-description sent: - + You sent an audio clip diff --git a/resources/langs/nheko_et.ts b/resources/langs/nheko_et.ts index 198ec332..57eac1c3 100644 --- a/resources/langs/nheko_et.ts +++ b/resources/langs/nheko_et.ts @@ -38,7 +38,7 @@ AwaitingVerificationConfirmation - + Awaiting Confirmation Ootan kinnitust @@ -48,7 +48,7 @@ Ootan et teine osapool lõpetaks verifitseerimise. - + Cancel Katkesta @@ -125,7 +125,7 @@ ChatPage - + Failed to invite user: %1 Kutse saatmine kasutajale ei õnnestunud: %1 @@ -157,12 +157,12 @@ - + Confirm invite Kinnita kutse - + Do you really want to invite %1 (%2)? Kas sa tõesti soovid saata kutset kasutajale %1 (%2)? @@ -227,12 +227,12 @@ Suhtluskeeld eemaldatud: %1 - + Do you really want to start a private chat with %1? Kas sa kindlasti soovid alustada otsevestlust kasutajaga %1? - + Cache migration failed! Puhvri versiooniuuendus ebaõnnestus! @@ -352,7 +352,7 @@ CrossSigningSecrets - + Decrypt secrets Dekrüpti andmed @@ -426,12 +426,12 @@ EmojiPicker - + Search Otsi - + People Inimesed @@ -604,10 +604,48 @@ Suuna sõnum edasi + + ImagePackSettingsDialog + + + Image pack settings + + + + + Private pack + + + + + Pack from this room + + + + + Globally enabled pack + + + + + Enable globally + + + + + Enables this pack to be used in all rooms + + + + + Close + Sulge + + InputBar - + Select a file Vali fail @@ -617,17 +655,43 @@ Kõik failid (*) - + Failed to upload media. Please try again. Meediafailide üleslaadimine ei õnnestunud. Palun proovi uuesti. - InviteeItem + InviteDialog - - Remove - Eemalda + + Invite users to %1 + + + + + User ID to invite + Kasutajatunnus, kellele soovid kutset saata + + + + @joe:matrix.org + Example user id. The name 'joe' can be localized however you want. + + + + + Add + + + + + Invite + + + + + Cancel + @@ -744,28 +808,15 @@ Näiteks: https://server.minu:8787 Ühekordne sisselogimine ei õnnestunud - - MemberList - - - Room members - Jututoa liikmed - - - - OK - Sobib - - MessageDelegate - + Encryption enabled Krüptimine on kasutusel - + room name changed to: %1 jututoa uus nimi on: %1 @@ -775,7 +826,7 @@ Näiteks: https://server.minu:8787 eemaldas jututoa nime - + topic changed to: %1 jututoa uus teema on: %1 @@ -785,17 +836,17 @@ Näiteks: https://server.minu:8787 teema on eemaldatud - + %1 changed the room avatar %1 muutis jututoa tunnuspilti - + %1 created and configured room: %2 %1 lõi ja seadistas jututoa: %2 - + %1 placed a voice call. %1 helistas. @@ -810,23 +861,23 @@ Näiteks: https://server.minu:8787 %1 helistas. - + Negotiating call... Ühendan kõnet… - + %1 answered the call. %1 vastas kõnele. - + removed eemaldatud - + %1 ended the call. %1 lõpetas kõne. @@ -834,7 +885,7 @@ Näiteks: https://server.minu:8787 MessageInput - + Hang up Lõpeta kõne @@ -855,6 +906,11 @@ Näiteks: https://server.minu:8787 + Stickers + + + + Emoji Emoji @@ -872,17 +928,17 @@ Näiteks: https://server.minu:8787 MessageView - + Edit Muuda - + React Reageeri - + Reply Vasta @@ -892,7 +948,7 @@ Näiteks: https://server.minu:8787 Valikud - + &Copy &Kopeeri @@ -1100,7 +1156,7 @@ Näiteks: https://server.minu:8787 Placeholder - + unimplemented event: implementeerimata sündmus: @@ -1220,7 +1276,7 @@ Näiteks: https://server.minu:8787 ReplyPopup - + Close Sulge @@ -1233,7 +1289,7 @@ Näiteks: https://server.minu:8787 RoomInfo - + no version stored salvestatud versiooni ei leidu @@ -1241,7 +1297,7 @@ Näiteks: https://server.minu:8787 RoomList - + New tag Uus silt @@ -1250,6 +1306,16 @@ Näiteks: https://server.minu:8787 Enter the tag you want to use: Kirjuta silt, mida soovid kasutada: + + + Leave Room + + + + + Are you sure you want to leave this room? + + Leave room @@ -1281,17 +1347,7 @@ Näiteks: https://server.minu:8787 Loo uus silt... - - Accept - Nõustu - - - - Decline - Keeldu - - - + Status Message Olekuteade @@ -1341,20 +1397,42 @@ Näiteks: https://server.minu:8787 Kasutaja seadistused + + RoomMembers + + + Members of %1 + + + + + %n people in %1 + Summary above list of members + + + + + + + + Invite more people + + + RoomSettings - + Room Settings Jututoa seadistused - + %1 member(s) %1 liige(t) - + SETTINGS SEADISTUSED @@ -1410,21 +1488,22 @@ Näiteks: https://server.minu:8787 Krüptimine on nhekos hetkel veel katseline ning nii mõndagi võib ootamatult katki minna. <br>Palun arvesta, et krüptimist ei saa hiljem enam välja lülitada. - - Respond to key requests - Vasta krüptovõtmete päringutele + + Sticker & Emote Settings + - - Whether or not the client should respond automatically with the session keys - upon request. Use with caution, this is a temporary measure to test the - E2E implementation until device verification is completed. - Kas klient peaks automaatselt vastama või mitte vastama sessioonivõtmete päringule. -Kasuta seda võimalust ettevaatlikult. Tegemist on ajutise lahendusega läbiva krüptimise -testimiseks seni, kuni terviklik seadmete verifitseerimine on implementeeritud. + + Change + - + + Change what packs are enabled, remove packs or create new ones + + + + INFO TEAVE @@ -1439,12 +1518,7 @@ testimiseks seni, kuni terviklik seadmete verifitseerimine on implementeeritud.< Jututoa versioon - - OK - Sobib - - - + Failed to enable encryption: %1 Krüptimise kasutuselevõtmine ei õnnestunud: %1 @@ -1475,6 +1549,24 @@ testimiseks seni, kuni terviklik seadmete verifitseerimine on implementeeritud.< Viga faili üleslaadimisel: %1 + + RoomlistModel + + + Pending invite. + + + + + Previewing this room + + + + + No preview available + + + ScreenShare @@ -1532,7 +1624,7 @@ testimiseks seni, kuni terviklik seadmete verifitseerimine on implementeeritud.< StatusIndicator - + Failed Ebaõnnestus @@ -1552,6 +1644,14 @@ testimiseks seni, kuni terviklik seadmete verifitseerimine on implementeeritud.< Loetud + + StickerPicker + + + Search + Otsi + + Success @@ -1573,7 +1673,7 @@ testimiseks seni, kuni terviklik seadmete verifitseerimine on implementeeritud.< TimelineModel - + Message redaction failed: %1 Sõnumi ümbersõnastamine ebaõnnestus: %1 @@ -1584,7 +1684,7 @@ testimiseks seni, kuni terviklik seadmete verifitseerimine on implementeeritud.< Sündmuse krüptimine ei õnnestunud, katkestame saatmise! - + Save image Salvesta pilt @@ -1718,12 +1818,12 @@ testimiseks seni, kuni terviklik seadmete verifitseerimine on implementeeritud.< %1 muutis oma koputust jututoa uksele. - + You joined this room. Sa liitusid jututoaga. - + %1 has changed their avatar and changed their display name to %2. %1 muutis oma tunnuspilti ja seadistas uueks kuvatavaks nimeks %2. @@ -1752,7 +1852,7 @@ testimiseks seni, kuni terviklik seadmete verifitseerimine on implementeeritud.< TimelineRow - + Edited Muudetud @@ -1760,17 +1860,32 @@ testimiseks seni, kuni terviklik seadmete verifitseerimine on implementeeritud.< TimelineView - + No room open Ühtegi jututuba pole avatud - + %1 member(s) %1 liige(t) - + + join the conversation + + + + + accept invite + + + + + decline invite + + + + Back to room list Tagasi jututubade loendisse @@ -1778,7 +1893,7 @@ testimiseks seni, kuni terviklik seadmete verifitseerimine on implementeeritud.< TimelineViewManager - + No encrypted private chat found with this user. Create an encrypted private chat with this user and try again. Ühtegi krüptitud vestlust selle kasutajaga ei leidunud. Palun loo temaga krüptitud vestlus ja proovi uuesti. @@ -1786,18 +1901,17 @@ testimiseks seni, kuni terviklik seadmete verifitseerimine on implementeeritud.< TopBar - + Back to room list Tagasi jututubade loendisse - - + No room selected Jututuba on valimata - + Room options Jututoa valikud @@ -1838,7 +1952,7 @@ testimiseks seni, kuni terviklik seadmete verifitseerimine on implementeeritud.< UserProfile - + Global User Profile Üldine kasutajaprofiil @@ -1848,7 +1962,7 @@ testimiseks seni, kuni terviklik seadmete verifitseerimine on implementeeritud.< Kasutajaprofiil jututoas - + Verify Verifitseeri @@ -1897,7 +2011,7 @@ testimiseks seni, kuni terviklik seadmete verifitseerimine on implementeeritud.< UserSettings - + Default Vaikimisi @@ -1906,7 +2020,7 @@ testimiseks seni, kuni terviklik seadmete verifitseerimine on implementeeritud.< UserSettingsPage - + Minimize to tray Vähenda tegumiribale @@ -2363,7 +2477,7 @@ See tavaliselt tähendab, et rakenduse ikoon tegumiribal annab mingit sorti anim Waiting - + Waiting for other party… Ootan teise osapoole tegevust… @@ -2414,7 +2528,7 @@ See tavaliselt tähendab, et rakenduse ikoon tegumiribal annab mingit sorti anim descriptiveTime - + Yesterday Eile @@ -2485,19 +2599,6 @@ See tavaliselt tähendab, et rakenduse ikoon tegumiribal annab mingit sorti anim Ava kasutaja registreerimise tagavaravariant, läbi kõik sammud ja kinnita seda, kui kõik valmis on. - - dialogs::InviteUsers - - - Cancel - Tühista - - - - User ID to invite - Kasutajatunnus, kellele soovid kutset saata - - dialogs::JoinRoom @@ -2611,7 +2712,7 @@ Meedia suurus: %2 message-description sent: - + You sent an audio clip Sa saatsid helifaili diff --git a/resources/langs/nheko_fi.ts b/resources/langs/nheko_fi.ts index 9c48e98f..422c2957 100644 --- a/resources/langs/nheko_fi.ts +++ b/resources/langs/nheko_fi.ts @@ -38,7 +38,7 @@ AwaitingVerificationConfirmation - + Awaiting Confirmation Odotetaan vahvistusta @@ -48,7 +48,7 @@ - + Cancel Peruuta @@ -125,7 +125,7 @@ ChatPage - + Failed to invite user: %1 @@ -157,12 +157,12 @@ - + Confirm invite - + Do you really want to invite %1 (%2)? @@ -227,12 +227,12 @@ - + Do you really want to start a private chat with %1? - + Cache migration failed! @@ -352,7 +352,7 @@ CrossSigningSecrets - + Decrypt secrets @@ -426,12 +426,12 @@ EmojiPicker - + Search Hae - + People Ihmiset @@ -604,10 +604,48 @@ Välitä viesti + + ImagePackSettingsDialog + + + Image pack settings + + + + + Private pack + + + + + Pack from this room + + + + + Globally enabled pack + + + + + Enable globally + + + + + Enables this pack to be used in all rooms + + + + + Close + Sulje + + InputBar - + Select a file Valitse tiedosto @@ -617,17 +655,43 @@ Kaikki Tiedostot (*) - + Failed to upload media. Please try again. - InviteeItem + InviteDialog - - Remove - Poista + + Invite users to %1 + + + + + User ID to invite + Käyttäjätunnus kutsuttavaksi + + + + @joe:matrix.org + Example user id. The name 'joe' can be localized however you want. + + + + + Add + + + + + Invite + + + + + Cancel + Peruuta @@ -740,23 +804,10 @@ Example: https://server.my:8787 - - MemberList - - - Room members - Huoneen jäsenet - - - - OK - OK - - MessageDelegate - + removed @@ -767,7 +818,7 @@ Example: https://server.my:8787 Salaus on käytössä - + room name changed to: %1 huoneen nimi muutettu: %1 @@ -777,7 +828,7 @@ Example: https://server.my:8787 - + topic changed to: %1 @@ -787,17 +838,17 @@ Example: https://server.my:8787 - + %1 changed the room avatar - + %1 created and configured room: %2 - + %1 placed a voice call. @@ -812,17 +863,17 @@ Example: https://server.my:8787 %1 soitti puhelun. - + %1 answered the call. %1 vastasi puheluun. - + %1 ended the call. - + Negotiating call... @@ -830,7 +881,7 @@ Example: https://server.my:8787 MessageInput - + Hang up @@ -851,6 +902,11 @@ Example: https://server.my:8787 + Stickers + + + + Emoji Emoji @@ -868,17 +924,17 @@ Example: https://server.my:8787 MessageView - + Edit Muokkaa - + React Reagoi - + Reply Vastaa @@ -888,7 +944,7 @@ Example: https://server.my:8787 Asetukset - + &Copy @@ -1096,7 +1152,7 @@ Example: https://server.my:8787 Placeholder - + unimplemented event: @@ -1216,7 +1272,7 @@ Example: https://server.my:8787 ReplyPopup - + Close Sulje @@ -1229,7 +1285,7 @@ Example: https://server.my:8787 RoomInfo - + no version stored ei tallennettua versiota @@ -1237,7 +1293,7 @@ Example: https://server.my:8787 RoomList - + New tag @@ -1246,6 +1302,16 @@ Example: https://server.my:8787 Enter the tag you want to use: + + + Leave Room + + + + + Are you sure you want to leave this room? + + Leave room @@ -1277,17 +1343,7 @@ Example: https://server.my:8787 - - Accept - Hyväksy - - - - Decline - Hylkää - - - + Status Message @@ -1337,20 +1393,42 @@ Example: https://server.my:8787 Käyttäjäasetukset + + RoomMembers + + + Members of %1 + + + + + %n people in %1 + Summary above list of members + + + + + + + + Invite more people + + + RoomSettings - + Room Settings - + %1 member(s) - + SETTINGS @@ -1406,19 +1484,22 @@ Example: https://server.my:8787 - - Respond to key requests + + Sticker & Emote Settings - - Whether or not the client should respond automatically with the session keys - upon request. Use with caution, this is a temporary measure to test the - E2E implementation until device verification is completed. + + Change - + + Change what packs are enabled, remove packs or create new ones + + + + INFO @@ -1433,12 +1514,7 @@ Example: https://server.my:8787 Huoneen versio - - OK - OK - - - + Failed to enable encryption: %1 Salauksen aktivointi epäonnistui: %1 @@ -1469,6 +1545,24 @@ Example: https://server.my:8787 Kuvan lähetys epäonnistui: %s + + RoomlistModel + + + Pending invite. + + + + + Previewing this room + + + + + No preview available + + + ScreenShare @@ -1526,7 +1620,7 @@ Example: https://server.my:8787 StatusIndicator - + Failed @@ -1546,6 +1640,14 @@ Example: https://server.my:8787 + + StickerPicker + + + Search + Hae + + Success @@ -1567,7 +1669,7 @@ Example: https://server.my:8787 TimelineModel - + Message redaction failed: %1 Viestin muokkaus epäonnistui: %1 @@ -1578,7 +1680,7 @@ Example: https://server.my:8787 - + Save image Tallenna kuva @@ -1712,12 +1814,12 @@ Example: https://server.my:8787 - + You joined this room. Sinä liityit tähän huoneeseen. - + %1 has changed their avatar and changed their display name to %2. @@ -1746,7 +1848,7 @@ Example: https://server.my:8787 TimelineRow - + Edited Muokattu @@ -1754,17 +1856,32 @@ Example: https://server.my:8787 TimelineView - + No room open - + %1 member(s) - + + join the conversation + + + + + accept invite + + + + + decline invite + + + + Back to room list @@ -1772,7 +1889,7 @@ Example: https://server.my:8787 TimelineViewManager - + No encrypted private chat found with this user. Create an encrypted private chat with this user and try again. @@ -1780,18 +1897,17 @@ Example: https://server.my:8787 TopBar - + Back to room list - - + No room selected - + Room options Huoneen asetukset @@ -1832,7 +1948,7 @@ Example: https://server.my:8787 UserProfile - + Global User Profile @@ -1842,7 +1958,7 @@ Example: https://server.my:8787 - + Verify @@ -1891,7 +2007,7 @@ Example: https://server.my:8787 UserSettings - + Default @@ -1900,7 +2016,7 @@ Example: https://server.my:8787 UserSettingsPage - + Minimize to tray Pienennä ilmoitusalueelle @@ -2346,7 +2462,7 @@ This usually causes the application icon in the task bar to animate in some fash Waiting - + Waiting for other party… @@ -2397,7 +2513,7 @@ This usually causes the application icon in the task bar to animate in some fash descriptiveTime - + Yesterday Eilen @@ -2468,19 +2584,6 @@ This usually causes the application icon in the task bar to animate in some fash - - dialogs::InviteUsers - - - Cancel - Peruuta - - - - User ID to invite - Käyttäjätunnus kutsuttavaksi - - dialogs::JoinRoom @@ -2594,7 +2697,7 @@ Median koko: %2 message-description sent: - + You sent an audio clip Lähetit äänileikkeen diff --git a/resources/langs/nheko_fr.ts b/resources/langs/nheko_fr.ts index b6345d62..c6d42299 100644 --- a/resources/langs/nheko_fr.ts +++ b/resources/langs/nheko_fr.ts @@ -38,7 +38,7 @@ AwaitingVerificationConfirmation - + Awaiting Confirmation Attente de confirmation @@ -48,7 +48,7 @@ Attente de la vérification par le correspondant. - + Cancel Annuler @@ -125,7 +125,7 @@ ChatPage - + Failed to invite user: %1 Échec lors de l'invitation de %1 @@ -157,12 +157,12 @@ - + Confirm invite Confirmer l'invitation - + Do you really want to invite %1 (%2)? Voulez-vous vraiment inviter %1 (%2) ? @@ -227,12 +227,12 @@ %1 n'est plus banni(e) - + Do you really want to start a private chat with %1? Voulez-vous vraimer commencer une discussion privée avec %1 ? - + Cache migration failed! Échec de la migration du cache ! @@ -352,7 +352,7 @@ CrossSigningSecrets - + Decrypt secrets Déchiffrer les secrets @@ -426,12 +426,12 @@ EmojiPicker - + Search Chercher - + People Personnes @@ -604,10 +604,48 @@ + + ImagePackSettingsDialog + + + Image pack settings + + + + + Private pack + + + + + Pack from this room + + + + + Globally enabled pack + + + + + Enable globally + + + + + Enables this pack to be used in all rooms + + + + + Close + Fermer + + InputBar - + Select a file Sélectionnez un fichier @@ -617,17 +655,43 @@ Tous les types de fichiers (*) - + Failed to upload media. Please try again. Échec de l'envoi du média. Veuillez réessayer. - InviteeItem + InviteDialog - - Remove - Retirer + + Invite users to %1 + + + + + User ID to invite + Identifiant d'utilisateur à inviter + + + + @joe:matrix.org + Example user id. The name 'joe' can be localized however you want. + + + + + Add + + + + + Invite + + + + + Cancel + Annuler @@ -744,23 +808,10 @@ Exemple : https ://monserveur.example.com :8787 Échec de la connexion SSO - - MemberList - - - Room members - Membres du salon - - - - OK - OK - - MessageDelegate - + removed retiré @@ -771,7 +822,7 @@ Exemple : https ://monserveur.example.com :8787 Chiffrement activé - + room name changed to: %1 nom du salon changé en : %1 @@ -781,7 +832,7 @@ Exemple : https ://monserveur.example.com :8787 nom du salon retiré - + topic changed to: %1 sujet changé pour : %1 @@ -791,17 +842,17 @@ Exemple : https ://monserveur.example.com :8787 sujet retiré - + %1 changed the room avatar - + %1 created and configured room: %2 %1 a créé et configuré le salon : %2 - + %1 placed a voice call. %1 a effectué un appel vocal. @@ -816,17 +867,17 @@ Exemple : https ://monserveur.example.com :8787 %1 a appelé. - + %1 answered the call. %1 a répondu à l'appel. - + %1 ended the call. %1 a terminé l'appel. - + Negotiating call... Négociation de l'appel… @@ -834,7 +885,7 @@ Exemple : https ://monserveur.example.com :8787 MessageInput - + Hang up Raccrocher @@ -855,6 +906,11 @@ Exemple : https ://monserveur.example.com :8787 + Stickers + + + + Emoji Émoji @@ -872,17 +928,17 @@ Exemple : https ://monserveur.example.com :8787 MessageView - + Edit Modifier - + React Réagir - + Reply Répondre @@ -892,7 +948,7 @@ Exemple : https ://monserveur.example.com :8787 Options - + &Copy @@ -1100,7 +1156,7 @@ Exemple : https ://monserveur.example.com :8787 Placeholder - + unimplemented event: Évènement non implémenté : @@ -1220,7 +1276,7 @@ Exemple : https ://monserveur.example.com :8787 ReplyPopup - + Close Fermer @@ -1233,7 +1289,7 @@ Exemple : https ://monserveur.example.com :8787 RoomInfo - + no version stored pas de version enregistrée @@ -1241,7 +1297,7 @@ Exemple : https ://monserveur.example.com :8787 RoomList - + New tag @@ -1250,6 +1306,16 @@ Exemple : https ://monserveur.example.com :8787 Enter the tag you want to use: + + + Leave Room + + + + + Are you sure you want to leave this room? + + Leave room @@ -1281,17 +1347,7 @@ Exemple : https ://monserveur.example.com :8787 - - Accept - - - - - Decline - - - - + Status Message @@ -1341,20 +1397,42 @@ Exemple : https ://monserveur.example.com :8787 Paramètres utilisateur + + RoomMembers + + + Members of %1 + + + + + %n people in %1 + Summary above list of members + + + + + + + + Invite more people + + + RoomSettings - + Room Settings Configuration du salon - + %1 member(s) %1 membre(s) - + SETTINGS CONFIGURATION @@ -1410,21 +1488,22 @@ Exemple : https ://monserveur.example.com :8787 Le chiffrement actuellement expérimental et des comportements inattendus peuvent être rencontrés. <br>Veuillez noter qu'il n'est pas possible de le désactiver par la suite. - - Respond to key requests - Répondre aux demandes de clé + + Sticker & Emote Settings + - - Whether or not the client should respond automatically with the session keys - upon request. Use with caution, this is a temporary measure to test the - E2E implementation until device verification is completed. - Si le client doit répondre automatiquement avec les clés de session lorsqu'elles sont -demandées. Utiliser précautionneusement, il s'agit d'une mesure temporaire afin de -tester le chiffrement de bout en bout tant que la vérification des appareils n'est pas au point. + + Change + - + + Change what packs are enabled, remove packs or create new ones + + + + INFO INFO @@ -1439,12 +1518,7 @@ tester le chiffrement de bout en bout tant que la vérification des appareils n& Version du salon - - OK - OK - - - + Failed to enable encryption: %1 Échec de l'activation du chiffrement  : %1 @@ -1475,6 +1549,24 @@ tester le chiffrement de bout en bout tant que la vérification des appareils n& Échec de l'envoi de l'image  : %s + + RoomlistModel + + + Pending invite. + + + + + Previewing this room + + + + + No preview available + + + ScreenShare @@ -1532,7 +1624,7 @@ tester le chiffrement de bout en bout tant que la vérification des appareils n& StatusIndicator - + Failed Échec @@ -1552,6 +1644,14 @@ tester le chiffrement de bout en bout tant que la vérification des appareils n& Lu + + StickerPicker + + + Search + Chercher + + Success @@ -1573,7 +1673,7 @@ tester le chiffrement de bout en bout tant que la vérification des appareils n& TimelineModel - + Message redaction failed: %1 Échec de la suppression du message : %1 @@ -1584,7 +1684,7 @@ tester le chiffrement de bout en bout tant que la vérification des appareils n& Échec du chiffrement de l'évènement, envoi abandonné ! - + Save image Enregistrer l'image @@ -1718,12 +1818,12 @@ tester le chiffrement de bout en bout tant que la vérification des appareils n& %1 ne frappe plus au salon. - + You joined this room. Vous avez rejoint ce salon. - + %1 has changed their avatar and changed their display name to %2. @@ -1752,7 +1852,7 @@ tester le chiffrement de bout en bout tant que la vérification des appareils n& TimelineRow - + Edited Modifié @@ -1760,17 +1860,32 @@ tester le chiffrement de bout en bout tant que la vérification des appareils n& TimelineView - + No room open Aucun salon ouvert - + %1 member(s) %1 membre(s) - + + join the conversation + + + + + accept invite + + + + + decline invite + + + + Back to room list Revenir à la liste des salons @@ -1778,7 +1893,7 @@ tester le chiffrement de bout en bout tant que la vérification des appareils n& TimelineViewManager - + No encrypted private chat found with this user. Create an encrypted private chat with this user and try again. Pas de discussion privée et chiffrée trouvée avec cet utilisateur. Créez-en une et réessayez. @@ -1786,18 +1901,17 @@ tester le chiffrement de bout en bout tant que la vérification des appareils n& TopBar - + Back to room list Revenir à la liste des salons - - + No room selected Pas de salon sélectionné - + Room options Options du salon @@ -1838,7 +1952,7 @@ tester le chiffrement de bout en bout tant que la vérification des appareils n& UserProfile - + Global User Profile Profil général de l'utilisateur @@ -1848,7 +1962,7 @@ tester le chiffrement de bout en bout tant que la vérification des appareils n& Profil utilisateur spécifique au salon - + Verify Vérifier @@ -1897,7 +2011,7 @@ tester le chiffrement de bout en bout tant que la vérification des appareils n& UserSettings - + Default Défaut @@ -1906,7 +2020,7 @@ tester le chiffrement de bout en bout tant que la vérification des appareils n& UserSettingsPage - + Minimize to tray Réduire à la barre des tâches @@ -2365,7 +2479,7 @@ Cela met l'application en évidence dans la barre des tâches. Waiting - + Waiting for other party… Attente du correspondant… @@ -2416,7 +2530,7 @@ Cela met l'application en évidence dans la barre des tâches. descriptiveTime - + Yesterday Hier @@ -2487,19 +2601,6 @@ Cela met l'application en évidence dans la barre des tâches.Ouvrez la solution de repli, suivez les étapes et confirmez après les avoir terminées. - - dialogs::InviteUsers - - - Cancel - Annuler - - - - User ID to invite - Identifiant d'utilisateur à inviter - - dialogs::JoinRoom @@ -2613,7 +2714,7 @@ Taille du média : %2 message-description sent: - + You sent an audio clip Vous avez envoyé un message audio diff --git a/resources/langs/nheko_hu.ts b/resources/langs/nheko_hu.ts index e989a6ce..7c29338c 100644 --- a/resources/langs/nheko_hu.ts +++ b/resources/langs/nheko_hu.ts @@ -38,7 +38,7 @@ AwaitingVerificationConfirmation - + Awaiting Confirmation Várakozás megerősítésre @@ -48,7 +48,7 @@ Várakozás a másik oldalra a hitelesítés befejezéséhez. - + Cancel Mégse @@ -125,7 +125,7 @@ ChatPage - + Failed to invite user: %1 Nem sikerült meghívni a felhasználót: %1 @@ -157,12 +157,12 @@ - + Confirm invite Meghívás megerősítése - + Do you really want to invite %1 (%2)? Biztos, hogy meg akarod hívni a következő felhasználót: %1 (%2)? @@ -227,12 +227,12 @@ Kitiltás feloldva a felhasználónak: %1 - + Do you really want to start a private chat with %1? Biztosan privát csevegést akarsz indítani %1 felhasználóval? - + Cache migration failed! Gyorsítótár migráció nem sikerült! @@ -352,7 +352,7 @@ CrossSigningSecrets - + Decrypt secrets Titkos tároló feloldása @@ -426,12 +426,12 @@ EmojiPicker - + Search Keresés - + People Emberek @@ -604,10 +604,48 @@ + + ImagePackSettingsDialog + + + Image pack settings + + + + + Private pack + + + + + Pack from this room + + + + + Globally enabled pack + + + + + Enable globally + + + + + Enables this pack to be used in all rooms + + + + + Close + Bezárás + + InputBar - + Select a file Fájl kiválasztása @@ -617,17 +655,43 @@ Minden fájl (*) - + Failed to upload media. Please try again. Nem sikerült feltölteni a médiafájlt. Kérlek, próbáld újra! - InviteeItem + InviteDialog - - Remove - Eltávolítás + + Invite users to %1 + + + + + User ID to invite + Meghívandó felhasználó azonosítója + + + + @joe:matrix.org + Example user id. The name 'joe' can be localized however you want. + + + + + Add + + + + + Invite + + + + + Cancel + Mégse @@ -744,28 +808,15 @@ Példa: https://szerver.em:8787 SSO bejelentkezés nem sikerült - - MemberList - - - Room members - Szobatagok - - - - OK - OK - - MessageDelegate - + Encryption enabled Titkosítás bekapcsolva - + room name changed to: %1 a szoba neve megváltoztatva erre: %1 @@ -775,7 +826,7 @@ Példa: https://szerver.em:8787 szobanév eltávolítva - + topic changed to: %1 a téma megváltoztatva erre: %1 @@ -785,17 +836,17 @@ Példa: https://szerver.em:8787 téma eltávolítva - + %1 changed the room avatar - + %1 created and configured room: %2 %1 létrehozta és beállította a következő szobát: %2 - + %1 placed a voice call. %1 hanghívást kezdeményezett. @@ -810,23 +861,23 @@ Példa: https://szerver.em:8787 %1 hívást kezdeményezett. - + Negotiating call... Hívás előkészítése… - + %1 answered the call. %1 fogadta a hívást. - + removed eltávolítva - + %1 ended the call. %1 befejezte a hívást. @@ -834,7 +885,7 @@ Példa: https://szerver.em:8787 MessageInput - + Hang up Hívás befejezése @@ -855,6 +906,11 @@ Példa: https://szerver.em:8787 + Stickers + + + + Emoji Hangulatjelek @@ -872,17 +928,17 @@ Példa: https://szerver.em:8787 MessageView - + Edit Szerkesztés - + React Reakció - + Reply Válasz @@ -892,7 +948,7 @@ Példa: https://szerver.em:8787 Műveletek - + &Copy @@ -1100,7 +1156,7 @@ Példa: https://szerver.em:8787 Placeholder - + unimplemented event: nem implementált esemény: @@ -1220,7 +1276,7 @@ Példa: https://szerver.em:8787 ReplyPopup - + Close Bezárás @@ -1233,7 +1289,7 @@ Példa: https://szerver.em:8787 RoomInfo - + no version stored nincs tárolva verzió @@ -1241,7 +1297,7 @@ Példa: https://szerver.em:8787 RoomList - + New tag @@ -1250,6 +1306,16 @@ Példa: https://szerver.em:8787 Enter the tag you want to use: + + + Leave Room + + + + + Are you sure you want to leave this room? + + Leave room @@ -1281,17 +1347,7 @@ Példa: https://szerver.em:8787 - - Accept - Elfogadás - - - - Decline - Elutasítás - - - + Status Message @@ -1341,20 +1397,41 @@ Példa: https://szerver.em:8787 Felhasználói beállítások + + RoomMembers + + + Members of %1 + + + + + %n people in %1 + Summary above list of members + + + + + + + Invite more people + + + RoomSettings - + Room Settings Szobabeállítások - + %1 member(s) %1 tag - + SETTINGS BEÁLLÍTÁSOK @@ -1410,21 +1487,22 @@ Példa: https://szerver.em:8787 A titkosítás jelenleg kísérleti stádiumú és váratlan furcsaságok történhetnek.<br>Kérlek, vedd vigyelembe, hogy ha egyszer aktiváltad, nem lehet utána kikapcsolni. - - Respond to key requests - Válasz kulcskérelmekre + + Sticker & Emote Settings + - - Whether or not the client should respond automatically with the session keys - upon request. Use with caution, this is a temporary measure to test the - E2E implementation until device verification is completed. - Válaszoljon-e kérés esetén a kliens munkamenetkulcsokkal automatikusan. -Óvatosan használandó, mivel ez csak egy átmeneti megoldás a végponttól -végpontig (E2E) titkosítás tesztelésére, amíg be nincs fejezve az eszközhitelesítés. + + Change + - + + Change what packs are enabled, remove packs or create new ones + + + + INFO INFÓ @@ -1439,12 +1517,7 @@ végpontig (E2E) titkosítás tesztelésére, amíg be nincs fejezve az eszközh Szoba verziója - - OK - OK - - - + Failed to enable encryption: %1 Nem sikerült a titkosítás aktiválása: %1 @@ -1475,6 +1548,24 @@ végpontig (E2E) titkosítás tesztelésére, amíg be nincs fejezve az eszközh Nem sikerült a kép feltöltése: %s + + RoomlistModel + + + Pending invite. + + + + + Previewing this room + + + + + No preview available + + + ScreenShare @@ -1532,7 +1623,7 @@ végpontig (E2E) titkosítás tesztelésére, amíg be nincs fejezve az eszközh StatusIndicator - + Failed Sikertelen @@ -1552,6 +1643,14 @@ végpontig (E2E) titkosítás tesztelésére, amíg be nincs fejezve az eszközh Elolvasva + + StickerPicker + + + Search + Keresés + + Success @@ -1573,7 +1672,7 @@ végpontig (E2E) titkosítás tesztelésére, amíg be nincs fejezve az eszközh TimelineModel - + Message redaction failed: %1 Az üzenet visszavonása nem sikerült: %1 @@ -1584,7 +1683,7 @@ végpontig (E2E) titkosítás tesztelésére, amíg be nincs fejezve az eszközh Nem sikerült titkosítani az eseményt, küldés megszakítva! - + Save image Kép mentése @@ -1717,12 +1816,12 @@ végpontig (E2E) titkosítás tesztelésére, amíg be nincs fejezve az eszközh %1 visszavonta a kopogását. - + You joined this room. Csatlakoztál ehhez a szobához. - + %1 has changed their avatar and changed their display name to %2. @@ -1751,7 +1850,7 @@ végpontig (E2E) titkosítás tesztelésére, amíg be nincs fejezve az eszközh TimelineRow - + Edited Szerkesztve @@ -1759,17 +1858,32 @@ végpontig (E2E) titkosítás tesztelésére, amíg be nincs fejezve az eszközh TimelineView - + No room open Nincs nyitott szoba - + %1 member(s) %1 tag - + + join the conversation + + + + + accept invite + + + + + decline invite + + + + Back to room list Vissza a szobák listájára @@ -1777,7 +1891,7 @@ végpontig (E2E) titkosítás tesztelésére, amíg be nincs fejezve az eszközh TimelineViewManager - + No encrypted private chat found with this user. Create an encrypted private chat with this user and try again. Nem található titkosított privát csevegés ezzel a felhasználóval. Hozz létre egy titkosított privát csevegést vele, és próbáld újra! @@ -1785,18 +1899,17 @@ végpontig (E2E) titkosítás tesztelésére, amíg be nincs fejezve az eszközh TopBar - + Back to room list Vissza a szobák listájára - - + No room selected Nincs kiválasztva szoba - + Room options Szoba beállításai @@ -1837,7 +1950,7 @@ végpontig (E2E) titkosítás tesztelésére, amíg be nincs fejezve az eszközh UserProfile - + Global User Profile Globális felhasználói profil @@ -1847,7 +1960,7 @@ végpontig (E2E) titkosítás tesztelésére, amíg be nincs fejezve az eszközh Szobai felhasználói profil - + Verify Hitelesítés @@ -1896,7 +2009,7 @@ végpontig (E2E) titkosítás tesztelésére, amíg be nincs fejezve az eszközh UserSettings - + Default Alapértelmezett @@ -1905,7 +2018,7 @@ végpontig (E2E) titkosítás tesztelésére, amíg be nincs fejezve az eszközh UserSettingsPage - + Minimize to tray Kicsinyítés a tálcára @@ -2363,7 +2476,7 @@ Ettől általában animálttá válik az alkalmazásablakok listáján szereplő Waiting - + Waiting for other party… Várakozás a másik félre… @@ -2414,7 +2527,7 @@ Ettől általában animálttá válik az alkalmazásablakok listáján szereplő descriptiveTime - + Yesterday Tegnap @@ -2485,19 +2598,6 @@ Ettől általában animálttá válik az alkalmazásablakok listáján szereplő Nyisd meg a fallback-ket, kövesd az utasításokat, és erősítsd meg, ha végeztél velük! - - dialogs::InviteUsers - - - Cancel - Mégse - - - - User ID to invite - Meghívandó felhasználó azonosítója - - dialogs::JoinRoom @@ -2611,7 +2711,7 @@ Média mérete: %2 message-description sent: - + You sent an audio clip Küldtél egy hangfájlt diff --git a/resources/langs/nheko_it.ts b/resources/langs/nheko_it.ts index b0b8ec48..5a1d45f8 100644 --- a/resources/langs/nheko_it.ts +++ b/resources/langs/nheko_it.ts @@ -38,7 +38,7 @@ AwaitingVerificationConfirmation - + Awaiting Confirmation In attesa di conferma @@ -48,7 +48,7 @@ In attesa della conferma dall'altra parte per la verifica. - + Cancel Annulla @@ -125,7 +125,7 @@ ChatPage - + Failed to invite user: %1 Impossibile invitare l'utente: %1 @@ -157,12 +157,12 @@ - + Confirm invite Conferma Invito - + Do you really want to invite %1 (%2)? Vuoi davvero inviare %1 (%2)? @@ -227,12 +227,12 @@ Rimosso il ban dall'utente: %1 - + Do you really want to start a private chat with %1? Sei sicuro di voler avviare una chat privata con %1? - + Cache migration failed! Migrazione della cache fallita! @@ -352,7 +352,7 @@ CrossSigningSecrets - + Decrypt secrets Decifra i segreti @@ -426,12 +426,12 @@ EmojiPicker - + Search Cerca - + People Membri @@ -604,10 +604,48 @@ Inoltra Messaggio + + ImagePackSettingsDialog + + + Image pack settings + + + + + Private pack + + + + + Pack from this room + + + + + Globally enabled pack + + + + + Enable globally + + + + + Enables this pack to be used in all rooms + + + + + Close + Chiudi + + InputBar - + Select a file Seleziona un file @@ -617,17 +655,43 @@ Tutti i File (*) - + Failed to upload media. Please try again. Impossibile inviare il file multimediale. Per favore riprova. - InviteeItem + InviteDialog - - Remove - Rimuovi + + Invite users to %1 + + + + + User ID to invite + ID utente da invitare + + + + @joe:matrix.org + Example user id. The name 'joe' can be localized however you want. + + + + + Add + + + + + Invite + + + + + Cancel + Annulla @@ -744,23 +808,10 @@ Esempio: https://server.mio:8787 Accesso SSO fallito - - MemberList - - - Room members - Membri della stanza - - - - OK - OK - - MessageDelegate - + removed rimosso @@ -771,7 +822,7 @@ Esempio: https://server.mio:8787 Crittografia abilitata - + room name changed to: %1 nome della stanza cambiato in: %1 @@ -781,7 +832,7 @@ Esempio: https://server.mio:8787 nome della stanza rimosso - + topic changed to: %1 argomento cambiato in: %1 @@ -791,17 +842,17 @@ Esempio: https://server.mio:8787 argomento rimosso - + %1 changed the room avatar - + %1 created and configured room: %2 %1 creato e configurata stanza: %2 - + %1 placed a voice call. %1 ha avviato una chiamata audio. @@ -816,17 +867,17 @@ Esempio: https://server.mio:8787 - + %1 answered the call. %1 ha risposto alla chiamata. - + %1 ended the call. %1 ha terminato la chiamata. - + Negotiating call... @@ -834,7 +885,7 @@ Esempio: https://server.mio:8787 MessageInput - + Hang up Termina @@ -855,6 +906,11 @@ Esempio: https://server.mio:8787 + Stickers + + + + Emoji Emoji @@ -872,17 +928,17 @@ Esempio: https://server.mio:8787 MessageView - + Edit Modifica - + React Reagisci - + Reply Risposta @@ -892,7 +948,7 @@ Esempio: https://server.mio:8787 Opzioni - + &Copy @@ -1101,7 +1157,7 @@ Verificare %1 adesso? Placeholder - + unimplemented event: evento non implementato: @@ -1221,7 +1277,7 @@ Verificare %1 adesso? ReplyPopup - + Close Chiudi @@ -1234,7 +1290,7 @@ Verificare %1 adesso? RoomInfo - + no version stored nessuna versione memorizzata @@ -1242,7 +1298,7 @@ Verificare %1 adesso? RoomList - + New tag @@ -1251,6 +1307,16 @@ Verificare %1 adesso? Enter the tag you want to use: + + + Leave Room + + + + + Are you sure you want to leave this room? + + Leave room @@ -1282,17 +1348,7 @@ Verificare %1 adesso? - - Accept - Accetta - - - - Decline - Rifiuta - - - + Status Message @@ -1342,20 +1398,42 @@ Verificare %1 adesso? Impostazioni utente + + RoomMembers + + + Members of %1 + + + + + %n people in %1 + Summary above list of members + + + + + + + + Invite more people + + + RoomSettings - + Room Settings - + %1 member(s) - + SETTINGS @@ -1411,19 +1489,22 @@ Verificare %1 adesso? - - Respond to key requests + + Sticker & Emote Settings - - Whether or not the client should respond automatically with the session keys - upon request. Use with caution, this is a temporary measure to test the - E2E implementation until device verification is completed. + + Change - + + Change what packs are enabled, remove packs or create new ones + + + + INFO @@ -1438,12 +1519,7 @@ Verificare %1 adesso? - - OK - OK - - - + Failed to enable encryption: %1 Impossibile abilitare la crittografia: %1 @@ -1474,6 +1550,24 @@ Verificare %1 adesso? Impossibile fare l'upload dell'immagine: %s + + RoomlistModel + + + Pending invite. + + + + + Previewing this room + + + + + No preview available + + + ScreenShare @@ -1531,7 +1625,7 @@ Verificare %1 adesso? StatusIndicator - + Failed Fallito @@ -1551,6 +1645,14 @@ Verificare %1 adesso? Letto + + StickerPicker + + + Search + Cerca + + Success @@ -1572,7 +1674,7 @@ Verificare %1 adesso? TimelineModel - + Message redaction failed: %1 Oscuramento del messaggio fallito: %1 @@ -1583,7 +1685,7 @@ Verificare %1 adesso? - + Save image Salva immagine @@ -1717,12 +1819,12 @@ Verificare %1 adesso? %1 ha oscurato la sua bussata. - + You joined this room. Sei entrato in questa stanza. - + %1 has changed their avatar and changed their display name to %2. @@ -1751,7 +1853,7 @@ Verificare %1 adesso? TimelineRow - + Edited @@ -1759,17 +1861,32 @@ Verificare %1 adesso? TimelineView - + No room open Nessuna stanza aperta - + %1 member(s) - + + join the conversation + + + + + accept invite + + + + + decline invite + + + + Back to room list @@ -1777,7 +1894,7 @@ Verificare %1 adesso? TimelineViewManager - + No encrypted private chat found with this user. Create an encrypted private chat with this user and try again. @@ -1785,18 +1902,17 @@ Verificare %1 adesso? TopBar - + Back to room list - - + No room selected - + Room options Opzioni della stanza @@ -1837,7 +1953,7 @@ Verificare %1 adesso? UserProfile - + Global User Profile @@ -1847,7 +1963,7 @@ Verificare %1 adesso? - + Verify @@ -1896,7 +2012,7 @@ Verificare %1 adesso? UserSettings - + Default @@ -1905,7 +2021,7 @@ Verificare %1 adesso? UserSettingsPage - + Minimize to tray Minimizza nella tray @@ -2351,7 +2467,7 @@ This usually causes the application icon in the task bar to animate in some fash Waiting - + Waiting for other party… @@ -2402,7 +2518,7 @@ This usually causes the application icon in the task bar to animate in some fash descriptiveTime - + Yesterday Ieri @@ -2473,19 +2589,6 @@ This usually causes the application icon in the task bar to animate in some fash Apri il ripiego, segui i passaggi e conferma dopo averli completati. - - dialogs::InviteUsers - - - Cancel - Annulla - - - - User ID to invite - ID utente da invitare - - dialogs::JoinRoom @@ -2599,7 +2702,7 @@ Peso media: %2 message-description sent: - + You sent an audio clip Hai inviato una clip audio diff --git a/resources/langs/nheko_ja.ts b/resources/langs/nheko_ja.ts index c7872ce0..1ec862b0 100644 --- a/resources/langs/nheko_ja.ts +++ b/resources/langs/nheko_ja.ts @@ -38,7 +38,7 @@ AwaitingVerificationConfirmation - + Awaiting Confirmation @@ -48,7 +48,7 @@ - + Cancel キャンセル @@ -125,7 +125,7 @@ ChatPage - + Failed to invite user: %1 ユーザーを招待できませんでした: %1 @@ -157,12 +157,12 @@ - + Confirm invite - + Do you really want to invite %1 (%2)? @@ -227,12 +227,12 @@ 永久追放を解除されたユーザー: %1 - + Do you really want to start a private chat with %1? - + Cache migration failed! @@ -352,7 +352,7 @@ CrossSigningSecrets - + Decrypt secrets @@ -426,12 +426,12 @@ EmojiPicker - + Search - + People @@ -604,10 +604,48 @@ + + ImagePackSettingsDialog + + + Image pack settings + + + + + Private pack + + + + + Pack from this room + + + + + Globally enabled pack + + + + + Enable globally + + + + + Enables this pack to be used in all rooms + + + + + Close + 閉じる + + InputBar - + Select a file ファイルを選択 @@ -617,17 +655,43 @@ 全てのファイル (*) - + Failed to upload media. Please try again. メディアをアップロードできませんでした。やり直して下さい。 - InviteeItem + InviteDialog - - Remove - 削除 + + Invite users to %1 + + + + + User ID to invite + 招待するユーザーのID + + + + @joe:matrix.org + Example user id. The name 'joe' can be localized however you want. + + + + + Add + + + + + Invite + + + + + Cancel + キャンセル @@ -740,23 +804,10 @@ Example: https://server.my:8787 - - MemberList - - - Room members - 部屋の参加者 - - - - OK - OK - - MessageDelegate - + removed @@ -767,7 +818,7 @@ Example: https://server.my:8787 暗号化が有効です - + room name changed to: %1 部屋名が変更されました: %1 @@ -777,7 +828,7 @@ Example: https://server.my:8787 部屋名が削除されました - + topic changed to: %1 話題が変更されました: %1 @@ -787,17 +838,17 @@ Example: https://server.my:8787 話題が削除されました - + %1 changed the room avatar - + %1 created and configured room: %2 - + %1 placed a voice call. @@ -812,17 +863,17 @@ Example: https://server.my:8787 - + %1 answered the call. - + %1 ended the call. - + Negotiating call... @@ -830,7 +881,7 @@ Example: https://server.my:8787 MessageInput - + Hang up @@ -851,6 +902,11 @@ Example: https://server.my:8787 + Stickers + + + + Emoji 絵文字 @@ -868,17 +924,17 @@ Example: https://server.my:8787 MessageView - + Edit - + React - + Reply 返信 @@ -888,7 +944,7 @@ Example: https://server.my:8787 オプション - + &Copy @@ -1096,7 +1152,7 @@ Example: https://server.my:8787 Placeholder - + unimplemented event: 未実装のイベント: @@ -1216,7 +1272,7 @@ Example: https://server.my:8787 ReplyPopup - + Close 閉じる @@ -1229,7 +1285,7 @@ Example: https://server.my:8787 RoomInfo - + no version stored バージョンが保存されていません @@ -1237,7 +1293,7 @@ Example: https://server.my:8787 RoomList - + New tag @@ -1246,6 +1302,16 @@ Example: https://server.my:8787 Enter the tag you want to use: + + + Leave Room + + + + + Are you sure you want to leave this room? + + Leave room @@ -1277,17 +1343,7 @@ Example: https://server.my:8787 - - Accept - 容認 - - - - Decline - 拒否 - - - + Status Message @@ -1337,20 +1393,41 @@ Example: https://server.my:8787 ユーザー設定 + + RoomMembers + + + Members of %1 + + + + + %n people in %1 + Summary above list of members + + + + + + + Invite more people + + + RoomSettings - + Room Settings - + %1 member(s) - + SETTINGS @@ -1406,19 +1483,22 @@ Example: https://server.my:8787 - - Respond to key requests + + Sticker & Emote Settings - - Whether or not the client should respond automatically with the session keys - upon request. Use with caution, this is a temporary measure to test the - E2E implementation until device verification is completed. + + Change - + + Change what packs are enabled, remove packs or create new ones + + + + INFO @@ -1433,12 +1513,7 @@ Example: https://server.my:8787 - - OK - OK - - - + Failed to enable encryption: %1 暗号化を有効にできませんでした: %1 @@ -1469,6 +1544,24 @@ Example: https://server.my:8787 画像をアップロードできませんでした: %s + + RoomlistModel + + + Pending invite. + + + + + Previewing this room + + + + + No preview available + + + ScreenShare @@ -1526,7 +1619,7 @@ Example: https://server.my:8787 StatusIndicator - + Failed 失敗 @@ -1546,6 +1639,14 @@ Example: https://server.my:8787 既読 + + StickerPicker + + + Search + + + Success @@ -1567,7 +1668,7 @@ Example: https://server.my:8787 TimelineModel - + Message redaction failed: %1 メッセージを編集できませんでした: %1 @@ -1578,7 +1679,7 @@ Example: https://server.my:8787 - + Save image 画像を保存 @@ -1711,12 +1812,12 @@ Example: https://server.my:8787 %1がノックを編集しました。 - + You joined this room. - + %1 has changed their avatar and changed their display name to %2. @@ -1745,7 +1846,7 @@ Example: https://server.my:8787 TimelineRow - + Edited @@ -1753,17 +1854,32 @@ Example: https://server.my:8787 TimelineView - + No room open 部屋が開いていません - + %1 member(s) - + + join the conversation + + + + + accept invite + + + + + decline invite + + + + Back to room list @@ -1771,7 +1887,7 @@ Example: https://server.my:8787 TimelineViewManager - + No encrypted private chat found with this user. Create an encrypted private chat with this user and try again. @@ -1779,18 +1895,17 @@ Example: https://server.my:8787 TopBar - + Back to room list - - + No room selected - + Room options 部屋のオプション @@ -1831,7 +1946,7 @@ Example: https://server.my:8787 UserProfile - + Global User Profile @@ -1841,7 +1956,7 @@ Example: https://server.my:8787 - + Verify @@ -1890,7 +2005,7 @@ Example: https://server.my:8787 UserSettings - + Default @@ -1899,7 +2014,7 @@ Example: https://server.my:8787 UserSettingsPage - + Minimize to tray トレイへ最小化 @@ -2345,7 +2460,7 @@ This usually causes the application icon in the task bar to animate in some fash Waiting - + Waiting for other party… @@ -2396,7 +2511,7 @@ This usually causes the application icon in the task bar to animate in some fash descriptiveTime - + Yesterday 昨日 @@ -2467,19 +2582,6 @@ This usually causes the application icon in the task bar to animate in some fash - - dialogs::InviteUsers - - - Cancel - キャンセル - - - - User ID to invite - 招待するユーザーのID - - dialogs::JoinRoom @@ -2593,7 +2695,7 @@ Media size: %2 message-description sent: - + You sent an audio clip 音声データを送信しました diff --git a/resources/langs/nheko_ml.ts b/resources/langs/nheko_ml.ts index 0bdf3b63..011107c2 100644 --- a/resources/langs/nheko_ml.ts +++ b/resources/langs/nheko_ml.ts @@ -38,7 +38,7 @@ AwaitingVerificationConfirmation - + Awaiting Confirmation സ്ഥിരീകരണത്തിനായി കാത്തിരിക്കുന്നു @@ -48,7 +48,7 @@ - + Cancel റദ്ദാക്കു @@ -125,7 +125,7 @@ ChatPage - + Failed to invite user: %1 ഉപയോക്താവിനെ ക്ഷണിക്കുന്നതിൽ പരാജയപ്പെട്ടു: %1 @@ -157,12 +157,12 @@ - + Confirm invite ക്ഷണം ഉറപ്പാക്കു - + Do you really want to invite %1 (%2)? @@ -227,12 +227,12 @@ - + Do you really want to start a private chat with %1? - + Cache migration failed! @@ -352,7 +352,7 @@ CrossSigningSecrets - + Decrypt secrets @@ -426,12 +426,12 @@ EmojiPicker - + Search തിരയുക - + People ആളുകൾ @@ -604,10 +604,48 @@ + + ImagePackSettingsDialog + + + Image pack settings + + + + + Private pack + + + + + Pack from this room + + + + + Globally enabled pack + + + + + Enable globally + + + + + Enables this pack to be used in all rooms + + + + + Close + അടയ്‌ക്കുക + + InputBar - + Select a file ഒരു ഫയൽ തിരഞ്ഞെടുക്കുക @@ -617,17 +655,43 @@ - + Failed to upload media. Please try again. - InviteeItem + InviteDialog - - Remove - നീക്കംചെയ്യുക + + Invite users to %1 + + + + + User ID to invite + + + + + @joe:matrix.org + Example user id. The name 'joe' can be localized however you want. + + + + + Add + + + + + Invite + + + + + Cancel + റദ്ദാക്കു @@ -740,28 +804,15 @@ Example: https://server.my:8787 - - MemberList - - - Room members - - - - - OK - ശരി - - MessageDelegate - + Encryption enabled - + room name changed to: %1 @@ -771,7 +822,7 @@ Example: https://server.my:8787 - + topic changed to: %1 @@ -781,17 +832,17 @@ Example: https://server.my:8787 - + %1 changed the room avatar - + %1 created and configured room: %2 - + %1 placed a voice call. @@ -806,23 +857,23 @@ Example: https://server.my:8787 - + Negotiating call... - + %1 answered the call. - + removed നീക്കംചെയ്‌തു - + %1 ended the call. @@ -830,7 +881,7 @@ Example: https://server.my:8787 MessageInput - + Hang up @@ -851,6 +902,11 @@ Example: https://server.my:8787 + Stickers + + + + Emoji ഇമോജി @@ -868,17 +924,17 @@ Example: https://server.my:8787 MessageView - + Edit - + React - + Reply @@ -888,7 +944,7 @@ Example: https://server.my:8787 - + &Copy @@ -1096,7 +1152,7 @@ Example: https://server.my:8787 Placeholder - + unimplemented event: @@ -1216,7 +1272,7 @@ Example: https://server.my:8787 ReplyPopup - + Close അടയ്‌ക്കുക @@ -1229,7 +1285,7 @@ Example: https://server.my:8787 RoomInfo - + no version stored @@ -1237,7 +1293,7 @@ Example: https://server.my:8787 RoomList - + New tag @@ -1246,6 +1302,16 @@ Example: https://server.my:8787 Enter the tag you want to use: + + + Leave Room + + + + + Are you sure you want to leave this room? + + Leave room @@ -1277,17 +1343,7 @@ Example: https://server.my:8787 - - Accept - - - - - Decline - നിരസിക്കുക - - - + Status Message @@ -1337,20 +1393,42 @@ Example: https://server.my:8787 + + RoomMembers + + + Members of %1 + + + + + %n people in %1 + Summary above list of members + + + + + + + + Invite more people + + + RoomSettings - + Room Settings - + %1 member(s) - + SETTINGS @@ -1406,19 +1484,22 @@ Example: https://server.my:8787 - - Respond to key requests + + Sticker & Emote Settings - - Whether or not the client should respond automatically with the session keys - upon request. Use with caution, this is a temporary measure to test the - E2E implementation until device verification is completed. + + Change - + + Change what packs are enabled, remove packs or create new ones + + + + INFO @@ -1433,12 +1514,7 @@ Example: https://server.my:8787 - - OK - ശരി - - - + Failed to enable encryption: %1 @@ -1469,6 +1545,24 @@ Example: https://server.my:8787 + + RoomlistModel + + + Pending invite. + + + + + Previewing this room + + + + + No preview available + + + ScreenShare @@ -1526,7 +1620,7 @@ Example: https://server.my:8787 StatusIndicator - + Failed @@ -1546,6 +1640,14 @@ Example: https://server.my:8787 + + StickerPicker + + + Search + തിരയുക + + Success @@ -1567,7 +1669,7 @@ Example: https://server.my:8787 TimelineModel - + Message redaction failed: %1 @@ -1578,7 +1680,7 @@ Example: https://server.my:8787 - + Save image @@ -1712,12 +1814,12 @@ Example: https://server.my:8787 - + You joined this room. നിങ്ങൾ ഈ മുറിയിൽ ചേർന്നു. - + %1 has changed their avatar and changed their display name to %2. @@ -1746,7 +1848,7 @@ Example: https://server.my:8787 TimelineRow - + Edited @@ -1754,17 +1856,32 @@ Example: https://server.my:8787 TimelineView - + No room open - + %1 member(s) - + + join the conversation + + + + + accept invite + + + + + decline invite + + + + Back to room list @@ -1772,7 +1889,7 @@ Example: https://server.my:8787 TimelineViewManager - + No encrypted private chat found with this user. Create an encrypted private chat with this user and try again. @@ -1780,18 +1897,17 @@ Example: https://server.my:8787 TopBar - + Back to room list - - + No room selected - + Room options @@ -1832,7 +1948,7 @@ Example: https://server.my:8787 UserProfile - + Global User Profile @@ -1842,7 +1958,7 @@ Example: https://server.my:8787 - + Verify @@ -1891,7 +2007,7 @@ Example: https://server.my:8787 UserSettings - + Default @@ -1900,7 +2016,7 @@ Example: https://server.my:8787 UserSettingsPage - + Minimize to tray @@ -2346,7 +2462,7 @@ This usually causes the application icon in the task bar to animate in some fash Waiting - + Waiting for other party… @@ -2397,7 +2513,7 @@ This usually causes the application icon in the task bar to animate in some fash descriptiveTime - + Yesterday @@ -2468,19 +2584,6 @@ This usually causes the application icon in the task bar to animate in some fash - - dialogs::InviteUsers - - - Cancel - റദ്ദാക്കു - - - - User ID to invite - - - dialogs::JoinRoom @@ -2592,7 +2695,7 @@ Media size: %2 message-description sent: - + You sent an audio clip diff --git a/resources/langs/nheko_nl.ts b/resources/langs/nheko_nl.ts index 3f2a147f..b21db075 100644 --- a/resources/langs/nheko_nl.ts +++ b/resources/langs/nheko_nl.ts @@ -38,7 +38,7 @@ AwaitingVerificationConfirmation - + Awaiting Confirmation @@ -48,7 +48,7 @@ - + Cancel Annuleren @@ -125,7 +125,7 @@ ChatPage - + Failed to invite user: %1 Gebruiker uitnodigen mislukt: %1 @@ -157,12 +157,12 @@ - + Confirm invite - + Do you really want to invite %1 (%2)? @@ -227,12 +227,12 @@ - + Do you really want to start a private chat with %1? - + Cache migration failed! @@ -352,7 +352,7 @@ CrossSigningSecrets - + Decrypt secrets @@ -426,12 +426,12 @@ EmojiPicker - + Search - + People @@ -604,10 +604,48 @@ + + ImagePackSettingsDialog + + + Image pack settings + + + + + Private pack + + + + + Pack from this room + + + + + Globally enabled pack + + + + + Enable globally + + + + + Enables this pack to be used in all rooms + + + + + Close + + + InputBar - + Select a file Kies een bestand @@ -617,18 +655,44 @@ Alle bestanden (*) - + Failed to upload media. Please try again. - InviteeItem + InviteDialog - - Remove + + Invite users to %1 + + + User ID to invite + Uit te nodigen gebruikers-id + + + + @joe:matrix.org + Example user id. The name 'joe' can be localized however you want. + + + + + Add + + + + + Invite + + + + + Cancel + Annuleren + LoginPage @@ -740,23 +804,10 @@ Example: https://server.my:8787 - - MemberList - - - Room members - Kamerleden - - - - OK - - - MessageDelegate - + removed @@ -767,7 +818,7 @@ Example: https://server.my:8787 - + room name changed to: %1 @@ -777,7 +828,7 @@ Example: https://server.my:8787 - + topic changed to: %1 @@ -787,17 +838,17 @@ Example: https://server.my:8787 - + %1 changed the room avatar - + %1 created and configured room: %2 - + %1 placed a voice call. @@ -812,17 +863,17 @@ Example: https://server.my:8787 - + %1 answered the call. - + %1 ended the call. - + Negotiating call... @@ -830,7 +881,7 @@ Example: https://server.my:8787 MessageInput - + Hang up @@ -851,6 +902,11 @@ Example: https://server.my:8787 + Stickers + + + + Emoji @@ -868,17 +924,17 @@ Example: https://server.my:8787 MessageView - + Edit - + React - + Reply @@ -888,7 +944,7 @@ Example: https://server.my:8787 - + &Copy @@ -1096,7 +1152,7 @@ Example: https://server.my:8787 Placeholder - + unimplemented event: @@ -1216,7 +1272,7 @@ Example: https://server.my:8787 ReplyPopup - + Close @@ -1229,7 +1285,7 @@ Example: https://server.my:8787 RoomInfo - + no version stored @@ -1237,7 +1293,7 @@ Example: https://server.my:8787 RoomList - + New tag @@ -1246,6 +1302,16 @@ Example: https://server.my:8787 Enter the tag you want to use: + + + Leave Room + + + + + Are you sure you want to leave this room? + + Leave room @@ -1277,17 +1343,7 @@ Example: https://server.my:8787 - - Accept - Accepteren - - - - Decline - Afwijzen - - - + Status Message @@ -1337,20 +1393,42 @@ Example: https://server.my:8787 + + RoomMembers + + + Members of %1 + + + + + %n people in %1 + Summary above list of members + + + + + + + + Invite more people + + + RoomSettings - + Room Settings - + %1 member(s) - + SETTINGS @@ -1406,19 +1484,22 @@ Example: https://server.my:8787 - - Respond to key requests + + Sticker & Emote Settings - - Whether or not the client should respond automatically with the session keys - upon request. Use with caution, this is a temporary measure to test the - E2E implementation until device verification is completed. + + Change - + + Change what packs are enabled, remove packs or create new ones + + + + INFO @@ -1433,12 +1514,7 @@ Example: https://server.my:8787 - - OK - - - - + Failed to enable encryption: %1 @@ -1469,6 +1545,24 @@ Example: https://server.my:8787 + + RoomlistModel + + + Pending invite. + + + + + Previewing this room + + + + + No preview available + + + ScreenShare @@ -1526,7 +1620,7 @@ Example: https://server.my:8787 StatusIndicator - + Failed @@ -1546,6 +1640,14 @@ Example: https://server.my:8787 + + StickerPicker + + + Search + + + Success @@ -1567,7 +1669,7 @@ Example: https://server.my:8787 TimelineModel - + Message redaction failed: %1 @@ -1578,7 +1680,7 @@ Example: https://server.my:8787 - + Save image Afbeelding opslaan @@ -1712,12 +1814,12 @@ Example: https://server.my:8787 - + You joined this room. Je bent lid geworden van deze kamer. - + %1 has changed their avatar and changed their display name to %2. @@ -1746,7 +1848,7 @@ Example: https://server.my:8787 TimelineRow - + Edited @@ -1754,17 +1856,32 @@ Example: https://server.my:8787 TimelineView - + No room open - + %1 member(s) - + + join the conversation + + + + + accept invite + + + + + decline invite + + + + Back to room list @@ -1772,7 +1889,7 @@ Example: https://server.my:8787 TimelineViewManager - + No encrypted private chat found with this user. Create an encrypted private chat with this user and try again. @@ -1780,18 +1897,17 @@ Example: https://server.my:8787 TopBar - + Back to room list - - + No room selected - + Room options @@ -1832,7 +1948,7 @@ Example: https://server.my:8787 UserProfile - + Global User Profile @@ -1842,7 +1958,7 @@ Example: https://server.my:8787 - + Verify @@ -1891,7 +2007,7 @@ Example: https://server.my:8787 UserSettings - + Default @@ -1900,7 +2016,7 @@ Example: https://server.my:8787 UserSettingsPage - + Minimize to tray Minimaliseren naar systeemvak @@ -2346,7 +2462,7 @@ This usually causes the application icon in the task bar to animate in some fash Waiting - + Waiting for other party… @@ -2397,7 +2513,7 @@ This usually causes the application icon in the task bar to animate in some fash descriptiveTime - + Yesterday @@ -2468,19 +2584,6 @@ This usually causes the application icon in the task bar to animate in some fash - - dialogs::InviteUsers - - - Cancel - Annuleren - - - - User ID to invite - Uit te nodigen gebruikers-id - - dialogs::JoinRoom @@ -2594,7 +2697,7 @@ Mediagrootte: %2 message-description sent: - + You sent an audio clip diff --git a/resources/langs/nheko_pl.ts b/resources/langs/nheko_pl.ts index 6abcd147..72e4e771 100644 --- a/resources/langs/nheko_pl.ts +++ b/resources/langs/nheko_pl.ts @@ -38,7 +38,7 @@ AwaitingVerificationConfirmation - + Awaiting Confirmation Oczekiwanie na potwierdzenie @@ -48,7 +48,7 @@ Oczekiwanie na dokończenie weryfikacji przez drugą stronę. - + Cancel Anuluj @@ -125,7 +125,7 @@ ChatPage - + Failed to invite user: %1 Nie udało się zaprosić użytkownika: %1 @@ -157,12 +157,12 @@ - + Confirm invite - + Do you really want to invite %1 (%2)? Czy na pewno chcesz zaprosić %1 (%2)? @@ -227,12 +227,12 @@ Odblokowano użytkownika: %1 - + Do you really want to start a private chat with %1? - + Cache migration failed! Nie udało się przenieść pamięci podręcznej! @@ -352,7 +352,7 @@ CrossSigningSecrets - + Decrypt secrets @@ -426,12 +426,12 @@ EmojiPicker - + Search Szukaj - + People Ludzie @@ -604,10 +604,48 @@ + + ImagePackSettingsDialog + + + Image pack settings + + + + + Private pack + + + + + Pack from this room + + + + + Globally enabled pack + + + + + Enable globally + + + + + Enables this pack to be used in all rooms + + + + + Close + Zamknij + + InputBar - + Select a file Wybierz plik @@ -617,17 +655,43 @@ Wszystkie pliki (*) - + Failed to upload media. Please try again. - InviteeItem + InviteDialog - - Remove - Usuń + + Invite users to %1 + + + + + User ID to invite + ID użytkownika do zaproszenia + + + + @joe:matrix.org + Example user id. The name 'joe' can be localized however you want. + + + + + Add + + + + + Invite + + + + + Cancel + Anuluj @@ -742,23 +806,10 @@ Example: https://server.my:8787 Logowanie SSO zakończone niepowodzeniem - - MemberList - - - Room members - Członkowie pokoju - - - - OK - OK - - MessageDelegate - + removed @@ -769,7 +820,7 @@ Example: https://server.my:8787 Szyfrowanie włączone - + room name changed to: %1 Nazwa pokoju zmieniona na: %1 @@ -779,7 +830,7 @@ Example: https://server.my:8787 usunięto nazwę pokoju - + topic changed to: %1 temat zmieniono na: %1 @@ -789,17 +840,17 @@ Example: https://server.my:8787 usunięto temat - + %1 changed the room avatar - + %1 created and configured room: %2 %1 utworzył i skonfigurował pokój: %2 - + %1 placed a voice call. %1 rozpoczął(-ęła) połączenie głosowe. @@ -814,17 +865,17 @@ Example: https://server.my:8787 %1 rozpoczął(-ęła) połączenie. - + %1 answered the call. %1 odebrał(a) połączenie. - + %1 ended the call. %1 zakończył(a) połączenie. - + Negotiating call... Negocjowanie połączenia… @@ -832,7 +883,7 @@ Example: https://server.my:8787 MessageInput - + Hang up @@ -853,6 +904,11 @@ Example: https://server.my:8787 + Stickers + + + + Emoji Emoji @@ -870,17 +926,17 @@ Example: https://server.my:8787 MessageView - + Edit - + React - + Reply @@ -890,7 +946,7 @@ Example: https://server.my:8787 - + &Copy @@ -1098,7 +1154,7 @@ Example: https://server.my:8787 Placeholder - + unimplemented event: Niezaimplementowane wydarzenie: @@ -1218,7 +1274,7 @@ Example: https://server.my:8787 ReplyPopup - + Close Zamknij @@ -1231,7 +1287,7 @@ Example: https://server.my:8787 RoomInfo - + no version stored @@ -1239,7 +1295,7 @@ Example: https://server.my:8787 RoomList - + New tag @@ -1248,6 +1304,16 @@ Example: https://server.my:8787 Enter the tag you want to use: + + + Leave Room + + + + + Are you sure you want to leave this room? + + Leave room @@ -1279,17 +1345,7 @@ Example: https://server.my:8787 - - Accept - Akceptuj - - - - Decline - Odrzuć - - - + Status Message @@ -1339,20 +1395,43 @@ Example: https://server.my:8787 Ustawienia użytkownika + + RoomMembers + + + Members of %1 + + + + + %n people in %1 + Summary above list of members + + + + + + + + + Invite more people + + + RoomSettings - + Room Settings - + %1 member(s) - + SETTINGS @@ -1408,19 +1487,22 @@ Example: https://server.my:8787 - - Respond to key requests + + Sticker & Emote Settings - - Whether or not the client should respond automatically with the session keys - upon request. Use with caution, this is a temporary measure to test the - E2E implementation until device verification is completed. + + Change - + + Change what packs are enabled, remove packs or create new ones + + + + INFO @@ -1435,12 +1517,7 @@ Example: https://server.my:8787 - - OK - OK - - - + Failed to enable encryption: %1 Nie udało się włączyć szyfrowania: %1 @@ -1471,6 +1548,24 @@ Example: https://server.my:8787 Nie udało się wysłać obrazu: %s + + RoomlistModel + + + Pending invite. + + + + + Previewing this room + + + + + No preview available + + + ScreenShare @@ -1528,7 +1623,7 @@ Example: https://server.my:8787 StatusIndicator - + Failed @@ -1548,6 +1643,14 @@ Example: https://server.my:8787 + + StickerPicker + + + Search + Szukaj + + Success @@ -1569,7 +1672,7 @@ Example: https://server.my:8787 TimelineModel - + Message redaction failed: %1 Redagowanie wiadomości nie powiodło się: %1 @@ -1580,7 +1683,7 @@ Example: https://server.my:8787 - + Save image Zapisz obraz @@ -1715,12 +1818,12 @@ Example: https://server.my:8787 - + You joined this room. Dołączyłeś(-łaś) do tego pokoju. - + %1 has changed their avatar and changed their display name to %2. @@ -1749,7 +1852,7 @@ Example: https://server.my:8787 TimelineRow - + Edited @@ -1757,17 +1860,32 @@ Example: https://server.my:8787 TimelineView - + No room open - + %1 member(s) - + + join the conversation + + + + + accept invite + + + + + decline invite + + + + Back to room list @@ -1775,7 +1893,7 @@ Example: https://server.my:8787 TimelineViewManager - + No encrypted private chat found with this user. Create an encrypted private chat with this user and try again. @@ -1783,18 +1901,17 @@ Example: https://server.my:8787 TopBar - + Back to room list - - + No room selected - + Room options Ustawienia pokoju @@ -1835,7 +1952,7 @@ Example: https://server.my:8787 UserProfile - + Global User Profile @@ -1845,7 +1962,7 @@ Example: https://server.my:8787 - + Verify @@ -1894,7 +2011,7 @@ Example: https://server.my:8787 UserSettings - + Default @@ -1903,7 +2020,7 @@ Example: https://server.my:8787 UserSettingsPage - + Minimize to tray Zminimalizuj do paska zadań @@ -2349,7 +2466,7 @@ This usually causes the application icon in the task bar to animate in some fash Waiting - + Waiting for other party… @@ -2400,7 +2517,7 @@ This usually causes the application icon in the task bar to animate in some fash descriptiveTime - + Yesterday @@ -2471,19 +2588,6 @@ This usually causes the application icon in the task bar to animate in some fash - - dialogs::InviteUsers - - - Cancel - Anuluj - - - - User ID to invite - ID użytkownika do zaproszenia - - dialogs::JoinRoom @@ -2597,7 +2701,7 @@ Rozmiar multimediów: %2 message-description sent: - + You sent an audio clip diff --git a/resources/langs/nheko_pt_BR.ts b/resources/langs/nheko_pt_BR.ts index fa0ea193..b83585fb 100644 --- a/resources/langs/nheko_pt_BR.ts +++ b/resources/langs/nheko_pt_BR.ts @@ -38,7 +38,7 @@ AwaitingVerificationConfirmation - + Awaiting Confirmation @@ -48,7 +48,7 @@ Esperando o outro lado completar a verificação. - + Cancel Cancelar @@ -125,7 +125,7 @@ ChatPage - + Failed to invite user: %1 Falha ao convidar usuário: %1 @@ -157,12 +157,12 @@ - + Confirm invite Confirmar convite - + Do you really want to invite %1 (%2)? @@ -227,12 +227,12 @@ Usuário desbanido: %1 - + Do you really want to start a private chat with %1? - + Cache migration failed! Migração do cache falhou! @@ -352,7 +352,7 @@ CrossSigningSecrets - + Decrypt secrets @@ -426,12 +426,12 @@ EmojiPicker - + Search - + People @@ -604,10 +604,48 @@ + + ImagePackSettingsDialog + + + Image pack settings + + + + + Private pack + + + + + Pack from this room + + + + + Globally enabled pack + + + + + Enable globally + + + + + Enables this pack to be used in all rooms + + + + + Close + + + InputBar - + Select a file @@ -617,18 +655,44 @@ - + Failed to upload media. Please try again. - InviteeItem + InviteDialog - - Remove + + Invite users to %1 + + + User ID to invite + + + + + @joe:matrix.org + Example user id. The name 'joe' can be localized however you want. + + + + + Add + + + + + Invite + + + + + Cancel + Cancelar + LoginPage @@ -740,28 +804,15 @@ Example: https://server.my:8787 - - MemberList - - - Room members - - - - - OK - - - MessageDelegate - + Encryption enabled - + room name changed to: %1 @@ -771,7 +822,7 @@ Example: https://server.my:8787 - + topic changed to: %1 @@ -781,17 +832,17 @@ Example: https://server.my:8787 - + %1 changed the room avatar - + %1 created and configured room: %2 - + %1 placed a voice call. @@ -806,23 +857,23 @@ Example: https://server.my:8787 - + Negotiating call... - + %1 answered the call. - + removed - + %1 ended the call. @@ -830,7 +881,7 @@ Example: https://server.my:8787 MessageInput - + Hang up @@ -851,6 +902,11 @@ Example: https://server.my:8787 + Stickers + + + + Emoji @@ -868,17 +924,17 @@ Example: https://server.my:8787 MessageView - + Edit - + React - + Reply @@ -888,7 +944,7 @@ Example: https://server.my:8787 - + &Copy @@ -1096,7 +1152,7 @@ Example: https://server.my:8787 Placeholder - + unimplemented event: @@ -1216,7 +1272,7 @@ Example: https://server.my:8787 ReplyPopup - + Close @@ -1229,7 +1285,7 @@ Example: https://server.my:8787 RoomInfo - + no version stored @@ -1237,7 +1293,7 @@ Example: https://server.my:8787 RoomList - + New tag @@ -1246,6 +1302,16 @@ Example: https://server.my:8787 Enter the tag you want to use: + + + Leave Room + + + + + Are you sure you want to leave this room? + + Leave room @@ -1277,17 +1343,7 @@ Example: https://server.my:8787 - - Accept - Aceitar - - - - Decline - Rejeitar - - - + Status Message @@ -1337,20 +1393,42 @@ Example: https://server.my:8787 + + RoomMembers + + + Members of %1 + + + + + %n people in %1 + Summary above list of members + + + + + + + + Invite more people + + + RoomSettings - + Room Settings - + %1 member(s) - + SETTINGS @@ -1406,19 +1484,22 @@ Example: https://server.my:8787 - - Respond to key requests + + Sticker & Emote Settings - - Whether or not the client should respond automatically with the session keys - upon request. Use with caution, this is a temporary measure to test the - E2E implementation until device verification is completed. + + Change - + + Change what packs are enabled, remove packs or create new ones + + + + INFO @@ -1433,12 +1514,7 @@ Example: https://server.my:8787 - - OK - - - - + Failed to enable encryption: %1 @@ -1469,6 +1545,24 @@ Example: https://server.my:8787 + + RoomlistModel + + + Pending invite. + + + + + Previewing this room + + + + + No preview available + + + ScreenShare @@ -1526,7 +1620,7 @@ Example: https://server.my:8787 StatusIndicator - + Failed @@ -1546,6 +1640,14 @@ Example: https://server.my:8787 + + StickerPicker + + + Search + + + Success @@ -1567,7 +1669,7 @@ Example: https://server.my:8787 TimelineModel - + Message redaction failed: %1 @@ -1578,7 +1680,7 @@ Example: https://server.my:8787 - + Save image @@ -1712,12 +1814,12 @@ Example: https://server.my:8787 - + You joined this room. Você entrou nessa sala. - + %1 has changed their avatar and changed their display name to %2. @@ -1746,7 +1848,7 @@ Example: https://server.my:8787 TimelineRow - + Edited @@ -1754,17 +1856,32 @@ Example: https://server.my:8787 TimelineView - + No room open - + %1 member(s) - + + join the conversation + + + + + accept invite + + + + + decline invite + + + + Back to room list @@ -1772,7 +1889,7 @@ Example: https://server.my:8787 TimelineViewManager - + No encrypted private chat found with this user. Create an encrypted private chat with this user and try again. @@ -1780,18 +1897,17 @@ Example: https://server.my:8787 TopBar - + Back to room list - - + No room selected - + Room options @@ -1832,7 +1948,7 @@ Example: https://server.my:8787 UserProfile - + Global User Profile @@ -1842,7 +1958,7 @@ Example: https://server.my:8787 - + Verify @@ -1891,7 +2007,7 @@ Example: https://server.my:8787 UserSettings - + Default @@ -1900,7 +2016,7 @@ Example: https://server.my:8787 UserSettingsPage - + Minimize to tray @@ -2346,7 +2462,7 @@ This usually causes the application icon in the task bar to animate in some fash Waiting - + Waiting for other party… @@ -2397,7 +2513,7 @@ This usually causes the application icon in the task bar to animate in some fash descriptiveTime - + Yesterday @@ -2468,19 +2584,6 @@ This usually causes the application icon in the task bar to animate in some fash - - dialogs::InviteUsers - - - Cancel - Cancelar - - - - User ID to invite - - - dialogs::JoinRoom @@ -2592,7 +2695,7 @@ Media size: %2 message-description sent: - + You sent an audio clip diff --git a/resources/langs/nheko_pt_PT.ts b/resources/langs/nheko_pt_PT.ts index 81343f97..a0a8c8a8 100644 --- a/resources/langs/nheko_pt_PT.ts +++ b/resources/langs/nheko_pt_PT.ts @@ -38,7 +38,7 @@ AwaitingVerificationConfirmation - + Awaiting Confirmation @@ -48,7 +48,7 @@ - + Cancel @@ -125,7 +125,7 @@ ChatPage - + Failed to invite user: %1 @@ -157,12 +157,12 @@ - + Confirm invite - + Do you really want to invite %1 (%2)? @@ -227,12 +227,12 @@ - + Do you really want to start a private chat with %1? - + Cache migration failed! @@ -352,7 +352,7 @@ CrossSigningSecrets - + Decrypt secrets @@ -426,12 +426,12 @@ EmojiPicker - + Search - + People @@ -604,10 +604,48 @@ + + ImagePackSettingsDialog + + + Image pack settings + + + + + Private pack + + + + + Pack from this room + + + + + Globally enabled pack + + + + + Enable globally + + + + + Enables this pack to be used in all rooms + + + + + Close + + + InputBar - + Select a file @@ -617,16 +655,42 @@ - + Failed to upload media. Please try again. - InviteeItem + InviteDialog - - Remove + + Invite users to %1 + + + + + User ID to invite + + + + + @joe:matrix.org + Example user id. The name 'joe' can be localized however you want. + + + + + Add + + + + + Invite + + + + + Cancel @@ -740,28 +804,15 @@ Example: https://server.my:8787 - - MemberList - - - Room members - - - - - OK - - - MessageDelegate - + Encryption enabled - + room name changed to: %1 @@ -771,7 +822,7 @@ Example: https://server.my:8787 - + topic changed to: %1 @@ -781,17 +832,17 @@ Example: https://server.my:8787 - + %1 changed the room avatar - + %1 created and configured room: %2 - + %1 placed a voice call. @@ -806,23 +857,23 @@ Example: https://server.my:8787 - + Negotiating call... - + %1 answered the call. - + removed - + %1 ended the call. @@ -830,7 +881,7 @@ Example: https://server.my:8787 MessageInput - + Hang up @@ -851,6 +902,11 @@ Example: https://server.my:8787 + Stickers + + + + Emoji @@ -868,17 +924,17 @@ Example: https://server.my:8787 MessageView - + Edit - + React - + Reply @@ -888,7 +944,7 @@ Example: https://server.my:8787 - + &Copy @@ -1096,7 +1152,7 @@ Example: https://server.my:8787 Placeholder - + unimplemented event: @@ -1216,7 +1272,7 @@ Example: https://server.my:8787 ReplyPopup - + Close @@ -1229,7 +1285,7 @@ Example: https://server.my:8787 RoomInfo - + no version stored @@ -1237,7 +1293,7 @@ Example: https://server.my:8787 RoomList - + New tag @@ -1246,6 +1302,16 @@ Example: https://server.my:8787 Enter the tag you want to use: + + + Leave Room + + + + + Are you sure you want to leave this room? + + Leave room @@ -1277,17 +1343,7 @@ Example: https://server.my:8787 - - Accept - - - - - Decline - - - - + Status Message @@ -1337,20 +1393,42 @@ Example: https://server.my:8787 + + RoomMembers + + + Members of %1 + + + + + %n people in %1 + Summary above list of members + + + + + + + + Invite more people + + + RoomSettings - + Room Settings - + %1 member(s) - + SETTINGS @@ -1406,19 +1484,22 @@ Example: https://server.my:8787 - - Respond to key requests + + Sticker & Emote Settings - - Whether or not the client should respond automatically with the session keys - upon request. Use with caution, this is a temporary measure to test the - E2E implementation until device verification is completed. + + Change - + + Change what packs are enabled, remove packs or create new ones + + + + INFO @@ -1433,12 +1514,7 @@ Example: https://server.my:8787 - - OK - - - - + Failed to enable encryption: %1 @@ -1469,6 +1545,24 @@ Example: https://server.my:8787 + + RoomlistModel + + + Pending invite. + + + + + Previewing this room + + + + + No preview available + + + ScreenShare @@ -1526,7 +1620,7 @@ Example: https://server.my:8787 StatusIndicator - + Failed @@ -1546,6 +1640,14 @@ Example: https://server.my:8787 + + StickerPicker + + + Search + + + Success @@ -1567,7 +1669,7 @@ Example: https://server.my:8787 TimelineModel - + Message redaction failed: %1 @@ -1578,7 +1680,7 @@ Example: https://server.my:8787 - + Save image @@ -1712,12 +1814,12 @@ Example: https://server.my:8787 - + You joined this room. - + %1 has changed their avatar and changed their display name to %2. @@ -1746,7 +1848,7 @@ Example: https://server.my:8787 TimelineRow - + Edited @@ -1754,17 +1856,32 @@ Example: https://server.my:8787 TimelineView - + No room open - + %1 member(s) - + + join the conversation + + + + + accept invite + + + + + decline invite + + + + Back to room list @@ -1772,7 +1889,7 @@ Example: https://server.my:8787 TimelineViewManager - + No encrypted private chat found with this user. Create an encrypted private chat with this user and try again. @@ -1780,18 +1897,17 @@ Example: https://server.my:8787 TopBar - + Back to room list - - + No room selected - + Room options @@ -1832,7 +1948,7 @@ Example: https://server.my:8787 UserProfile - + Global User Profile @@ -1842,7 +1958,7 @@ Example: https://server.my:8787 - + Verify @@ -1891,7 +2007,7 @@ Example: https://server.my:8787 UserSettings - + Default @@ -1900,7 +2016,7 @@ Example: https://server.my:8787 UserSettingsPage - + Minimize to tray @@ -2346,7 +2462,7 @@ This usually causes the application icon in the task bar to animate in some fash Waiting - + Waiting for other party… @@ -2397,7 +2513,7 @@ This usually causes the application icon in the task bar to animate in some fash descriptiveTime - + Yesterday @@ -2468,19 +2584,6 @@ This usually causes the application icon in the task bar to animate in some fash - - dialogs::InviteUsers - - - Cancel - - - - - User ID to invite - - - dialogs::JoinRoom @@ -2592,7 +2695,7 @@ Media size: %2 message-description sent: - + You sent an audio clip diff --git a/resources/langs/nheko_ro.ts b/resources/langs/nheko_ro.ts index c21bb069..6ea496f1 100644 --- a/resources/langs/nheko_ro.ts +++ b/resources/langs/nheko_ro.ts @@ -38,7 +38,7 @@ AwaitingVerificationConfirmation - + Awaiting Confirmation @@ -48,7 +48,7 @@ - + Cancel @@ -125,7 +125,7 @@ ChatPage - + Failed to invite user: %1 Nu s-a putut invita utilizatorul: %1 @@ -157,12 +157,12 @@ - + Confirm invite - + Do you really want to invite %1 (%2)? @@ -227,12 +227,12 @@ Utilizator dezinterzis: %1 - + Do you really want to start a private chat with %1? - + Cache migration failed! Nu s-a putut migra cache-ul! @@ -352,7 +352,7 @@ CrossSigningSecrets - + Decrypt secrets @@ -426,12 +426,12 @@ EmojiPicker - + Search - + People @@ -604,10 +604,48 @@ + + ImagePackSettingsDialog + + + Image pack settings + + + + + Private pack + + + + + Pack from this room + + + + + Globally enabled pack + + + + + Enable globally + + + + + Enables this pack to be used in all rooms + + + + + Close + Închide + + InputBar - + Select a file @@ -617,17 +655,43 @@ Toate fișierele (*) - + Failed to upload media. Please try again. - InviteeItem + InviteDialog - - Remove - Ștergere + + Invite users to %1 + + + + + User ID to invite + IDul utilizatorului de invitat + + + + @joe:matrix.org + Example user id. The name 'joe' can be localized however you want. + + + + + Add + + + + + Invite + + + + + Cancel + @@ -744,23 +808,10 @@ Exemplu: https://serverul.meu:8787 Conectarea SSO a eșuat - - MemberList - - - Room members - Membrii camerei - - - - OK - OK - - MessageDelegate - + removed @@ -771,7 +822,7 @@ Exemplu: https://serverul.meu:8787 Criptare activată - + room name changed to: %1 numele camerei schimbat la: %1 @@ -781,7 +832,7 @@ Exemplu: https://serverul.meu:8787 numele camerei șters - + topic changed to: %1 subiect schimbat la: %1 @@ -791,17 +842,17 @@ Exemplu: https://serverul.meu:8787 subiect șters - + %1 changed the room avatar - + %1 created and configured room: %2 %1 a creat și configurat camera: %2 - + %1 placed a voice call. @@ -816,17 +867,17 @@ Exemplu: https://serverul.meu:8787 - + %1 answered the call. %1 a răspuns apelului. - + %1 ended the call. %1 a închis apelul. - + Negotiating call... @@ -834,7 +885,7 @@ Exemplu: https://serverul.meu:8787 MessageInput - + Hang up @@ -855,6 +906,11 @@ Exemplu: https://serverul.meu:8787 + Stickers + + + + Emoji @@ -872,17 +928,17 @@ Exemplu: https://serverul.meu:8787 MessageView - + Edit - + React - + Reply Răspuns @@ -892,7 +948,7 @@ Exemplu: https://serverul.meu:8787 Opțiuni - + &Copy @@ -1100,7 +1156,7 @@ Exemplu: https://serverul.meu:8787 Placeholder - + unimplemented event: eveniment neimplementat: @@ -1220,7 +1276,7 @@ Exemplu: https://serverul.meu:8787 ReplyPopup - + Close Închide @@ -1233,7 +1289,7 @@ Exemplu: https://serverul.meu:8787 RoomInfo - + no version stored nicio versiune stocată @@ -1241,7 +1297,7 @@ Exemplu: https://serverul.meu:8787 RoomList - + New tag @@ -1250,6 +1306,16 @@ Exemplu: https://serverul.meu:8787 Enter the tag you want to use: + + + Leave Room + + + + + Are you sure you want to leave this room? + + Leave room @@ -1281,17 +1347,7 @@ Exemplu: https://serverul.meu:8787 - - Accept - Acceptare - - - - Decline - Refuzare - - - + Status Message @@ -1341,20 +1397,43 @@ Exemplu: https://serverul.meu:8787 Setări utilizator + + RoomMembers + + + Members of %1 + + + + + %n people in %1 + Summary above list of members + + + + + + + + + Invite more people + + + RoomSettings - + Room Settings - + %1 member(s) - + SETTINGS @@ -1410,19 +1489,22 @@ Exemplu: https://serverul.meu:8787 - - Respond to key requests + + Sticker & Emote Settings - - Whether or not the client should respond automatically with the session keys - upon request. Use with caution, this is a temporary measure to test the - E2E implementation until device verification is completed. + + Change - + + Change what packs are enabled, remove packs or create new ones + + + + INFO @@ -1437,12 +1519,7 @@ Exemplu: https://serverul.meu:8787 - - OK - OK - - - + Failed to enable encryption: %1 Nu s-a putut activa criptarea: %1 @@ -1473,6 +1550,24 @@ Exemplu: https://serverul.meu:8787 Nu s-a putut încărca imaginea: %s + + RoomlistModel + + + Pending invite. + + + + + Previewing this room + + + + + No preview available + + + ScreenShare @@ -1530,7 +1625,7 @@ Exemplu: https://serverul.meu:8787 StatusIndicator - + Failed Eșuat @@ -1550,6 +1645,14 @@ Exemplu: https://serverul.meu:8787 Citit + + StickerPicker + + + Search + + + Success @@ -1571,7 +1674,7 @@ Exemplu: https://serverul.meu:8787 TimelineModel - + Message redaction failed: %1 Redactare mesaj eșuată: %1 @@ -1582,7 +1685,7 @@ Exemplu: https://serverul.meu:8787 - + Save image Salvați imaginea @@ -1717,12 +1820,12 @@ Exemplu: https://serverul.meu:8787 %1 și-a redactat ciocănitul. - + You joined this room. Te-ai alăturat camerei. - + %1 has changed their avatar and changed their display name to %2. @@ -1751,7 +1854,7 @@ Exemplu: https://serverul.meu:8787 TimelineRow - + Edited @@ -1759,17 +1862,32 @@ Exemplu: https://serverul.meu:8787 TimelineView - + No room open Nicio cameră deschisă - + %1 member(s) - + + join the conversation + + + + + accept invite + + + + + decline invite + + + + Back to room list @@ -1777,7 +1895,7 @@ Exemplu: https://serverul.meu:8787 TimelineViewManager - + No encrypted private chat found with this user. Create an encrypted private chat with this user and try again. @@ -1785,18 +1903,17 @@ Exemplu: https://serverul.meu:8787 TopBar - + Back to room list - - + No room selected - + Room options @@ -1837,7 +1954,7 @@ Exemplu: https://serverul.meu:8787 UserProfile - + Global User Profile @@ -1847,7 +1964,7 @@ Exemplu: https://serverul.meu:8787 - + Verify @@ -1896,7 +2013,7 @@ Exemplu: https://serverul.meu:8787 UserSettings - + Default @@ -1905,7 +2022,7 @@ Exemplu: https://serverul.meu:8787 UserSettingsPage - + Minimize to tray Minimizează în bara de notificări @@ -2351,7 +2468,7 @@ This usually causes the application icon in the task bar to animate in some fash Waiting - + Waiting for other party… @@ -2402,7 +2519,7 @@ This usually causes the application icon in the task bar to animate in some fash descriptiveTime - + Yesterday Ieri @@ -2473,19 +2590,6 @@ This usually causes the application icon in the task bar to animate in some fash Deschideți fallback, urmăriți pașii și confirmați după ce i-ați completat. - - dialogs::InviteUsers - - - Cancel - Anulare - - - - User ID to invite - IDul utilizatorului de invitat - - dialogs::JoinRoom @@ -2599,7 +2703,7 @@ Dimensiune media: %2 message-description sent: - + You sent an audio clip Ai trimis un clip audio diff --git a/resources/langs/nheko_ru.ts b/resources/langs/nheko_ru.ts index 6f2b19af..67a306f2 100644 --- a/resources/langs/nheko_ru.ts +++ b/resources/langs/nheko_ru.ts @@ -38,7 +38,7 @@ AwaitingVerificationConfirmation - + Awaiting Confirmation Ожидание Подтверждения @@ -48,7 +48,7 @@ Ожидание подтверждения у собеседника. - + Cancel Отмена @@ -125,7 +125,7 @@ ChatPage - + Failed to invite user: %1 Не удалось пригласить пользователя: %1 @@ -157,12 +157,12 @@ - + Confirm invite Подтвердите приглашение - + Do you really want to invite %1 (%2)? Вы точно хотите пригласить %1 (%2)? @@ -227,12 +227,12 @@ Разблокированный пользователь: %1 - + Do you really want to start a private chat with %1? Вы действительно хотите начать личную переписку с %1? - + Cache migration failed! Миграция кэша не удалась! @@ -352,7 +352,7 @@ CrossSigningSecrets - + Decrypt secrets Расшифровать секреты @@ -426,12 +426,12 @@ EmojiPicker - + Search Поиск - + People Люди @@ -604,10 +604,48 @@ Переслать Сообщение + + ImagePackSettingsDialog + + + Image pack settings + + + + + Private pack + + + + + Pack from this room + + + + + Globally enabled pack + + + + + Enable globally + + + + + Enables this pack to be used in all rooms + + + + + Close + Закрыть + + InputBar - + Select a file Выберите файл @@ -617,17 +655,43 @@ Все файлы (*) - + Failed to upload media. Please try again. Не удалось загрузить медиа. Пожалуйста попробуйте ещё раз - InviteeItem + InviteDialog - - Remove - Удалить + + Invite users to %1 + + + + + User ID to invite + Идентификатор пользователя + + + + @joe:matrix.org + Example user id. The name 'joe' can be localized however you want. + + + + + Add + + + + + Invite + + + + + Cancel + @@ -744,23 +808,10 @@ Example: https://server.my:8787 SSO вход не удался - - MemberList - - - Room members - Участники комнаты - - - - OK - ОК - - MessageDelegate - + removed убрано @@ -771,7 +822,7 @@ Example: https://server.my:8787 Шифрование включено - + room name changed to: %1 имя комнаты изменено на: %1 @@ -781,7 +832,7 @@ Example: https://server.my:8787 название комнаты убрано - + topic changed to: %1 тема изменена на: %1 @@ -791,17 +842,17 @@ Example: https://server.my:8787 тема убрана - + %1 changed the room avatar - + %1 created and configured room: %2 %1 создал и настроил комнату: %2 - + %1 placed a voice call. %1 начал голосовой звонок. @@ -816,17 +867,17 @@ Example: https://server.my:8787 %1 начал вызов. - + %1 answered the call. %1 ответил на звонок. - + %1 ended the call. %1 завершил вызов. - + Negotiating call... Совершение звонка... @@ -834,7 +885,7 @@ Example: https://server.my:8787 MessageInput - + Hang up Завершить звонок @@ -855,6 +906,11 @@ Example: https://server.my:8787 + Stickers + + + + Emoji Эмоджи @@ -872,17 +928,17 @@ Example: https://server.my:8787 MessageView - + Edit Редактировать - + React Реакция - + Reply Ответить @@ -892,7 +948,7 @@ Example: https://server.my:8787 Опции - + &Copy @@ -1100,7 +1156,7 @@ Example: https://server.my:8787 Placeholder - + unimplemented event: не реализованное событие @@ -1220,7 +1276,7 @@ Example: https://server.my:8787 ReplyPopup - + Close Закрыть @@ -1233,7 +1289,7 @@ Example: https://server.my:8787 RoomInfo - + no version stored нет сохраненной версии @@ -1241,7 +1297,7 @@ Example: https://server.my:8787 RoomList - + New tag @@ -1250,6 +1306,16 @@ Example: https://server.my:8787 Enter the tag you want to use: + + + Leave Room + + + + + Are you sure you want to leave this room? + + Leave room @@ -1281,17 +1347,7 @@ Example: https://server.my:8787 - - Accept - Принять - - - - Decline - Отказаться - - - + Status Message @@ -1341,20 +1397,43 @@ Example: https://server.my:8787 Пользовательские настройки + + RoomMembers + + + Members of %1 + + + + + %n people in %1 + Summary above list of members + + + + + + + + + Invite more people + + + RoomSettings - + Room Settings Настройки комнаты - + %1 member(s) %1 участник(ов) - + SETTINGS НАЙСТРОЙКИ @@ -1410,19 +1489,22 @@ Example: https://server.my:8787 - - Respond to key requests - Отвечать на запрос ключей - - - - Whether or not the client should respond automatically with the session keys - upon request. Use with caution, this is a temporary measure to test the - E2E implementation until device verification is completed. + + Sticker & Emote Settings - + + Change + + + + + Change what packs are enabled, remove packs or create new ones + + + + INFO ИНФОРМАЦИЯ @@ -1437,12 +1519,7 @@ Example: https://server.my:8787 Версия Комнаты - - OK - ОК - - - + Failed to enable encryption: %1 Не удалось включить шифрование: %1 @@ -1473,6 +1550,24 @@ Example: https://server.my:8787 Не удалось загрузить изображение: %s + + RoomlistModel + + + Pending invite. + + + + + Previewing this room + + + + + No preview available + + + ScreenShare @@ -1530,7 +1625,7 @@ Example: https://server.my:8787 StatusIndicator - + Failed Не удалоcь @@ -1550,6 +1645,14 @@ Example: https://server.my:8787 Прочитано + + StickerPicker + + + Search + Поиск + + Success @@ -1571,7 +1674,7 @@ Example: https://server.my:8787 TimelineModel - + Message redaction failed: %1 Ошибка редактирования сообщения: %1 @@ -1582,7 +1685,7 @@ Example: https://server.my:8787 Не удалось зашифровать сообщение, отправка отменена! - + Save image Сохранить изображение @@ -1717,12 +1820,12 @@ Example: https://server.my:8787 %1 отредактировал его "стук". - + You joined this room. Вы присоединились к этой комнате. - + %1 has changed their avatar and changed their display name to %2. @@ -1751,7 +1854,7 @@ Example: https://server.my:8787 TimelineRow - + Edited Изменено @@ -1759,17 +1862,32 @@ Example: https://server.my:8787 TimelineView - + No room open Комната не выбрана - + %1 member(s) %1 участник(ов) - + + join the conversation + + + + + accept invite + + + + + decline invite + + + + Back to room list Вернуться к списку комнат @@ -1777,7 +1895,7 @@ Example: https://server.my:8787 TimelineViewManager - + No encrypted private chat found with this user. Create an encrypted private chat with this user and try again. Не найдено личного чата с этим пользователем. Создайте зашифрованный личный чат с этим пользователем и попытайтесь еще раз. @@ -1785,18 +1903,17 @@ Example: https://server.my:8787 TopBar - + Back to room list Вернуться к списку комнат - - + No room selected Комнаты не выбраны - + Room options Настройки комнаты @@ -1837,7 +1954,7 @@ Example: https://server.my:8787 UserProfile - + Global User Profile Глобальный Пользовательский Профиль @@ -1847,7 +1964,7 @@ Example: https://server.my:8787 Поользовательский Профиль в Комнате - + Verify Верифицировать @@ -1896,7 +2013,7 @@ Example: https://server.my:8787 UserSettings - + Default По умолчанию @@ -1905,7 +2022,7 @@ Example: https://server.my:8787 UserSettingsPage - + Minimize to tray Сворачивать в системную панель @@ -2357,7 +2474,7 @@ This usually causes the application icon in the task bar to animate in some fash Waiting - + Waiting for other party… @@ -2408,7 +2525,7 @@ This usually causes the application icon in the task bar to animate in some fash descriptiveTime - + Yesterday Вчера @@ -2479,19 +2596,6 @@ This usually causes the application icon in the task bar to animate in some fash Запустите резервный вариант, пройдите его шаги и подтвердите завершение. - - dialogs::InviteUsers - - - Cancel - Отмена - - - - User ID to invite - Идентификатор пользователя - - dialogs::JoinRoom @@ -2605,7 +2709,7 @@ Media size: %2 message-description sent: - + You sent an audio clip Вы отправили аудио запись diff --git a/resources/langs/nheko_si.ts b/resources/langs/nheko_si.ts index a80adb1b..cf425990 100644 --- a/resources/langs/nheko_si.ts +++ b/resources/langs/nheko_si.ts @@ -38,7 +38,7 @@ AwaitingVerificationConfirmation - + Awaiting Confirmation @@ -48,7 +48,7 @@ - + Cancel @@ -125,7 +125,7 @@ ChatPage - + Failed to invite user: %1 @@ -157,12 +157,12 @@ - + Confirm invite - + Do you really want to invite %1 (%2)? @@ -227,12 +227,12 @@ - + Do you really want to start a private chat with %1? - + Cache migration failed! @@ -352,7 +352,7 @@ CrossSigningSecrets - + Decrypt secrets @@ -426,12 +426,12 @@ EmojiPicker - + Search - + People @@ -604,10 +604,48 @@ + + ImagePackSettingsDialog + + + Image pack settings + + + + + Private pack + + + + + Pack from this room + + + + + Globally enabled pack + + + + + Enable globally + + + + + Enables this pack to be used in all rooms + + + + + Close + + + InputBar - + Select a file @@ -617,16 +655,42 @@ - + Failed to upload media. Please try again. - InviteeItem + InviteDialog - - Remove + + Invite users to %1 + + + + + User ID to invite + + + + + @joe:matrix.org + Example user id. The name 'joe' can be localized however you want. + + + + + Add + + + + + Invite + + + + + Cancel @@ -740,23 +804,10 @@ Example: https://server.my:8787 - - MemberList - - - Room members - - - - - OK - - - MessageDelegate - + removed @@ -767,7 +818,7 @@ Example: https://server.my:8787 - + room name changed to: %1 @@ -777,7 +828,7 @@ Example: https://server.my:8787 - + topic changed to: %1 @@ -787,17 +838,17 @@ Example: https://server.my:8787 - + %1 changed the room avatar - + %1 created and configured room: %2 - + %1 placed a voice call. @@ -812,17 +863,17 @@ Example: https://server.my:8787 - + %1 answered the call. - + %1 ended the call. - + Negotiating call... @@ -830,7 +881,7 @@ Example: https://server.my:8787 MessageInput - + Hang up @@ -851,6 +902,11 @@ Example: https://server.my:8787 + Stickers + + + + Emoji @@ -868,17 +924,17 @@ Example: https://server.my:8787 MessageView - + Edit - + React - + Reply @@ -888,7 +944,7 @@ Example: https://server.my:8787 - + &Copy @@ -1096,7 +1152,7 @@ Example: https://server.my:8787 Placeholder - + unimplemented event: @@ -1216,7 +1272,7 @@ Example: https://server.my:8787 ReplyPopup - + Close @@ -1229,7 +1285,7 @@ Example: https://server.my:8787 RoomInfo - + no version stored @@ -1237,7 +1293,7 @@ Example: https://server.my:8787 RoomList - + New tag @@ -1246,6 +1302,16 @@ Example: https://server.my:8787 Enter the tag you want to use: + + + Leave Room + + + + + Are you sure you want to leave this room? + + Leave room @@ -1277,17 +1343,7 @@ Example: https://server.my:8787 - - Accept - - - - - Decline - - - - + Status Message @@ -1337,20 +1393,42 @@ Example: https://server.my:8787 + + RoomMembers + + + Members of %1 + + + + + %n people in %1 + Summary above list of members + + + + + + + + Invite more people + + + RoomSettings - + Room Settings - + %1 member(s) - + SETTINGS @@ -1406,19 +1484,22 @@ Example: https://server.my:8787 - - Respond to key requests + + Sticker & Emote Settings - - Whether or not the client should respond automatically with the session keys - upon request. Use with caution, this is a temporary measure to test the - E2E implementation until device verification is completed. + + Change - + + Change what packs are enabled, remove packs or create new ones + + + + INFO @@ -1433,12 +1514,7 @@ Example: https://server.my:8787 - - OK - - - - + Failed to enable encryption: %1 @@ -1469,6 +1545,24 @@ Example: https://server.my:8787 + + RoomlistModel + + + Pending invite. + + + + + Previewing this room + + + + + No preview available + + + ScreenShare @@ -1526,7 +1620,7 @@ Example: https://server.my:8787 StatusIndicator - + Failed @@ -1546,6 +1640,14 @@ Example: https://server.my:8787 + + StickerPicker + + + Search + + + Success @@ -1567,7 +1669,7 @@ Example: https://server.my:8787 TimelineModel - + Message redaction failed: %1 @@ -1578,7 +1680,7 @@ Example: https://server.my:8787 - + Save image @@ -1712,12 +1814,12 @@ Example: https://server.my:8787 - + You joined this room. - + %1 has changed their avatar and changed their display name to %2. @@ -1746,7 +1848,7 @@ Example: https://server.my:8787 TimelineRow - + Edited @@ -1754,17 +1856,32 @@ Example: https://server.my:8787 TimelineView - + No room open - + %1 member(s) - + + join the conversation + + + + + accept invite + + + + + decline invite + + + + Back to room list @@ -1772,7 +1889,7 @@ Example: https://server.my:8787 TimelineViewManager - + No encrypted private chat found with this user. Create an encrypted private chat with this user and try again. @@ -1780,18 +1897,17 @@ Example: https://server.my:8787 TopBar - + Back to room list - - + No room selected - + Room options @@ -1832,7 +1948,7 @@ Example: https://server.my:8787 UserProfile - + Global User Profile @@ -1842,7 +1958,7 @@ Example: https://server.my:8787 - + Verify @@ -1891,7 +2007,7 @@ Example: https://server.my:8787 UserSettings - + Default @@ -1900,7 +2016,7 @@ Example: https://server.my:8787 UserSettingsPage - + Minimize to tray @@ -2346,7 +2462,7 @@ This usually causes the application icon in the task bar to animate in some fash Waiting - + Waiting for other party… @@ -2397,7 +2513,7 @@ This usually causes the application icon in the task bar to animate in some fash descriptiveTime - + Yesterday @@ -2468,19 +2584,6 @@ This usually causes the application icon in the task bar to animate in some fash - - dialogs::InviteUsers - - - Cancel - - - - - User ID to invite - - - dialogs::JoinRoom @@ -2592,7 +2695,7 @@ Media size: %2 message-description sent: - + You sent an audio clip diff --git a/resources/langs/nheko_sv.ts b/resources/langs/nheko_sv.ts index 8069dcea..25db1c4b 100644 --- a/resources/langs/nheko_sv.ts +++ b/resources/langs/nheko_sv.ts @@ -38,7 +38,7 @@ AwaitingVerificationConfirmation - + Awaiting Confirmation Inväntar Bekräftelse @@ -48,7 +48,7 @@ Väntar på att motparten ska slutföra verifikationen. - + Cancel Avbryt @@ -125,7 +125,7 @@ ChatPage - + Failed to invite user: %1 Kunde inte bjuda in användare: %1 @@ -157,12 +157,12 @@ - + Confirm invite Bekräfta inbjudan - + Do you really want to invite %1 (%2)? Är du säker på att du vill bjuda in %1 (%2)? @@ -227,12 +227,12 @@ Hävde bannlysningen av användare: %1 - + Do you really want to start a private chat with %1? - + Cache migration failed! Cache-migration misslyckades! @@ -352,7 +352,7 @@ CrossSigningSecrets - + Decrypt secrets Dekryptera hemliga nycklar @@ -426,12 +426,12 @@ EmojiPicker - + Search Sök - + People Personer @@ -604,10 +604,48 @@ + + ImagePackSettingsDialog + + + Image pack settings + + + + + Private pack + + + + + Pack from this room + + + + + Globally enabled pack + + + + + Enable globally + + + + + Enables this pack to be used in all rooms + + + + + Close + Stäng + + InputBar - + Select a file Välj en fil @@ -617,17 +655,43 @@ Alla Filer (*) - + Failed to upload media. Please try again. Kunde inte ladda upp media. Vänligen försök igen. - InviteeItem + InviteDialog - - Remove - Ta bort + + Invite users to %1 + + + + + User ID to invite + Användar-ID att bjuda in + + + + @joe:matrix.org + Example user id. The name 'joe' can be localized however you want. + + + + + Add + + + + + Invite + + + + + Cancel + Avbryt @@ -744,28 +808,15 @@ Exempel: https://server.my:8787 SSO-inloggning misslyckades - - MemberList - - - Room members - Rumsmedlemmar - - - - OK - OK - - MessageDelegate - + Encryption enabled Kryptering aktiverad - + room name changed to: %1 rummets namn ändrat till: %1 @@ -775,7 +826,7 @@ Exempel: https://server.my:8787 tog bort rummets namn - + topic changed to: %1 ämne ändrat till: %1 @@ -785,17 +836,17 @@ Exempel: https://server.my:8787 tog bort ämne - + %1 changed the room avatar - + %1 created and configured room: %2 %1 skapade och konfigurerade rum: %2 - + %1 placed a voice call. %1 påbörjade ett röstsamtal. @@ -810,23 +861,23 @@ Exempel: https://server.my:8787 %1 påbörjade ett samtal. - + Negotiating call... Förhandlar samtal… - + %1 answered the call. %1 besvarade samtalet. - + removed borttagen - + %1 ended the call. %1 avslutade samtalet. @@ -834,7 +885,7 @@ Exempel: https://server.my:8787 MessageInput - + Hang up Lägg på @@ -855,6 +906,11 @@ Exempel: https://server.my:8787 + Stickers + + + + Emoji Emoji @@ -872,17 +928,17 @@ Exempel: https://server.my:8787 MessageView - + Edit - + React Reagera - + Reply Svara @@ -892,7 +948,7 @@ Exempel: https://server.my:8787 Alternativ - + &Copy @@ -1100,7 +1156,7 @@ Exempel: https://server.my:8787 Placeholder - + unimplemented event: ej implementerat event: @@ -1220,7 +1276,7 @@ Exempel: https://server.my:8787 ReplyPopup - + Close Stäng @@ -1233,7 +1289,7 @@ Exempel: https://server.my:8787 RoomInfo - + no version stored ingen version lagrad @@ -1241,7 +1297,7 @@ Exempel: https://server.my:8787 RoomList - + New tag @@ -1250,6 +1306,16 @@ Exempel: https://server.my:8787 Enter the tag you want to use: + + + Leave Room + + + + + Are you sure you want to leave this room? + + Leave room @@ -1281,17 +1347,7 @@ Exempel: https://server.my:8787 - - Accept - Godkänn - - - - Decline - - - - + Status Message @@ -1341,20 +1397,42 @@ Exempel: https://server.my:8787 Användarinställningar + + RoomMembers + + + Members of %1 + + + + + %n people in %1 + Summary above list of members + + + + + + + + Invite more people + + + RoomSettings - + Room Settings - + %1 member(s) - + SETTINGS @@ -1410,19 +1488,22 @@ Exempel: https://server.my:8787 - - Respond to key requests + + Sticker & Emote Settings - - Whether or not the client should respond automatically with the session keys - upon request. Use with caution, this is a temporary measure to test the - E2E implementation until device verification is completed. + + Change - + + Change what packs are enabled, remove packs or create new ones + + + + INFO @@ -1437,12 +1518,7 @@ Exempel: https://server.my:8787 - - OK - OK - - - + Failed to enable encryption: %1 Kunde inte aktivera kryptering: %1 @@ -1473,6 +1549,24 @@ Exempel: https://server.my:8787 Kunde inte ladda upp bilden: %s + + RoomlistModel + + + Pending invite. + + + + + Previewing this room + + + + + No preview available + + + ScreenShare @@ -1530,7 +1624,7 @@ Exempel: https://server.my:8787 StatusIndicator - + Failed Misslyckat @@ -1550,6 +1644,14 @@ Exempel: https://server.my:8787 Läst + + StickerPicker + + + Search + Sök + + Success @@ -1571,7 +1673,7 @@ Exempel: https://server.my:8787 TimelineModel - + Message redaction failed: %1 Kunde inte maskera meddelande: %1 @@ -1582,7 +1684,7 @@ Exempel: https://server.my:8787 Kunde inte kryptera event, sändning avbruten! - + Save image Spara bild @@ -1716,12 +1818,12 @@ Exempel: https://server.my:8787 %1 maskerade sin knackning. - + You joined this room. Du gick med i detta rum. - + %1 has changed their avatar and changed their display name to %2. @@ -1750,7 +1852,7 @@ Exempel: https://server.my:8787 TimelineRow - + Edited @@ -1758,17 +1860,32 @@ Exempel: https://server.my:8787 TimelineView - + No room open Inget rum öppet - + %1 member(s) - + + join the conversation + + + + + accept invite + + + + + decline invite + + + + Back to room list Tillbaka till rumlista @@ -1776,7 +1893,7 @@ Exempel: https://server.my:8787 TimelineViewManager - + No encrypted private chat found with this user. Create an encrypted private chat with this user and try again. Ingen krypterad privat chatt med denna användare kunde hittas. Skapa en krypterad privat chatt med användaren och försök igen. @@ -1784,18 +1901,17 @@ Exempel: https://server.my:8787 TopBar - + Back to room list Tillbaka till rumlista - - + No room selected Inget rum markerat - + Room options Alternativ för rum @@ -1836,7 +1952,7 @@ Exempel: https://server.my:8787 UserProfile - + Global User Profile @@ -1846,7 +1962,7 @@ Exempel: https://server.my:8787 - + Verify Bekräfta @@ -1895,7 +2011,7 @@ Exempel: https://server.my:8787 UserSettings - + Default @@ -1904,7 +2020,7 @@ Exempel: https://server.my:8787 UserSettingsPage - + Minimize to tray Minimera till systemtråg @@ -2358,7 +2474,7 @@ Detta gör vanligtvis att ikonen i aktivitetsfältet animeras på något sätt.< Waiting - + Waiting for other party… Väntar på motparten… @@ -2409,7 +2525,7 @@ Detta gör vanligtvis att ikonen i aktivitetsfältet animeras på något sätt.< descriptiveTime - + Yesterday Igår @@ -2480,19 +2596,6 @@ Detta gör vanligtvis att ikonen i aktivitetsfältet animeras på något sätt.< Öppna reserven, följ stegen och bekräfta när du slutfört dem. - - dialogs::InviteUsers - - - Cancel - Avbryt - - - - User ID to invite - Användar-ID att bjuda in - - dialogs::JoinRoom @@ -2606,7 +2709,7 @@ Mediastorlek: %2 message-description sent: - + You sent an audio clip Du skickade ett ljudklipp diff --git a/resources/langs/nheko_zh_CN.ts b/resources/langs/nheko_zh_CN.ts index d468dfaa..75e9db95 100644 --- a/resources/langs/nheko_zh_CN.ts +++ b/resources/langs/nheko_zh_CN.ts @@ -38,7 +38,7 @@ AwaitingVerificationConfirmation - + Awaiting Confirmation @@ -48,7 +48,7 @@ - + Cancel 取消 @@ -125,7 +125,7 @@ ChatPage - + Failed to invite user: %1 邀请用户失败: %1 @@ -157,12 +157,12 @@ - + Confirm invite - + Do you really want to invite %1 (%2)? @@ -227,12 +227,12 @@ 解禁用户: %1 - + Do you really want to start a private chat with %1? - + Cache migration failed! 缓存迁移失败! @@ -352,7 +352,7 @@ CrossSigningSecrets - + Decrypt secrets @@ -426,12 +426,12 @@ EmojiPicker - + Search - + People @@ -604,10 +604,48 @@ + + ImagePackSettingsDialog + + + Image pack settings + + + + + Private pack + + + + + Pack from this room + + + + + Globally enabled pack + + + + + Enable globally + + + + + Enables this pack to be used in all rooms + + + + + Close + + + InputBar - + Select a file 选择一个文件 @@ -617,17 +655,43 @@ 所有文件(*) - + Failed to upload media. Please try again. - InviteeItem + InviteDialog - - Remove - 移除 + + Invite users to %1 + + + + + User ID to invite + 要邀请的用户 ID + + + + @joe:matrix.org + Example user id. The name 'joe' can be localized however you want. + + + + + Add + + + + + Invite + + + + + Cancel + 取消 @@ -740,23 +804,10 @@ Example: https://server.my:8787 - - MemberList - - - Room members - 聊天室成员 - - - - OK - - - MessageDelegate - + removed @@ -767,7 +818,7 @@ Example: https://server.my:8787 - + room name changed to: %1 @@ -777,7 +828,7 @@ Example: https://server.my:8787 - + topic changed to: %1 @@ -787,17 +838,17 @@ Example: https://server.my:8787 - + %1 changed the room avatar - + %1 created and configured room: %2 - + %1 placed a voice call. @@ -812,17 +863,17 @@ Example: https://server.my:8787 - + %1 answered the call. - + %1 ended the call. - + Negotiating call... @@ -830,7 +881,7 @@ Example: https://server.my:8787 MessageInput - + Hang up @@ -851,6 +902,11 @@ Example: https://server.my:8787 + Stickers + + + + Emoji @@ -868,17 +924,17 @@ Example: https://server.my:8787 MessageView - + Edit - + React - + Reply @@ -888,7 +944,7 @@ Example: https://server.my:8787 - + &Copy @@ -1096,7 +1152,7 @@ Example: https://server.my:8787 Placeholder - + unimplemented event: @@ -1216,7 +1272,7 @@ Example: https://server.my:8787 ReplyPopup - + Close @@ -1229,7 +1285,7 @@ Example: https://server.my:8787 RoomInfo - + no version stored @@ -1237,7 +1293,7 @@ Example: https://server.my:8787 RoomList - + New tag @@ -1246,6 +1302,16 @@ Example: https://server.my:8787 Enter the tag you want to use: + + + Leave Room + + + + + Are you sure you want to leave this room? + + Leave room @@ -1277,17 +1343,7 @@ Example: https://server.my:8787 - - Accept - 接受 - - - - Decline - 拒绝 - - - + Status Message @@ -1337,20 +1393,41 @@ Example: https://server.my:8787 用户设置 + + RoomMembers + + + Members of %1 + + + + + %n people in %1 + Summary above list of members + + + + + + + Invite more people + + + RoomSettings - + Room Settings - + %1 member(s) - + SETTINGS @@ -1406,19 +1483,22 @@ Example: https://server.my:8787 - - Respond to key requests + + Sticker & Emote Settings - - Whether or not the client should respond automatically with the session keys - upon request. Use with caution, this is a temporary measure to test the - E2E implementation until device verification is completed. + + Change - + + Change what packs are enabled, remove packs or create new ones + + + + INFO @@ -1433,12 +1513,7 @@ Example: https://server.my:8787 - - OK - - - - + Failed to enable encryption: %1 启用加密失败:%1 @@ -1469,6 +1544,24 @@ Example: https://server.my:8787 上传图像失败:%s + + RoomlistModel + + + Pending invite. + + + + + Previewing this room + + + + + No preview available + + + ScreenShare @@ -1526,7 +1619,7 @@ Example: https://server.my:8787 StatusIndicator - + Failed @@ -1546,6 +1639,14 @@ Example: https://server.my:8787 + + StickerPicker + + + Search + + + Success @@ -1567,7 +1668,7 @@ Example: https://server.my:8787 TimelineModel - + Message redaction failed: %1 删除消息失败:%1 @@ -1578,7 +1679,7 @@ Example: https://server.my:8787 - + Save image 保存图像 @@ -1711,12 +1812,12 @@ Example: https://server.my:8787 - + You joined this room. 您已加入此房间 - + %1 has changed their avatar and changed their display name to %2. @@ -1745,7 +1846,7 @@ Example: https://server.my:8787 TimelineRow - + Edited @@ -1753,17 +1854,32 @@ Example: https://server.my:8787 TimelineView - + No room open - + %1 member(s) - + + join the conversation + + + + + accept invite + + + + + decline invite + + + + Back to room list @@ -1771,7 +1887,7 @@ Example: https://server.my:8787 TimelineViewManager - + No encrypted private chat found with this user. Create an encrypted private chat with this user and try again. @@ -1779,18 +1895,17 @@ Example: https://server.my:8787 TopBar - + Back to room list - - + No room selected - + Room options 聊天室选项 @@ -1831,7 +1946,7 @@ Example: https://server.my:8787 UserProfile - + Global User Profile @@ -1841,7 +1956,7 @@ Example: https://server.my:8787 - + Verify @@ -1890,7 +2005,7 @@ Example: https://server.my:8787 UserSettings - + Default @@ -1899,7 +2014,7 @@ Example: https://server.my:8787 UserSettingsPage - + Minimize to tray 最小化至托盘 @@ -2345,7 +2460,7 @@ This usually causes the application icon in the task bar to animate in some fash Waiting - + Waiting for other party… @@ -2396,7 +2511,7 @@ This usually causes the application icon in the task bar to animate in some fash descriptiveTime - + Yesterday @@ -2467,19 +2582,6 @@ This usually causes the application icon in the task bar to animate in some fash - - dialogs::InviteUsers - - - Cancel - 取消 - - - - User ID to invite - 要邀请的用户 ID - - dialogs::JoinRoom @@ -2593,7 +2695,7 @@ Media size: %2 message-description sent: - + You sent an audio clip diff --git a/resources/qml/Avatar.qml b/resources/qml/Avatar.qml index 6c12952a..9685dde1 100644 --- a/resources/qml/Avatar.qml +++ b/resources/qml/Avatar.qml @@ -11,10 +11,11 @@ import im.nheko 1.0 Rectangle { id: avatar - property alias url: img.source + property string url property string userid property string displayName property alias textColor: label.color + property bool crop: true signal clicked(var mouse) @@ -44,12 +45,13 @@ Rectangle { anchors.fill: parent asynchronous: true - fillMode: Image.PreserveAspectCrop + fillMode: avatar.crop ? Image.PreserveAspectCrop : Image.PreserveAspectFit mipmap: true smooth: true sourceSize.width: avatar.width sourceSize.height: avatar.height layer.enabled: true + source: avatar.url + ((avatar.crop || !avatar.url) ? "" : "?scale") MouseArea { id: mouseArea diff --git a/resources/qml/ChatPage.qml b/resources/qml/ChatPage.qml index ae37187b..e56d7d46 100644 --- a/resources/qml/ChatPage.qml +++ b/resources/qml/ChatPage.qml @@ -56,6 +56,7 @@ Rectangle { RoomList { id: roomlist + implicitHeight: chatPage.height collapsed: parent.collapsed } diff --git a/resources/qml/Completer.qml b/resources/qml/Completer.qml index 333fb11d..00fc3216 100644 --- a/resources/qml/Completer.qml +++ b/resources/qml/Completer.qml @@ -70,7 +70,7 @@ Popup { onCompleterNameChanged: { if (completerName) { if (completerName == "user") - completer = TimelineManager.completerFor(completerName, room.roomId()); + completer = TimelineManager.completerFor(completerName, room.roomId); else completer = TimelineManager.completerFor(completerName); completer.setSearchString(""); diff --git a/resources/qml/ForwardCompleter.qml b/resources/qml/ForwardCompleter.qml index 127b59c2..525477cd 100644 --- a/resources/qml/ForwardCompleter.qml +++ b/resources/qml/ForwardCompleter.qml @@ -109,15 +109,17 @@ Popup { } Connections { - onCompletionSelected: { + function onCompletionSelected(id) { room.forwardMessage(messageContextMenu.eventId, id); forwardMessagePopup.close(); } - onCountChanged: { + + function onCountChanged() { if (completerPopup.count > 0 && (completerPopup.currentIndex < 0 || completerPopup.currentIndex >= completerPopup.count)) completerPopup.currentIndex = 0; } + target: completerPopup } diff --git a/resources/qml/InviteDialog.qml b/resources/qml/InviteDialog.qml new file mode 100644 index 00000000..2c0e15a7 --- /dev/null +++ b/resources/qml/InviteDialog.qml @@ -0,0 +1,159 @@ +// SPDX-FileCopyrightText: 2021 Nheko Contributors +// +// SPDX-License-Identifier: GPL-3.0-or-later + +import QtQuick 2.12 +import QtQuick.Controls 2.12 +import QtQuick.Layouts 1.12 +import im.nheko 1.0 + +ApplicationWindow { + id: inviteDialogRoot + + property string roomId + property string plainRoomName + property InviteesModel invitees + + function addInvite() { + if (inviteeEntry.isValidMxid) { + invitees.addUser(inviteeEntry.text); + inviteeEntry.clear(); + } + } + + function cleanUpAndClose() { + if (inviteeEntry.isValidMxid) + addInvite(); + + invitees.accept(); + close(); + } + + title: qsTr("Invite users to %1").arg(plainRoomName) + height: 380 + width: 340 + palette: Nheko.colors + color: Nheko.colors.window + flags: Qt.Dialog | Qt.WindowCloseButtonHint + Component.onCompleted: Nheko.reparent(inviteDialogRoot) + + Shortcut { + sequence: "Ctrl+Enter" + onActivated: cleanUpAndClose() + } + + Shortcut { + sequence: StandardKey.Cancel + onActivated: inviteDialogRoot.close() + } + + ColumnLayout { + anchors.fill: parent + anchors.margins: Nheko.paddingMedium + spacing: Nheko.paddingMedium + + Label { + text: qsTr("User ID to invite") + Layout.fillWidth: true + color: Nheko.colors.text + } + + RowLayout { + spacing: Nheko.paddingMedium + + MatrixTextField { + id: inviteeEntry + + property bool isValidMxid: text.match("@.+?:.{3,}") + + backgroundColor: Nheko.colors.window + placeholderText: qsTr("@joe:matrix.org", "Example user id. The name 'joe' can be localized however you want.") + Layout.fillWidth: true + onAccepted: { + if (isValidMxid) + addInvite(); + + } + Component.onCompleted: forceActiveFocus() + Keys.onShortcutOverride: event.accepted = ((event.key === Qt.Key_Return || event.key === Qt.Key_Enter) && (event.modifiers & Qt.ControlModifier)) + Keys.onPressed: { + if ((event.key === Qt.Key_Return || event.key === Qt.Key_Enter) && (event.modifiers === Qt.ControlModifier)) + cleanUpAndClose(); + + } + } + + Button { + text: qsTr("Add") + enabled: inviteeEntry.isValidMxid + onClicked: addInvite() + } + + } + + ListView { + id: inviteesList + + Layout.fillWidth: true + Layout.fillHeight: true + model: invitees + + delegate: RowLayout { + spacing: Nheko.paddingMedium + + Avatar { + width: Nheko.avatarSize + height: Nheko.avatarSize + userid: model.mxid + url: model.avatarUrl.replace("mxc://", "image://MxcImage/") + displayName: model.displayName + onClicked: TimelineManager.openGlobalUserProfile(model.mxid) + } + + ColumnLayout { + spacing: Nheko.paddingSmall + + Label { + text: model.displayName + color: TimelineManager.userColor(model ? model.mxid : "", Nheko.colors.window) + font.pointSize: fontMetrics.font.pointSize + } + + Label { + text: model.mxid + color: Nheko.colors.buttonText + font.pointSize: fontMetrics.font.pointSize * 0.9 + } + + Item { + Layout.fillHeight: true + Layout.fillWidth: true + } + + } + + } + + } + + } + + footer: DialogButtonBox { + id: buttons + + Button { + text: qsTr("Invite") + DialogButtonBox.buttonRole: DialogButtonBox.AcceptRole + enabled: invitees.count > 0 + onClicked: cleanUpAndClose() + } + + Button { + text: qsTr("Cancel") + DialogButtonBox.buttonRole: DialogButtonBox.DestructiveRole + onClicked: inviteDialogRoot.close() + } + + } + +} diff --git a/resources/qml/MatrixText.qml b/resources/qml/MatrixText.qml index 35e5f7e7..7253cbe6 100644 --- a/resources/qml/MatrixText.qml +++ b/resources/qml/MatrixText.qml @@ -18,8 +18,8 @@ TextEdit { //enabled: selectByMouse color: Nheko.colors.text onLinkActivated: Nheko.openLink(link) - ToolTip.visible: hoveredLink - ToolTip.text: hoveredLink + ToolTip.visible: hoveredLink || false + ToolTip.text: hoveredLink || "" Component.onCompleted: { TimelineManager.fixImageRendering(r.textDocument, r); } diff --git a/resources/qml/MatrixTextField.qml b/resources/qml/MatrixTextField.qml index 3c660bac..80732b27 100644 --- a/resources/qml/MatrixTextField.qml +++ b/resources/qml/MatrixTextField.qml @@ -10,6 +10,8 @@ import im.nheko 1.0 TextField { id: input + property alias backgroundColor: backgroundRect.color + palette: Nheko.colors color: Nheko.colors.text @@ -62,6 +64,8 @@ TextField { } background: Rectangle { + id: backgroundRect + color: Nheko.colors.base } diff --git a/resources/qml/MessageInput.qml b/resources/qml/MessageInput.qml index 415d67a7..7fb09684 100644 --- a/resources/qml/MessageInput.qml +++ b/resources/qml/MessageInput.qml @@ -7,7 +7,7 @@ import "./voip" import QtQuick 2.12 import QtQuick.Controls 2.3 import QtQuick.Layouts 1.2 -import QtQuick.Window 2.2 +import QtQuick.Window 2.13 import im.nheko 1.0 Rectangle { @@ -261,18 +261,24 @@ Rectangle { background: null Connections { - onRoomChanged: { + function onRoomChanged() { messageInput.clear(); - messageInput.append(room.input.text()); + if (room) + messageInput.append(room.input.text()); + messageInput.completerTriggeredAt = -1; popup.completerName = ""; messageInput.forceActiveFocus(); } + target: timelineView } Connections { - onCompletionClicked: messageInput.insertCompletion(completion) + function onCompletionClicked(completion) { + messageInput.insertCompletion(completion); + } + target: popup } @@ -284,28 +290,39 @@ Rectangle { } Connections { - ignoreUnknownSignals: true - onInsertText: { + function onInsertText(text) { messageInput.remove(messageInput.selectionStart, messageInput.selectionEnd); messageInput.insert(messageInput.cursorPosition, text); } - onTextChanged: { + + function onTextChanged(newText) { messageInput.text = newText; messageInput.cursorPosition = newText.length; } + + ignoreUnknownSignals: true target: room ? room.input : null } Connections { + function onReplyChanged() { + messageInput.forceActiveFocus(); + } + + function onEditChanged() { + messageInput.forceActiveFocus(); + } + ignoreUnknownSignals: true - onReplyChanged: messageInput.forceActiveFocus() - onEditChanged: messageInput.forceActiveFocus() target: room } Connections { + function onFocusInput() { + messageInput.forceActiveFocus(); + } + target: TimelineManager - onFocusInput: messageInput.forceActiveFocus() } MouseArea { @@ -331,7 +348,7 @@ Rectangle { image: ":/icons/icons/ui/sticky-note-solid.svg" ToolTip.visible: hovered ToolTip.text: qsTr("Stickers") - onClicked: stickerPopup.visible ? stickerPopup.close() : stickerPopup.show(stickerButton, room.roomId(), function(row) { + onClicked: stickerPopup.visible ? stickerPopup.close() : stickerPopup.show(stickerButton, room.roomId, function(row) { room.input.sticker(stickerPopup.model.sourceModel, row); TimelineManager.focusMessageInput(); }) @@ -355,7 +372,7 @@ Rectangle { image: ":/icons/icons/ui/smile.png" ToolTip.visible: hovered ToolTip.text: qsTr("Emoji") - onClicked: emojiPopup.visible ? emojiPopup.close() : emojiPopup.show(function(emoji) { + onClicked: emojiPopup.visible ? emojiPopup.close() : emojiPopup.show(emojiButton, function(emoji) { messageInput.insert(messageInput.cursorPosition, emoji); TimelineManager.focusMessageInput(); }) diff --git a/resources/qml/MessageView.qml b/resources/qml/MessageView.qml index f56af237..79cbd700 100644 --- a/resources/qml/MessageView.qml +++ b/resources/qml/MessageView.qml @@ -10,7 +10,7 @@ import QtGraphicalEffects 1.0 import QtQuick 2.15 import QtQuick.Controls 2.15 import QtQuick.Layouts 1.2 -import QtQuick.Window 2.2 +import QtQuick.Window 2.13 import im.nheko 1.0 ScrollView { @@ -200,15 +200,22 @@ ScrollView { } Connections { + function onFocusChanged() { + readTimer.running = TimelineManager.isWindowFocused; + } + target: TimelineManager - onFocusChanged: readTimer.running = TimelineManager.isWindowFocused } Timer { id: readTimer // force current read index to update - onTriggered: chat.model.setCurrentIndex(chat.model.currentIndex) + onTriggered: { + if (chat.model) + chat.model.setCurrentIndex(chat.model.currentIndex); + + } interval: 1000 } @@ -265,11 +272,15 @@ ScrollView { } Connections { - target: chat.model - onRoomAvatarUrlChanged: { + function onRoomAvatarUrlChanged() { messageUserAvatar.url = chat.model.avatarUrl(userId).replace("mxc://", "image://MxcImage/"); } - onScrollToIndex: chat.positionViewAtIndex(index, ListView.Visible) + + function onScrollToIndex(index) { + chat.positionViewAtIndex(index, ListView.Visible); + } + + target: chat.model } Label { @@ -338,9 +349,11 @@ ScrollView { required property string callType required property var reactions required property int trustlevel + required property int encryptionError required property var timestamp required property int status required property int index + required property int relatedEventCacheBuster required property string previousMessageUserId required property string day required property string previousMessageDay @@ -444,8 +457,10 @@ ScrollView { callType: wrapper.callType reactions: wrapper.reactions trustlevel: wrapper.trustlevel + encryptionError: wrapper.encryptionError timestamp: wrapper.timestamp status: wrapper.status + relatedEventCacheBuster: wrapper.relatedEventCacheBuster y: section.visible && section.active ? section.y + section.height : 0 HoverHandler { @@ -465,22 +480,34 @@ ScrollView { } Connections { - target: chat - onMovementEnded: { + function onMovementEnded() { if (y + height + 2 * chat.spacing > chat.contentY + chat.height && y < chat.contentY + chat.height) chat.model.currentIndex = index; } + + target: chat } } - footer: Spinner { + footer: Item { anchors.horizontalCenter: parent.horizontalCenter - running: chat.model && chat.model.paginationInProgress - foreground: Nheko.colors.mid + anchors.margins: Nheko.paddingLarge visible: chat.model && chat.model.paginationInProgress - z: 3 + // hacky, but works + height: loadingSpinner.height + 2 * Nheko.paddingLarge + + Spinner { + id: loadingSpinner + + anchors.centerIn: parent + anchors.margins: Nheko.paddingLarge + running: chat.model && chat.model.paginationInProgress + foreground: Nheko.colors.mid + z: 3 + } + } } @@ -555,7 +582,7 @@ ScrollView { Platform.MenuItem { text: qsTr("Read receip&ts") - onTriggered: room.readReceiptsAction(messageContextMenu.eventId) + onTriggered: room.showReadReceipts(messageContextMenu.eventId) } Platform.MenuItem { diff --git a/resources/qml/PrivacyScreen.qml b/resources/qml/PrivacyScreen.qml index ca73cfd7..5f18a1ce 100644 --- a/resources/qml/PrivacyScreen.qml +++ b/resources/qml/PrivacyScreen.qml @@ -13,8 +13,7 @@ Item { property int screenTimeout Connections { - target: TimelineManager - onFocusChanged: { + function onFocusChanged() { if (TimelineManager.isWindowFocused) { screenSaverTimer.stop(); screenSaver.state = "Invisible"; @@ -24,6 +23,8 @@ Item { } } + + target: TimelineManager } Timer { diff --git a/resources/qml/QuickSwitcher.qml b/resources/qml/QuickSwitcher.qml index 8c4f47ca..61155acf 100644 --- a/resources/qml/QuickSwitcher.qml +++ b/resources/qml/QuickSwitcher.qml @@ -71,15 +71,17 @@ Popup { } Connections { - onCompletionSelected: { + function onCompletionSelected(id) { Rooms.setCurrentRoom(id); quickSwitcher.close(); } - onCountChanged: { + + function onCountChanged() { if (completerPopup.count > 0 && (completerPopup.currentIndex < 0 || completerPopup.currentIndex >= completerPopup.count)) completerPopup.currentIndex = 0; } + target: completerPopup } diff --git a/resources/qml/RawMessageDialog.qml b/resources/qml/RawMessageDialog.qml new file mode 100644 index 00000000..e2a476cd --- /dev/null +++ b/resources/qml/RawMessageDialog.qml @@ -0,0 +1,52 @@ +// SPDX-FileCopyrightText: 2021 Nheko Contributors +// +// SPDX-License-Identifier: GPL-3.0-or-later + +import QtQuick 2.15 +import QtQuick.Controls 2.15 +import im.nheko 1.0 + +ApplicationWindow { + id: rawMessageRoot + + property alias rawMessage: rawMessageView.text + + height: 420 + width: 420 + palette: Nheko.colors + color: Nheko.colors.window + flags: Qt.Tool | Qt.WindowStaysOnTopHint | Qt.WindowCloseButtonHint + Component.onCompleted: Nheko.reparent(rawMessageRoot) + + Shortcut { + sequence: StandardKey.Cancel + onActivated: rawMessageRoot.close() + } + + ScrollView { + anchors.margins: Nheko.paddingMedium + anchors.fill: parent + palette: Nheko.colors + padding: Nheko.paddingMedium + + TextArea { + id: rawMessageView + + font: Nheko.monospaceFont() + color: Nheko.colors.text + readOnly: true + + background: Rectangle { + color: Nheko.colors.base + } + + } + + } + + footer: DialogButtonBox { + standardButtons: DialogButtonBox.Ok + onAccepted: rawMessageRoot.close() + } + +} diff --git a/resources/qml/ReadReceipts.qml b/resources/qml/ReadReceipts.qml new file mode 100644 index 00000000..9adbfd5c --- /dev/null +++ b/resources/qml/ReadReceipts.qml @@ -0,0 +1,130 @@ +// SPDX-FileCopyrightText: 2021 Nheko Contributors +// +// SPDX-License-Identifier: GPL-3.0-or-later + +import QtQuick 2.15 +import QtQuick.Controls 2.15 +import QtQuick.Layouts 1.15 +import im.nheko 1.0 + +ApplicationWindow { + id: readReceiptsRoot + + property ReadReceiptsProxy readReceipts + property Room room + + height: 380 + width: 340 + minimumHeight: 380 + minimumWidth: headerTitle.width + 2 * Nheko.paddingMedium + palette: Nheko.colors + color: Nheko.colors.window + flags: Qt.Dialog | Qt.WindowCloseButtonHint + Component.onCompleted: Nheko.reparent(readReceiptsRoot) + + Shortcut { + sequence: StandardKey.Cancel + onActivated: readReceiptsRoot.close() + } + + ColumnLayout { + anchors.fill: parent + anchors.margins: Nheko.paddingMedium + spacing: Nheko.paddingMedium + + Label { + id: headerTitle + + color: Nheko.colors.text + Layout.alignment: Qt.AlignCenter + text: qsTr("Read receipts") + font.pointSize: fontMetrics.font.pointSize * 1.5 + } + + ScrollView { + palette: Nheko.colors + padding: Nheko.paddingMedium + ScrollBar.horizontal.visible: false + Layout.fillHeight: true + Layout.minimumHeight: 200 + Layout.fillWidth: true + + ListView { + id: readReceiptsList + + clip: true + spacing: Nheko.paddingMedium + boundsBehavior: Flickable.StopAtBounds + model: readReceipts + + delegate: RowLayout { + spacing: Nheko.paddingMedium + + Avatar { + width: Nheko.avatarSize + height: Nheko.avatarSize + userid: model.mxid + url: model.avatarUrl.replace("mxc://", "image://MxcImage/") + displayName: model.displayName + onClicked: room.openUserProfile(model.mxid) + ToolTip.visible: avatarHover.hovered + ToolTip.text: model.mxid + + HoverHandler { + id: avatarHover + } + + } + + ColumnLayout { + spacing: Nheko.paddingSmall + + Label { + text: model.displayName + color: TimelineManager.userColor(model ? model.mxid : "", Nheko.colors.window) + font.pointSize: fontMetrics.font.pointSize + ToolTip.visible: displayNameHover.hovered + ToolTip.text: model.mxid + + TapHandler { + onSingleTapped: room.openUserProfile(userId) + } + + CursorShape { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + } + + HoverHandler { + id: displayNameHover + } + + } + + Label { + text: model.timestamp + color: Nheko.colors.buttonText + font.pointSize: fontMetrics.font.pointSize * 0.9 + } + + Item { + Layout.fillHeight: true + Layout.fillWidth: true + } + + } + + } + + } + + } + + } + + footer: DialogButtonBox { + standardButtons: DialogButtonBox.Ok + onAccepted: readReceiptsRoot.close() + } + +} diff --git a/resources/qml/RoomList.qml b/resources/qml/RoomList.qml index a1ce8d7e..98532606 100644 --- a/resources/qml/RoomList.qml +++ b/resources/qml/RoomList.qml @@ -32,11 +32,12 @@ Page { } Connections { - onActiveTimelineChanged: { - roomlist.positionViewAtIndex(Rooms.roomidToIndex(Rooms.currentRoom.roomId()), ListView.Contain); - console.log("Test" + Rooms.currentRoom.roomId() + " " + Rooms.roomidToIndex(Rooms.currentRoom.roomId())); + function onCurrentRoomChanged() { + roomlist.positionViewAtIndex(Rooms.roomidToIndex(Rooms.currentRoom.roomId), ListView.Contain); + console.log("Test" + Rooms.currentRoom.roomId + " " + Rooms.roomidToIndex(Rooms.currentRoom.roomId)); } - target: TimelineManager + + target: Rooms } Platform.Menu { @@ -61,9 +62,19 @@ Page { } } + Platform.MessageDialog { + id: leaveRoomDialog + + title: qsTr("Leave Room") + text: qsTr("Are you sure you want to leave this room?") + modality: Qt.ApplicationModal + onAccepted: Rooms.leave(roomContextMenu.roomid) + buttons: Dialog.Ok | Dialog.Cancel + } + Platform.MenuItem { text: qsTr("Leave room") - onTriggered: Rooms.leave(roomContextMenu.roomid) + onTriggered: leaveRoomDialog.open() } Platform.MenuSeparator { @@ -133,7 +144,7 @@ Page { states: [ State { name: "highlight" - when: hovered.hovered && !((Rooms.currentRoom && roomId == Rooms.currentRoom.roomId()) || Rooms.currentRoomPreview.roomid == roomId) + when: hovered.hovered && !((Rooms.currentRoom && roomId == Rooms.currentRoom.roomId) || Rooms.currentRoomPreview.roomid == roomId) PropertyChanges { target: roomItem @@ -147,7 +158,7 @@ Page { }, State { name: "selected" - when: (Rooms.currentRoom && roomId == Rooms.currentRoom.roomId()) || Rooms.currentRoomPreview.roomid == roomId + when: (Rooms.currentRoom && roomId == Rooms.currentRoom.roomId) || Rooms.currentRoomPreview.roomid == roomId PropertyChanges { target: roomItem @@ -161,31 +172,38 @@ Page { } ] - TapHandler { - margin: -Nheko.paddingSmall - acceptedButtons: Qt.RightButton - onSingleTapped: { - if (!TimelineManager.isInvite) - roomContextMenu.show(roomId, tags); + // NOTE(Nico): We want to prevent the touch areas from overlapping. For some reason we need to add 1px of padding for that... + Item { + anchors.fill: parent + anchors.margins: 1 + TapHandler { + acceptedButtons: Qt.RightButton + onSingleTapped: { + if (!TimelineManager.isInvite) + roomContextMenu.show(roomId, tags); + + } + gesturePolicy: TapHandler.ReleaseWithinBounds + acceptedDevices: PointerDevice.Mouse | PointerDevice.Stylus | PointerDevice.TouchPad } - gesturePolicy: TapHandler.ReleaseWithinBounds - } - TapHandler { - margin: -Nheko.paddingSmall - onSingleTapped: Rooms.setCurrentRoom(roomId) - onLongPressed: { - if (!isInvite) - roomContextMenu.show(roomId, tags); + TapHandler { + margin: -Nheko.paddingSmall + onSingleTapped: Rooms.setCurrentRoom(roomId) + onLongPressed: { + if (!isInvite) + roomContextMenu.show(roomId, tags); + } } - } - HoverHandler { - id: hovered + HoverHandler { + id: hovered + + acceptedDevices: PointerDevice.Mouse | PointerDevice.Stylus | PointerDevice.TouchPad + } - margin: -Nheko.paddingSmall } RowLayout { @@ -421,6 +439,7 @@ Page { url: (userInfoGrid.profile ? userInfoGrid.profile.avatarUrl : "").replace("mxc://", "image://MxcImage/") displayName: userInfoGrid.profile ? userInfoGrid.profile.displayName : "" userid: userInfoGrid.profile ? userInfoGrid.profile.userid : "" + enabled: false } ColumnLayout { diff --git a/resources/qml/RoomMembers.qml b/resources/qml/RoomMembers.qml new file mode 100644 index 00000000..447e6fd1 --- /dev/null +++ b/resources/qml/RoomMembers.qml @@ -0,0 +1,148 @@ +// SPDX-FileCopyrightText: 2021 Nheko Contributors +// +// SPDX-License-Identifier: GPL-3.0-or-later + +import "./ui" +import QtQuick 2.12 +import QtQuick.Controls 2.12 +import QtQuick.Layouts 1.12 +import QtQuick.Window 2.13 +import im.nheko 1.0 + +ApplicationWindow { + id: roomMembersRoot + + property MemberList members + + title: qsTr("Members of %1").arg(members.roomName) + height: 650 + width: 420 + minimumHeight: 420 + palette: Nheko.colors + color: Nheko.colors.window + flags: Qt.Dialog | Qt.WindowCloseButtonHint + Component.onCompleted: Nheko.reparent(roomMembersRoot) + + Shortcut { + sequence: StandardKey.Cancel + onActivated: roomMembersRoot.close() + } + + ColumnLayout { + anchors.fill: parent + anchors.margins: Nheko.paddingMedium + spacing: Nheko.paddingMedium + + Avatar { + id: roomAvatar + + width: 130 + height: width + displayName: members.roomName + Layout.alignment: Qt.AlignHCenter + url: members.avatarUrl.replace("mxc://", "image://MxcImage/") + onClicked: TimelineManager.openRoomSettings(members.roomId) + } + + ElidedLabel { + font.pixelSize: fontMetrics.font.pixelSize * 2 + fullText: qsTr("%n people in %1", "Summary above list of members", members.memberCount).arg(members.roomName) + Layout.alignment: Qt.AlignHCenter + elideWidth: parent.width - Nheko.paddingMedium + } + + ImageButton { + Layout.alignment: Qt.AlignHCenter + image: ":/icons/icons/ui/add-square-button.png" + hoverEnabled: true + ToolTip.visible: hovered + ToolTip.text: qsTr("Invite more people") + onClicked: TimelineManager.openInviteUsers(members.roomId) + } + + ScrollView { + palette: Nheko.colors + padding: Nheko.paddingMedium + ScrollBar.horizontal.visible: false + Layout.fillHeight: true + Layout.minimumHeight: 200 + Layout.fillWidth: true + + ListView { + id: memberList + + clip: true + spacing: Nheko.paddingMedium + boundsBehavior: Flickable.StopAtBounds + model: members + + ScrollHelper { + flickable: parent + anchors.fill: parent + enabled: !Settings.mobileMode + } + + delegate: RowLayout { + spacing: Nheko.paddingMedium + + Avatar { + width: Nheko.avatarSize + height: Nheko.avatarSize + userid: model.mxid + url: model.avatarUrl.replace("mxc://", "image://MxcImage/") + displayName: model.displayName + onClicked: Rooms.currentRoom.openUserProfile(model.mxid) + } + + ColumnLayout { + spacing: Nheko.paddingSmall + + Label { + text: model.displayName + color: TimelineManager.userColor(model ? model.mxid : "", Nheko.colors.window) + font.pointSize: fontMetrics.font.pointSize + } + + Label { + text: model.mxid + color: Nheko.colors.buttonText + font.pointSize: fontMetrics.font.pointSize * 0.9 + } + + Item { + Layout.fillHeight: true + Layout.fillWidth: true + } + + } + + } + + footer: Item { + width: parent.width + visible: (members.numUsersLoaded < members.memberCount) && members.loadingMoreMembers + // use the default height if it's visible, otherwise no height at all + height: membersLoadingSpinner.height + anchors.margins: Nheko.paddingMedium + + Spinner { + id: membersLoadingSpinner + + anchors.centerIn: parent + height: visible ? 35 : 0 + } + + } + + } + + } + + } + + footer: DialogButtonBox { + standardButtons: DialogButtonBox.Ok + onAccepted: roomMembersRoot.close() + } + +} diff --git a/resources/qml/RoomSettings.qml b/resources/qml/RoomSettings.qml index c852b837..69cf427c 100644 --- a/resources/qml/RoomSettings.qml +++ b/resources/qml/RoomSettings.qml @@ -4,10 +4,10 @@ import "./ui" import Qt.labs.platform 1.1 as Platform -import QtQuick 2.9 +import QtQuick 2.15 import QtQuick.Controls 2.3 import QtQuick.Layouts 1.2 -import QtQuick.Window 2.3 +import QtQuick.Window 2.13 import im.nheko 1.0 ApplicationWindow { @@ -15,14 +15,13 @@ ApplicationWindow { property var roomSettings - x: MainWindow.x + (MainWindow.width / 2) - (width / 2) - y: MainWindow.y + (MainWindow.height / 2) - (height / 2) minimumWidth: 420 minimumHeight: 650 palette: Nheko.colors color: Nheko.colors.window modality: Qt.NonModal - flags: Qt.Dialog + flags: Qt.Dialog | Qt.WindowCloseButtonHint + Component.onCompleted: Nheko.reparent(roomSettingsDialog) title: qsTr("Room Settings") Shortcut { @@ -98,13 +97,23 @@ ApplicationWindow { MatrixText { text: roomSettings.roomName - font.pixelSize: 24 + font.pixelSize: fontMetrics.font.pixelSize * 2 Layout.alignment: Qt.AlignHCenter } MatrixText { text: qsTr("%1 member(s)").arg(roomSettings.memberCount) Layout.alignment: Qt.AlignHCenter + + TapHandler { + onTapped: TimelineManager.openRoomMembers(roomSettings.roomId) + } + + CursorShape { + cursorShape: Qt.PointingHandCursor + anchors.fill: parent + } + } } @@ -145,7 +154,7 @@ ApplicationWindow { GridLayout { columns: 2 - rowSpacing: 10 + rowSpacing: Nheko.paddingLarge MatrixText { text: qsTr("SETTINGS") @@ -171,7 +180,7 @@ ApplicationWindow { } MatrixText { - text: "Room access" + text: qsTr("Room access") Layout.fillWidth: true } @@ -209,7 +218,7 @@ ApplicationWindow { title: qsTr("End-to-End Encryption") text: qsTr("Encryption is currently experimental and things might break unexpectedly.
Please take note that it can't be disabled afterwards.") - modality: Qt.NonModal + modality: Qt.Modal onAccepted: { if (roomSettings.isEncryptionEnabled) return ; @@ -223,19 +232,13 @@ ApplicationWindow { } MatrixText { - visible: roomSettings.isEncryptionEnabled - text: qsTr("Respond to key requests") + text: qsTr("Sticker & Emote Settings") } - ToggleButton { - visible: roomSettings.isEncryptionEnabled - ToolTip.text: qsTr("Whether or not the client should respond automatically with the session keys - upon request. Use with caution, this is a temporary measure to test the - E2E implementation until device verification is completed.") - checked: roomSettings.respondsToKeyRequests - onClicked: { - roomSettings.changeKeyRequestsPreference(checked); - } + Button { + text: qsTr("Change") + ToolTip.text: qsTr("Change what packs are enabled, remove packs or create new ones") + onClicked: TimelineManager.openImagePackSettings(roomSettings.roomId) Layout.alignment: Qt.AlignRight } @@ -264,7 +267,7 @@ ApplicationWindow { MatrixText { text: roomSettings.roomId - font.pixelSize: 14 + font.pixelSize: fontMetrics.font.pixelSize * 1.2 Layout.alignment: Qt.AlignRight } @@ -274,16 +277,16 @@ ApplicationWindow { MatrixText { text: roomSettings.roomVersion - font.pixelSize: 14 + font.pixelSize: fontMetrics.font.pixelSize * 1.2 Layout.alignment: Qt.AlignRight } } - Button { - Layout.alignment: Qt.AlignRight - text: qsTr("OK") - onClicked: close() + DialogButtonBox { + Layout.fillWidth: true + standardButtons: DialogButtonBox.Ok + onAccepted: close() } } diff --git a/resources/qml/Root.qml b/resources/qml/Root.qml index 5316e20d..b229acda 100644 --- a/resources/qml/Root.qml +++ b/resources/qml/Root.qml @@ -4,14 +4,15 @@ import "./delegates" import "./device-verification" +import "./dialogs" import "./emoji" import "./voip" import Qt.labs.platform 1.1 as Platform import QtGraphicalEffects 1.0 -import QtQuick 2.9 -import QtQuick.Controls 2.5 +import QtQuick 2.15 +import QtQuick.Controls 2.15 import QtQuick.Layouts 1.3 -import QtQuick.Window 2.2 +import QtQuick.Window 2.15 import im.nheko 1.0 import im.nheko.EmojiModel 1.0 @@ -47,6 +48,14 @@ Page { } + Component { + id: roomMembersComponent + + RoomMembers { + } + + } + Component { id: mobileCallInviteDialog @@ -63,6 +72,46 @@ Page { } + Component { + id: deviceVerificationDialog + + DeviceVerification { + } + + } + + Component { + id: inviteDialog + + InviteDialog { + } + + } + + Component { + id: packSettingsComponent + + ImagePackSettingsDialog { + } + + } + + Component { + id: readReceiptsDialog + + ReadReceipts { + } + + } + + Component { + id: rawMessageDialog + + RawMessageDialog { + } + + } + Shortcut { sequence: "Ctrl+K" onActivated: { @@ -82,38 +131,64 @@ Page { onActivated: Rooms.previousRoom() } - Component { - id: deviceVerificationDialog - - DeviceVerification { - } - - } - Connections { - target: TimelineManager - onNewDeviceVerificationRequest: { + function onNewDeviceVerificationRequest(flow) { var dialog = deviceVerificationDialog.createObject(timelineRoot, { "flow": flow }); dialog.show(); } - onOpenProfile: { + + function onOpenProfile(profile) { var userProfile = userProfileComponent.createObject(timelineRoot, { "profile": profile }); userProfile.show(); } + + function onShowImagePackSettings(packlist) { + var packSet = packSettingsComponent.createObject(timelineRoot, { + "packlist": packlist + }); + packSet.show(); + } + + function onOpenRoomMembersDialog(members) { + var membersDialog = roomMembersComponent.createObject(timelineRoot, { + "members": members, + "roomName": Rooms.currentRoom.roomName + }); + membersDialog.show(); + } + + function onOpenRoomSettingsDialog(settings) { + var roomSettings = roomSettingsComponent.createObject(timelineRoot, { + "roomSettings": settings + }); + roomSettings.show(); + } + + function onOpenInviteUsersDialog(invitees) { + var dialog = inviteDialog.createObject(timelineRoot, { + "roomId": Rooms.currentRoom.roomId, + "plainRoomName": Rooms.currentRoom.plainRoomName, + "invitees": invitees + }); + dialog.show(); + } + + target: TimelineManager } Connections { - target: CallManager - onNewInviteState: { + function onNewInviteState() { if (CallManager.haveCallInvite && Settings.mobileMode) { var dialog = mobileCallInviteDialog.createObject(msgView); dialog.open(); } } + + target: CallManager } ChatPage { diff --git a/resources/qml/ScrollHelper.qml b/resources/qml/ScrollHelper.qml index 2dd56f27..e84e67fd 100644 --- a/resources/qml/ScrollHelper.qml +++ b/resources/qml/ScrollHelper.qml @@ -23,6 +23,9 @@ MouseArea { // console.warn("Delta: ", wheel.pixelDelta.y); // console.warn("Old position: ", flickable.contentY); // console.warn("New position: ", newPos); + // breaks ListView's with headers... + //if (typeof (flickableItem.headerItem) !== "undefined" && flickableItem.headerItem) + // minYExtent += flickableItem.headerItem.height; id: root @@ -55,9 +58,6 @@ MouseArea { var minYExtent = flickableItem.originY + flickableItem.topMargin; var maxYExtent = (flickableItem.contentHeight + flickableItem.bottomMargin + flickableItem.originY) - flickableItem.height; - if (typeof (flickableItem.headerItem) !== "undefined" && flickableItem.headerItem) - minYExtent += flickableItem.headerItem.height; - //Avoid overscrolling return Math.max(minYExtent, Math.min(maxYExtent, flickableItem.contentY - pixelDelta)); } diff --git a/resources/qml/StatusIndicator.qml b/resources/qml/StatusIndicator.qml index 7e471d69..0af02b3c 100644 --- a/resources/qml/StatusIndicator.qml +++ b/resources/qml/StatusIndicator.qml @@ -34,7 +34,7 @@ ImageButton { } onClicked: { if (status == MtxEvent.Read) - room.readReceiptsAction(eventId); + room.showReadReceipts(eventId); } image: { diff --git a/resources/qml/TimelineRow.qml b/resources/qml/TimelineRow.qml index 70db08e7..c612479a 100644 --- a/resources/qml/TimelineRow.qml +++ b/resources/qml/TimelineRow.qml @@ -7,7 +7,7 @@ import "./emoji" import QtQuick 2.12 import QtQuick.Controls 2.3 import QtQuick.Layouts 1.2 -import QtQuick.Window 2.2 +import QtQuick.Window 2.13 import im.nheko 1.0 Item { @@ -38,8 +38,10 @@ Item { required property string callType required property var reactions required property int trustlevel + required property int encryptionError required property var timestamp required property int status + required property int relatedEventCacheBuster anchors.left: parent.left anchors.right: parent.right @@ -90,25 +92,27 @@ Item { } visible: replyTo - userColor: replyTo, TimelineManager.userColor(userId, Nheko.colors.base) - blurhash: replyTo, fromModel(Room.Blurhash) ?? "" - body: replyTo, fromModel(Room.Body) ?? "" - formattedBody: replyTo, fromModel(Room.FormattedBody) ?? "" + userColor: r.relatedEventCacheBuster, TimelineManager.userColor(userId, Nheko.colors.base) + blurhash: r.relatedEventCacheBuster, fromModel(Room.Blurhash) ?? "" + body: r.relatedEventCacheBuster, fromModel(Room.Body) ?? "" + formattedBody: r.relatedEventCacheBuster, fromModel(Room.FormattedBody) ?? "" eventId: fromModel(Room.EventId) ?? "" - filename: replyTo, fromModel(Room.Filename) ?? "" - filesize: replyTo, fromModel(Room.Filesize) ?? "" - proportionalHeight: replyTo, fromModel(Room.ProportionalHeight) ?? 1 - type: replyTo, fromModel(Room.Type) ?? MtxEvent.UnknownMessage - typeString: replyTo, fromModel(Room.TypeString) ?? "" - url: replyTo, fromModel(Room.Url) ?? "" - originalWidth: replyTo, fromModel(Room.OriginalWidth) ?? 0 - isOnlyEmoji: replyTo, fromModel(Room.IsOnlyEmoji) ?? false - userId: replyTo, fromModel(Room.UserId) ?? "" - userName: replyTo, fromModel(Room.UserName) ?? "" - thumbnailUrl: replyTo, fromModel(Room.ThumbnailUrl) ?? "" - roomTopic: replyTo, fromModel(Room.RoomTopic) ?? "" - roomName: replyTo, fromModel(Room.RoomName) ?? "" - callType: replyTo, fromModel(Room.CallType) ?? "" + filename: r.relatedEventCacheBuster, fromModel(Room.Filename) ?? "" + filesize: r.relatedEventCacheBuster, fromModel(Room.Filesize) ?? "" + proportionalHeight: r.relatedEventCacheBuster, fromModel(Room.ProportionalHeight) ?? 1 + type: r.relatedEventCacheBuster, fromModel(Room.Type) ?? MtxEvent.UnknownMessage + typeString: r.relatedEventCacheBuster, fromModel(Room.TypeString) ?? "" + url: r.relatedEventCacheBuster, fromModel(Room.Url) ?? "" + originalWidth: r.relatedEventCacheBuster, fromModel(Room.OriginalWidth) ?? 0 + isOnlyEmoji: r.relatedEventCacheBuster, fromModel(Room.IsOnlyEmoji) ?? false + userId: r.relatedEventCacheBuster, fromModel(Room.UserId) ?? "" + userName: r.relatedEventCacheBuster, fromModel(Room.UserName) ?? "" + thumbnailUrl: r.relatedEventCacheBuster, fromModel(Room.ThumbnailUrl) ?? "" + roomTopic: r.relatedEventCacheBuster, fromModel(Room.RoomTopic) ?? "" + roomName: r.relatedEventCacheBuster, fromModel(Room.RoomName) ?? "" + callType: r.relatedEventCacheBuster, fromModel(Room.CallType) ?? "" + encryptionError: r.relatedEventCacheBuster, fromModel(Room.EncryptionError) ?? "" + relatedEventCacheBuster: r.relatedEventCacheBuster, fromModel(Room.RelatedEventCacheBuster) ?? 0 } // actual message content @@ -134,6 +138,8 @@ Item { roomTopic: r.roomTopic roomName: r.roomName callType: r.callType + encryptionError: r.encryptionError + relatedEventCacheBuster: r.relatedEventCacheBuster isReply: false } diff --git a/resources/qml/TimelineView.qml b/resources/qml/TimelineView.qml index 148a5817..6fc9d51b 100644 --- a/resources/qml/TimelineView.qml +++ b/resources/qml/TimelineView.qml @@ -13,7 +13,7 @@ import QtGraphicalEffects 1.0 import QtQuick 2.9 import QtQuick.Controls 2.5 import QtQuick.Layouts 1.3 -import QtQuick.Window 2.2 +import QtQuick.Window 2.13 import im.nheko 1.0 import im.nheko.EmojiModel 1.0 @@ -246,17 +246,26 @@ Item { NhekoDropArea { anchors.fill: parent - roomid: room ? room.roomId() : "" + roomid: room ? room.roomId : "" } Connections { - target: room - onOpenRoomSettingsDialog: { - var roomSettings = roomSettingsComponent.createObject(timelineRoot, { - "roomSettings": settings + function onOpenReadReceiptsDialog(rr) { + var dialog = readReceiptsDialog.createObject(timelineRoot, { + "readReceipts": rr, + "room": room }); - roomSettings.show(); + dialog.show(); } + + function onShowRawMessageDialog(rawMessage) { + var dialog = rawMessageDialog.createObject(timelineRoot, { + "rawMessage": rawMessage + }); + dialog.show(); + } + + target: room } } diff --git a/resources/qml/TopBar.qml b/resources/qml/TopBar.qml index 58aba0c7..8543d02a 100644 --- a/resources/qml/TopBar.qml +++ b/resources/qml/TopBar.qml @@ -24,7 +24,7 @@ Rectangle { TapHandler { onSingleTapped: { if (room) - room.openRoomSettings(); + TimelineManager.openRoomSettings(room.roomId); eventPoint.accepted = true; } @@ -66,7 +66,7 @@ Rectangle { displayName: roomName onClicked: { if (room) - room.openRoomSettings(); + TimelineManager.openRoomSettings(room.roomId); } } @@ -111,22 +111,22 @@ Rectangle { Platform.MenuItem { visible: room ? room.permissions.canInvite() : false text: qsTr("Invite users") - onTriggered: TimelineManager.openInviteUsersDialog() + onTriggered: TimelineManager.openInviteUsers(room.roomId) } Platform.MenuItem { text: qsTr("Members") - onTriggered: TimelineManager.openMemberListDialog(room.roomId()) + onTriggered: TimelineManager.openRoomMembers(room.roomId) } Platform.MenuItem { text: qsTr("Leave room") - onTriggered: TimelineManager.openLeaveRoomDialog(room.roomId()) + onTriggered: TimelineManager.openLeaveRoomDialog(room.roomId) } Platform.MenuItem { text: qsTr("Settings") - onTriggered: room.openRoomSettings() + onTriggered: TimelineManager.openRoomSettings(room.roomId) } } diff --git a/resources/qml/UserProfile.qml b/resources/qml/UserProfile.qml index 826d3165..767d2317 100644 --- a/resources/qml/UserProfile.qml +++ b/resources/qml/UserProfile.qml @@ -4,19 +4,20 @@ import "./device-verification" import "./ui" -import QtQuick 2.9 -import QtQuick.Controls 2.3 +import QtQuick 2.15 +import QtQuick.Controls 2.15 import QtQuick.Layouts 1.2 -import QtQuick.Window 2.3 +import QtQuick.Window 2.13 import im.nheko 1.0 ApplicationWindow { + // this does not work in ApplicationWindow, just in Window + //transientParent: Nheko.mainwindow() + id: userProfileDialog property var profile - x: MainWindow.x + (MainWindow.width / 2) - (width / 2) - y: MainWindow.y + (MainWindow.height / 2) - (height / 2) height: 650 width: 420 minimumHeight: 420 @@ -24,7 +25,8 @@ ApplicationWindow { color: Nheko.colors.window title: profile.isGlobalUserProfile ? qsTr("Global User Profile") : qsTr("Room User Profile") modality: Qt.NonModal - flags: Qt.Dialog + flags: Qt.Dialog | Qt.WindowCloseButtonHint + Component.onCompleted: Nheko.reparent(userProfileDialog) Shortcut { sequence: StandardKey.Cancel @@ -83,12 +85,13 @@ ApplicationWindow { } Connections { - target: profile - onDisplayError: { + function onDisplayError(errorMessage) { errorText.text = errorMessage; errorText.opacity = 1; hideErrorAnimation.restart(); } + + target: profile } TextInput { diff --git a/resources/qml/components/AvatarListTile.qml b/resources/qml/components/AvatarListTile.qml new file mode 100644 index 00000000..36c26a97 --- /dev/null +++ b/resources/qml/components/AvatarListTile.qml @@ -0,0 +1,133 @@ +// SPDX-FileCopyrightText: 2021 Nheko Contributors +// +// SPDX-License-Identifier: GPL-3.0-or-later + +import ".." +import QtQuick 2.15 +import QtQuick.Controls 2.15 +import QtQuick.Layouts 1.15 +import im.nheko 1.0 + +Rectangle { + id: tile + + property color background: Nheko.colors.window + property color importantText: Nheko.colors.text + property color unimportantText: Nheko.colors.buttonText + property color bubbleBackground: Nheko.colors.highlight + property color bubbleText: Nheko.colors.highlightedText + property int avatarSize: Math.ceil(fontMetrics.lineSpacing * 2.3) + required property string avatarUrl + required property string title + required property string subtitle + required property int index + required property int selectedIndex + property bool crop: true + + color: background + height: avatarSize + 2 * Nheko.paddingMedium + width: ListView.view.width + state: "normal" + states: [ + State { + name: "highlight" + when: hovered.hovered && !(index == selectedIndex) + + PropertyChanges { + target: tile + background: Nheko.colors.dark + importantText: Nheko.colors.brightText + unimportantText: Nheko.colors.brightText + bubbleBackground: Nheko.colors.highlight + bubbleText: Nheko.colors.highlightedText + } + + }, + State { + name: "selected" + when: index == selectedIndex + + PropertyChanges { + target: tile + background: Nheko.colors.highlight + importantText: Nheko.colors.highlightedText + unimportantText: Nheko.colors.highlightedText + bubbleBackground: Nheko.colors.highlightedText + bubbleText: Nheko.colors.highlight + } + + } + ] + + HoverHandler { + id: hovered + } + + RowLayout { + spacing: Nheko.paddingMedium + anchors.fill: parent + anchors.margins: Nheko.paddingMedium + + Avatar { + id: avatar + + enabled: false + Layout.alignment: Qt.AlignVCenter + height: avatarSize + width: avatarSize + url: tile.avatarUrl.replace("mxc://", "image://MxcImage/") + displayName: title + crop: tile.crop + } + + ColumnLayout { + id: textContent + + Layout.alignment: Qt.AlignLeft + Layout.fillWidth: true + Layout.minimumWidth: 100 + width: parent.width - avatar.width + Layout.preferredWidth: parent.width - avatar.width + spacing: Nheko.paddingSmall + + RowLayout { + Layout.fillWidth: true + spacing: 0 + + ElidedLabel { + Layout.alignment: Qt.AlignBottom + color: tile.importantText + elideWidth: textContent.width - Nheko.paddingMedium + fullText: title + textFormat: Text.PlainText + } + + Item { + Layout.fillWidth: true + } + + } + + RowLayout { + Layout.fillWidth: true + spacing: 0 + + ElidedLabel { + color: tile.unimportantText + font.pixelSize: fontMetrics.font.pixelSize * 0.9 + elideWidth: textContent.width - Nheko.paddingSmall + fullText: subtitle + textFormat: Text.PlainText + } + + Item { + Layout.fillWidth: true + } + + } + + } + + } + +} diff --git a/resources/qml/delegates/Encrypted.qml b/resources/qml/delegates/Encrypted.qml new file mode 100644 index 00000000..cd00a9d4 --- /dev/null +++ b/resources/qml/delegates/Encrypted.qml @@ -0,0 +1,48 @@ +// SPDX-FileCopyrightText: 2021 Nheko Contributors +// +// SPDX-License-Identifier: GPL-3.0-or-later + +import ".." +import QtQuick.Controls 2.1 +import QtQuick.Layouts 1.2 +import im.nheko 1.0 + +ColumnLayout { + id: r + + required property int encryptionError + required property string eventId + + width: parent ? parent.width : undefined + + MatrixText { + text: { + switch (encryptionError) { + case Olm.MissingSession: + return qsTr("There is no key to unlock this message. We requested the key automatically, but you can try requesting it again if you are impatient."); + case Olm.MissingSessionIndex: + return qsTr("This message couldn't be decrypted, because we only have a key for newer messages. You can try requesting access to this message."); + case Olm.DbError: + return qsTr("There was an internal error reading the decryption key from the database."); + case Olm.DecryptionFailed: + return qsTr("There was an error decrypting this message."); + case Olm.ParsingFailed: + return qsTr("The message couldn't be parsed."); + case Olm.ReplayAttack: + return qsTr("The encryption key was reused! Someone is possibly trying to insert false messages into this chat!"); + default: + return qsTr("Unknown decryption error"); + } + } + color: Nheko.colors.buttonText + width: r ? r.width : undefined + } + + Button { + palette: Nheko.colors + visible: encryptionError == Olm.MissingSession || encryptionError == Olm.MissingSessionIndex + text: qsTr("Request key") + onClicked: room.requestKeyForEvent(eventId) + } + +} diff --git a/resources/qml/delegates/MessageDelegate.qml b/resources/qml/delegates/MessageDelegate.qml index 0b060629..a8bdf183 100644 --- a/resources/qml/delegates/MessageDelegate.qml +++ b/resources/qml/delegates/MessageDelegate.qml @@ -29,6 +29,8 @@ Item { required property string roomTopic required property string roomName required property string callType + required property int encryptionError + required property int relatedEventCacheBuster height: chooser.childrenRect.height @@ -188,6 +190,16 @@ Item { } + DelegateChoice { + roleValue: MtxEvent.Encrypted + + Encrypted { + encryptionError: d.encryptionError + eventId: d.eventId + } + + } + DelegateChoice { roleValue: MtxEvent.Name @@ -231,7 +243,7 @@ Item { body: formatted isOnlyEmoji: false isReply: d.isReply - formatted: qsTr("%1 created and configured room: %2").arg(d.userName).arg(room.roomId()) + formatted: qsTr("%1 created and configured room: %2").arg(d.userName).arg(room.roomId) } } @@ -301,7 +313,7 @@ Item { body: formatted isOnlyEmoji: false isReply: d.isReply - formatted: room.formatPowerLevelEvent(d.eventId) + formatted: d.relatedEventCacheBuster, room.formatPowerLevelEvent(d.eventId) } } @@ -313,7 +325,7 @@ Item { body: formatted isOnlyEmoji: false isReply: d.isReply - formatted: room.formatJoinRuleEvent(d.eventId) + formatted: d.relatedEventCacheBuster, room.formatJoinRuleEvent(d.eventId) } } @@ -325,7 +337,7 @@ Item { body: formatted isOnlyEmoji: false isReply: d.isReply - formatted: room.formatHistoryVisibilityEvent(d.eventId) + formatted: d.relatedEventCacheBuster, room.formatHistoryVisibilityEvent(d.eventId) } } @@ -337,7 +349,7 @@ Item { body: formatted isOnlyEmoji: false isReply: d.isReply - formatted: room.formatGuestAccessEvent(d.eventId) + formatted: d.relatedEventCacheBuster, room.formatGuestAccessEvent(d.eventId) } } @@ -349,7 +361,7 @@ Item { body: formatted isOnlyEmoji: false isReply: d.isReply - formatted: room.formatMemberEvent(d.eventId) + formatted: d.relatedEventCacheBuster, room.formatMemberEvent(d.eventId) } } diff --git a/resources/qml/delegates/PlayableMediaMessage.qml b/resources/qml/delegates/PlayableMediaMessage.qml index 3face74d..aa7ee530 100644 --- a/resources/qml/delegates/PlayableMediaMessage.qml +++ b/resources/qml/delegates/PlayableMediaMessage.qml @@ -52,7 +52,7 @@ ColumnLayout { id: mediaCachedObserver target: room - onMediaCached: { + function onMediaCached(mxcUrl, cacheUrl) { if (mxcUrl == url) { mediaCached = true media.source = "file://" + cacheUrl diff --git a/resources/qml/delegates/Reply.qml b/resources/qml/delegates/Reply.qml index 3a188d78..8bbce10e 100644 --- a/resources/qml/delegates/Reply.qml +++ b/resources/qml/delegates/Reply.qml @@ -5,7 +5,7 @@ import QtQuick 2.12 import QtQuick.Controls 2.3 import QtQuick.Layouts 1.2 -import QtQuick.Window 2.2 +import QtQuick.Window 2.13 import im.nheko 1.0 Item { @@ -30,6 +30,8 @@ Item { property string roomTopic property string roomName property string callType + property int encryptionError + property int relatedEventCacheBuster width: parent.width height: replyContainer.height @@ -95,6 +97,8 @@ Item { roomTopic: r.roomTopic roomName: r.roomName callType: r.callType + relatedEventCacheBuster: r.relatedEventCacheBuster + encryptionError: r.encryptionError enabled: false width: parent.width isReply: true diff --git a/resources/qml/device-verification/DeviceVerification.qml b/resources/qml/device-verification/DeviceVerification.qml index e2c66c5a..8e0271d6 100644 --- a/resources/qml/device-verification/DeviceVerification.qml +++ b/resources/qml/device-verification/DeviceVerification.qml @@ -4,7 +4,7 @@ import QtQuick 2.10 import QtQuick.Controls 2.3 -import QtQuick.Window 2.10 +import QtQuick.Window 2.13 import im.nheko 1.0 ApplicationWindow { @@ -14,13 +14,12 @@ ApplicationWindow { onClosing: TimelineManager.removeVerificationFlow(flow) title: stack.currentItem.title - flags: Qt.Dialog modality: Qt.NonModal palette: Nheko.colors height: stack.implicitHeight width: stack.implicitWidth - x: MainWindow.x + (MainWindow.width / 2) - (width / 2) - y: MainWindow.y + (MainWindow.height / 2) - (height / 2) + flags: Qt.Dialog | Qt.WindowCloseButtonHint + Component.onCompleted: Nheko.reparent(dialog) StackView { id: stack diff --git a/resources/qml/dialogs/ImagePackEditorDialog.qml b/resources/qml/dialogs/ImagePackEditorDialog.qml new file mode 100644 index 00000000..b839c9e3 --- /dev/null +++ b/resources/qml/dialogs/ImagePackEditorDialog.qml @@ -0,0 +1,301 @@ +// SPDX-FileCopyrightText: 2021 Nheko Contributors +// +// SPDX-License-Identifier: GPL-3.0-or-later + +import ".." +import "../components" +import Qt.labs.platform 1.1 +import QtQuick 2.12 +import QtQuick.Controls 2.12 +import QtQuick.Layouts 1.12 +import im.nheko 1.0 + +ApplicationWindow { + //Component.onCompleted: Nheko.reparent(win) + + id: win + + property int avatarSize: Math.ceil(fontMetrics.lineSpacing * 2.3) + property SingleImagePackModel imagePack + property int currentImageIndex: -1 + readonly property int stickerDim: 128 + readonly property int stickerDimPad: 128 + Nheko.paddingSmall + + title: qsTr("Editing image pack") + height: 600 + width: 600 + palette: Nheko.colors + color: Nheko.colors.base + modality: Qt.WindowModal + flags: Qt.Dialog | Qt.WindowCloseButtonHint + + AdaptiveLayout { + id: adaptiveView + + anchors.fill: parent + singlePageMode: false + pageIndex: 0 + + AdaptiveLayoutElement { + id: packlistC + + visible: Settings.groupView + minimumWidth: 200 + collapsedWidth: 200 + preferredWidth: 300 + maximumWidth: 300 + clip: true + + ListView { + //required property bool isEmote + //required property bool isSticker + + model: imagePack + + ScrollHelper { + flickable: parent + anchors.fill: parent + enabled: !Settings.mobileMode + } + + header: AvatarListTile { + title: imagePack.packname + avatarUrl: imagePack.avatarUrl + subtitle: imagePack.statekey + index: -1 + selectedIndex: currentImageIndex + + TapHandler { + onSingleTapped: currentImageIndex = -1 + } + + Rectangle { + anchors.left: parent.left + anchors.verticalCenter: parent.verticalCenter + height: parent.height - Nheko.paddingSmall * 2 + width: 3 + color: Nheko.colors.highlight + } + + } + + footer: Button { + palette: Nheko.colors + onClicked: addFilesDialog.open() + width: ListView.view.width + text: qsTr("Add images") + + FileDialog { + id: addFilesDialog + + folder: StandardPaths.writableLocation(StandardPaths.PicturesLocation) + fileMode: FileDialog.OpenFiles + nameFilters: [qsTr("Stickers (*.png *.webp)")] + onAccepted: imagePack.addStickers(files) + } + + } + + delegate: AvatarListTile { + id: packItem + + property color background: Nheko.colors.window + property color importantText: Nheko.colors.text + property color unimportantText: Nheko.colors.buttonText + property color bubbleBackground: Nheko.colors.highlight + property color bubbleText: Nheko.colors.highlightedText + required property string shortCode + required property string url + required property string body + + title: shortCode + subtitle: body + avatarUrl: url + selectedIndex: currentImageIndex + crop: false + + TapHandler { + onSingleTapped: currentImageIndex = index + } + + } + + } + + } + + AdaptiveLayoutElement { + id: packinfoC + + Rectangle { + color: Nheko.colors.window + + GridLayout { + anchors.fill: parent + anchors.margins: Nheko.paddingMedium + visible: currentImageIndex == -1 + enabled: visible + columns: 2 + rowSpacing: Nheko.paddingLarge + + Avatar { + Layout.columnSpan: 2 + url: imagePack.avatarUrl.replace("mxc://", "image://MxcImage/") + displayName: imagePack.packname + height: 130 + width: 130 + crop: false + Layout.alignment: Qt.AlignHCenter + } + + MatrixText { + visible: imagePack.roomid + text: qsTr("State key") + } + + MatrixTextField { + visible: imagePack.roomid + Layout.fillWidth: true + text: imagePack.statekey + onTextEdited: imagePack.statekey = text + } + + MatrixText { + text: qsTr("Packname") + } + + MatrixTextField { + Layout.fillWidth: true + text: imagePack.packname + onTextEdited: imagePack.packname = text + } + + MatrixText { + text: qsTr("Attrbution") + } + + MatrixTextField { + Layout.fillWidth: true + text: imagePack.attribution + onTextEdited: imagePack.attribution = text + } + + MatrixText { + text: qsTr("Use as Emoji") + } + + ToggleButton { + checked: imagePack.isEmotePack + onClicked: imagePack.isEmotePack = checked + Layout.alignment: Qt.AlignRight + } + + MatrixText { + text: qsTr("Use as Sticker") + } + + ToggleButton { + checked: imagePack.isStickerPack + onClicked: imagePack.isStickerPack = checked + Layout.alignment: Qt.AlignRight + } + + Item { + Layout.columnSpan: 2 + Layout.fillHeight: true + } + + } + + GridLayout { + anchors.fill: parent + anchors.margins: Nheko.paddingMedium + visible: currentImageIndex >= 0 + enabled: visible + columns: 2 + rowSpacing: Nheko.paddingLarge + + Avatar { + Layout.columnSpan: 2 + url: imagePack.data(imagePack.index(currentImageIndex, 0), SingleImagePackModel.Url).replace("mxc://", "image://MxcImage/") + displayName: imagePack.data(imagePack.index(currentImageIndex, 0), SingleImagePackModel.ShortCode) + height: 130 + width: 130 + crop: false + Layout.alignment: Qt.AlignHCenter + } + + MatrixText { + text: qsTr("Shortcode") + } + + MatrixTextField { + Layout.fillWidth: true + text: imagePack.data(imagePack.index(currentImageIndex, 0), SingleImagePackModel.ShortCode) + onTextEdited: imagePack.setData(imagePack.index(currentImageIndex, 0), text, SingleImagePackModel.ShortCode) + } + + MatrixText { + text: qsTr("Body") + } + + MatrixTextField { + Layout.fillWidth: true + text: imagePack.data(imagePack.index(currentImageIndex, 0), SingleImagePackModel.Body) + onTextEdited: imagePack.setData(imagePack.index(currentImageIndex, 0), text, SingleImagePackModel.Body) + } + + MatrixText { + text: qsTr("Use as Emoji") + } + + ToggleButton { + checked: imagePack.data(imagePack.index(currentImageIndex, 0), SingleImagePackModel.IsEmote) + onClicked: imagePack.setData(imagePack.index(currentImageIndex, 0), checked, SingleImagePackModel.IsEmote) + Layout.alignment: Qt.AlignRight + } + + MatrixText { + text: qsTr("Use as Sticker") + } + + ToggleButton { + checked: imagePack.data(imagePack.index(currentImageIndex, 0), SingleImagePackModel.IsSticker) + onClicked: imagePack.setData(imagePack.index(currentImageIndex, 0), checked, SingleImagePackModel.IsSticker) + Layout.alignment: Qt.AlignRight + } + + Item { + Layout.columnSpan: 2 + Layout.fillHeight: true + } + + } + + } + + } + + } + + footer: DialogButtonBox { + id: buttons + + Button { + text: qsTr("Cancel") + DialogButtonBox.buttonRole: DialogButtonBox.DestructiveRole + onClicked: win.close() + } + + Button { + text: qsTr("Save") + DialogButtonBox.buttonRole: DialogButtonBox.ApplyRole + onClicked: { + imagePack.save(); + win.close(); + } + } + + } + +} diff --git a/resources/qml/dialogs/ImagePackSettingsDialog.qml b/resources/qml/dialogs/ImagePackSettingsDialog.qml new file mode 100644 index 00000000..5181619c --- /dev/null +++ b/resources/qml/dialogs/ImagePackSettingsDialog.qml @@ -0,0 +1,260 @@ +// SPDX-FileCopyrightText: 2021 Nheko Contributors +// +// SPDX-License-Identifier: GPL-3.0-or-later + +import ".." +import "../components" +import QtQuick 2.12 +import QtQuick.Controls 2.12 +import QtQuick.Layouts 1.12 +import im.nheko 1.0 + +ApplicationWindow { + id: win + + property ImagePackListModel packlist + property int avatarSize: Math.ceil(fontMetrics.lineSpacing * 2.3) + property SingleImagePackModel currentPack: packlist.packAt(currentPackIndex) + property int currentPackIndex: 0 + readonly property int stickerDim: 128 + readonly property int stickerDimPad: 128 + Nheko.paddingSmall + + title: qsTr("Image pack settings") + height: 600 + width: 800 + palette: Nheko.colors + color: Nheko.colors.base + modality: Qt.NonModal + flags: Qt.Dialog | Qt.WindowCloseButtonHint + Component.onCompleted: Nheko.reparent(win) + + Component { + id: packEditor + + ImagePackEditorDialog { + } + + } + + AdaptiveLayout { + id: adaptiveView + + anchors.fill: parent + singlePageMode: false + pageIndex: 0 + + AdaptiveLayoutElement { + id: packlistC + + visible: Settings.groupView + minimumWidth: 200 + collapsedWidth: 200 + preferredWidth: 300 + maximumWidth: 300 + + ListView { + model: packlist + clip: true + + ScrollHelper { + flickable: parent + anchors.fill: parent + enabled: !Settings.mobileMode + } + + footer: ColumnLayout { + Button { + palette: Nheko.colors + onClicked: { + var dialog = packEditor.createObject(timelineRoot, { + "imagePack": packlist.newPack(false) + }); + dialog.show(); + } + width: packlist.width + visible: !packlist.containsAccountPack + text: qsTr("Create account pack") + } + + Button { + palette: Nheko.colors + onClicked: { + var dialog = packEditor.createObject(timelineRoot, { + "imagePack": packlist.newPack(true) + }); + dialog.show(); + } + width: packlist.width + text: qsTr("New room pack") + } + + } + + delegate: AvatarListTile { + id: packItem + + property color background: Nheko.colors.window + property color importantText: Nheko.colors.text + property color unimportantText: Nheko.colors.buttonText + property color bubbleBackground: Nheko.colors.highlight + property color bubbleText: Nheko.colors.highlightedText + required property string displayName + required property bool fromAccountData + required property bool fromCurrentRoom + + title: displayName + subtitle: { + if (fromAccountData) + return qsTr("Private pack"); + else if (fromCurrentRoom) + return qsTr("Pack from this room"); + else + return qsTr("Globally enabled pack"); + } + selectedIndex: currentPackIndex + + TapHandler { + onSingleTapped: currentPackIndex = index + } + + } + + } + + } + + AdaptiveLayoutElement { + id: packinfoC + + Rectangle { + color: Nheko.colors.window + + ColumnLayout { + id: packinfo + + property string packName: currentPack ? currentPack.packname : "" + property string attribution: currentPack ? currentPack.attribution : "" + property string avatarUrl: currentPack ? currentPack.avatarUrl : "" + + anchors.fill: parent + anchors.margins: Nheko.paddingLarge + spacing: Nheko.paddingLarge + + Avatar { + url: packinfo.avatarUrl.replace("mxc://", "image://MxcImage/") + displayName: packinfo.packName + height: 100 + width: 100 + Layout.alignment: Qt.AlignHCenter + enabled: false + } + + MatrixText { + text: packinfo.packName + font.pixelSize: Math.ceil(fontMetrics.pixelSize * 1.1) + horizontalAlignment: TextEdit.AlignHCenter + Layout.alignment: Qt.AlignHCenter + Layout.preferredWidth: packinfoC.width - Nheko.paddingLarge * 2 + } + + MatrixText { + text: packinfo.attribution + wrapMode: TextEdit.Wrap + horizontalAlignment: TextEdit.AlignHCenter + Layout.alignment: Qt.AlignHCenter + Layout.preferredWidth: packinfoC.width - Nheko.paddingLarge * 2 + } + + GridLayout { + Layout.alignment: Qt.AlignHCenter + visible: currentPack && currentPack.roomid != "" + columns: 2 + rowSpacing: Nheko.paddingMedium + + MatrixText { + text: qsTr("Enable globally") + } + + ToggleButton { + ToolTip.text: qsTr("Enables this pack to be used in all rooms") + checked: currentPack ? currentPack.isGloballyEnabled : false + onClicked: currentPack.isGloballyEnabled = !currentPack.isGloballyEnabled + Layout.alignment: Qt.AlignRight + } + + } + + Button { + Layout.alignment: Qt.AlignHCenter + text: qsTr("Edit") + enabled: currentPack.canEdit + onClicked: { + var dialog = packEditor.createObject(timelineRoot, { + "imagePack": currentPack + }); + dialog.show(); + } + } + + GridView { + Layout.fillHeight: true + Layout.fillWidth: true + model: currentPack + cellWidth: stickerDimPad + cellHeight: stickerDimPad + boundsBehavior: Flickable.StopAtBounds + clip: true + currentIndex: -1 // prevent sorting from stealing focus + cacheBuffer: 500 + + ScrollHelper { + flickable: parent + anchors.fill: parent + enabled: !Settings.mobileMode + } + + // Individual emoji + delegate: AbstractButton { + width: stickerDim + height: stickerDim + hoverEnabled: true + ToolTip.text: ":" + model.shortCode + ": - " + model.body + ToolTip.visible: hovered + + contentItem: Image { + height: stickerDim + width: stickerDim + source: model.url.replace("mxc://", "image://MxcImage/") + fillMode: Image.PreserveAspectFit + } + + background: Rectangle { + anchors.fill: parent + color: hovered ? Nheko.colors.highlight : 'transparent' + radius: 5 + } + + } + + } + + } + + } + + } + + } + + footer: DialogButtonBox { + id: buttons + + Button { + text: qsTr("Close") + DialogButtonBox.buttonRole: DialogButtonBox.AcceptRole + onClicked: win.close() + } + + } + +} diff --git a/resources/qml/dialogs/InputDialog.qml b/resources/qml/dialogs/InputDialog.qml index 134b78a3..e0f17851 100644 --- a/resources/qml/dialogs/InputDialog.qml +++ b/resources/qml/dialogs/InputDialog.qml @@ -16,6 +16,7 @@ ApplicationWindow { modality: Qt.NonModal flags: Qt.Dialog + Component.onCompleted: Nheko.reparent(inputDialog) width: 350 height: fontMetrics.lineSpacing * 7 diff --git a/resources/qml/emoji/StickerPicker.qml b/resources/qml/emoji/StickerPicker.qml index 813c0b12..3731a948 100644 --- a/resources/qml/emoji/StickerPicker.qml +++ b/resources/qml/emoji/StickerPicker.qml @@ -122,7 +122,6 @@ Menu { id: gridView model: roomid ? TimelineManager.completerFor("stickers", roomid) : null - Layout.preferredHeight: cellHeight * 3.5 Layout.preferredWidth: stickersPerRow * stickerDimPad + 20 Layout.leftMargin: 4 diff --git a/resources/qml/voip/CallInvite.qml b/resources/qml/voip/CallInvite.qml index 1b57976d..253fa25c 100644 --- a/resources/qml/voip/CallInvite.qml +++ b/resources/qml/voip/CallInvite.qml @@ -23,12 +23,13 @@ Popup { } Connections { - target: CallManager - onNewInviteState: { + function onNewInviteState() { if (!CallManager.haveCallInvite) close(); } + + target: CallManager } ColumnLayout { diff --git a/resources/qml/voip/PlaceCall.qml b/resources/qml/voip/PlaceCall.qml index 5f564853..97932cc9 100644 --- a/resources/qml/voip/PlaceCall.qml +++ b/resources/qml/voip/PlaceCall.qml @@ -88,7 +88,7 @@ Popup { onClicked: { if (buttonLayout.validateMic()) { Settings.microphone = micCombo.currentText; - CallManager.sendInvite(room.roomId(), CallType.VOICE); + CallManager.sendInvite(room.roomId, CallType.VOICE); close(); } } @@ -102,7 +102,7 @@ Popup { if (buttonLayout.validateMic()) { Settings.microphone = micCombo.currentText; Settings.camera = cameraCombo.currentText; - CallManager.sendInvite(room.roomId(), CallType.VIDEO); + CallManager.sendInvite(room.roomId, CallType.VIDEO); close(); } } diff --git a/resources/qml/voip/ScreenShare.qml b/resources/qml/voip/ScreenShare.qml index a10057b2..8cd43b1c 100644 --- a/resources/qml/voip/ScreenShare.qml +++ b/resources/qml/voip/ScreenShare.qml @@ -136,7 +136,7 @@ Popup { Settings.screenSharePiP = pipCheckBox.checked; Settings.screenShareRemoteVideo = remoteVideoCheckBox.checked; Settings.screenShareHideCursor = hideCursorCheckBox.checked; - CallManager.sendInvite(room.roomId(), CallType.SCREEN, windowCombo.currentIndex); + CallManager.sendInvite(room.roomId, CallType.SCREEN, windowCombo.currentIndex); close(); } } diff --git a/resources/res.qrc b/resources/res.qrc index d2024eda..e1761cc0 100644 --- a/resources/res.qrc +++ b/resources/res.qrc @@ -10,7 +10,6 @@ icons/ui/do-not-disturb-rounded-sign@2x.png icons/ui/round-remove-button.png icons/ui/round-remove-button@2x.png - icons/ui/double-tick-indicator.png icons/ui/double-tick-indicator@2x.png icons/ui/lock.png @@ -56,22 +55,17 @@ icons/ui/pause-symbol@2x.png icons/ui/remove-symbol.png icons/ui/remove-symbol@2x.png - icons/ui/world.png icons/ui/world@2x.png - icons/ui/tag.png icons/ui/tag@2x.png icons/ui/star.png icons/ui/star@2x.png icons/ui/lowprio.png icons/ui/lowprio@2x.png - icons/ui/edit.png icons/ui/edit@2x.png - icons/ui/mail-reply.png - icons/ui/place-call.png icons/ui/end-call.png icons/ui/microphone-mute.png @@ -79,7 +73,6 @@ icons/ui/screen-share.png icons/ui/toggle-camera-view.png icons/ui/video-call.png - icons/emoji-categories/people.png icons/emoji-categories/people@2x.png icons/emoji-categories/nature.png @@ -100,16 +93,12 @@ nheko.png nheko.svg - splash.png splash@2x.png - register.png register@2x.png - login.png login@2x.png - nheko-512.png nheko-256.png nheko-128.png @@ -124,7 +113,6 @@ qtquickcontrols2.conf - qml/Root.qml qml/ChatPage.qml qml/CommunitiesList.qml @@ -156,14 +144,15 @@ qml/emoji/StickerPicker.qml qml/UserProfile.qml qml/delegates/MessageDelegate.qml - qml/delegates/TextMessage.qml - qml/delegates/NoticeMessage.qml - qml/delegates/ImageMessage.qml - qml/delegates/PlayableMediaMessage.qml + qml/delegates/Encrypted.qml qml/delegates/FileMessage.qml + qml/delegates/ImageMessage.qml + qml/delegates/NoticeMessage.qml qml/delegates/Pill.qml qml/delegates/Placeholder.qml + qml/delegates/PlayableMediaMessage.qml qml/delegates/Reply.qml + qml/delegates/TextMessage.qml qml/device-verification/Waiting.qml qml/device-verification/DeviceVerification.qml qml/device-verification/DigitVerification.qml @@ -172,6 +161,8 @@ qml/device-verification/Failed.qml qml/device-verification/Success.qml qml/dialogs/InputDialog.qml + qml/dialogs/ImagePackSettingsDialog.qml + qml/dialogs/ImagePackEditorDialog.qml qml/ui/Ripple.qml qml/ui/Spinner.qml qml/ui/animations/BlinkAnimation.qml @@ -185,7 +176,12 @@ qml/voip/VideoCall.qml qml/components/AdaptiveLayout.qml qml/components/AdaptiveLayoutElement.qml + qml/components/AvatarListTile.qml qml/components/FlatButton.qml + qml/RoomMembers.qml + qml/InviteDialog.qml + qml/ReadReceipts.qml + qml/RawMessageDialog.qml media/ring.ogg diff --git a/src/Cache.cpp b/src/Cache.cpp index 0bcf9fbf..ee991dc2 100644 --- a/src/Cache.cpp +++ b/src/Cache.cpp @@ -125,7 +125,7 @@ template bool containsStateUpdates(const T &e) { - return std::visit([](const auto &ev) { return Cache::isStateEvent(ev); }, e); + return std::visit([](const auto &ev) { return Cache::isStateEvent_; }, e); } bool @@ -158,7 +158,7 @@ Cache::isHiddenEvent(lmdb::txn &txn, index.session_id = encryptedEvent->content.session_id; index.sender_key = encryptedEvent->content.sender_key; - auto result = olm::decryptEvent(index, *encryptedEvent); + auto result = olm::decryptEvent(index, *encryptedEvent, true); if (!result.error) e = result.event.value(); } @@ -288,6 +288,9 @@ Cache::setup() outboundMegolmSessionDb_ = lmdb::dbi::open(txn, OUTBOUND_MEGOLM_SESSIONS_DB, MDB_CREATE); megolmSessionDataDb_ = lmdb::dbi::open(txn, MEGOLM_SESSIONS_DATA_DB, MDB_CREATE); + // What rooms are encrypted + encryptedRooms_ = lmdb::dbi::open(txn, ENCRYPTED_ROOMS_DB, MDB_CREATE); + txn.commit(); databaseReady_ = true; @@ -298,8 +301,7 @@ Cache::setEncryptedRoom(lmdb::txn &txn, const std::string &room_id) { nhlog::db()->info("mark room {} as encrypted", room_id); - auto db = lmdb::dbi::open(txn, ENCRYPTED_ROOMS_DB, MDB_CREATE); - db.put(txn, room_id, "0"); + encryptedRooms_.put(txn, room_id, "0"); } bool @@ -308,8 +310,7 @@ Cache::isRoomEncrypted(const std::string &room_id) std::string_view unused; auto txn = ro_txn(env_); - auto db = lmdb::dbi::open(txn, ENCRYPTED_ROOMS_DB, MDB_CREATE); - auto res = db.get(txn, room_id, unused); + auto res = encryptedRooms_.get(txn, room_id, unused); return res; } @@ -715,32 +716,29 @@ Cache::restoreOlmAccount() } void -Cache::storeSecret(const std::string &name, const std::string &secret) +Cache::storeSecret(const std::string name, const std::string secret) { auto settings = UserSettings::instance(); - QKeychain::WritePasswordJob job(QCoreApplication::applicationName()); - job.setAutoDelete(false); - job.setInsecureFallback(true); - job.setKey("matrix." + - QString(QCryptographicHash::hash(settings->profile().toUtf8(), - QCryptographicHash::Sha256)) + - "." + name.c_str()); - job.setTextData(QString::fromStdString(secret)); - QEventLoop loop; - job.connect(&job, &QKeychain::Job::finished, &loop, &QEventLoop::quit); - job.start(); - loop.exec(); - - if (job.error()) { - nhlog::db()->warn( - "Storing secret '{}' failed: {}", name, job.errorString().toStdString()); - } else { - emit secretChanged(name); - } + auto job = new QKeychain::WritePasswordJob(QCoreApplication::applicationName()); + job->setInsecureFallback(true); + job->setKey("matrix." + + QString(QCryptographicHash::hash(settings->profile().toUtf8(), + QCryptographicHash::Sha256)) + + "." + name.c_str()); + job->setTextData(QString::fromStdString(secret)); + QObject::connect(job, &QKeychain::Job::finished, job, [name, this](QKeychain::Job *job) { + if (job->error()) { + nhlog::db()->warn( + "Storing secret '{}' failed: {}", name, job->errorString().toStdString()); + } else { + emit secretChanged(name); + } + }); + job->start(); } void -Cache::deleteSecret(const std::string &name) +Cache::deleteSecret(const std::string name) { auto settings = UserSettings::instance(); QKeychain::DeletePasswordJob job(QCoreApplication::applicationName()); @@ -750,6 +748,8 @@ Cache::deleteSecret(const std::string &name) QString(QCryptographicHash::hash(settings->profile().toUtf8(), QCryptographicHash::Sha256)) + "." + name.c_str()); + // FIXME(Nico): Nested event loops are dangerous. Some other slots may resume in the mean + // time! QEventLoop loop; job.connect(&job, &QKeychain::Job::finished, &loop, &QEventLoop::quit); job.start(); @@ -759,7 +759,7 @@ Cache::deleteSecret(const std::string &name) } std::optional -Cache::secret(const std::string &name) +Cache::secret(const std::string name) { auto settings = UserSettings::instance(); QKeychain::ReadPasswordJob job(QCoreApplication::applicationName()); @@ -769,6 +769,8 @@ Cache::secret(const std::string &name) QString(QCryptographicHash::hash(settings->profile().toUtf8(), QCryptographicHash::Sha256)) + "." + name.c_str()); + // FIXME(Nico): Nested event loops are dangerous. Some other slots may resume in the mean + // time! QEventLoop loop; job.connect(&job, &QKeychain::Job::finished, &loop, &QEventLoop::quit); job.start(); @@ -3383,26 +3385,30 @@ Cache::getChildRoomIds(const std::string &room_id) } std::vector -Cache::getImagePacks(const std::string &room_id, bool stickers) +Cache::getImagePacks(const std::string &room_id, std::optional stickers) { auto txn = ro_txn(env_); std::vector infos; - auto addPack = [&infos, stickers](const mtx::events::msc2545::ImagePack &pack) { - if (!pack.pack || (stickers ? pack.pack->is_sticker() : pack.pack->is_emoji())) { + auto addPack = [&infos, stickers](const mtx::events::msc2545::ImagePack &pack, + const std::string &source_room, + const std::string &state_key) { + if (!pack.pack || !stickers.has_value() || + (stickers.value() ? pack.pack->is_sticker() : pack.pack->is_emoji())) { ImagePackInfo info; - if (pack.pack) - info.packname = pack.pack->display_name; + info.source_room = source_room; + info.state_key = state_key; + info.pack.pack = pack.pack; for (const auto &img : pack.images) { - if (img.second.overrides_usage() && + if (stickers.has_value() && img.second.overrides_usage() && (stickers ? !img.second.is_sticker() : !img.second.is_emoji())) continue; - info.images.insert(img); + info.pack.images.insert(img); } - if (!info.images.empty()) + if (!info.pack.images.empty()) infos.push_back(std::move(info)); } }; @@ -3414,7 +3420,7 @@ Cache::getImagePacks(const std::string &room_id, bool stickers) std::get_if>( &*accountpack); if (tmp) - addPack(tmp->content); + addPack(tmp->content, "", ""); } // packs from rooms, that were enabled globally @@ -3433,7 +3439,7 @@ Cache::getImagePacks(const std::string &room_id, bool stickers) if (auto pack = getStateEvent( txn, room_id2, state_id)) - addPack(pack->content); + addPack(pack->content, room_id2, state_id); } } } @@ -3441,16 +3447,23 @@ Cache::getImagePacks(const std::string &room_id, bool stickers) // packs from current room if (auto pack = getStateEvent(txn, room_id)) { - addPack(pack->content); + addPack(pack->content, room_id, ""); } for (const auto &pack : getStateEventsWithType(txn, room_id)) { - addPack(pack.content); + addPack(pack.content, room_id, pack.state_key); } return infos; } +std::optional +Cache::getAccountData(mtx::events::EventType type, const std::string &room_id) +{ + auto txn = ro_txn(env_); + return getAccountData(txn, type, room_id); +} + std::optional Cache::getAccountData(lmdb::txn &txn, mtx::events::EventType type, const std::string &room_id) { @@ -3529,7 +3542,7 @@ Cache::roomMembers(const std::string &room_id) } std::map> -Cache::getMembersWithKeys(const std::string &room_id) +Cache::getMembersWithKeys(const std::string &room_id, bool verified_only) { std::string_view keys; @@ -3546,10 +3559,51 @@ Cache::getMembersWithKeys(const std::string &room_id) auto res = keysDb.get(txn, user_id, keys); if (res) { - members[std::string(user_id)] = - json::parse(keys).get(); + auto k = json::parse(keys).get(); + if (verified_only) { + auto verif = verificationStatus(std::string(user_id)); + if (verif.user_verified == crypto::Trust::Verified || + !verif.verified_devices.empty()) { + auto keyCopy = k; + keyCopy.device_keys.clear(); + + std::copy_if( + k.device_keys.begin(), + k.device_keys.end(), + std::inserter(keyCopy.device_keys, + keyCopy.device_keys.end()), + [&verif](const auto &key) { + auto curve25519 = key.second.keys.find( + "curve25519:" + key.first); + if (curve25519 == key.second.keys.end()) + return false; + if (auto t = + verif.verified_device_keys.find( + curve25519->second); + t == + verif.verified_device_keys.end() || + t->second != crypto::Trust::Verified) + return false; + + return key.first == + key.second.device_id && + std::find( + verif.verified_devices.begin(), + verif.verified_devices.end(), + key.first) != + verif.verified_devices.end(); + }); + + if (!keyCopy.device_keys.empty()) + members[std::string(user_id)] = + std::move(keyCopy); + } + } else { + members[std::string(user_id)] = std::move(k); + } } else { - members[std::string(user_id)] = {}; + if (!verified_only) + members[std::string(user_id)] = {}; } } cursor.close(); @@ -4240,6 +4294,8 @@ to_json(nlohmann::json &obj, const GroupSessionData &msg) obj["forwarding_curve25519_key_chain"] = msg.forwarding_curve25519_key_chain; obj["currently"] = msg.currently; + + obj["indices"] = msg.indices; } void @@ -4253,6 +4309,8 @@ from_json(const nlohmann::json &obj, GroupSessionData &msg) obj.value("forwarding_curve25519_key_chain", std::vector{}); msg.currently = obj.value("currently", SharedWithUsers{}); + + msg.indices = obj.value("indices", std::map()); } void diff --git a/src/CacheCryptoStructs.h b/src/CacheCryptoStructs.h index 409c9d67..69d64885 100644 --- a/src/CacheCryptoStructs.h +++ b/src/CacheCryptoStructs.h @@ -50,6 +50,9 @@ struct GroupSessionData std::string sender_claimed_ed25519_key; std::vector forwarding_curve25519_key_chain; + //! map from index to event_id to check for replay attacks + std::map indices; + // who has access to this session. // Rotate, when a user leaves the room and share, when a user gets added. SharedWithUsers currently; diff --git a/src/CacheStructs.h b/src/CacheStructs.h index f274d70f..4a5c5c76 100644 --- a/src/CacheStructs.h +++ b/src/CacheStructs.h @@ -113,6 +113,7 @@ struct RoomSearchResult struct ImagePackInfo { - std::string packname; - std::map images; + mtx::events::msc2545::ImagePack pack; + std::string source_room; + std::string state_key; }; diff --git a/src/Cache_p.h b/src/Cache_p.h index 13fbc371..30c365a6 100644 --- a/src/Cache_p.h +++ b/src/Cache_p.h @@ -48,7 +48,8 @@ public: // user cache stores user keys std::optional userKeys(const std::string &user_id); std::map> getMembersWithKeys( - const std::string &room_id); + const std::string &room_id, + bool verified_only); void updateUserKeys(const std::string &sync_token, const mtx::responses::QueryKeys &keyQuery); void markUserKeysOutOfDate(lmdb::txn &txn, @@ -97,6 +98,12 @@ public: return getStateEvent(txn, room_id, state_key); } + //! retrieve a specific event from account data + //! pass empty room_id for global account data + std::optional getAccountData( + mtx::events::EventType type, + const std::string &room_id = ""); + //! Retrieve member info from a room. std::vector getMembers(const std::string &room_id, std::size_t startIndex = 0, @@ -225,7 +232,8 @@ public: std::vector getParentRoomIds(const std::string &room_id); std::vector getChildRoomIds(const std::string &room_id); - std::vector getImagePacks(const std::string &room_id, bool stickers); + std::vector getImagePacks(const std::string &room_id, + std::optional stickers); //! Mark a room that uses e2e encryption. void setEncryptedRoom(lmdb::txn &txn, const std::string &room_id); @@ -278,20 +286,14 @@ public: void saveOlmAccount(const std::string &pickled); std::string restoreOlmAccount(); - void storeSecret(const std::string &name, const std::string &secret); - void deleteSecret(const std::string &name); - std::optional secret(const std::string &name); + void storeSecret(const std::string name, const std::string secret); + void deleteSecret(const std::string name); + std::optional secret(const std::string name); template - static constexpr bool isStateEvent(const mtx::events::StateEvent &) - { - return true; - } - template - static constexpr bool isStateEvent(const mtx::events::Event &) - { - return false; - } + constexpr static bool isStateEvent_ = + std::is_same_v>, + mtx::events::StateEvent().content)>>; static int compare_state_key(const MDB_val *a, const MDB_val *b) { @@ -408,11 +410,27 @@ private: } std::visit( - [&txn, &statesdb, &stateskeydb, &eventsDb](auto e) { - if constexpr (isStateEvent(e)) { + [&txn, &statesdb, &stateskeydb, &eventsDb, &membersdb](const auto &e) { + if constexpr (isStateEvent_) { eventsDb.put(txn, e.event_id, json(e).dump()); - if (e.type != EventType::Unsupported) { + if (std::is_same_v< + std::remove_cv_t>, + StateEvent>) { + if (e.type == EventType::RoomMember) + membersdb.del(txn, e.state_key, ""); + else if (e.state_key.empty()) + statesdb.del(txn, to_string(e.type)); + else + stateskeydb.del( + txn, + to_string(e.type), + json::object({ + {"key", e.state_key}, + {"id", e.event_id}, + }) + .dump()); + } else if (e.type != EventType::Unsupported) { if (e.state_key.empty()) statesdb.put( txn, to_string(e.type), json(e).dump()); @@ -682,6 +700,8 @@ private: lmdb::dbi outboundMegolmSessionDb_; lmdb::dbi megolmSessionDataDb_; + lmdb::dbi encryptedRooms_; + QString localUserId_; QString cacheDirectory_; diff --git a/src/ChatPage.cpp b/src/ChatPage.cpp index 10a91557..42e3bc7b 100644 --- a/src/ChatPage.cpp +++ b/src/ChatPage.cpp @@ -31,7 +31,6 @@ #include "notifications/Manager.h" -#include "dialogs/ReadReceipts.h" #include "timeline/TimelineViewManager.h" #include "blurhash.hpp" @@ -116,29 +115,31 @@ ChatPage::ChatPage(QSharedPointer userSettings, QWidget *parent) connect(this, &ChatPage::loggedOut, this, &ChatPage::logout); - connect(view_manager_, &TimelineViewManager::inviteUsers, this, [this](QStringList users) { - const auto room_id = currentRoom().toStdString(); + connect( + view_manager_, + &TimelineViewManager::inviteUsers, + this, + [this](QString roomId, QStringList users) { + for (int ii = 0; ii < users.size(); ++ii) { + QTimer::singleShot(ii * 500, this, [this, roomId, ii, users]() { + const auto user = users.at(ii); - for (int ii = 0; ii < users.size(); ++ii) { - QTimer::singleShot(ii * 500, this, [this, room_id, ii, users]() { - const auto user = users.at(ii); + http::client()->invite_user( + roomId.toStdString(), + user.toStdString(), + [this, user](const mtx::responses::RoomInvite &, + mtx::http::RequestErr err) { + if (err) { + emit showNotification( + tr("Failed to invite user: %1").arg(user)); + return; + } - http::client()->invite_user( - room_id, - user.toStdString(), - [this, user](const mtx::responses::RoomInvite &, - mtx::http::RequestErr err) { - if (err) { - emit showNotification( - tr("Failed to invite user: %1").arg(user)); - return; - } - - emit showNotification(tr("Invited user: %1").arg(user)); - }); - }); - } - }); + emit showNotification(tr("Invited user: %1").arg(user)); + }); + }); + } + }); connect(this, &ChatPage::leftRoom, this, &ChatPage::removeRoom); connect(this, &ChatPage::newRoom, this, &ChatPage::changeRoom, Qt::QueuedConnection); @@ -927,31 +928,33 @@ ChatPage::currentPresence() const void ChatPage::ensureOneTimeKeyCount(const std::map &counts) { - for (const auto &entry : counts) { - if (entry.second < MAX_ONETIME_KEYS) { - const int nkeys = MAX_ONETIME_KEYS - entry.second; + uint16_t count = 0; + if (auto c = counts.find(mtx::crypto::SIGNED_CURVE25519); c != counts.end()) + count = c->second; - nhlog::crypto()->info("uploading {} {} keys", nkeys, entry.first); - olm::client()->generate_one_time_keys(nkeys); + if (count < MAX_ONETIME_KEYS) { + const int nkeys = MAX_ONETIME_KEYS - count; - http::client()->upload_keys( - olm::client()->create_upload_keys_request(), - [](const mtx::responses::UploadKeys &, mtx::http::RequestErr err) { - if (err) { - nhlog::crypto()->warn( - "failed to update one-time keys: {} {} {}", - err->matrix_error.error, - static_cast(err->status_code), - static_cast(err->error_code)); + nhlog::crypto()->info( + "uploading {} {} keys", nkeys, mtx::crypto::SIGNED_CURVE25519); + olm::client()->generate_one_time_keys(nkeys); - if (err->status_code < 400 || err->status_code >= 500) - return; - } + http::client()->upload_keys( + olm::client()->create_upload_keys_request(), + [](const mtx::responses::UploadKeys &, mtx::http::RequestErr err) { + if (err) { + nhlog::crypto()->warn("failed to update one-time keys: {} {} {}", + err->matrix_error.error, + static_cast(err->status_code), + static_cast(err->error_code)); - // mark as published anyway, otherwise we may end up in a loop. - olm::mark_keys_as_published(); - }); - } + if (err->status_code < 400 || err->status_code >= 500) + return; + } + + // mark as published anyway, otherwise we may end up in a loop. + olm::mark_keys_as_published(); + }); } } @@ -1024,8 +1027,15 @@ ChatPage::decryptDownloadedSecrets(mtx::secret_storage::AesHmacSha2KeyDescriptio auto decryptionKey = mtx::crypto::key_from_recoverykey(text.toStdString(), keyDesc); - if (!decryptionKey) - decryptionKey = mtx::crypto::key_from_passphrase(text.toStdString(), keyDesc); + if (!decryptionKey && keyDesc.passphrase) { + try { + decryptionKey = + mtx::crypto::key_from_passphrase(text.toStdString(), keyDesc); + } catch (std::exception &e) { + nhlog::crypto()->error("Failed to derive secret key from passphrase: {}", + e.what()); + } + } if (!decryptionKey) { QMessageBox::information( diff --git a/src/ImagePackModel.cpp b/src/CombinedImagePackModel.cpp similarity index 77% rename from src/ImagePackModel.cpp rename to src/CombinedImagePackModel.cpp index 9b0dca8d..341a34ec 100644 --- a/src/ImagePackModel.cpp +++ b/src/CombinedImagePackModel.cpp @@ -2,21 +2,24 @@ // // SPDX-License-Identifier: GPL-3.0-or-later -#include "ImagePackModel.h" +#include "CombinedImagePackModel.h" #include "Cache_p.h" #include "CompletionModelRoles.h" -ImagePackModel::ImagePackModel(const std::string &roomId, bool stickers, QObject *parent) +CombinedImagePackModel::CombinedImagePackModel(const std::string &roomId, + bool stickers, + QObject *parent) : QAbstractListModel(parent) , room_id(roomId) { auto packs = cache::client()->getImagePacks(room_id, stickers); for (const auto &pack : packs) { - QString packname = QString::fromStdString(pack.packname); + QString packname = + pack.pack.pack ? QString::fromStdString(pack.pack.pack->display_name) : ""; - for (const auto &img : pack.images) { + for (const auto &img : pack.pack.images) { ImageDesc i{}; i.shortcode = QString::fromStdString(img.first); i.packname = packname; @@ -27,13 +30,13 @@ ImagePackModel::ImagePackModel(const std::string &roomId, bool stickers, QObject } int -ImagePackModel::rowCount(const QModelIndex &) const +CombinedImagePackModel::rowCount(const QModelIndex &) const { return (int)images.size(); } QHash -ImagePackModel::roleNames() const +CombinedImagePackModel::roleNames() const { return { {CompletionModel::CompletionRole, "completionRole"}, @@ -48,7 +51,7 @@ ImagePackModel::roleNames() const } QVariant -ImagePackModel::data(const QModelIndex &index, int role) const +CombinedImagePackModel::data(const QModelIndex &index, int role) const { if (hasIndex(index.row(), index.column(), index.parent())) { switch (role) { diff --git a/src/ImagePackModel.h b/src/CombinedImagePackModel.h similarity index 87% rename from src/ImagePackModel.h rename to src/CombinedImagePackModel.h index 937014ec..f0f69799 100644 --- a/src/ImagePackModel.h +++ b/src/CombinedImagePackModel.h @@ -8,7 +8,7 @@ #include -class ImagePackModel : public QAbstractListModel +class CombinedImagePackModel : public QAbstractListModel { Q_OBJECT public: @@ -21,7 +21,7 @@ public: OriginalRow, }; - ImagePackModel(const std::string &roomId, bool stickers, QObject *parent = nullptr); + CombinedImagePackModel(const std::string &roomId, bool stickers, QObject *parent = nullptr); QHash roleNames() const override; int rowCount(const QModelIndex &parent = QModelIndex()) const override; QVariant data(const QModelIndex &index, int role) const override; diff --git a/src/ImagePackListModel.cpp b/src/ImagePackListModel.cpp new file mode 100644 index 00000000..6392de22 --- /dev/null +++ b/src/ImagePackListModel.cpp @@ -0,0 +1,94 @@ +// SPDX-FileCopyrightText: 2021 Nheko Contributors +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "ImagePackListModel.h" + +#include + +#include "Cache_p.h" +#include "SingleImagePackModel.h" + +ImagePackListModel::ImagePackListModel(const std::string &roomId, QObject *parent) + : QAbstractListModel(parent) + , room_id(roomId) +{ + auto packs_ = cache::client()->getImagePacks(room_id, std::nullopt); + + for (const auto &pack : packs_) { + packs.push_back( + QSharedPointer(new SingleImagePackModel(pack))); + } +} + +int +ImagePackListModel::rowCount(const QModelIndex &) const +{ + return (int)packs.size(); +} + +QHash +ImagePackListModel::roleNames() const +{ + return { + {Roles::DisplayName, "displayName"}, + {Roles::AvatarUrl, "avatarUrl"}, + {Roles::FromAccountData, "fromAccountData"}, + {Roles::FromCurrentRoom, "fromCurrentRoom"}, + {Roles::StateKey, "statekey"}, + {Roles::RoomId, "roomid"}, + }; +} + +QVariant +ImagePackListModel::data(const QModelIndex &index, int role) const +{ + if (hasIndex(index.row(), index.column(), index.parent())) { + const auto &pack = packs.at(index.row()); + switch (role) { + case Roles::DisplayName: + return pack->packname(); + case Roles::AvatarUrl: + return pack->avatarUrl(); + case Roles::FromAccountData: + return pack->roomid().isEmpty(); + case Roles::FromCurrentRoom: + return pack->roomid().toStdString() == this->room_id; + case Roles::StateKey: + return pack->statekey(); + case Roles::RoomId: + return pack->roomid(); + default: + return {}; + } + } + return {}; +} + +SingleImagePackModel * +ImagePackListModel::packAt(int row) +{ + if (row < 0 || static_cast(row) >= packs.size()) + return {}; + auto e = packs.at(row).get(); + QQmlEngine::setObjectOwnership(e, QQmlEngine::CppOwnership); + return e; +} + +SingleImagePackModel * +ImagePackListModel::newPack(bool inRoom) +{ + ImagePackInfo info{}; + if (inRoom) + info.source_room = room_id; + return new SingleImagePackModel(info); +} + +bool +ImagePackListModel::containsAccountPack() const +{ + for (const auto &p : packs) + if (p->roomid().isEmpty()) + return true; + return false; +} diff --git a/src/ImagePackListModel.h b/src/ImagePackListModel.h new file mode 100644 index 00000000..2aa5abb2 --- /dev/null +++ b/src/ImagePackListModel.h @@ -0,0 +1,41 @@ +// SPDX-FileCopyrightText: 2021 Nheko Contributors +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include +#include +#include + +class SingleImagePackModel; +class ImagePackListModel : public QAbstractListModel +{ + Q_OBJECT + Q_PROPERTY(bool containsAccountPack READ containsAccountPack CONSTANT) +public: + enum Roles + { + DisplayName = Qt::UserRole, + AvatarUrl, + FromAccountData, + FromCurrentRoom, + StateKey, + RoomId, + }; + + ImagePackListModel(const std::string &roomId, QObject *parent = nullptr); + QHash roleNames() const override; + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + QVariant data(const QModelIndex &index, int role) const override; + + Q_INVOKABLE SingleImagePackModel *packAt(int row); + Q_INVOKABLE SingleImagePackModel *newPack(bool inRoom); + + bool containsAccountPack() const; + +private: + std::string room_id; + + std::vector> packs; +}; diff --git a/src/InviteeItem.cpp b/src/InviteeItem.cpp deleted file mode 100644 index 27f02560..00000000 --- a/src/InviteeItem.cpp +++ /dev/null @@ -1,28 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Nheko Contributors -// -// SPDX-License-Identifier: GPL-3.0-or-later - -#include -#include -#include - -#include "InviteeItem.h" - -constexpr int SidePadding = 10; - -InviteeItem::InviteeItem(mtx::identifiers::User user, QWidget *parent) - : QWidget{parent} - , user_{QString::fromStdString(user.to_string())} -{ - auto topLayout_ = new QHBoxLayout(this); - topLayout_->setSpacing(0); - topLayout_->setContentsMargins(SidePadding, 0, 3 * SidePadding, 0); - - name_ = new QLabel(user_, this); - removeUserBtn_ = new QPushButton(tr("Remove"), this); - - topLayout_->addWidget(name_); - topLayout_->addWidget(removeUserBtn_, 0, Qt::AlignRight); - - connect(removeUserBtn_, &QPushButton::clicked, this, &InviteeItem::removeItem); -} diff --git a/src/InviteeItem.h b/src/InviteeItem.h deleted file mode 100644 index 014541ea..00000000 --- a/src/InviteeItem.h +++ /dev/null @@ -1,31 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Nheko Contributors -// -// SPDX-License-Identifier: GPL-3.0-or-later - -#pragma once - -#include - -#include - -class QPushButton; -class QLabel; - -class InviteeItem : public QWidget -{ - Q_OBJECT - -public: - InviteeItem(mtx::identifiers::User user, QWidget *parent = nullptr); - - QString userID() { return user_; } - -signals: - void removeItem(); - -private: - QString user_; - - QLabel *name_; - QPushButton *removeUserBtn_; -}; diff --git a/src/InviteesModel.cpp b/src/InviteesModel.cpp new file mode 100644 index 00000000..27b2116f --- /dev/null +++ b/src/InviteesModel.cpp @@ -0,0 +1,84 @@ +// SPDX-FileCopyrightText: 2021 Nheko Contributors +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "InviteesModel.h" + +#include "Cache.h" +#include "Logging.h" +#include "MatrixClient.h" +#include "mtx/responses/profile.hpp" + +InviteesModel::InviteesModel(QObject *parent) + : QAbstractListModel{parent} +{} + +void +InviteesModel::addUser(QString mxid) +{ + beginInsertRows(QModelIndex(), invitees_.count(), invitees_.count()); + + auto invitee = new Invitee{mxid, this}; + auto indexOfInvitee = invitees_.count(); + connect(invitee, &Invitee::userInfoLoaded, this, [this, indexOfInvitee]() { + emit dataChanged(index(indexOfInvitee), index(indexOfInvitee)); + }); + + invitees_.push_back(invitee); + + endInsertRows(); + emit countChanged(); +} + +QHash +InviteesModel::roleNames() const +{ + return {{Mxid, "mxid"}, {DisplayName, "displayName"}, {AvatarUrl, "avatarUrl"}}; +} + +QVariant +InviteesModel::data(const QModelIndex &index, int role) const +{ + if (!index.isValid() || index.row() >= (int)invitees_.size() || index.row() < 0) + return {}; + + switch (role) { + case Mxid: + return invitees_[index.row()]->mxid_; + case DisplayName: + return invitees_[index.row()]->displayName_; + case AvatarUrl: + return invitees_[index.row()]->avatarUrl_; + default: + return {}; + } +} + +QStringList +InviteesModel::mxids() +{ + QStringList mxidList; + for (int i = 0; i < invitees_.length(); ++i) + mxidList.push_back(invitees_[i]->mxid_); + return mxidList; +} + +Invitee::Invitee(const QString &mxid, QObject *parent) + : QObject{parent} + , mxid_{mxid} +{ + http::client()->get_profile( + mxid_.toStdString(), + [this](const mtx::responses::Profile &res, mtx::http::RequestErr err) { + if (err) { + nhlog::net()->warn("failed to retrieve profile info"); + emit userInfoLoaded(); + return; + } + + displayName_ = QString::fromStdString(res.display_name); + avatarUrl_ = QString::fromStdString(res.avatar_url); + + emit userInfoLoaded(); + }); +} diff --git a/src/InviteesModel.h b/src/InviteesModel.h new file mode 100644 index 00000000..a4e19ebb --- /dev/null +++ b/src/InviteesModel.h @@ -0,0 +1,63 @@ +// SPDX-FileCopyrightText: 2021 Nheko Contributors +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#ifndef INVITEESMODEL_H +#define INVITEESMODEL_H + +#include +#include + +class Invitee : public QObject +{ + Q_OBJECT + +public: + Invitee(const QString &mxid, QObject *parent = nullptr); + +signals: + void userInfoLoaded(); + +private: + const QString mxid_; + QString displayName_; + QString avatarUrl_; + + friend class InviteesModel; +}; + +class InviteesModel : public QAbstractListModel +{ + Q_OBJECT + + Q_PROPERTY(int count READ rowCount NOTIFY countChanged) + +public: + enum Roles + { + Mxid, + DisplayName, + AvatarUrl, + }; + + InviteesModel(QObject *parent = nullptr); + + Q_INVOKABLE void addUser(QString mxid); + + QHash roleNames() const override; + int rowCount(const QModelIndex & = QModelIndex()) const override + { + return (int)invitees_.size(); + } + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + QStringList mxids(); + +signals: + void accept(); + void countChanged(); + +private: + QVector invitees_; +}; + +#endif // INVITEESMODEL_H diff --git a/src/Logging.cpp b/src/Logging.cpp index 642e8957..67bcaf7a 100644 --- a/src/Logging.cpp +++ b/src/Logging.cpp @@ -30,19 +30,11 @@ qmlMessageHandler(QtMsgType type, const QMessageLogContext &context, const QStri const char *function = context.function ? context.function : ""; if ( - // Surpress binding wrning for now, as we can't set restore mode to keep compat with - // qt 5.10 - msg.contains(QStringLiteral( - "QML Binding: Not restoring previous value because restoreMode has not been set.")) || // The default style has the point size set. If you use pixel size anywhere, you get // that warning, which is useless, since sometimes you need the pixel size to match the // text to the size of the outer element for example. This is done in the avatar and // without that you get one warning for every Avatar displayed, which is stupid! - msg.endsWith(QStringLiteral("Both point size and pixel size set. Using pixel size.")) || - // The new syntax breaks rebinding on Qt < 5.15. Until we can drop that, we still need it. - msg.endsWith(QStringLiteral( - "QML Connections: Implicitly defined onFoo properties in Connections are " - "deprecated. Use this syntax instead: function onFoo() { ... }"))) + msg.endsWith(QStringLiteral("Both point size and pixel size set. Using pixel size."))) return; switch (type) { diff --git a/src/MainWindow.cpp b/src/MainWindow.cpp index ed337ca4..8bc90f29 100644 --- a/src/MainWindow.cpp +++ b/src/MainWindow.cpp @@ -21,6 +21,7 @@ #include "LoginPage.h" #include "MainWindow.h" #include "MatrixClient.h" +#include "MemberList.h" #include "RegisterPage.h" #include "TrayIcon.h" #include "UserSettingsPage.h" @@ -32,12 +33,9 @@ #include "ui/SnackBar.h" #include "dialogs/CreateRoom.h" -#include "dialogs/InviteUsers.h" #include "dialogs/JoinRoom.h" #include "dialogs/LeaveRoom.h" #include "dialogs/Logout.h" -#include "dialogs/MemberList.h" -#include "dialogs/ReadReceipts.h" MainWindow *MainWindow::instance_ = nullptr; @@ -310,14 +308,6 @@ MainWindow::hasActiveUser() settings.contains(prefix + "auth/user_id"); } -void -MainWindow::openMemberListDialog(const QString &room_id) -{ - auto dialog = new dialogs::MemberList(room_id, this); - - showDialog(dialog); -} - void MainWindow::openLeaveRoomDialog(const QString &room_id) { @@ -341,18 +331,6 @@ MainWindow::showOverlayProgressBar() showSolidOverlayModal(spinner_); } -void -MainWindow::openInviteUsersDialog(std::function callback) -{ - auto dialog = new dialogs::InviteUsers(this); - connect(dialog, &dialogs::InviteUsers::sendInvites, this, [callback](QStringList invitees) { - if (!invitees.isEmpty()) - callback(invitees); - }); - - showDialog(dialog); -} - void MainWindow::openJoinRoomDialog(std::function callback) { @@ -419,27 +397,6 @@ MainWindow::openLogoutDialog() showDialog(dialog); } -void -MainWindow::openReadReceiptsDialog(const QString &event_id) -{ - auto dialog = new dialogs::ReadReceipts(this); - - const auto room_id = chat_page_->currentRoom(); - - try { - dialog->addUsers(cache::readReceipts(event_id, room_id)); - } catch (const lmdb::error &) { - nhlog::db()->warn("failed to retrieve read receipts for {} {}", - event_id.toStdString(), - chat_page_->currentRoom().toStdString()); - dialog->deleteLater(); - - return; - } - - showDialog(dialog); -} - bool MainWindow::hasActiveDialogs() const { diff --git a/src/MainWindow.h b/src/MainWindow.h index 3571f079..d423af9f 100644 --- a/src/MainWindow.h +++ b/src/MainWindow.h @@ -65,8 +65,6 @@ public: std::function callback); void openJoinRoomDialog(std::function callback); void openLogoutDialog(); - void openMemberListDialog(const QString &room_id); - void openReadReceiptsDialog(const QString &event_id); void hideOverlay(); void showSolidOverlayModal(QWidget *content, diff --git a/src/MemberList.cpp b/src/MemberList.cpp new file mode 100644 index 00000000..196647fe --- /dev/null +++ b/src/MemberList.cpp @@ -0,0 +1,100 @@ +// SPDX-FileCopyrightText: 2021 Nheko Contributors +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "MemberList.h" + +#include "Cache.h" +#include "ChatPage.h" +#include "Config.h" +#include "Logging.h" +#include "Utils.h" +#include "timeline/TimelineViewManager.h" + +MemberList::MemberList(const QString &room_id, QObject *parent) + : QAbstractListModel{parent} + , room_id_{room_id} +{ + try { + info_ = cache::singleRoomInfo(room_id_.toStdString()); + } catch (const lmdb::error &) { + nhlog::db()->warn("failed to retrieve room info from cache: {}", + room_id_.toStdString()); + } + + try { + auto members = cache::getMembers(room_id_.toStdString()); + addUsers(members); + numUsersLoaded_ = members.size(); + } catch (const lmdb::error &e) { + nhlog::db()->critical("Failed to retrieve members from cache: {}", e.what()); + } +} + +void +MemberList::addUsers(const std::vector &members) +{ + beginInsertRows( + QModelIndex{}, m_memberList.count(), m_memberList.count() + members.size() - 1); + + for (const auto &member : members) + m_memberList.push_back( + {member, + ChatPage::instance()->timelineManager()->rooms()->currentRoom()->avatarUrl( + member.user_id)}); + + endInsertRows(); +} + +QHash +MemberList::roleNames() const +{ + return { + {Mxid, "mxid"}, + {DisplayName, "displayName"}, + {AvatarUrl, "avatarUrl"}, + }; +} + +QVariant +MemberList::data(const QModelIndex &index, int role) const +{ + if (!index.isValid() || index.row() >= (int)m_memberList.size() || index.row() < 0) + return {}; + + switch (role) { + case Mxid: + return m_memberList[index.row()].first.user_id; + case DisplayName: + return m_memberList[index.row()].first.display_name; + case AvatarUrl: + return m_memberList[index.row()].second; + default: + return {}; + } +} + +bool +MemberList::canFetchMore(const QModelIndex &) const +{ + const size_t numMembers = rowCount(); + if (numMembers > 1 && numMembers < info_.member_count) + return true; + else + return false; +} + +void +MemberList::fetchMore(const QModelIndex &) +{ + loadingMoreMembers_ = true; + emit loadingMoreMembersChanged(); + + auto members = cache::getMembers(room_id_.toStdString(), rowCount()); + addUsers(members); + numUsersLoaded_ += members.size(); + emit numUsersLoadedChanged(); + + loadingMoreMembers_ = false; + emit loadingMoreMembersChanged(); +} diff --git a/src/MemberList.h b/src/MemberList.h new file mode 100644 index 00000000..e6522694 --- /dev/null +++ b/src/MemberList.h @@ -0,0 +1,67 @@ +// SPDX-FileCopyrightText: 2021 Nheko Contributors +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include + +#include "CacheStructs.h" + +class MemberList : public QAbstractListModel +{ + Q_OBJECT + + Q_PROPERTY(QString roomName READ roomName NOTIFY roomNameChanged) + Q_PROPERTY(int memberCount READ memberCount NOTIFY memberCountChanged) + Q_PROPERTY(QString avatarUrl READ avatarUrl NOTIFY avatarUrlChanged) + Q_PROPERTY(QString roomId READ roomId NOTIFY roomIdChanged) + Q_PROPERTY(int numUsersLoaded READ numUsersLoaded NOTIFY numUsersLoadedChanged) + Q_PROPERTY(bool loadingMoreMembers READ loadingMoreMembers NOTIFY loadingMoreMembersChanged) + +public: + enum Roles + { + Mxid, + DisplayName, + AvatarUrl, + }; + MemberList(const QString &room_id, QObject *parent = nullptr); + + QHash roleNames() const override; + int rowCount(const QModelIndex &parent = QModelIndex()) const override + { + Q_UNUSED(parent) + return static_cast(m_memberList.size()); + } + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + + QString roomName() const { return QString::fromStdString(info_.name); } + int memberCount() const { return info_.member_count; } + QString avatarUrl() const { return QString::fromStdString(info_.avatar_url); } + QString roomId() const { return room_id_; } + int numUsersLoaded() const { return numUsersLoaded_; } + bool loadingMoreMembers() const { return loadingMoreMembers_; } + +signals: + void roomNameChanged(); + void memberCountChanged(); + void avatarUrlChanged(); + void roomIdChanged(); + void numUsersLoadedChanged(); + void loadingMoreMembersChanged(); + +public slots: + void addUsers(const std::vector &users); + +protected: + bool canFetchMore(const QModelIndex &) const override; + void fetchMore(const QModelIndex &) override; + +private: + QVector> m_memberList; + QString room_id_; + RoomInfo info_; + int numUsersLoaded_{0}; + bool loadingMoreMembers_{false}; +}; diff --git a/src/MxcImageProvider.cpp b/src/MxcImageProvider.cpp index ab6540a4..b8648269 100644 --- a/src/MxcImageProvider.cpp +++ b/src/MxcImageProvider.cpp @@ -22,7 +22,14 @@ QHash infos; QQuickImageResponse * MxcImageProvider::requestImageResponse(const QString &id, const QSize &requestedSize) { - MxcImageResponse *response = new MxcImageResponse(id, requestedSize); + auto id_ = id; + bool crop = true; + if (id.endsWith("?scale")) { + crop = false; + id_.remove("?scale"); + } + + MxcImageResponse *response = new MxcImageResponse(id_, crop, requestedSize); pool.start(response); return response; } @@ -36,20 +43,24 @@ void MxcImageResponse::run() { MxcImageProvider::download( - m_id, m_requestedSize, [this](QString, QSize, QImage image, QString) { + m_id, + m_requestedSize, + [this](QString, QSize, QImage image, QString) { if (image.isNull()) { m_error = "Failed to download image."; } else { m_image = image; } emit finished(); - }); + }, + m_crop); } void MxcImageProvider::download(const QString &id, const QSize &requestedSize, - std::function then) + std::function then, + bool crop) { std::optional encryptionInfo; auto temp = infos.find("mxc://" + id); @@ -58,11 +69,12 @@ MxcImageProvider::download(const QString &id, if (requestedSize.isValid() && !encryptionInfo) { QString fileName = - QString("%1_%2x%3_crop") + QString("%1_%2x%3_%4") .arg(QString::fromUtf8(id.toUtf8().toBase64(QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals))) .arg(requestedSize.width()) - .arg(requestedSize.height()); + .arg(requestedSize.height()) + .arg(crop ? "crop" : "scale"); QFileInfo fileInfo(QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + "/media_cache", fileName); @@ -85,7 +97,7 @@ MxcImageProvider::download(const QString &id, opts.mxc_url = "mxc://" + id.toStdString(); opts.width = requestedSize.width() > 0 ? requestedSize.width() : -1; opts.height = requestedSize.height() > 0 ? requestedSize.height() : -1; - opts.method = "crop"; + opts.method = crop ? "crop" : "scale"; http::client()->get_thumbnail( opts, [fileInfo, requestedSize, then, id](const std::string &res, @@ -196,7 +208,6 @@ MxcImageProvider::download(const QString &id, image.setText("original filename", QString::fromStdString(originalFilename)); image.setText("mxc url", "mxc://" + id); - image.save(fileInfo.absoluteFilePath()); then(id, requestedSize, image, fileInfo.absoluteFilePath()); }); } catch (std::exception &e) { diff --git a/src/MxcImageProvider.h b/src/MxcImageProvider.h index 7b960836..61d82852 100644 --- a/src/MxcImageProvider.h +++ b/src/MxcImageProvider.h @@ -19,9 +19,10 @@ class MxcImageResponse , public QRunnable { public: - MxcImageResponse(const QString &id, const QSize &requestedSize) + MxcImageResponse(const QString &id, bool crop, const QSize &requestedSize) : m_id(id) , m_requestedSize(requestedSize) + , m_crop(crop) { setAutoDelete(false); } @@ -37,6 +38,7 @@ public: QString m_id, m_error; QSize m_requestedSize; QImage m_image; + bool m_crop; }; class MxcImageProvider @@ -51,7 +53,8 @@ public slots: static void addEncryptionInfo(mtx::crypto::EncryptedFile info); static void download(const QString &id, const QSize &requestedSize, - std::function then); + std::function then, + bool crop = true); private: QThreadPool pool; diff --git a/src/Olm.cpp b/src/Olm.cpp index 18e2ddcf..e4ab0aa1 100644 --- a/src/Olm.cpp +++ b/src/Olm.cpp @@ -212,14 +212,21 @@ handle_olm_message(const OlmMessage &msg, const UserKeyCache &otherUserDeviceKey nhlog::crypto()->info("sender : {}", msg.sender); nhlog::crypto()->info("sender_key: {}", msg.sender_key); + if (msg.sender_key == olm::client()->identity_keys().ed25519) { + nhlog::crypto()->warn("Ignoring olm message from ourselves!"); + return; + } + const auto my_key = olm::client()->identity_keys().curve25519; + bool failed_decryption = false; + for (const auto &cipher : msg.ciphertext) { // We skip messages not meant for the current device. if (cipher.first != my_key) { nhlog::crypto()->debug( "Skipping message for {} since we are {}.", cipher.first, my_key); - return; + continue; } const auto type = cipher.second.type; @@ -234,6 +241,7 @@ handle_olm_message(const OlmMessage &msg, const UserKeyCache &otherUserDeviceKey msg.sender, msg.sender_key, cipher.second); } else { nhlog::crypto()->error("Undecryptable olm message!"); + failed_decryption = true; continue; } } @@ -278,11 +286,17 @@ handle_olm_message(const OlmMessage &msg, const UserKeyCache &otherUserDeviceKey bool from_their_device = false; for (auto [device_id, key] : otherUserDeviceKeys.device_keys) { - if (key.keys.at("curve25519:" + device_id) == msg.sender_key) { - if (key.keys.at("ed25519:" + device_id) == sender_ed25519) { - from_their_device = true; - break; - } + auto c_key = key.keys.find("curve25519:" + device_id); + auto e_key = key.keys.find("ed25519:" + device_id); + + if (c_key == key.keys.end() || e_key == key.keys.end()) { + nhlog::crypto()->warn( + "Skipping device {} as we have no keys for it.", + device_id); + } else if (c_key->second == msg.sender_key && + e_key->second == sender_ed25519) { + from_their_device = true; + break; } } if (!from_their_device) { @@ -423,22 +437,28 @@ handle_olm_message(const OlmMessage &msg, const UserKeyCache &otherUserDeviceKey } return; + } else { + failed_decryption = true; } } - try { - std::map> targets; - for (auto [device_id, key] : otherUserDeviceKeys.device_keys) { - if (key.keys.at("curve25519:" + device_id) == msg.sender_key) - targets[msg.sender].push_back(device_id); - } + if (failed_decryption) { + try { + std::map> targets; + for (auto [device_id, key] : otherUserDeviceKeys.device_keys) { + if (key.keys.at("curve25519:" + device_id) == msg.sender_key) + targets[msg.sender].push_back(device_id); + } - send_encrypted_to_device_messages( - targets, mtx::events::DeviceEvent{}, true); - nhlog::crypto()->info( - "Recovering from broken olm channel with {}:{}", msg.sender, msg.sender_key); - } catch (std::exception &e) { - nhlog::crypto()->error("Failed to recover from broken olm sessions: {}", e.what()); + send_encrypted_to_device_messages( + targets, mtx::events::DeviceEvent{}, true); + nhlog::crypto()->info("Recovering from broken olm channel with {}:{}", + msg.sender, + msg.sender_key); + } catch (std::exception &e) { + nhlog::crypto()->error("Failed to recover from broken olm sessions: {}", + e.what()); + } } } @@ -504,7 +524,8 @@ encrypt_group_message(const std::string &room_id, const std::string &device_id, auto own_user_id = http::client()->user_id().to_string(); - auto members = cache::client()->getMembersWithKeys(room_id); + auto members = cache::client()->getMembersWithKeys( + room_id, UserSettings::instance()->onlyShareKeysWithVerifiedUsers()); std::map> sendSessionTo; mtx::crypto::OutboundGroupSessionPtr session = nullptr; @@ -955,13 +976,12 @@ handle_key_request_message(const mtx::events::DeviceEventdebug("ignoring key request for room {}", req.content.room_id); return; } - if (verifiedDevice || utils::respondsToKeyRequests(req.content.room_id)) { + if (verifiedDevice) { // share the minimum index we have minimumIndex = -1; } @@ -1008,7 +1028,8 @@ send_megolm_key_to_device(const std::string &user_id, DecryptionResult decryptEvent(const MegolmSessionIndex &index, - const mtx::events::EncryptedEvent &event) + const mtx::events::EncryptedEvent &event, + bool dont_write_db) { try { if (!cache::client()->inboundMegolmSessionExists(index)) { @@ -1023,10 +1044,26 @@ decryptEvent(const MegolmSessionIndex &index, std::string msg_str; try { auto session = cache::client()->getInboundMegolmSession(index); + auto sessionData = + cache::client()->getMegolmSessionData(index).value_or(GroupSessionData{}); auto res = olm::client()->decrypt_group_message(session.get(), event.content.ciphertext); msg_str = std::string((char *)res.data.data(), res.data.size()); + + if (!event.event_id.empty() && event.event_id[0] == '$') { + auto oldIdx = sessionData.indices.find(res.message_index); + if (oldIdx != sessionData.indices.end()) { + if (oldIdx->second != event.event_id) + return {DecryptionErrorCode::ReplayAttack, + std::nullopt, + std::nullopt}; + } else if (!dont_write_db) { + sessionData.indices[res.message_index] = event.event_id; + cache::client()->saveInboundMegolmSession( + index, std::move(session), sessionData); + } + } } catch (const lmdb::error &e) { return {DecryptionErrorCode::DbError, e.what(), std::nullopt}; } catch (const mtx::crypto::olm_exception &e) { @@ -1035,24 +1072,24 @@ decryptEvent(const MegolmSessionIndex &index, return {DecryptionErrorCode::DecryptionFailed, e.what(), std::nullopt}; } - // Add missing fields for the event. - json body = json::parse(msg_str); - body["event_id"] = event.event_id; - body["sender"] = event.sender; - body["origin_server_ts"] = event.origin_server_ts; - body["unsigned"] = event.unsigned_data; - - // relations are unencrypted in content... - mtx::common::add_relations(body["content"], event.content.relations); - - mtx::events::collections::TimelineEvent te; try { + // Add missing fields for the event. + json body = json::parse(msg_str); + body["event_id"] = event.event_id; + body["sender"] = event.sender; + body["origin_server_ts"] = event.origin_server_ts; + body["unsigned"] = event.unsigned_data; + + // relations are unencrypted in content... + mtx::common::add_relations(body["content"], event.content.relations); + + mtx::events::collections::TimelineEvent te; mtx::events::collections::from_json(body, te); + + return {DecryptionErrorCode::NoError, std::nullopt, std::move(te.data)}; } catch (std::exception &e) { return {DecryptionErrorCode::ParsingFailed, e.what(), std::nullopt}; } - - return {std::nullopt, std::nullopt, std::move(te.data)}; } crypto::Trust @@ -1081,6 +1118,8 @@ send_encrypted_to_device_messages(const std::map> pks; + auto our_curve = olm::client()->identity_keys().curve25519; + for (const auto &[user, devices] : targets) { auto deviceKeys = cache::client()->userKeys(user); @@ -1114,12 +1153,32 @@ send_encrypted_to_device_messages(const std::mapwarn("Skipping our own device, since sending " + "ourselves olm messages makes no sense."); + continue; + } + + auto session = cache::getLatestOlmSession(device_curve); if (!session || force_new_session) { - claims.one_time_keys[user][device] = mtx::crypto::SIGNED_CURVE25519; - pks[user][device].ed25519 = d.keys.at("ed25519:" + device); - pks[user][device].curve25519 = d.keys.at("curve25519:" + device); + static QMap, qint64> rateLimit; + auto currentTime = QDateTime::currentSecsSinceEpoch(); + if (rateLimit.value(QPair(user, device)) + 60 * 60 * 10 < + currentTime) { + claims.one_time_keys[user][device] = + mtx::crypto::SIGNED_CURVE25519; + pks[user][device].ed25519 = d.keys.at("ed25519:" + device); + pks[user][device].curve25519 = + d.keys.at("curve25519:" + device); + + rateLimit.insert(QPair(user, device), currentTime); + } else { + nhlog::crypto()->warn("Not creating new session with {}:{} " + "because of rate limit", + user, + device); + } continue; } @@ -1129,7 +1188,7 @@ send_encrypted_to_device_messages(const std::map(); try { @@ -1187,22 +1246,40 @@ send_encrypted_to_device_messages(const std::mapat("key"); - auto id_key = pks.at(user_id).at(device_id).curve25519; + auto sign_key = pks.at(user_id).at(device_id).ed25519; + auto id_key = pks.at(user_id).at(device_id).curve25519; + + // Verify signature + { + auto signedKey = *rd.second.begin(); + std::string signature = + signedKey["signatures"][user_id].value( + "ed25519:" + device_id, ""); + + if (signature.empty() || + !mtx::crypto::ed25519_verify_signature( + sign_key, signedKey, signature)) { + nhlog::net()->warn( + "Skipping device {} as its one time key " + "has an invalid signature.", + device_id); + continue; + } + } + auto session = olm::client()->create_outbound_session(id_key, otk); messages[mtx::identifiers::parse( user_id)][device_id] = olm::client() - ->create_olm_encrypted_content( - session.get(), - ev_json, - UserId(user_id), - pks.at(user_id).at(device_id).ed25519, - id_key) + ->create_olm_encrypted_content(session.get(), + ev_json, + UserId(user_id), + sign_key, + id_key) .get(); try { @@ -1248,8 +1325,8 @@ send_encrypted_to_device_messages(const std::mapquery_keys( req, - [ev_json, BindPks](const mtx::responses::QueryKeys &res, - mtx::http::RequestErr err) { + [ev_json, BindPks, our_curve](const mtx::responses::QueryKeys &res, + mtx::http::RequestErr err) { if (err) { nhlog::net()->warn("failed to query device keys: {} {}", err->matrix_error.error, @@ -1291,6 +1368,13 @@ send_encrypted_to_device_messages(const std::mapwarn( + "Skipping our own device, since sending " + "ourselves olm messages makes no sense."); + continue; + } + try { if (!mtx::crypto::verify_identity_signature( dev.second, device_id, user_id)) { @@ -1360,9 +1444,12 @@ request_cross_signing_keys() body, [request_id = secretRequest.request_id, secretName](mtx::http::RequestErr err) { if (err) { - request_id_to_secret_name.erase(request_id); nhlog::net()->error("Failed to send request for secrect '{}'", secretName); + // Cancel request on UI thread + QTimer::singleShot(1, cache::client(), [request_id]() { + request_id_to_secret_name.erase(request_id); + }); return; } }); diff --git a/src/Olm.h b/src/Olm.h index a18cbbfb..ab86ca00 100644 --- a/src/Olm.h +++ b/src/Olm.h @@ -14,9 +14,11 @@ constexpr auto OLM_ALGO = "m.olm.v1.curve25519-aes-sha2"; namespace olm { +Q_NAMESPACE -enum class DecryptionErrorCode +enum DecryptionErrorCode { + NoError, MissingSession, // Session was not found, retrieve from backup or request from other devices // and try again MissingSessionIndex, // Session was found, but it does not reach back enough to this index, @@ -25,14 +27,13 @@ enum class DecryptionErrorCode DecryptionFailed, // libolm error ParsingFailed, // Failed to parse the actual event ReplayAttack, // Megolm index reused - UnknownFingerprint, // Unknown device Fingerprint }; +Q_ENUM_NS(DecryptionErrorCode) struct DecryptionResult { - std::optional error; + DecryptionErrorCode error; std::optional error_message; - std::optional event; }; @@ -80,9 +81,11 @@ encrypt_group_message(const std::string &room_id, const std::string &device_id, nlohmann::json body); +//! Decrypt an event. Use dont_write_db to prevent db writes when already in a write transaction. DecryptionResult decryptEvent(const MegolmSessionIndex &index, - const mtx::events::EncryptedEvent &event); + const mtx::events::EncryptedEvent &event, + bool dont_write_db = false); crypto::Trust calculate_trust(const std::string &user_id, const std::string &curve25519); diff --git a/src/ReadReceiptsModel.cpp b/src/ReadReceiptsModel.cpp new file mode 100644 index 00000000..25262c59 --- /dev/null +++ b/src/ReadReceiptsModel.cpp @@ -0,0 +1,131 @@ +// SPDX-FileCopyrightText: 2021 Nheko Contributors +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "ReadReceiptsModel.h" + +#include + +#include "Cache.h" +#include "Cache_p.h" +#include "Logging.h" +#include "Utils.h" + +ReadReceiptsModel::ReadReceiptsModel(QString event_id, QString room_id, QObject *parent) + : QAbstractListModel{parent} + , event_id_{event_id} + , room_id_{room_id} +{ + try { + addUsers(cache::readReceipts(event_id_, room_id_)); + } catch (const lmdb::error &) { + nhlog::db()->warn("failed to retrieve read receipts for {} {}", + event_id_.toStdString(), + room_id_.toStdString()); + + return; + } + + connect(cache::client(), &Cache::newReadReceipts, this, &ReadReceiptsModel::update); +} + +void +ReadReceiptsModel::update() +{ + try { + addUsers(cache::readReceipts(event_id_, room_id_)); + } catch (const lmdb::error &) { + nhlog::db()->warn("failed to retrieve read receipts for {} {}", + event_id_.toStdString(), + room_id_.toStdString()); + + return; + } +} + +QHash +ReadReceiptsModel::roleNames() const +{ + // Note: RawTimestamp is purposely not included here + return { + {Mxid, "mxid"}, + {DisplayName, "displayName"}, + {AvatarUrl, "avatarUrl"}, + {Timestamp, "timestamp"}, + }; +} + +QVariant +ReadReceiptsModel::data(const QModelIndex &index, int role) const +{ + if (!index.isValid() || index.row() >= (int)readReceipts_.size() || index.row() < 0) + return {}; + + switch (role) { + case Mxid: + return readReceipts_[index.row()].first; + case DisplayName: + return cache::displayName(room_id_, readReceipts_[index.row()].first); + case AvatarUrl: + return cache::avatarUrl(room_id_, readReceipts_[index.row()].first); + case Timestamp: + return dateFormat(readReceipts_[index.row()].second); + case RawTimestamp: + return readReceipts_[index.row()].second; + default: + return {}; + } +} + +void +ReadReceiptsModel::addUsers( + const std::multimap> &users) +{ + auto newReceipts = users.size() - readReceipts_.size(); + + if (newReceipts > 0) { + beginInsertRows( + QModelIndex{}, readReceipts_.size(), readReceipts_.size() + newReceipts - 1); + + for (const auto &user : users) { + QPair item = { + QString::fromStdString(user.second), + QDateTime::fromMSecsSinceEpoch(user.first)}; + if (!readReceipts_.contains(item)) + readReceipts_.push_back(item); + } + + endInsertRows(); + } +} + +QString +ReadReceiptsModel::dateFormat(const QDateTime &then) const +{ + auto now = QDateTime::currentDateTime(); + auto days = then.daysTo(now); + + if (days == 0) + return QLocale::system().toString(then.time(), QLocale::ShortFormat); + else if (days < 2) + return tr("Yesterday, %1") + .arg(QLocale::system().toString(then.time(), QLocale::ShortFormat)); + else if (days < 7) + //: %1 is the name of the current day, %2 is the time the read receipt was read. The + //: result may look like this: Monday, 7:15 + return QString("%1, %2") + .arg(then.toString("dddd")) + .arg(QLocale::system().toString(then.time(), QLocale::ShortFormat)); + + return QLocale::system().toString(then.time(), QLocale::ShortFormat); +} + +ReadReceiptsProxy::ReadReceiptsProxy(QString event_id, QString room_id, QObject *parent) + : QSortFilterProxyModel{parent} + , model_{event_id, room_id, this} +{ + setSourceModel(&model_); + setSortRole(ReadReceiptsModel::RawTimestamp); + sort(0, Qt::DescendingOrder); + setDynamicSortFilter(true); +} diff --git a/src/ReadReceiptsModel.h b/src/ReadReceiptsModel.h new file mode 100644 index 00000000..3b45716c --- /dev/null +++ b/src/ReadReceiptsModel.h @@ -0,0 +1,73 @@ +// SPDX-FileCopyrightText: 2021 Nheko Contributors +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#ifndef READRECEIPTSMODEL_H +#define READRECEIPTSMODEL_H + +#include +#include +#include +#include +#include + +class ReadReceiptsModel : public QAbstractListModel +{ + Q_OBJECT + +public: + enum Roles + { + Mxid, + DisplayName, + AvatarUrl, + Timestamp, + RawTimestamp, + }; + + explicit ReadReceiptsModel(QString event_id, QString room_id, QObject *parent = nullptr); + + QString eventId() const { return event_id_; } + QString roomId() const { return room_id_; } + + QHash roleNames() const override; + int rowCount(const QModelIndex &parent) const override + { + Q_UNUSED(parent) + return readReceipts_.size(); + } + QVariant data(const QModelIndex &index, int role) const override; + +public slots: + void addUsers(const std::multimap> &users); + void update(); + +private: + QString dateFormat(const QDateTime &then) const; + + QString event_id_; + QString room_id_; + QVector> readReceipts_; +}; + +class ReadReceiptsProxy : public QSortFilterProxyModel +{ + Q_OBJECT + + Q_PROPERTY(QString eventId READ eventId CONSTANT) + Q_PROPERTY(QString roomId READ roomId CONSTANT) + +public: + explicit ReadReceiptsProxy(QString event_id, QString room_id, QObject *parent = nullptr); + + QString eventId() const { return event_id_; } + QString roomId() const { return room_id_; } + +private: + QString event_id_; + QString room_id_; + + ReadReceiptsModel model_; +}; + +#endif // READRECEIPTSMODEL_H diff --git a/src/RegisterPage.cpp b/src/RegisterPage.cpp index 1588d07d..bae24df0 100644 --- a/src/RegisterPage.cpp +++ b/src/RegisterPage.cpp @@ -12,6 +12,7 @@ #include #include +#include #include "Config.h" #include "Logging.h" @@ -93,6 +94,7 @@ RegisterPage::RegisterPage(QWidget *parent) server_input_ = new TextField(); server_input_->setLabel(tr("Homeserver")); + server_input_->setRegexp(QRegularExpression(".+")); server_input_->setToolTip( tr("A server that allows registration. Since matrix is decentralized, you need to first " "find a server you can register on or host your own.")); @@ -145,178 +147,39 @@ RegisterPage::RegisterPage(QWidget *parent) top_layout_->addLayout(button_layout_); top_layout_->addWidget(error_label_, 0, Qt::AlignHCenter); top_layout_->addStretch(1); - - connect( - this, - &RegisterPage::versionErrorCb, - this, - [this](const QString &msg) { - error_server_label_->show(); - server_input_->setValid(false); - showError(error_server_label_, msg); - }, - Qt::QueuedConnection); + setLayout(top_layout_); connect(back_button_, SIGNAL(clicked()), this, SLOT(onBackButtonClicked())); connect(register_button_, SIGNAL(clicked()), this, SLOT(onRegisterButtonClicked())); connect(username_input_, SIGNAL(returnPressed()), register_button_, SLOT(click())); - connect(username_input_, &TextField::editingFinished, this, &RegisterPage::checkFields); + connect(username_input_, &TextField::editingFinished, this, &RegisterPage::checkUsername); connect(password_input_, SIGNAL(returnPressed()), register_button_, SLOT(click())); - connect(password_input_, &TextField::editingFinished, this, &RegisterPage::checkFields); + connect(password_input_, &TextField::editingFinished, this, &RegisterPage::checkPassword); connect(password_confirmation_, SIGNAL(returnPressed()), register_button_, SLOT(click())); - connect( - password_confirmation_, &TextField::editingFinished, this, &RegisterPage::checkFields); + connect(password_confirmation_, + &TextField::editingFinished, + this, + &RegisterPage::checkPasswordConfirmation); connect(server_input_, SIGNAL(returnPressed()), register_button_, SLOT(click())); - connect(server_input_, &TextField::editingFinished, this, &RegisterPage::checkFields); - connect(this, &RegisterPage::registerErrorCb, this, [this](const QString &msg) { - showError(msg); - }); - connect( - this, - &RegisterPage::registrationFlow, - this, - [this](const std::string &user, - const std::string &pass, - const mtx::user_interactive::Unauthorized &unauthorized) { - auto completed_stages = unauthorized.completed; - auto flows = unauthorized.flows; - auto session = unauthorized.session.empty() ? http::client()->generate_txn_id() - : unauthorized.session; - - nhlog::ui()->info("Completed stages: {}", completed_stages.size()); - - if (!completed_stages.empty()) - flows.erase(std::remove_if( - flows.begin(), - flows.end(), - [completed_stages](auto flow) { - if (completed_stages.size() > flow.stages.size()) - return true; - for (size_t f = 0; f < completed_stages.size(); f++) - if (completed_stages[f] != flow.stages[f]) - return true; - return false; - }), - flows.end()); - - if (flows.empty()) { - nhlog::net()->error("No available registration flows!"); - emit registerErrorCb(tr("No supported registration flows!")); - return; - } - - auto current_stage = flows.front().stages.at(completed_stages.size()); - - if (current_stage == mtx::user_interactive::auth_types::recaptcha) { - auto captchaDialog = - new dialogs::ReCaptcha(QString::fromStdString(session), this); - - connect(captchaDialog, - &dialogs::ReCaptcha::confirmation, - this, - [this, user, pass, session, captchaDialog]() { - captchaDialog->close(); - captchaDialog->deleteLater(); - - emit registerAuth( - user, - pass, - mtx::user_interactive::Auth{ - session, mtx::user_interactive::auth::Fallback{}}); - }); - connect(captchaDialog, - &dialogs::ReCaptcha::cancel, - this, - &RegisterPage::errorOccurred); - - QTimer::singleShot( - 1000, this, [captchaDialog]() { captchaDialog->show(); }); - } else if (current_stage == mtx::user_interactive::auth_types::dummy) { - emit registerAuth(user, - pass, - mtx::user_interactive::Auth{ - session, mtx::user_interactive::auth::Dummy{}}); - } else { - // use fallback - auto dialog = - new dialogs::FallbackAuth(QString::fromStdString(current_stage), - QString::fromStdString(session), - this); - - connect(dialog, - &dialogs::FallbackAuth::confirmation, - this, - [this, user, pass, session, dialog]() { - dialog->close(); - dialog->deleteLater(); - - emit registerAuth( - user, - pass, - mtx::user_interactive::Auth{ - session, mtx::user_interactive::auth::Fallback{}}); - }); - connect(dialog, - &dialogs::FallbackAuth::cancel, - this, - &RegisterPage::errorOccurred); - - dialog->show(); - } - }); + connect(server_input_, &TextField::editingFinished, this, &RegisterPage::checkServer); connect( this, - &RegisterPage::registerAuth, + &RegisterPage::serverError, this, - [this](const std::string &user, - const std::string &pass, - const mtx::user_interactive::Auth &auth) { - http::client()->registration( - user, - pass, - auth, - [this, user, pass](const mtx::responses::Register &res, - mtx::http::RequestErr err) { - if (!err) { - http::client()->set_user(res.user_id); - http::client()->set_access_token(res.access_token); - http::client()->set_device_id(res.device_id); + [this](const QString &msg) { + server_input_->setValid(false); + showError(error_server_label_, msg); + }, + Qt::QueuedConnection); - emit registerOk(); - return; - } - - // The server requires registration flows. - if (err->status_code == 401) { - if (err->matrix_error.unauthorized.flows.empty()) { - nhlog::net()->warn( - "failed to retrieve registration flows: ({}) " - "{}", - static_cast(err->status_code), - err->matrix_error.error); - emit registerErrorCb( - QString::fromStdString(err->matrix_error.error)); - return; - } - - emit registrationFlow( - user, pass, err->matrix_error.unauthorized); - return; - } - - nhlog::net()->warn("failed to register: status_code ({}), " - "matrix_error: ({}), parser error ({})", - static_cast(err->status_code), - err->matrix_error.error, - err->parse_error); - - emit registerErrorCb(QString::fromStdString(err->matrix_error.error)); - }); - }); - - setLayout(top_layout_); + connect(this, &RegisterPage::wellKnownLookup, this, &RegisterPage::doWellKnownLookup); + connect(this, &RegisterPage::versionsCheck, this, &RegisterPage::doVersionsCheck); + connect(this, &RegisterPage::registration, this, &RegisterPage::doRegistration); + connect(this, &RegisterPage::UIA, this, &RegisterPage::doUIA); + connect( + this, &RegisterPage::registrationWithAuth, this, &RegisterPage::doRegistrationWithAuth); } void @@ -345,191 +208,298 @@ RegisterPage::showError(QLabel *label, const QString &msg) int height = rect.height(); label->setFixedHeight((int)qCeil(width / 200.0) * height); label->setText(msg); + label->show(); } bool RegisterPage::checkOneField(QLabel *label, const TextField *t_field, const QString &msg) { if (t_field->isValid()) { - label->setText(""); label->hide(); return true; } else { - label->show(); showError(label, msg); return false; } } bool -RegisterPage::checkFields() +RegisterPage::checkUsername() { - error_label_->setText(""); - error_username_label_->setText(""); - error_password_label_->setText(""); - error_password_confirmation_label_->setText(""); - error_server_label_->setText(""); + return checkOneField(error_username_label_, + username_input_, + tr("The username must not be empty, and must contain only the " + "characters a-z, 0-9, ., _, =, -, and /.")); +} - error_username_label_->hide(); - error_password_label_->hide(); - error_password_confirmation_label_->hide(); - error_server_label_->hide(); +bool +RegisterPage::checkPassword() +{ + return checkOneField( + error_password_label_, password_input_, tr("Password is not long enough (min 8 chars)")); +} - password_confirmation_->setValid(true); - server_input_->setValid(true); - - bool all_fields_good = true; - if (username_input_->isModified() && - !checkOneField(error_username_label_, - username_input_, - tr("The username must not be empty, and must contain only the " - "characters a-z, 0-9, ., _, =, -, and /."))) { - all_fields_good = false; - } else if (password_input_->isModified() && - !checkOneField(error_password_label_, - password_input_, - tr("Password is not long enough (min 8 chars)"))) { - all_fields_good = false; - } else if (password_confirmation_->isModified() && - password_input_->text() != password_confirmation_->text()) { - error_password_confirmation_label_->show(); +bool +RegisterPage::checkPasswordConfirmation() +{ + if (password_input_->text() == password_confirmation_->text()) { + error_password_confirmation_label_->hide(); + password_confirmation_->setValid(true); + return true; + } else { showError(error_password_confirmation_label_, tr("Passwords don't match")); password_confirmation_->setValid(false); - all_fields_good = false; - } else if (server_input_->isModified() && - (!server_input_->hasAcceptableInput() || server_input_->text().isEmpty())) { - error_server_label_->show(); - showError(error_server_label_, tr("Invalid server name")); - server_input_->setValid(false); - all_fields_good = false; + return false; } - if (!username_input_->isModified() || !password_input_->isModified() || - !password_confirmation_->isModified() || !server_input_->isModified()) { - all_fields_good = false; - } - return all_fields_good; +} + +bool +RegisterPage::checkServer() +{ + // This doesn't check that the server is reachable, + // just that the input is not obviously wrong. + return checkOneField(error_server_label_, server_input_, tr("Invalid server name")); } void RegisterPage::onRegisterButtonClicked() { - if (!checkFields()) { - showError(error_label_, - tr("One or more fields have invalid inputs. Please correct those issues " - "and try again.")); - return; - } else { - auto username = username_input_->text().toStdString(); - auto password = password_input_->text().toStdString(); - auto server = server_input_->text().toStdString(); + if (checkUsername() && checkPassword() && checkPasswordConfirmation() && checkServer()) { + auto server = server_input_->text().toStdString(); http::client()->set_server(server); http::client()->verify_certificates( !UserSettings::instance()->disableCertificateValidation()); - http::client()->well_known( - [this, username, password](const mtx::responses::WellKnown &res, - mtx::http::RequestErr err) { - if (err) { - if (err->status_code == 404) { - nhlog::net()->info("Autodiscovery: No .well-known."); - checkVersionAndRegister(username, password); - return; - } + // This starts a chain of `emit`s which ends up doing the + // registration. Signals are used rather than normal function + // calls so that the dialogs used in UIA work correctly. + // + // The sequence of events looks something like this: + // + // dowellKnownLookup + // v + // doVersionsCheck + // v + // doRegistration + // v + // doUIA <-----------------+ + // v | More auth required + // doRegistrationWithAuth -+ + // | Success + // v + // registering - if (!err->parse_error.empty()) { - emit versionErrorCb(tr( - "Autodiscovery failed. Received malformed response.")); - nhlog::net()->error( - "Autodiscovery failed. Received malformed response."); - return; - } - - emit versionErrorCb(tr("Autodiscovery failed. Unknown error when " - "requesting .well-known.")); - nhlog::net()->error("Autodiscovery failed. Unknown error when " - "requesting .well-known. {} {}", - err->status_code, - err->error_code); - return; - } - - nhlog::net()->info("Autodiscovery: Discovered '" + - res.homeserver.base_url + "'"); - http::client()->set_server(res.homeserver.base_url); - checkVersionAndRegister(username, password); - }); + emit wellKnownLookup(); emit registering(); } } void -RegisterPage::checkVersionAndRegister(const std::string &username, const std::string &password) +RegisterPage::doWellKnownLookup() { - http::client()->versions( - [this, username, password](const mtx::responses::Versions &, mtx::http::RequestErr err) { + http::client()->well_known( + [this](const mtx::responses::WellKnown &res, mtx::http::RequestErr err) { if (err) { if (err->status_code == 404) { - emit versionErrorCb(tr("The required endpoints were not found. " - "Possibly not a Matrix server.")); + nhlog::net()->info("Autodiscovery: No .well-known."); + // Check that the homeserver can be reached + emit versionsCheck(); return; } if (!err->parse_error.empty()) { - emit versionErrorCb(tr("Received malformed response. Make sure " - "the homeserver domain is valid.")); + emit serverError( + tr("Autodiscovery failed. Received malformed response.")); + nhlog::net()->error( + "Autodiscovery failed. Received malformed response."); return; } - emit versionErrorCb(tr( - "An unknown error occured. Make sure the homeserver domain is valid.")); + emit serverError(tr("Autodiscovery failed. Unknown error when " + "requesting .well-known.")); + nhlog::net()->error("Autodiscovery failed. Unknown error when " + "requesting .well-known. {} {}", + err->status_code, + err->error_code); return; } - http::client()->registration( - username, - password, - [this, username, password](const mtx::responses::Register &res, - mtx::http::RequestErr err) { - if (!err) { - http::client()->set_user(res.user_id); - http::client()->set_access_token(res.access_token); - - emit registerOk(); - return; - } - - // The server requires registration flows. - if (err->status_code == 401) { - if (err->matrix_error.unauthorized.flows.empty()) { - nhlog::net()->warn( - "failed to retrieve registration flows1: ({}) " - "{}", - static_cast(err->status_code), - err->matrix_error.error); - emit errorOccurred(); - emit registerErrorCb( - QString::fromStdString(err->matrix_error.error)); - return; - } - - emit registrationFlow( - username, password, err->matrix_error.unauthorized); - return; - } - - nhlog::net()->error( - "failed to register: status_code ({}), matrix_error({})", - static_cast(err->status_code), - err->matrix_error.error); - - emit registerErrorCb(QString::fromStdString(err->matrix_error.error)); - emit errorOccurred(); - }); + nhlog::net()->info("Autodiscovery: Discovered '" + res.homeserver.base_url + "'"); + http::client()->set_server(res.homeserver.base_url); + // Check that the homeserver can be reached + emit versionsCheck(); }); } +void +RegisterPage::doVersionsCheck() +{ + // Make a request to /_matrix/client/versions to check the address + // given is a Matrix homeserver. + http::client()->versions( + [this](const mtx::responses::Versions &, mtx::http::RequestErr err) { + if (err) { + if (err->status_code == 404) { + emit serverError( + tr("The required endpoints were not found. Possibly " + "not a Matrix server.")); + return; + } + + if (!err->parse_error.empty()) { + emit serverError( + tr("Received malformed response. Make sure the homeserver " + "domain is valid.")); + return; + } + + emit serverError(tr("An unknown error occured. Make sure the " + "homeserver domain is valid.")); + return; + } + + // Attempt registration without an `auth` dict + emit registration(); + }); +} + +void +RegisterPage::doRegistration() +{ + // These inputs should still be alright, but check just in case + if (checkUsername() && checkPassword() && checkPasswordConfirmation()) { + auto username = username_input_->text().toStdString(); + auto password = password_input_->text().toStdString(); + http::client()->registration(username, password, registrationCb()); + } +} + +void +RegisterPage::doRegistrationWithAuth(const mtx::user_interactive::Auth &auth) +{ + // These inputs should still be alright, but check just in case + if (checkUsername() && checkPassword() && checkPasswordConfirmation()) { + auto username = username_input_->text().toStdString(); + auto password = password_input_->text().toStdString(); + http::client()->registration(username, password, auth, registrationCb()); + } +} + +mtx::http::Callback +RegisterPage::registrationCb() +{ + // Return a function to be used as the callback when an attempt at + // registration is made. + return [this](const mtx::responses::Register &res, mtx::http::RequestErr err) { + if (!err) { + http::client()->set_user(res.user_id); + http::client()->set_access_token(res.access_token); + emit registerOk(); + return; + } + + // The server requires registration flows. + if (err->status_code == 401) { + if (err->matrix_error.unauthorized.flows.empty()) { + nhlog::net()->warn("failed to retrieve registration flows: " + "status_code({}), matrix_error({}) ", + static_cast(err->status_code), + err->matrix_error.error); + showError(QString::fromStdString(err->matrix_error.error)); + return; + } + + // Attempt to complete a UIA stage + emit UIA(err->matrix_error.unauthorized); + return; + } + + nhlog::net()->error("failed to register: status_code ({}), matrix_error({})", + static_cast(err->status_code), + err->matrix_error.error); + + showError(QString::fromStdString(err->matrix_error.error)); + }; +} + +void +RegisterPage::doUIA(const mtx::user_interactive::Unauthorized &unauthorized) +{ + auto completed_stages = unauthorized.completed; + auto flows = unauthorized.flows; + auto session = + unauthorized.session.empty() ? http::client()->generate_txn_id() : unauthorized.session; + + nhlog::ui()->info("Completed stages: {}", completed_stages.size()); + + if (!completed_stages.empty()) { + // Get rid of all flows which don't start with the sequence of + // stages that have already been completed. + flows.erase( + std::remove_if(flows.begin(), + flows.end(), + [completed_stages](auto flow) { + if (completed_stages.size() > flow.stages.size()) + return true; + for (size_t f = 0; f < completed_stages.size(); f++) + if (completed_stages[f] != flow.stages[f]) + return true; + return false; + }), + flows.end()); + } + + if (flows.empty()) { + nhlog::ui()->error("No available registration flows!"); + showError(tr("No supported registration flows!")); + return; + } + + auto current_stage = flows.front().stages.at(completed_stages.size()); + + if (current_stage == mtx::user_interactive::auth_types::recaptcha) { + auto captchaDialog = new dialogs::ReCaptcha(QString::fromStdString(session), this); + + connect(captchaDialog, + &dialogs::ReCaptcha::confirmation, + this, + [this, session, captchaDialog]() { + captchaDialog->close(); + captchaDialog->deleteLater(); + doRegistrationWithAuth(mtx::user_interactive::Auth{ + session, mtx::user_interactive::auth::Fallback{}}); + }); + + connect( + captchaDialog, &dialogs::ReCaptcha::cancel, this, &RegisterPage::errorOccurred); + + QTimer::singleShot(1000, this, [captchaDialog]() { captchaDialog->show(); }); + + } else if (current_stage == mtx::user_interactive::auth_types::dummy) { + doRegistrationWithAuth( + mtx::user_interactive::Auth{session, mtx::user_interactive::auth::Dummy{}}); + + } else { + // use fallback + auto dialog = new dialogs::FallbackAuth( + QString::fromStdString(current_stage), QString::fromStdString(session), this); + + connect( + dialog, &dialogs::FallbackAuth::confirmation, this, [this, session, dialog]() { + dialog->close(); + dialog->deleteLater(); + emit registrationWithAuth(mtx::user_interactive::Auth{ + session, mtx::user_interactive::auth::Fallback{}}); + }); + + connect(dialog, &dialogs::FallbackAuth::cancel, this, &RegisterPage::errorOccurred); + + dialog->show(); + } +} + void RegisterPage::paintEvent(QPaintEvent *) { diff --git a/src/RegisterPage.h b/src/RegisterPage.h index 0e4a45d0..42ea00cb 100644 --- a/src/RegisterPage.h +++ b/src/RegisterPage.h @@ -10,6 +10,7 @@ #include #include +#include class FlatButton; class RaisedButton; @@ -33,17 +34,16 @@ signals: void errorOccurred(); //! Used to trigger the corresponding slot outside of the main thread. - void versionErrorCb(const QString &err); + void serverError(const QString &err); + + void wellKnownLookup(); + void versionsCheck(); + void registration(); + void UIA(const mtx::user_interactive::Unauthorized &unauthorized); + void registrationWithAuth(const mtx::user_interactive::Auth &auth); void registering(); void registerOk(); - void registerErrorCb(const QString &msg); - void registrationFlow(const std::string &user, - const std::string &pass, - const mtx::user_interactive::Unauthorized &unauthorized); - void registerAuth(const std::string &user, - const std::string &pass, - const mtx::user_interactive::Auth &auth); private slots: void onBackButtonClicked(); @@ -51,12 +51,22 @@ private slots: // function for showing different errors void showError(const QString &msg); + void showError(QLabel *label, const QString &msg); + + bool checkOneField(QLabel *label, const TextField *t_field, const QString &msg); + bool checkUsername(); + bool checkPassword(); + bool checkPasswordConfirmation(); + bool checkServer(); + + void doWellKnownLookup(); + void doVersionsCheck(); + void doRegistration(); + void doUIA(const mtx::user_interactive::Unauthorized &unauthorized); + void doRegistrationWithAuth(const mtx::user_interactive::Auth &auth); + mtx::http::Callback registrationCb(); private: - bool checkOneField(QLabel *label, const TextField *t_field, const QString &msg); - bool checkFields(); - void showError(QLabel *label, const QString &msg); - void checkVersionAndRegister(const std::string &username, const std::string &password); QVBoxLayout *top_layout_; QHBoxLayout *back_layout_; @@ -69,6 +79,7 @@ private: QLabel *error_password_label_; QLabel *error_password_confirmation_label_; QLabel *error_server_label_; + QLabel *error_registration_token_label_; FlatButton *back_button_; RaisedButton *register_button_; @@ -81,4 +92,5 @@ private: TextField *password_input_; TextField *password_confirmation_; TextField *server_input_; + TextField *registration_token_input_; }; diff --git a/src/SingleImagePackModel.cpp b/src/SingleImagePackModel.cpp new file mode 100644 index 00000000..7bf55617 --- /dev/null +++ b/src/SingleImagePackModel.cpp @@ -0,0 +1,350 @@ +// SPDX-FileCopyrightText: 2021 Nheko Contributors +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "SingleImagePackModel.h" + +#include +#include + +#include "Cache_p.h" +#include "ChatPage.h" +#include "Logging.h" +#include "MatrixClient.h" +#include "Utils.h" +#include "timeline/Permissions.h" +#include "timeline/TimelineModel.h" + +Q_DECLARE_METATYPE(mtx::common::ImageInfo) + +SingleImagePackModel::SingleImagePackModel(ImagePackInfo pack_, QObject *parent) + : QAbstractListModel(parent) + , roomid_(std::move(pack_.source_room)) + , statekey_(std::move(pack_.state_key)) + , old_statekey_(statekey_) + , pack(std::move(pack_.pack)) +{ + [[maybe_unused]] static auto imageInfoType = qRegisterMetaType(); + + if (!pack.pack) + pack.pack = mtx::events::msc2545::ImagePack::PackDescription{}; + + for (const auto &e : pack.images) + shortcodes.push_back(e.first); + + connect(this, &SingleImagePackModel::addImage, this, &SingleImagePackModel::addImageCb); +} + +int +SingleImagePackModel::rowCount(const QModelIndex &) const +{ + return (int)shortcodes.size(); +} + +QHash +SingleImagePackModel::roleNames() const +{ + return { + {Roles::Url, "url"}, + {Roles::ShortCode, "shortCode"}, + {Roles::Body, "body"}, + {Roles::IsEmote, "isEmote"}, + {Roles::IsSticker, "isSticker"}, + }; +} + +QVariant +SingleImagePackModel::data(const QModelIndex &index, int role) const +{ + if (hasIndex(index.row(), index.column(), index.parent())) { + const auto &img = pack.images.at(shortcodes.at(index.row())); + switch (role) { + case Url: + return QString::fromStdString(img.url); + case ShortCode: + return QString::fromStdString(shortcodes.at(index.row())); + case Body: + return QString::fromStdString(img.body); + case IsEmote: + return img.overrides_usage() ? img.is_emoji() : pack.pack->is_emoji(); + case IsSticker: + return img.overrides_usage() ? img.is_sticker() : pack.pack->is_sticker(); + default: + return {}; + } + } + return {}; +} + +bool +SingleImagePackModel::setData(const QModelIndex &index, const QVariant &value, int role) +{ + using mtx::events::msc2545::PackUsage; + + if (hasIndex(index.row(), index.column(), index.parent())) { + auto &img = pack.images.at(shortcodes.at(index.row())); + switch (role) { + case ShortCode: { + auto newCode = value.toString().toStdString(); + + // otherwise we delete this by accident + if (pack.images.count(newCode)) + return false; + + auto tmp = img; + auto oldCode = shortcodes.at(index.row()); + pack.images.erase(oldCode); + shortcodes[index.row()] = newCode; + pack.images.insert({newCode, tmp}); + + emit dataChanged( + this->index(index.row()), this->index(index.row()), {Roles::ShortCode}); + return true; + } + case Body: + img.body = value.toString().toStdString(); + emit dataChanged( + this->index(index.row()), this->index(index.row()), {Roles::Body}); + return true; + case IsEmote: { + bool isEmote = value.toBool(); + bool isSticker = + img.overrides_usage() ? img.is_sticker() : pack.pack->is_sticker(); + + img.usage.set(PackUsage::Emoji, isEmote); + img.usage.set(PackUsage::Sticker, isSticker); + + if (img.usage == pack.pack->usage) + img.usage.reset(); + + emit dataChanged( + this->index(index.row()), this->index(index.row()), {Roles::IsEmote}); + + return true; + } + case IsSticker: { + bool isEmote = + img.overrides_usage() ? img.is_emoji() : pack.pack->is_emoji(); + bool isSticker = value.toBool(); + + img.usage.set(PackUsage::Emoji, isEmote); + img.usage.set(PackUsage::Sticker, isSticker); + + if (img.usage == pack.pack->usage) + img.usage.reset(); + + emit dataChanged( + this->index(index.row()), this->index(index.row()), {Roles::IsSticker}); + + return true; + } + } + } + return false; +} + +bool +SingleImagePackModel::isGloballyEnabled() const +{ + if (auto roomPacks = + cache::client()->getAccountData(mtx::events::EventType::ImagePackRooms)) { + if (auto tmp = std::get_if< + mtx::events::EphemeralEvent>( + &*roomPacks)) { + if (tmp->content.rooms.count(roomid_) && + tmp->content.rooms.at(roomid_).count(statekey_)) + return true; + } + } + return false; +} +void +SingleImagePackModel::setGloballyEnabled(bool enabled) +{ + mtx::events::msc2545::ImagePackRooms content{}; + if (auto roomPacks = + cache::client()->getAccountData(mtx::events::EventType::ImagePackRooms)) { + if (auto tmp = std::get_if< + mtx::events::EphemeralEvent>( + &*roomPacks)) { + content = tmp->content; + } + } + + if (enabled) + content.rooms[roomid_][statekey_] = {}; + else + content.rooms[roomid_].erase(statekey_); + + http::client()->put_account_data(content, [](mtx::http::RequestErr) { + // emit this->globallyEnabledChanged(); + }); +} + +bool +SingleImagePackModel::canEdit() const +{ + if (roomid_.empty()) + return true; + else + return Permissions(QString::fromStdString(roomid_)) + .canChange(qml_mtx_events::ImagePackInRoom); +} + +void +SingleImagePackModel::setPackname(QString val) +{ + auto val_ = val.toStdString(); + if (val_ != this->pack.pack->display_name) { + this->pack.pack->display_name = val_; + emit packnameChanged(); + } +} + +void +SingleImagePackModel::setAttribution(QString val) +{ + auto val_ = val.toStdString(); + if (val_ != this->pack.pack->attribution) { + this->pack.pack->attribution = val_; + emit attributionChanged(); + } +} + +void +SingleImagePackModel::setAvatarUrl(QString val) +{ + auto val_ = val.toStdString(); + if (val_ != this->pack.pack->avatar_url) { + this->pack.pack->avatar_url = val_; + emit avatarUrlChanged(); + } +} + +void +SingleImagePackModel::setStatekey(QString val) +{ + auto val_ = val.toStdString(); + if (val_ != statekey_) { + statekey_ = val_; + emit statekeyChanged(); + } +} + +void +SingleImagePackModel::setIsStickerPack(bool val) +{ + using mtx::events::msc2545::PackUsage; + if (val != pack.pack->is_sticker()) { + pack.pack->usage.set(PackUsage::Sticker, val); + emit isStickerPackChanged(); + } +} + +void +SingleImagePackModel::setIsEmotePack(bool val) +{ + using mtx::events::msc2545::PackUsage; + if (val != pack.pack->is_emoji()) { + pack.pack->usage.set(PackUsage::Emoji, val); + emit isEmotePackChanged(); + } +} + +void +SingleImagePackModel::save() +{ + if (roomid_.empty()) { + http::client()->put_account_data(pack, [](mtx::http::RequestErr e) { + if (e) + ChatPage::instance()->showNotification( + tr("Failed to update image pack: {}") + .arg(QString::fromStdString(e->matrix_error.error))); + }); + } else { + if (old_statekey_ != statekey_) { + http::client()->send_state_event( + roomid_, + to_string(mtx::events::EventType::ImagePackInRoom), + old_statekey_, + nlohmann::json::object(), + [](const mtx::responses::EventId &, mtx::http::RequestErr e) { + if (e) + ChatPage::instance()->showNotification( + tr("Failed to delete old image pack: {}") + .arg(QString::fromStdString(e->matrix_error.error))); + }); + } + + http::client()->send_state_event( + roomid_, + statekey_, + pack, + [this](const mtx::responses::EventId &, mtx::http::RequestErr e) { + if (e) + ChatPage::instance()->showNotification( + tr("Failed to update image pack: {}") + .arg(QString::fromStdString(e->matrix_error.error))); + + nhlog::net()->info("Uploaded image pack: {}", statekey_); + }); + } +} + +void +SingleImagePackModel::addStickers(QList files) +{ + for (const auto &f : files) { + auto file = QFile(f.toLocalFile()); + if (!file.open(QFile::ReadOnly)) { + ChatPage::instance()->showNotification( + tr("Failed to open image: {}").arg(f.toLocalFile())); + return; + } + + auto bytes = file.readAll(); + auto img = utils::readImage(bytes); + + mtx::common::ImageInfo info{}; + + auto sz = img.size() / 2; + if (sz.width() > 512 || sz.height() > 512) { + sz.scale(512, 512, Qt::AspectRatioMode::KeepAspectRatio); + } + + info.h = sz.height(); + info.w = sz.width(); + info.size = bytes.size(); + + auto filename = f.fileName().toStdString(); + http::client()->upload( + bytes.toStdString(), + QMimeDatabase().mimeTypeForFile(f.toLocalFile()).name().toStdString(), + filename, + [this, filename, info](const mtx::responses::ContentURI &uri, + mtx::http::RequestErr e) { + if (e) { + ChatPage::instance()->showNotification( + tr("Failed to upload image: {}") + .arg(QString::fromStdString(e->matrix_error.error))); + return; + } + + emit addImage(uri.content_uri, filename, info); + }); + } +} +void +SingleImagePackModel::addImageCb(std::string uri, std::string filename, mtx::common::ImageInfo info) +{ + mtx::events::msc2545::PackImage img{}; + img.url = uri; + img.info = info; + beginInsertRows( + QModelIndex(), static_cast(shortcodes.size()), static_cast(shortcodes.size())); + + pack.images[filename] = img; + shortcodes.push_back(filename); + + endInsertRows(); +} diff --git a/src/SingleImagePackModel.h b/src/SingleImagePackModel.h new file mode 100644 index 00000000..cd38b3b6 --- /dev/null +++ b/src/SingleImagePackModel.h @@ -0,0 +1,93 @@ +// SPDX-FileCopyrightText: 2021 Nheko Contributors +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include +#include +#include + +#include + +#include "CacheStructs.h" + +class SingleImagePackModel : public QAbstractListModel +{ + Q_OBJECT + + Q_PROPERTY(QString roomid READ roomid CONSTANT) + Q_PROPERTY(QString statekey READ statekey WRITE setStatekey NOTIFY statekeyChanged) + Q_PROPERTY( + QString attribution READ attribution WRITE setAttribution NOTIFY attributionChanged) + Q_PROPERTY(QString packname READ packname WRITE setPackname NOTIFY packnameChanged) + Q_PROPERTY(QString avatarUrl READ avatarUrl WRITE setAvatarUrl NOTIFY avatarUrlChanged) + Q_PROPERTY( + bool isStickerPack READ isStickerPack WRITE setIsStickerPack NOTIFY isStickerPackChanged) + Q_PROPERTY(bool isEmotePack READ isEmotePack WRITE setIsEmotePack NOTIFY isEmotePackChanged) + Q_PROPERTY(bool isGloballyEnabled READ isGloballyEnabled WRITE setGloballyEnabled NOTIFY + globallyEnabledChanged) + Q_PROPERTY(bool canEdit READ canEdit CONSTANT) + +public: + enum Roles + { + Url = Qt::UserRole, + ShortCode, + Body, + IsEmote, + IsSticker, + }; + Q_ENUM(Roles); + + SingleImagePackModel(ImagePackInfo pack_, QObject *parent = nullptr); + QHash roleNames() const override; + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + QVariant data(const QModelIndex &index, int role) const override; + bool setData(const QModelIndex &index, + const QVariant &value, + int role = Qt::EditRole) override; + + QString roomid() const { return QString::fromStdString(roomid_); } + QString statekey() const { return QString::fromStdString(statekey_); } + QString packname() const { return QString::fromStdString(pack.pack->display_name); } + QString attribution() const { return QString::fromStdString(pack.pack->attribution); } + QString avatarUrl() const { return QString::fromStdString(pack.pack->avatar_url); } + bool isStickerPack() const { return pack.pack->is_sticker(); } + bool isEmotePack() const { return pack.pack->is_emoji(); } + + bool isGloballyEnabled() const; + bool canEdit() const; + void setGloballyEnabled(bool enabled); + + void setPackname(QString val); + void setAttribution(QString val); + void setAvatarUrl(QString val); + void setStatekey(QString val); + void setIsStickerPack(bool val); + void setIsEmotePack(bool val); + + Q_INVOKABLE void save(); + Q_INVOKABLE void addStickers(QList files); + +signals: + void globallyEnabledChanged(); + void statekeyChanged(); + void attributionChanged(); + void packnameChanged(); + void avatarUrlChanged(); + void isEmotePackChanged(); + void isStickerPackChanged(); + + void addImage(std::string uri, std::string filename, mtx::common::ImageInfo info); + +private slots: + void addImageCb(std::string uri, std::string filename, mtx::common::ImageInfo info); + +private: + std::string roomid_; + std::string statekey_, old_statekey_; + + mtx::events::msc2545::ImagePack pack; + std::vector shortcodes; +}; diff --git a/src/UserSettingsPage.cpp b/src/UserSettingsPage.cpp index ffaebe61..ab6ac492 100644 --- a/src/UserSettingsPage.cpp +++ b/src/UserSettingsPage.cpp @@ -90,13 +90,11 @@ UserSettings::load(std::optional profile) decryptSidebar_ = settings.value("user/decrypt_sidebar", true).toBool(); privacyScreen_ = settings.value("user/privacy_screen", false).toBool(); privacyScreenTimeout_ = settings.value("user/privacy_screen_timeout", 0).toInt(); - shareKeysWithTrustedUsers_ = - settings.value("user/automatically_share_keys_with_trusted_users", false).toBool(); - mobileMode_ = settings.value("user/mobile_mode", false).toBool(); - emojiFont_ = settings.value("user/emoji_font_family", "default").toString(); - baseFontSize_ = settings.value("user/font_size", QFont().pointSizeF()).toDouble(); - auto tempPresence = settings.value("user/presence", "").toString().toStdString(); - auto presenceValue = QMetaEnum::fromType().keyToValue(tempPresence.c_str()); + mobileMode_ = settings.value("user/mobile_mode", false).toBool(); + emojiFont_ = settings.value("user/emoji_font_family", "default").toString(); + baseFontSize_ = settings.value("user/font_size", QFont().pointSizeF()).toDouble(); + auto tempPresence = settings.value("user/presence", "").toString().toStdString(); + auto presenceValue = QMetaEnum::fromType().keyToValue(tempPresence.c_str()); if (presenceValue < 0) presenceValue = 0; presence_ = static_cast(presenceValue); @@ -123,6 +121,12 @@ UserSettings::load(std::optional profile) userId_ = settings.value(prefix + "auth/user_id", "").toString(); deviceId_ = settings.value(prefix + "auth/device_id", "").toString(); + shareKeysWithTrustedUsers_ = + settings.value(prefix + "user/automatically_share_keys_with_trusted_users", false) + .toBool(); + onlyShareKeysWithVerifiedUsers_ = + settings.value(prefix + "user/only_share_keys_with_verified_users", false).toBool(); + disableCertificateValidation_ = settings.value("disable_certificate_validation", false).toBool(); @@ -401,6 +405,17 @@ UserSettings::setUseStunServer(bool useStunServer) save(); } +void +UserSettings::setOnlyShareKeysWithVerifiedUsers(bool shareKeys) +{ + if (shareKeys == onlyShareKeysWithVerifiedUsers_) + return; + + onlyShareKeysWithVerifiedUsers_ = shareKeys; + emit onlyShareKeysWithVerifiedUsersChanged(shareKeys); + save(); +} + void UserSettings::setShareKeysWithTrustedUsers(bool shareKeys) { @@ -610,8 +625,6 @@ UserSettings::save() settings.setValue("decrypt_sidebar", decryptSidebar_); settings.setValue("privacy_screen", privacyScreen_); settings.setValue("privacy_screen_timeout", privacyScreenTimeout_); - settings.setValue("automatically_share_keys_with_trusted_users", - shareKeysWithTrustedUsers_); settings.setValue("mobile_mode", mobileMode_); settings.setValue("font_size", baseFontSize_); settings.setValue("typing_notifications", typingNotifications_); @@ -650,6 +663,11 @@ UserSettings::save() settings.setValue(prefix + "auth/user_id", userId_); settings.setValue(prefix + "auth/device_id", deviceId_); + settings.setValue(prefix + "user/automatically_share_keys_with_trusted_users", + shareKeysWithTrustedUsers_); + settings.setValue(prefix + "user/only_share_keys_with_verified_users", + onlyShareKeysWithVerifiedUsers_); + settings.setValue("disable_certificate_validation", disableCertificateValidation_); settings.sync(); @@ -703,41 +721,43 @@ UserSettingsPage::UserSettingsPage(QSharedPointer settings, QWidge general_->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Fixed); general_->setFont(font); - trayToggle_ = new Toggle{this}; - startInTrayToggle_ = new Toggle{this}; - avatarCircles_ = new Toggle{this}; - decryptSidebar_ = new Toggle(this); - privacyScreen_ = new Toggle{this}; - shareKeysWithTrustedUsers_ = new Toggle(this); - groupViewToggle_ = new Toggle{this}; - timelineButtonsToggle_ = new Toggle{this}; - typingNotifications_ = new Toggle{this}; - messageHoverHighlight_ = new Toggle{this}; - enlargeEmojiOnlyMessages_ = new Toggle{this}; - sortByImportance_ = new Toggle{this}; - readReceipts_ = new Toggle{this}; - markdown_ = new Toggle{this}; - desktopNotifications_ = new Toggle{this}; - alertOnNotification_ = new Toggle{this}; - useStunServer_ = new Toggle{this}; - mobileMode_ = new Toggle{this}; - scaleFactorCombo_ = new QComboBox{this}; - fontSizeCombo_ = new QComboBox{this}; - fontSelectionCombo_ = new QFontComboBox{this}; - emojiFontSelectionCombo_ = new QComboBox{this}; - ringtoneCombo_ = new QComboBox{this}; - microphoneCombo_ = new QComboBox{this}; - cameraCombo_ = new QComboBox{this}; - cameraResolutionCombo_ = new QComboBox{this}; - cameraFrameRateCombo_ = new QComboBox{this}; - timelineMaxWidthSpin_ = new QSpinBox{this}; - privacyScreenTimeout_ = new QSpinBox{this}; + trayToggle_ = new Toggle{this}; + startInTrayToggle_ = new Toggle{this}; + avatarCircles_ = new Toggle{this}; + decryptSidebar_ = new Toggle(this); + privacyScreen_ = new Toggle{this}; + onlyShareKeysWithVerifiedUsers_ = new Toggle(this); + shareKeysWithTrustedUsers_ = new Toggle(this); + groupViewToggle_ = new Toggle{this}; + timelineButtonsToggle_ = new Toggle{this}; + typingNotifications_ = new Toggle{this}; + messageHoverHighlight_ = new Toggle{this}; + enlargeEmojiOnlyMessages_ = new Toggle{this}; + sortByImportance_ = new Toggle{this}; + readReceipts_ = new Toggle{this}; + markdown_ = new Toggle{this}; + desktopNotifications_ = new Toggle{this}; + alertOnNotification_ = new Toggle{this}; + useStunServer_ = new Toggle{this}; + mobileMode_ = new Toggle{this}; + scaleFactorCombo_ = new QComboBox{this}; + fontSizeCombo_ = new QComboBox{this}; + fontSelectionCombo_ = new QFontComboBox{this}; + emojiFontSelectionCombo_ = new QComboBox{this}; + ringtoneCombo_ = new QComboBox{this}; + microphoneCombo_ = new QComboBox{this}; + cameraCombo_ = new QComboBox{this}; + cameraResolutionCombo_ = new QComboBox{this}; + cameraFrameRateCombo_ = new QComboBox{this}; + timelineMaxWidthSpin_ = new QSpinBox{this}; + privacyScreenTimeout_ = new QSpinBox{this}; trayToggle_->setChecked(settings_->tray()); startInTrayToggle_->setChecked(settings_->startInTray()); avatarCircles_->setChecked(settings_->avatarCircles()); decryptSidebar_->setChecked(settings_->decryptSidebar()); privacyScreen_->setChecked(settings_->privacyScreen()); + onlyShareKeysWithVerifiedUsers_->setChecked(settings_->onlyShareKeysWithVerifiedUsers()); shareKeysWithTrustedUsers_->setChecked(settings_->shareKeysWithTrustedUsers()); groupViewToggle_->setChecked(settings_->groupView()); timelineButtonsToggle_->setChecked(settings_->buttonsInTimeline()); @@ -1008,10 +1028,14 @@ UserSettingsPage::UserSettingsPage(QSharedPointer settings, QWidge formLayout_->addRow(new HorizontalLine{this}); boxWrap(tr("Device ID"), deviceIdValue_); boxWrap(tr("Device Fingerprint"), deviceFingerprintValue_); - boxWrap( - tr("Share keys with verified users and devices"), - shareKeysWithTrustedUsers_, - tr("Automatically replies to key requests from other users, if they are verified.")); + boxWrap(tr("Send encrypted messages to verified users only"), + onlyShareKeysWithVerifiedUsers_, + tr("Requires a user to be verified to send encrypted messages to them. This " + "improves safety but makes E2EE more tedious.")); + boxWrap(tr("Share keys with verified users and devices"), + shareKeysWithTrustedUsers_, + tr("Automatically replies to key requests from other users, if they are verified, " + "even if that device shouldn't have access to those keys otherwise.")); formLayout_->addRow(new HorizontalLine{this}); formLayout_->addRow(sessionKeysLabel, sessionKeysLayout); formLayout_->addRow(crossSigningKeysLabel, crossSigningKeysLayout); @@ -1179,6 +1203,10 @@ UserSettingsPage::UserSettingsPage(QSharedPointer settings, QWidge } }); + connect(onlyShareKeysWithVerifiedUsers_, &Toggle::toggled, this, [this](bool enabled) { + settings_->setOnlyShareKeysWithVerifiedUsers(enabled); + }); + connect(shareKeysWithTrustedUsers_, &Toggle::toggled, this, [this](bool enabled) { settings_->setShareKeysWithTrustedUsers(enabled); }); @@ -1271,6 +1299,7 @@ UserSettingsPage::showEvent(QShowEvent *) groupViewToggle_->setState(settings_->groupView()); decryptSidebar_->setState(settings_->decryptSidebar()); privacyScreen_->setState(settings_->privacyScreen()); + onlyShareKeysWithVerifiedUsers_->setState(settings_->onlyShareKeysWithVerifiedUsers()); shareKeysWithTrustedUsers_->setState(settings_->shareKeysWithTrustedUsers()); avatarCircles_->setState(settings_->avatarCircles()); typingNotifications_->setState(settings_->typingNotifications()); @@ -1399,7 +1428,7 @@ UserSettingsPage::exportSessionKeys() QString suffix("-----END MEGOLM SESSION DATA-----"); QString newline("\n"); QTextStream out(&file); - out << prefix << newline << b64 << newline << suffix; + out << prefix << newline << b64 << newline << suffix << newline; file.close(); } catch (const std::exception &e) { QMessageBox::warning(this, tr("Error"), e.what()); diff --git a/src/UserSettingsPage.h b/src/UserSettingsPage.h index acb08569..096aab81 100644 --- a/src/UserSettingsPage.h +++ b/src/UserSettingsPage.h @@ -88,6 +88,8 @@ class UserSettings : public QObject setScreenShareHideCursor NOTIFY screenShareHideCursorChanged) Q_PROPERTY( bool useStunServer READ useStunServer WRITE setUseStunServer NOTIFY useStunServerChanged) + Q_PROPERTY(bool onlyShareKeysWithVerifiedUsers READ onlyShareKeysWithVerifiedUsers WRITE + setOnlyShareKeysWithVerifiedUsers NOTIFY onlyShareKeysWithVerifiedUsersChanged) Q_PROPERTY(bool shareKeysWithTrustedUsers READ shareKeysWithTrustedUsers WRITE setShareKeysWithTrustedUsers NOTIFY shareKeysWithTrustedUsersChanged) Q_PROPERTY(QString profile READ profile WRITE setProfile NOTIFY profileChanged) @@ -152,6 +154,7 @@ public: void setScreenShareRemoteVideo(bool state); void setScreenShareHideCursor(bool state); void setUseStunServer(bool state); + void setOnlyShareKeysWithVerifiedUsers(bool state); void setShareKeysWithTrustedUsers(bool state); void setProfile(QString profile); void setUserId(QString userId); @@ -208,6 +211,7 @@ public: bool screenShareHideCursor() const { return screenShareHideCursor_; } bool useStunServer() const { return useStunServer_; } bool shareKeysWithTrustedUsers() const { return shareKeysWithTrustedUsers_; } + bool onlyShareKeysWithVerifiedUsers() const { return onlyShareKeysWithVerifiedUsers_; } QString profile() const { return profile_; } QString userId() const { return userId_; } QString accessToken() const { return accessToken_; } @@ -252,6 +256,7 @@ signals: void screenShareRemoteVideoChanged(bool state); void screenShareHideCursorChanged(bool state); void useStunServerChanged(bool state); + void onlyShareKeysWithVerifiedUsersChanged(bool state); void shareKeysWithTrustedUsersChanged(bool state); void profileChanged(QString profile); void userIdChanged(QString userId); @@ -284,6 +289,7 @@ private: bool privacyScreen_; int privacyScreenTimeout_; bool shareKeysWithTrustedUsers_; + bool onlyShareKeysWithVerifiedUsers_; bool mobileMode_; int timelineMaxWidth_; int roomListWidth_; @@ -372,6 +378,7 @@ private: Toggle *privacyScreen_; QSpinBox *privacyScreenTimeout_; Toggle *shareKeysWithTrustedUsers_; + Toggle *onlyShareKeysWithVerifiedUsers_; Toggle *mobileMode_; QLabel *deviceFingerprintValue_; QLabel *deviceIdValue_; diff --git a/src/Utils.cpp b/src/Utils.cpp index 8d5ae4a9..41013e39 100644 --- a/src/Utils.cpp +++ b/src/Utils.cpp @@ -172,32 +172,6 @@ utils::scaleFactor() return settings.value("settings/scale_factor", -1).toFloat(); } -bool -utils::respondsToKeyRequests(const std::string &roomId) -{ - return respondsToKeyRequests(QString::fromStdString(roomId)); -} - -bool -utils::respondsToKeyRequests(const QString &roomId) -{ - if (roomId.isEmpty()) - return false; - - QSettings settings; - return settings.value("rooms/respond_to_key_requests/" + roomId, false).toBool(); -} - -void -utils::setKeyRequestsPreference(QString roomId, bool value) -{ - if (roomId.isEmpty()) - return; - - QSettings settings; - settings.setValue("rooms/respond_to_key_requests/" + roomId, value); -} - QString utils::descriptiveTime(const QDateTime &then) { @@ -556,7 +530,7 @@ utils::markdownToHtml(const QString &text, bool rainbowify) // Use colors as described here: // https://shark.comfsm.fm/~dleeling/cis/hsl_rainbow.html auto color = - QColor::fromHslF((charIdx - 1.0) / textLen * (5. / 6.), 1.0, 0.5); + QColor::fromHslF((charIdx - 1.0) / textLen * (5. / 6.), 0.9, 0.5); // format color for HTML auto colorString = color.name(QColor::NameFormat::HexRgb); // create HTML element for current char diff --git a/src/Utils.h b/src/Utils.h index 1d48e2c7..8f37a574 100644 --- a/src/Utils.h +++ b/src/Utils.h @@ -67,15 +67,6 @@ scaleFactor(); void setScaleFactor(float factor); -//! Whether or not we should respond to key requests for the given room. -bool -respondsToKeyRequests(const QString &roomId); -bool -respondsToKeyRequests(const std::string &roomId); - -void -setKeyRequestsPreference(QString roomId, bool value); - //! Human friendly timestamp representation. QString descriptiveTime(const QDateTime &then); diff --git a/src/dialogs/InviteUsers.cpp b/src/dialogs/InviteUsers.cpp deleted file mode 100644 index 9dd6085f..00000000 --- a/src/dialogs/InviteUsers.cpp +++ /dev/null @@ -1,158 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Nheko Contributors -// -// SPDX-License-Identifier: GPL-3.0-or-later - -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "dialogs/InviteUsers.h" - -#include "Config.h" -#include "InviteeItem.h" -#include "ui/TextField.h" - -#include - -using namespace dialogs; - -InviteUsers::InviteUsers(QWidget *parent) - : QFrame(parent) -{ - setAutoFillBackground(true); - setWindowFlags(Qt::Tool | Qt::WindowStaysOnTopHint); - setWindowModality(Qt::WindowModal); - setAttribute(Qt::WA_DeleteOnClose, true); - - setMinimumWidth(conf::window::minModalWidth); - setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Maximum); - - auto layout = new QVBoxLayout(this); - layout->setSpacing(conf::modals::WIDGET_SPACING); - layout->setMargin(conf::modals::WIDGET_MARGIN); - - auto buttonLayout = new QHBoxLayout(); - buttonLayout->setSpacing(0); - buttonLayout->setMargin(0); - - confirmBtn_ = new QPushButton("Invite", this); - confirmBtn_->setDefault(true); - cancelBtn_ = new QPushButton(tr("Cancel"), this); - - buttonLayout->addStretch(1); - buttonLayout->setSpacing(15); - buttonLayout->addWidget(cancelBtn_); - buttonLayout->addWidget(confirmBtn_); - - inviteeInput_ = new TextField(this); - inviteeInput_->setLabel(tr("User ID to invite")); - - inviteeList_ = new QListWidget; - inviteeList_->setFrameStyle(QFrame::NoFrame); - inviteeList_->setSelectionMode(QAbstractItemView::NoSelection); - inviteeList_->setAttribute(Qt::WA_MacShowFocusRect, 0); - inviteeList_->setSpacing(5); - - errorLabel_ = new QLabel(this); - errorLabel_->setAlignment(Qt::AlignCenter); - - layout->addWidget(inviteeInput_); - layout->addWidget(errorLabel_); - layout->addWidget(inviteeList_); - layout->addLayout(buttonLayout); - - connect(inviteeInput_, &TextField::returnPressed, this, &InviteUsers::addUser); - connect(confirmBtn_, &QPushButton::clicked, [this]() { - if (!inviteeInput_->text().trimmed().isEmpty()) { - addUser(); - } - - emit sendInvites(invitedUsers()); - - inviteeInput_->clear(); - inviteeList_->clear(); - errorLabel_->hide(); - - emit close(); - }); - - connect(cancelBtn_, &QPushButton::clicked, [this]() { - inviteeInput_->clear(); - inviteeList_->clear(); - errorLabel_->hide(); - - emit close(); - }); -} - -void -InviteUsers::addUser() -{ - auto user_id = inviteeInput_->text(); - - try { - namespace ids = mtx::identifiers; - auto user = ids::parse(user_id.toStdString()); - - auto item = new QListWidgetItem(inviteeList_); - auto invitee = new InviteeItem(user, this); - - item->setSizeHint(invitee->minimumSizeHint()); - item->setFlags(Qt::NoItemFlags); - item->setTextAlignment(Qt::AlignCenter); - - inviteeList_->setItemWidget(item, invitee); - - connect(invitee, &InviteeItem::removeItem, this, [this, item]() { - emit removeInvitee(item); - }); - - errorLabel_->hide(); - inviteeInput_->clear(); - } catch (std::exception &e) { - errorLabel_->setText(e.what()); - errorLabel_->show(); - } -} - -void -InviteUsers::removeInvitee(QListWidgetItem *item) -{ - int row = inviteeList_->row(item); - auto widget = inviteeList_->takeItem(row); - - inviteeList_->removeItemWidget(widget); -} - -QStringList -InviteUsers::invitedUsers() const -{ - QStringList users; - - for (int ii = 0; ii < inviteeList_->count(); ++ii) { - auto item = inviteeList_->item(ii); - auto widget = inviteeList_->itemWidget(item); - auto invitee = qobject_cast(widget); - - if (invitee) - users << invitee->userID(); - else - qDebug() << "Cast InviteeItem failed"; - } - - return users; -} - -void -InviteUsers::showEvent(QShowEvent *event) -{ - inviteeInput_->setFocus(); - - QFrame::showEvent(event); -} diff --git a/src/dialogs/InviteUsers.h b/src/dialogs/InviteUsers.h deleted file mode 100644 index e40183c1..00000000 --- a/src/dialogs/InviteUsers.h +++ /dev/null @@ -1,45 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Nheko Contributors -// -// SPDX-License-Identifier: GPL-3.0-or-later - -#pragma once - -#include -#include - -class QPushButton; -class QLabel; -class TextField; -class QListWidget; -class QListWidgetItem; - -namespace dialogs { - -class InviteUsers : public QFrame -{ - Q_OBJECT -public: - explicit InviteUsers(QWidget *parent = nullptr); - -protected: - void showEvent(QShowEvent *event) override; - -signals: - void sendInvites(QStringList invitees); - -private slots: - void removeInvitee(QListWidgetItem *item); - -private: - void addUser(); - QStringList invitedUsers() const; - - QPushButton *confirmBtn_; - QPushButton *cancelBtn_; - - TextField *inviteeInput_; - QLabel *errorLabel_; - - QListWidget *inviteeList_; -}; -} // dialogs diff --git a/src/dialogs/MemberList.cpp b/src/dialogs/MemberList.cpp deleted file mode 100644 index 21eb72b0..00000000 --- a/src/dialogs/MemberList.cpp +++ /dev/null @@ -1,146 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Nheko Contributors -// -// SPDX-License-Identifier: GPL-3.0-or-later - -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "dialogs/MemberList.h" - -#include "Cache.h" -#include "ChatPage.h" -#include "Config.h" -#include "Logging.h" -#include "Utils.h" -#include "ui/Avatar.h" - -using namespace dialogs; - -MemberItem::MemberItem(const RoomMember &member, QWidget *parent) - : QWidget(parent) -{ - topLayout_ = new QHBoxLayout(this); - topLayout_->setMargin(0); - - textLayout_ = new QVBoxLayout; - textLayout_->setMargin(0); - textLayout_->setSpacing(0); - - avatar_ = new Avatar(this, 44); - avatar_->setLetter(utils::firstChar(member.display_name)); - - avatar_->setImage(ChatPage::instance()->currentRoom(), member.user_id); - - QFont nameFont; - nameFont.setPointSizeF(nameFont.pointSizeF() * 1.1); - - userId_ = new QLabel(member.user_id, this); - userName_ = new QLabel(member.display_name, this); - userName_->setFont(nameFont); - - textLayout_->addWidget(userName_); - textLayout_->addWidget(userId_); - - topLayout_->addWidget(avatar_); - topLayout_->addLayout(textLayout_, 1); -} - -void -MemberItem::paintEvent(QPaintEvent *) -{ - QStyleOption opt; - opt.init(this); - QPainter p(this); - style()->drawPrimitive(QStyle::PE_Widget, &opt, &p, this); -} - -MemberList::MemberList(const QString &room_id, QWidget *parent) - : QFrame(parent) - , room_id_{room_id} -{ - setAutoFillBackground(true); - setWindowFlags(Qt::Tool | Qt::WindowStaysOnTopHint); - setWindowModality(Qt::WindowModal); - setAttribute(Qt::WA_DeleteOnClose, true); - - auto layout = new QVBoxLayout(this); - layout->setSpacing(conf::modals::WIDGET_SPACING); - layout->setMargin(conf::modals::WIDGET_MARGIN); - - list_ = new QListWidget; - list_->setFrameStyle(QFrame::NoFrame); - list_->setSelectionMode(QAbstractItemView::NoSelection); - list_->setSpacing(5); - - QFont largeFont; - largeFont.setPointSizeF(largeFont.pointSizeF() * 1.5); - - setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Maximum); - setMinimumHeight(list_->sizeHint().height() * 2); - setMinimumWidth(std::max(list_->sizeHint().width() + 4 * conf::modals::WIDGET_MARGIN, - QFontMetrics(largeFont).averageCharWidth() * 30 - - 2 * conf::modals::WIDGET_MARGIN)); - - QFont font; - font.setPointSizeF(font.pointSizeF() * conf::modals::LABEL_MEDIUM_SIZE_RATIO); - - topLabel_ = new QLabel(tr("Room members"), this); - topLabel_->setAlignment(Qt::AlignCenter); - topLabel_->setFont(font); - - auto okBtn = new QPushButton(tr("OK"), this); - - auto buttonLayout = new QHBoxLayout(); - buttonLayout->setSpacing(15); - buttonLayout->addStretch(1); - buttonLayout->addWidget(okBtn); - - layout->addWidget(topLabel_); - layout->addWidget(list_); - layout->addLayout(buttonLayout); - - list_->clear(); - - connect(list_->verticalScrollBar(), &QAbstractSlider::valueChanged, this, [this](int pos) { - if (pos != list_->verticalScrollBar()->maximum()) - return; - - const size_t numMembers = list_->count() - 1; - - if (numMembers > 0) - addUsers(cache::getMembers(room_id_.toStdString(), numMembers)); - }); - - try { - addUsers(cache::getMembers(room_id_.toStdString())); - } catch (const lmdb::error &e) { - nhlog::db()->critical("Failed to retrieve members from cache: {}", e.what()); - } - - auto closeShortcut = new QShortcut(QKeySequence(QKeySequence::Cancel), this); - connect(closeShortcut, &QShortcut::activated, this, &MemberList::close); - connect(okBtn, &QPushButton::clicked, this, &MemberList::close); -} - -void -MemberList::addUsers(const std::vector &members) -{ - for (const auto &member : members) { - auto user = new MemberItem(member, this); - auto item = new QListWidgetItem; - - item->setSizeHint(user->minimumSizeHint()); - item->setFlags(Qt::NoItemFlags); - item->setTextAlignment(Qt::AlignCenter); - - list_->insertItem(list_->count() - 1, item); - list_->setItemWidget(item, user); - } -} diff --git a/src/dialogs/MemberList.h b/src/dialogs/MemberList.h deleted file mode 100644 index b822eec8..00000000 --- a/src/dialogs/MemberList.h +++ /dev/null @@ -1,57 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Nheko Contributors -// -// SPDX-License-Identifier: GPL-3.0-or-later - -#pragma once - -#include -#include - -class Avatar; -class QPushButton; -class QHBoxLayout; -class QLabel; -class QVBoxLayout; - -struct RoomMember; - -template -class QSharedPointer; - -namespace dialogs { - -class MemberItem : public QWidget -{ - Q_OBJECT - -public: - MemberItem(const RoomMember &member, QWidget *parent); - -protected: - void paintEvent(QPaintEvent *) override; - -private: - QHBoxLayout *topLayout_; - QVBoxLayout *textLayout_; - - Avatar *avatar_; - - QLabel *userName_; - QLabel *userId_; -}; - -class MemberList : public QFrame -{ - Q_OBJECT -public: - MemberList(const QString &room_id, QWidget *parent = nullptr); - -public slots: - void addUsers(const std::vector &users); - -private: - QString room_id_; - QLabel *topLabel_; - QListWidget *list_; -}; -} // dialogs diff --git a/src/dialogs/RawMessage.h b/src/dialogs/RawMessage.h deleted file mode 100644 index e95f675c..00000000 --- a/src/dialogs/RawMessage.h +++ /dev/null @@ -1,60 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Nheko Contributors -// -// SPDX-License-Identifier: GPL-3.0-or-later - -#pragma once - -#include -#include -#include -#include -#include - -#include "nlohmann/json.hpp" - -#include "Logging.h" -#include "MainWindow.h" -#include "ui/FlatButton.h" - -namespace dialogs { - -class RawMessage : public QWidget -{ - Q_OBJECT -public: - RawMessage(QString msg, QWidget *parent = nullptr) - : QWidget{parent} - { - QFont monospaceFont = QFontDatabase::systemFont(QFontDatabase::FixedFont); - - auto layout = new QVBoxLayout{this}; - auto viewer = new QTextBrowser{this}; - viewer->setFont(monospaceFont); - viewer->setText(msg); - - layout->setSpacing(0); - layout->setMargin(0); - layout->addWidget(viewer); - - setAutoFillBackground(true); - setWindowFlags(Qt::Tool | Qt::WindowStaysOnTopHint); - setAttribute(Qt::WA_DeleteOnClose, true); - - QSize winsize; - QPoint center; - - auto window = MainWindow::instance(); - if (window) { - winsize = window->frameGeometry().size(); - center = window->frameGeometry().center(); - - move(center.x() - (width() * 0.5), center.y() - (height() * 0.5)); - } else { - nhlog::ui()->warn("unable to retrieve MainWindow's size"); - } - - raise(); - show(); - } -}; -} // namespace dialogs diff --git a/src/dialogs/ReadReceipts.cpp b/src/dialogs/ReadReceipts.cpp deleted file mode 100644 index fa7132fd..00000000 --- a/src/dialogs/ReadReceipts.cpp +++ /dev/null @@ -1,179 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Nheko Contributors -// -// SPDX-License-Identifier: GPL-3.0-or-later - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "dialogs/ReadReceipts.h" - -#include "AvatarProvider.h" -#include "Cache.h" -#include "ChatPage.h" -#include "Config.h" -#include "Utils.h" -#include "ui/Avatar.h" - -using namespace dialogs; - -ReceiptItem::ReceiptItem(QWidget *parent, - const QString &user_id, - uint64_t timestamp, - const QString &room_id) - : QWidget(parent) -{ - topLayout_ = new QHBoxLayout(this); - topLayout_->setMargin(0); - - textLayout_ = new QVBoxLayout; - textLayout_->setMargin(0); - textLayout_->setSpacing(conf::modals::TEXT_SPACING); - - QFont nameFont; - nameFont.setPointSizeF(nameFont.pointSizeF() * 1.1); - - auto displayName = cache::displayName(room_id, user_id); - - avatar_ = new Avatar(this, 44); - avatar_->setLetter(utils::firstChar(displayName)); - - // If it's a matrix id we use the second letter. - if (displayName.size() > 1 && displayName.at(0) == '@') - avatar_->setLetter(QChar(displayName.at(1))); - - userName_ = new QLabel(displayName, this); - userName_->setFont(nameFont); - - timestamp_ = new QLabel(dateFormat(QDateTime::fromMSecsSinceEpoch(timestamp)), this); - - textLayout_->addWidget(userName_); - textLayout_->addWidget(timestamp_); - - topLayout_->addWidget(avatar_); - topLayout_->addLayout(textLayout_, 1); - - avatar_->setImage(ChatPage::instance()->currentRoom(), user_id); -} - -void -ReceiptItem::paintEvent(QPaintEvent *) -{ - QStyleOption opt; - opt.init(this); - QPainter p(this); - style()->drawPrimitive(QStyle::PE_Widget, &opt, &p, this); -} - -QString -ReceiptItem::dateFormat(const QDateTime &then) const -{ - auto now = QDateTime::currentDateTime(); - auto days = then.daysTo(now); - - if (days == 0) - return tr("Today %1") - .arg(QLocale::system().toString(then.time(), QLocale::ShortFormat)); - else if (days < 2) - return tr("Yesterday %1") - .arg(QLocale::system().toString(then.time(), QLocale::ShortFormat)); - else if (days < 7) - return QString("%1 %2") - .arg(then.toString("dddd")) - .arg(QLocale::system().toString(then.time(), QLocale::ShortFormat)); - - return QLocale::system().toString(then.time(), QLocale::ShortFormat); -} - -ReadReceipts::ReadReceipts(QWidget *parent) - : QFrame(parent) -{ - setAutoFillBackground(true); - setWindowFlags(Qt::Tool | Qt::WindowStaysOnTopHint); - setWindowModality(Qt::WindowModal); - setAttribute(Qt::WA_DeleteOnClose, true); - - auto layout = new QVBoxLayout(this); - layout->setSpacing(conf::modals::WIDGET_SPACING); - layout->setMargin(conf::modals::WIDGET_MARGIN); - - userList_ = new QListWidget; - userList_->setFrameStyle(QFrame::NoFrame); - userList_->setSelectionMode(QAbstractItemView::NoSelection); - userList_->setSpacing(conf::modals::TEXT_SPACING); - - QFont largeFont; - largeFont.setPointSizeF(largeFont.pointSizeF() * 1.5); - - setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Maximum); - setMinimumHeight(userList_->sizeHint().height() * 2); - setMinimumWidth(std::max(userList_->sizeHint().width() + 4 * conf::modals::WIDGET_MARGIN, - QFontMetrics(largeFont).averageCharWidth() * 30 - - 2 * conf::modals::WIDGET_MARGIN)); - - QFont font; - font.setPointSizeF(font.pointSizeF() * conf::modals::LABEL_MEDIUM_SIZE_RATIO); - - topLabel_ = new QLabel(tr("Read receipts"), this); - topLabel_->setAlignment(Qt::AlignCenter); - topLabel_->setFont(font); - - auto okBtn = new QPushButton(tr("Close"), this); - - auto buttonLayout = new QHBoxLayout(); - buttonLayout->setSpacing(15); - buttonLayout->addStretch(1); - buttonLayout->addWidget(okBtn); - - layout->addWidget(topLabel_); - layout->addWidget(userList_); - layout->addLayout(buttonLayout); - - auto closeShortcut = new QShortcut(QKeySequence(QKeySequence::Cancel), this); - connect(closeShortcut, &QShortcut::activated, this, &ReadReceipts::close); - connect(okBtn, &QPushButton::clicked, this, &ReadReceipts::close); -} - -void -ReadReceipts::addUsers(const std::multimap> &receipts) -{ - // We want to remove any previous items that have been set. - userList_->clear(); - - for (const auto &receipt : receipts) { - auto user = new ReceiptItem(this, - QString::fromStdString(receipt.second), - receipt.first, - ChatPage::instance()->currentRoom()); - auto item = new QListWidgetItem(userList_); - - item->setSizeHint(user->minimumSizeHint()); - item->setFlags(Qt::NoItemFlags); - item->setTextAlignment(Qt::AlignCenter); - - userList_->setItemWidget(item, user); - } -} - -void -ReadReceipts::paintEvent(QPaintEvent *) -{ - QStyleOption opt; - opt.init(this); - QPainter p(this); - style()->drawPrimitive(QStyle::PE_Widget, &opt, &p, this); -} - -void -ReadReceipts::hideEvent(QHideEvent *event) -{ - userList_->clear(); - QFrame::hideEvent(event); -} diff --git a/src/dialogs/ReadReceipts.h b/src/dialogs/ReadReceipts.h deleted file mode 100644 index 5c6c5d2b..00000000 --- a/src/dialogs/ReadReceipts.h +++ /dev/null @@ -1,61 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Nheko Contributors -// -// SPDX-License-Identifier: GPL-3.0-or-later - -#pragma once - -#include -#include - -class Avatar; -class QLabel; -class QListWidget; -class QHBoxLayout; -class QVBoxLayout; - -namespace dialogs { - -class ReceiptItem : public QWidget -{ - Q_OBJECT - -public: - ReceiptItem(QWidget *parent, - const QString &user_id, - uint64_t timestamp, - const QString &room_id); - -protected: - void paintEvent(QPaintEvent *) override; - -private: - QString dateFormat(const QDateTime &then) const; - - QHBoxLayout *topLayout_; - QVBoxLayout *textLayout_; - - Avatar *avatar_; - - QLabel *userName_; - QLabel *timestamp_; -}; - -class ReadReceipts : public QFrame -{ - Q_OBJECT -public: - explicit ReadReceipts(QWidget *parent = nullptr); - -public slots: - void addUsers(const std::multimap> &users); - -protected: - void paintEvent(QPaintEvent *event) override; - void hideEvent(QHideEvent *event) override; - -private: - QLabel *topLabel_; - - QListWidget *userList_; -}; -} // dialogs diff --git a/src/notifications/ManagerLinux.cpp b/src/notifications/ManagerLinux.cpp index 598b2bd0..2809de87 100644 --- a/src/notifications/ManagerLinux.cpp +++ b/src/notifications/ManagerLinux.cpp @@ -295,12 +295,9 @@ operator<<(QDBusArgument &arg, const QImage &image) int channels = i.isGrayscale() ? 1 : (i.hasAlphaChannel() ? 4 : 3); arg << i.depth() / channels; arg << channels; -#if QT_VERSION < QT_VERSION_CHECK(5, 10, 0) - arg << QByteArray(reinterpret_cast(i.bits()), i.byteCount()); -#else arg << QByteArray(reinterpret_cast(i.bits()), i.sizeInBytes()); -#endif arg.endStructure(); + return arg; } diff --git a/src/timeline/EventStore.cpp b/src/timeline/EventStore.cpp index 9a91ff79..742f8dbb 100644 --- a/src/timeline/EventStore.cpp +++ b/src/timeline/EventStore.cpp @@ -20,8 +20,7 @@ Q_DECLARE_METATYPE(Reaction) -QCache EventStore::decryptedEvents_{ - 1000}; +QCache EventStore::decryptedEvents_{1000}; QCache EventStore::events_by_id_{ 1000}; QCache EventStore::events_{1000}; @@ -144,12 +143,16 @@ EventStore::EventStore(std::string room_id, QObject *) mtx::events::msg::Encrypted>) { auto event = decryptEvent({room_id_, e.event_id}, e); - if (auto dec = - std::get_if>(event)) { - emit updateFlowEventId( - event_id.event_id.to_string()); + if (event->event) { + if (auto dec = std::get_if< + mtx::events::RoomEvent< + mtx::events::msg:: + KeyVerificationRequest>>( + &event->event.value())) { + emit updateFlowEventId( + event_id.event_id + .to_string()); + } } } }); @@ -393,12 +396,12 @@ EventStore::handleSync(const mtx::responses::Timeline &events) if (auto encrypted = std::get_if>( &event)) { - mtx::events::collections::TimelineEvents *d_event = - decryptEvent({room_id_, encrypted->event_id}, *encrypted); - if (std::visit( + auto d_event = decryptEvent({room_id_, encrypted->event_id}, *encrypted); + if (d_event->event && + std::visit( [](auto e) { return (e.sender != utils::localUser().toStdString()); }, - *d_event)) { - handle_room_verification(*d_event); + *d_event->event)) { + handle_room_verification(*d_event->event); } } } @@ -599,11 +602,15 @@ EventStore::get(int idx, bool decrypt) events_.insert(index, event_ptr); } - if (decrypt) + if (decrypt) { if (auto encrypted = std::get_if>( - event_ptr)) - return decryptEvent({room_id_, encrypted->event_id}, *encrypted); + event_ptr)) { + auto decrypted = decryptEvent({room_id_, encrypted->event_id}, *encrypted); + if (decrypted->event) + return &*decrypted->event; + } + } return event_ptr; } @@ -629,7 +636,7 @@ EventStore::indexToId(int idx) const return cache::client()->getTimelineEventId(room_id_, toInternalIdx(idx)); } -mtx::events::collections::TimelineEvents * +olm::DecryptionResult * EventStore::decryptEvent(const IdIndex &idx, const mtx::events::EncryptedEvent &e) { @@ -641,57 +648,24 @@ EventStore::decryptEvent(const IdIndex &idx, index.session_id = e.content.session_id; index.sender_key = e.content.sender_key; - auto asCacheEntry = [&idx](mtx::events::collections::TimelineEvents &&event) { - auto event_ptr = new mtx::events::collections::TimelineEvents(std::move(event)); + auto asCacheEntry = [&idx](olm::DecryptionResult &&event) { + auto event_ptr = new olm::DecryptionResult(std::move(event)); decryptedEvents_.insert(idx, event_ptr); return event_ptr; }; auto decryptionResult = olm::decryptEvent(index, e); - mtx::events::RoomEvent dummy; - dummy.origin_server_ts = e.origin_server_ts; - dummy.event_id = e.event_id; - dummy.sender = e.sender; - if (decryptionResult.error) { - switch (*decryptionResult.error) { + switch (decryptionResult.error) { case olm::DecryptionErrorCode::MissingSession: case olm::DecryptionErrorCode::MissingSessionIndex: { - if (decryptionResult.error == olm::DecryptionErrorCode::MissingSession) - dummy.content.body = - tr("-- Encrypted Event (No keys found for decryption) --", - "Placeholder, when the message was not decrypted yet or can't " - "be " - "decrypted.") - .toStdString(); - else - dummy.content.body = - tr("-- Encrypted Event (Key not valid for this index) --", - "Placeholder, when the message can't be decrypted with this " - "key since it is not valid for this index ") - .toStdString(); nhlog::crypto()->info("Could not find inbound megolm session ({}, {}, {})", index.room_id, index.session_id, e.sender); - // we may not want to request keys during initial sync and such - if (suppressKeyRequests) - break; - // TODO: Check if this actually works and look in key backup - auto copy = e; - copy.room_id = room_id_; - if (pending_key_requests.count(e.content.session_id)) { - pending_key_requests.at(e.content.session_id) - .events.push_back(copy); - } else { - PendingKeyRequests request; - request.request_id = - "key_request." + http::client()->generate_txn_id(); - request.events.push_back(copy); - olm::send_key_request_for(copy, request.request_id); - pending_key_requests[e.content.session_id] = request; - } + + requestSession(e, false); break; } case olm::DecryptionErrorCode::DbError: @@ -701,12 +675,6 @@ EventStore::decryptEvent(const IdIndex &idx, index.session_id, index.sender_key, decryptionResult.error_message.value_or("")); - dummy.content.body = - tr("-- Decryption Error (failed to retrieve megolm keys from db) --", - "Placeholder, when the message can't be decrypted, because the DB " - "access " - "failed.") - .toStdString(); break; case olm::DecryptionErrorCode::DecryptionFailed: nhlog::crypto()->critical( @@ -715,22 +683,8 @@ EventStore::decryptEvent(const IdIndex &idx, index.session_id, index.sender_key, decryptionResult.error_message.value_or("")); - dummy.content.body = - tr("-- Decryption Error (%1) --", - "Placeholder, when the message can't be decrypted. In this case, the " - "Olm " - "decrytion returned an error, which is passed as %1.") - .arg( - QString::fromStdString(decryptionResult.error_message.value_or(""))) - .toStdString(); break; case olm::DecryptionErrorCode::ParsingFailed: - dummy.content.body = - tr("-- Encrypted Event (Unknown event type) --", - "Placeholder, when the message was decrypted, but we couldn't parse " - "it, because " - "Nheko/mtxclient don't support that event type yet.") - .toStdString(); break; case olm::DecryptionErrorCode::ReplayAttack: nhlog::crypto()->critical( @@ -738,85 +692,50 @@ EventStore::decryptEvent(const IdIndex &idx, e.event_id, room_id_, index.sender_key); - dummy.content.body = - tr("-- Replay attack! This message index was reused! --").toStdString(); break; - case olm::DecryptionErrorCode::UnknownFingerprint: - // TODO: don't fail, just show in UI. - nhlog::crypto()->critical("Message by unverified fingerprint {}", - index.sender_key); - dummy.content.body = - tr("-- Message by unverified device! --").toStdString(); + case olm::DecryptionErrorCode::NoError: + // unreachable break; } - return asCacheEntry(std::move(dummy)); - } - - std::string msg_str; - try { - auto session = cache::client()->getInboundMegolmSession(index); - auto res = - olm::client()->decrypt_group_message(session.get(), e.content.ciphertext); - msg_str = std::string((char *)res.data.data(), res.data.size()); - } catch (const lmdb::error &e) { - nhlog::db()->critical("failed to retrieve megolm session with index ({}, {}, {})", - index.room_id, - index.session_id, - index.sender_key, - e.what()); - dummy.content.body = - tr("-- Decryption Error (failed to retrieve megolm keys from db) --", - "Placeholder, when the message can't be decrypted, because the DB " - "access " - "failed.") - .toStdString(); - return asCacheEntry(std::move(dummy)); - } catch (const mtx::crypto::olm_exception &e) { - nhlog::crypto()->critical("failed to decrypt message with index ({}, {}, {}): {}", - index.room_id, - index.session_id, - index.sender_key, - e.what()); - dummy.content.body = - tr("-- Decryption Error (%1) --", - "Placeholder, when the message can't be decrypted. In this case, the " - "Olm " - "decrytion returned an error, which is passed as %1.") - .arg(e.what()) - .toStdString(); - return asCacheEntry(std::move(dummy)); - } - - // Add missing fields for the event. - json body = json::parse(msg_str); - body["event_id"] = e.event_id; - body["sender"] = e.sender; - body["origin_server_ts"] = e.origin_server_ts; - body["unsigned"] = e.unsigned_data; - - // relations are unencrypted in content... - mtx::common::add_relations(body["content"], e.content.relations); - - json event_array = json::array(); - event_array.push_back(body); - - std::vector temp_events; - mtx::responses::utils::parse_timeline_events(event_array, temp_events); - - if (temp_events.size() == 1) { - auto encInfo = mtx::accessors::file(temp_events[0]); - - if (encInfo) - emit newEncryptedImage(encInfo.value()); - - return asCacheEntry(std::move(temp_events[0])); + return asCacheEntry(std::move(decryptionResult)); } auto encInfo = mtx::accessors::file(decryptionResult.event.value()); if (encInfo) emit newEncryptedImage(encInfo.value()); - return asCacheEntry(std::move(decryptionResult.event.value())); + return asCacheEntry(std::move(decryptionResult)); +} + +void +EventStore::requestSession(const mtx::events::EncryptedEvent &ev, + bool manual) +{ + // we may not want to request keys during initial sync and such + if (suppressKeyRequests) + return; + + // TODO: Look in key backup + auto copy = ev; + copy.room_id = room_id_; + if (pending_key_requests.count(ev.content.session_id)) { + auto &r = pending_key_requests.at(ev.content.session_id); + r.events.push_back(copy); + + // automatically request once every 10 min, manually every 1 min + qint64 delay = manual ? 60 : (60 * 10); + if (r.requested_at + delay < QDateTime::currentSecsSinceEpoch()) { + r.requested_at = QDateTime::currentSecsSinceEpoch(); + olm::send_key_request_for(copy, r.request_id); + } + } else { + PendingKeyRequests request; + request.request_id = "key_request." + http::client()->generate_txn_id(); + request.requested_at = QDateTime::currentSecsSinceEpoch(); + request.events.push_back(copy); + olm::send_key_request_for(copy, request.request_id); + pending_key_requests[ev.content.session_id] = request; + } } void @@ -877,15 +796,56 @@ EventStore::get(std::string id, std::string_view related_to, bool decrypt, bool events_by_id_.insert(index, event_ptr); } - if (decrypt) + if (decrypt) { if (auto encrypted = std::get_if>( - event_ptr)) - return decryptEvent(index, *encrypted); + event_ptr)) { + auto decrypted = decryptEvent(index, *encrypted); + if (decrypted->event) + return &*decrypted->event; + } + } return event_ptr; } +olm::DecryptionErrorCode +EventStore::decryptionError(std::string id) +{ + if (this->thread() != QThread::currentThread()) + nhlog::db()->warn("{} called from a different thread!", __func__); + + if (id.empty()) + return olm::DecryptionErrorCode::NoError; + + IdIndex index{room_id_, std::move(id)}; + auto edits_ = edits(index.id); + if (!edits_.empty()) { + index.id = mtx::accessors::event_id(edits_.back()); + auto event_ptr = + new mtx::events::collections::TimelineEvents(std::move(edits_.back())); + events_by_id_.insert(index, event_ptr); + } + + auto event_ptr = events_by_id_.object(index); + if (!event_ptr) { + auto event = cache::client()->getEvent(room_id_, index.id); + if (!event) { + return olm::DecryptionErrorCode::NoError; + } + event_ptr = new mtx::events::collections::TimelineEvents(std::move(event->data)); + events_by_id_.insert(index, event_ptr); + } + + if (auto encrypted = + std::get_if>(event_ptr)) { + auto decrypted = decryptEvent(index, *encrypted); + return decrypted->error; + } + + return olm::DecryptionErrorCode::NoError; +} + void EventStore::fetchMore() { diff --git a/src/timeline/EventStore.h b/src/timeline/EventStore.h index 7c404102..59c1c7c0 100644 --- a/src/timeline/EventStore.h +++ b/src/timeline/EventStore.h @@ -15,6 +15,7 @@ #include #include +#include "Olm.h" #include "Reaction.h" class EventStore : public QObject @@ -78,6 +79,9 @@ public: mtx::events::collections::TimelineEvents *get(int idx, bool decrypt = true); QVariantList reactions(const std::string &event_id); + olm::DecryptionErrorCode decryptionError(std::string id); + void requestSession(const mtx::events::EncryptedEvent &ev, + bool manual); int size() const { @@ -119,7 +123,7 @@ public slots: private: std::vector edits(const std::string &event_id); - mtx::events::collections::TimelineEvents *decryptEvent( + olm::DecryptionResult *decryptEvent( const IdIndex &idx, const mtx::events::EncryptedEvent &e); void handle_room_verification(mtx::events::collections::TimelineEvents event); @@ -129,7 +133,7 @@ private: uint64_t first = std::numeric_limits::max(), last = std::numeric_limits::max(); - static QCache decryptedEvents_; + static QCache decryptedEvents_; static QCache events_; static QCache events_by_id_; @@ -137,6 +141,7 @@ private: { std::string request_id; std::vector> events; + qint64 requested_at; }; std::map pending_key_requests; diff --git a/src/timeline/InputBar.cpp b/src/timeline/InputBar.cpp index 56d0d1ce..f17081e5 100644 --- a/src/timeline/InputBar.cpp +++ b/src/timeline/InputBar.cpp @@ -19,9 +19,9 @@ #include "Cache.h" #include "ChatPage.h" +#include "CombinedImagePackModel.h" #include "CompletionProxyModel.h" #include "Config.h" -#include "ImagePackModel.h" #include "Logging.h" #include "MainWindow.h" #include "MatrixClient.h" @@ -503,7 +503,7 @@ InputBar::video(const QString &filename, } void -InputBar::sticker(ImagePackModel *model, int row) +InputBar::sticker(CombinedImagePackModel *model, int row) { if (!model || row < 0) return; diff --git a/src/timeline/InputBar.h b/src/timeline/InputBar.h index acedceb7..2e6fb5c0 100644 --- a/src/timeline/InputBar.h +++ b/src/timeline/InputBar.h @@ -12,7 +12,7 @@ #include class TimelineModel; -class ImagePackModel; +class CombinedImagePackModel; class QMimeData; class QDropEvent; class QStringList; @@ -58,7 +58,7 @@ public slots: MarkdownOverride useMarkdown = MarkdownOverride::NOT_SPECIFIED, bool rainbowify = false); void reaction(const QString &reactedEvent, const QString &reactionKey); - void sticker(ImagePackModel *model, int row); + void sticker(CombinedImagePackModel *model, int row); private slots: void startTyping(); diff --git a/src/timeline/Permissions.cpp b/src/timeline/Permissions.cpp index 1eaab468..e4957045 100644 --- a/src/timeline/Permissions.cpp +++ b/src/timeline/Permissions.cpp @@ -8,9 +8,9 @@ #include "MatrixClient.h" #include "TimelineModel.h" -Permissions::Permissions(TimelineModel *parent) +Permissions::Permissions(QString roomId, QObject *parent) : QObject(parent) - , room(parent) + , roomId_(roomId) { invalidate(); } @@ -19,7 +19,7 @@ void Permissions::invalidate() { pl = cache::client() - ->getStateEvent(room->roomId().toStdString()) + ->getStateEvent(roomId_.toStdString()) .value_or(mtx::events::StateEvent{}) .content; } diff --git a/src/timeline/Permissions.h b/src/timeline/Permissions.h index f7e6f389..7aab1ddb 100644 --- a/src/timeline/Permissions.h +++ b/src/timeline/Permissions.h @@ -15,7 +15,7 @@ class Permissions : public QObject Q_OBJECT public: - Permissions(TimelineModel *parent); + Permissions(QString roomId, QObject *parent = nullptr); Q_INVOKABLE bool canInvite(); Q_INVOKABLE bool canBan(); @@ -28,6 +28,6 @@ public: void invalidate(); private: - TimelineModel *room; + QString roomId_; mtx::events::state::PowerLevels pl; }; diff --git a/src/timeline/RoomlistModel.cpp b/src/timeline/RoomlistModel.cpp index f7f377fb..f4c927ac 100644 --- a/src/timeline/RoomlistModel.cpp +++ b/src/timeline/RoomlistModel.cpp @@ -533,6 +533,8 @@ RoomlistModel::initializeRooms() for (const auto &id : cache::client()->roomIds()) addRoom(id, true); + nhlog::db()->info("Restored {} rooms from cache", rowCount()); + endResetModel(); } diff --git a/src/timeline/TimelineModel.cpp b/src/timeline/TimelineModel.cpp index abfe28a9..99e00a67 100644 --- a/src/timeline/TimelineModel.cpp +++ b/src/timeline/TimelineModel.cpp @@ -25,11 +25,12 @@ #include "Logging.h" #include "MainWindow.h" #include "MatrixClient.h" +#include "MemberList.h" #include "MxcImageProvider.h" #include "Olm.h" +#include "ReadReceiptsModel.h" #include "TimelineViewManager.h" #include "Utils.h" -#include "dialogs/RawMessage.h" Q_DECLARE_METATYPE(QModelIndex) @@ -307,6 +308,15 @@ qml_mtx_events::fromRoomEventType(qml_mtx_events::EventType t) case qml_mtx_events::KeyVerificationDone: case qml_mtx_events::KeyVerificationReady: return mtx::events::EventType::RoomMessage; + //! m.image_pack, currently im.ponies.room_emotes + case qml_mtx_events::ImagePackInRoom: + return mtx::events::EventType::ImagePackRooms; + //! m.image_pack, currently im.ponies.user_emotes + case qml_mtx_events::ImagePackInAccountData: + return mtx::events::EventType::ImagePackInAccountData; + //! m.image_pack.rooms, currently im.ponies.emote_rooms + case qml_mtx_events::ImagePackRooms: + return mtx::events::EventType::ImagePackRooms; default: return mtx::events::EventType::Unsupported; }; @@ -317,6 +327,7 @@ TimelineModel::TimelineModel(TimelineViewManager *manager, QString room_id, QObj , events(room_id.toStdString(), this) , room_id_(room_id) , manager_(manager) + , permissions_{room_id} { lastMessage_.timestamp = 0; @@ -325,6 +336,10 @@ TimelineModel::TimelineModel(TimelineViewManager *manager, QString room_id, QObj this->isSpace_ = create->content.type == mtx::events::state::room_type::space; this->isEncrypted_ = cache::isRoomEncrypted(room_id_.toStdString()); + // this connection will simplify adding the plainRoomNameChanged() signal everywhere that it + // needs to be + connect(this, &TimelineModel::roomNameChanged, this, &TimelineModel::plainRoomNameChanged); + connect( this, &TimelineModel::redactionFailed, @@ -344,6 +359,7 @@ TimelineModel::TimelineModel(TimelineViewManager *manager, QString room_id, QObj &EventStore::dataChanged, this, [this](int from, int to) { + relatedEventCacheBuster++; nhlog::ui()->debug( "data changed {} to {}", events.size() - to - 1, events.size() - from - 1); emit dataChanged(index(events.size() - to - 1, 0), @@ -436,6 +452,7 @@ TimelineModel::roleNames() const {IsEditable, "isEditable"}, {IsEncrypted, "isEncrypted"}, {Trustlevel, "trustlevel"}, + {EncryptionError, "encryptionError"}, {ReplyTo, "replyTo"}, {Reactions, "reactions"}, {RoomId, "roomId"}, @@ -443,6 +460,7 @@ TimelineModel::roleNames() const {RoomTopic, "roomTopic"}, {CallType, "callType"}, {Dump, "dump"}, + {RelatedEventCacheBuster, "relatedEventCacheBuster"}, }; } int @@ -622,6 +640,9 @@ TimelineModel::data(const mtx::events::collections::TimelineEvents &event, int r return crypto::Trust::Unverified; } + case EncryptionError: + return events.decryptionError(event_id(event)); + case ReplyTo: return QVariant(QString::fromStdString(relations(event).reply_to().value_or(""))); case Reactions: { @@ -673,9 +694,12 @@ TimelineModel::data(const mtx::events::collections::TimelineEvents &event, int r m.insert(names[RoomName], data(event, static_cast(RoomName))); m.insert(names[RoomTopic], data(event, static_cast(RoomTopic))); m.insert(names[CallType], data(event, static_cast(CallType))); + m.insert(names[EncryptionError], data(event, static_cast(EncryptionError))); return QVariant(m); } + case RelatedEventCacheBuster: + return relatedEventCacheBuster; default: return QVariant(); } @@ -1015,14 +1039,13 @@ TimelineModel::formatDateSeparator(QDate date) const } void -TimelineModel::viewRawMessage(QString id) const +TimelineModel::viewRawMessage(QString id) { auto e = events.get(id.toStdString(), "", false); if (!e) return; std::string ev = mtx::accessors::serialize_event(*e).dump(4); - auto dialog = new dialogs::RawMessage(QString::fromStdString(ev)); - Q_UNUSED(dialog); + emit showRawMessageDialog(QString::fromStdString(ev)); } void @@ -1036,15 +1059,14 @@ TimelineModel::forwardMessage(QString eventId, QString roomId) } void -TimelineModel::viewDecryptedRawMessage(QString id) const +TimelineModel::viewDecryptedRawMessage(QString id) { auto e = events.get(id.toStdString(), ""); if (!e) return; std::string ev = mtx::accessors::serialize_event(*e).dump(4); - auto dialog = new dialogs::RawMessage(QString::fromStdString(ev)); - Q_UNUSED(dialog); + emit showRawMessageDialog(QString::fromStdString(ev)); } void @@ -1056,14 +1078,6 @@ TimelineModel::openUserProfile(QString userid) emit manager_->openProfile(userProfile); } -void -TimelineModel::openRoomSettings() -{ - RoomSettings *settings = new RoomSettings(roomId(), this); - connect(this, &TimelineModel::roomAvatarUrlChanged, settings, &RoomSettings::avatarChanged); - openRoomSettingsDialog(settings); -} - void TimelineModel::replyAction(QString id) { @@ -1087,9 +1101,9 @@ TimelineModel::relatedInfo(QString id) } void -TimelineModel::readReceiptsAction(QString id) const +TimelineModel::showReadReceipts(QString id) { - MainWindow::instance()->openReadReceiptsDialog(id); + emit openReadReceiptsDialog(new ReadReceiptsProxy{id, roomId(), this}); } void @@ -1542,6 +1556,17 @@ TimelineModel::scrollTimerEvent() } } +void +TimelineModel::requestKeyForEvent(QString id) +{ + auto encrypted_event = events.get(id.toStdString(), "", false); + if (encrypted_event) { + if (auto ev = std::get_if>( + encrypted_event)) + events.requestSession(*ev, true); + } +} + void TimelineModel::copyLinkToEvent(QString eventId) const { diff --git a/src/timeline/TimelineModel.h b/src/timeline/TimelineModel.h index 0e2895d4..ad7cfbbb 100644 --- a/src/timeline/TimelineModel.h +++ b/src/timeline/TimelineModel.h @@ -17,7 +17,10 @@ #include "CacheStructs.h" #include "EventStore.h" #include "InputBar.h" +#include "InviteesModel.h" +#include "MemberList.h" #include "Permissions.h" +#include "ReadReceiptsModel.h" #include "ui/RoomSettings.h" #include "ui/UserProfile.h" @@ -104,7 +107,13 @@ enum EventType KeyVerificationCancel, KeyVerificationKey, KeyVerificationDone, - KeyVerificationReady + KeyVerificationReady, + //! m.image_pack, currently im.ponies.room_emotes + ImagePackInRoom, + //! m.image_pack, currently im.ponies.user_emotes + ImagePackInAccountData, + //! m.image_pack.rooms, currently im.ponies.emote_rooms + ImagePackRooms, }; Q_ENUM_NS(EventType) mtx::events::EventType fromRoomEventType(qml_mtx_events::EventType); @@ -158,7 +167,9 @@ class TimelineModel : public QAbstractListModel Q_PROPERTY(QString edit READ edit WRITE setEdit NOTIFY editChanged RESET resetEdit) Q_PROPERTY( bool paginationInProgress READ paginationInProgress NOTIFY paginationInProgressChanged) + Q_PROPERTY(QString roomId READ roomId CONSTANT) Q_PROPERTY(QString roomName READ roomName NOTIFY roomNameChanged) + Q_PROPERTY(QString plainRoomName READ plainRoomName NOTIFY plainRoomNameChanged) Q_PROPERTY(QString roomAvatarUrl READ roomAvatarUrl NOTIFY roomAvatarUrlChanged) Q_PROPERTY(QString roomTopic READ roomTopic NOTIFY roomTopicChanged) Q_PROPERTY(int roomMemberCount READ roomMemberCount NOTIFY roomMemberCountChanged) @@ -201,6 +212,7 @@ public: IsEditable, IsEncrypted, Trustlevel, + EncryptionError, ReplyTo, Reactions, RoomId, @@ -208,6 +220,7 @@ public: RoomTopic, CallType, Dump, + RelatedEventCacheBuster, }; Q_ENUM(Roles); @@ -230,14 +243,13 @@ public: Q_INVOKABLE QString formatGuestAccessEvent(QString id); Q_INVOKABLE QString formatPowerLevelEvent(QString id); - Q_INVOKABLE void viewRawMessage(QString id) const; + Q_INVOKABLE void viewRawMessage(QString id); Q_INVOKABLE void forwardMessage(QString eventId, QString roomId); - Q_INVOKABLE void viewDecryptedRawMessage(QString id) const; + Q_INVOKABLE void viewDecryptedRawMessage(QString id); Q_INVOKABLE void openUserProfile(QString userid); - Q_INVOKABLE void openRoomSettings(); Q_INVOKABLE void editAction(QString id); Q_INVOKABLE void replyAction(QString id); - Q_INVOKABLE void readReceiptsAction(QString id) const; + Q_INVOKABLE void showReadReceipts(QString id); Q_INVOKABLE void redactEvent(QString id); Q_INVOKABLE int idToIndex(QString id) const; Q_INVOKABLE QString indexToId(int index) const; @@ -253,6 +265,8 @@ public: endResetModel(); } + Q_INVOKABLE void requestKeyForEvent(QString id); + std::vector<::Reaction> reactions(const std::string &event_id) { auto list = events.reactions(event_id); @@ -344,6 +358,8 @@ signals: void typingUsersChanged(std::vector users); void replyChanged(QString reply); void editChanged(QString reply); + void openReadReceiptsDialog(ReadReceiptsProxy *rr); + void showRawMessageDialog(QString rawMessage); void paginationInProgressChanged(const bool); void newCallEvent(const mtx::events::collections::TimelineEvents &event); void scrollToIndex(int index); @@ -351,14 +367,13 @@ signals: void lastMessageChanged(); void notificationsChanged(); - void openRoomSettingsDialog(RoomSettings *settings); - void newMessageToSend(mtx::events::collections::TimelineEvents event); void addPendingMessageToStore(mtx::events::collections::TimelineEvents event); void updateFlowEventId(std::string event_id); void encryptionChanged(); void roomNameChanged(); + void plainRoomNameChanged(); void roomTopicChanged(); void roomAvatarUrlChanged(); void roomMemberCountChanged(); @@ -388,7 +403,7 @@ private: TimelineViewManager *manager_; InputBar input_{this}; - Permissions permissions_{this}; + Permissions permissions_; QTimer showEventTimer{this}; QString eventIdToShow; @@ -400,6 +415,8 @@ private: int notification_count = 0, highlight_count = 0; + unsigned int relatedEventCacheBuster = 0; + bool decryptDescription = true; bool m_paginationInProgress = false; bool isSpace_ = false; diff --git a/src/timeline/TimelineViewManager.cpp b/src/timeline/TimelineViewManager.cpp index 3e69f92b..b23ed278 100644 --- a/src/timeline/TimelineViewManager.cpp +++ b/src/timeline/TimelineViewManager.cpp @@ -15,16 +15,20 @@ #include "ChatPage.h" #include "Clipboard.h" #include "ColorImageProvider.h" +#include "CombinedImagePackModel.h" #include "CompletionProxyModel.h" #include "DelegateChooser.h" #include "DeviceVerificationFlow.h" #include "EventAccessors.h" -#include "ImagePackModel.h" +#include "ImagePackListModel.h" +#include "InviteesModel.h" #include "Logging.h" #include "MainWindow.h" #include "MatrixClient.h" #include "MxcImageProvider.h" +#include "ReadReceiptsModel.h" #include "RoomsModel.h" +#include "SingleImagePackModel.h" #include "UserSettingsPage.h" #include "UsersModel.h" #include "dialogs/ImageOverlay.h" @@ -145,7 +149,7 @@ TimelineViewManager::TimelineViewManager(CallManager *callManager, ChatPage *par qRegisterMetaType(); qRegisterMetaType(); qRegisterMetaType(); - qRegisterMetaType(); + qRegisterMetaType(); qmlRegisterUncreatableMetaObject(qml_mtx_events::staticMetaObject, "im.nheko", @@ -153,6 +157,8 @@ TimelineViewManager::TimelineViewManager(CallManager *callManager, ChatPage *par 0, "MtxEvent", "Can't instantiate enum!"); + qmlRegisterUncreatableMetaObject( + olm::staticMetaObject, "im.nheko", 1, 0, "Olm", "Can't instantiate enum!"); qmlRegisterUncreatableMetaObject( crypto::staticMetaObject, "im.nheko", 1, 0, "Crypto", "Can't instantiate enum!"); qmlRegisterUncreatableMetaObject(verification::staticMetaObject, @@ -174,6 +180,8 @@ TimelineViewManager::TimelineViewManager(CallManager *callManager, ChatPage *par 0, "UserProfileModel", "UserProfile needs to be instantiated on the C++ side"); + qmlRegisterUncreatableType( + "im.nheko", 1, 0, "MemberList", "MemberList needs to be instantiated on the C++ side"); qmlRegisterUncreatableType( "im.nheko", 1, @@ -182,6 +190,30 @@ TimelineViewManager::TimelineViewManager(CallManager *callManager, ChatPage *par "Room Settings needs to be instantiated on the C++ side"); qmlRegisterUncreatableType( "im.nheko", 1, 0, "Room", "Room needs to be instantiated on the C++ side"); + qmlRegisterUncreatableType( + "im.nheko", + 1, + 0, + "ImagePackListModel", + "ImagePackListModel needs to be instantiated on the C++ side"); + qmlRegisterUncreatableType( + "im.nheko", + 1, + 0, + "SingleImagePackModel", + "SingleImagePackModel needs to be instantiated on the C++ side"); + qmlRegisterUncreatableType( + "im.nheko", + 1, + 0, + "InviteesModel", + "InviteesModel needs to be instantiated on the C++ side"); + qmlRegisterUncreatableType( + "im.nheko", + 1, + 0, + "ReadReceiptsProxy", + "ReadReceiptsProxy needs to be instantiated on the C++ side"); static auto self = this; qmlRegisterSingletonType( @@ -342,6 +374,41 @@ TimelineViewManager::TimelineViewManager(CallManager *callManager, ChatPage *par &TimelineViewManager::openImageOverlayInternal); } +void +TimelineViewManager::openRoomMembers(QString room_id) +{ + MemberList *memberList = new MemberList(room_id, this); + emit openRoomMembersDialog(memberList); +} + +void +TimelineViewManager::openRoomSettings(QString room_id) +{ + RoomSettings *settings = new RoomSettings(room_id, this); + connect(rooms_->getRoomById(room_id).data(), + &TimelineModel::roomAvatarUrlChanged, + settings, + &RoomSettings::avatarChanged); + emit openRoomSettingsDialog(settings); +} + +void +TimelineViewManager::openInviteUsers(QString roomId) +{ + InviteesModel *model = new InviteesModel{this}; + connect(model, &InviteesModel::accept, this, [this, model, roomId]() { + emit inviteUsers(roomId, model->mxids()); + }); + emit openInviteUsersDialog(model); +} + +void +TimelineViewManager::openGlobalUserProfile(QString userId) +{ + UserProfile *profile = new UserProfile{QString{}, userId, this}; + emit openProfile(profile); +} + void TimelineViewManager::setVideoCallItem() { @@ -399,6 +466,12 @@ TimelineViewManager::openImageOverlay(QString mxcUrl, QString eventId) }); } +void +TimelineViewManager::openImagePackSettings(QString roomid) +{ + emit showImagePackSettings(new ImagePackListModel(roomid.toStdString(), this)); +} + void TimelineViewManager::openImageOverlayInternal(QString eventId, QImage img) { @@ -421,17 +494,6 @@ TimelineViewManager::openImageOverlayInternal(QString eventId, QImage img) }); } -void -TimelineViewManager::openInviteUsersDialog() -{ - MainWindow::instance()->openInviteUsersDialog( - [this](const QStringList &invitees) { emit inviteUsers(invitees); }); -} -void -TimelineViewManager::openMemberListDialog(QString roomid) const -{ - MainWindow::instance()->openMemberListDialog(roomid); -} void TimelineViewManager::openLeaveRoomDialog(QString roomid) const { @@ -596,7 +658,7 @@ TimelineViewManager::completerFor(QString completerName, QString roomId) roomModel->setParent(proxy); return proxy; } else if (completerName == "stickers") { - auto stickerModel = new ImagePackModel(roomId.toStdString(), true); + auto stickerModel = new CombinedImagePackModel(roomId.toStdString(), true); auto proxy = new CompletionProxyModel(stickerModel, 1, static_cast(-1) / 4); stickerModel->setParent(proxy); return proxy; diff --git a/src/timeline/TimelineViewManager.h b/src/timeline/TimelineViewManager.h index 15b4f523..54e3a935 100644 --- a/src/timeline/TimelineViewManager.h +++ b/src/timeline/TimelineViewManager.h @@ -33,6 +33,7 @@ class ColorImageProvider; class UserSettings; class ChatPage; class DeviceVerificationFlow; +class ImagePackListModel; class TimelineViewManager : public QObject { @@ -57,6 +58,7 @@ public: Q_INVOKABLE bool isInitialSync() const { return isInitialSync_; } bool isWindowFocused() const { return isWindowFocused_; } Q_INVOKABLE void openImageOverlay(QString mxcUrl, QString eventId); + Q_INVOKABLE void openImagePackSettings(QString roomid); Q_INVOKABLE QColor userColor(QString id, QColor background); Q_INVOKABLE QString escapeEmoji(QString str) const; Q_INVOKABLE QString htmlEscape(QString str) const { return str.toHtmlEscaped(); } @@ -64,9 +66,12 @@ public: Q_INVOKABLE QString userPresence(QString id) const; Q_INVOKABLE QString userStatus(QString id) const; + Q_INVOKABLE void openRoomMembers(QString room_id); + Q_INVOKABLE void openRoomSettings(QString room_id); + Q_INVOKABLE void openInviteUsers(QString roomId); + Q_INVOKABLE void openGlobalUserProfile(QString userId); + Q_INVOKABLE void focusMessageInput(); - Q_INVOKABLE void openInviteUsersDialog(); - Q_INVOKABLE void openMemberListDialog(QString roomid) const; Q_INVOKABLE void openLeaveRoomDialog(QString roomid) const; Q_INVOKABLE void removeVerificationFlow(DeviceVerificationFlow *flow); @@ -81,11 +86,17 @@ signals: void replyingEventChanged(QString replyingEvent); void replyClosed(); void newDeviceVerificationRequest(DeviceVerificationFlow *flow); - void inviteUsers(QStringList users); + void inviteUsers(QString roomId, QStringList users); + void showRoomList(); + void narrowViewChanged(); void focusChanged(); void focusInput(); void openImageOverlayInternalCb(QString eventId, QImage img); + void openRoomMembersDialog(MemberList *members); + void openRoomSettingsDialog(RoomSettings *settings); + void openInviteUsersDialog(InviteesModel *invitees); void openProfile(UserProfile *profile); + void showImagePackSettings(ImagePackListModel *packlist); public slots: void updateReadReceipts(const QString &room_id, const std::vector &event_ids); diff --git a/src/ui/Avatar.cpp b/src/ui/Avatar.cpp deleted file mode 100644 index 154a0e2c..00000000 --- a/src/ui/Avatar.cpp +++ /dev/null @@ -1,168 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Nheko Contributors -// -// SPDX-License-Identifier: GPL-3.0-or-later - -#include -#include -#include - -#include "AvatarProvider.h" -#include "Utils.h" -#include "ui/Avatar.h" - -Avatar::Avatar(QWidget *parent, int size) - : QWidget(parent) - , size_(size) -{ - type_ = ui::AvatarType::Letter; - letter_ = "A"; - - QFont _font(font()); - _font.setPointSizeF(ui::FontSize); - setFont(_font); - - QSizePolicy policy(QSizePolicy::MinimumExpanding, QSizePolicy::MinimumExpanding); - setSizePolicy(policy); -} - -QColor -Avatar::textColor() const -{ - if (!text_color_.isValid()) - return QColor("black"); - - return text_color_; -} - -QColor -Avatar::backgroundColor() const -{ - if (!text_color_.isValid()) - return QColor("white"); - - return background_color_; -} - -QSize -Avatar::sizeHint() const -{ - return QSize(size_ + 2, size_ + 2); -} - -void -Avatar::setTextColor(const QColor &color) -{ - text_color_ = color; -} - -void -Avatar::setBackgroundColor(const QColor &color) -{ - background_color_ = color; -} - -void -Avatar::setLetter(const QString &letter) -{ - letter_ = letter; - type_ = ui::AvatarType::Letter; - update(); -} - -void -Avatar::setImage(const QString &avatar_url) -{ - avatar_url_ = avatar_url; - AvatarProvider::resolve(avatar_url, - static_cast(size_ * pixmap_.devicePixelRatio()), - this, - [this, requestedRatio = pixmap_.devicePixelRatio()](QPixmap pm) { - if (pm.isNull()) - return; - type_ = ui::AvatarType::Image; - pixmap_ = pm; - pixmap_.setDevicePixelRatio(requestedRatio); - update(); - }); -} - -void -Avatar::setImage(const QString &room, const QString &user) -{ - room_ = room; - user_ = user; - AvatarProvider::resolve(room, - user, - static_cast(size_ * pixmap_.devicePixelRatio()), - this, - [this, requestedRatio = pixmap_.devicePixelRatio()](QPixmap pm) { - if (pm.isNull()) - return; - type_ = ui::AvatarType::Image; - pixmap_ = pm; - pixmap_.setDevicePixelRatio(requestedRatio); - update(); - }); -} - -void -Avatar::setDevicePixelRatio(double ratio) -{ - if (type_ == ui::AvatarType::Image && abs(pixmap_.devicePixelRatio() - ratio) > 0.01) { - pixmap_ = pixmap_.scaled(QSize(size_, size_) * ratio); - pixmap_.setDevicePixelRatio(ratio); - - if (!avatar_url_.isEmpty()) - setImage(avatar_url_); - else - setImage(room_, user_); - } -} - -void -Avatar::paintEvent(QPaintEvent *) -{ - bool rounded = QSettings().value(QStringLiteral("user/avatar_circles"), true).toBool(); - - QPainter painter(this); - - painter.setRenderHints(QPainter::Antialiasing | QPainter::SmoothPixmapTransform | - QPainter::TextAntialiasing); - - QRectF r = rect(); - const int hs = size_ / 2; - - if (type_ != ui::AvatarType::Image) { - QBrush brush; - brush.setStyle(Qt::SolidPattern); - brush.setColor(backgroundColor()); - - painter.setPen(Qt::NoPen); - painter.setBrush(brush); - rounded ? painter.drawEllipse(r) : painter.drawRoundedRect(r, 3, 3); - } else if (painter.isActive()) { - setDevicePixelRatio(painter.device()->devicePixelRatioF()); - } - - switch (type_) { - case ui::AvatarType::Image: { - QPainterPath ppath; - - rounded ? ppath.addEllipse(width() / 2 - hs, height() / 2 - hs, size_, size_) - : ppath.addRoundedRect(r, 3, 3); - - painter.setClipPath(ppath); - painter.drawPixmap(QRect(width() / 2 - hs, height() / 2 - hs, size_, size_), - pixmap_); - break; - } - case ui::AvatarType::Letter: { - painter.setPen(textColor()); - painter.setBrush(Qt::NoBrush); - painter.drawText(r.translated(0, -1), Qt::AlignCenter, letter_); - break; - } - default: - break; - } -} diff --git a/src/ui/Avatar.h b/src/ui/Avatar.h deleted file mode 100644 index bbf05be3..00000000 --- a/src/ui/Avatar.h +++ /dev/null @@ -1,48 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Nheko Contributors -// -// SPDX-License-Identifier: GPL-3.0-or-later - -#pragma once - -#include -#include -#include - -#include "Theme.h" - -class Avatar : public QWidget -{ - Q_OBJECT - - Q_PROPERTY(QColor textColor WRITE setTextColor READ textColor) - Q_PROPERTY(QColor backgroundColor WRITE setBackgroundColor READ backgroundColor) - -public: - explicit Avatar(QWidget *parent = nullptr, int size = ui::AvatarSize); - - void setBackgroundColor(const QColor &color); - void setImage(const QString &avatar_url); - void setImage(const QString &room, const QString &user); - void setLetter(const QString &letter); - void setTextColor(const QColor &color); - void setDevicePixelRatio(double ratio); - - QColor backgroundColor() const; - QColor textColor() const; - - QSize sizeHint() const override; - -protected: - void paintEvent(QPaintEvent *event) override; - -private: - void init(); - - ui::AvatarType type_; - QString letter_; - QString avatar_url_, room_, user_; - QColor background_color_; - QColor text_color_; - QPixmap pixmap_; - int size_; -}; diff --git a/src/ui/InfoMessage.cpp b/src/ui/InfoMessage.cpp index fb3b306a..ebe0e63f 100644 --- a/src/ui/InfoMessage.cpp +++ b/src/ui/InfoMessage.cpp @@ -29,13 +29,7 @@ InfoMessage::InfoMessage(QString msg, QWidget *parent) initFont(); QFontMetrics fm{font()}; -#if QT_VERSION < QT_VERSION_CHECK(5, 11, 0) - // width deprecated in 5.13 - width_ = fm.width(msg_) + HPadding * 2; -#else - width_ = fm.horizontalAdvance(msg_) + HPadding * 2; -#endif - + width_ = fm.horizontalAdvance(msg_) + HPadding * 2; height_ = fm.ascent() + 2 * VPadding; setFixedHeight(height_ + 2 * HMargin); @@ -77,12 +71,7 @@ DateSeparator::DateSeparator(QDateTime datetime, QWidget *parent) msg_ = datetime.date().toString(fmt); QFontMetrics fm{font()}; -#if QT_VERSION < QT_VERSION_CHECK(5, 11, 0) - // width deprecated in 5.13 - width_ = fm.width(msg_) + HPadding * 2; -#else - width_ = fm.horizontalAdvance(msg_) + HPadding * 2; -#endif + width_ = fm.horizontalAdvance(msg_) + HPadding * 2; height_ = fm.ascent() + 2 * VPadding; setFixedHeight(height_ + 2 * HMargin); diff --git a/src/ui/NhekoGlobalObject.cpp b/src/ui/NhekoGlobalObject.cpp index fea10839..9e0d706b 100644 --- a/src/ui/NhekoGlobalObject.cpp +++ b/src/ui/NhekoGlobalObject.cpp @@ -6,6 +6,7 @@ #include #include +#include #include "Cache_p.h" #include "ChatPage.h" @@ -140,3 +141,9 @@ Nheko::openJoinRoomDialog() const MainWindow::instance()->openJoinRoomDialog( [](const QString &room_id) { ChatPage::instance()->joinRoom(room_id); }); } + +void +Nheko::reparent(QWindow *win) const +{ + win->setTransientParent(MainWindow::instance()->windowHandle()); +} diff --git a/src/ui/NhekoGlobalObject.h b/src/ui/NhekoGlobalObject.h index 14135fd1..d4d119dc 100644 --- a/src/ui/NhekoGlobalObject.h +++ b/src/ui/NhekoGlobalObject.h @@ -4,12 +4,15 @@ #pragma once +#include #include #include #include "Theme.h" #include "UserProfile.h" +class QWindow; + class Nheko : public QObject { Q_OBJECT @@ -38,12 +41,17 @@ public: int paddingLarge() const { return 20; } UserProfile *currentUser() const; + Q_INVOKABLE QFont monospaceFont() const + { + return QFontDatabase::systemFont(QFontDatabase::FixedFont); + } Q_INVOKABLE void openLink(QString link) const; Q_INVOKABLE void setStatusMessage(QString msg) const; Q_INVOKABLE void showUserSettingsPage() const; Q_INVOKABLE void openLogoutDialog() const; Q_INVOKABLE void openCreateRoomDialog() const; Q_INVOKABLE void openJoinRoomDialog() const; + Q_INVOKABLE void reparent(QWindow *win) const; public slots: void updateUserProfile(); diff --git a/src/ui/Painter.h b/src/ui/Painter.h index 3353f0c7..9f974116 100644 --- a/src/ui/Painter.h +++ b/src/ui/Painter.h @@ -27,12 +27,7 @@ public: { QFontMetrics m(fontMetrics()); if (textWidth < 0) { -#if QT_VERSION < QT_VERSION_CHECK(5, 11, 0) - // deprecated in 5.13: - textWidth = m.width(text); -#else textWidth = m.horizontalAdvance(text); -#endif } drawText((outerw - x - textWidth), y + m.ascent(), text); } diff --git a/src/ui/RoomSettings.cpp b/src/ui/RoomSettings.cpp index f78ef09b..fcba8205 100644 --- a/src/ui/RoomSettings.cpp +++ b/src/ui/RoomSettings.cpp @@ -291,19 +291,6 @@ RoomSettings::accessJoinRules() return accessRules_; } -bool -RoomSettings::respondsToKeyRequests() -{ - return usesEncryption_ && utils::respondsToKeyRequests(roomid_); -} - -void -RoomSettings::changeKeyRequestsPreference(bool isOn) -{ - utils::setKeyRequestsPreference(roomid_, isOn); - emit keyRequestsChanged(); -} - void RoomSettings::enableEncryption() { diff --git a/src/ui/RoomSettings.h b/src/ui/RoomSettings.h index 367f3111..1c8b47d6 100644 --- a/src/ui/RoomSettings.h +++ b/src/ui/RoomSettings.h @@ -78,7 +78,6 @@ class RoomSettings : public QObject Q_PROPERTY(bool canChangeJoinRules READ canChangeJoinRules CONSTANT) Q_PROPERTY(bool canChangeNameAndTopic READ canChangeNameAndTopic CONSTANT) Q_PROPERTY(bool isEncryptionEnabled READ isEncryptionEnabled NOTIFY encryptionChanged) - Q_PROPERTY(bool respondsToKeyRequests READ respondsToKeyRequests NOTIFY keyRequestsChanged) public: RoomSettings(QString roomid, QObject *parent = nullptr); @@ -91,7 +90,6 @@ public: int memberCount() const; int notifications(); int accessJoinRules(); - bool respondsToKeyRequests(); bool isLoading() const; //! Whether the user has enough power level to send m.room.join_rules events. bool canChangeJoinRules() const; @@ -106,7 +104,6 @@ public: Q_INVOKABLE void openEditModal(); Q_INVOKABLE void changeAccessRules(int index); Q_INVOKABLE void changeNotifications(int currentIndex); - Q_INVOKABLE void changeKeyRequestsPreference(bool isOn); signals: void loadingChanged(); @@ -114,7 +111,6 @@ signals: void roomTopicChanged(); void avatarUrlChanged(); void encryptionChanged(); - void keyRequestsChanged(); void notificationsChanged(); void accessJoinRulesChanged(); void displayError(const QString &errorMessage); @@ -136,4 +132,4 @@ private: RoomInfo info_; int notifications_ = 0; int accessRules_ = 0; -}; \ No newline at end of file +};