Browse Source

Merge branch 'dla/feature/room_list_decoration' of https://github.com/vector-im/element-x-android into dla/feature/room_list_decoration

pull/1330/head
David Langley 1 year ago
parent
commit
c160a37b2c
  1. 2
      build.gradle.kts
  2. 1
      changelog.d/1337.bugfix
  3. 1
      changelog.d/1347.bugfix
  4. 6
      features/analytics/impl/src/main/res/values-de/translations.xml
  5. 6
      features/call/src/main/res/values-de/translations.xml
  6. 6
      features/call/src/main/res/values-sk/translations.xml
  7. 11
      features/createroom/impl/src/main/res/values-de/translations.xml
  8. 10
      features/ftue/impl/src/main/res/values-de/translations.xml
  9. 4
      features/invitelist/impl/src/main/kotlin/io/element/android/features/invitelist/impl/InviteListPresenter.kt
  10. 9
      features/invitelist/impl/src/main/res/values-de/translations.xml
  11. 39
      features/login/impl/src/main/res/values-de/translations.xml
  12. 8
      features/logout/api/src/main/res/values-de/translations.xml
  13. 37
      features/messages/impl/src/main/res/values-de/translations.xml
  14. 10
      features/onboarding/impl/src/main/res/values-de/translations.xml
  15. 94
      features/poll/api/src/main/kotlin/io/element/android/features/poll/api/PollAnswerView.kt
  16. 55
      features/poll/api/src/main/kotlin/io/element/android/features/poll/api/PollContentView.kt
  17. 3
      features/poll/impl/src/main/res/values-de/translations.xml
  18. 7
      features/preferences/impl/build.gradle.kts
  19. 13
      features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/PreferencesFlowNode.kt
  20. 9
      features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootNode.kt
  21. 10
      features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootView.kt
  22. 12
      features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/editprofile/EditUserProfileEvents.kt
  23. 56
      features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/editprofile/EditUserProfileNode.kt
  24. 149
      features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/editprofile/EditUserProfilePresenter.kt
  25. 33
      features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/editprofile/EditUserProfileState.kt
  26. 40
      features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/editprofile/EditUserProfileStateProvider.kt
  27. 190
      features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/editprofile/EditUserProfileView.kt
  28. 9
      features/preferences/impl/src/main/res/values/localazy.xml
  29. 429
      features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/user/editprofile/EditUserProfilePresenterTest.kt
  30. 5
      features/rageshake/api/src/main/res/values-de/translations.xml
  31. 15
      features/rageshake/impl/src/main/res/values-de/translations.xml
  32. 74
      features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/edit/RoomDetailsEditView.kt
  33. 46
      features/roomdetails/impl/src/main/res/values-de/translations.xml
  34. 9
      features/roomlist/impl/src/main/res/values-de/translations.xml
  35. 15
      features/verifysession/impl/src/main/res/values-de/translations.xml
  36. 4
      gradle/libs.versions.toml
  37. 1
      libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/file/File.kt
  38. 24
      libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/ui/View.kt
  39. 4
      libraries/androidutils/src/main/res/values-de/translations.xml
  40. 6
      libraries/core/src/main/kotlin/io/element/android/libraries/core/log/logger/LoggerTag.kt
  41. 84
      libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/LabelledOutlinedTextField.kt
  42. 2
      libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/AvatarSize.kt
  43. 54
      libraries/eventformatter/impl/src/main/res/values-de/translations.xml
  44. 3
      libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClient.kt
  45. 17
      libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt
  46. 5
      libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomSummaryListProcessor.kt
  47. 3
      libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/MatrixTimelineDiffProcessor.kt
  48. 2
      libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/tracing/RustTracingTree.kt
  49. 38
      libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt
  50. 97
      libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/EditableAvatarView.kt
  51. 7
      libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/MatrixUserProvider.kt
  52. 2
      libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/notifications/NotificationDrawerManager.kt
  53. 3
      libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/PushersManager.kt
  54. 76
      libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotificationDrawerManager.kt
  55. 9
      libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotifiableEventProcessor.kt
  56. 3
      libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotifiableEventResolver.kt
  57. 19
      libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationBroadcastReceiver.kt
  58. 3
      libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationEventPersistence.kt
  59. 23
      libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationRenderer.kt
  60. 5
      libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationState.kt
  61. 5
      libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/DefaultPushHandler.kt
  62. 45
      libraries/push/impl/src/main/res/values-de/translations.xml
  63. 2
      libraries/push/test/src/main/kotlin/io/element/android/libraries/push/test/notifications/FakeNotificationDrawerManager.kt
  64. 2
      libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/pushproviders/firebase/FirebaseNewTokenHandler.kt
  65. 2
      libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/pushproviders/firebase/FirebasePushProvider.kt
  66. 2
      libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/pushproviders/firebase/VectorFirebaseMessagingService.kt
  67. 2
      libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/pushproviders/unifiedpush/UnifiedPushNewGatewayHandler.kt
  68. 2
      libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/pushproviders/unifiedpush/VectorUnifiedPushMessagingReceiver.kt
  69. 55
      libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/SoftKeyboardEffect.kt
  70. 19
      libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/TextComposer.kt
  71. 4
      libraries/textcomposer/impl/src/main/res/values-de/translations.xml
  72. 4
      libraries/ui-strings/src/main/res/values-cs/translations.xml
  73. 206
      libraries/ui-strings/src/main/res/values-de/translations.xml
  74. 8
      libraries/ui-strings/src/main/res/values-fr/translations.xml
  75. 4
      libraries/ui-strings/src/main/res/values-ru/translations.xml
  76. 17
      libraries/ui-strings/src/main/res/values-sk/translations.xml
  77. 3
      libraries/ui-strings/src/main/res/values/localazy.xml
  78. BIN
      tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemPollView-D-13_14_null_0,NEXUS_5,1.0,en].png
  79. BIN
      tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemPollView-D-13_14_null_1,NEXUS_5,1.0,en].png
  80. BIN
      tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemPollView-N-13_15_null_0,NEXUS_5,1.0,en].png
  81. BIN
      tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemPollView-N-13_15_null_1,NEXUS_5,1.0,en].png
  82. BIN
      tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemEventTimestampBelow_0_null,NEXUS_5,1.0,en].png
  83. BIN
      tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerDisclosedNotSelected-D-0_0_null,NEXUS_5,1.0,en].png
  84. BIN
      tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerDisclosedNotSelected-N-0_1_null,NEXUS_5,1.0,en].png
  85. BIN
      tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerDisclosedNotSelected_0_null,NEXUS_5,1.0,en].png
  86. BIN
      tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerDisclosedSelected-D-1_1_null,NEXUS_5,1.0,en].png
  87. BIN
      tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerDisclosedSelected-N-1_2_null,NEXUS_5,1.0,en].png
  88. BIN
      tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerDisclosedSelected_0_null,NEXUS_5,1.0,en].png
  89. BIN
      tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerEndedSelected-D-6_6_null,NEXUS_5,1.0,en].png
  90. BIN
      tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerEndedSelected-N-6_7_null,NEXUS_5,1.0,en].png
  91. BIN
      tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerEndedSelected_0_null,NEXUS_5,1.0,en].png
  92. BIN
      tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerEndedWinnerNotSelected-D-4_4_null,NEXUS_5,1.0,en].png
  93. BIN
      tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerEndedWinnerNotSelected-N-4_5_null,NEXUS_5,1.0,en].png
  94. BIN
      tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerEndedWinnerNotSelected_0_null,NEXUS_5,1.0,en].png
  95. BIN
      tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerEndedWinnerSelected-D-5_5_null,NEXUS_5,1.0,en].png
  96. BIN
      tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerEndedWinnerSelected-N-5_6_null,NEXUS_5,1.0,en].png
  97. BIN
      tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerEndedWinnerSelected_0_null,NEXUS_5,1.0,en].png
  98. BIN
      tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerUndisclosedNotSelected-D-2_2_null,NEXUS_5,1.0,en].png
  99. BIN
      tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerUndisclosedNotSelected-N-2_3_null,NEXUS_5,1.0,en].png
  100. BIN
      tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerUndisclosedNotSelected_0_null,NEXUS_5,1.0,en].png
  101. Some files were not shown because too many files have changed in this diff Show More

2
build.gradle.kts

@ -6,7 +6,7 @@ import org.jetbrains.kotlin.cli.common.toBooleanLenient @@ -6,7 +6,7 @@ import org.jetbrains.kotlin.cli.common.toBooleanLenient
buildscript {
dependencies {
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.10")
classpath("com.google.gms:google-services:4.3.15")
classpath("com.google.gms:google-services:4.4.0")
}
}

1
changelog.d/1337.bugfix

@ -0,0 +1 @@ @@ -0,0 +1 @@
[Rich text editor] Ensure keyboard opens for reply and text formatting modes

1
changelog.d/1347.bugfix

@ -0,0 +1 @@ @@ -0,0 +1 @@
[Rich text editor] Fix placeholder spilling onto multiple lines

6
features/analytics/impl/src/main/res/values-de/translations.xml

@ -1,4 +1,10 @@ @@ -1,4 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_analytics_prompt_data_usage">"Wir zeichnen keine persönlichen Daten auf und erstellen keine Profile."</string>
<string name="screen_analytics_prompt_help_us_improve">"Teilen Sie anonyme Nutzungsdaten, um uns bei der Identifizierung von Problemen zu helfen."</string>
<string name="screen_analytics_prompt_read_terms">"Sie können alle unsere Bedingungen lesen%1$s."</string>
<string name="screen_analytics_prompt_read_terms_content_link">"hier"</string>
<string name="screen_analytics_prompt_settings">"Sie können diese Funktion jederzeit deaktivieren"</string>
<string name="screen_analytics_prompt_third_party_sharing">"Wir geben Ihre Daten nicht an Dritte weiter"</string>
<string name="screen_analytics_prompt_title">"Hilf uns %1$s zu verbessern"</string>
</resources>

6
features/call/src/main/res/values-de/translations.xml

@ -0,0 +1,6 @@ @@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="call_foreground_service_channel_title_android">"Laufender Anruf"</string>
<string name="call_foreground_service_message_android">"Tippen, um zum Anruf zurückzukehren"</string>
<string name="call_foreground_service_title_android">"☎ Anruf läuft"</string>
</resources>

6
features/call/src/main/res/values-sk/translations.xml

@ -0,0 +1,6 @@ @@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="call_foreground_service_channel_title_android">"Prebiehajúci hovor"</string>
<string name="call_foreground_service_message_android">"Ťuknutím sa vrátite k hovoru"</string>
<string name="call_foreground_service_title_android">"☎ Prebieha hovor"</string>
</resources>

11
features/createroom/impl/src/main/res/values-de/translations.xml

@ -1,4 +1,15 @@ @@ -1,4 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_create_room_action_create_room">"Neuer Raum"</string>
<string name="screen_create_room_action_invite_people">"Freunde zu Element einladen"</string>
<string name="screen_create_room_add_people_title">"Personen einladen"</string>
<string name="screen_create_room_error_creating_room">"Beim Erstellen des Raums ist ein Fehler aufgetreten"</string>
<string name="screen_create_room_private_option_description">"Die Nachrichten in diesem Raum sind verschlüsselt. Die Verschlüsselung kann nicht nachträglich deaktiviert werden."</string>
<string name="screen_create_room_private_option_title">"Privater Raum (nur auf Einladung)"</string>
<string name="screen_create_room_public_option_description">"Die Nachrichten sind nicht verschlüsselt und können von jedem gelesen werden. Die Verschlüsselung kann zu einem späteren Zeitpunkt aktiviert werden."</string>
<string name="screen_create_room_public_option_title">"Öffentlicher Raum (für alle)"</string>
<string name="screen_create_room_room_name_label">"Raumname"</string>
<string name="screen_create_room_topic_label">"Thema (optional)"</string>
<string name="screen_start_chat_error_starting_chat">"Beim Versuch, einen Chat zu starten, ist ein Fehler aufgetreten"</string>
<string name="screen_create_room_title">"Raum erstellen"</string>
</resources>

10
features/ftue/impl/src/main/res/values-de/translations.xml

@ -1,5 +1,13 @@ @@ -1,5 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_notification_optin_subtitle">"Du kannst deine Einstellungen später ändern."</string>
<string name="screen_migration_message">"Dies ist ein einmaliger Vorgang, danke fürs Warten."</string>
<string name="screen_migration_title">"Richten Sie Ihr Konto ein."</string>
<string name="screen_notification_optin_subtitle">"Sie können Ihre Einstellungen später ändern."</string>
<string name="screen_notification_optin_title">"Erlaube Benachrichtigungen und verpasse keine Nachricht"</string>
<string name="screen_welcome_bullet_1">"Anrufe, Umfragen, Suchfunktionen und mehr werden im Laufe des Jahres hinzugefügt."</string>
<string name="screen_welcome_bullet_2">"Der Nachrichtenverlauf für verschlüsselte Räume wird in diesem Update nicht verfügbar sein."</string>
<string name="screen_welcome_bullet_3">"Wir würden uns freuen, von Ihnen zu hören. Teilen Sie uns Ihre Meinung über die Einstellungsseite mit."</string>
<string name="screen_welcome_button">"Los geht\'s!"</string>
<string name="screen_welcome_subtitle">"Folgendes müssen Sie wissen:"</string>
<string name="screen_welcome_title">"Willkommen bei %1$s!"</string>
</resources>

4
features/invitelist/impl/src/main/kotlin/io/element/android/features/invitelist/impl/InviteListPresenter.kt

@ -141,7 +141,7 @@ class InviteListPresenter @Inject constructor( @@ -141,7 +141,7 @@ class InviteListPresenter @Inject constructor(
suspend {
client.getRoom(roomId)?.use {
it.join().getOrThrow()
notificationDrawerManager.clearMembershipNotificationForRoom(client.sessionId, roomId)
notificationDrawerManager.clearMembershipNotificationForRoom(client.sessionId, roomId, doRender = true)
analyticsService.capture(it.toAnalyticsJoinedRoom(JoinedRoom.Trigger.Invite))
}
roomId
@ -152,7 +152,7 @@ class InviteListPresenter @Inject constructor( @@ -152,7 +152,7 @@ class InviteListPresenter @Inject constructor(
suspend {
client.getRoom(roomId)?.use {
it.leave().getOrThrow()
notificationDrawerManager.clearMembershipNotificationForRoom(client.sessionId, roomId)
notificationDrawerManager.clearMembershipNotificationForRoom(client.sessionId, roomId, doRender = true)
}.let { }
}.runCatchingUpdatingState(declinedAction)
}

9
features/invitelist/impl/src/main/res/values-de/translations.xml

@ -0,0 +1,9 @@ @@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_invites_decline_chat_message">"Möchten Sie die Einladung zum Betreten von %1$s wirklich ablehnen?"</string>
<string name="screen_invites_decline_chat_title">"Einladung ablehnen"</string>
<string name="screen_invites_decline_direct_chat_message">"Sind Sie sicher, dass Sie diesen privaten Chat mit %1$s ablehnen möchten?"</string>
<string name="screen_invites_decline_direct_chat_title">"Chat ablehnen"</string>
<string name="screen_invites_empty_list">"Keine Einladungen"</string>
<string name="screen_invites_invited_you">"%1$s (%2$s) hat dich eingeladen"</string>
</resources>

39
features/login/impl/src/main/res/values-de/translations.xml

@ -1,8 +1,47 @@ @@ -1,8 +1,47 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_account_provider_change">"Kontoanbieter ändern"</string>
<string name="screen_account_provider_form_hint">"Homeserver-Adresse"</string>
<string name="screen_account_provider_form_notice">"Geben Sie einen Suchbegriff oder eine Domainadresse ein."</string>
<string name="screen_account_provider_form_subtitle">"Suchen Sie nach einem Unternehmen, einer Community oder einem privaten Server."</string>
<string name="screen_account_provider_form_title">"Kontoanbieter finden"</string>
<string name="screen_account_provider_signin_subtitle">"Hier werden Ihre Gespräche gespeichert – genau so, wie Sie einen E-Mail-Anbieter nutzen würden, um Ihre E-Mails aufzubewahren."</string>
<string name="screen_account_provider_signin_title">"Sie sind dabei, sich bei %s anzumelden"</string>
<string name="screen_account_provider_signup_subtitle">"Hier werden Ihre Gespräche gespeichert – genau so, wie Sie einen E-Mail-Anbieter nutzen würden, um Ihre E-Mails aufzubewahren."</string>
<string name="screen_account_provider_signup_title">"Sie sind dabei, ein Konto bei %s zu erstellen"</string>
<string name="screen_change_account_provider_matrix_org_subtitle">"Matrix.org ist ein großer, kostenloser Server im öffentlichen Matrix-Netzwerk für eine sichere, dezentralisierte Kommunikation, der von der Matrix.org Foundation betrieben wird."</string>
<string name="screen_change_account_provider_other">"Sonstige"</string>
<string name="screen_change_account_provider_subtitle">"Verwenden Sie einen anderen Kontoanbieter, z. B. Ihren eigenen privaten Server oder ein Geschäftskonto."</string>
<string name="screen_change_account_provider_title">"Kontoanbieter wechseln"</string>
<string name="screen_change_server_error_invalid_homeserver">"Wir konnten diesen Homeserver nicht erreichen. Bitte überprüfen Sie, ob Sie die Homeserver-URL korrekt eingegeben haben. Wenn die URL korrekt ist, wenden Sie sich an Ihren Homeserver-Administrator, um weitere Hilfe zu erhalten."</string>
<string name="screen_change_server_error_no_sliding_sync_message">"Dieser Server unterstützt derzeit kein Sliding Sync."</string>
<string name="screen_change_server_form_header">"Homeserver-URL"</string>
<string name="screen_change_server_form_notice">"Sie können nur eine Verbindung zu einem vorhandenen Server herstellen, der Sliding Sync unterstützt. Ihr Homeserver-Administrator muss das konfigurieren. %1$s"</string>
<string name="screen_change_server_subtitle">"Wie lautet die Adresse Ihres Servers?"</string>
<string name="screen_login_error_deactivated_account">"Dieses Konto wurde deaktiviert."</string>
<string name="screen_login_error_invalid_credentials">"Falscher Benutzername und/oder Passwort"</string>
<string name="screen_login_error_invalid_user_id">"Dies ist keine gültige Benutzerkennung. Erwartetes Format: \'@user:homeserver.org\'"</string>
<string name="screen_login_error_unsupported_authentication">"Der ausgewählte Homeserver unterstützt weder den Login per Passwort noch per OIDC. Bitte kontaktieren Sie Ihren Admin oder wählen Sie einen anderen Homeserver."</string>
<string name="screen_login_form_header">"Geben Sie Ihre Daten ein"</string>
<string name="screen_login_title">"Willkommen zurück!"</string>
<string name="screen_login_title_with_homeserver">"Anmelden bei %1$s"</string>
<string name="screen_server_confirmation_change_server">"Kontoanbieter wechseln"</string>
<string name="screen_server_confirmation_message_login_element_dot_io">"Ein privater Server für die Mitarbeiter von Element."</string>
<string name="screen_server_confirmation_message_login_matrix_dot_org">"Matrix ist ein offenes Netzwerk für eine sichere, dezentrale Kommunikation."</string>
<string name="screen_server_confirmation_message_register">"Hier werden Ihre Gespräche gespeichert - so wie Sie Ihre E-Mails bei einem E-Mail-Anbieter aufbewahren würden."</string>
<string name="screen_server_confirmation_title_login">"Sie sind dabei, sich bei %1$s anzumelden"</string>
<string name="screen_server_confirmation_title_register">"Sie sind dabei, ein Konto auf %1$s zu erstellen"</string>
<string name="screen_waitlist_message">"Derzeit besteht eine hohe Nachfrage nach %1$s auf %2$s. Kehren Sie in ein paar Tagen zur App zurück und versuchen Sie es erneut.
Danke für Ihre Geduld!"</string>
<string name="screen_waitlist_message_success">"Willkommen bei %1$s!"</string>
<string name="screen_waitlist_title">"Sie sind fast am Ziel."</string>
<string name="screen_waitlist_title_success">"Sie sind dabei."</string>
<string name="screen_account_provider_continue">"Weiter"</string>
<string name="screen_change_server_submit">"Weiter"</string>
<string name="screen_change_server_title">"Wählen Sie Ihren Server aus"</string>
<string name="screen_login_password_hint">"Passwort"</string>
<string name="screen_login_submit">"Weiter"</string>
<string name="screen_login_subtitle">"Matrix ist ein offenes Netzwerk für eine sichere, dezentrale Kommunikation."</string>
<string name="screen_login_username_hint">"Benutzername"</string>
</resources>

8
features/logout/api/src/main/res/values-de/translations.xml

@ -0,0 +1,8 @@ @@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_signout_confirmation_dialog_content">"Sind Sie sicher, dass Sie sich abmelden wollen?"</string>
<string name="screen_signout_confirmation_dialog_title">"Abmelden"</string>
<string name="screen_signout_in_progress_dialog_content">"Abmelden…"</string>
<string name="screen_signout_confirmation_dialog_submit">"Abmelden"</string>
<string name="screen_signout_preference_item">"Abmelden"</string>
</resources>

37
features/messages/impl/src/main/res/values-de/translations.xml

@ -1,5 +1,42 @@ @@ -1,5 +1,42 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<plurals name="room_timeline_state_changes">
<item quantity="one">"%1$d Raumänderung"</item>
<item quantity="other">"%1$d Raumänderungen"</item>
</plurals>
<string name="screen_room_attachment_source_camera">"Kamera"</string>
<string name="screen_room_attachment_source_camera_photo">"Foto machen"</string>
<string name="screen_room_attachment_source_camera_video">"Video aufnehmen"</string>
<string name="screen_room_attachment_source_files">"Anhang"</string>
<string name="screen_room_attachment_source_gallery">"Foto- und Videobibliothek"</string>
<string name="screen_room_attachment_source_location">"Standort"</string>
<string name="screen_room_attachment_source_poll">"Umfrage"</string>
<string name="screen_room_attachment_text_formatting">"Textformatierung"</string>
<string name="screen_room_encrypted_history_banner">"Der Nachrichtenverlauf ist derzeit in diesem Raum nicht verfügbar"</string>
<string name="screen_room_error_failed_retrieving_user_details">"Benutzerdetails konnten nicht abgerufen werden"</string>
<string name="screen_room_invite_again_alert_message">"Möchten Sie sie wieder einladen?"</string>
<string name="screen_room_invite_again_alert_title">"Sie sind allein in diesem Chat"</string>
<string name="screen_room_message_copied">"Nachricht wurde kopiert"</string>
<string name="screen_room_no_permission_to_post">"Sie sind nicht berechtigt, in diesem Raum zu posten"</string>
<string name="screen_room_notification_settings_allow_custom">"Benutzerdefinierte Einstellung zulassen"</string>
<string name="screen_room_notification_settings_allow_custom_footnote">"Wenn Sie diese Option aktivieren, wird Ihre Standardeinstellung außer Kraft gesetzt."</string>
<string name="screen_room_notification_settings_custom_settings_title">"Benachrichtigen Sie mich in diesem Chat bei"</string>
<string name="screen_room_notification_settings_default_setting_footnote">"Sie können das in Ihrem %1$s ändern."</string>
<string name="screen_room_notification_settings_default_setting_footnote_content_link">"Globale Einstellungen"</string>
<string name="screen_room_notification_settings_default_setting_title">"Standardeinstellung"</string>
<string name="screen_room_notification_settings_edit_remove_setting">"Benutzerdefinierte Einstellung entfernen"</string>
<string name="screen_room_notification_settings_error_loading_settings">"Beim Laden der Benachrichtigungseinstellungen ist ein Fehler aufgetreten."</string>
<string name="screen_room_notification_settings_error_restoring_default">"Fehler beim Wiederherstellen des Standardmodus. Bitte versuchen Sie es erneut."</string>
<string name="screen_room_notification_settings_error_setting_mode">"Fehler beim Einstellen des Modus. Bitte versuchen Sie es erneut."</string>
<string name="screen_room_notification_settings_mode_all_messages">"Alle Nachrichten"</string>
<string name="screen_room_notification_settings_mode_mentions_and_keywords">"Nur Erwähnungen und Schlüsselwörter"</string>
<string name="screen_room_notification_settings_room_custom_settings_title">"Benachrichtigen Sie mich in diesem Raum bei"</string>
<string name="screen_room_reactions_show_less">"Weniger anzeigen"</string>
<string name="screen_room_reactions_show_more">"Mehr anzeigen"</string>
<string name="screen_room_retry_send_menu_send_again_action">"Erneut senden"</string>
<string name="screen_room_retry_send_menu_title">"Ihre Nachricht konnte nicht gesendet werden"</string>
<string name="screen_room_timeline_add_reaction">"Emoji hinzufügen"</string>
<string name="screen_room_timeline_less_reactions">"Weniger anzeigen"</string>
<string name="screen_room_error_failed_processing_media">"Fehler beim Verarbeiten des hochgeladenen Mediums. Bitte versuchen Sie es erneut."</string>
<string name="screen_room_retry_send_menu_remove_action">"Entfernen"</string>
</resources>

10
features/onboarding/impl/src/main/res/values-de/translations.xml

@ -0,0 +1,10 @@ @@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_onboarding_sign_in_manually">"Manuell anmelden"</string>
<string name="screen_onboarding_sign_in_with_qr_code">"Mit QR-Code anmelden"</string>
<string name="screen_onboarding_sign_up">"Konto erstellen"</string>
<string name="screen_onboarding_subtitle">"Sicher kommunizieren und zusammenarbeiten"</string>
<string name="screen_onboarding_welcome_message">"Willkommen beim schnellsten Element aller Zeiten. Optimiert für Geschwindigkeit und Einfachheit."</string>
<string name="screen_onboarding_welcome_subtitle">"Willkommen zu %1$s. Aufgeladen, für Geschwindigkeit und Einfachheit."</string>
<string name="screen_onboarding_welcome_title">"Seien Sie in Ihrem Element"</string>
</resources>

94
features/poll/api/src/main/kotlin/io/element/android/features/poll/api/PollAnswerView.kt

@ -21,24 +21,21 @@ import androidx.compose.foundation.layout.Row @@ -21,24 +21,21 @@ import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.selection.selectable
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.CheckCircle
import androidx.compose.material.icons.filled.RadioButtonUnchecked
import androidx.compose.material3.IconButtonDefaults
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.StrokeCap
import androidx.compose.ui.res.pluralStringResource
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import io.element.android.libraries.designsystem.preview.ElementThemedPreview
import io.element.android.libraries.designsystem.preview.DayNightPreviews
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.theme.components.Icon
import io.element.android.libraries.designsystem.theme.components.IconToggleButton
import io.element.android.libraries.designsystem.theme.components.LinearProgressIndicator
import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.designsystem.theme.progressIndicatorTrackColor
@ -47,41 +44,33 @@ import io.element.android.libraries.theme.ElementTheme @@ -47,41 +44,33 @@ import io.element.android.libraries.theme.ElementTheme
import io.element.android.libraries.ui.strings.CommonPlurals
@Composable
fun PollAnswerView(
internal fun PollAnswerView(
answerItem: PollAnswerItem,
onClick: () -> Unit,
modifier: Modifier = Modifier,
) {
Row(
modifier
.fillMaxWidth()
.selectable(
selected = answerItem.isSelected,
enabled = answerItem.isEnabled,
onClick = onClick,
role = Role.RadioButton,
)
modifier = modifier.fillMaxWidth(),
) {
IconToggleButton(
modifier = Modifier.size(22.dp),
checked = answerItem.isSelected,
enabled = answerItem.isEnabled,
colors = IconButtonDefaults.iconToggleButtonColors(
contentColor = ElementTheme.colors.iconSecondary,
checkedContentColor = ElementTheme.colors.iconPrimary,
disabledContentColor = ElementTheme.colors.iconDisabled,
),
onCheckedChange = { onClick() },
) {
Icon(
imageVector = if (answerItem.isSelected) {
Icons.Default.CheckCircle
Icon(
imageVector = if (answerItem.isSelected) {
Icons.Default.CheckCircle
} else {
Icons.Default.RadioButtonUnchecked
},
contentDescription = null,
modifier = Modifier
.padding(0.5.dp)
.size(22.dp),
tint = if (answerItem.isEnabled) {
if (answerItem.isSelected) {
ElementTheme.colors.iconPrimary
} else {
Icons.Default.RadioButtonUnchecked
},
contentDescription = null,
)
}
ElementTheme.colors.iconSecondary
}
} else {
ElementTheme.colors.iconDisabled
},
)
Spacer(modifier = Modifier.width(12.dp))
Column {
Row {
@ -119,65 +108,58 @@ fun PollAnswerView( @@ -119,65 +108,58 @@ fun PollAnswerView(
}
}
@Preview
@DayNightPreviews
@Composable
internal fun PollAnswerDisclosedNotSelectedPreview() = ElementThemedPreview {
internal fun PollAnswerDisclosedNotSelectedPreview() = ElementPreview {
PollAnswerView(
answerItem = aPollAnswerItem(isDisclosed = true, isSelected = false),
onClick = { },
)
}
@Preview
@DayNightPreviews
@Composable
internal fun PollAnswerDisclosedSelectedPreview() = ElementThemedPreview {
internal fun PollAnswerDisclosedSelectedPreview() = ElementPreview {
PollAnswerView(
answerItem = aPollAnswerItem(isDisclosed = true, isSelected = true),
onClick = { }
)
}
@Preview
@DayNightPreviews
@Composable
internal fun PollAnswerUndisclosedNotSelectedPreview() = ElementThemedPreview {
internal fun PollAnswerUndisclosedNotSelectedPreview() = ElementPreview {
PollAnswerView(
answerItem = aPollAnswerItem(isDisclosed = false, isSelected = false),
onClick = { },
)
}
@Preview
@DayNightPreviews
@Composable
internal fun PollAnswerUndisclosedSelectedPreview() = ElementThemedPreview {
internal fun PollAnswerUndisclosedSelectedPreview() = ElementPreview {
PollAnswerView(
answerItem = aPollAnswerItem(isDisclosed = false, isSelected = true),
onClick = { }
)
}
@Preview
@DayNightPreviews
@Composable
internal fun PollAnswerEndedWinnerNotSelectedPreview() = ElementThemedPreview {
internal fun PollAnswerEndedWinnerNotSelectedPreview() = ElementPreview {
PollAnswerView(
answerItem = aPollAnswerItem(isDisclosed = true, isSelected = false, isEnabled = false, isWinner = true),
onClick = { }
)
}
@Preview
@DayNightPreviews
@Composable
internal fun PollAnswerEndedWinnerSelectedPreview() = ElementThemedPreview {
internal fun PollAnswerEndedWinnerSelectedPreview() = ElementPreview {
PollAnswerView(
answerItem = aPollAnswerItem(isDisclosed = true, isSelected = true, isEnabled = false, isWinner = true),
onClick = { }
)
}
@Preview
@DayNightPreviews
@Composable
internal fun PollAnswerEndedSelectedPreview() = ElementThemedPreview {
internal fun PollAnswerEndedSelectedPreview() = ElementPreview {
PollAnswerView(
answerItem = aPollAnswerItem(isDisclosed = true, isSelected = true, isEnabled = false, isWinner = false),
onClick = { }
)
}

55
features/poll/api/src/main/kotlin/io/element/android/features/poll/api/PollContentView.kt

@ -23,11 +23,14 @@ import androidx.compose.foundation.layout.Row @@ -23,11 +23,14 @@ import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.selection.selectable
import androidx.compose.foundation.selection.selectableGroup
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.unit.dp
import io.element.android.libraries.designsystem.VectorIcons
import io.element.android.libraries.designsystem.preview.DayNightPreviews
@ -56,24 +59,24 @@ fun PollContentView( @@ -56,24 +59,24 @@ fun PollContentView(
}
Column(
modifier = modifier
.selectableGroup()
.fillMaxWidth(),
modifier = modifier.fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(16.dp),
) {
PollTitle(title = question, isPollEnded = isPollEnded)
PollAnswers(answerItems = answerItems, onAnswerSelected = ::onAnswerSelected)
when {
isPollEnded || pollKind == PollKind.Disclosed -> DisclosedPollBottomNotice(answerItems)
pollKind == PollKind.Undisclosed -> UndisclosedPollBottomNotice()
if (isPollEnded || pollKind == PollKind.Disclosed) {
val votesCount = remember(answerItems) { answerItems.sumOf { it.votesCount } }
DisclosedPollBottomNotice(votesCount = votesCount)
} else {
UndisclosedPollBottomNotice()
}
}
}
@Composable
internal fun PollTitle(
private fun PollTitle(
title: String,
isPollEnded: Boolean,
modifier: Modifier = Modifier
@ -85,13 +88,13 @@ internal fun PollTitle( @@ -85,13 +88,13 @@ internal fun PollTitle(
if (isPollEnded) {
Icon(
resourceId = VectorIcons.PollEnd,
contentDescription = null,
contentDescription = stringResource(id = CommonStrings.a11y_poll_end),
modifier = Modifier.size(22.dp)
)
} else {
Icon(
resourceId = VectorIcons.Poll,
contentDescription = null,
contentDescription = stringResource(id = CommonStrings.a11y_poll),
modifier = Modifier.size(22.dp)
)
}
@ -103,27 +106,35 @@ internal fun PollTitle( @@ -103,27 +106,35 @@ internal fun PollTitle(
}
@Composable
internal fun PollAnswers(
private fun PollAnswers(
answerItems: ImmutableList<PollAnswerItem>,
onAnswerSelected: (PollAnswer) -> Unit,
modifier: Modifier = Modifier,
) {
answerItems.forEach { answerItem ->
PollAnswerView(
modifier = modifier,
answerItem = answerItem,
onClick = { onAnswerSelected(answerItem.answer) }
)
Column(
modifier = modifier.selectableGroup(),
verticalArrangement = Arrangement.spacedBy(16.dp),
) {
answerItems.forEach {
PollAnswerView(
answerItem = it,
modifier = Modifier
.selectable(
selected = it.isSelected,
enabled = it.isEnabled,
onClick = { onAnswerSelected(it.answer) },
role = Role.RadioButton,
),
)
}
}
}
@Composable
internal fun ColumnScope.DisclosedPollBottomNotice(
answerItems: ImmutableList<PollAnswerItem>,
private fun ColumnScope.DisclosedPollBottomNotice(
votesCount: Int,
modifier: Modifier = Modifier
) {
val votesCount = answerItems.sumOf { it.votesCount }
Text(
modifier = modifier.align(Alignment.End),
style = ElementTheme.typography.fontBodyXsRegular,
@ -133,7 +144,9 @@ internal fun ColumnScope.DisclosedPollBottomNotice( @@ -133,7 +144,9 @@ internal fun ColumnScope.DisclosedPollBottomNotice(
}
@Composable
fun ColumnScope.UndisclosedPollBottomNotice(modifier: Modifier = Modifier) {
private fun ColumnScope.UndisclosedPollBottomNotice(
modifier: Modifier = Modifier
) {
Text(
modifier = modifier
.align(Alignment.Start)

3
features/poll/impl/src/main/res/values-de/translations.xml

@ -4,8 +4,9 @@ @@ -4,8 +4,9 @@
<string name="screen_create_poll_anonymous_desc">"Ergebnisse erst nach Ende der Umfrage anzeigen"</string>
<string name="screen_create_poll_anonymous_headline">"Anonyme Umfrage"</string>
<string name="screen_create_poll_answer_hint">"Option %1$d"</string>
<string name="screen_create_poll_discard_confirmation">"Bist du sicher, dass du diese Umfrage verwerfen willst?"</string>
<string name="screen_create_poll_discard_confirmation">"Sind Sie sicher, dass Sie diese Umfrage verwerfen wollen?"</string>
<string name="screen_create_poll_discard_confirmation_title">"Umfrage verwerfen"</string>
<string name="screen_create_poll_question_desc">"Frage oder Thema"</string>
<string name="screen_create_poll_question_hint">"Worum geht es bei der Umfrage?"</string>
<string name="screen_create_poll_title">"Umfrage erstellen"</string>
</resources>

7
features/preferences/impl/build.gradle.kts

@ -44,10 +44,12 @@ dependencies { @@ -44,10 +44,12 @@ dependencies {
implementation(projects.libraries.preferences.api)
implementation(projects.libraries.testtags)
implementation(projects.libraries.uiStrings)
implementation(projects.libraries.matrixui)
implementation(projects.libraries.mediapickers.api)
implementation(projects.libraries.mediaupload.api)
implementation(projects.features.rageshake.api)
implementation(projects.features.analytics.api)
implementation(projects.features.ftue.api)
implementation(projects.libraries.matrixui)
implementation(projects.features.logout.api)
implementation(projects.services.analytics.api)
implementation(projects.services.toolbox.api)
@ -64,8 +66,11 @@ dependencies { @@ -64,8 +66,11 @@ dependencies {
testImplementation(libs.molecule.runtime)
testImplementation(libs.test.truth)
testImplementation(libs.test.turbine)
testImplementation(libs.test.mockk)
testImplementation(projects.libraries.matrix.test)
testImplementation(projects.libraries.featureflag.test)
testImplementation(projects.libraries.mediapickers.test)
testImplementation(projects.libraries.mediaupload.test)
testImplementation(projects.libraries.preferences.test)
testImplementation(projects.libraries.pushstore.test)
testImplementation(projects.features.rageshake.test)

13
features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/PreferencesFlowNode.kt

@ -38,10 +38,12 @@ import io.element.android.features.preferences.impl.developer.tracing.ConfigureT @@ -38,10 +38,12 @@ import io.element.android.features.preferences.impl.developer.tracing.ConfigureT
import io.element.android.features.preferences.impl.notifications.NotificationSettingsNode
import io.element.android.features.preferences.impl.notifications.edit.EditDefaultNotificationSettingNode
import io.element.android.features.preferences.impl.root.PreferencesRootNode
import io.element.android.features.preferences.impl.user.editprofile.EditUserProfileNode
import io.element.android.libraries.architecture.BackstackNode
import io.element.android.libraries.architecture.animation.rememberDefaultTransitionHandler
import io.element.android.libraries.architecture.createNode
import io.element.android.libraries.di.SessionScope
import io.element.android.libraries.matrix.api.user.MatrixUser
import kotlinx.parcelize.Parcelize
@ContributesNode(SessionScope::class)
@ -81,6 +83,9 @@ class PreferencesFlowNode @AssistedInject constructor( @@ -81,6 +83,9 @@ class PreferencesFlowNode @AssistedInject constructor(
@Parcelize
data class EditDefaultNotificationSetting(val isOneToOne: Boolean) : NavTarget
@Parcelize
data class UserProfile(val matrixUser: MatrixUser) : NavTarget
}
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
@ -114,6 +119,10 @@ class PreferencesFlowNode @AssistedInject constructor( @@ -114,6 +119,10 @@ class PreferencesFlowNode @AssistedInject constructor(
override fun onOpenAdvancedSettings() {
backstack.push(NavTarget.AdvancedSettings)
}
override fun onOpenUserProfile(matrixUser: MatrixUser) {
backstack.push(NavTarget.UserProfile(matrixUser))
}
}
createNode<PreferencesRootNode>(buildContext, plugins = listOf(callback))
}
@ -149,6 +158,10 @@ class PreferencesFlowNode @AssistedInject constructor( @@ -149,6 +158,10 @@ class PreferencesFlowNode @AssistedInject constructor(
NavTarget.AdvancedSettings -> {
createNode<AdvancedSettingsNode>(buildContext)
}
is NavTarget.UserProfile -> {
val inputs = EditUserProfileNode.Inputs(navTarget.matrixUser)
createNode<EditUserProfileNode>(buildContext, listOf(inputs))
}
}
}

9
features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootNode.kt

@ -29,6 +29,7 @@ import dagger.assisted.AssistedInject @@ -29,6 +29,7 @@ import dagger.assisted.AssistedInject
import io.element.android.anvilannotations.ContributesNode
import io.element.android.libraries.androidutils.browser.openUrlInChromeCustomTab
import io.element.android.libraries.di.SessionScope
import io.element.android.libraries.matrix.api.user.MatrixUser
import io.element.android.libraries.theme.ElementTheme
import timber.log.Timber
@ -47,6 +48,7 @@ class PreferencesRootNode @AssistedInject constructor( @@ -47,6 +48,7 @@ class PreferencesRootNode @AssistedInject constructor(
fun onOpenDeveloperSettings()
fun onOpenNotificationSettings()
fun onOpenAdvancedSettings()
fun onOpenUserProfile(matrixUser: MatrixUser)
}
private fun onOpenBugReport() {
@ -91,6 +93,10 @@ class PreferencesRootNode @AssistedInject constructor( @@ -91,6 +93,10 @@ class PreferencesRootNode @AssistedInject constructor(
plugins<Callback>().forEach { it.onOpenNotificationSettings() }
}
private fun onOpenUserProfile(matrixUser: MatrixUser) {
plugins<Callback>().forEach { it.onOpenUserProfile(matrixUser) }
}
@Composable
override fun View(modifier: Modifier) {
val state = presenter.present()
@ -108,7 +114,8 @@ class PreferencesRootNode @AssistedInject constructor( @@ -108,7 +114,8 @@ class PreferencesRootNode @AssistedInject constructor(
onOpenAdvancedSettings = this::onOpenAdvancedSettings,
onSuccessLogout = { onSuccessLogout(activity, it) },
onManageAccountClicked = { onManageAccountClicked(activity, it, isDark) },
onOpenNotificationSettings = this::onOpenNotificationSettings
onOpenNotificationSettings = this::onOpenNotificationSettings,
onOpenUserProfile = this::onOpenUserProfile,
)
}

10
features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootView.kt

@ -16,6 +16,7 @@ @@ -16,6 +16,7 @@
package io.element.android.features.preferences.impl.root
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
@ -62,6 +63,7 @@ fun PreferencesRootView( @@ -62,6 +63,7 @@ fun PreferencesRootView(
onOpenAdvancedSettings: () -> Unit,
onSuccessLogout: (logoutUrlResult: String?) -> Unit,
onOpenNotificationSettings: () -> Unit,
onOpenUserProfile: (MatrixUser) -> Unit,
modifier: Modifier = Modifier,
) {
val snackbarHostState = rememberSnackbarHostState(snackbarMessage = state.snackbarMessage)
@ -73,7 +75,12 @@ fun PreferencesRootView( @@ -73,7 +75,12 @@ fun PreferencesRootView(
title = stringResource(id = CommonStrings.common_settings),
snackbarHost = { SnackbarHost(snackbarHostState) }
) {
UserPreferences(state.myUser)
UserPreferences(
modifier = Modifier.clickable {
state.myUser?.let(onOpenUserProfile)
},
user = state.myUser,
)
if (state.showCompleteVerification) {
PreferenceText(
title = stringResource(id = CommonStrings.action_complete_verification),
@ -181,5 +188,6 @@ private fun ContentToPreview(matrixUser: MatrixUser) { @@ -181,5 +188,6 @@ private fun ContentToPreview(matrixUser: MatrixUser) {
onSuccessLogout = {},
onManageAccountClicked = {},
onOpenNotificationSettings = {},
onOpenUserProfile = {},
)
}

12
libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/log/LoggerTag.kt → features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/editprofile/EditUserProfileEvents.kt

@ -14,9 +14,13 @@ @@ -14,9 +14,13 @@
* limitations under the License.
*/
package io.element.android.libraries.push.impl.log
package io.element.android.features.preferences.impl.user.editprofile
import io.element.android.libraries.core.log.logger.LoggerTag
import io.element.android.libraries.matrix.ui.media.AvatarAction
internal val pushLoggerTag = LoggerTag("Push")
internal val notificationLoggerTag = LoggerTag("Notification", pushLoggerTag)
sealed interface EditUserProfileEvents {
data class HandleAvatarAction(val action: AvatarAction) : EditUserProfileEvents
data class UpdateDisplayName(val name: String) : EditUserProfileEvents
data object Save : EditUserProfileEvents
data object CancelSaveChanges : EditUserProfileEvents
}

56
features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/editprofile/EditUserProfileNode.kt

@ -0,0 +1,56 @@ @@ -0,0 +1,56 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.preferences.impl.user.editprofile
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import io.element.android.anvilannotations.ContributesNode
import io.element.android.libraries.architecture.NodeInputs
import io.element.android.libraries.architecture.inputs
import io.element.android.libraries.di.SessionScope
import io.element.android.libraries.matrix.api.user.MatrixUser
@ContributesNode(SessionScope::class)
class EditUserProfileNode @AssistedInject constructor(
@Assisted buildContext: BuildContext,
@Assisted plugins: List<Plugin>,
presenterFactory: EditUserProfilePresenter.Factory,
) : Node(buildContext, plugins = plugins) {
data class Inputs(
val matrixUser: MatrixUser
) : NodeInputs
val matrixUser = inputs<Inputs>().matrixUser
val presenter = presenterFactory.create(matrixUser)
@Composable
override fun View(modifier: Modifier) {
val state = presenter.present()
EditUserProfileView(
state = state,
onBackPressed = ::navigateUp,
onProfileEdited = ::navigateUp,
modifier = modifier
)
}
}

149
features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/editprofile/EditUserProfilePresenter.kt

@ -0,0 +1,149 @@ @@ -0,0 +1,149 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.preferences.impl.user.editprofile
import android.net.Uri
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.core.net.toUri
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import io.element.android.libraries.architecture.Async
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.architecture.runCatchingUpdatingState
import io.element.android.libraries.core.mimetype.MimeTypes
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.user.MatrixUser
import io.element.android.libraries.matrix.ui.media.AvatarAction
import io.element.android.libraries.mediapickers.api.PickerProvider
import io.element.android.libraries.mediaupload.api.MediaPreProcessor
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import timber.log.Timber
class EditUserProfilePresenter @AssistedInject constructor(
@Assisted private val matrixUser: MatrixUser,
private val matrixClient: MatrixClient,
private val mediaPickerProvider: PickerProvider,
private val mediaPreProcessor: MediaPreProcessor,
) : Presenter<EditUserProfileState> {
@AssistedFactory
interface Factory {
fun create(matrixUser: MatrixUser): EditUserProfilePresenter
}
@Composable
override fun present(): EditUserProfileState {
var userAvatarUri by rememberSaveable { mutableStateOf(matrixUser.avatarUrl?.let { Uri.parse(it) }) }
var userDisplayName by rememberSaveable { mutableStateOf(matrixUser.displayName) }
val cameraPhotoPicker = mediaPickerProvider.registerCameraPhotoPicker(
onResult = { uri -> if (uri != null) userAvatarUri = uri }
)
val galleryImagePicker = mediaPickerProvider.registerGalleryImagePicker(
onResult = { uri -> if (uri != null) userAvatarUri = uri }
)
val avatarActions by remember(userAvatarUri) {
derivedStateOf {
listOfNotNull(
AvatarAction.TakePhoto,
AvatarAction.ChoosePhoto,
AvatarAction.Remove.takeIf { userAvatarUri != null },
).toImmutableList()
}
}
val saveAction: MutableState<Async<Unit>> = remember { mutableStateOf(Async.Uninitialized) }
val localCoroutineScope = rememberCoroutineScope()
fun handleEvents(event: EditUserProfileEvents) {
when (event) {
is EditUserProfileEvents.Save -> localCoroutineScope.saveChanges(userDisplayName, userAvatarUri, matrixUser, saveAction)
is EditUserProfileEvents.HandleAvatarAction -> {
when (event.action) {
AvatarAction.ChoosePhoto -> galleryImagePicker.launch()
AvatarAction.TakePhoto -> cameraPhotoPicker.launch()
AvatarAction.Remove -> userAvatarUri = null
}
}
is EditUserProfileEvents.UpdateDisplayName -> userDisplayName = event.name
EditUserProfileEvents.CancelSaveChanges -> saveAction.value = Async.Uninitialized
}
}
val canSave = remember(userDisplayName, userAvatarUri) {
val hasProfileChanged = hasDisplayNameChanged(userDisplayName, matrixUser) ||
hasAvatarUrlChanged(userAvatarUri, matrixUser)
!userDisplayName.isNullOrBlank() && hasProfileChanged
}
return EditUserProfileState(
userId = matrixUser.userId,
displayName = userDisplayName.orEmpty(),
userAvatarUrl = userAvatarUri,
avatarActions = avatarActions,
saveButtonEnabled = canSave && saveAction.value !is Async.Loading,
saveAction = saveAction.value,
eventSink = { handleEvents(it) },
)
}
private fun hasDisplayNameChanged(name: String?, currentUser: MatrixUser) =
name?.trim() != currentUser.displayName?.trim()
private fun hasAvatarUrlChanged(avatarUri: Uri?, currentUser: MatrixUser) =
// Need to call `toUri()?.toString()` to make the test pass (we mockk Uri)
avatarUri?.toString()?.trim() != currentUser.avatarUrl?.toUri()?.toString()?.trim()
private fun CoroutineScope.saveChanges(name: String?, avatarUri: Uri?, currentUser: MatrixUser, action: MutableState<Async<Unit>>) = launch {
val results = mutableListOf<Result<Unit>>()
suspend {
if (!name.isNullOrEmpty() && name.trim() != currentUser.displayName.orEmpty().trim()) {
results.add(matrixClient.setDisplayName(name).onFailure {
Timber.e(it, "Failed to set user's display name")
})
}
if (avatarUri?.toString()?.trim() != currentUser.avatarUrl?.trim()) {
results.add(updateAvatar(avatarUri).onFailure {
Timber.e(it, "Failed to update user's avatar")
})
}
if (results.all { it.isSuccess }) Unit else results.first { it.isFailure }.getOrThrow()
}.runCatchingUpdatingState(action)
}
private suspend fun updateAvatar(avatarUri: Uri?): Result<Unit> {
return runCatching {
if (avatarUri != null) {
val preprocessed = mediaPreProcessor.process(avatarUri, MimeTypes.Jpeg, compressIfPossible = false).getOrThrow()
matrixClient.uploadAvatar(MimeTypes.Jpeg, preprocessed.file.readBytes()).getOrThrow()
} else {
matrixClient.removeAvatar().getOrThrow()
}
}.onFailure { Timber.e(it, "Unable to update avatar") }
}
}

33
features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/editprofile/EditUserProfileState.kt

@ -0,0 +1,33 @@ @@ -0,0 +1,33 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.preferences.impl.user.editprofile
import android.net.Uri
import io.element.android.libraries.architecture.Async
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.ui.media.AvatarAction
import kotlinx.collections.immutable.ImmutableList
data class EditUserProfileState(
val userId: UserId?,
val displayName: String,
val userAvatarUrl: Uri?,
val avatarActions: ImmutableList<AvatarAction>,
val saveButtonEnabled: Boolean,
val saveAction: Async<Unit>,
val eventSink: (EditUserProfileEvents) -> Unit
)

40
features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/editprofile/EditUserProfileStateProvider.kt

@ -0,0 +1,40 @@ @@ -0,0 +1,40 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.preferences.impl.user.editprofile
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.libraries.architecture.Async
import io.element.android.libraries.matrix.api.core.UserId
import kotlinx.collections.immutable.persistentListOf
open class EditUserProfileStateProvider : PreviewParameterProvider<EditUserProfileState> {
override val values: Sequence<EditUserProfileState>
get() = sequenceOf(
aEditUserProfileState(),
// Add other states here
)
}
fun aEditUserProfileState() = EditUserProfileState(
userId = UserId("@john.doe:matrix.org"),
displayName = "John Doe",
userAvatarUrl = null,
avatarActions = persistentListOf(),
saveAction = Async.Uninitialized,
saveButtonEnabled = true,
eventSink = {}
)

190
features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/editprofile/EditUserProfileView.kt

@ -0,0 +1,190 @@ @@ -0,0 +1,190 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.preferences.impl.user.editprofile
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.ModalBottomSheetValue
import androidx.compose.material.rememberModalBottomSheetState
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusManager
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
import io.element.android.features.preferences.impl.R
import io.element.android.libraries.architecture.Async
import io.element.android.libraries.designsystem.components.LabelledOutlinedTextField
import io.element.android.libraries.designsystem.components.ProgressDialog
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
import io.element.android.libraries.designsystem.components.button.BackButton
import io.element.android.libraries.designsystem.components.dialogs.ErrorDialog
import io.element.android.libraries.designsystem.preview.DayNightPreviews
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.theme.aliasScreenTitle
import io.element.android.libraries.designsystem.theme.components.Scaffold
import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.designsystem.theme.components.TextButton
import io.element.android.libraries.designsystem.theme.components.TopAppBar
import io.element.android.libraries.matrix.ui.components.AvatarActionBottomSheet
import io.element.android.libraries.matrix.ui.components.EditableAvatarView
import io.element.android.libraries.theme.ElementTheme
import io.element.android.libraries.ui.strings.CommonStrings
import kotlinx.coroutines.launch
@OptIn(ExperimentalMaterialApi::class, ExperimentalMaterial3Api::class)
@Composable
fun EditUserProfileView(
state: EditUserProfileState,
onBackPressed: () -> Unit,
onProfileEdited: () -> Unit,
modifier: Modifier = Modifier,
) {
val coroutineScope = rememberCoroutineScope()
val focusManager = LocalFocusManager.current
val itemActionsBottomSheetState = rememberModalBottomSheetState(
initialValue = ModalBottomSheetValue.Hidden,
)
fun onAvatarClicked() {
focusManager.clearFocus()
coroutineScope.launch {
itemActionsBottomSheetState.show()
}
}
Scaffold(
modifier = modifier.clearFocusOnTap(focusManager),
topBar = {
TopAppBar(
title = {
Text(
text = stringResource(R.string.screen_edit_profile_title),
style = ElementTheme.typography.aliasScreenTitle,
)
},
navigationIcon = { BackButton(onClick = onBackPressed) },
actions = {
TextButton(
text = stringResource(CommonStrings.action_save),
enabled = state.saveButtonEnabled,
onClick = {
focusManager.clearFocus()
state.eventSink(EditUserProfileEvents.Save)
},
)
}
)
},
) { padding ->
Column(
modifier = Modifier
.padding(padding)
.padding(horizontal = 16.dp)
.navigationBarsPadding()
.imePadding()
.verticalScroll(rememberScrollState())
) {
Spacer(modifier = Modifier.height(24.dp))
EditableAvatarView(
userId = state.userId?.value,
displayName = state.displayName,
avatarUrl = state.userAvatarUrl,
avatarSize = AvatarSize.RoomHeader,
onAvatarClicked = { onAvatarClicked() },
modifier = Modifier.align(Alignment.CenterHorizontally),
)
Spacer(modifier = Modifier.height(16.dp))
state.userId?.let {
Text(
modifier = Modifier.fillMaxWidth(),
text = it.value,
style = ElementTheme.typography.fontBodyLgRegular,
textAlign = TextAlign.Center,
)
}
Spacer(modifier = Modifier.height(40.dp))
LabelledOutlinedTextField(
label = stringResource(R.string.screen_edit_profile_display_name),
value = state.displayName,
placeholder = stringResource(CommonStrings.common_room_name_placeholder),
singleLine = true,
onValueChange = { state.eventSink(EditUserProfileEvents.UpdateDisplayName(it)) },
)
}
AvatarActionBottomSheet(
actions = state.avatarActions,
modalBottomSheetState = itemActionsBottomSheetState,
onActionSelected = { state.eventSink(EditUserProfileEvents.HandleAvatarAction(it)) }
)
when (state.saveAction) {
is Async.Loading -> {
ProgressDialog(text = stringResource(R.string.screen_edit_profile_updating_details))
}
is Async.Failure -> {
ErrorDialog(
title = stringResource(R.string.screen_edit_profile_error_title),
content = stringResource(R.string.screen_edit_profile_error),
onDismiss = { state.eventSink(EditUserProfileEvents.CancelSaveChanges) },
)
}
is Async.Success -> {
LaunchedEffect(state.saveAction) {
onProfileEdited()
}
}
else -> Unit
}
}
}
private fun Modifier.clearFocusOnTap(focusManager: FocusManager): Modifier =
pointerInput(Unit) {
detectTapGestures(onTap = {
focusManager.clearFocus()
})
}
@DayNightPreviews
@Composable
internal fun EditUserProfileViewPreview(@PreviewParameter(EditUserProfileStateProvider::class) state: EditUserProfileState) =
ElementPreview {
EditUserProfileView(
onBackPressed = {},
onProfileEdited = {},
state = state,
)
}

9
features/preferences/impl/src/main/res/values/localazy.xml

@ -0,0 +1,9 @@ @@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_edit_profile_display_name">"Display name"</string>
<string name="screen_edit_profile_display_name_placeholder">"Your display name"</string>
<string name="screen_edit_profile_error">"An unknown error was encountered and the information couldn\'t be changed."</string>
<string name="screen_edit_profile_error_title">"Unable to update profile"</string>
<string name="screen_edit_profile_title">"Edit profile"</string>
<string name="screen_edit_profile_updating_details">"Updating profile…"</string>
</resources>

429
features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/user/editprofile/EditUserProfilePresenterTest.kt

@ -0,0 +1,429 @@ @@ -0,0 +1,429 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.preferences.impl.user.editprofile
import android.net.Uri
import app.cash.molecule.RecompositionMode
import app.cash.molecule.moleculeFlow
import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.architecture.Async
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.user.MatrixUser
import io.element.android.libraries.matrix.test.AN_AVATAR_URL
import io.element.android.libraries.matrix.test.A_USER_ID
import io.element.android.libraries.matrix.test.FakeMatrixClient
import io.element.android.libraries.matrix.ui.components.aMatrixUser
import io.element.android.libraries.matrix.ui.media.AvatarAction
import io.element.android.libraries.mediapickers.test.FakePickerProvider
import io.element.android.libraries.mediaupload.api.MediaUploadInfo
import io.element.android.libraries.mediaupload.test.FakeMediaPreProcessor
import io.element.android.tests.testutils.WarmUpRule
import io.element.android.tests.testutils.consumeItemsUntilPredicate
import io.mockk.every
import io.mockk.mockk
import io.mockk.mockkStatic
import io.mockk.unmockkAll
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest
import org.junit.After
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import java.io.File
@ExperimentalCoroutinesApi
class EditUserProfilePresenterTest {
@get:Rule
val warmUpRule = WarmUpRule()
private lateinit var fakePickerProvider: FakePickerProvider
private lateinit var fakeMediaPreProcessor: FakeMediaPreProcessor
private val userAvatarUri: Uri = mockk()
private val anotherAvatarUri: Uri = mockk()
private val fakeFileContents = ByteArray(2)
@Before
fun setup() {
fakePickerProvider = FakePickerProvider()
fakeMediaPreProcessor = FakeMediaPreProcessor()
mockkStatic(Uri::class)
every { Uri.parse(AN_AVATAR_URL) } returns userAvatarUri
every { Uri.parse(ANOTHER_AVATAR_URL) } returns anotherAvatarUri
}
@After
fun tearDown() {
unmockkAll()
}
private fun createEditUserProfilePresenter(
matrixClient: MatrixClient = FakeMatrixClient(),
matrixUser: MatrixUser = aMatrixUser(),
): EditUserProfilePresenter {
return EditUserProfilePresenter(
matrixClient = matrixClient,
matrixUser = matrixUser,
mediaPickerProvider = fakePickerProvider,
mediaPreProcessor = fakeMediaPreProcessor,
)
}
@Test
fun `present - initial state is created from user info`() = runTest {
val user = aMatrixUser(avatarUrl = AN_AVATAR_URL)
val presenter = createEditUserProfilePresenter(matrixUser = user)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
val initialState = awaitItem()
assertThat(initialState.userId).isEqualTo(user.userId)
assertThat(initialState.displayName).isEqualTo(user.displayName)
assertThat(initialState.userAvatarUrl).isEqualTo(userAvatarUri)
assertThat(initialState.avatarActions).containsExactly(
AvatarAction.ChoosePhoto,
AvatarAction.TakePhoto,
AvatarAction.Remove
)
assertThat(initialState.saveButtonEnabled).isFalse()
assertThat(initialState.saveAction).isInstanceOf(Async.Uninitialized::class.java)
}
}
@Test
fun `present - updates state in response to changes`() = runTest {
val user = aMatrixUser(id = A_USER_ID.value, displayName = "Name", avatarUrl = AN_AVATAR_URL)
val presenter = createEditUserProfilePresenter(matrixUser = user)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
val initialState = awaitItem()
assertThat(initialState.displayName).isEqualTo("Name")
assertThat(initialState.userAvatarUrl).isEqualTo(userAvatarUri)
initialState.eventSink(EditUserProfileEvents.UpdateDisplayName("Name II"))
awaitItem().apply {
assertThat(displayName).isEqualTo("Name II")
assertThat(userAvatarUrl).isEqualTo(userAvatarUri)
}
initialState.eventSink(EditUserProfileEvents.UpdateDisplayName("Name III"))
awaitItem().apply {
assertThat(displayName).isEqualTo("Name III")
assertThat(userAvatarUrl).isEqualTo(userAvatarUri)
}
initialState.eventSink(EditUserProfileEvents.HandleAvatarAction(AvatarAction.Remove))
awaitItem().apply {
assertThat(displayName).isEqualTo("Name III")
assertThat(userAvatarUrl).isNull()
}
}
}
@Test
fun `present - obtains avatar uris from gallery`() = runTest {
val user = aMatrixUser(id = A_USER_ID.value, displayName = "Name", avatarUrl = AN_AVATAR_URL)
fakePickerProvider.givenResult(anotherAvatarUri)
val presenter = createEditUserProfilePresenter(matrixUser = user)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
val initialState = awaitItem()
assertThat(initialState.userAvatarUrl).isEqualTo(userAvatarUri)
initialState.eventSink(EditUserProfileEvents.HandleAvatarAction(AvatarAction.ChoosePhoto))
awaitItem().apply {
assertThat(userAvatarUrl).isEqualTo(anotherAvatarUri)
}
}
}
@Test
fun `present - obtains avatar uris from camera`() = runTest {
val user = aMatrixUser(id = A_USER_ID.value, displayName = "Name", avatarUrl = AN_AVATAR_URL)
fakePickerProvider.givenResult(anotherAvatarUri)
val presenter = createEditUserProfilePresenter(matrixUser = user)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
val initialState = awaitItem()
assertThat(initialState.userAvatarUrl).isEqualTo(userAvatarUri)
initialState.eventSink(EditUserProfileEvents.HandleAvatarAction(AvatarAction.TakePhoto))
awaitItem().apply {
assertThat(userAvatarUrl).isEqualTo(anotherAvatarUri)
}
}
}
@Test
fun `present - updates save button state`() = runTest {
val user = aMatrixUser(id = A_USER_ID.value, displayName = "Name", avatarUrl = AN_AVATAR_URL)
fakePickerProvider.givenResult(userAvatarUri)
val presenter = createEditUserProfilePresenter(matrixUser = user)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
val initialState = awaitItem()
assertThat(initialState.saveButtonEnabled).isFalse()
// Once a change is made, the save button is enabled
initialState.eventSink(EditUserProfileEvents.UpdateDisplayName("Name II"))
awaitItem().apply {
assertThat(saveButtonEnabled).isTrue()
}
// If it's reverted then the save disables again
initialState.eventSink(EditUserProfileEvents.UpdateDisplayName("Name"))
awaitItem().apply {
assertThat(saveButtonEnabled).isFalse()
}
// Make a change...
initialState.eventSink(EditUserProfileEvents.HandleAvatarAction(AvatarAction.Remove))
awaitItem().apply {
assertThat(saveButtonEnabled).isTrue()
}
// Revert it...
initialState.eventSink(EditUserProfileEvents.HandleAvatarAction(AvatarAction.ChoosePhoto))
awaitItem().apply {
assertThat(saveButtonEnabled).isFalse()
}
}
}
@Test
fun `present - updates save button state when initial values are null`() = runTest {
val user = aMatrixUser(id = A_USER_ID.value, displayName = "Name", avatarUrl = null)
fakePickerProvider.givenResult(userAvatarUri)
val presenter = createEditUserProfilePresenter(matrixUser = user)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
val initialState = awaitItem()
assertThat(initialState.saveButtonEnabled).isFalse()
// Once a change is made, the save button is enabled
initialState.eventSink(EditUserProfileEvents.UpdateDisplayName("Name II"))
awaitItem().apply {
assertThat(saveButtonEnabled).isTrue()
}
// If it's reverted then the save disables again
initialState.eventSink(EditUserProfileEvents.UpdateDisplayName("Name"))
awaitItem().apply {
assertThat(saveButtonEnabled).isFalse()
}
// Make a change...
initialState.eventSink(EditUserProfileEvents.HandleAvatarAction(AvatarAction.ChoosePhoto))
awaitItem().apply {
assertThat(saveButtonEnabled).isTrue()
}
// Revert it...
initialState.eventSink(EditUserProfileEvents.HandleAvatarAction(AvatarAction.Remove))
awaitItem().apply {
assertThat(saveButtonEnabled).isFalse()
}
}
}
@Test
fun `present - save changes room details if different`() = runTest {
val matrixClient = FakeMatrixClient()
val user = aMatrixUser(id = A_USER_ID.value, displayName = "Name", avatarUrl = AN_AVATAR_URL)
val presenter = createEditUserProfilePresenter(
matrixClient = matrixClient,
matrixUser = user
)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
val initialState = awaitItem()
initialState.eventSink(EditUserProfileEvents.UpdateDisplayName("New name"))
initialState.eventSink(EditUserProfileEvents.HandleAvatarAction(AvatarAction.Remove))
initialState.eventSink(EditUserProfileEvents.Save)
consumeItemsUntilPredicate { matrixClient.setDisplayNameCalled && matrixClient.removeAvatarCalled && !matrixClient.uploadAvatarCalled }
assertThat(matrixClient.setDisplayNameCalled).isTrue()
assertThat(matrixClient.removeAvatarCalled).isTrue()
assertThat(matrixClient.uploadAvatarCalled).isFalse()
cancelAndIgnoreRemainingEvents()
}
}
@Test
fun `present - save does not change room details if they're the same trimmed`() = runTest {
val matrixClient = FakeMatrixClient()
val user = aMatrixUser(id = A_USER_ID.value, displayName = "Name", avatarUrl = AN_AVATAR_URL)
val presenter = createEditUserProfilePresenter(
matrixClient = matrixClient,
matrixUser = user
)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
val initialState = awaitItem()
initialState.eventSink(EditUserProfileEvents.UpdateDisplayName(" Name "))
initialState.eventSink(EditUserProfileEvents.Save)
consumeItemsUntilPredicate { matrixClient.setDisplayNameCalled && !matrixClient.removeAvatarCalled && !matrixClient.uploadAvatarCalled }
assertThat(matrixClient.setDisplayNameCalled).isFalse()
assertThat(matrixClient.uploadAvatarCalled).isFalse()
assertThat(matrixClient.removeAvatarCalled).isFalse()
cancelAndIgnoreRemainingEvents()
}
}
@Test
fun `present - save does not change name if it's now empty`() = runTest {
val matrixClient = FakeMatrixClient()
val user = aMatrixUser(id = A_USER_ID.value, displayName = "Name", avatarUrl = AN_AVATAR_URL)
val presenter = createEditUserProfilePresenter(
matrixClient = matrixClient,
matrixUser = user
)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
val initialState = awaitItem()
initialState.eventSink(EditUserProfileEvents.UpdateDisplayName(""))
initialState.eventSink(EditUserProfileEvents.Save)
assertThat(matrixClient.setDisplayNameCalled).isFalse()
assertThat(matrixClient.uploadAvatarCalled).isFalse()
assertThat(matrixClient.removeAvatarCalled).isFalse()
cancelAndIgnoreRemainingEvents()
}
}
@Test
fun `present - save processes and sets avatar when processor returns successfully`() = runTest {
val matrixClient = FakeMatrixClient()
val user = aMatrixUser(id = A_USER_ID.value, displayName = "Name", avatarUrl = AN_AVATAR_URL)
givenPickerReturnsFile()
val presenter = createEditUserProfilePresenter(
matrixClient = matrixClient,
matrixUser = user
)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
val initialState = awaitItem()
initialState.eventSink(EditUserProfileEvents.HandleAvatarAction(AvatarAction.ChoosePhoto))
initialState.eventSink(EditUserProfileEvents.Save)
consumeItemsUntilPredicate { matrixClient.uploadAvatarCalled }
assertThat(matrixClient.uploadAvatarCalled).isTrue()
}
}
@Test
fun `present - save does not set avatar data if processor fails`() = runTest {
val matrixClient = FakeMatrixClient()
val user = aMatrixUser(id = A_USER_ID.value, displayName = "Name", avatarUrl = AN_AVATAR_URL)
val presenter = createEditUserProfilePresenter(
matrixClient = matrixClient,
matrixUser = user
)
fakePickerProvider.givenResult(anotherAvatarUri)
fakeMediaPreProcessor.givenResult(Result.failure(Throwable("Oh no")))
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
val initialState = awaitItem()
initialState.eventSink(EditUserProfileEvents.HandleAvatarAction(AvatarAction.ChoosePhoto))
initialState.eventSink(EditUserProfileEvents.Save)
skipItems(2)
assertThat(matrixClient.uploadAvatarCalled).isFalse()
assertThat(awaitItem().saveAction).isInstanceOf(Async.Failure::class.java)
}
}
@Test
fun `present - sets save action to failure if name update fails`() = runTest {
val user = aMatrixUser(id = A_USER_ID.value, displayName = "Name", avatarUrl = AN_AVATAR_URL)
val matrixClient = FakeMatrixClient().apply {
givenSetDisplayNameResult(Result.failure(Throwable("!")))
}
saveAndAssertFailure(user, matrixClient, EditUserProfileEvents.UpdateDisplayName("New name"))
}
@Test
fun `present - sets save action to failure if removing avatar fails`() = runTest {
val user = aMatrixUser(id = A_USER_ID.value, displayName = "Name", avatarUrl = AN_AVATAR_URL)
val matrixClient = FakeMatrixClient().apply {
givenRemoveAvatarResult(Result.failure(Throwable("!")))
}
saveAndAssertFailure(user, matrixClient, EditUserProfileEvents.HandleAvatarAction(AvatarAction.Remove))
}
@Test
fun `present - sets save action to failure if setting avatar fails`() = runTest {
givenPickerReturnsFile()
val user = aMatrixUser(id = A_USER_ID.value, displayName = "Name", avatarUrl = AN_AVATAR_URL)
val matrixClient = FakeMatrixClient().apply {
givenUploadAvatarResult(Result.failure(Throwable("!")))
}
saveAndAssertFailure(user, matrixClient, EditUserProfileEvents.HandleAvatarAction(AvatarAction.ChoosePhoto))
}
@Test
fun `present - CancelSaveChanges resets save action state`() = runTest {
givenPickerReturnsFile()
val user = aMatrixUser(id = A_USER_ID.value, displayName = "Name", avatarUrl = AN_AVATAR_URL)
val matrixClient = FakeMatrixClient().apply {
givenSetDisplayNameResult(Result.failure(Throwable("!")))
}
val presenter = createEditUserProfilePresenter(matrixUser = user, matrixClient = matrixClient)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
val initialState = awaitItem()
initialState.eventSink(EditUserProfileEvents.UpdateDisplayName("foo"))
initialState.eventSink(EditUserProfileEvents.Save)
skipItems(2)
assertThat(awaitItem().saveAction).isInstanceOf(Async.Failure::class.java)
initialState.eventSink(EditUserProfileEvents.CancelSaveChanges)
assertThat(awaitItem().saveAction).isInstanceOf(Async.Uninitialized::class.java)
}
}
private suspend fun saveAndAssertFailure(matrixUser: MatrixUser, matrixClient: MatrixClient, event: EditUserProfileEvents) {
val presenter = createEditUserProfilePresenter(matrixUser = matrixUser, matrixClient = matrixClient)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
val initialState = awaitItem()
initialState.eventSink(event)
initialState.eventSink(EditUserProfileEvents.Save)
skipItems(1)
assertThat(awaitItem().saveAction).isInstanceOf(Async.Loading::class.java)
assertThat(awaitItem().saveAction).isInstanceOf(Async.Failure::class.java)
}
}
private fun givenPickerReturnsFile() {
mockkStatic(File::readBytes)
val processedFile: File = mockk {
every { readBytes() } returns fakeFileContents
}
fakePickerProvider.givenResult(anotherAvatarUri)
fakeMediaPreProcessor.givenResult(
Result.success(
MediaUploadInfo.AnyFile(
file = processedFile,
fileInfo = mockk(),
)
)
)
}
companion object {
private const val ANOTHER_AVATAR_URL = "example://camera/foo.jpg"
}
}

5
features/rageshake/api/src/main/res/values-de/translations.xml

@ -0,0 +1,5 @@ @@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="crash_detection_dialog_content">"%1$s ist bei der letzten Nutzung abgestürzt. Möchten Sie einen Absturzbericht mit uns teilen?"</string>
<string name="rageshake_detection_dialog_content">"Sie scheinen das Telefon aus Frustration zu schütteln. Möchten Sie den Bildschirm für den Fehlerbericht öffnen?"</string>
</resources>

15
features/rageshake/impl/src/main/res/values-de/translations.xml

@ -0,0 +1,15 @@ @@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_bug_report_attach_screenshot">"Bildschirmfoto anhängen"</string>
<string name="screen_bug_report_contact_me">"Sie können mich kontaktieren, wenn Sie weitere Fragen haben."</string>
<string name="screen_bug_report_contact_me_title">"Kontaktieren Sie mich"</string>
<string name="screen_bug_report_edit_screenshot">"Bildschirmfoto bearbeiten"</string>
<string name="screen_bug_report_editor_description">"Bitte beschreiben Sie den Fehler. Was haben Sie getan? Was haben Sie erwartet, was passiert? Was ist tatsächlich passiert. Bitte gehen Sie so detailliert wie möglich vor."</string>
<string name="screen_bug_report_editor_placeholder">"Beschreiben Sie den Fehler…"</string>
<string name="screen_bug_report_editor_supporting">"Wenn möglich, verfassen Sie die Beschreibung bitte auf Englisch."</string>
<string name="screen_bug_report_include_crash_logs">"Absturzprotokolle senden"</string>
<string name="screen_bug_report_include_logs">"Protokolle zulassen"</string>
<string name="screen_bug_report_include_screenshot">"Bildschirmfoto senden"</string>
<string name="screen_bug_report_logs_description">"Die Protokolle werden Ihrer Nachricht beigefügt, um sicherzustellen, dass alles ordnungsgemäß funktioniert. Um Ihre Nachricht ohne Protokolle zu senden, deaktivieren Sie diese Einstellung."</string>
<string name="screen_bug_report_rash_logs_alert_title">"%1$s ist bei der letzten Nutzung abgestürzt. Möchten Sie einen Absturzbericht mit uns teilen?"</string>
</resources>

74
features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/edit/RoomDetailsEditView.kt

@ -18,37 +18,27 @@ @@ -18,37 +18,27 @@
package io.element.android.features.roomdetails.impl.edit
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.ModalBottomSheetValue
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.AddAPhoto
import androidx.compose.material.rememberModalBottomSheetState
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.focus.FocusManager
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalFocusManager
@ -61,21 +51,18 @@ import io.element.android.features.roomdetails.impl.R @@ -61,21 +51,18 @@ import io.element.android.features.roomdetails.impl.R
import io.element.android.libraries.architecture.Async
import io.element.android.libraries.designsystem.components.LabelledTextField
import io.element.android.libraries.designsystem.components.ProgressDialog
import io.element.android.libraries.designsystem.components.avatar.Avatar
import io.element.android.libraries.designsystem.components.avatar.AvatarData
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
import io.element.android.libraries.designsystem.components.button.BackButton
import io.element.android.libraries.designsystem.components.dialogs.ErrorDialog
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
import io.element.android.libraries.designsystem.theme.aliasScreenTitle
import io.element.android.libraries.designsystem.theme.components.Icon
import io.element.android.libraries.designsystem.theme.components.Scaffold
import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.designsystem.theme.components.TextButton
import io.element.android.libraries.designsystem.theme.components.TopAppBar
import io.element.android.libraries.matrix.ui.components.AvatarActionBottomSheet
import io.element.android.libraries.matrix.ui.components.UnsavedAvatar
import io.element.android.libraries.matrix.ui.components.EditableAvatarView
import io.element.android.libraries.theme.ElementTheme
import io.element.android.libraries.ui.strings.CommonStrings
import kotlinx.coroutines.launch
@ -134,7 +121,14 @@ fun RoomDetailsEditView( @@ -134,7 +121,14 @@ fun RoomDetailsEditView(
.verticalScroll(rememberScrollState())
) {
Spacer(modifier = Modifier.height(24.dp))
EditableAvatarView(state, ::onAvatarClicked)
EditableAvatarView(
userId = state.roomId,
displayName = state.roomName,
avatarUrl = state.roomAvatarUrl,
avatarSize = AvatarSize.EditRoomDetails,
onAvatarClicked = ::onAvatarClicked,
modifier = Modifier.fillMaxWidth(),
)
Spacer(modifier = Modifier.height(60.dp))
if (state.canChangeName) {
@ -202,56 +196,6 @@ fun RoomDetailsEditView( @@ -202,56 +196,6 @@ fun RoomDetailsEditView(
}
}
@Composable
private fun EditableAvatarView(
state: RoomDetailsEditState,
onAvatarClicked: () -> Unit,
modifier: Modifier = Modifier,
) {
Column(modifier = modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally) {
Box(
modifier = Modifier
.size(70.dp)
.clickable(onClick = onAvatarClicked, enabled = state.canChangeAvatar)
) {
// TODO this might be able to be simplified into a single component once send/receive media is done
when (state.roomAvatarUrl?.scheme) {
null, "mxc" -> {
Avatar(
avatarData = AvatarData(state.roomId, state.roomName, state.roomAvatarUrl?.toString(), size = AvatarSize.RoomHeader),
modifier = Modifier.fillMaxSize(),
)
}
else -> {
UnsavedAvatar(
avatarUri = state.roomAvatarUrl,
modifier = Modifier.fillMaxSize(),
)
}
}
if (state.canChangeAvatar) {
Box(
modifier = Modifier
.align(Alignment.BottomEnd)
.clip(CircleShape)
.background(MaterialTheme.colorScheme.primary)
.size(24.dp),
contentAlignment = Alignment.Center,
) {
Icon(
modifier = Modifier.size(16.dp),
imageVector = Icons.Outlined.AddAPhoto,
contentDescription = "",
tint = MaterialTheme.colorScheme.onPrimary,
)
}
}
}
}
}
@Composable
private fun LabelledReadOnlyField(
title: String,

46
features/roomdetails/impl/src/main/res/values-de/translations.xml

@ -1,6 +1,50 @@ @@ -1,6 +1,50 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<plurals name="screen_room_member_list_header_title">
<item quantity="one">"%1$d Person"</item>
<item quantity="other">"%1$d Personen"</item>
</plurals>
<string name="screen_room_details_add_topic_title">"Thema hinzufügen"</string>
<string name="screen_room_details_already_a_member">"Bereits Mitglied"</string>
<string name="screen_room_details_already_invited">"Bereits eingeladen"</string>
<string name="screen_room_details_edit_room_title">"Raum bearbeiten"</string>
<string name="screen_room_details_edition_error">"Es ist ein unbekannter Fehler aufgetreten und die Informationen konnten nicht geändert werden."</string>
<string name="screen_room_details_edition_error_title">"Raum kann nicht aktualisiert werden"</string>
<string name="screen_room_details_encryption_enabled_subtitle">"Nachrichten sind mit Schlössern gesichert. Nur Sie und die Empfänger haben die eindeutigen Schlüssel, um sie zu entsperren."</string>
<string name="screen_room_details_encryption_enabled_title">"Nachrichtenverschlüsselung aktiviert"</string>
<string name="screen_room_details_error_loading_notification_settings">"Beim Laden der Benachrichtigungseinstellungen ist ein Fehler aufgetreten."</string>
<string name="screen_room_details_error_muting">"Die Stummschaltung dieses Raums ist fehlgeschlagen, bitte versuchen Sie es erneut."</string>
<string name="screen_room_details_error_unmuting">"Die Deaktivierung der Stummschaltung dieses Raums ist fehlgeschlagen, bitte versuchen Sie es erneut."</string>
<string name="screen_room_details_invite_people_title">"Personen einladen"</string>
<string name="screen_room_details_notification_mode_custom">"Benutzerdefiniert"</string>
<string name="screen_room_details_notification_mode_default">"Standard"</string>
<string name="screen_room_details_notification_title">"Benachrichtigungen"</string>
<string name="screen_room_details_room_name_label">"Raumname"</string>
<string name="screen_room_details_share_room_title">"Raum teilen"</string>
<string name="screen_room_details_updating_room">"Raum wird aktualisiert…"</string>
<string name="screen_room_member_list_pending_header_title">"Ausstehend"</string>
<string name="screen_room_member_list_room_members_header_title">"Raummitglieder"</string>
<string name="screen_room_notification_settings_allow_custom">"Benutzerdefinierte Einstellung zulassen"</string>
<string name="screen_room_notification_settings_allow_custom_footnote">"Wenn Sie diese Option aktivieren, wird Ihre Standardeinstellung außer Kraft gesetzt."</string>
<string name="screen_room_notification_settings_custom_settings_title">"Benachrichtigen Sie mich in diesem Chat bei"</string>
<string name="screen_room_notification_settings_default_setting_footnote">"Sie können das in Ihrem %1$s ändern."</string>
<string name="screen_room_notification_settings_default_setting_footnote_content_link">"Globale Einstellungen"</string>
<string name="screen_room_notification_settings_default_setting_title">"Standardeinstellung"</string>
<string name="screen_room_notification_settings_edit_remove_setting">"Benutzerdefinierte Einstellung entfernen"</string>
<string name="screen_room_notification_settings_error_loading_settings">"Beim Laden der Benachrichtigungseinstellungen ist ein Fehler aufgetreten."</string>
<string name="screen_room_notification_settings_error_restoring_default">"Fehler beim Wiederherstellen des Standardmodus. Bitte versuchen Sie es erneut."</string>
<string name="screen_room_notification_settings_error_setting_mode">"Fehler beim Einstellen des Modus. Bitte versuchen Sie es erneut."</string>
<string name="screen_room_notification_settings_mode_all_messages">"Alle Nachrichten"</string>
<string name="screen_room_notification_settings_mode_mentions_and_keywords">"Nur Erwähnungen und Schlüsselwörter"</string>
<string name="screen_room_notification_settings_room_custom_settings_title">"Benachrichtigen Sie mich in diesem Raum bei"</string>
<string name="screen_dm_details_block_alert_action">"Sperren"</string>
<string name="screen_dm_details_block_alert_description">"Gesperrte Benutzer können Ihnen keine Nachrichten senden und alle ihre Nachrichten werden ausgeblendet. Sie können sie jederzeit entsperren."</string>
<string name="screen_dm_details_block_user">"Benutzer sperren"</string>
<string name="screen_dm_details_unblock_alert_action">"Entsperren"</string>
<string name="screen_dm_details_unblock_alert_description">"Sie können dann wieder alle Nachrichten von ihnen sehen."</string>
<string name="screen_dm_details_unblock_user">"Benutzer entsperren"</string>
<string name="screen_room_details_leave_room_title">"Raum verlassen"</string>
<string name="screen_room_details_people_title">"Menschen"</string>
<string name="screen_room_details_people_title">"Personen"</string>
<string name="screen_room_details_security_title">"Sicherheit"</string>
<string name="screen_room_details_topic_title">"Thema"</string>
</resources>

9
features/roomlist/impl/src/main/res/values-de/translations.xml

@ -0,0 +1,9 @@ @@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_roomlist_a11y_create_message">"Eine neue Unterhaltung oder einen neuen Raum erstellen"</string>
<string name="screen_roomlist_empty_message">"Beginnen Sie, indem Sie jemandem eine Nachricht senden."</string>
<string name="screen_roomlist_empty_title">"Noch keine Chats."</string>
<string name="screen_roomlist_main_space_title">"Alle Chats"</string>
<string name="session_verification_banner_message">"Es sieht aus, als würden Sie ein neues Gerät verwenden. Verifizieren Sie es mit einem anderen Gerät, damit Sie auf Ihre verschlüsselten Nachrichten zugreifen können."</string>
<string name="session_verification_banner_title">"Bestätigen Sie Ihre Identität"</string>
</resources>

15
features/verifysession/impl/src/main/res/values-de/translations.xml

@ -1,4 +1,19 @@ @@ -1,4 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_session_verification_cancelled_subtitle">"Etwas scheint nicht zu stimmen. Entweder ist das Zeitlimit für die Anfrage abgelaufen oder die Anfrage wurde abgelehnt."</string>
<string name="screen_session_verification_compare_emojis_subtitle">"Vergewissern Sie sich, dass die folgenden Emojis mit denen in Ihrer anderen Session übereinstimmen."</string>
<string name="screen_session_verification_compare_emojis_title">"Emojis vergleichen"</string>
<string name="screen_session_verification_complete_subtitle">"Ihre neue Session ist nun verifiziert. Sie hat Zugriff auf Ihre verschlüsselten Nachrichten und wird von anderen Benutzern als vertrauenswürdig eingestuft."</string>
<string name="screen_session_verification_open_existing_session_subtitle">"Beweisen Sie Ihre Identität, um auf Ihren verschlüsselten Nachrichtenverlauf zuzugreifen."</string>
<string name="screen_session_verification_open_existing_session_title">"Öffnen Sie eine bestehende Sitzung"</string>
<string name="screen_session_verification_positive_button_canceled">"Verifizierung wiederholen"</string>
<string name="screen_session_verification_positive_button_initial">"Ich bin bereit"</string>
<string name="screen_session_verification_positive_button_verifying_ongoing">"Warten auf eine Übereinstimmung"</string>
<string name="screen_session_verification_request_accepted_subtitle">"Vergleichen Sie die einzelnen Emojis und stellen Sie sicher, dass sie in der gleichen Reihenfolge erscheinen."</string>
<string name="screen_session_verification_they_dont_match">"Sie stimmen nicht überein"</string>
<string name="screen_session_verification_they_match">"Sie stimmen überein"</string>
<string name="screen_session_verification_waiting_to_accept_subtitle">"Akzeptieren Sie die Anfrage, um den Verifizierungsprozess in Ihrer anderen Session zu starten, um fortzufahren."</string>
<string name="screen_session_verification_waiting_to_accept_title">"Warten auf die Annahme der Anfrage"</string>
<string name="screen_session_verification_cancelled_title">"Verifizierung abgebrochen"</string>
<string name="screen_session_verification_positive_button_ready">"Start"</string>
</resources>

4
gradle/libs.versions.toml

@ -66,7 +66,7 @@ android_gradle_plugin = { module = "com.android.tools.build:gradle", version.ref @@ -66,7 +66,7 @@ android_gradle_plugin = { module = "com.android.tools.build:gradle", version.ref
android_desugar = "com.android.tools:desugar_jdk_libs:2.0.3"
kotlin_gradle_plugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" }
# https://firebase.google.com/docs/android/setup#available-libraries
google_firebase_bom = "com.google.firebase:firebase-bom:32.3.0"
google_firebase_bom = "com.google.firebase:firebase-bom:32.3.1"
# AndroidX
androidx_material = { module = "com.google.android.material:material", version.ref = "material" }
@ -150,7 +150,7 @@ jsoup = { module = "org.jsoup:jsoup", version.ref = "jsoup" } @@ -150,7 +150,7 @@ jsoup = { module = "org.jsoup:jsoup", version.ref = "jsoup" }
appyx_core = { module = "com.bumble.appyx:core", version.ref = "appyx" }
molecule-runtime = { module = "app.cash.molecule:molecule-runtime", version.ref = "molecule" }
timber = "com.jakewharton.timber:timber:5.0.1"
matrix_sdk = "org.matrix.rustcomponents:sdk-android:0.1.53"
matrix_sdk = "org.matrix.rustcomponents:sdk-android:0.1.54"
matrix_richtexteditor = { module = "io.element.android:wysiwyg", version.ref = "wysiwyg" }
matrix_richtexteditor_compose = { module = "io.element.android:wysiwyg-compose", version.ref = "wysiwyg" }
sqldelight-driver-android = { module = "com.squareup.sqldelight:android-driver", version.ref = "sqldelight" }

1
libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/file/File.kt

@ -25,6 +25,7 @@ import java.util.Locale @@ -25,6 +25,7 @@ import java.util.Locale
import java.util.UUID
fun File.safeDelete() {
if (exists().not()) return
tryOrNull(
onError = {
Timber.e(it, "Error, unable to delete file $path")

24
libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/ui/View.kt

@ -17,8 +17,11 @@ @@ -17,8 +17,11 @@
package io.element.android.libraries.androidutils.ui
import android.view.View
import android.view.ViewTreeObserver
import android.view.inputmethod.InputMethodManager
import androidx.core.content.getSystemService
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlin.coroutines.resume
fun View.hideKeyboard() {
val imm = context?.getSystemService<InputMethodManager>()
@ -41,3 +44,24 @@ fun View.setHorizontalPadding(padding: Int) { @@ -41,3 +44,24 @@ fun View.setHorizontalPadding(padding: Int) {
paddingBottom
)
}
suspend fun View.awaitWindowFocus() = suspendCancellableCoroutine { continuation ->
if (hasWindowFocus()) {
continuation.resume(Unit)
} else {
val listener = object : ViewTreeObserver.OnWindowFocusChangeListener {
override fun onWindowFocusChanged(hasFocus: Boolean) {
if (hasFocus) {
viewTreeObserver.removeOnWindowFocusChangeListener(this)
continuation.resume(Unit)
}
}
}
viewTreeObserver.addOnWindowFocusChangeListener(listener)
continuation.invokeOnCancellation {
viewTreeObserver.removeOnWindowFocusChangeListener(listener)
}
}
}

4
libraries/androidutils/src/main/res/values-de/translations.xml

@ -0,0 +1,4 @@ @@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="error_no_compatible_app_found">"Für diese Aktion wurde keine kompatible App gefunden."</string>
</resources>

6
libraries/core/src/main/kotlin/io/element/android/libraries/core/log/logger/LoggerTag.kt

@ -24,10 +24,8 @@ package io.element.android.libraries.core.log.logger @@ -24,10 +24,8 @@ package io.element.android.libraries.core.log.logger
*/
open class LoggerTag(name: String, parentTag: LoggerTag? = null) {
object SYNC : LoggerTag("SYNC")
object VOIP : LoggerTag("VOIP")
object CRYPTO : LoggerTag("CRYPTO")
object RENDEZVOUS : LoggerTag("RZ")
object PushLoggerTag : LoggerTag("Push")
object NotificationLoggerTag : LoggerTag("Notification", PushLoggerTag)
val value: String = if (parentTag == null) {
name

84
libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/LabelledOutlinedTextField.kt

@ -0,0 +1,84 @@ @@ -0,0 +1,84 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.libraries.designsystem.components
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import io.element.android.libraries.designsystem.preview.DayNightPreviews
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.theme.components.OutlinedTextField
import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.theme.ElementTheme
@Composable
fun LabelledOutlinedTextField(
label: String,
value: String,
modifier: Modifier = Modifier,
placeholder: String? = null,
singleLine: Boolean = false,
maxLines: Int = if (singleLine) 1 else Int.MAX_VALUE,
keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
onValueChange: (String) -> Unit = {},
) {
Column(
modifier = modifier,
verticalArrangement = Arrangement.spacedBy(8.dp),
) {
Text(
modifier = Modifier.padding(horizontal = 16.dp),
style = ElementTheme.typography.fontBodyMdRegular,
color = MaterialTheme.colorScheme.primary,
text = label
)
OutlinedTextField(
modifier = Modifier.fillMaxWidth(),
value = value,
placeholder = placeholder?.let { { Text(placeholder) } },
onValueChange = onValueChange,
singleLine = singleLine,
maxLines = maxLines,
keyboardOptions = keyboardOptions,
)
}
}
@DayNightPreviews
@Composable
internal fun LabelledOutlinedTextFieldPreview() = ElementPreview {
Column {
LabelledOutlinedTextField(
label = "Room name",
value = "",
placeholder = "e.g. Product Sprint",
)
LabelledOutlinedTextField(
label = "Room name",
value = "a room name",
placeholder = "e.g. Product Sprint",
)
}
}

2
libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/AvatarSize.kt

@ -43,5 +43,7 @@ enum class AvatarSize(val dp: Dp) { @@ -43,5 +43,7 @@ enum class AvatarSize(val dp: Dp) {
RoomInviteItem(52.dp),
InviteSender(16.dp),
EditRoomDetails(70.dp),
NotificationsOptIn(32.dp),
}

54
libraries/eventformatter/impl/src/main/res/values-de/translations.xml

@ -1,5 +1,57 @@ @@ -1,5 +1,57 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="state_event_avatar_changed_too">"(Avatar wurde auch geändert)"</string>
<string name="state_event_avatar_url_changed">"%1$s hat den Avatar geändert"</string>
<string name="state_event_avatar_url_changed_by_you">"Sie haben Ihren Avatar geändert"</string>
<string name="state_event_display_name_changed_from">"%1$s hat den Anzeigenamen von %2$s auf %3$s geändert"</string>
<string name="state_event_display_name_changed_from_by_you">"Sie haben Ihren Anzeigenamen von %1$s auf %2$s geändert"</string>
<string name="state_event_display_name_removed">"%1$s hat den Anzeigenamen entfernt (war %2$s)"</string>
<string name="state_event_display_name_removed_by_you">"Sie haben Ihren Anzeigenamen entfernt (war %1$s)"</string>
<string name="state_event_display_name_set">"%1$s setzen ihren Anzeigenamen auf %2$s"</string>
<string name="state_event_display_name_set_by_you">"Sie haben Ihren Anzeigenamen zu %1$s geändert"</string>
<string name="state_event_room_avatar_changed">"%1$s hat den Raum-Avatar geändert"</string>
<string name="state_event_room_avatar_changed_by_you">"Sie haben den Raum-Avatar geändert"</string>
<string name="state_event_room_avatar_removed">"%1$s hat den Raum-Avatar entfernt"</string>
<string name="state_event_room_avatar_removed_by_you">"Sie haben den Raum-Avatar entfernt"</string>
<string name="state_event_room_ban">"%1$s hat %2$s gesperrt"</string>
<string name="state_event_room_ban_by_you">"Sie haben %1$s gesperrt"</string>
<string name="state_event_room_created">"%1$s hat den Raum erstellt"</string>
<string name="state_event_room_created_by_you">"Du hast den Raum erstellt"</string>
<string name="state_event_room_created_by_you">"Sie haben den Raum erstellt"</string>
<string name="state_event_room_invite">"%1$s hat %2$s eingeladen"</string>
<string name="state_event_room_invite_accepted">"%1$s hat die Einladung angenommen"</string>
<string name="state_event_room_invite_accepted_by_you">"Sie haben die Einladung angenommen"</string>
<string name="state_event_room_invite_by_you">"Sie haben %1$s eingeladen"</string>
<string name="state_event_room_invite_you">"%1$s hat dich eingeladen"</string>
<string name="state_event_room_join">"%1$s hat den Raum betreten"</string>
<string name="state_event_room_join_by_you">"Sie haben den Raum betreten"</string>
<string name="state_event_room_knock">"%1$s hat angefragt beizutreten"</string>
<string name="state_event_room_knock_accepted">"%1$s hat %2$s den Beitritt erlaubt"</string>
<string name="state_event_room_knock_accepted_by_you">"%1$s hat Ihnen den Betritt erlaubt"</string>
<string name="state_event_room_knock_by_you">"Sie haben angefragt beizutreten"</string>
<string name="state_event_room_knock_denied">"%1$s hat die Beitrittsanfrage von %2$s abgelehnt"</string>
<string name="state_event_room_knock_denied_by_you">"Sie haben die Beitrittsanfrage von %1$s abgelehnt"</string>
<string name="state_event_room_knock_denied_you">"%1$s hat Ihre Beitrittsanfrage abgelehnt"</string>
<string name="state_event_room_knock_retracted">"%1$s ist nicht mehr an einem Beitritt interessiert"</string>
<string name="state_event_room_knock_retracted_by_you">"Sie haben Ihre Beitrittsanfrage zurückgezogen"</string>
<string name="state_event_room_leave">"%1$s hat den Raum verlassen"</string>
<string name="state_event_room_leave_by_you">"Sie haben den Raum verlassen"</string>
<string name="state_event_room_name_changed">"%1$s hat den Raumnamen geändert in: %2$s"</string>
<string name="state_event_room_name_changed_by_you">"Sie haben den Raumnamen geändert in: %1$s"</string>
<string name="state_event_room_name_removed">"%1$s hat den Raumnamen entfernt"</string>
<string name="state_event_room_name_removed_by_you">"Sie haben den Raumnamen entfernt"</string>
<string name="state_event_room_reject">"%1$s hat die Einladung abgelehnt"</string>
<string name="state_event_room_reject_by_you">"Sie haben die Einladung abgelehnt"</string>
<string name="state_event_room_remove">"%1$s hat %2$s entfernt"</string>
<string name="state_event_room_remove_by_you">"Sie haben %1$s entfernt"</string>
<string name="state_event_room_third_party_invite">"%1$s hat eine Einladung an %2$s gesendet, dem Raum beizutreten"</string>
<string name="state_event_room_third_party_invite_by_you">"Sie haben eine Einladung an %1$s gesendet, dem Raum beizutreten"</string>
<string name="state_event_room_third_party_revoked_invite">"%1$s hat die Einladung an %2$s zum Betreten des Raums zurückgezogen"</string>
<string name="state_event_room_third_party_revoked_invite_by_you">"Sie haben die Einladung an %1$s zum Betreten des Raums zurückgezogen"</string>
<string name="state_event_room_topic_changed">"%1$s hat das Thema geändert in: %2$s"</string>
<string name="state_event_room_topic_changed_by_you">"Sie haben das Thema geändert in: %1$s"</string>
<string name="state_event_room_topic_removed">"%1$s hat das Raumthema entfernt"</string>
<string name="state_event_room_topic_removed_by_you">"Sie haben das Raumthema entfernt"</string>
<string name="state_event_room_unban">"%1$s hat die Sperre für %2$s aufgehoben"</string>
<string name="state_event_room_unban_by_you">"Sie haben die Sperre für %1$s aufgehoben"</string>
<string name="state_event_room_unknown_membership_change">"%1$s hat eine unbekannte Raumänderung vorgenommen"</string>
</resources>

3
libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClient.kt

@ -47,6 +47,9 @@ interface MatrixClient : Closeable { @@ -47,6 +47,9 @@ interface MatrixClient : Closeable {
suspend fun createDM(userId: UserId): Result<RoomId>
suspend fun getProfile(userId: UserId): Result<MatrixUser>
suspend fun searchUsers(searchTerm: String, limit: Long): Result<MatrixSearchUserResults>
suspend fun setDisplayName(displayName: String): Result<Unit>
suspend fun uploadAvatar(mimeType: String, data: ByteArray): Result<Unit>
suspend fun removeAvatar(): Result<Unit>
fun syncService(): SyncService
fun sessionVerificationService(): SessionVerificationService
fun pushersService(): PushersService

17
libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt

@ -276,6 +276,23 @@ class RustMatrixClient constructor( @@ -276,6 +276,23 @@ class RustMatrixClient constructor(
}
}
override suspend fun setDisplayName(displayName: String): Result<Unit> =
withContext(sessionDispatcher) {
runCatching { client.setDisplayName(displayName) }
}
@OptIn(ExperimentalUnsignedTypes::class)
override suspend fun uploadAvatar(mimeType: String, data: ByteArray): Result<Unit> =
withContext(sessionDispatcher) {
runCatching { client.uploadAvatar(mimeType, data.toUByteArray().toList()) }
}
override suspend fun removeAvatar(): Result<Unit> =
withContext(sessionDispatcher) {
runCatching { client.removeAvatar() }
}
override fun syncService(): SyncService = rustSyncService
override fun sessionVerificationService(): SessionVerificationService = verificationService

5
libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomSummaryListProcessor.kt

@ -111,6 +111,9 @@ class RoomSummaryListProcessor( @@ -111,6 +111,9 @@ class RoomSummaryListProcessor(
RoomListEntriesUpdate.Clear -> {
clear()
}
is RoomListEntriesUpdate.Truncate -> {
subList(update.length.toInt(), size).clear()
}
}
}
@ -119,7 +122,7 @@ class RoomSummaryListProcessor( @@ -119,7 +122,7 @@ class RoomSummaryListProcessor(
RoomListEntry.Empty -> buildEmptyRoomSummary()
is RoomListEntry.Filled -> buildAndCacheRoomSummaryForIdentifier(entry.roomId)
is RoomListEntry.Invalidated -> {
roomSummariesByIdentifier[entry.roomId] ?: buildEmptyRoomSummary()
roomSummariesByIdentifier[entry.roomId] ?: buildAndCacheRoomSummaryForIdentifier(entry.roomId)
}
}
}

3
libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/MatrixTimelineDiffProcessor.kt

@ -98,6 +98,9 @@ internal class MatrixTimelineDiffProcessor( @@ -98,6 +98,9 @@ internal class MatrixTimelineDiffProcessor(
TimelineChange.CLEAR -> {
clear()
}
TimelineChange.TRUNCATE -> {
// Not supported
}
}
}

2
libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/tracing/RustTracingTree.kt

@ -49,7 +49,7 @@ internal class RustTracingTree(private val retrieveFromStackTrace: Boolean) : Ti @@ -49,7 +49,7 @@ internal class RustTracingTree(private val retrieveFromStackTrace: Boolean) : Ti
line = location.line,
level = logLevel,
target = Target.ELEMENT.filter,
message = message,
message = if (tag != null) "[$tag] $message" else message,
)
}

38
libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt

@ -58,6 +58,13 @@ class FakeMatrixClient( @@ -58,6 +58,13 @@ class FakeMatrixClient(
private val accountManagementUrlString: Result<String?> = Result.success(null),
) : MatrixClient {
var setDisplayNameCalled: Boolean = false
private set
var uploadAvatarCalled: Boolean = false
private set
var removeAvatarCalled: Boolean = false
private set
private var ignoreUserResult: Result<Unit> = Result.success(Unit)
private var unignoreUserResult: Result<Unit> = Result.success(Unit)
private var createRoomResult: Result<RoomId> = Result.success(A_ROOM_ID)
@ -69,6 +76,9 @@ class FakeMatrixClient( @@ -69,6 +76,9 @@ class FakeMatrixClient(
private val searchUserResults = mutableMapOf<String, Result<MatrixSearchUserResults>>()
private val getProfileResults = mutableMapOf<UserId, Result<MatrixUser>>()
private var uploadMediaResult: Result<String> = Result.success(AN_AVATAR_URL)
private var setDisplayNameResult: Result<Unit> = Result.success(Unit)
private var uploadAvatarResult: Result<Unit> = Result.success(Unit)
private var removeAvatarResult: Result<Unit> = Result.success(Unit)
override suspend fun getRoom(roomId: RoomId): MatrixRoom? {
return getRoomResults[roomId]
@ -133,6 +143,7 @@ class FakeMatrixClient( @@ -133,6 +143,7 @@ class FakeMatrixClient(
override suspend fun getAccountManagementUrl(action: AccountManagementAction?): Result<String?> {
return accountManagementUrlString
}
override suspend fun uploadMedia(
mimeType: String,
data: ByteArray,
@ -141,6 +152,21 @@ class FakeMatrixClient( @@ -141,6 +152,21 @@ class FakeMatrixClient(
return uploadMediaResult
}
override suspend fun setDisplayName(displayName: String): Result<Unit> = simulateLongTask {
setDisplayNameCalled = true
return setDisplayNameResult
}
override suspend fun uploadAvatar(mimeType: String, data: ByteArray): Result<Unit> = simulateLongTask {
uploadAvatarCalled = true
return uploadAvatarResult
}
override suspend fun removeAvatar(): Result<Unit> = simulateLongTask {
removeAvatarCalled = true
return removeAvatarResult
}
override fun sessionVerificationService(): SessionVerificationService = sessionVerificationService
override fun pushersService(): PushersService = pushersService
@ -197,4 +223,16 @@ class FakeMatrixClient( @@ -197,4 +223,16 @@ class FakeMatrixClient(
fun givenUploadMediaResult(result: Result<String>) {
uploadMediaResult = result
}
fun givenSetDisplayNameResult(result: Result<Unit>) {
setDisplayNameResult = result
}
fun givenUploadAvatarResult(result: Result<Unit>) {
uploadAvatarResult = result
}
fun givenRemoveAvatarResult(result: Result<Unit>) {
removeAvatarResult = result
}
}

97
libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/EditableAvatarView.kt

@ -0,0 +1,97 @@ @@ -0,0 +1,97 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.libraries.matrix.ui.components
import android.net.Uri
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.AddAPhoto
import androidx.compose.material.ripple.rememberRipple
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.unit.dp
import io.element.android.libraries.designsystem.components.avatar.Avatar
import io.element.android.libraries.designsystem.components.avatar.AvatarData
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
import io.element.android.libraries.designsystem.theme.components.Icon
@Composable
fun EditableAvatarView(
userId: String?,
displayName: String?,
avatarUrl: Uri?,
avatarSize: AvatarSize,
onAvatarClicked: () -> Unit,
modifier: Modifier = Modifier,
) {
Column(modifier = modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally) {
Box(
modifier = Modifier
.size(avatarSize.dp)
.clickable(
interactionSource = remember { MutableInteractionSource() },
onClick = onAvatarClicked,
indication = rememberRipple(bounded = false),
)
) {
when (avatarUrl?.scheme) {
null, "mxc" -> {
userId?.let {
Avatar(
avatarData = AvatarData(it, displayName, avatarUrl?.toString(), size = avatarSize),
modifier = Modifier.fillMaxSize(),
)
}
}
else -> {
UnsavedAvatar(
avatarUri = avatarUrl,
modifier = Modifier.fillMaxSize(),
)
}
}
Box(
modifier = Modifier
.align(Alignment.BottomEnd)
.clip(CircleShape)
.background(MaterialTheme.colorScheme.primary)
.size(24.dp),
contentAlignment = Alignment.Center,
) {
Icon(
modifier = Modifier.size(16.dp),
imageVector = Icons.Outlined.AddAPhoto,
contentDescription = "",
tint = MaterialTheme.colorScheme.onPrimary,
)
}
}
}
}

7
libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/MatrixUserProvider.kt

@ -28,9 +28,14 @@ open class MatrixUserProvider : PreviewParameterProvider<MatrixUser> { @@ -28,9 +28,14 @@ open class MatrixUserProvider : PreviewParameterProvider<MatrixUser> {
)
}
fun aMatrixUser(id: String = "@id_of_alice:server.org", displayName: String = "Alice") = MatrixUser(
fun aMatrixUser(
id: String = "@id_of_alice:server.org",
displayName: String = "Alice",
avatarUrl: String? = null,
) = MatrixUser(
userId = UserId(id),
displayName = displayName,
avatarUrl = avatarUrl,
)
fun aMatrixUserList() = listOf(

2
libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/notifications/NotificationDrawerManager.kt

@ -21,5 +21,5 @@ import io.element.android.libraries.matrix.api.core.SessionId @@ -21,5 +21,5 @@ import io.element.android.libraries.matrix.api.core.SessionId
interface NotificationDrawerManager {
fun clearMembershipNotificationForSession(sessionId: SessionId)
fun clearMembershipNotificationForRoom(sessionId: SessionId, roomId: RoomId)
fun clearMembershipNotificationForRoom(sessionId: SessionId, roomId: RoomId, doRender: Boolean)
}

3
libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/PushersManager.kt

@ -24,7 +24,6 @@ import io.element.android.libraries.matrix.api.core.EventId @@ -24,7 +24,6 @@ import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.matrix.api.pusher.SetHttpPusherData
import io.element.android.libraries.push.impl.config.PushConfig
import io.element.android.libraries.push.impl.log.pushLoggerTag
import io.element.android.libraries.push.impl.pushgateway.PushGatewayNotifyRequest
import io.element.android.libraries.pushproviders.api.PusherSubscriber
import io.element.android.libraries.pushstore.api.UserPushStoreFactory
@ -35,7 +34,7 @@ import javax.inject.Inject @@ -35,7 +34,7 @@ import javax.inject.Inject
internal const val DEFAULT_PUSHER_FILE_TAG = "mobile"
private val loggerTag = LoggerTag("PushersManager", pushLoggerTag)
private val loggerTag = LoggerTag("PushersManager", LoggerTag.PushLoggerTag)
@ContributesBinding(AppScope::class)
class PushersManager @Inject constructor(

76
libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotificationDrawerManager.kt

@ -20,6 +20,7 @@ import io.element.android.libraries.androidutils.throttler.FirstThrottler @@ -20,6 +20,7 @@ import io.element.android.libraries.androidutils.throttler.FirstThrottler
import io.element.android.libraries.core.cache.CircularCache
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.core.data.tryOrNull
import io.element.android.libraries.core.log.logger.LoggerTag
import io.element.android.libraries.core.meta.BuildMeta
import io.element.android.libraries.di.AppScope
import io.element.android.libraries.di.SingleIn
@ -41,6 +42,8 @@ import kotlinx.coroutines.withContext @@ -41,6 +42,8 @@ import kotlinx.coroutines.withContext
import timber.log.Timber
import javax.inject.Inject
private val loggerTag = LoggerTag("DefaultNotificationDrawerManager", LoggerTag.NotificationLoggerTag)
/**
* The NotificationDrawerManager receives notification events as they arrived (from event stream or fcm) and
* organise them in order to display them in the notification drawer.
@ -89,7 +92,11 @@ class DefaultNotificationDrawerManager @Inject constructor( @@ -89,7 +92,11 @@ class DefaultNotificationDrawerManager @Inject constructor(
is NavigationState.Space -> {}
is NavigationState.Room -> {
// Cleanup notification for current room
clearMessagesForRoom(navigationState.parentSpace.parentSession.sessionId, navigationState.roomId)
clearMessagesForRoom(
sessionId = navigationState.parentSpace.parentSession.sessionId,
roomId = navigationState.roomId,
doRender = true,
)
}
is NavigationState.Thread -> {
onEnteringThread(
@ -112,13 +119,13 @@ class DefaultNotificationDrawerManager @Inject constructor( @@ -112,13 +119,13 @@ class DefaultNotificationDrawerManager @Inject constructor(
private fun NotificationEventQueue.onNotifiableEventReceived(notifiableEvent: NotifiableEvent) {
if (buildMeta.lowPrivacyLoggingEnabled) {
Timber.d("onNotifiableEventReceived(): $notifiableEvent")
Timber.tag(loggerTag.value).d("onNotifiableEventReceived(): $notifiableEvent")
} else {
Timber.d("onNotifiableEventReceived(): is push: ${notifiableEvent.canBeReplaced}")
Timber.tag(loggerTag.value).d("onNotifiableEventReceived(): is push: ${notifiableEvent.canBeReplaced}")
}
if (filteredEventDetector.shouldBeIgnored(notifiableEvent)) {
Timber.d("onNotifiableEventReceived(): ignore the event")
Timber.tag(loggerTag.value).d("onNotifiableEventReceived(): ignore the event")
return
}
@ -132,7 +139,7 @@ class DefaultNotificationDrawerManager @Inject constructor( @@ -132,7 +139,7 @@ class DefaultNotificationDrawerManager @Inject constructor(
* Events might be grouped and there might not be one notification per event!
*/
fun onNotifiableEventReceived(notifiableEvent: NotifiableEvent) {
updateEvents {
updateEvents(doRender = true) {
it.onNotifiableEventReceived(notifiableEvent)
}
}
@ -140,8 +147,8 @@ class DefaultNotificationDrawerManager @Inject constructor( @@ -140,8 +147,8 @@ class DefaultNotificationDrawerManager @Inject constructor(
/**
* Clear all known events and refresh the notification drawer.
*/
fun clearAllMessagesEvents(sessionId: SessionId) {
updateEvents {
fun clearAllMessagesEvents(sessionId: SessionId, doRender: Boolean) {
updateEvents(doRender = doRender) {
it.clearMessagesForSession(sessionId)
}
}
@ -150,7 +157,7 @@ class DefaultNotificationDrawerManager @Inject constructor( @@ -150,7 +157,7 @@ class DefaultNotificationDrawerManager @Inject constructor(
* Clear all notifications related to the session and refresh the notification drawer.
*/
fun clearAllEvents(sessionId: SessionId) {
updateEvents {
updateEvents(doRender = true) {
it.clearAllForSession(sessionId)
}
}
@ -160,14 +167,14 @@ class DefaultNotificationDrawerManager @Inject constructor( @@ -160,14 +167,14 @@ class DefaultNotificationDrawerManager @Inject constructor(
* Used to ignore events related to that room (no need to display notification) and clean any existing notification on this room.
* Can also be called when a notification for this room is dismissed by the user.
*/
fun clearMessagesForRoom(sessionId: SessionId, roomId: RoomId) {
updateEvents {
fun clearMessagesForRoom(sessionId: SessionId, roomId: RoomId, doRender: Boolean) {
updateEvents(doRender = doRender) {
it.clearMessagesForRoom(sessionId, roomId)
}
}
override fun clearMembershipNotificationForSession(sessionId: SessionId) {
updateEvents {
updateEvents(doRender = true) {
it.clearMembershipNotificationForSession(sessionId)
}
}
@ -175,8 +182,12 @@ class DefaultNotificationDrawerManager @Inject constructor( @@ -175,8 +182,12 @@ class DefaultNotificationDrawerManager @Inject constructor(
/**
* Clear invitation notification for the provided room.
*/
override fun clearMembershipNotificationForRoom(sessionId: SessionId, roomId: RoomId) {
updateEvents {
override fun clearMembershipNotificationForRoom(
sessionId: SessionId,
roomId: RoomId,
doRender: Boolean,
) {
updateEvents(doRender = doRender) {
it.clearMembershipNotificationForRoom(sessionId, roomId)
}
}
@ -184,8 +195,8 @@ class DefaultNotificationDrawerManager @Inject constructor( @@ -184,8 +195,8 @@ class DefaultNotificationDrawerManager @Inject constructor(
/**
* Clear the notifications for a single event.
*/
fun clearEvent(eventId: EventId) {
updateEvents {
fun clearEvent(eventId: EventId, doRender: Boolean) {
updateEvents(doRender = doRender) {
it.clearEvent(eventId)
}
}
@ -195,14 +206,14 @@ class DefaultNotificationDrawerManager @Inject constructor( @@ -195,14 +206,14 @@ class DefaultNotificationDrawerManager @Inject constructor(
* Used to ignore events related to that thread (no need to display notification) and clean any existing notification on this room.
*/
private fun onEnteringThread(sessionId: SessionId, roomId: RoomId, threadId: ThreadId) {
updateEvents {
updateEvents(doRender = true) {
it.clearMessagesForThread(sessionId, roomId, threadId)
}
}
// TODO EAx Must be per account
fun notificationStyleChanged() {
updateEvents {
updateEvents(doRender = true) {
val newSettings = true // pushDataStore.useCompleteNotificationFormat()
if (newSettings != useCompleteNotificationFormat) {
// Settings has changed, remove all current notifications
@ -212,41 +223,46 @@ class DefaultNotificationDrawerManager @Inject constructor( @@ -212,41 +223,46 @@ class DefaultNotificationDrawerManager @Inject constructor(
}
}
private fun updateEvents(action: DefaultNotificationDrawerManager.(NotificationEventQueue) -> Unit) {
notificationState.updateQueuedEvents(this) { queuedEvents, _ ->
private fun updateEvents(
doRender: Boolean,
action: (NotificationEventQueue) -> Unit,
) {
notificationState.updateQueuedEvents { queuedEvents, _ ->
action(queuedEvents)
}
coroutineScope.refreshNotificationDrawer()
coroutineScope.refreshNotificationDrawer(doRender)
}
private fun CoroutineScope.refreshNotificationDrawer() = launch {
private fun CoroutineScope.refreshNotificationDrawer(doRender: Boolean) = launch {
// Implement last throttler
val canHandle = firstThrottler.canHandle()
Timber.v("refreshNotificationDrawer(), delay: ${canHandle.waitMillis()} ms")
Timber.tag(loggerTag.value).v("refreshNotificationDrawer($doRender), delay: ${canHandle.waitMillis()} ms")
withContext(dispatchers.io) {
delay(canHandle.waitMillis())
try {
refreshNotificationDrawerBg()
refreshNotificationDrawerBg(doRender)
} catch (throwable: Throwable) {
// It can happen if for instance session has been destroyed. It's a bit ugly to try catch like this, but it's safer
Timber.w(throwable, "refreshNotificationDrawerBg failure")
Timber.tag(loggerTag.value).w(throwable, "refreshNotificationDrawerBg failure")
}
}
}
private suspend fun refreshNotificationDrawerBg() {
Timber.v("refreshNotificationDrawerBg()")
val eventsToRender = notificationState.updateQueuedEvents(this) { queuedEvents, renderedEvents ->
private suspend fun refreshNotificationDrawerBg(doRender: Boolean) {
Timber.tag(loggerTag.value).v("refreshNotificationDrawerBg($doRender)")
val eventsToRender = notificationState.updateQueuedEvents { queuedEvents, renderedEvents ->
notifiableEventProcessor.process(queuedEvents.rawEvents(), renderedEvents).also {
queuedEvents.clearAndAdd(it.onlyKeptEvents())
}
}
if (notificationState.hasAlreadyRendered(eventsToRender)) {
Timber.d("Skipping notification update due to event list not changing")
Timber.tag(loggerTag.value).d("Skipping notification update due to event list not changing")
} else {
notificationState.clearAndAddRenderedEvents(eventsToRender)
renderEvents(eventsToRender)
if (doRender) {
renderEvents(eventsToRender)
}
persistEvents()
}
}
@ -265,7 +281,7 @@ class DefaultNotificationDrawerManager @Inject constructor( @@ -265,7 +281,7 @@ class DefaultNotificationDrawerManager @Inject constructor(
eventsForSessions.forEach { (sessionId, notifiableEvents) ->
val currentUser = tryOrNull(
onError = { Timber.e(it, "Unable to retrieve info for user ${sessionId.value}") },
onError = { Timber.tag(loggerTag.value).e(it, "Unable to retrieve info for user ${sessionId.value}") },
operation = {
val client = matrixClientProvider.getOrRestore(sessionId).getOrThrow()
// myUserDisplayName cannot be empty else NotificationCompat.MessagingStyle() will crash

9
libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotifiableEventProcessor.kt

@ -16,6 +16,7 @@ @@ -16,6 +16,7 @@
package io.element.android.libraries.push.impl.notifications
import io.element.android.libraries.core.log.logger.LoggerTag
import io.element.android.libraries.matrix.api.timeline.item.event.EventType
import io.element.android.libraries.push.impl.notifications.model.FallbackNotifiableEvent
import io.element.android.libraries.push.impl.notifications.model.InviteNotifiableEvent
@ -29,6 +30,8 @@ import javax.inject.Inject @@ -29,6 +30,8 @@ import javax.inject.Inject
private typealias ProcessedEvents = List<ProcessedEvent<NotifiableEvent>>
private val loggerTag = LoggerTag("NotifiableEventProcessor", LoggerTag.NotificationLoggerTag)
class NotifiableEventProcessor @Inject constructor(
private val outdatedDetector: OutdatedEventDetector,
private val appNavigationStateService: AppNavigationStateService,
@ -45,10 +48,10 @@ class NotifiableEventProcessor @Inject constructor( @@ -45,10 +48,10 @@ class NotifiableEventProcessor @Inject constructor(
is NotifiableMessageEvent -> when {
it.shouldIgnoreEventInRoom(appState) -> {
ProcessedEvent.Type.REMOVE
.also { Timber.d("notification message removed due to currently viewing the same room or thread") }
.also { Timber.tag(loggerTag.value).d("notification message removed due to currently viewing the same room or thread") }
}
outdatedDetector.isMessageOutdated(it) -> ProcessedEvent.Type.REMOVE
.also { Timber.d("notification message removed due to being read") }
.also { Timber.tag(loggerTag.value).d("notification message removed due to being read") }
else -> ProcessedEvent.Type.KEEP
}
is SimpleNotifiableEvent -> when (it.type) {
@ -58,7 +61,7 @@ class NotifiableEventProcessor @Inject constructor( @@ -58,7 +61,7 @@ class NotifiableEventProcessor @Inject constructor(
is FallbackNotifiableEvent -> when {
it.shouldIgnoreEventInRoom(appState) -> {
ProcessedEvent.Type.REMOVE
.also { Timber.d("notification fallback removed due to currently viewing the same room or thread") }
.also { Timber.tag(loggerTag.value).d("notification fallback removed due to currently viewing the same room or thread") }
}
else -> ProcessedEvent.Type.KEEP
}

3
libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotifiableEventResolver.kt

@ -36,7 +36,6 @@ import io.element.android.libraries.matrix.api.timeline.item.event.TextMessageTy @@ -36,7 +36,6 @@ import io.element.android.libraries.matrix.api.timeline.item.event.TextMessageTy
import io.element.android.libraries.matrix.api.timeline.item.event.UnknownMessageType
import io.element.android.libraries.matrix.api.timeline.item.event.VideoMessageType
import io.element.android.libraries.push.impl.R
import io.element.android.libraries.push.impl.log.pushLoggerTag
import io.element.android.libraries.push.impl.notifications.model.FallbackNotifiableEvent
import io.element.android.libraries.push.impl.notifications.model.InviteNotifiableEvent
import io.element.android.libraries.push.impl.notifications.model.NotifiableEvent
@ -47,7 +46,7 @@ import io.element.android.services.toolbox.api.systemclock.SystemClock @@ -47,7 +46,7 @@ import io.element.android.services.toolbox.api.systemclock.SystemClock
import timber.log.Timber
import javax.inject.Inject
private val loggerTag = LoggerTag("NotifiableEventResolver", pushLoggerTag)
private val loggerTag = LoggerTag("NotifiableEventResolver", LoggerTag.NotificationLoggerTag)
/**
* The notifiable event resolver is able to create a NotifiableEvent (view model for notifications) from an sdk Event.

19
libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationBroadcastReceiver.kt

@ -24,11 +24,10 @@ import io.element.android.libraries.core.log.logger.LoggerTag @@ -24,11 +24,10 @@ import io.element.android.libraries.core.log.logger.LoggerTag
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.push.impl.log.notificationLoggerTag
import timber.log.Timber
import javax.inject.Inject
private val loggerTag = LoggerTag("NotificationBroadcastReceiver", notificationLoggerTag)
private val loggerTag = LoggerTag("NotificationBroadcastReceiver", LoggerTag.NotificationLoggerTag)
/**
* Receives actions broadcast by notification (on click, on dismiss, inline replies, etc.).
@ -41,34 +40,34 @@ class NotificationBroadcastReceiver : BroadcastReceiver() { @@ -41,34 +40,34 @@ class NotificationBroadcastReceiver : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
if (intent == null || context == null) return
context.bindings<NotificationBroadcastReceiverBindings>().inject(this)
Timber.tag(loggerTag.value).v("NotificationBroadcastReceiver received : $intent")
val sessionId = intent.extras?.getString(KEY_SESSION_ID)?.let(::SessionId) ?: return
val roomId = intent.getStringExtra(KEY_ROOM_ID)?.let(::RoomId)
val eventId = intent.getStringExtra(KEY_EVENT_ID)?.let(::EventId)
Timber.tag(loggerTag.value).d("onReceive: ${intent.action} ${intent.data} for: ${roomId?.value}/${eventId?.value}")
when (intent.action) {
actionIds.smartReply ->
handleSmartReply(intent, context)
actionIds.dismissRoom -> if (roomId != null) {
defaultNotificationDrawerManager.clearMessagesForRoom(sessionId, roomId)
defaultNotificationDrawerManager.clearMessagesForRoom(sessionId, roomId, doRender = false)
}
actionIds.dismissSummary ->
defaultNotificationDrawerManager.clearAllMessagesEvents(sessionId)
defaultNotificationDrawerManager.clearAllMessagesEvents(sessionId, doRender = false)
actionIds.dismissInvite -> if (roomId != null) {
defaultNotificationDrawerManager.clearMembershipNotificationForRoom(sessionId, roomId)
defaultNotificationDrawerManager.clearMembershipNotificationForRoom(sessionId, roomId, doRender = false)
}
actionIds.dismissEvent -> if (eventId != null) {
defaultNotificationDrawerManager.clearEvent(eventId)
defaultNotificationDrawerManager.clearEvent(eventId, doRender = false)
}
actionIds.markRoomRead -> if (roomId != null) {
defaultNotificationDrawerManager.clearMessagesForRoom(sessionId, roomId)
defaultNotificationDrawerManager.clearMessagesForRoom(sessionId, roomId, doRender = true)
handleMarkAsRead(sessionId, roomId)
}
actionIds.join -> if (roomId != null) {
defaultNotificationDrawerManager.clearMembershipNotificationForRoom(sessionId, roomId)
defaultNotificationDrawerManager.clearMembershipNotificationForRoom(sessionId, roomId, doRender = true)
handleJoinRoom(sessionId, roomId)
}
actionIds.reject -> if (roomId != null) {
defaultNotificationDrawerManager.clearMembershipNotificationForRoom(sessionId, roomId)
defaultNotificationDrawerManager.clearMembershipNotificationForRoom(sessionId, roomId, doRender = true)
handleRejectRoom(sessionId, roomId)
}
}

3
libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationEventPersistence.kt

@ -22,7 +22,6 @@ import io.element.android.libraries.androidutils.file.safeDelete @@ -22,7 +22,6 @@ import io.element.android.libraries.androidutils.file.safeDelete
import io.element.android.libraries.core.data.tryOrNull
import io.element.android.libraries.core.log.logger.LoggerTag
import io.element.android.libraries.di.ApplicationContext
import io.element.android.libraries.push.impl.log.notificationLoggerTag
import io.element.android.libraries.push.impl.notifications.model.NotifiableEvent
import timber.log.Timber
import java.io.File
@ -33,7 +32,7 @@ import javax.inject.Inject @@ -33,7 +32,7 @@ import javax.inject.Inject
private const val ROOMS_NOTIFICATIONS_FILE_NAME_LEGACY = "im.vector.notifications.cache"
private const val FILE_NAME = "notifications.bin"
private val loggerTag = LoggerTag("NotificationEventPersistence", notificationLoggerTag)
private val loggerTag = LoggerTag("NotificationEventPersistence", LoggerTag.NotificationLoggerTag)
class NotificationEventPersistence @Inject constructor(
@ApplicationContext private val context: Context,

23
libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationRenderer.kt

@ -16,6 +16,7 @@ @@ -16,6 +16,7 @@
package io.element.android.libraries.push.impl.notifications
import io.element.android.libraries.core.log.logger.LoggerTag
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.user.MatrixUser
import io.element.android.libraries.push.impl.notifications.model.FallbackNotifiableEvent
@ -26,6 +27,8 @@ import io.element.android.libraries.push.impl.notifications.model.SimpleNotifiab @@ -26,6 +27,8 @@ import io.element.android.libraries.push.impl.notifications.model.SimpleNotifiab
import timber.log.Timber
import javax.inject.Inject
private val loggerTag = LoggerTag("NotificationRenderer", LoggerTag.NotificationLoggerTag)
class NotificationRenderer @Inject constructor(
private val notificationIdProvider: NotificationIdProvider,
private val notificationDisplayer: NotificationDisplayer,
@ -54,7 +57,7 @@ class NotificationRenderer @Inject constructor( @@ -54,7 +57,7 @@ class NotificationRenderer @Inject constructor(
// Remove summary first to avoid briefly displaying it after dismissing the last notification
if (summaryNotification == SummaryNotification.Removed) {
Timber.d("Removing summary notification")
Timber.tag(loggerTag.value).d("Removing summary notification")
notificationDisplayer.cancelNotificationMessage(
tag = null,
id = notificationIdProvider.getSummaryNotificationId(currentUser.userId)
@ -64,14 +67,14 @@ class NotificationRenderer @Inject constructor( @@ -64,14 +67,14 @@ class NotificationRenderer @Inject constructor(
roomNotifications.forEach { wrapper ->
when (wrapper) {
is RoomNotification.Removed -> {
Timber.d("Removing room messages notification ${wrapper.roomId}")
Timber.tag(loggerTag.value).d("Removing room messages notification ${wrapper.roomId}")
notificationDisplayer.cancelNotificationMessage(
tag = wrapper.roomId.value,
id = notificationIdProvider.getRoomMessagesNotificationId(currentUser.userId)
)
}
is RoomNotification.Message -> if (useCompleteNotificationFormat) {
Timber.d("Updating room messages notification ${wrapper.meta.roomId}")
Timber.tag(loggerTag.value).d("Updating room messages notification ${wrapper.meta.roomId}")
notificationDisplayer.showNotificationMessage(
tag = wrapper.meta.roomId.value,
id = notificationIdProvider.getRoomMessagesNotificationId(currentUser.userId),
@ -84,14 +87,14 @@ class NotificationRenderer @Inject constructor( @@ -84,14 +87,14 @@ class NotificationRenderer @Inject constructor(
invitationNotifications.forEach { wrapper ->
when (wrapper) {
is OneShotNotification.Removed -> {
Timber.d("Removing invitation notification ${wrapper.key}")
Timber.tag(loggerTag.value).d("Removing invitation notification ${wrapper.key}")
notificationDisplayer.cancelNotificationMessage(
tag = wrapper.key,
id = notificationIdProvider.getRoomInvitationNotificationId(currentUser.userId)
)
}
is OneShotNotification.Append -> if (useCompleteNotificationFormat) {
Timber.d("Updating invitation notification ${wrapper.meta.key}")
Timber.tag(loggerTag.value).d("Updating invitation notification ${wrapper.meta.key}")
notificationDisplayer.showNotificationMessage(
tag = wrapper.meta.key,
id = notificationIdProvider.getRoomInvitationNotificationId(currentUser.userId),
@ -104,14 +107,14 @@ class NotificationRenderer @Inject constructor( @@ -104,14 +107,14 @@ class NotificationRenderer @Inject constructor(
simpleNotifications.forEach { wrapper ->
when (wrapper) {
is OneShotNotification.Removed -> {
Timber.d("Removing simple notification ${wrapper.key}")
Timber.tag(loggerTag.value).d("Removing simple notification ${wrapper.key}")
notificationDisplayer.cancelNotificationMessage(
tag = wrapper.key,
id = notificationIdProvider.getRoomEventNotificationId(currentUser.userId)
)
}
is OneShotNotification.Append -> if (useCompleteNotificationFormat) {
Timber.d("Updating simple notification ${wrapper.meta.key}")
Timber.tag(loggerTag.value).d("Updating simple notification ${wrapper.meta.key}")
notificationDisplayer.showNotificationMessage(
tag = wrapper.meta.key,
id = notificationIdProvider.getRoomEventNotificationId(currentUser.userId),
@ -124,14 +127,14 @@ class NotificationRenderer @Inject constructor( @@ -124,14 +127,14 @@ class NotificationRenderer @Inject constructor(
fallbackNotifications.forEach { wrapper ->
when (wrapper) {
is OneShotNotification.Removed -> {
Timber.d("Removing fallback notification ${wrapper.key}")
Timber.tag(loggerTag.value).d("Removing fallback notification ${wrapper.key}")
notificationDisplayer.cancelNotificationMessage(
tag = wrapper.key,
id = notificationIdProvider.getFallbackNotificationId(currentUser.userId)
)
}
is OneShotNotification.Append -> if (useCompleteNotificationFormat) {
Timber.d("Updating fallback notification ${wrapper.meta.key}")
Timber.tag(loggerTag.value).d("Updating fallback notification ${wrapper.meta.key}")
notificationDisplayer.showNotificationMessage(
tag = wrapper.meta.key,
id = notificationIdProvider.getFallbackNotificationId(currentUser.userId),
@ -143,7 +146,7 @@ class NotificationRenderer @Inject constructor( @@ -143,7 +146,7 @@ class NotificationRenderer @Inject constructor(
// Update summary last to avoid briefly displaying it before other notifications
if (summaryNotification is SummaryNotification.Update) {
Timber.d("Updating summary notification")
Timber.tag(loggerTag.value).d("Updating summary notification")
notificationDisplayer.showNotificationMessage(
tag = null,
id = notificationIdProvider.getSummaryNotificationId(currentUser.userId),

5
libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationState.kt

@ -39,11 +39,10 @@ class NotificationState( @@ -39,11 +39,10 @@ class NotificationState(
) {
fun <T> updateQueuedEvents(
drawerManager: DefaultNotificationDrawerManager,
action: DefaultNotificationDrawerManager.(NotificationEventQueue, List<ProcessedEvent<NotifiableEvent>>) -> T
action: (NotificationEventQueue, List<ProcessedEvent<NotifiableEvent>>) -> T
): T {
return synchronized(queuedEvents) {
action(drawerManager, queuedEvents, renderedEvents)
action(queuedEvents, renderedEvents)
}
}

5
libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/DefaultPushHandler.kt

@ -24,7 +24,6 @@ import io.element.android.libraries.core.meta.BuildMeta @@ -24,7 +24,6 @@ import io.element.android.libraries.core.meta.BuildMeta
import io.element.android.libraries.di.AppScope
import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService
import io.element.android.libraries.push.impl.PushersManager
import io.element.android.libraries.push.impl.log.pushLoggerTag
import io.element.android.libraries.push.impl.notifications.DefaultNotificationDrawerManager
import io.element.android.libraries.push.impl.notifications.NotifiableEventResolver
import io.element.android.libraries.push.impl.store.DefaultPushDataStore
@ -40,7 +39,7 @@ import kotlinx.coroutines.launch @@ -40,7 +39,7 @@ import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
private val loggerTag = LoggerTag("PushHandler", pushLoggerTag)
private val loggerTag = LoggerTag("PushHandler", LoggerTag.PushLoggerTag)
@ContributesBinding(AppScope::class)
class DefaultPushHandler @Inject constructor(
@ -67,7 +66,7 @@ class DefaultPushHandler @Inject constructor( @@ -67,7 +66,7 @@ class DefaultPushHandler @Inject constructor(
* @param pushData the data received in the push.
*/
override suspend fun handle(pushData: PushData) {
Timber.tag(loggerTag.value).d("## handling pushData")
Timber.tag(loggerTag.value).d("## handling pushData: ${pushData.roomId}/${pushData.eventId}")
if (buildMeta.lowPrivacyLoggingEnabled) {
Timber.tag(loggerTag.value).d("## pushData: $pushData")

45
libraries/push/impl/src/main/res/values-de/translations.xml

@ -1,7 +1,52 @@ @@ -1,7 +1,52 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="notification_channel_call">"Anruf"</string>
<string name="notification_channel_listening_for_events">"Auf Ereignisse achten"</string>
<string name="notification_channel_noisy">"Laute Benachrichtigungen"</string>
<string name="notification_channel_silent">"Stumme Benachrichtigungen"</string>
<string name="notification_inline_reply_failed">"** Fehler beim Senden - bitte Raum öffnen"</string>
<string name="notification_invitation_action_join">"Beitreten"</string>
<string name="notification_invitation_action_reject">"Ablehnen"</string>
<string name="notification_invite_body">"Sie wurden zu einem Chat eingeladen"</string>
<string name="notification_new_messages">"Neue Nachrichten"</string>
<string name="notification_reaction_body">"Reagiert mit %1$s"</string>
<string name="notification_room_action_mark_as_read">"Als gelesen markieren"</string>
<string name="notification_room_invite_body">"Sie wurden eingeladen, den Raum zu betreten"</string>
<string name="notification_sender_me">"Ich"</string>
<string name="notification_test_push_notification_content">"Sie sehen sich die Benachrichtigung an! Klicken Sie hier!"</string>
<string name="notification_ticker_text_dm">"%1$s: %2$s"</string>
<string name="notification_ticker_text_group">"%1$s: %2$s %3$s"</string>
<string name="notification_unread_notified_messages_and_invitation">"%1$s und %2$s"</string>
<string name="notification_unread_notified_messages_in_room">"%1$s in %2$s"</string>
<string name="notification_unread_notified_messages_in_room_and_invitation">"%1$s in %2$s und %3$s"</string>
<plurals name="notification_compat_summary_line_for_room">
<item quantity="one">"%1$s: %2$d Nachricht"</item>
<item quantity="other">"%1$s: %2$d Nachrichten"</item>
</plurals>
<plurals name="notification_compat_summary_title">
<item quantity="one">"%d Mitteilung"</item>
<item quantity="other">"%d Mitteilungen"</item>
</plurals>
<plurals name="notification_invitations">
<item quantity="one">"%d Einladung"</item>
<item quantity="other">"%d Einladungen"</item>
</plurals>
<plurals name="notification_new_messages_for_room">
<item quantity="one">"%d neue Nachricht"</item>
<item quantity="other">"%d neue Nachrichten"</item>
</plurals>
<plurals name="notification_unread_notified_messages">
<item quantity="one">"%d ungelesene gemeldete Nachricht"</item>
<item quantity="other">"%d ungelesene gemeldete Nachrichten"</item>
</plurals>
<plurals name="notification_unread_notified_messages_in_room_rooms">
<item quantity="one">"%d Raum"</item>
<item quantity="other">"%d Räume"</item>
</plurals>
<string name="push_choose_distributor_dialog_title_android">"Wählen Sie aus, wie Sie Benachrichtigungen erhalten möchten"</string>
<string name="push_distributor_background_sync_android">"Hintergrundsynchronisation"</string>
<string name="push_distributor_firebase_android">"Google-Dienste"</string>
<string name="push_no_valid_google_play_services_apk_android">"Keine gültigen Google Play-Dienste gefunden. Benachrichtigungen funktionieren möglicherweise nicht richtig."</string>
<string name="notification_fallback_content">"Benachrichtigung"</string>
<string name="notification_room_action_quick_reply">"Schnelle Antwort"</string>
</resources>

2
libraries/push/test/src/main/kotlin/io/element/android/libraries/push/test/notifications/FakeNotificationDrawerManager.kt

@ -28,7 +28,7 @@ class FakeNotificationDrawerManager : NotificationDrawerManager { @@ -28,7 +28,7 @@ class FakeNotificationDrawerManager : NotificationDrawerManager {
clearMemberShipNotificationForSessionCallsCount.merge(sessionId.value, 1) { oldValue, value -> oldValue + value }
}
override fun clearMembershipNotificationForRoom(sessionId: SessionId, roomId: RoomId) {
override fun clearMembershipNotificationForRoom(sessionId: SessionId, roomId: RoomId, doRender: Boolean) {
val key = getMembershipNotificationKey(sessionId, roomId)
clearMemberShipNotificationForRoomCallsCount.merge(key, 1) { oldValue, value -> oldValue + value }
}

2
libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/pushproviders/firebase/FirebaseNewTokenHandler.kt

@ -26,7 +26,7 @@ import io.element.android.libraries.sessionstorage.api.toUserList @@ -26,7 +26,7 @@ import io.element.android.libraries.sessionstorage.api.toUserList
import timber.log.Timber
import javax.inject.Inject
private val loggerTag = LoggerTag("FirebaseNewTokenHandler")
private val loggerTag = LoggerTag("FirebaseNewTokenHandler", LoggerTag.PushLoggerTag)
/**
* Handle new token receive from Firebase. Will update all the sessions which are using Firebase as a push provider.

2
libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/pushproviders/firebase/FirebasePushProvider.kt

@ -26,7 +26,7 @@ import io.element.android.libraries.pushproviders.api.PusherSubscriber @@ -26,7 +26,7 @@ import io.element.android.libraries.pushproviders.api.PusherSubscriber
import timber.log.Timber
import javax.inject.Inject
private val loggerTag = LoggerTag("FirebasePushProvider")
private val loggerTag = LoggerTag("FirebasePushProvider", LoggerTag.PushLoggerTag)
@ContributesMultibinding(AppScope::class)
class FirebasePushProvider @Inject constructor(

2
libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/pushproviders/firebase/VectorFirebaseMessagingService.kt

@ -27,7 +27,7 @@ import kotlinx.coroutines.launch @@ -27,7 +27,7 @@ import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
private val loggerTag = LoggerTag("Firebase")
private val loggerTag = LoggerTag("VectorFirebaseMessagingService", LoggerTag.PushLoggerTag)
class VectorFirebaseMessagingService : FirebaseMessagingService() {
@Inject lateinit var firebaseNewTokenHandler: FirebaseNewTokenHandler

2
libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/pushproviders/unifiedpush/UnifiedPushNewGatewayHandler.kt

@ -24,7 +24,7 @@ import io.element.android.libraries.pushstore.api.clientsecret.PushClientSecret @@ -24,7 +24,7 @@ import io.element.android.libraries.pushstore.api.clientsecret.PushClientSecret
import timber.log.Timber
import javax.inject.Inject
private val loggerTag = LoggerTag("UnifiedPushNewGatewayHandler")
private val loggerTag = LoggerTag("UnifiedPushNewGatewayHandler", LoggerTag.PushLoggerTag)
/**
* Handle new endpoint received from UnifiedPush. Will update all the sessions which are using UnifiedPush as a push provider.

2
libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/pushproviders/unifiedpush/VectorUnifiedPushMessagingReceiver.kt

@ -28,7 +28,7 @@ import org.unifiedpush.android.connector.MessagingReceiver @@ -28,7 +28,7 @@ import org.unifiedpush.android.connector.MessagingReceiver
import timber.log.Timber
import javax.inject.Inject
private val loggerTag = LoggerTag("VectorUnifiedPushMessagingReceiver")
private val loggerTag = LoggerTag("VectorUnifiedPushMessagingReceiver", LoggerTag.PushLoggerTag)
class VectorUnifiedPushMessagingReceiver : MessagingReceiver() {
@Inject lateinit var pushParser: UnifiedPushParser

55
libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/SoftKeyboardEffect.kt

@ -0,0 +1,55 @@ @@ -0,0 +1,55 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.libraries.textcomposer
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.viewinterop.AndroidView
import io.element.android.libraries.androidutils.ui.awaitWindowFocus
import io.element.android.libraries.androidutils.ui.showKeyboard
/**
* Shows the soft keyboard when a given key changes to meet the required condition.
*
* Uses [showKeyboard] to show the keyboard for compatibility with [AndroidView].
*
* @param T
* @param key The key to watch for changes.
* @param onRequestFocus A callback to request focus to the view that will receive the keyboard input.
* @param predicate The predicate that [key] must meet before showing the keyboard.
*/
@Composable
internal fun <T> SoftKeyboardEffect(
key: T,
onRequestFocus: () -> Unit,
predicate: (T) -> Boolean,
) {
val view = LocalView.current
LaunchedEffect(key) {
if (predicate(key)) {
// Await window focus in case returning from a dialog
view.awaitWindowFocus()
// Show the keyboard, temporarily using the root view for focus
view.showKeyboard(andRequestFocus = true)
// Refocus to the correct view
onRequestFocus()
}
}
}

19
libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/TextComposer.kt

@ -43,7 +43,6 @@ import androidx.compose.material.icons.filled.Close @@ -43,7 +43,6 @@ import androidx.compose.material.icons.filled.Close
import androidx.compose.material.ripple.rememberRipple
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
@ -52,7 +51,6 @@ import androidx.compose.ui.Modifier @@ -52,7 +51,6 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.text.style.TextAlign
@ -84,7 +82,6 @@ import io.element.android.wysiwyg.compose.RichTextEditor @@ -84,7 +82,6 @@ import io.element.android.wysiwyg.compose.RichTextEditor
import io.element.android.wysiwyg.compose.RichTextEditorDefaults
import io.element.android.wysiwyg.compose.RichTextEditorState
import io.element.android.wysiwyg.view.models.InlineFormat
import kotlinx.coroutines.android.awaitFrame
import uniffi.wysiwyg_composer.ActionState
import uniffi.wysiwyg_composer.ComposerAction
@ -223,17 +220,11 @@ fun TextComposer( @@ -223,17 +220,11 @@ fun TextComposer(
}
}
// Request focus when changing mode, and show keyboard.
val keyboard = LocalSoftwareKeyboardController.current
LaunchedEffect(composerMode) {
if (composerMode is MessageComposerMode.Special) {
onRequestFocus()
keyboard?.let {
awaitFrame()
it.show()
}
}
SoftKeyboardEffect(composerMode, onRequestFocus) {
it is MessageComposerMode.Special
}
SoftKeyboardEffect(showTextFormatting, onRequestFocus) { it }
}
@Composable
@ -270,6 +261,8 @@ private fun TextInput( @@ -270,6 +261,8 @@ private fun TextInput(
style = defaultTypography.copy(
color = ElementTheme.colors.textSecondary,
),
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
}

4
libraries/textcomposer/impl/src/main/res/values-de/translations.xml

@ -0,0 +1,4 @@ @@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="rich_text_editor_a11y_add_attachment">"Anhang hinzufügen"</string>
</resources>

4
libraries/ui-strings/src/main/res/values-cs/translations.xml

@ -195,9 +195,9 @@ @@ -195,9 +195,9 @@
<string name="screen_notification_settings_additional_settings_section_title">"Další nastavení"</string>
<string name="screen_notification_settings_calls_label">"Halsové a video hovory"</string>
<string name="screen_notification_settings_configuration_mismatch">"Neshoda konfigurace"</string>
<string name="screen_notification_settings_configuration_mismatch_description">"Zjednodušili jsme nastavení oznámení, abychom usnadnili hledání možností.
<string name="screen_notification_settings_configuration_mismatch_description">"Zjednodušili jsme nastavení oznámení, abychom usnadnili hledání možností.
Některá vlastní nastavení, která jste si vybrali v minulosti, se zde nezobrazují, ale jsou stále aktivní.
Některá vlastní nastavení, která jste si vybrali v minulosti, se zde nezobrazují, ale jsou stále aktivní.
Pokud budete pokračovat, některá nastavení se mohou změnit."</string>
<string name="screen_notification_settings_direct_chats">"Přímé zprávy"</string>

206
libraries/ui-strings/src/main/res/values-de/translations.xml

@ -1,65 +1,265 @@ @@ -1,65 +1,265 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="a11y_hide_password">"Passwort verbergen"</string>
<string name="a11y_notifications_mentions_only">"Nur Erwähnungen"</string>
<string name="a11y_notifications_muted">"Stummgeschaltet"</string>
<string name="a11y_poll">"Umfrage"</string>
<string name="a11y_poll_end">"Umfrage beendet"</string>
<string name="a11y_send_files">"Dateien senden"</string>
<string name="a11y_show_password">"Passwort anzeigen"</string>
<string name="a11y_user_menu">"Benutzermenü"</string>
<string name="action_accept">"Akzeptieren"</string>
<string name="action_back">"Zurück"</string>
<string name="action_cancel">"Abbrechen"</string>
<string name="action_choose_photo">"Foto auswählen"</string>
<string name="action_clear">"Löschen"</string>
<string name="action_close">"Schließen"</string>
<string name="action_complete_verification">"Verifizierung abschließen"</string>
<string name="action_confirm">"Bestätigen"</string>
<string name="action_continue">"Weiter"</string>
<string name="action_copy">"Kopieren"</string>
<string name="action_copy_link">"Link kopieren"</string>
<string name="action_copy_link_to_message">"Link zur Nachricht kopieren"</string>
<string name="action_create">"Erstellen"</string>
<string name="action_create_a_room">"Raum erstellen"</string>
<string name="action_decline">"Ablehnen"</string>
<string name="action_disable">"Deaktivieren"</string>
<string name="action_done">"Erledigt"</string>
<string name="action_edit">"Bearbeiten"</string>
<string name="action_enable">"Aktivieren"</string>
<string name="action_end_poll">"Umfrage beenden"</string>
<string name="action_forgot_password">"Passwort vergessen?"</string>
<string name="action_forward">"Weiter"</string>
<string name="action_invite">"Einladen"</string>
<string name="action_invite_friends">"Freunde einladen"</string>
<string name="action_invite_friends_to_app">"Freunde einladen %1$s"</string>
<string name="action_invite_people_to_app">"Laden Sie Personen in %1$s ein"</string>
<string name="action_invites_list">"Einladungen"</string>
<string name="action_learn_more">"Mehr erfahren"</string>
<string name="action_leave">"Verlassen"</string>
<string name="action_leave_room">"Raum verlassen"</string>
<string name="action_manage_account">"Konto verwalten"</string>
<string name="action_manage_devices">"Geräte verwalten"</string>
<string name="action_next">"Weiter"</string>
<string name="action_no">"Nein"</string>
<string name="action_not_now">"Nicht jetzt"</string>
<string name="action_ok">"OK"</string>
<string name="action_open_with">"Öffnen mit"</string>
<string name="action_quick_reply">"Schnelle Antwort"</string>
<string name="action_quote">"Zitat"</string>
<string name="action_react">"Reagieren"</string>
<string name="action_remove">"Entfernen"</string>
<string name="action_reply">"Antwort"</string>
<string name="action_reply">"Antworten"</string>
<string name="action_reply_in_thread">"Im Thread antworten"</string>
<string name="action_report_bug">"Fehler melden"</string>
<string name="action_report_content">"Inhalt melden"</string>
<string name="action_retry">"Erneut versuchen"</string>
<string name="action_retry_decryption">"Entschlüsselung wiederholen"</string>
<string name="action_save">"Speichern"</string>
<string name="action_search">"Suchen"</string>
<string name="action_send">"Senden"</string>
<string name="action_send_message">"Nachricht senden"</string>
<string name="action_share">"Teilen"</string>
<string name="action_share_link">"Link teilen"</string>
<string name="action_skip">"Überspringen"</string>
<string name="action_start">"Start"</string>
<string name="action_start_chat">"Chat starten"</string>
<string name="action_start_verification">"Überprüfung starten"</string>
<string name="action_start_verification">"Verifizierung starten"</string>
<string name="action_static_map_load">"Tippen Sie, um die Karte zu laden"</string>
<string name="action_take_photo">"Foto machen"</string>
<string name="action_view_source">"Quelle anzeigen"</string>
<string name="action_yes">"Ja"</string>
<string name="common_about">"Über"</string>
<string name="common_acceptable_use_policy">"Nutzungsrichtlinie"</string>
<string name="common_advanced_settings">"Erweiterte Einstellungen"</string>
<string name="common_analytics">"Analysedaten"</string>
<string name="common_audio">"Audio"</string>
<string name="common_bubbles">"Blasen"</string>
<string name="common_copyright">"Copyright"</string>
<string name="common_creating_room">"Raum wird erstellt…"</string>
<string name="common_current_user_left_room">"Raum verlassen"</string>
<string name="common_decryption_error">"Dekodierungsfehler"</string>
<string name="common_developer_options">"Entwickleroptionen"</string>
<string name="common_edited_suffix">"(bearbeitet)"</string>
<string name="common_editing">"Bearbeitung"</string>
<string name="common_emote">"* %1$s %2$s"</string>
<string name="common_encryption_enabled">"Verschlüsselung aktiviert"</string>
<string name="common_error">"Fehler"</string>
<string name="common_file">"Datei"</string>
<string name="common_file_saved_on_disk_android">"Datei wurde unter Downloads gespeichert"</string>
<string name="common_forward_message">"Nachricht weiterleiten"</string>
<string name="common_gif">"GIF"</string>
<string name="common_image">"Bild"</string>
<string name="common_in_reply_to">"Als Antwort auf %1$s"</string>
<string name="common_invite_unknown_profile">"Diese Matrix-ID kann nicht gefunden werden, daher wird die Einladung möglicherweise nicht empfangen."</string>
<string name="common_leaving_room">"Raum verlassen"</string>
<string name="common_link_copied_to_clipboard">"Link in die Zwischenablage kopiert"</string>
<string name="common_loading">"Laden…"</string>
<string name="common_message">"Nachricht"</string>
<string name="common_message_layout">"Nachrichtenlayout"</string>
<string name="common_message_removed">"Nachricht entfernt"</string>
<string name="common_modern">"Modern"</string>
<string name="common_mute">"Stummschalten"</string>
<string name="common_no_results">"Keine Ergebnisse"</string>
<string name="common_offline">"Offline"</string>
<string name="common_password">"Passwort"</string>
<string name="common_people">"Menschen"</string>
<string name="common_people">"Personen"</string>
<string name="common_permalink">"Permalink"</string>
<string name="common_poll_total_votes">"Stimmen insgesamt: %1$s"</string>
<string name="common_poll_undisclosed_text">"Die Ergebnisse werden nach Ende der Umfrage angezeigt"</string>
<string name="common_privacy_policy">"Datenschutz­erklärung"</string>
<string name="common_reaction">"Reaktion"</string>
<string name="common_reactions">"Reaktionen"</string>
<string name="common_refreshing">"Wird erneuert…"</string>
<string name="common_replying_to">"%1$s antworten"</string>
<string name="common_report_a_bug">"Einen Fehler melden"</string>
<string name="common_report_submitted">"Bericht eingereicht"</string>
<string name="common_rich_text_editor">"Rich-Text-Editor"</string>
<string name="common_room_name">"Raumname"</string>
<string name="common_room_name_placeholder">"z.B. Ihr Projektname"</string>
<string name="common_search_for_someone">"Nach jemandem suchen"</string>
<string name="common_search_results">"Suchergebnisse"</string>
<string name="common_security">"Sicherheit"</string>
<string name="common_select_your_server">"Wählen Sie Ihren Server aus"</string>
<string name="common_sending">"Wird gesendet…"</string>
<string name="common_server_not_supported">"Server wird nicht unterstützt"</string>
<string name="common_server_url">"Server-URL"</string>
<string name="common_settings">"Einstellungen"</string>
<string name="common_shared_location">"Geteilter Standort"</string>
<string name="common_starting_chat">"Chat wird gestartet…"</string>
<string name="common_sticker">"Sticker"</string>
<string name="common_success">"Erfolg"</string>
<string name="common_suggestions">"Vorschläge"</string>
<string name="common_syncing">"Synchronisieren"</string>
<string name="common_text">"Text"</string>
<string name="common_third_party_notices">"Hinweise von Drittanbietern"</string>
<string name="common_thread">"Thread"</string>
<string name="common_topic">"Thema"</string>
<string name="common_topic_placeholder">"Worum geht es in diesem Raum?"</string>
<string name="common_unable_to_decrypt">"Entschlüsselung nicht möglich"</string>
<string name="common_unable_to_invite_message">"Einladungen konnten nicht an einen oder mehrere Benutzer gesendet werden."</string>
<string name="common_unable_to_invite_title">"Einladung(en) konnte(n) nicht gesendet werden"</string>
<string name="common_unmute">"Stummschaltung aufheben"</string>
<string name="common_unsupported_event">"Nicht unterstütztes Ereignis"</string>
<string name="common_username">"Benutzername"</string>
<string name="common_verification_cancelled">"Verifizierung abgebrochen"</string>
<string name="common_verification_complete">"Verifizierung abgeschlossen"</string>
<string name="common_video">"Video"</string>
<string name="common_waiting">"Warten…"</string>
<string name="dialog_title_confirmation">"Bestätigung"</string>
<string name="dialog_title_warning">"Warnung"</string>
<string name="emoji_picker_category_activity">"Aktivitäten"</string>
<string name="emoji_picker_category_flags">"Flaggen"</string>
<string name="emoji_picker_category_foods">"Essen &amp; Trinken"</string>
<string name="emoji_picker_category_nature">"Tiere &amp; Natur"</string>
<string name="emoji_picker_category_objects">"Objekte"</string>
<string name="emoji_picker_category_people">"Smileys &amp; Menschen"</string>
<string name="emoji_picker_category_places">"Reisen &amp; Orte"</string>
<string name="emoji_picker_category_symbols">"Symbole"</string>
<string name="error_failed_creating_the_permalink">"Fehler beim Erstellen des Permalinks"</string>
<string name="error_failed_loading_map">"%1$s konnte die Karte nicht laden. Bitte versuchen Sie es später erneut."</string>
<string name="error_failed_loading_messages">"Fehler beim Laden der Nachrichten"</string>
<string name="error_failed_locating_user">"%1$s konnte nicht auf Ihren Standort zugreifen. Bitte versuchen Sie es später erneut."</string>
<string name="error_missing_location_auth_android">"%1$s hat keine Erlaubnis, auf Ihren Standort zuzugreifen. Sie können den Zugriff in den Einstellungen aktivieren."</string>
<string name="error_missing_location_rationale_android">"%1$s hat keine Erlaubnis, auf Ihren Standort zuzugreifen. Aktivieren Sie unten den Zugriff."</string>
<string name="error_some_messages_have_not_been_sent">"Einige Nachrichten wurden nicht gesendet"</string>
<string name="error_unknown">"Entschuldigung, es ist ein Fehler aufgetreten"</string>
<string name="invite_friends_rich_title">"🔐 Begleite mich auf %1$s"</string>
<string name="invite_friends_text">"Hey, sprechen Sie mit mir auf %1$s: %2$s"</string>
<string name="leave_room_alert_empty_subtitle">"Sind Sie sicher, dass Sie diesen Raum verlassen möchten? Sie sind die einzige Person hier. Wenn Sie austreten, kann in Zukunft niemand mehr eintreten, auch Sie nicht."</string>
<string name="leave_room_alert_private_subtitle">"Sind Sie sicher, dass Sie diesen Raum verlassen möchten? Dieser Raum ist nicht öffentlich und Sie können ihm ohne Einladung nicht erneut beitreten."</string>
<string name="leave_room_alert_subtitle">"Sind Sie sicher, dass Sie den Raum verlassen wollen?"</string>
<string name="login_initial_device_name_android">"%1$s Android"</string>
<plurals name="common_member_count">
<item quantity="one">"%1$d Mitglied"</item>
<item quantity="other">"%1$d Mitglieder"</item>
</plurals>
<plurals name="common_poll_votes_count">
<item quantity="one">"%d Stimme"</item>
<item quantity="other">"%d Stimmen"</item>
</plurals>
<string name="preference_rageshake">"Schütteln Sie heftig zum Melden von Fehlern"</string>
<string name="rageshake_dialog_content">"Sie scheinen das Telefon aus Frustration zu schütteln. Möchten Sie den Bildschirm für den Fehlerbericht öffnen?"</string>
<string name="report_content_explanation">"Diese Meldung wird an den Administrator Ihres Homeservers weitergeleitet. Dieser kann keine verschlüsselten Nachrichten lesen."</string>
<string name="report_content_hint">"Grund für die Meldung dieses Inhalts"</string>
<string name="rich_text_editor_bullet_list">"Aufzählungsliste umschalten"</string>
<string name="rich_text_editor_close_formatting_options">"Formatierungsoptionen schließen"</string>
<string name="rich_text_editor_code_block">"Codeblock umschalten"</string>
<string name="rich_text_editor_composer_placeholder">"Nachricht…"</string>
<string name="rich_text_editor_create_link">"Einen Link erstellen"</string>
<string name="rich_text_editor_edit_link">"Link bearbeiten"</string>
<string name="rich_text_editor_format_bold">"Fettes Format anwenden"</string>
<string name="rich_text_editor_format_italic">"Kursives Format anwenden"</string>
<string name="rich_text_editor_format_strikethrough">"Durchgestrichenes Format anwenden"</string>
<string name="rich_text_editor_format_underline">"Unterstreichungsformat anwenden"</string>
<string name="rich_text_editor_full_screen_toggle">"Vollbildmodus umschalten"</string>
<string name="rich_text_editor_indent">"Einrückung"</string>
<string name="rich_text_editor_inline_code">"Inline-Codeformat anwenden"</string>
<string name="rich_text_editor_link">"Link setzen"</string>
<string name="rich_text_editor_numbered_list">"Nummerierte Liste umschalten"</string>
<string name="rich_text_editor_open_compose_options">"Optionen zum Verfassen öffnen"</string>
<string name="rich_text_editor_quote">"Vorschlag umschalten"</string>
<string name="rich_text_editor_remove_link">"Link entfernen"</string>
<string name="rich_text_editor_unindent">"Ohne Einrückung"</string>
<string name="rich_text_editor_url_placeholder">"Link"</string>
<string name="room_timeline_beginning_of_room">"Dies ist der Anfang von %1$s."</string>
<string name="room_timeline_beginning_of_room_no_name">"Dies ist der Anfang dieses Gesprächs."</string>
<string name="room_timeline_read_marker_title">"Neu"</string>
<string name="screen_analytics_settings_share_data">"Analysedaten teilen"</string>
<string name="screen_edit_profile_display_name">"Anzeigename"</string>
<string name="screen_edit_profile_display_name_placeholder">"Ihr Anzeigename"</string>
<string name="screen_edit_profile_error">"Ein unbekannter Fehler ist aufgetreten und die Informationen konnten nicht geändert werden."</string>
<string name="screen_edit_profile_error_title">"Profil kann nicht aktualisiert werden"</string>
<string name="screen_edit_profile_title">"Profil bearbeiten"</string>
<string name="screen_edit_profile_updating_details">"Profil wird aktualisiert…"</string>
<string name="screen_media_picker_error_failed_selection">"Medienauswahl fehlgeschlagen, bitte versuchen Sie es erneut."</string>
<string name="screen_media_upload_preview_error_failed_processing">"Fehler beim Verarbeiten des hochgeladenen Mediums. Bitte versuchen Sie es erneut."</string>
<string name="screen_media_upload_preview_error_failed_sending">"Das Hochladen der Medien ist fehlgeschlagen. Bitte versuchen Sie es erneut."</string>
<string name="screen_notification_settings_additional_settings_section_title">"Zusätzliche Einstellungen"</string>
<string name="screen_notification_settings_calls_label">"Audio- und Videoanrufe"</string>
<string name="screen_notification_settings_configuration_mismatch">"Konfiguration stimmt nicht überein"</string>
<string name="screen_notification_settings_configuration_mismatch_description">"Wir haben die Einstellungen für Benachrichtigungen vereinfacht, damit die Optionen leichter zu finden sind.
Einige benutzerdefinierte Einstellungen, die Sie in der Vergangenheit gewählt haben, werden hier nicht angezeigt, sind aber immer noch aktiv.
Wenn Sie fortfahren, können sich einige Ihrer Einstellungen ändern."</string>
<string name="screen_notification_settings_direct_chats">"Direkte Chats"</string>
<string name="screen_notification_settings_edit_custom_settings_section_title">"Benutzerdefinierte Einstellung pro Chat"</string>
<string name="screen_notification_settings_edit_failed_updating_default_mode">"Beim Aktualisieren der Benachrichtigungseinstellungen ist ein Fehler aufgetreten."</string>
<string name="screen_notification_settings_edit_mode_all_messages">"Alle Nachrichten"</string>
<string name="screen_notification_settings_edit_mode_mentions_and_keywords">"Nur Erwähnungen und Schlüsselwörter"</string>
<string name="screen_notification_settings_edit_screen_direct_section_header">"Bei direkten Chats, benachrichtigen Sie mich bei"</string>
<string name="screen_notification_settings_edit_screen_group_section_header">"Bei Gruppenchats benachrichtigen Sie mich bei"</string>
<string name="screen_notification_settings_enable_notifications">"Benachrichtigungen auf diesem Gerät aktivieren"</string>
<string name="screen_notification_settings_failed_fixing_configuration">"Die Konfiguration wurde nicht korrigiert, bitte versuchen Sie es erneut."</string>
<string name="screen_notification_settings_group_chats">"Gruppenchats"</string>
<string name="screen_notification_settings_mentions_section_title">"Erwähnungen"</string>
<string name="screen_notification_settings_mode_all">"Alle"</string>
<string name="screen_notification_settings_mode_mentions">"Erwähnungen"</string>
<string name="screen_notification_settings_notification_section_title">"Benachrichtige mich bei"</string>
<string name="screen_notification_settings_room_mention_label">"Benachrichtige mich bei @room"</string>
<string name="screen_notification_settings_system_notifications_action_required">"Um Benachrichtigungen zu erhalten, ändern Sie bitte Ihre %1$s."</string>
<string name="screen_notification_settings_system_notifications_action_required_content_link">"Systemeinstellungen"</string>
<string name="screen_notification_settings_system_notifications_turned_off">"Systembenachrichtigungen deaktiviert"</string>
<string name="screen_notification_settings_title">"Benachrichtigungen"</string>
<string name="screen_report_content_block_user_hint">"Prüfen Sie, ob Sie alle aktuellen und zukünftigen Nachrichten dieses Benutzers ausblenden möchten"</string>
<string name="screen_settings_oidc_account">"Konto und Geräte"</string>
<string name="screen_share_location_title">"Standort teilen"</string>
<string name="screen_share_my_location_action">"Meinen Standort teilen"</string>
<string name="screen_share_open_apple_maps">"In Apple Maps öffnen"</string>
<string name="screen_share_open_google_maps">"In Google Maps öffnen"</string>
<string name="screen_share_open_osm_maps">"In OpenStreetMap öffnen"</string>
<string name="screen_share_this_location_action">"Diesen Standort teilen"</string>
<string name="screen_view_location_title">"Standort"</string>
<string name="settings_rageshake">"Rageshake"</string>
<string name="settings_rageshake_detection_threshold">"Erkennungsschwelle"</string>
<string name="settings_title_general">"Allgemein"</string>
<string name="settings_version_number">"Version: %1$s (%2$s)"</string>
<string name="test_language_identifier">"en"</string>
<string name="dialog_title_error">"Fehler"</string>
<string name="dialog_title_success">"Erfolg"</string>
<string name="screen_analytics_settings_help_us_improve">"Teilen Sie anonyme Nutzungsdaten, um uns bei der Identifizierung von Problemen zu helfen."</string>
<string name="screen_analytics_settings_read_terms">"Sie können alle unsere Bedingungen lesen%1$s."</string>
<string name="screen_analytics_settings_read_terms_content_link">"hier"</string>
<string name="screen_report_content_block_user">"Benutzer sperren"</string>
</resources>

8
libraries/ui-strings/src/main/res/values-fr/translations.xml

@ -3,6 +3,8 @@ @@ -3,6 +3,8 @@
<string name="a11y_hide_password">"Masquer le mot de passe"</string>
<string name="a11y_notifications_mentions_only">"Mentions uniquement"</string>
<string name="a11y_notifications_muted">"En sourdine"</string>
<string name="a11y_poll">"Sondage"</string>
<string name="a11y_poll_end">"Sondage terminé"</string>
<string name="a11y_send_files">"Envoyer des fichiers"</string>
<string name="a11y_show_password">"Afficher le mot de passe"</string>
<string name="a11y_user_menu">"Menu utilisateur"</string>
@ -204,6 +206,12 @@ @@ -204,6 +206,12 @@
<string name="room_timeline_beginning_of_room_no_name">"Ceci est le début de cette conversation."</string>
<string name="room_timeline_read_marker_title">"Nouveau"</string>
<string name="screen_analytics_settings_share_data">"Partagez des données de statistiques d\'utilisation"</string>
<string name="screen_edit_profile_display_name">"Nom d\'affichage"</string>
<string name="screen_edit_profile_display_name_placeholder">"Votre nom d\'affichage"</string>
<string name="screen_edit_profile_error">"Une erreur inconnue s\'est produite et les informations n\'ont pas pu être modifiées."</string>
<string name="screen_edit_profile_error_title">"Impossible de mettre à jour le profil"</string>
<string name="screen_edit_profile_title">"Modifier le profil"</string>
<string name="screen_edit_profile_updating_details">"Mise à jour du profil…"</string>
<string name="screen_media_picker_error_failed_selection">"Échec de la sélection du média, veuillez réessayer."</string>
<string name="screen_media_upload_preview_error_failed_processing">"Échec du traitement des médias à télécharger, veuillez réessayer."</string>
<string name="screen_media_upload_preview_error_failed_sending">"Échec du téléchargement du média, veuillez réessayer."</string>

4
libraries/ui-strings/src/main/res/values-ru/translations.xml

@ -194,9 +194,9 @@ @@ -194,9 +194,9 @@
<string name="screen_notification_settings_additional_settings_section_title">"Дополнительные параметры"</string>
<string name="screen_notification_settings_calls_label">"Аудио и видео звонки"</string>
<string name="screen_notification_settings_configuration_mismatch">"Несоответствие конфигурации"</string>
<string name="screen_notification_settings_configuration_mismatch_description">"Мы упростили настройки уведомлений, чтобы упростить поиск опций.
<string name="screen_notification_settings_configuration_mismatch_description">"Мы упростили настройки уведомлений, чтобы упростить поиск опций.
Некоторые пользовательские настройки, выбранные вами ранее, не отображаются в данном меню, но они все еще активны.
Некоторые пользовательские настройки, выбранные вами ранее, не отображаются в данном меню, но они все еще активны.
Если вы продолжите, некоторые настройки могут быть изменены."</string>
<string name="screen_notification_settings_direct_chats">"Прямые чаты"</string>

17
libraries/ui-strings/src/main/res/values-sk/translations.xml

@ -3,6 +3,8 @@ @@ -3,6 +3,8 @@
<string name="a11y_hide_password">"Skryť heslo"</string>
<string name="a11y_notifications_mentions_only">"Iba zmienky"</string>
<string name="a11y_notifications_muted">"Stlmené"</string>
<string name="a11y_poll">"Anketa"</string>
<string name="a11y_poll_end">"Ukončená anketa"</string>
<string name="a11y_send_files">"Odoslať súbory"</string>
<string name="a11y_show_password">"Zobraziť heslo"</string>
<string name="a11y_user_menu">"Používateľské menu"</string>
@ -36,6 +38,8 @@ @@ -36,6 +38,8 @@
<string name="action_learn_more">"Zistiť viac"</string>
<string name="action_leave">"Opustiť"</string>
<string name="action_leave_room">"Opustiť miestnosť"</string>
<string name="action_manage_account">"Spravovať účet"</string>
<string name="action_manage_devices">"Spravovať zariadenia"</string>
<string name="action_next">"Ďalej"</string>
<string name="action_no">"Nie"</string>
<string name="action_not_now">"Teraz nie"</string>
@ -46,6 +50,7 @@ @@ -46,6 +50,7 @@
<string name="action_react">"Reagovať"</string>
<string name="action_remove">"Odstrániť"</string>
<string name="action_reply">"Odpovedať"</string>
<string name="action_reply_in_thread">"Odpovedať vo vlákne"</string>
<string name="action_report_bug">"Nahlásiť chybu"</string>
<string name="action_report_content">"Nahlásiť obsah"</string>
<string name="action_retry">"Skúsiť znova"</string>
@ -66,6 +71,7 @@ @@ -66,6 +71,7 @@
<string name="action_yes">"Áno"</string>
<string name="common_about">"O aplikácii"</string>
<string name="common_acceptable_use_policy">"Zásady prijateľného používania"</string>
<string name="common_advanced_settings">"Pokročilé nastavenia"</string>
<string name="common_analytics">"Analytika"</string>
<string name="common_audio">"Zvuk"</string>
<string name="common_bubbles">"Bubliny"</string>
@ -84,6 +90,7 @@ @@ -84,6 +90,7 @@
<string name="common_forward_message">"Preposlať správu"</string>
<string name="common_gif">"GIF"</string>
<string name="common_image">"Obrázok"</string>
<string name="common_in_reply_to">"V odpovedi na %1$s"</string>
<string name="common_invite_unknown_profile">"Toto Matrix ID sa nedá nájsť, takže pozvánka nemusí byť prijatá."</string>
<string name="common_leaving_room">"Opustenie miestnosti"</string>
<string name="common_link_copied_to_clipboard">"Odkaz bol skopírovaný do schránky"</string>
@ -101,11 +108,13 @@ @@ -101,11 +108,13 @@
<string name="common_poll_total_votes">"Celkový počet hlasov: %1$s"</string>
<string name="common_poll_undisclosed_text">"Výsledky sa zobrazia po ukončení ankety"</string>
<string name="common_privacy_policy">"Zásady ochrany osobných údajov"</string>
<string name="common_reaction">"Reakcia"</string>
<string name="common_reactions">"Reakcie"</string>
<string name="common_refreshing">"Obnovuje sa…"</string>
<string name="common_replying_to">"Odpoveď na %1$s"</string>
<string name="common_report_a_bug">"Nahlásiť chybu"</string>
<string name="common_report_submitted">"Nahlásenie bolo odoslané"</string>
<string name="common_rich_text_editor">"Rozšírený textový editor"</string>
<string name="common_room_name">"Názov miestnosti"</string>
<string name="common_room_name_placeholder">"napr. názov vášho projektu"</string>
<string name="common_search_for_someone">"Vyhľadať niekoho"</string>
@ -124,6 +133,7 @@ @@ -124,6 +133,7 @@
<string name="common_syncing">"Synchronizuje sa"</string>
<string name="common_text">"Text"</string>
<string name="common_third_party_notices">"Oznámenia tretích strán"</string>
<string name="common_thread">"Vlákno"</string>
<string name="common_topic">"Téma"</string>
<string name="common_topic_placeholder">"O čom je táto miestnosť?"</string>
<string name="common_unable_to_decrypt">"Nie je možné dešifrovať"</string>
@ -191,12 +201,19 @@ @@ -191,12 +201,19 @@
<string name="rich_text_editor_numbered_list">"Prepnúť číslovaný zoznam"</string>
<string name="rich_text_editor_open_compose_options">"Otvoriť možnosti písania"</string>
<string name="rich_text_editor_quote">"Prepnúť citáciu"</string>
<string name="rich_text_editor_remove_link">"Odstrániť odkaz"</string>
<string name="rich_text_editor_unindent">"Zrušiť odsadenie"</string>
<string name="rich_text_editor_url_placeholder">"Odkaz"</string>
<string name="room_timeline_beginning_of_room">"Toto je začiatok %1$s."</string>
<string name="room_timeline_beginning_of_room_no_name">"Toto je začiatok tejto konverzácie."</string>
<string name="room_timeline_read_marker_title">"Nové"</string>
<string name="screen_analytics_settings_share_data">"Zdieľať analytické údaje"</string>
<string name="screen_edit_profile_display_name">"Zobrazované meno"</string>
<string name="screen_edit_profile_display_name_placeholder">"Vaše zobrazované meno"</string>
<string name="screen_edit_profile_error">"Vyskytla sa neznáma chyba a informácie nebolo možné zmeniť."</string>
<string name="screen_edit_profile_error_title">"Nepodarilo sa aktualizovať profil"</string>
<string name="screen_edit_profile_title">"Upraviť profil"</string>
<string name="screen_edit_profile_updating_details">"Aktualizácia profilu…"</string>
<string name="screen_media_picker_error_failed_selection">"Nepodarilo sa vybrať médium, skúste to prosím znova."</string>
<string name="screen_media_upload_preview_error_failed_processing">"Nepodarilo sa spracovať médiá na odoslanie, skúste to prosím znova."</string>
<string name="screen_media_upload_preview_error_failed_sending">"Nepodarilo sa nahrať médiá, skúste to prosím znova."</string>

3
libraries/ui-strings/src/main/res/values/localazy.xml

@ -3,6 +3,8 @@ @@ -3,6 +3,8 @@
<string name="a11y_hide_password">"Hide password"</string>
<string name="a11y_notifications_mentions_only">"Mentions only"</string>
<string name="a11y_notifications_muted">"Muted"</string>
<string name="a11y_poll">"Poll"</string>
<string name="a11y_poll_end">"Ended poll"</string>
<string name="a11y_send_files">"Send files"</string>
<string name="a11y_show_password">"Show password"</string>
<string name="a11y_user_menu">"User menu"</string>
@ -208,6 +210,7 @@ @@ -208,6 +210,7 @@
<string name="screen_edit_profile_display_name_placeholder">"Your display name"</string>
<string name="screen_edit_profile_error">"An unknown error was encountered and the information couldn\'t be changed."</string>
<string name="screen_edit_profile_error_title">"Unable to update profile"</string>
<string name="screen_edit_profile_title">"Edit profile"</string>
<string name="screen_edit_profile_updating_details">"Updating profile…"</string>
<string name="screen_media_picker_error_failed_selection">"Failed selecting media, please try again."</string>
<string name="screen_media_upload_preview_error_failed_processing">"Failed processing media to upload, please try again."</string>

BIN
tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemPollView-D-13_14_null_0,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemPollView-D-13_14_null_1,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemPollView-N-13_15_null_0,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemPollView-N-13_15_null_1,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemEventTimestampBelow_0_null,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerDisclosedNotSelected-D-0_0_null,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerDisclosedNotSelected-N-0_1_null,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerDisclosedNotSelected_0_null,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerDisclosedSelected-D-1_1_null,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerDisclosedSelected-N-1_2_null,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerDisclosedSelected_0_null,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerEndedSelected-D-6_6_null,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerEndedSelected-N-6_7_null,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerEndedSelected_0_null,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerEndedWinnerNotSelected-D-4_4_null,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerEndedWinnerNotSelected-N-4_5_null,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerEndedWinnerNotSelected_0_null,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerEndedWinnerSelected-D-5_5_null,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerEndedWinnerSelected-N-5_6_null,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerEndedWinnerSelected_0_null,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerUndisclosedNotSelected-D-2_2_null,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerUndisclosedNotSelected-N-2_3_null,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerUndisclosedNotSelected_0_null,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save