David Langley
1 year ago
140 changed files with 2173 additions and 340 deletions
@ -0,0 +1 @@
@@ -0,0 +1 @@
|
||||
[Rich text editor] Ensure keyboard opens for reply and text formatting modes |
@ -0,0 +1 @@
@@ -0,0 +1 @@
|
||||
[Rich text editor] Fix placeholder spilling onto multiple lines |
@ -1,4 +1,10 @@
@@ -1,4 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?> |
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> |
||||
<string name="screen_analytics_prompt_data_usage">"Wir zeichnen keine persönlichen Daten auf und erstellen keine Profile."</string> |
||||
<string name="screen_analytics_prompt_help_us_improve">"Teilen Sie anonyme Nutzungsdaten, um uns bei der Identifizierung von Problemen zu helfen."</string> |
||||
<string name="screen_analytics_prompt_read_terms">"Sie können alle unsere Bedingungen lesen%1$s."</string> |
||||
<string name="screen_analytics_prompt_read_terms_content_link">"hier"</string> |
||||
<string name="screen_analytics_prompt_settings">"Sie können diese Funktion jederzeit deaktivieren"</string> |
||||
<string name="screen_analytics_prompt_third_party_sharing">"Wir geben Ihre Daten nicht an Dritte weiter"</string> |
||||
<string name="screen_analytics_prompt_title">"Hilf uns %1$s zu verbessern"</string> |
||||
</resources> |
||||
|
@ -1,4 +1,15 @@
@@ -1,4 +1,15 @@
|
||||
<?xml version="1.0" encoding="utf-8"?> |
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> |
||||
<string name="screen_create_room_action_create_room">"Neuer Raum"</string> |
||||
<string name="screen_create_room_action_invite_people">"Freunde zu Element einladen"</string> |
||||
<string name="screen_create_room_add_people_title">"Personen einladen"</string> |
||||
<string name="screen_create_room_error_creating_room">"Beim Erstellen des Raums ist ein Fehler aufgetreten"</string> |
||||
<string name="screen_create_room_private_option_description">"Die Nachrichten in diesem Raum sind verschlüsselt. Die Verschlüsselung kann nicht nachträglich deaktiviert werden."</string> |
||||
<string name="screen_create_room_private_option_title">"Privater Raum (nur auf Einladung)"</string> |
||||
<string name="screen_create_room_public_option_description">"Die Nachrichten sind nicht verschlüsselt und können von jedem gelesen werden. Die Verschlüsselung kann zu einem späteren Zeitpunkt aktiviert werden."</string> |
||||
<string name="screen_create_room_public_option_title">"Öffentlicher Raum (für alle)"</string> |
||||
<string name="screen_create_room_room_name_label">"Raumname"</string> |
||||
<string name="screen_create_room_topic_label">"Thema (optional)"</string> |
||||
<string name="screen_start_chat_error_starting_chat">"Beim Versuch, einen Chat zu starten, ist ein Fehler aufgetreten"</string> |
||||
<string name="screen_create_room_title">"Raum erstellen"</string> |
||||
</resources> |
||||
|
@ -1,5 +1,13 @@
@@ -1,5 +1,13 @@
|
||||
<?xml version="1.0" encoding="utf-8"?> |
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> |
||||
<string name="screen_notification_optin_subtitle">"Du kannst deine Einstellungen später ändern."</string> |
||||
<string name="screen_migration_message">"Dies ist ein einmaliger Vorgang, danke fürs Warten."</string> |
||||
<string name="screen_migration_title">"Richten Sie Ihr Konto ein."</string> |
||||
<string name="screen_notification_optin_subtitle">"Sie können Ihre Einstellungen später ändern."</string> |
||||
<string name="screen_notification_optin_title">"Erlaube Benachrichtigungen und verpasse keine Nachricht"</string> |
||||
<string name="screen_welcome_bullet_1">"Anrufe, Umfragen, Suchfunktionen und mehr werden im Laufe des Jahres hinzugefügt."</string> |
||||
<string name="screen_welcome_bullet_2">"Der Nachrichtenverlauf für verschlüsselte Räume wird in diesem Update nicht verfügbar sein."</string> |
||||
<string name="screen_welcome_bullet_3">"Wir würden uns freuen, von Ihnen zu hören. Teilen Sie uns Ihre Meinung über die Einstellungsseite mit."</string> |
||||
<string name="screen_welcome_button">"Los geht\'s!"</string> |
||||
<string name="screen_welcome_subtitle">"Folgendes müssen Sie wissen:"</string> |
||||
<string name="screen_welcome_title">"Willkommen bei %1$s!"</string> |
||||
</resources> |
||||
|
@ -0,0 +1,9 @@
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?> |
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> |
||||
<string name="screen_invites_decline_chat_message">"Möchten Sie die Einladung zum Betreten von %1$s wirklich ablehnen?"</string> |
||||
<string name="screen_invites_decline_chat_title">"Einladung ablehnen"</string> |
||||
<string name="screen_invites_decline_direct_chat_message">"Sind Sie sicher, dass Sie diesen privaten Chat mit %1$s ablehnen möchten?"</string> |
||||
<string name="screen_invites_decline_direct_chat_title">"Chat ablehnen"</string> |
||||
<string name="screen_invites_empty_list">"Keine Einladungen"</string> |
||||
<string name="screen_invites_invited_you">"%1$s (%2$s) hat dich eingeladen"</string> |
||||
</resources> |
@ -1,8 +1,47 @@
@@ -1,8 +1,47 @@
|
||||
<?xml version="1.0" encoding="utf-8"?> |
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> |
||||
<string name="screen_account_provider_change">"Kontoanbieter ändern"</string> |
||||
<string name="screen_account_provider_form_hint">"Homeserver-Adresse"</string> |
||||
<string name="screen_account_provider_form_notice">"Geben Sie einen Suchbegriff oder eine Domainadresse ein."</string> |
||||
<string name="screen_account_provider_form_subtitle">"Suchen Sie nach einem Unternehmen, einer Community oder einem privaten Server."</string> |
||||
<string name="screen_account_provider_form_title">"Kontoanbieter finden"</string> |
||||
<string name="screen_account_provider_signin_subtitle">"Hier werden Ihre Gespräche gespeichert – genau so, wie Sie einen E-Mail-Anbieter nutzen würden, um Ihre E-Mails aufzubewahren."</string> |
||||
<string name="screen_account_provider_signin_title">"Sie sind dabei, sich bei %s anzumelden"</string> |
||||
<string name="screen_account_provider_signup_subtitle">"Hier werden Ihre Gespräche gespeichert – genau so, wie Sie einen E-Mail-Anbieter nutzen würden, um Ihre E-Mails aufzubewahren."</string> |
||||
<string name="screen_account_provider_signup_title">"Sie sind dabei, ein Konto bei %s zu erstellen"</string> |
||||
<string name="screen_change_account_provider_matrix_org_subtitle">"Matrix.org ist ein großer, kostenloser Server im öffentlichen Matrix-Netzwerk für eine sichere, dezentralisierte Kommunikation, der von der Matrix.org Foundation betrieben wird."</string> |
||||
<string name="screen_change_account_provider_other">"Sonstige"</string> |
||||
<string name="screen_change_account_provider_subtitle">"Verwenden Sie einen anderen Kontoanbieter, z. B. Ihren eigenen privaten Server oder ein Geschäftskonto."</string> |
||||
<string name="screen_change_account_provider_title">"Kontoanbieter wechseln"</string> |
||||
<string name="screen_change_server_error_invalid_homeserver">"Wir konnten diesen Homeserver nicht erreichen. Bitte überprüfen Sie, ob Sie die Homeserver-URL korrekt eingegeben haben. Wenn die URL korrekt ist, wenden Sie sich an Ihren Homeserver-Administrator, um weitere Hilfe zu erhalten."</string> |
||||
<string name="screen_change_server_error_no_sliding_sync_message">"Dieser Server unterstützt derzeit kein Sliding Sync."</string> |
||||
<string name="screen_change_server_form_header">"Homeserver-URL"</string> |
||||
<string name="screen_change_server_form_notice">"Sie können nur eine Verbindung zu einem vorhandenen Server herstellen, der Sliding Sync unterstützt. Ihr Homeserver-Administrator muss das konfigurieren. %1$s"</string> |
||||
<string name="screen_change_server_subtitle">"Wie lautet die Adresse Ihres Servers?"</string> |
||||
<string name="screen_login_error_deactivated_account">"Dieses Konto wurde deaktiviert."</string> |
||||
<string name="screen_login_error_invalid_credentials">"Falscher Benutzername und/oder Passwort"</string> |
||||
<string name="screen_login_error_invalid_user_id">"Dies ist keine gültige Benutzerkennung. Erwartetes Format: \'@user:homeserver.org\'"</string> |
||||
<string name="screen_login_error_unsupported_authentication">"Der ausgewählte Homeserver unterstützt weder den Login per Passwort noch per OIDC. Bitte kontaktieren Sie Ihren Admin oder wählen Sie einen anderen Homeserver."</string> |
||||
<string name="screen_login_form_header">"Geben Sie Ihre Daten ein"</string> |
||||
<string name="screen_login_title">"Willkommen zurück!"</string> |
||||
<string name="screen_login_title_with_homeserver">"Anmelden bei %1$s"</string> |
||||
<string name="screen_server_confirmation_change_server">"Kontoanbieter wechseln"</string> |
||||
<string name="screen_server_confirmation_message_login_element_dot_io">"Ein privater Server für die Mitarbeiter von Element."</string> |
||||
<string name="screen_server_confirmation_message_login_matrix_dot_org">"Matrix ist ein offenes Netzwerk für eine sichere, dezentrale Kommunikation."</string> |
||||
<string name="screen_server_confirmation_message_register">"Hier werden Ihre Gespräche gespeichert - so wie Sie Ihre E-Mails bei einem E-Mail-Anbieter aufbewahren würden."</string> |
||||
<string name="screen_server_confirmation_title_login">"Sie sind dabei, sich bei %1$s anzumelden"</string> |
||||
<string name="screen_server_confirmation_title_register">"Sie sind dabei, ein Konto auf %1$s zu erstellen"</string> |
||||
<string name="screen_waitlist_message">"Derzeit besteht eine hohe Nachfrage nach %1$s auf %2$s. Kehren Sie in ein paar Tagen zur App zurück und versuchen Sie es erneut. |
||||
|
||||
Danke für Ihre Geduld!"</string> |
||||
<string name="screen_waitlist_message_success">"Willkommen bei %1$s!"</string> |
||||
<string name="screen_waitlist_title">"Sie sind fast am Ziel."</string> |
||||
<string name="screen_waitlist_title_success">"Sie sind dabei."</string> |
||||
<string name="screen_account_provider_continue">"Weiter"</string> |
||||
<string name="screen_change_server_submit">"Weiter"</string> |
||||
<string name="screen_change_server_title">"Wählen Sie Ihren Server aus"</string> |
||||
<string name="screen_login_password_hint">"Passwort"</string> |
||||
<string name="screen_login_submit">"Weiter"</string> |
||||
<string name="screen_login_subtitle">"Matrix ist ein offenes Netzwerk für eine sichere, dezentrale Kommunikation."</string> |
||||
<string name="screen_login_username_hint">"Benutzername"</string> |
||||
</resources> |
||||
|
@ -0,0 +1,8 @@
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?> |
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> |
||||
<string name="screen_signout_confirmation_dialog_content">"Sind Sie sicher, dass Sie sich abmelden wollen?"</string> |
||||
<string name="screen_signout_confirmation_dialog_title">"Abmelden"</string> |
||||
<string name="screen_signout_in_progress_dialog_content">"Abmelden…"</string> |
||||
<string name="screen_signout_confirmation_dialog_submit">"Abmelden"</string> |
||||
<string name="screen_signout_preference_item">"Abmelden"</string> |
||||
</resources> |
@ -1,5 +1,42 @@
@@ -1,5 +1,42 @@
|
||||
<?xml version="1.0" encoding="utf-8"?> |
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> |
||||
<plurals name="room_timeline_state_changes"> |
||||
<item quantity="one">"%1$d Raumänderung"</item> |
||||
<item quantity="other">"%1$d Raumänderungen"</item> |
||||
</plurals> |
||||
<string name="screen_room_attachment_source_camera">"Kamera"</string> |
||||
<string name="screen_room_attachment_source_camera_photo">"Foto machen"</string> |
||||
<string name="screen_room_attachment_source_camera_video">"Video aufnehmen"</string> |
||||
<string name="screen_room_attachment_source_files">"Anhang"</string> |
||||
<string name="screen_room_attachment_source_gallery">"Foto- und Videobibliothek"</string> |
||||
<string name="screen_room_attachment_source_location">"Standort"</string> |
||||
<string name="screen_room_attachment_source_poll">"Umfrage"</string> |
||||
<string name="screen_room_attachment_text_formatting">"Textformatierung"</string> |
||||
<string name="screen_room_encrypted_history_banner">"Der Nachrichtenverlauf ist derzeit in diesem Raum nicht verfügbar"</string> |
||||
<string name="screen_room_error_failed_retrieving_user_details">"Benutzerdetails konnten nicht abgerufen werden"</string> |
||||
<string name="screen_room_invite_again_alert_message">"Möchten Sie sie wieder einladen?"</string> |
||||
<string name="screen_room_invite_again_alert_title">"Sie sind allein in diesem Chat"</string> |
||||
<string name="screen_room_message_copied">"Nachricht wurde kopiert"</string> |
||||
<string name="screen_room_no_permission_to_post">"Sie sind nicht berechtigt, in diesem Raum zu posten"</string> |
||||
<string name="screen_room_notification_settings_allow_custom">"Benutzerdefinierte Einstellung zulassen"</string> |
||||
<string name="screen_room_notification_settings_allow_custom_footnote">"Wenn Sie diese Option aktivieren, wird Ihre Standardeinstellung außer Kraft gesetzt."</string> |
||||
<string name="screen_room_notification_settings_custom_settings_title">"Benachrichtigen Sie mich in diesem Chat bei"</string> |
||||
<string name="screen_room_notification_settings_default_setting_footnote">"Sie können das in Ihrem %1$s ändern."</string> |
||||
<string name="screen_room_notification_settings_default_setting_footnote_content_link">"Globale Einstellungen"</string> |
||||
<string name="screen_room_notification_settings_default_setting_title">"Standardeinstellung"</string> |
||||
<string name="screen_room_notification_settings_edit_remove_setting">"Benutzerdefinierte Einstellung entfernen"</string> |
||||
<string name="screen_room_notification_settings_error_loading_settings">"Beim Laden der Benachrichtigungseinstellungen ist ein Fehler aufgetreten."</string> |
||||
<string name="screen_room_notification_settings_error_restoring_default">"Fehler beim Wiederherstellen des Standardmodus. Bitte versuchen Sie es erneut."</string> |
||||
<string name="screen_room_notification_settings_error_setting_mode">"Fehler beim Einstellen des Modus. Bitte versuchen Sie es erneut."</string> |
||||
<string name="screen_room_notification_settings_mode_all_messages">"Alle Nachrichten"</string> |
||||
<string name="screen_room_notification_settings_mode_mentions_and_keywords">"Nur Erwähnungen und Schlüsselwörter"</string> |
||||
<string name="screen_room_notification_settings_room_custom_settings_title">"Benachrichtigen Sie mich in diesem Raum bei"</string> |
||||
<string name="screen_room_reactions_show_less">"Weniger anzeigen"</string> |
||||
<string name="screen_room_reactions_show_more">"Mehr anzeigen"</string> |
||||
<string name="screen_room_retry_send_menu_send_again_action">"Erneut senden"</string> |
||||
<string name="screen_room_retry_send_menu_title">"Ihre Nachricht konnte nicht gesendet werden"</string> |
||||
<string name="screen_room_timeline_add_reaction">"Emoji hinzufügen"</string> |
||||
<string name="screen_room_timeline_less_reactions">"Weniger anzeigen"</string> |
||||
<string name="screen_room_error_failed_processing_media">"Fehler beim Verarbeiten des hochgeladenen Mediums. Bitte versuchen Sie es erneut."</string> |
||||
<string name="screen_room_retry_send_menu_remove_action">"Entfernen"</string> |
||||
</resources> |
||||
|
@ -0,0 +1,10 @@
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?> |
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> |
||||
<string name="screen_onboarding_sign_in_manually">"Manuell anmelden"</string> |
||||
<string name="screen_onboarding_sign_in_with_qr_code">"Mit QR-Code anmelden"</string> |
||||
<string name="screen_onboarding_sign_up">"Konto erstellen"</string> |
||||
<string name="screen_onboarding_subtitle">"Sicher kommunizieren und zusammenarbeiten"</string> |
||||
<string name="screen_onboarding_welcome_message">"Willkommen beim schnellsten Element aller Zeiten. Optimiert für Geschwindigkeit und Einfachheit."</string> |
||||
<string name="screen_onboarding_welcome_subtitle">"Willkommen zu %1$s. Aufgeladen, für Geschwindigkeit und Einfachheit."</string> |
||||
<string name="screen_onboarding_welcome_title">"Seien Sie in Ihrem Element"</string> |
||||
</resources> |
@ -0,0 +1,56 @@
@@ -0,0 +1,56 @@
|
||||
/* |
||||
* Copyright (c) 2023 New Vector Ltd |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0 |
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package io.element.android.features.preferences.impl.user.editprofile |
||||
|
||||
import androidx.compose.runtime.Composable |
||||
import androidx.compose.ui.Modifier |
||||
import com.bumble.appyx.core.modality.BuildContext |
||||
import com.bumble.appyx.core.node.Node |
||||
import com.bumble.appyx.core.plugin.Plugin |
||||
import dagger.assisted.Assisted |
||||
import dagger.assisted.AssistedInject |
||||
import io.element.android.anvilannotations.ContributesNode |
||||
import io.element.android.libraries.architecture.NodeInputs |
||||
import io.element.android.libraries.architecture.inputs |
||||
import io.element.android.libraries.di.SessionScope |
||||
import io.element.android.libraries.matrix.api.user.MatrixUser |
||||
|
||||
@ContributesNode(SessionScope::class) |
||||
class EditUserProfileNode @AssistedInject constructor( |
||||
@Assisted buildContext: BuildContext, |
||||
@Assisted plugins: List<Plugin>, |
||||
presenterFactory: EditUserProfilePresenter.Factory, |
||||
) : Node(buildContext, plugins = plugins) { |
||||
|
||||
data class Inputs( |
||||
val matrixUser: MatrixUser |
||||
) : NodeInputs |
||||
|
||||
val matrixUser = inputs<Inputs>().matrixUser |
||||
val presenter = presenterFactory.create(matrixUser) |
||||
|
||||
@Composable |
||||
override fun View(modifier: Modifier) { |
||||
val state = presenter.present() |
||||
EditUserProfileView( |
||||
state = state, |
||||
onBackPressed = ::navigateUp, |
||||
onProfileEdited = ::navigateUp, |
||||
modifier = modifier |
||||
) |
||||
} |
||||
} |
@ -0,0 +1,149 @@
@@ -0,0 +1,149 @@
|
||||
/* |
||||
* Copyright (c) 2023 New Vector Ltd |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0 |
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package io.element.android.features.preferences.impl.user.editprofile |
||||
|
||||
import android.net.Uri |
||||
import androidx.compose.runtime.Composable |
||||
import androidx.compose.runtime.MutableState |
||||
import androidx.compose.runtime.derivedStateOf |
||||
import androidx.compose.runtime.getValue |
||||
import androidx.compose.runtime.mutableStateOf |
||||
import androidx.compose.runtime.remember |
||||
import androidx.compose.runtime.rememberCoroutineScope |
||||
import androidx.compose.runtime.saveable.rememberSaveable |
||||
import androidx.compose.runtime.setValue |
||||
import androidx.core.net.toUri |
||||
import dagger.assisted.Assisted |
||||
import dagger.assisted.AssistedFactory |
||||
import dagger.assisted.AssistedInject |
||||
import io.element.android.libraries.architecture.Async |
||||
import io.element.android.libraries.architecture.Presenter |
||||
import io.element.android.libraries.architecture.runCatchingUpdatingState |
||||
import io.element.android.libraries.core.mimetype.MimeTypes |
||||
import io.element.android.libraries.matrix.api.MatrixClient |
||||
import io.element.android.libraries.matrix.api.user.MatrixUser |
||||
import io.element.android.libraries.matrix.ui.media.AvatarAction |
||||
import io.element.android.libraries.mediapickers.api.PickerProvider |
||||
import io.element.android.libraries.mediaupload.api.MediaPreProcessor |
||||
import kotlinx.collections.immutable.toImmutableList |
||||
import kotlinx.coroutines.CoroutineScope |
||||
import kotlinx.coroutines.launch |
||||
import timber.log.Timber |
||||
|
||||
class EditUserProfilePresenter @AssistedInject constructor( |
||||
@Assisted private val matrixUser: MatrixUser, |
||||
private val matrixClient: MatrixClient, |
||||
private val mediaPickerProvider: PickerProvider, |
||||
private val mediaPreProcessor: MediaPreProcessor, |
||||
) : Presenter<EditUserProfileState> { |
||||
|
||||
@AssistedFactory |
||||
interface Factory { |
||||
fun create(matrixUser: MatrixUser): EditUserProfilePresenter |
||||
} |
||||
|
||||
@Composable |
||||
override fun present(): EditUserProfileState { |
||||
var userAvatarUri by rememberSaveable { mutableStateOf(matrixUser.avatarUrl?.let { Uri.parse(it) }) } |
||||
var userDisplayName by rememberSaveable { mutableStateOf(matrixUser.displayName) } |
||||
val cameraPhotoPicker = mediaPickerProvider.registerCameraPhotoPicker( |
||||
onResult = { uri -> if (uri != null) userAvatarUri = uri } |
||||
) |
||||
val galleryImagePicker = mediaPickerProvider.registerGalleryImagePicker( |
||||
onResult = { uri -> if (uri != null) userAvatarUri = uri } |
||||
) |
||||
|
||||
val avatarActions by remember(userAvatarUri) { |
||||
derivedStateOf { |
||||
listOfNotNull( |
||||
AvatarAction.TakePhoto, |
||||
AvatarAction.ChoosePhoto, |
||||
AvatarAction.Remove.takeIf { userAvatarUri != null }, |
||||
).toImmutableList() |
||||
} |
||||
} |
||||
|
||||
val saveAction: MutableState<Async<Unit>> = remember { mutableStateOf(Async.Uninitialized) } |
||||
val localCoroutineScope = rememberCoroutineScope() |
||||
fun handleEvents(event: EditUserProfileEvents) { |
||||
when (event) { |
||||
is EditUserProfileEvents.Save -> localCoroutineScope.saveChanges(userDisplayName, userAvatarUri, matrixUser, saveAction) |
||||
is EditUserProfileEvents.HandleAvatarAction -> { |
||||
when (event.action) { |
||||
AvatarAction.ChoosePhoto -> galleryImagePicker.launch() |
||||
AvatarAction.TakePhoto -> cameraPhotoPicker.launch() |
||||
AvatarAction.Remove -> userAvatarUri = null |
||||
} |
||||
} |
||||
|
||||
is EditUserProfileEvents.UpdateDisplayName -> userDisplayName = event.name |
||||
EditUserProfileEvents.CancelSaveChanges -> saveAction.value = Async.Uninitialized |
||||
} |
||||
} |
||||
|
||||
val canSave = remember(userDisplayName, userAvatarUri) { |
||||
val hasProfileChanged = hasDisplayNameChanged(userDisplayName, matrixUser) || |
||||
hasAvatarUrlChanged(userAvatarUri, matrixUser) |
||||
!userDisplayName.isNullOrBlank() && hasProfileChanged |
||||
} |
||||
|
||||
return EditUserProfileState( |
||||
userId = matrixUser.userId, |
||||
displayName = userDisplayName.orEmpty(), |
||||
userAvatarUrl = userAvatarUri, |
||||
avatarActions = avatarActions, |
||||
saveButtonEnabled = canSave && saveAction.value !is Async.Loading, |
||||
saveAction = saveAction.value, |
||||
eventSink = { handleEvents(it) }, |
||||
) |
||||
} |
||||
|
||||
private fun hasDisplayNameChanged(name: String?, currentUser: MatrixUser) = |
||||
name?.trim() != currentUser.displayName?.trim() |
||||
|
||||
private fun hasAvatarUrlChanged(avatarUri: Uri?, currentUser: MatrixUser) = |
||||
// Need to call `toUri()?.toString()` to make the test pass (we mockk Uri) |
||||
avatarUri?.toString()?.trim() != currentUser.avatarUrl?.toUri()?.toString()?.trim() |
||||
|
||||
private fun CoroutineScope.saveChanges(name: String?, avatarUri: Uri?, currentUser: MatrixUser, action: MutableState<Async<Unit>>) = launch { |
||||
val results = mutableListOf<Result<Unit>>() |
||||
suspend { |
||||
if (!name.isNullOrEmpty() && name.trim() != currentUser.displayName.orEmpty().trim()) { |
||||
results.add(matrixClient.setDisplayName(name).onFailure { |
||||
Timber.e(it, "Failed to set user's display name") |
||||
}) |
||||
} |
||||
if (avatarUri?.toString()?.trim() != currentUser.avatarUrl?.trim()) { |
||||
results.add(updateAvatar(avatarUri).onFailure { |
||||
Timber.e(it, "Failed to update user's avatar") |
||||
}) |
||||
} |
||||
if (results.all { it.isSuccess }) Unit else results.first { it.isFailure }.getOrThrow() |
||||
}.runCatchingUpdatingState(action) |
||||
} |
||||
|
||||
private suspend fun updateAvatar(avatarUri: Uri?): Result<Unit> { |
||||
return runCatching { |
||||
if (avatarUri != null) { |
||||
val preprocessed = mediaPreProcessor.process(avatarUri, MimeTypes.Jpeg, compressIfPossible = false).getOrThrow() |
||||
matrixClient.uploadAvatar(MimeTypes.Jpeg, preprocessed.file.readBytes()).getOrThrow() |
||||
} else { |
||||
matrixClient.removeAvatar().getOrThrow() |
||||
} |
||||
}.onFailure { Timber.e(it, "Unable to update avatar") } |
||||
} |
||||
} |
@ -0,0 +1,33 @@
@@ -0,0 +1,33 @@
|
||||
/* |
||||
* Copyright (c) 2023 New Vector Ltd |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0 |
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package io.element.android.features.preferences.impl.user.editprofile |
||||
|
||||
import android.net.Uri |
||||
import io.element.android.libraries.architecture.Async |
||||
import io.element.android.libraries.matrix.api.core.UserId |
||||
import io.element.android.libraries.matrix.ui.media.AvatarAction |
||||
import kotlinx.collections.immutable.ImmutableList |
||||
|
||||
data class EditUserProfileState( |
||||
val userId: UserId?, |
||||
val displayName: String, |
||||
val userAvatarUrl: Uri?, |
||||
val avatarActions: ImmutableList<AvatarAction>, |
||||
val saveButtonEnabled: Boolean, |
||||
val saveAction: Async<Unit>, |
||||
val eventSink: (EditUserProfileEvents) -> Unit |
||||
) |
@ -0,0 +1,40 @@
@@ -0,0 +1,40 @@
|
||||
/* |
||||
* Copyright (c) 2023 New Vector Ltd |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0 |
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package io.element.android.features.preferences.impl.user.editprofile |
||||
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider |
||||
import io.element.android.libraries.architecture.Async |
||||
import io.element.android.libraries.matrix.api.core.UserId |
||||
import kotlinx.collections.immutable.persistentListOf |
||||
|
||||
open class EditUserProfileStateProvider : PreviewParameterProvider<EditUserProfileState> { |
||||
override val values: Sequence<EditUserProfileState> |
||||
get() = sequenceOf( |
||||
aEditUserProfileState(), |
||||
// Add other states here |
||||
) |
||||
} |
||||
|
||||
fun aEditUserProfileState() = EditUserProfileState( |
||||
userId = UserId("@john.doe:matrix.org"), |
||||
displayName = "John Doe", |
||||
userAvatarUrl = null, |
||||
avatarActions = persistentListOf(), |
||||
saveAction = Async.Uninitialized, |
||||
saveButtonEnabled = true, |
||||
eventSink = {} |
||||
) |
@ -0,0 +1,190 @@
@@ -0,0 +1,190 @@
|
||||
/* |
||||
* Copyright (c) 2023 New Vector Ltd |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0 |
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package io.element.android.features.preferences.impl.user.editprofile |
||||
|
||||
import androidx.compose.foundation.gestures.detectTapGestures |
||||
import androidx.compose.foundation.layout.Column |
||||
import androidx.compose.foundation.layout.Spacer |
||||
import androidx.compose.foundation.layout.fillMaxWidth |
||||
import androidx.compose.foundation.layout.height |
||||
import androidx.compose.foundation.layout.imePadding |
||||
import androidx.compose.foundation.layout.navigationBarsPadding |
||||
import androidx.compose.foundation.layout.padding |
||||
import androidx.compose.foundation.rememberScrollState |
||||
import androidx.compose.foundation.verticalScroll |
||||
import androidx.compose.material.ExperimentalMaterialApi |
||||
import androidx.compose.material.ModalBottomSheetValue |
||||
import androidx.compose.material.rememberModalBottomSheetState |
||||
import androidx.compose.material3.ExperimentalMaterial3Api |
||||
import androidx.compose.runtime.Composable |
||||
import androidx.compose.runtime.LaunchedEffect |
||||
import androidx.compose.runtime.rememberCoroutineScope |
||||
import androidx.compose.ui.Alignment |
||||
import androidx.compose.ui.Modifier |
||||
import androidx.compose.ui.focus.FocusManager |
||||
import androidx.compose.ui.input.pointer.pointerInput |
||||
import androidx.compose.ui.platform.LocalFocusManager |
||||
import androidx.compose.ui.res.stringResource |
||||
import androidx.compose.ui.text.style.TextAlign |
||||
import androidx.compose.ui.tooling.preview.PreviewParameter |
||||
import androidx.compose.ui.unit.dp |
||||
import io.element.android.features.preferences.impl.R |
||||
import io.element.android.libraries.architecture.Async |
||||
import io.element.android.libraries.designsystem.components.LabelledOutlinedTextField |
||||
import io.element.android.libraries.designsystem.components.ProgressDialog |
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarSize |
||||
import io.element.android.libraries.designsystem.components.button.BackButton |
||||
import io.element.android.libraries.designsystem.components.dialogs.ErrorDialog |
||||
import io.element.android.libraries.designsystem.preview.DayNightPreviews |
||||
import io.element.android.libraries.designsystem.preview.ElementPreview |
||||
import io.element.android.libraries.designsystem.theme.aliasScreenTitle |
||||
import io.element.android.libraries.designsystem.theme.components.Scaffold |
||||
import io.element.android.libraries.designsystem.theme.components.Text |
||||
import io.element.android.libraries.designsystem.theme.components.TextButton |
||||
import io.element.android.libraries.designsystem.theme.components.TopAppBar |
||||
import io.element.android.libraries.matrix.ui.components.AvatarActionBottomSheet |
||||
import io.element.android.libraries.matrix.ui.components.EditableAvatarView |
||||
import io.element.android.libraries.theme.ElementTheme |
||||
import io.element.android.libraries.ui.strings.CommonStrings |
||||
import kotlinx.coroutines.launch |
||||
|
||||
@OptIn(ExperimentalMaterialApi::class, ExperimentalMaterial3Api::class) |
||||
@Composable |
||||
fun EditUserProfileView( |
||||
state: EditUserProfileState, |
||||
onBackPressed: () -> Unit, |
||||
onProfileEdited: () -> Unit, |
||||
modifier: Modifier = Modifier, |
||||
) { |
||||
val coroutineScope = rememberCoroutineScope() |
||||
val focusManager = LocalFocusManager.current |
||||
val itemActionsBottomSheetState = rememberModalBottomSheetState( |
||||
initialValue = ModalBottomSheetValue.Hidden, |
||||
) |
||||
|
||||
fun onAvatarClicked() { |
||||
focusManager.clearFocus() |
||||
coroutineScope.launch { |
||||
itemActionsBottomSheetState.show() |
||||
} |
||||
} |
||||
|
||||
Scaffold( |
||||
modifier = modifier.clearFocusOnTap(focusManager), |
||||
topBar = { |
||||
TopAppBar( |
||||
title = { |
||||
Text( |
||||
text = stringResource(R.string.screen_edit_profile_title), |
||||
style = ElementTheme.typography.aliasScreenTitle, |
||||
) |
||||
}, |
||||
navigationIcon = { BackButton(onClick = onBackPressed) }, |
||||
actions = { |
||||
TextButton( |
||||
text = stringResource(CommonStrings.action_save), |
||||
enabled = state.saveButtonEnabled, |
||||
onClick = { |
||||
focusManager.clearFocus() |
||||
state.eventSink(EditUserProfileEvents.Save) |
||||
}, |
||||
) |
||||
} |
||||
) |
||||
}, |
||||
) { padding -> |
||||
Column( |
||||
modifier = Modifier |
||||
.padding(padding) |
||||
.padding(horizontal = 16.dp) |
||||
.navigationBarsPadding() |
||||
.imePadding() |
||||
.verticalScroll(rememberScrollState()) |
||||
) { |
||||
Spacer(modifier = Modifier.height(24.dp)) |
||||
EditableAvatarView( |
||||
userId = state.userId?.value, |
||||
displayName = state.displayName, |
||||
avatarUrl = state.userAvatarUrl, |
||||
avatarSize = AvatarSize.RoomHeader, |
||||
onAvatarClicked = { onAvatarClicked() }, |
||||
modifier = Modifier.align(Alignment.CenterHorizontally), |
||||
) |
||||
Spacer(modifier = Modifier.height(16.dp)) |
||||
state.userId?.let { |
||||
Text( |
||||
modifier = Modifier.fillMaxWidth(), |
||||
text = it.value, |
||||
style = ElementTheme.typography.fontBodyLgRegular, |
||||
textAlign = TextAlign.Center, |
||||
) |
||||
} |
||||
Spacer(modifier = Modifier.height(40.dp)) |
||||
LabelledOutlinedTextField( |
||||
label = stringResource(R.string.screen_edit_profile_display_name), |
||||
value = state.displayName, |
||||
placeholder = stringResource(CommonStrings.common_room_name_placeholder), |
||||
singleLine = true, |
||||
onValueChange = { state.eventSink(EditUserProfileEvents.UpdateDisplayName(it)) }, |
||||
) |
||||
} |
||||
|
||||
AvatarActionBottomSheet( |
||||
actions = state.avatarActions, |
||||
modalBottomSheetState = itemActionsBottomSheetState, |
||||
onActionSelected = { state.eventSink(EditUserProfileEvents.HandleAvatarAction(it)) } |
||||
) |
||||
|
||||
when (state.saveAction) { |
||||
is Async.Loading -> { |
||||
ProgressDialog(text = stringResource(R.string.screen_edit_profile_updating_details)) |
||||
} |
||||
is Async.Failure -> { |
||||
ErrorDialog( |
||||
title = stringResource(R.string.screen_edit_profile_error_title), |
||||
content = stringResource(R.string.screen_edit_profile_error), |
||||
onDismiss = { state.eventSink(EditUserProfileEvents.CancelSaveChanges) }, |
||||
) |
||||
} |
||||
is Async.Success -> { |
||||
LaunchedEffect(state.saveAction) { |
||||
onProfileEdited() |
||||
} |
||||
} |
||||
else -> Unit |
||||
} |
||||
} |
||||
} |
||||
|
||||
private fun Modifier.clearFocusOnTap(focusManager: FocusManager): Modifier = |
||||
pointerInput(Unit) { |
||||
detectTapGestures(onTap = { |
||||
focusManager.clearFocus() |
||||
}) |
||||
} |
||||
|
||||
@DayNightPreviews |
||||
@Composable |
||||
internal fun EditUserProfileViewPreview(@PreviewParameter(EditUserProfileStateProvider::class) state: EditUserProfileState) = |
||||
ElementPreview { |
||||
EditUserProfileView( |
||||
onBackPressed = {}, |
||||
onProfileEdited = {}, |
||||
state = state, |
||||
) |
||||
} |
||||
|
@ -0,0 +1,9 @@
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?> |
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> |
||||
<string name="screen_edit_profile_display_name">"Display name"</string> |
||||
<string name="screen_edit_profile_display_name_placeholder">"Your display name"</string> |
||||
<string name="screen_edit_profile_error">"An unknown error was encountered and the information couldn\'t be changed."</string> |
||||
<string name="screen_edit_profile_error_title">"Unable to update profile"</string> |
||||
<string name="screen_edit_profile_title">"Edit profile"</string> |
||||
<string name="screen_edit_profile_updating_details">"Updating profile…"</string> |
||||
</resources> |
@ -0,0 +1,429 @@
@@ -0,0 +1,429 @@
|
||||
/* |
||||
* Copyright (c) 2023 New Vector Ltd |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0 |
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package io.element.android.features.preferences.impl.user.editprofile |
||||
|
||||
import android.net.Uri |
||||
import app.cash.molecule.RecompositionMode |
||||
import app.cash.molecule.moleculeFlow |
||||
import app.cash.turbine.test |
||||
import com.google.common.truth.Truth.assertThat |
||||
import io.element.android.libraries.architecture.Async |
||||
import io.element.android.libraries.matrix.api.MatrixClient |
||||
import io.element.android.libraries.matrix.api.user.MatrixUser |
||||
import io.element.android.libraries.matrix.test.AN_AVATAR_URL |
||||
import io.element.android.libraries.matrix.test.A_USER_ID |
||||
import io.element.android.libraries.matrix.test.FakeMatrixClient |
||||
import io.element.android.libraries.matrix.ui.components.aMatrixUser |
||||
import io.element.android.libraries.matrix.ui.media.AvatarAction |
||||
import io.element.android.libraries.mediapickers.test.FakePickerProvider |
||||
import io.element.android.libraries.mediaupload.api.MediaUploadInfo |
||||
import io.element.android.libraries.mediaupload.test.FakeMediaPreProcessor |
||||
import io.element.android.tests.testutils.WarmUpRule |
||||
import io.element.android.tests.testutils.consumeItemsUntilPredicate |
||||
import io.mockk.every |
||||
import io.mockk.mockk |
||||
import io.mockk.mockkStatic |
||||
import io.mockk.unmockkAll |
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi |
||||
import kotlinx.coroutines.test.runTest |
||||
import org.junit.After |
||||
import org.junit.Before |
||||
import org.junit.Rule |
||||
import org.junit.Test |
||||
import java.io.File |
||||
|
||||
@ExperimentalCoroutinesApi |
||||
class EditUserProfilePresenterTest { |
||||
|
||||
@get:Rule |
||||
val warmUpRule = WarmUpRule() |
||||
|
||||
private lateinit var fakePickerProvider: FakePickerProvider |
||||
private lateinit var fakeMediaPreProcessor: FakeMediaPreProcessor |
||||
|
||||
private val userAvatarUri: Uri = mockk() |
||||
private val anotherAvatarUri: Uri = mockk() |
||||
|
||||
private val fakeFileContents = ByteArray(2) |
||||
|
||||
@Before |
||||
fun setup() { |
||||
fakePickerProvider = FakePickerProvider() |
||||
fakeMediaPreProcessor = FakeMediaPreProcessor() |
||||
mockkStatic(Uri::class) |
||||
|
||||
every { Uri.parse(AN_AVATAR_URL) } returns userAvatarUri |
||||
every { Uri.parse(ANOTHER_AVATAR_URL) } returns anotherAvatarUri |
||||
} |
||||
|
||||
@After |
||||
fun tearDown() { |
||||
unmockkAll() |
||||
} |
||||
|
||||
private fun createEditUserProfilePresenter( |
||||
matrixClient: MatrixClient = FakeMatrixClient(), |
||||
matrixUser: MatrixUser = aMatrixUser(), |
||||
): EditUserProfilePresenter { |
||||
return EditUserProfilePresenter( |
||||
matrixClient = matrixClient, |
||||
matrixUser = matrixUser, |
||||
mediaPickerProvider = fakePickerProvider, |
||||
mediaPreProcessor = fakeMediaPreProcessor, |
||||
) |
||||
} |
||||
|
||||
@Test |
||||
fun `present - initial state is created from user info`() = runTest { |
||||
val user = aMatrixUser(avatarUrl = AN_AVATAR_URL) |
||||
val presenter = createEditUserProfilePresenter(matrixUser = user) |
||||
moleculeFlow(RecompositionMode.Immediate) { |
||||
presenter.present() |
||||
}.test { |
||||
val initialState = awaitItem() |
||||
assertThat(initialState.userId).isEqualTo(user.userId) |
||||
assertThat(initialState.displayName).isEqualTo(user.displayName) |
||||
assertThat(initialState.userAvatarUrl).isEqualTo(userAvatarUri) |
||||
assertThat(initialState.avatarActions).containsExactly( |
||||
AvatarAction.ChoosePhoto, |
||||
AvatarAction.TakePhoto, |
||||
AvatarAction.Remove |
||||
) |
||||
assertThat(initialState.saveButtonEnabled).isFalse() |
||||
assertThat(initialState.saveAction).isInstanceOf(Async.Uninitialized::class.java) |
||||
} |
||||
} |
||||
|
||||
@Test |
||||
fun `present - updates state in response to changes`() = runTest { |
||||
val user = aMatrixUser(id = A_USER_ID.value, displayName = "Name", avatarUrl = AN_AVATAR_URL) |
||||
val presenter = createEditUserProfilePresenter(matrixUser = user) |
||||
moleculeFlow(RecompositionMode.Immediate) { |
||||
presenter.present() |
||||
}.test { |
||||
val initialState = awaitItem() |
||||
assertThat(initialState.displayName).isEqualTo("Name") |
||||
assertThat(initialState.userAvatarUrl).isEqualTo(userAvatarUri) |
||||
initialState.eventSink(EditUserProfileEvents.UpdateDisplayName("Name II")) |
||||
awaitItem().apply { |
||||
assertThat(displayName).isEqualTo("Name II") |
||||
assertThat(userAvatarUrl).isEqualTo(userAvatarUri) |
||||
} |
||||
initialState.eventSink(EditUserProfileEvents.UpdateDisplayName("Name III")) |
||||
awaitItem().apply { |
||||
assertThat(displayName).isEqualTo("Name III") |
||||
assertThat(userAvatarUrl).isEqualTo(userAvatarUri) |
||||
} |
||||
initialState.eventSink(EditUserProfileEvents.HandleAvatarAction(AvatarAction.Remove)) |
||||
awaitItem().apply { |
||||
assertThat(displayName).isEqualTo("Name III") |
||||
assertThat(userAvatarUrl).isNull() |
||||
} |
||||
} |
||||
} |
||||
|
||||
@Test |
||||
fun `present - obtains avatar uris from gallery`() = runTest { |
||||
val user = aMatrixUser(id = A_USER_ID.value, displayName = "Name", avatarUrl = AN_AVATAR_URL) |
||||
fakePickerProvider.givenResult(anotherAvatarUri) |
||||
val presenter = createEditUserProfilePresenter(matrixUser = user) |
||||
moleculeFlow(RecompositionMode.Immediate) { |
||||
presenter.present() |
||||
}.test { |
||||
val initialState = awaitItem() |
||||
assertThat(initialState.userAvatarUrl).isEqualTo(userAvatarUri) |
||||
initialState.eventSink(EditUserProfileEvents.HandleAvatarAction(AvatarAction.ChoosePhoto)) |
||||
awaitItem().apply { |
||||
assertThat(userAvatarUrl).isEqualTo(anotherAvatarUri) |
||||
} |
||||
} |
||||
} |
||||
|
||||
@Test |
||||
fun `present - obtains avatar uris from camera`() = runTest { |
||||
val user = aMatrixUser(id = A_USER_ID.value, displayName = "Name", avatarUrl = AN_AVATAR_URL) |
||||
fakePickerProvider.givenResult(anotherAvatarUri) |
||||
val presenter = createEditUserProfilePresenter(matrixUser = user) |
||||
moleculeFlow(RecompositionMode.Immediate) { |
||||
presenter.present() |
||||
}.test { |
||||
val initialState = awaitItem() |
||||
assertThat(initialState.userAvatarUrl).isEqualTo(userAvatarUri) |
||||
initialState.eventSink(EditUserProfileEvents.HandleAvatarAction(AvatarAction.TakePhoto)) |
||||
awaitItem().apply { |
||||
assertThat(userAvatarUrl).isEqualTo(anotherAvatarUri) |
||||
} |
||||
} |
||||
} |
||||
|
||||
@Test |
||||
fun `present - updates save button state`() = runTest { |
||||
val user = aMatrixUser(id = A_USER_ID.value, displayName = "Name", avatarUrl = AN_AVATAR_URL) |
||||
fakePickerProvider.givenResult(userAvatarUri) |
||||
val presenter = createEditUserProfilePresenter(matrixUser = user) |
||||
moleculeFlow(RecompositionMode.Immediate) { |
||||
presenter.present() |
||||
}.test { |
||||
val initialState = awaitItem() |
||||
assertThat(initialState.saveButtonEnabled).isFalse() |
||||
// Once a change is made, the save button is enabled |
||||
initialState.eventSink(EditUserProfileEvents.UpdateDisplayName("Name II")) |
||||
awaitItem().apply { |
||||
assertThat(saveButtonEnabled).isTrue() |
||||
} |
||||
// If it's reverted then the save disables again |
||||
initialState.eventSink(EditUserProfileEvents.UpdateDisplayName("Name")) |
||||
awaitItem().apply { |
||||
assertThat(saveButtonEnabled).isFalse() |
||||
} |
||||
// Make a change... |
||||
initialState.eventSink(EditUserProfileEvents.HandleAvatarAction(AvatarAction.Remove)) |
||||
awaitItem().apply { |
||||
assertThat(saveButtonEnabled).isTrue() |
||||
} |
||||
// Revert it... |
||||
initialState.eventSink(EditUserProfileEvents.HandleAvatarAction(AvatarAction.ChoosePhoto)) |
||||
awaitItem().apply { |
||||
assertThat(saveButtonEnabled).isFalse() |
||||
} |
||||
} |
||||
} |
||||
|
||||
@Test |
||||
fun `present - updates save button state when initial values are null`() = runTest { |
||||
val user = aMatrixUser(id = A_USER_ID.value, displayName = "Name", avatarUrl = null) |
||||
fakePickerProvider.givenResult(userAvatarUri) |
||||
val presenter = createEditUserProfilePresenter(matrixUser = user) |
||||
moleculeFlow(RecompositionMode.Immediate) { |
||||
presenter.present() |
||||
}.test { |
||||
val initialState = awaitItem() |
||||
assertThat(initialState.saveButtonEnabled).isFalse() |
||||
// Once a change is made, the save button is enabled |
||||
initialState.eventSink(EditUserProfileEvents.UpdateDisplayName("Name II")) |
||||
awaitItem().apply { |
||||
assertThat(saveButtonEnabled).isTrue() |
||||
} |
||||
// If it's reverted then the save disables again |
||||
initialState.eventSink(EditUserProfileEvents.UpdateDisplayName("Name")) |
||||
awaitItem().apply { |
||||
assertThat(saveButtonEnabled).isFalse() |
||||
} |
||||
// Make a change... |
||||
initialState.eventSink(EditUserProfileEvents.HandleAvatarAction(AvatarAction.ChoosePhoto)) |
||||
awaitItem().apply { |
||||
assertThat(saveButtonEnabled).isTrue() |
||||
} |
||||
// Revert it... |
||||
initialState.eventSink(EditUserProfileEvents.HandleAvatarAction(AvatarAction.Remove)) |
||||
awaitItem().apply { |
||||
assertThat(saveButtonEnabled).isFalse() |
||||
} |
||||
} |
||||
} |
||||
|
||||
@Test |
||||
fun `present - save changes room details if different`() = runTest { |
||||
val matrixClient = FakeMatrixClient() |
||||
val user = aMatrixUser(id = A_USER_ID.value, displayName = "Name", avatarUrl = AN_AVATAR_URL) |
||||
val presenter = createEditUserProfilePresenter( |
||||
matrixClient = matrixClient, |
||||
matrixUser = user |
||||
) |
||||
moleculeFlow(RecompositionMode.Immediate) { |
||||
presenter.present() |
||||
}.test { |
||||
val initialState = awaitItem() |
||||
initialState.eventSink(EditUserProfileEvents.UpdateDisplayName("New name")) |
||||
initialState.eventSink(EditUserProfileEvents.HandleAvatarAction(AvatarAction.Remove)) |
||||
initialState.eventSink(EditUserProfileEvents.Save) |
||||
consumeItemsUntilPredicate { matrixClient.setDisplayNameCalled && matrixClient.removeAvatarCalled && !matrixClient.uploadAvatarCalled } |
||||
assertThat(matrixClient.setDisplayNameCalled).isTrue() |
||||
assertThat(matrixClient.removeAvatarCalled).isTrue() |
||||
assertThat(matrixClient.uploadAvatarCalled).isFalse() |
||||
cancelAndIgnoreRemainingEvents() |
||||
} |
||||
} |
||||
|
||||
@Test |
||||
fun `present - save does not change room details if they're the same trimmed`() = runTest { |
||||
val matrixClient = FakeMatrixClient() |
||||
val user = aMatrixUser(id = A_USER_ID.value, displayName = "Name", avatarUrl = AN_AVATAR_URL) |
||||
val presenter = createEditUserProfilePresenter( |
||||
matrixClient = matrixClient, |
||||
matrixUser = user |
||||
) |
||||
moleculeFlow(RecompositionMode.Immediate) { |
||||
presenter.present() |
||||
}.test { |
||||
val initialState = awaitItem() |
||||
initialState.eventSink(EditUserProfileEvents.UpdateDisplayName(" Name ")) |
||||
initialState.eventSink(EditUserProfileEvents.Save) |
||||
consumeItemsUntilPredicate { matrixClient.setDisplayNameCalled && !matrixClient.removeAvatarCalled && !matrixClient.uploadAvatarCalled } |
||||
assertThat(matrixClient.setDisplayNameCalled).isFalse() |
||||
assertThat(matrixClient.uploadAvatarCalled).isFalse() |
||||
assertThat(matrixClient.removeAvatarCalled).isFalse() |
||||
cancelAndIgnoreRemainingEvents() |
||||
} |
||||
} |
||||
|
||||
@Test |
||||
fun `present - save does not change name if it's now empty`() = runTest { |
||||
val matrixClient = FakeMatrixClient() |
||||
val user = aMatrixUser(id = A_USER_ID.value, displayName = "Name", avatarUrl = AN_AVATAR_URL) |
||||
val presenter = createEditUserProfilePresenter( |
||||
matrixClient = matrixClient, |
||||
matrixUser = user |
||||
) |
||||
moleculeFlow(RecompositionMode.Immediate) { |
||||
presenter.present() |
||||
}.test { |
||||
val initialState = awaitItem() |
||||
initialState.eventSink(EditUserProfileEvents.UpdateDisplayName("")) |
||||
initialState.eventSink(EditUserProfileEvents.Save) |
||||
assertThat(matrixClient.setDisplayNameCalled).isFalse() |
||||
assertThat(matrixClient.uploadAvatarCalled).isFalse() |
||||
assertThat(matrixClient.removeAvatarCalled).isFalse() |
||||
cancelAndIgnoreRemainingEvents() |
||||
} |
||||
} |
||||
|
||||
@Test |
||||
fun `present - save processes and sets avatar when processor returns successfully`() = runTest { |
||||
val matrixClient = FakeMatrixClient() |
||||
val user = aMatrixUser(id = A_USER_ID.value, displayName = "Name", avatarUrl = AN_AVATAR_URL) |
||||
givenPickerReturnsFile() |
||||
val presenter = createEditUserProfilePresenter( |
||||
matrixClient = matrixClient, |
||||
matrixUser = user |
||||
) |
||||
moleculeFlow(RecompositionMode.Immediate) { |
||||
presenter.present() |
||||
}.test { |
||||
val initialState = awaitItem() |
||||
initialState.eventSink(EditUserProfileEvents.HandleAvatarAction(AvatarAction.ChoosePhoto)) |
||||
initialState.eventSink(EditUserProfileEvents.Save) |
||||
consumeItemsUntilPredicate { matrixClient.uploadAvatarCalled } |
||||
assertThat(matrixClient.uploadAvatarCalled).isTrue() |
||||
} |
||||
} |
||||
|
||||
@Test |
||||
fun `present - save does not set avatar data if processor fails`() = runTest { |
||||
val matrixClient = FakeMatrixClient() |
||||
val user = aMatrixUser(id = A_USER_ID.value, displayName = "Name", avatarUrl = AN_AVATAR_URL) |
||||
val presenter = createEditUserProfilePresenter( |
||||
matrixClient = matrixClient, |
||||
matrixUser = user |
||||
) |
||||
fakePickerProvider.givenResult(anotherAvatarUri) |
||||
fakeMediaPreProcessor.givenResult(Result.failure(Throwable("Oh no"))) |
||||
moleculeFlow(RecompositionMode.Immediate) { |
||||
presenter.present() |
||||
}.test { |
||||
val initialState = awaitItem() |
||||
initialState.eventSink(EditUserProfileEvents.HandleAvatarAction(AvatarAction.ChoosePhoto)) |
||||
initialState.eventSink(EditUserProfileEvents.Save) |
||||
skipItems(2) |
||||
assertThat(matrixClient.uploadAvatarCalled).isFalse() |
||||
assertThat(awaitItem().saveAction).isInstanceOf(Async.Failure::class.java) |
||||
} |
||||
} |
||||
|
||||
@Test |
||||
fun `present - sets save action to failure if name update fails`() = runTest { |
||||
val user = aMatrixUser(id = A_USER_ID.value, displayName = "Name", avatarUrl = AN_AVATAR_URL) |
||||
val matrixClient = FakeMatrixClient().apply { |
||||
givenSetDisplayNameResult(Result.failure(Throwable("!"))) |
||||
} |
||||
saveAndAssertFailure(user, matrixClient, EditUserProfileEvents.UpdateDisplayName("New name")) |
||||
} |
||||
|
||||
@Test |
||||
fun `present - sets save action to failure if removing avatar fails`() = runTest { |
||||
val user = aMatrixUser(id = A_USER_ID.value, displayName = "Name", avatarUrl = AN_AVATAR_URL) |
||||
val matrixClient = FakeMatrixClient().apply { |
||||
givenRemoveAvatarResult(Result.failure(Throwable("!"))) |
||||
} |
||||
saveAndAssertFailure(user, matrixClient, EditUserProfileEvents.HandleAvatarAction(AvatarAction.Remove)) |
||||
} |
||||
|
||||
@Test |
||||
fun `present - sets save action to failure if setting avatar fails`() = runTest { |
||||
givenPickerReturnsFile() |
||||
val user = aMatrixUser(id = A_USER_ID.value, displayName = "Name", avatarUrl = AN_AVATAR_URL) |
||||
val matrixClient = FakeMatrixClient().apply { |
||||
givenUploadAvatarResult(Result.failure(Throwable("!"))) |
||||
} |
||||
saveAndAssertFailure(user, matrixClient, EditUserProfileEvents.HandleAvatarAction(AvatarAction.ChoosePhoto)) |
||||
} |
||||
|
||||
@Test |
||||
fun `present - CancelSaveChanges resets save action state`() = runTest { |
||||
givenPickerReturnsFile() |
||||
val user = aMatrixUser(id = A_USER_ID.value, displayName = "Name", avatarUrl = AN_AVATAR_URL) |
||||
val matrixClient = FakeMatrixClient().apply { |
||||
givenSetDisplayNameResult(Result.failure(Throwable("!"))) |
||||
} |
||||
val presenter = createEditUserProfilePresenter(matrixUser = user, matrixClient = matrixClient) |
||||
moleculeFlow(RecompositionMode.Immediate) { |
||||
presenter.present() |
||||
}.test { |
||||
val initialState = awaitItem() |
||||
initialState.eventSink(EditUserProfileEvents.UpdateDisplayName("foo")) |
||||
initialState.eventSink(EditUserProfileEvents.Save) |
||||
skipItems(2) |
||||
assertThat(awaitItem().saveAction).isInstanceOf(Async.Failure::class.java) |
||||
initialState.eventSink(EditUserProfileEvents.CancelSaveChanges) |
||||
assertThat(awaitItem().saveAction).isInstanceOf(Async.Uninitialized::class.java) |
||||
} |
||||
} |
||||
|
||||
private suspend fun saveAndAssertFailure(matrixUser: MatrixUser, matrixClient: MatrixClient, event: EditUserProfileEvents) { |
||||
val presenter = createEditUserProfilePresenter(matrixUser = matrixUser, matrixClient = matrixClient) |
||||
moleculeFlow(RecompositionMode.Immediate) { |
||||
presenter.present() |
||||
}.test { |
||||
val initialState = awaitItem() |
||||
initialState.eventSink(event) |
||||
initialState.eventSink(EditUserProfileEvents.Save) |
||||
skipItems(1) |
||||
assertThat(awaitItem().saveAction).isInstanceOf(Async.Loading::class.java) |
||||
assertThat(awaitItem().saveAction).isInstanceOf(Async.Failure::class.java) |
||||
} |
||||
} |
||||
|
||||
private fun givenPickerReturnsFile() { |
||||
mockkStatic(File::readBytes) |
||||
val processedFile: File = mockk { |
||||
every { readBytes() } returns fakeFileContents |
||||
} |
||||
fakePickerProvider.givenResult(anotherAvatarUri) |
||||
fakeMediaPreProcessor.givenResult( |
||||
Result.success( |
||||
MediaUploadInfo.AnyFile( |
||||
file = processedFile, |
||||
fileInfo = mockk(), |
||||
) |
||||
) |
||||
) |
||||
} |
||||
|
||||
companion object { |
||||
private const val ANOTHER_AVATAR_URL = "example://camera/foo.jpg" |
||||
} |
||||
} |
@ -0,0 +1,5 @@
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?> |
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> |
||||
<string name="crash_detection_dialog_content">"%1$s ist bei der letzten Nutzung abgestürzt. Möchten Sie einen Absturzbericht mit uns teilen?"</string> |
||||
<string name="rageshake_detection_dialog_content">"Sie scheinen das Telefon aus Frustration zu schütteln. Möchten Sie den Bildschirm für den Fehlerbericht öffnen?"</string> |
||||
</resources> |
@ -0,0 +1,15 @@
@@ -0,0 +1,15 @@
|
||||
<?xml version="1.0" encoding="utf-8"?> |
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> |
||||
<string name="screen_bug_report_attach_screenshot">"Bildschirmfoto anhängen"</string> |
||||
<string name="screen_bug_report_contact_me">"Sie können mich kontaktieren, wenn Sie weitere Fragen haben."</string> |
||||
<string name="screen_bug_report_contact_me_title">"Kontaktieren Sie mich"</string> |
||||
<string name="screen_bug_report_edit_screenshot">"Bildschirmfoto bearbeiten"</string> |
||||
<string name="screen_bug_report_editor_description">"Bitte beschreiben Sie den Fehler. Was haben Sie getan? Was haben Sie erwartet, was passiert? Was ist tatsächlich passiert. Bitte gehen Sie so detailliert wie möglich vor."</string> |
||||
<string name="screen_bug_report_editor_placeholder">"Beschreiben Sie den Fehler…"</string> |
||||
<string name="screen_bug_report_editor_supporting">"Wenn möglich, verfassen Sie die Beschreibung bitte auf Englisch."</string> |
||||
<string name="screen_bug_report_include_crash_logs">"Absturzprotokolle senden"</string> |
||||
<string name="screen_bug_report_include_logs">"Protokolle zulassen"</string> |
||||
<string name="screen_bug_report_include_screenshot">"Bildschirmfoto senden"</string> |
||||
<string name="screen_bug_report_logs_description">"Die Protokolle werden Ihrer Nachricht beigefügt, um sicherzustellen, dass alles ordnungsgemäß funktioniert. Um Ihre Nachricht ohne Protokolle zu senden, deaktivieren Sie diese Einstellung."</string> |
||||
<string name="screen_bug_report_rash_logs_alert_title">"%1$s ist bei der letzten Nutzung abgestürzt. Möchten Sie einen Absturzbericht mit uns teilen?"</string> |
||||
</resources> |
@ -1,6 +1,50 @@
@@ -1,6 +1,50 @@
|
||||
<?xml version="1.0" encoding="utf-8"?> |
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> |
||||
<plurals name="screen_room_member_list_header_title"> |
||||
<item quantity="one">"%1$d Person"</item> |
||||
<item quantity="other">"%1$d Personen"</item> |
||||
</plurals> |
||||
<string name="screen_room_details_add_topic_title">"Thema hinzufügen"</string> |
||||
<string name="screen_room_details_already_a_member">"Bereits Mitglied"</string> |
||||
<string name="screen_room_details_already_invited">"Bereits eingeladen"</string> |
||||
<string name="screen_room_details_edit_room_title">"Raum bearbeiten"</string> |
||||
<string name="screen_room_details_edition_error">"Es ist ein unbekannter Fehler aufgetreten und die Informationen konnten nicht geändert werden."</string> |
||||
<string name="screen_room_details_edition_error_title">"Raum kann nicht aktualisiert werden"</string> |
||||
<string name="screen_room_details_encryption_enabled_subtitle">"Nachrichten sind mit Schlössern gesichert. Nur Sie und die Empfänger haben die eindeutigen Schlüssel, um sie zu entsperren."</string> |
||||
<string name="screen_room_details_encryption_enabled_title">"Nachrichtenverschlüsselung aktiviert"</string> |
||||
<string name="screen_room_details_error_loading_notification_settings">"Beim Laden der Benachrichtigungseinstellungen ist ein Fehler aufgetreten."</string> |
||||
<string name="screen_room_details_error_muting">"Die Stummschaltung dieses Raums ist fehlgeschlagen, bitte versuchen Sie es erneut."</string> |
||||
<string name="screen_room_details_error_unmuting">"Die Deaktivierung der Stummschaltung dieses Raums ist fehlgeschlagen, bitte versuchen Sie es erneut."</string> |
||||
<string name="screen_room_details_invite_people_title">"Personen einladen"</string> |
||||
<string name="screen_room_details_notification_mode_custom">"Benutzerdefiniert"</string> |
||||
<string name="screen_room_details_notification_mode_default">"Standard"</string> |
||||
<string name="screen_room_details_notification_title">"Benachrichtigungen"</string> |
||||
<string name="screen_room_details_room_name_label">"Raumname"</string> |
||||
<string name="screen_room_details_share_room_title">"Raum teilen"</string> |
||||
<string name="screen_room_details_updating_room">"Raum wird aktualisiert…"</string> |
||||
<string name="screen_room_member_list_pending_header_title">"Ausstehend"</string> |
||||
<string name="screen_room_member_list_room_members_header_title">"Raummitglieder"</string> |
||||
<string name="screen_room_notification_settings_allow_custom">"Benutzerdefinierte Einstellung zulassen"</string> |
||||
<string name="screen_room_notification_settings_allow_custom_footnote">"Wenn Sie diese Option aktivieren, wird Ihre Standardeinstellung außer Kraft gesetzt."</string> |
||||
<string name="screen_room_notification_settings_custom_settings_title">"Benachrichtigen Sie mich in diesem Chat bei"</string> |
||||
<string name="screen_room_notification_settings_default_setting_footnote">"Sie können das in Ihrem %1$s ändern."</string> |
||||
<string name="screen_room_notification_settings_default_setting_footnote_content_link">"Globale Einstellungen"</string> |
||||
<string name="screen_room_notification_settings_default_setting_title">"Standardeinstellung"</string> |
||||
<string name="screen_room_notification_settings_edit_remove_setting">"Benutzerdefinierte Einstellung entfernen"</string> |
||||
<string name="screen_room_notification_settings_error_loading_settings">"Beim Laden der Benachrichtigungseinstellungen ist ein Fehler aufgetreten."</string> |
||||
<string name="screen_room_notification_settings_error_restoring_default">"Fehler beim Wiederherstellen des Standardmodus. Bitte versuchen Sie es erneut."</string> |
||||
<string name="screen_room_notification_settings_error_setting_mode">"Fehler beim Einstellen des Modus. Bitte versuchen Sie es erneut."</string> |
||||
<string name="screen_room_notification_settings_mode_all_messages">"Alle Nachrichten"</string> |
||||
<string name="screen_room_notification_settings_mode_mentions_and_keywords">"Nur Erwähnungen und Schlüsselwörter"</string> |
||||
<string name="screen_room_notification_settings_room_custom_settings_title">"Benachrichtigen Sie mich in diesem Raum bei"</string> |
||||
<string name="screen_dm_details_block_alert_action">"Sperren"</string> |
||||
<string name="screen_dm_details_block_alert_description">"Gesperrte Benutzer können Ihnen keine Nachrichten senden und alle ihre Nachrichten werden ausgeblendet. Sie können sie jederzeit entsperren."</string> |
||||
<string name="screen_dm_details_block_user">"Benutzer sperren"</string> |
||||
<string name="screen_dm_details_unblock_alert_action">"Entsperren"</string> |
||||
<string name="screen_dm_details_unblock_alert_description">"Sie können dann wieder alle Nachrichten von ihnen sehen."</string> |
||||
<string name="screen_dm_details_unblock_user">"Benutzer entsperren"</string> |
||||
<string name="screen_room_details_leave_room_title">"Raum verlassen"</string> |
||||
<string name="screen_room_details_people_title">"Menschen"</string> |
||||
<string name="screen_room_details_people_title">"Personen"</string> |
||||
<string name="screen_room_details_security_title">"Sicherheit"</string> |
||||
<string name="screen_room_details_topic_title">"Thema"</string> |
||||
</resources> |
||||
|
@ -0,0 +1,9 @@
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?> |
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> |
||||
<string name="screen_roomlist_a11y_create_message">"Eine neue Unterhaltung oder einen neuen Raum erstellen"</string> |
||||
<string name="screen_roomlist_empty_message">"Beginnen Sie, indem Sie jemandem eine Nachricht senden."</string> |
||||
<string name="screen_roomlist_empty_title">"Noch keine Chats."</string> |
||||
<string name="screen_roomlist_main_space_title">"Alle Chats"</string> |
||||
<string name="session_verification_banner_message">"Es sieht aus, als würden Sie ein neues Gerät verwenden. Verifizieren Sie es mit einem anderen Gerät, damit Sie auf Ihre verschlüsselten Nachrichten zugreifen können."</string> |
||||
<string name="session_verification_banner_title">"Bestätigen Sie Ihre Identität"</string> |
||||
</resources> |
@ -1,4 +1,19 @@
@@ -1,4 +1,19 @@
|
||||
<?xml version="1.0" encoding="utf-8"?> |
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> |
||||
<string name="screen_session_verification_cancelled_subtitle">"Etwas scheint nicht zu stimmen. Entweder ist das Zeitlimit für die Anfrage abgelaufen oder die Anfrage wurde abgelehnt."</string> |
||||
<string name="screen_session_verification_compare_emojis_subtitle">"Vergewissern Sie sich, dass die folgenden Emojis mit denen in Ihrer anderen Session übereinstimmen."</string> |
||||
<string name="screen_session_verification_compare_emojis_title">"Emojis vergleichen"</string> |
||||
<string name="screen_session_verification_complete_subtitle">"Ihre neue Session ist nun verifiziert. Sie hat Zugriff auf Ihre verschlüsselten Nachrichten und wird von anderen Benutzern als vertrauenswürdig eingestuft."</string> |
||||
<string name="screen_session_verification_open_existing_session_subtitle">"Beweisen Sie Ihre Identität, um auf Ihren verschlüsselten Nachrichtenverlauf zuzugreifen."</string> |
||||
<string name="screen_session_verification_open_existing_session_title">"Öffnen Sie eine bestehende Sitzung"</string> |
||||
<string name="screen_session_verification_positive_button_canceled">"Verifizierung wiederholen"</string> |
||||
<string name="screen_session_verification_positive_button_initial">"Ich bin bereit"</string> |
||||
<string name="screen_session_verification_positive_button_verifying_ongoing">"Warten auf eine Übereinstimmung"</string> |
||||
<string name="screen_session_verification_request_accepted_subtitle">"Vergleichen Sie die einzelnen Emojis und stellen Sie sicher, dass sie in der gleichen Reihenfolge erscheinen."</string> |
||||
<string name="screen_session_verification_they_dont_match">"Sie stimmen nicht überein"</string> |
||||
<string name="screen_session_verification_they_match">"Sie stimmen überein"</string> |
||||
<string name="screen_session_verification_waiting_to_accept_subtitle">"Akzeptieren Sie die Anfrage, um den Verifizierungsprozess in Ihrer anderen Session zu starten, um fortzufahren."</string> |
||||
<string name="screen_session_verification_waiting_to_accept_title">"Warten auf die Annahme der Anfrage"</string> |
||||
<string name="screen_session_verification_cancelled_title">"Verifizierung abgebrochen"</string> |
||||
<string name="screen_session_verification_positive_button_ready">"Start"</string> |
||||
</resources> |
||||
|
@ -0,0 +1,4 @@
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?> |
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> |
||||
<string name="error_no_compatible_app_found">"Für diese Aktion wurde keine kompatible App gefunden."</string> |
||||
</resources> |
@ -0,0 +1,84 @@
@@ -0,0 +1,84 @@
|
||||
/* |
||||
* Copyright (c) 2023 New Vector Ltd |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0 |
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package io.element.android.libraries.designsystem.components |
||||
|
||||
import androidx.compose.foundation.layout.Arrangement |
||||
import androidx.compose.foundation.layout.Column |
||||
import androidx.compose.foundation.layout.fillMaxWidth |
||||
import androidx.compose.foundation.layout.padding |
||||
import androidx.compose.foundation.text.KeyboardOptions |
||||
import androidx.compose.material3.MaterialTheme |
||||
import androidx.compose.runtime.Composable |
||||
import androidx.compose.ui.Modifier |
||||
import androidx.compose.ui.unit.dp |
||||
import io.element.android.libraries.designsystem.preview.DayNightPreviews |
||||
import io.element.android.libraries.designsystem.preview.ElementPreview |
||||
import io.element.android.libraries.designsystem.theme.components.OutlinedTextField |
||||
import io.element.android.libraries.designsystem.theme.components.Text |
||||
import io.element.android.libraries.theme.ElementTheme |
||||
|
||||
@Composable |
||||
fun LabelledOutlinedTextField( |
||||
label: String, |
||||
value: String, |
||||
modifier: Modifier = Modifier, |
||||
placeholder: String? = null, |
||||
singleLine: Boolean = false, |
||||
maxLines: Int = if (singleLine) 1 else Int.MAX_VALUE, |
||||
keyboardOptions: KeyboardOptions = KeyboardOptions.Default, |
||||
onValueChange: (String) -> Unit = {}, |
||||
) { |
||||
Column( |
||||
modifier = modifier, |
||||
verticalArrangement = Arrangement.spacedBy(8.dp), |
||||
) { |
||||
Text( |
||||
modifier = Modifier.padding(horizontal = 16.dp), |
||||
style = ElementTheme.typography.fontBodyMdRegular, |
||||
color = MaterialTheme.colorScheme.primary, |
||||
text = label |
||||
) |
||||
|
||||
OutlinedTextField( |
||||
modifier = Modifier.fillMaxWidth(), |
||||
value = value, |
||||
placeholder = placeholder?.let { { Text(placeholder) } }, |
||||
onValueChange = onValueChange, |
||||
singleLine = singleLine, |
||||
maxLines = maxLines, |
||||
keyboardOptions = keyboardOptions, |
||||
) |
||||
} |
||||
} |
||||
|
||||
@DayNightPreviews |
||||
@Composable |
||||
internal fun LabelledOutlinedTextFieldPreview() = ElementPreview { |
||||
Column { |
||||
LabelledOutlinedTextField( |
||||
label = "Room name", |
||||
value = "", |
||||
placeholder = "e.g. Product Sprint", |
||||
) |
||||
LabelledOutlinedTextField( |
||||
label = "Room name", |
||||
value = "a room name", |
||||
placeholder = "e.g. Product Sprint", |
||||
) |
||||
} |
||||
} |
||||
|
@ -1,5 +1,57 @@
@@ -1,5 +1,57 @@
|
||||
<?xml version="1.0" encoding="utf-8"?> |
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> |
||||
<string name="state_event_avatar_changed_too">"(Avatar wurde auch geändert)"</string> |
||||
<string name="state_event_avatar_url_changed">"%1$s hat den Avatar geändert"</string> |
||||
<string name="state_event_avatar_url_changed_by_you">"Sie haben Ihren Avatar geändert"</string> |
||||
<string name="state_event_display_name_changed_from">"%1$s hat den Anzeigenamen von %2$s auf %3$s geändert"</string> |
||||
<string name="state_event_display_name_changed_from_by_you">"Sie haben Ihren Anzeigenamen von %1$s auf %2$s geändert"</string> |
||||
<string name="state_event_display_name_removed">"%1$s hat den Anzeigenamen entfernt (war %2$s)"</string> |
||||
<string name="state_event_display_name_removed_by_you">"Sie haben Ihren Anzeigenamen entfernt (war %1$s)"</string> |
||||
<string name="state_event_display_name_set">"%1$s setzen ihren Anzeigenamen auf %2$s"</string> |
||||
<string name="state_event_display_name_set_by_you">"Sie haben Ihren Anzeigenamen zu %1$s geändert"</string> |
||||
<string name="state_event_room_avatar_changed">"%1$s hat den Raum-Avatar geändert"</string> |
||||
<string name="state_event_room_avatar_changed_by_you">"Sie haben den Raum-Avatar geändert"</string> |
||||
<string name="state_event_room_avatar_removed">"%1$s hat den Raum-Avatar entfernt"</string> |
||||
<string name="state_event_room_avatar_removed_by_you">"Sie haben den Raum-Avatar entfernt"</string> |
||||
<string name="state_event_room_ban">"%1$s hat %2$s gesperrt"</string> |
||||
<string name="state_event_room_ban_by_you">"Sie haben %1$s gesperrt"</string> |
||||
<string name="state_event_room_created">"%1$s hat den Raum erstellt"</string> |
||||
<string name="state_event_room_created_by_you">"Du hast den Raum erstellt"</string> |
||||
<string name="state_event_room_created_by_you">"Sie haben den Raum erstellt"</string> |
||||
<string name="state_event_room_invite">"%1$s hat %2$s eingeladen"</string> |
||||
<string name="state_event_room_invite_accepted">"%1$s hat die Einladung angenommen"</string> |
||||
<string name="state_event_room_invite_accepted_by_you">"Sie haben die Einladung angenommen"</string> |
||||
<string name="state_event_room_invite_by_you">"Sie haben %1$s eingeladen"</string> |
||||
<string name="state_event_room_invite_you">"%1$s hat dich eingeladen"</string> |
||||
<string name="state_event_room_join">"%1$s hat den Raum betreten"</string> |
||||
<string name="state_event_room_join_by_you">"Sie haben den Raum betreten"</string> |
||||
<string name="state_event_room_knock">"%1$s hat angefragt beizutreten"</string> |
||||
<string name="state_event_room_knock_accepted">"%1$s hat %2$s den Beitritt erlaubt"</string> |
||||
<string name="state_event_room_knock_accepted_by_you">"%1$s hat Ihnen den Betritt erlaubt"</string> |
||||
<string name="state_event_room_knock_by_you">"Sie haben angefragt beizutreten"</string> |
||||
<string name="state_event_room_knock_denied">"%1$s hat die Beitrittsanfrage von %2$s abgelehnt"</string> |
||||
<string name="state_event_room_knock_denied_by_you">"Sie haben die Beitrittsanfrage von %1$s abgelehnt"</string> |
||||
<string name="state_event_room_knock_denied_you">"%1$s hat Ihre Beitrittsanfrage abgelehnt"</string> |
||||
<string name="state_event_room_knock_retracted">"%1$s ist nicht mehr an einem Beitritt interessiert"</string> |
||||
<string name="state_event_room_knock_retracted_by_you">"Sie haben Ihre Beitrittsanfrage zurückgezogen"</string> |
||||
<string name="state_event_room_leave">"%1$s hat den Raum verlassen"</string> |
||||
<string name="state_event_room_leave_by_you">"Sie haben den Raum verlassen"</string> |
||||
<string name="state_event_room_name_changed">"%1$s hat den Raumnamen geändert in: %2$s"</string> |
||||
<string name="state_event_room_name_changed_by_you">"Sie haben den Raumnamen geändert in: %1$s"</string> |
||||
<string name="state_event_room_name_removed">"%1$s hat den Raumnamen entfernt"</string> |
||||
<string name="state_event_room_name_removed_by_you">"Sie haben den Raumnamen entfernt"</string> |
||||
<string name="state_event_room_reject">"%1$s hat die Einladung abgelehnt"</string> |
||||
<string name="state_event_room_reject_by_you">"Sie haben die Einladung abgelehnt"</string> |
||||
<string name="state_event_room_remove">"%1$s hat %2$s entfernt"</string> |
||||
<string name="state_event_room_remove_by_you">"Sie haben %1$s entfernt"</string> |
||||
<string name="state_event_room_third_party_invite">"%1$s hat eine Einladung an %2$s gesendet, dem Raum beizutreten"</string> |
||||
<string name="state_event_room_third_party_invite_by_you">"Sie haben eine Einladung an %1$s gesendet, dem Raum beizutreten"</string> |
||||
<string name="state_event_room_third_party_revoked_invite">"%1$s hat die Einladung an %2$s zum Betreten des Raums zurückgezogen"</string> |
||||
<string name="state_event_room_third_party_revoked_invite_by_you">"Sie haben die Einladung an %1$s zum Betreten des Raums zurückgezogen"</string> |
||||
<string name="state_event_room_topic_changed">"%1$s hat das Thema geändert in: %2$s"</string> |
||||
<string name="state_event_room_topic_changed_by_you">"Sie haben das Thema geändert in: %1$s"</string> |
||||
<string name="state_event_room_topic_removed">"%1$s hat das Raumthema entfernt"</string> |
||||
<string name="state_event_room_topic_removed_by_you">"Sie haben das Raumthema entfernt"</string> |
||||
<string name="state_event_room_unban">"%1$s hat die Sperre für %2$s aufgehoben"</string> |
||||
<string name="state_event_room_unban_by_you">"Sie haben die Sperre für %1$s aufgehoben"</string> |
||||
<string name="state_event_room_unknown_membership_change">"%1$s hat eine unbekannte Raumänderung vorgenommen"</string> |
||||
</resources> |
||||
|
@ -0,0 +1,97 @@
@@ -0,0 +1,97 @@
|
||||
/* |
||||
* Copyright (c) 2023 New Vector Ltd |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0 |
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package io.element.android.libraries.matrix.ui.components |
||||
|
||||
import android.net.Uri |
||||
import androidx.compose.foundation.background |
||||
import androidx.compose.foundation.clickable |
||||
import androidx.compose.foundation.interaction.MutableInteractionSource |
||||
import androidx.compose.foundation.layout.Box |
||||
import androidx.compose.foundation.layout.Column |
||||
import androidx.compose.foundation.layout.fillMaxSize |
||||
import androidx.compose.foundation.layout.fillMaxWidth |
||||
import androidx.compose.foundation.layout.size |
||||
import androidx.compose.foundation.shape.CircleShape |
||||
import androidx.compose.material.icons.Icons |
||||
import androidx.compose.material.icons.outlined.AddAPhoto |
||||
import androidx.compose.material.ripple.rememberRipple |
||||
import androidx.compose.material3.MaterialTheme |
||||
import androidx.compose.runtime.Composable |
||||
import androidx.compose.runtime.remember |
||||
import androidx.compose.ui.Alignment |
||||
import androidx.compose.ui.Modifier |
||||
import androidx.compose.ui.draw.clip |
||||
import androidx.compose.ui.unit.dp |
||||
import io.element.android.libraries.designsystem.components.avatar.Avatar |
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarData |
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarSize |
||||
import io.element.android.libraries.designsystem.theme.components.Icon |
||||
|
||||
@Composable |
||||
fun EditableAvatarView( |
||||
userId: String?, |
||||
displayName: String?, |
||||
avatarUrl: Uri?, |
||||
avatarSize: AvatarSize, |
||||
onAvatarClicked: () -> Unit, |
||||
modifier: Modifier = Modifier, |
||||
) { |
||||
Column(modifier = modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally) { |
||||
Box( |
||||
modifier = Modifier |
||||
.size(avatarSize.dp) |
||||
.clickable( |
||||
interactionSource = remember { MutableInteractionSource() }, |
||||
onClick = onAvatarClicked, |
||||
indication = rememberRipple(bounded = false), |
||||
) |
||||
) { |
||||
when (avatarUrl?.scheme) { |
||||
null, "mxc" -> { |
||||
userId?.let { |
||||
Avatar( |
||||
avatarData = AvatarData(it, displayName, avatarUrl?.toString(), size = avatarSize), |
||||
modifier = Modifier.fillMaxSize(), |
||||
) |
||||
} |
||||
} |
||||
else -> { |
||||
UnsavedAvatar( |
||||
avatarUri = avatarUrl, |
||||
modifier = Modifier.fillMaxSize(), |
||||
) |
||||
} |
||||
} |
||||
|
||||
Box( |
||||
modifier = Modifier |
||||
.align(Alignment.BottomEnd) |
||||
.clip(CircleShape) |
||||
.background(MaterialTheme.colorScheme.primary) |
||||
.size(24.dp), |
||||
contentAlignment = Alignment.Center, |
||||
) { |
||||
Icon( |
||||
modifier = Modifier.size(16.dp), |
||||
imageVector = Icons.Outlined.AddAPhoto, |
||||
contentDescription = "", |
||||
tint = MaterialTheme.colorScheme.onPrimary, |
||||
) |
||||
} |
||||
} |
||||
} |
||||
} |
@ -1,7 +1,52 @@
@@ -1,7 +1,52 @@
|
||||
<?xml version="1.0" encoding="utf-8"?> |
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> |
||||
<string name="notification_channel_call">"Anruf"</string> |
||||
<string name="notification_channel_listening_for_events">"Auf Ereignisse achten"</string> |
||||
<string name="notification_channel_noisy">"Laute Benachrichtigungen"</string> |
||||
<string name="notification_channel_silent">"Stumme Benachrichtigungen"</string> |
||||
<string name="notification_inline_reply_failed">"** Fehler beim Senden - bitte Raum öffnen"</string> |
||||
<string name="notification_invitation_action_join">"Beitreten"</string> |
||||
<string name="notification_invitation_action_reject">"Ablehnen"</string> |
||||
<string name="notification_invite_body">"Sie wurden zu einem Chat eingeladen"</string> |
||||
<string name="notification_new_messages">"Neue Nachrichten"</string> |
||||
<string name="notification_reaction_body">"Reagiert mit %1$s"</string> |
||||
<string name="notification_room_action_mark_as_read">"Als gelesen markieren"</string> |
||||
<string name="notification_room_invite_body">"Sie wurden eingeladen, den Raum zu betreten"</string> |
||||
<string name="notification_sender_me">"Ich"</string> |
||||
<string name="notification_test_push_notification_content">"Sie sehen sich die Benachrichtigung an! Klicken Sie hier!"</string> |
||||
<string name="notification_ticker_text_dm">"%1$s: %2$s"</string> |
||||
<string name="notification_ticker_text_group">"%1$s: %2$s %3$s"</string> |
||||
<string name="notification_unread_notified_messages_and_invitation">"%1$s und %2$s"</string> |
||||
<string name="notification_unread_notified_messages_in_room">"%1$s in %2$s"</string> |
||||
<string name="notification_unread_notified_messages_in_room_and_invitation">"%1$s in %2$s und %3$s"</string> |
||||
<plurals name="notification_compat_summary_line_for_room"> |
||||
<item quantity="one">"%1$s: %2$d Nachricht"</item> |
||||
<item quantity="other">"%1$s: %2$d Nachrichten"</item> |
||||
</plurals> |
||||
<plurals name="notification_compat_summary_title"> |
||||
<item quantity="one">"%d Mitteilung"</item> |
||||
<item quantity="other">"%d Mitteilungen"</item> |
||||
</plurals> |
||||
<plurals name="notification_invitations"> |
||||
<item quantity="one">"%d Einladung"</item> |
||||
<item quantity="other">"%d Einladungen"</item> |
||||
</plurals> |
||||
<plurals name="notification_new_messages_for_room"> |
||||
<item quantity="one">"%d neue Nachricht"</item> |
||||
<item quantity="other">"%d neue Nachrichten"</item> |
||||
</plurals> |
||||
<plurals name="notification_unread_notified_messages"> |
||||
<item quantity="one">"%d ungelesene gemeldete Nachricht"</item> |
||||
<item quantity="other">"%d ungelesene gemeldete Nachrichten"</item> |
||||
</plurals> |
||||
<plurals name="notification_unread_notified_messages_in_room_rooms"> |
||||
<item quantity="one">"%d Raum"</item> |
||||
<item quantity="other">"%d Räume"</item> |
||||
</plurals> |
||||
<string name="push_choose_distributor_dialog_title_android">"Wählen Sie aus, wie Sie Benachrichtigungen erhalten möchten"</string> |
||||
<string name="push_distributor_background_sync_android">"Hintergrundsynchronisation"</string> |
||||
<string name="push_distributor_firebase_android">"Google-Dienste"</string> |
||||
<string name="push_no_valid_google_play_services_apk_android">"Keine gültigen Google Play-Dienste gefunden. Benachrichtigungen funktionieren möglicherweise nicht richtig."</string> |
||||
<string name="notification_fallback_content">"Benachrichtigung"</string> |
||||
<string name="notification_room_action_quick_reply">"Schnelle Antwort"</string> |
||||
</resources> |
||||
|
@ -0,0 +1,55 @@
@@ -0,0 +1,55 @@
|
||||
/* |
||||
* Copyright (c) 2023 New Vector Ltd |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0 |
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package io.element.android.libraries.textcomposer |
||||
|
||||
import androidx.compose.runtime.Composable |
||||
import androidx.compose.runtime.LaunchedEffect |
||||
import androidx.compose.ui.platform.LocalView |
||||
import androidx.compose.ui.viewinterop.AndroidView |
||||
import io.element.android.libraries.androidutils.ui.awaitWindowFocus |
||||
import io.element.android.libraries.androidutils.ui.showKeyboard |
||||
|
||||
/** |
||||
* Shows the soft keyboard when a given key changes to meet the required condition. |
||||
* |
||||
* Uses [showKeyboard] to show the keyboard for compatibility with [AndroidView]. |
||||
* |
||||
* @param T |
||||
* @param key The key to watch for changes. |
||||
* @param onRequestFocus A callback to request focus to the view that will receive the keyboard input. |
||||
* @param predicate The predicate that [key] must meet before showing the keyboard. |
||||
*/ |
||||
@Composable |
||||
internal fun <T> SoftKeyboardEffect( |
||||
key: T, |
||||
onRequestFocus: () -> Unit, |
||||
predicate: (T) -> Boolean, |
||||
) { |
||||
val view = LocalView.current |
||||
LaunchedEffect(key) { |
||||
if (predicate(key)) { |
||||
// Await window focus in case returning from a dialog |
||||
view.awaitWindowFocus() |
||||
|
||||
// Show the keyboard, temporarily using the root view for focus |
||||
view.showKeyboard(andRequestFocus = true) |
||||
|
||||
// Refocus to the correct view |
||||
onRequestFocus() |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,4 @@
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?> |
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> |
||||
<string name="rich_text_editor_a11y_add_attachment">"Anhang hinzufügen"</string> |
||||
</resources> |
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue