Merge branch 'dla/feature/room_list_decoration' of https://github.com/vector-im/element-x-android into dla/feature/room_list_decoration
This commit is contained in:
commit
c160a37b2c
140 changed files with 2173 additions and 340 deletions
|
|
@ -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
Normal file
1
changelog.d/1337.bugfix
Normal file
|
|
@ -0,0 +1 @@
|
|||
[Rich text editor] Ensure keyboard opens for reply and text formatting modes
|
||||
1
changelog.d/1347.bugfix
Normal file
1
changelog.d/1347.bugfix
Normal file
|
|
@ -0,0 +1 @@
|
|||
[Rich text editor] Fix placeholder spilling onto multiple lines
|
||||
|
|
@ -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
Normal file
6
features/call/src/main/res/values-de/translations.xml
Normal file
|
|
@ -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
Normal file
6
features/call/src/main/res/values-sk/translations.xml
Normal file
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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(
|
|||
suspend {
|
||||
client.getRoom(roomId)?.use {
|
||||
it.leave().getOrThrow()
|
||||
notificationDrawerManager.clearMembershipNotificationForRoom(client.sessionId, roomId)
|
||||
notificationDrawerManager.clearMembershipNotificationForRoom(client.sessionId, roomId, doRender = true)
|
||||
}.let { }
|
||||
}.runCatchingUpdatingState(declinedAction)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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
|
|||
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(
|
|||
}
|
||||
}
|
||||
|
||||
@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 = { }
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(
|
|||
}
|
||||
|
||||
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(
|
|||
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(
|
|||
}
|
||||
|
||||
@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(
|
|||
}
|
||||
|
||||
@Composable
|
||||
fun ColumnScope.UndisclosedPollBottomNotice(modifier: Modifier = Modifier) {
|
||||
private fun ColumnScope.UndisclosedPollBottomNotice(
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Text(
|
||||
modifier = modifier
|
||||
.align(Alignment.Start)
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 {
|
|||
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)
|
||||
|
|
|
|||
|
|
@ -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(
|
|||
|
||||
@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(
|
|||
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(
|
|||
NavTarget.AdvancedSettings -> {
|
||||
createNode<AdvancedSettingsNode>(buildContext)
|
||||
}
|
||||
is NavTarget.UserProfile -> {
|
||||
val inputs = EditUserProfileNode.Inputs(navTarget.matrixUser)
|
||||
createNode<EditUserProfileNode>(buildContext, listOf(inputs))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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(
|
|||
fun onOpenDeveloperSettings()
|
||||
fun onOpenNotificationSettings()
|
||||
fun onOpenAdvancedSettings()
|
||||
fun onOpenUserProfile(matrixUser: MatrixUser)
|
||||
}
|
||||
|
||||
private fun onOpenBugReport() {
|
||||
|
|
@ -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(
|
|||
onOpenAdvancedSettings = this::onOpenAdvancedSettings,
|
||||
onSuccessLogout = { onSuccessLogout(activity, it) },
|
||||
onManageAccountClicked = { onManageAccountClicked(activity, it, isDark) },
|
||||
onOpenNotificationSettings = this::onOpenNotificationSettings
|
||||
onOpenNotificationSettings = this::onOpenNotificationSettings,
|
||||
onOpenUserProfile = this::onOpenUserProfile,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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(
|
|||
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(
|
|||
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) {
|
|||
onSuccessLogout = {},
|
||||
onManageAccountClicked = {},
|
||||
onOpenNotificationSettings = {},
|
||||
onOpenUserProfile = {},
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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") }
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
)
|
||||
|
|
@ -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 = {}
|
||||
)
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -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>
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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
|
|||
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(
|
|||
.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(
|
|||
}
|
||||
}
|
||||
|
||||
@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,
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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" }
|
|||
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" }
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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) {
|
|||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -43,5 +43,7 @@ enum class AvatarSize(val dp: Dp) {
|
|||
RoomInviteItem(52.dp),
|
||||
InviteSender(16.dp),
|
||||
|
||||
EditRoomDetails(70.dp),
|
||||
|
||||
NotificationsOptIn(32.dp),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -111,6 +111,9 @@ class RoomSummaryListProcessor(
|
|||
RoomListEntriesUpdate.Clear -> {
|
||||
clear()
|
||||
}
|
||||
is RoomListEntriesUpdate.Truncate -> {
|
||||
subList(update.length.toInt(), size).clear()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -98,6 +98,9 @@ internal class MatrixTimelineDiffProcessor(
|
|||
TimelineChange.CLEAR -> {
|
||||
clear()
|
||||
}
|
||||
TimelineChange.TRUNCATE -> {
|
||||
// Not supported
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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(
|
|||
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(
|
|||
override suspend fun getAccountManagementUrl(action: AccountManagementAction?): Result<String?> {
|
||||
return accountManagementUrlString
|
||||
}
|
||||
|
||||
override suspend fun uploadMedia(
|
||||
mimeType: String,
|
||||
data: ByteArray,
|
||||
|
|
@ -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(
|
|||
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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
|||
|
||||
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(
|
||||
|
|
|
|||
|
|
@ -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
|
|||
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(
|
|||
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(
|
|||
|
||||
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(
|
|||
* 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(
|
|||
/**
|
||||
* 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(
|
|||
* 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(
|
|||
* 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(
|
|||
/**
|
||||
* 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(
|
|||
/**
|
||||
* 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(
|
|||
* 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(
|
|||
}
|
||||
}
|
||||
|
||||
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(
|
|||
|
||||
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
|
||||
|
|
|
|||
|
|
@ -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
|
|||
|
||||
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(
|
|||
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(
|
|||
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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
|||
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.
|
||||
|
|
|
|||
|
|
@ -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() {
|
|||
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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
|||
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,
|
||||
|
|
|
|||
|
|
@ -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
|
|||
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(
|
|||
|
||||
// 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(
|
|||
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(
|
|||
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(
|
|||
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(
|
|||
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(
|
|||
|
||||
// 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),
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
|||
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(
|
|||
* @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")
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
|||
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
|
|||
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(
|
|||
}
|
||||
}
|
||||
|
||||
// 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(
|
|||
style = defaultTypography.copy(
|
||||
color = ElementTheme.colors.textSecondary,
|
||||
),
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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">"Datenschutzerklä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 & Trinken"</string>
|
||||
<string name="emoji_picker_category_nature">"Tiere & Natur"</string>
|
||||
<string name="emoji_picker_category_objects">"Objekte"</string>
|
||||
<string name="emoji_picker_category_people">"Smileys & Menschen"</string>
|
||||
<string name="emoji_picker_category_places">"Reisen & 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>
|
||||
|
|
|
|||
|
|
@ -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 @@
|
|||
<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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 @@
|
|||
<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 @@
|
|||
<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 @@
|
|||
<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 @@
|
|||
<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 @@
|
|||
<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 @@
|
|||
<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 @@
|
|||
<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,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 @@
|
|||
<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>
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:801f832469346524fdce0b5ad8654c3405daf0f21ae0601c62dc7f148b576ce8
|
||||
size 49074
|
||||
oid sha256:188c362ebd8bc32a47b66a080331db7643cd97714f2d4e952d7bec8c11520dcd
|
||||
size 49026
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:944eb2ef8abf2e8f1715d063020767940c80b40690aae1129f69d02ddafac9d0
|
||||
size 51029
|
||||
oid sha256:9de6ab591cb02f6545218a2606d031f4f93c82e71e99523eacaf77ffa78fadb1
|
||||
size 50940
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:adf6f3f79b9d8f62172171dd8a172bff1958bc4698df4586bf83c73fe4c6c6f3
|
||||
size 46198
|
||||
oid sha256:b4b00894025844927932e790a1738c85cfbb61e61a81dfbbbd7e342f38f40b99
|
||||
size 46061
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:38183d1d69e36c2570259c987be079d09ecfc0a0cba7508fc43d33e95c574121
|
||||
size 48368
|
||||
oid sha256:bd88ed3aeb9a20f148e914c4a5d4554220a1fecd8e8a8fe87400de78ff4bf248
|
||||
size 48237
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:54a4fe8e7f968bf487168a517baa0d9c9f6fc5b2354f4eff45f56de77b043f8f
|
||||
size 56535
|
||||
oid sha256:5490f2501c6ef257f926fc2f4bd9d94ef4e6e3017d4da290e2199eaeaa2ac5b5
|
||||
size 56571
|
||||
|
|
|
|||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:045a09f485856b01e1084a91fefca0ccd7f9e7ef7470a22a4db0a60e95fb8667
|
||||
size 23119
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:31a8f8e06cc101bcdccb973f3a003238cc093342f04613975cb9512072b051c2
|
||||
size 21619
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:9f97a2f591619aa2dd914f79a2bdfabea3e4e8239e6d80f29400e2c02c6ae21d
|
||||
size 39250
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:1abb2b1c635b5a6666ce7de0ffbe10c753d32c6f0f2e95e8cf17d481bfa5228b
|
||||
size 23024
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:609279d1f2e25b86b8ae5a598bb6602a43a6714fa8e98a4625c349470d9d7e85
|
||||
size 21345
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:03b4bfab1cbbbedd9219423834f3bdf47f40e20c0da791be2c892cb2af86cf7d
|
||||
size 38694
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:08550f84b603d0c0424d63e7aef09856a048b7a862288ac43911673019ae7a5d
|
||||
size 23075
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:7e6000321200788598744d82d8b11c9f2d6eb509bf8c9c0bf362c10d1b27b9ae
|
||||
size 21474
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:1672ff3a807b387b1f780920111c19484261b36884f1f68d72e108c90a4104d2
|
||||
size 38948
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:013bfb3d3bd78d8db9f0ac51b11a41c2b82daaa28f87cdfd866260d3fb40bf0d
|
||||
size 23125
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:7c039ab6649ff160b540256dacba1037b15c5907d9994a041cbab3be0ee5cacd
|
||||
size 21283
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:43da602c904210df0112a1af831cd2a2f7a7c5109d26605efad464a3c5b58995
|
||||
size 38864
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:a99ff857afe6478e579c96b984e1abdbf981638e6964a7d2c94b53153aff078d
|
||||
size 23135
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:d97ba3288f5fcdaaed917a994df9273d3e82d5a6e0c7978044dbaf03152e3269
|
||||
size 21270
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:bf806b2490fc8a1df278bde94535cb1b5d38f66531df85d6fd1251e6671d9f56
|
||||
size 38756
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:05af414578ab61e5b24b6e0a4895e0b52c3566625fd47f00b32f38b416dbe7fa
|
||||
size 21498
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:9ccde06f5825c37c1c58ef0d9aee44f7b65425988386a97ba744360d8c22e470
|
||||
size 19563
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:eeaa924482fa2fc29079f7f0d4bddacb96567a2133808bed8b49be2ad7afe80f
|
||||
size 36481
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue