diff --git a/build.gradle.kts b/build.gradle.kts index 5c958fc397..7e7c1c1c9e 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -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") } } diff --git a/changelog.d/1337.bugfix b/changelog.d/1337.bugfix new file mode 100644 index 0000000000..b7d6c300dd --- /dev/null +++ b/changelog.d/1337.bugfix @@ -0,0 +1 @@ +[Rich text editor] Ensure keyboard opens for reply and text formatting modes \ No newline at end of file diff --git a/changelog.d/1347.bugfix b/changelog.d/1347.bugfix new file mode 100644 index 0000000000..277efb1c95 --- /dev/null +++ b/changelog.d/1347.bugfix @@ -0,0 +1 @@ +[Rich text editor] Fix placeholder spilling onto multiple lines \ No newline at end of file diff --git a/features/analytics/impl/src/main/res/values-de/translations.xml b/features/analytics/impl/src/main/res/values-de/translations.xml index 4282ead092..a9b4173395 100644 --- a/features/analytics/impl/src/main/res/values-de/translations.xml +++ b/features/analytics/impl/src/main/res/values-de/translations.xml @@ -1,4 +1,10 @@ + "Wir zeichnen keine persönlichen Daten auf und erstellen keine Profile." + "Teilen Sie anonyme Nutzungsdaten, um uns bei der Identifizierung von Problemen zu helfen." + "Sie können alle unsere Bedingungen lesen%1$s." "hier" + "Sie können diese Funktion jederzeit deaktivieren" + "Wir geben Ihre Daten nicht an Dritte weiter" + "Hilf uns %1$s zu verbessern" diff --git a/features/call/src/main/res/values-de/translations.xml b/features/call/src/main/res/values-de/translations.xml new file mode 100644 index 0000000000..d58a616780 --- /dev/null +++ b/features/call/src/main/res/values-de/translations.xml @@ -0,0 +1,6 @@ + + + "Laufender Anruf" + "Tippen, um zum Anruf zurückzukehren" + "☎️ Anruf läuft" + diff --git a/features/call/src/main/res/values-sk/translations.xml b/features/call/src/main/res/values-sk/translations.xml new file mode 100644 index 0000000000..ec7d693f1d --- /dev/null +++ b/features/call/src/main/res/values-sk/translations.xml @@ -0,0 +1,6 @@ + + + "Prebiehajúci hovor" + "Ťuknutím sa vrátite k hovoru" + "☎️ Prebieha hovor" + diff --git a/features/createroom/impl/src/main/res/values-de/translations.xml b/features/createroom/impl/src/main/res/values-de/translations.xml index f033df7792..1a027c9d2d 100644 --- a/features/createroom/impl/src/main/res/values-de/translations.xml +++ b/features/createroom/impl/src/main/res/values-de/translations.xml @@ -1,4 +1,15 @@ + "Neuer Raum" + "Freunde zu Element einladen" + "Personen einladen" + "Beim Erstellen des Raums ist ein Fehler aufgetreten" + "Die Nachrichten in diesem Raum sind verschlüsselt. Die Verschlüsselung kann nicht nachträglich deaktiviert werden." + "Privater Raum (nur auf Einladung)" + "Die Nachrichten sind nicht verschlüsselt und können von jedem gelesen werden. Die Verschlüsselung kann zu einem späteren Zeitpunkt aktiviert werden." + "Öffentlicher Raum (für alle)" + "Raumname" + "Thema (optional)" + "Beim Versuch, einen Chat zu starten, ist ein Fehler aufgetreten" "Raum erstellen" diff --git a/features/ftue/impl/src/main/res/values-de/translations.xml b/features/ftue/impl/src/main/res/values-de/translations.xml index 36fc85fba5..8df01adc45 100644 --- a/features/ftue/impl/src/main/res/values-de/translations.xml +++ b/features/ftue/impl/src/main/res/values-de/translations.xml @@ -1,5 +1,13 @@ - "Du kannst deine Einstellungen später ändern." + "Dies ist ein einmaliger Vorgang, danke fürs Warten." + "Richten Sie Ihr Konto ein." + "Sie können Ihre Einstellungen später ändern." "Erlaube Benachrichtigungen und verpasse keine Nachricht" + "Anrufe, Umfragen, Suchfunktionen und mehr werden im Laufe des Jahres hinzugefügt." + "Der Nachrichtenverlauf für verschlüsselte Räume wird in diesem Update nicht verfügbar sein." + "Wir würden uns freuen, von Ihnen zu hören. Teilen Sie uns Ihre Meinung über die Einstellungsseite mit." + "Los geht\'s!" + "Folgendes müssen Sie wissen:" + "Willkommen bei %1$s!" diff --git a/features/invitelist/impl/src/main/kotlin/io/element/android/features/invitelist/impl/InviteListPresenter.kt b/features/invitelist/impl/src/main/kotlin/io/element/android/features/invitelist/impl/InviteListPresenter.kt index 8c43b815ae..648eb5094f 100644 --- a/features/invitelist/impl/src/main/kotlin/io/element/android/features/invitelist/impl/InviteListPresenter.kt +++ b/features/invitelist/impl/src/main/kotlin/io/element/android/features/invitelist/impl/InviteListPresenter.kt @@ -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) } diff --git a/features/invitelist/impl/src/main/res/values-de/translations.xml b/features/invitelist/impl/src/main/res/values-de/translations.xml new file mode 100644 index 0000000000..245634a971 --- /dev/null +++ b/features/invitelist/impl/src/main/res/values-de/translations.xml @@ -0,0 +1,9 @@ + + + "Möchten Sie die Einladung zum Betreten von %1$s wirklich ablehnen?" + "Einladung ablehnen" + "Sind Sie sicher, dass Sie diesen privaten Chat mit %1$s ablehnen möchten?" + "Chat ablehnen" + "Keine Einladungen" + "%1$s (%2$s) hat dich eingeladen" + diff --git a/features/login/impl/src/main/res/values-de/translations.xml b/features/login/impl/src/main/res/values-de/translations.xml index 8a69936ff8..ba012e8082 100644 --- a/features/login/impl/src/main/res/values-de/translations.xml +++ b/features/login/impl/src/main/res/values-de/translations.xml @@ -1,8 +1,47 @@ + "Kontoanbieter ändern" + "Homeserver-Adresse" + "Geben Sie einen Suchbegriff oder eine Domainadresse ein." + "Suchen Sie nach einem Unternehmen, einer Community oder einem privaten Server." + "Kontoanbieter finden" + "Hier werden Ihre Gespräche gespeichert – genau so, wie Sie einen E-Mail-Anbieter nutzen würden, um Ihre E-Mails aufzubewahren." + "Sie sind dabei, sich bei %s anzumelden" + "Hier werden Ihre Gespräche gespeichert – genau so, wie Sie einen E-Mail-Anbieter nutzen würden, um Ihre E-Mails aufzubewahren." + "Sie sind dabei, ein Konto bei %s zu erstellen" + "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." + "Sonstige" + "Verwenden Sie einen anderen Kontoanbieter, z. B. Ihren eigenen privaten Server oder ein Geschäftskonto." + "Kontoanbieter wechseln" + "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." + "Dieser Server unterstützt derzeit kein Sliding Sync." + "Homeserver-URL" + "Sie können nur eine Verbindung zu einem vorhandenen Server herstellen, der Sliding Sync unterstützt. Ihr Homeserver-Administrator muss das konfigurieren. %1$s" + "Wie lautet die Adresse Ihres Servers?" + "Dieses Konto wurde deaktiviert." + "Falscher Benutzername und/oder Passwort" + "Dies ist keine gültige Benutzerkennung. Erwartetes Format: \'@user:homeserver.org\'" + "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." + "Geben Sie Ihre Daten ein" + "Willkommen zurück!" + "Anmelden bei %1$s" + "Kontoanbieter wechseln" + "Ein privater Server für die Mitarbeiter von Element." + "Matrix ist ein offenes Netzwerk für eine sichere, dezentrale Kommunikation." + "Hier werden Ihre Gespräche gespeichert - so wie Sie Ihre E-Mails bei einem E-Mail-Anbieter aufbewahren würden." + "Sie sind dabei, sich bei %1$s anzumelden" + "Sie sind dabei, ein Konto auf %1$s zu erstellen" + "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!" + "Willkommen bei %1$s!" + "Sie sind fast am Ziel." + "Sie sind dabei." "Weiter" "Weiter" + "Wählen Sie Ihren Server aus" "Passwort" "Weiter" + "Matrix ist ein offenes Netzwerk für eine sichere, dezentrale Kommunikation." "Benutzername" diff --git a/features/logout/api/src/main/res/values-de/translations.xml b/features/logout/api/src/main/res/values-de/translations.xml new file mode 100644 index 0000000000..893cc983b0 --- /dev/null +++ b/features/logout/api/src/main/res/values-de/translations.xml @@ -0,0 +1,8 @@ + + + "Sind Sie sicher, dass Sie sich abmelden wollen?" + "Abmelden" + "Abmelden…" + "Abmelden" + "Abmelden" + diff --git a/features/messages/impl/src/main/res/values-de/translations.xml b/features/messages/impl/src/main/res/values-de/translations.xml index 5d43a8167f..8208418e34 100644 --- a/features/messages/impl/src/main/res/values-de/translations.xml +++ b/features/messages/impl/src/main/res/values-de/translations.xml @@ -1,5 +1,42 @@ + + "%1$d Raumänderung" + "%1$d Raumänderungen" + + "Kamera" + "Foto machen" + "Video aufnehmen" + "Anhang" + "Foto- und Videobibliothek" + "Standort" + "Umfrage" "Textformatierung" + "Der Nachrichtenverlauf ist derzeit in diesem Raum nicht verfügbar" + "Benutzerdetails konnten nicht abgerufen werden" + "Möchten Sie sie wieder einladen?" + "Sie sind allein in diesem Chat" + "Nachricht wurde kopiert" + "Sie sind nicht berechtigt, in diesem Raum zu posten" + "Benutzerdefinierte Einstellung zulassen" + "Wenn Sie diese Option aktivieren, wird Ihre Standardeinstellung außer Kraft gesetzt." + "Benachrichtigen Sie mich in diesem Chat bei" + "Sie können das in Ihrem %1$s ändern." + "Globale Einstellungen" + "Standardeinstellung" + "Benutzerdefinierte Einstellung entfernen" + "Beim Laden der Benachrichtigungseinstellungen ist ein Fehler aufgetreten." + "Fehler beim Wiederherstellen des Standardmodus. Bitte versuchen Sie es erneut." + "Fehler beim Einstellen des Modus. Bitte versuchen Sie es erneut." + "Alle Nachrichten" + "Nur Erwähnungen und Schlüsselwörter" + "Benachrichtigen Sie mich in diesem Raum bei" + "Weniger anzeigen" + "Mehr anzeigen" + "Erneut senden" + "Ihre Nachricht konnte nicht gesendet werden" + "Emoji hinzufügen" + "Weniger anzeigen" + "Fehler beim Verarbeiten des hochgeladenen Mediums. Bitte versuchen Sie es erneut." "Entfernen" diff --git a/features/onboarding/impl/src/main/res/values-de/translations.xml b/features/onboarding/impl/src/main/res/values-de/translations.xml new file mode 100644 index 0000000000..08bc2abeee --- /dev/null +++ b/features/onboarding/impl/src/main/res/values-de/translations.xml @@ -0,0 +1,10 @@ + + + "Manuell anmelden" + "Mit QR-Code anmelden" + "Konto erstellen" + "Sicher kommunizieren und zusammenarbeiten" + "Willkommen beim schnellsten Element aller Zeiten. Optimiert für Geschwindigkeit und Einfachheit." + "Willkommen zu %1$s. Aufgeladen, für Geschwindigkeit und Einfachheit." + "Seien Sie in Ihrem Element" + diff --git a/features/poll/api/src/main/kotlin/io/element/android/features/poll/api/PollAnswerView.kt b/features/poll/api/src/main/kotlin/io/element/android/features/poll/api/PollAnswerView.kt index 4e7a23094b..0e4b168866 100644 --- a/features/poll/api/src/main/kotlin/io/element/android/features/poll/api/PollAnswerView.kt +++ b/features/poll/api/src/main/kotlin/io/element/android/features/poll/api/PollAnswerView.kt @@ -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 = { } ) } diff --git a/features/poll/api/src/main/kotlin/io/element/android/features/poll/api/PollContentView.kt b/features/poll/api/src/main/kotlin/io/element/android/features/poll/api/PollContentView.kt index 7e03d7a46e..a47d69c780 100644 --- a/features/poll/api/src/main/kotlin/io/element/android/features/poll/api/PollContentView.kt +++ b/features/poll/api/src/main/kotlin/io/element/android/features/poll/api/PollContentView.kt @@ -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, 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, +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) diff --git a/features/poll/impl/src/main/res/values-de/translations.xml b/features/poll/impl/src/main/res/values-de/translations.xml index 762afd5fd8..c2b07baf16 100644 --- a/features/poll/impl/src/main/res/values-de/translations.xml +++ b/features/poll/impl/src/main/res/values-de/translations.xml @@ -4,8 +4,9 @@ "Ergebnisse erst nach Ende der Umfrage anzeigen" "Anonyme Umfrage" "Option %1$d" - "Bist du sicher, dass du diese Umfrage verwerfen willst?" + "Sind Sie sicher, dass Sie diese Umfrage verwerfen wollen?" "Umfrage verwerfen" "Frage oder Thema" + "Worum geht es bei der Umfrage?" "Umfrage erstellen" diff --git a/features/preferences/impl/build.gradle.kts b/features/preferences/impl/build.gradle.kts index 34a33ff80b..b28a068708 100644 --- a/features/preferences/impl/build.gradle.kts +++ b/features/preferences/impl/build.gradle.kts @@ -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) diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/PreferencesFlowNode.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/PreferencesFlowNode.kt index 238b4006e7..6cf0390db2 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/PreferencesFlowNode.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/PreferencesFlowNode.kt @@ -38,10 +38,12 @@ import io.element.android.features.preferences.impl.developer.tracing.ConfigureT 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(buildContext, plugins = listOf(callback)) } @@ -149,6 +158,10 @@ class PreferencesFlowNode @AssistedInject constructor( NavTarget.AdvancedSettings -> { createNode(buildContext) } + is NavTarget.UserProfile -> { + val inputs = EditUserProfileNode.Inputs(navTarget.matrixUser) + createNode(buildContext, listOf(inputs)) + } } } diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootNode.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootNode.kt index b4495d8899..407832627b 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootNode.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootNode.kt @@ -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().forEach { it.onOpenNotificationSettings() } } + private fun onOpenUserProfile(matrixUser: MatrixUser) { + plugins().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, ) } diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootView.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootView.kt index 16c7df6b51..933f22bc13 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootView.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootView.kt @@ -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 = {}, ) } diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/log/LoggerTag.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/editprofile/EditUserProfileEvents.kt similarity index 57% rename from libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/log/LoggerTag.kt rename to features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/editprofile/EditUserProfileEvents.kt index 3fa613d097..5c53ec23c4 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/log/LoggerTag.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/editprofile/EditUserProfileEvents.kt @@ -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 +} diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/editprofile/EditUserProfileNode.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/editprofile/EditUserProfileNode.kt new file mode 100644 index 0000000000..738ae4ec6d --- /dev/null +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/editprofile/EditUserProfileNode.kt @@ -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, + presenterFactory: EditUserProfilePresenter.Factory, +) : Node(buildContext, plugins = plugins) { + + data class Inputs( + val matrixUser: MatrixUser + ) : NodeInputs + + val matrixUser = 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 + ) + } +} diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/editprofile/EditUserProfilePresenter.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/editprofile/EditUserProfilePresenter.kt new file mode 100644 index 0000000000..793c4840d7 --- /dev/null +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/editprofile/EditUserProfilePresenter.kt @@ -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 { + + @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> = 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>) = launch { + val results = mutableListOf>() + 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 { + 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") } + } +} diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/editprofile/EditUserProfileState.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/editprofile/EditUserProfileState.kt new file mode 100644 index 0000000000..87668e6f45 --- /dev/null +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/editprofile/EditUserProfileState.kt @@ -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, + val saveButtonEnabled: Boolean, + val saveAction: Async, + val eventSink: (EditUserProfileEvents) -> Unit +) diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/editprofile/EditUserProfileStateProvider.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/editprofile/EditUserProfileStateProvider.kt new file mode 100644 index 0000000000..5e4ccb95cb --- /dev/null +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/editprofile/EditUserProfileStateProvider.kt @@ -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 { + override val values: Sequence + 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 = {} +) diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/editprofile/EditUserProfileView.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/editprofile/EditUserProfileView.kt new file mode 100644 index 0000000000..5b921c047c --- /dev/null +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/editprofile/EditUserProfileView.kt @@ -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, + ) + } + diff --git a/features/preferences/impl/src/main/res/values/localazy.xml b/features/preferences/impl/src/main/res/values/localazy.xml new file mode 100644 index 0000000000..f01ae2b5e1 --- /dev/null +++ b/features/preferences/impl/src/main/res/values/localazy.xml @@ -0,0 +1,9 @@ + + + "Display name" + "Your display name" + "An unknown error was encountered and the information couldn\'t be changed." + "Unable to update profile" + "Edit profile" + "Updating profile…" + diff --git a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/user/editprofile/EditUserProfilePresenterTest.kt b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/user/editprofile/EditUserProfilePresenterTest.kt new file mode 100644 index 0000000000..beece60c9a --- /dev/null +++ b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/user/editprofile/EditUserProfilePresenterTest.kt @@ -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" + } +} diff --git a/features/rageshake/api/src/main/res/values-de/translations.xml b/features/rageshake/api/src/main/res/values-de/translations.xml new file mode 100644 index 0000000000..7c49bf6225 --- /dev/null +++ b/features/rageshake/api/src/main/res/values-de/translations.xml @@ -0,0 +1,5 @@ + + + "%1$s ist bei der letzten Nutzung abgestürzt. Möchten Sie einen Absturzbericht mit uns teilen?" + "Sie scheinen das Telefon aus Frustration zu schütteln. Möchten Sie den Bildschirm für den Fehlerbericht öffnen?" + diff --git a/features/rageshake/impl/src/main/res/values-de/translations.xml b/features/rageshake/impl/src/main/res/values-de/translations.xml new file mode 100644 index 0000000000..f1a25237bf --- /dev/null +++ b/features/rageshake/impl/src/main/res/values-de/translations.xml @@ -0,0 +1,15 @@ + + + "Bildschirmfoto anhängen" + "Sie können mich kontaktieren, wenn Sie weitere Fragen haben." + "Kontaktieren Sie mich" + "Bildschirmfoto bearbeiten" + "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." + "Beschreiben Sie den Fehler…" + "Wenn möglich, verfassen Sie die Beschreibung bitte auf Englisch." + "Absturzprotokolle senden" + "Protokolle zulassen" + "Bildschirmfoto senden" + "Die Protokolle werden Ihrer Nachricht beigefügt, um sicherzustellen, dass alles ordnungsgemäß funktioniert. Um Ihre Nachricht ohne Protokolle zu senden, deaktivieren Sie diese Einstellung." + "%1$s ist bei der letzten Nutzung abgestürzt. Möchten Sie einen Absturzbericht mit uns teilen?" + diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/edit/RoomDetailsEditView.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/edit/RoomDetailsEditView.kt index cd0cbf878e..ac9853a387 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/edit/RoomDetailsEditView.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/edit/RoomDetailsEditView.kt @@ -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, diff --git a/features/roomdetails/impl/src/main/res/values-de/translations.xml b/features/roomdetails/impl/src/main/res/values-de/translations.xml index 7e33583ffd..e4f43c1ffe 100644 --- a/features/roomdetails/impl/src/main/res/values-de/translations.xml +++ b/features/roomdetails/impl/src/main/res/values-de/translations.xml @@ -1,6 +1,50 @@ + + "%1$d Person" + "%1$d Personen" + + "Thema hinzufügen" + "Bereits Mitglied" + "Bereits eingeladen" + "Raum bearbeiten" + "Es ist ein unbekannter Fehler aufgetreten und die Informationen konnten nicht geändert werden." + "Raum kann nicht aktualisiert werden" + "Nachrichten sind mit Schlössern gesichert. Nur Sie und die Empfänger haben die eindeutigen Schlüssel, um sie zu entsperren." + "Nachrichtenverschlüsselung aktiviert" + "Beim Laden der Benachrichtigungseinstellungen ist ein Fehler aufgetreten." + "Die Stummschaltung dieses Raums ist fehlgeschlagen, bitte versuchen Sie es erneut." + "Die Deaktivierung der Stummschaltung dieses Raums ist fehlgeschlagen, bitte versuchen Sie es erneut." + "Personen einladen" + "Benutzerdefiniert" + "Standard" + "Benachrichtigungen" + "Raumname" + "Raum teilen" + "Raum wird aktualisiert…" + "Ausstehend" + "Raummitglieder" + "Benutzerdefinierte Einstellung zulassen" + "Wenn Sie diese Option aktivieren, wird Ihre Standardeinstellung außer Kraft gesetzt." + "Benachrichtigen Sie mich in diesem Chat bei" + "Sie können das in Ihrem %1$s ändern." + "Globale Einstellungen" + "Standardeinstellung" + "Benutzerdefinierte Einstellung entfernen" + "Beim Laden der Benachrichtigungseinstellungen ist ein Fehler aufgetreten." + "Fehler beim Wiederherstellen des Standardmodus. Bitte versuchen Sie es erneut." + "Fehler beim Einstellen des Modus. Bitte versuchen Sie es erneut." + "Alle Nachrichten" + "Nur Erwähnungen und Schlüsselwörter" + "Benachrichtigen Sie mich in diesem Raum bei" + "Sperren" + "Gesperrte Benutzer können Ihnen keine Nachrichten senden und alle ihre Nachrichten werden ausgeblendet. Sie können sie jederzeit entsperren." + "Benutzer sperren" + "Entsperren" + "Sie können dann wieder alle Nachrichten von ihnen sehen." + "Benutzer entsperren" "Raum verlassen" - "Menschen" + "Personen" + "Sicherheit" "Thema" diff --git a/features/roomlist/impl/src/main/res/values-de/translations.xml b/features/roomlist/impl/src/main/res/values-de/translations.xml new file mode 100644 index 0000000000..8ce40240ae --- /dev/null +++ b/features/roomlist/impl/src/main/res/values-de/translations.xml @@ -0,0 +1,9 @@ + + + "Eine neue Unterhaltung oder einen neuen Raum erstellen" + "Beginnen Sie, indem Sie jemandem eine Nachricht senden." + "Noch keine Chats." + "Alle Chats" + "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." + "Bestätigen Sie Ihre Identität" + diff --git a/features/verifysession/impl/src/main/res/values-de/translations.xml b/features/verifysession/impl/src/main/res/values-de/translations.xml index 6699697e79..5b4850ae57 100644 --- a/features/verifysession/impl/src/main/res/values-de/translations.xml +++ b/features/verifysession/impl/src/main/res/values-de/translations.xml @@ -1,4 +1,19 @@ + "Etwas scheint nicht zu stimmen. Entweder ist das Zeitlimit für die Anfrage abgelaufen oder die Anfrage wurde abgelehnt." + "Vergewissern Sie sich, dass die folgenden Emojis mit denen in Ihrer anderen Session übereinstimmen." + "Emojis vergleichen" + "Ihre neue Session ist nun verifiziert. Sie hat Zugriff auf Ihre verschlüsselten Nachrichten und wird von anderen Benutzern als vertrauenswürdig eingestuft." + "Beweisen Sie Ihre Identität, um auf Ihren verschlüsselten Nachrichtenverlauf zuzugreifen." + "Öffnen Sie eine bestehende Sitzung" + "Verifizierung wiederholen" + "Ich bin bereit" + "Warten auf eine Übereinstimmung" + "Vergleichen Sie die einzelnen Emojis und stellen Sie sicher, dass sie in der gleichen Reihenfolge erscheinen." + "Sie stimmen nicht überein" + "Sie stimmen überein" + "Akzeptieren Sie die Anfrage, um den Verifizierungsprozess in Ihrer anderen Session zu starten, um fortzufahren." + "Warten auf die Annahme der Anfrage" + "Verifizierung abgebrochen" "Start" diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index b164dfbb95..67294eeb7a 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -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" } diff --git a/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/file/File.kt b/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/file/File.kt index ea214ff683..d7628202c0 100644 --- a/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/file/File.kt +++ b/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/file/File.kt @@ -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") diff --git a/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/ui/View.kt b/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/ui/View.kt index 0639f29d1f..8bbaf1e5c3 100644 --- a/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/ui/View.kt +++ b/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/ui/View.kt @@ -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() @@ -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) + } + } +} diff --git a/libraries/androidutils/src/main/res/values-de/translations.xml b/libraries/androidutils/src/main/res/values-de/translations.xml new file mode 100644 index 0000000000..99b7294eb5 --- /dev/null +++ b/libraries/androidutils/src/main/res/values-de/translations.xml @@ -0,0 +1,4 @@ + + + "Für diese Aktion wurde keine kompatible App gefunden." + diff --git a/libraries/core/src/main/kotlin/io/element/android/libraries/core/log/logger/LoggerTag.kt b/libraries/core/src/main/kotlin/io/element/android/libraries/core/log/logger/LoggerTag.kt index 2c9add5e8d..1bec5524cd 100644 --- a/libraries/core/src/main/kotlin/io/element/android/libraries/core/log/logger/LoggerTag.kt +++ b/libraries/core/src/main/kotlin/io/element/android/libraries/core/log/logger/LoggerTag.kt @@ -24,10 +24,8 @@ package io.element.android.libraries.core.log.logger */ 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 diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/LabelledOutlinedTextField.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/LabelledOutlinedTextField.kt new file mode 100644 index 0000000000..4939cac38d --- /dev/null +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/LabelledOutlinedTextField.kt @@ -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", + ) + } +} + diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/AvatarSize.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/AvatarSize.kt index 38b1df6dce..45d7780393 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/AvatarSize.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/AvatarSize.kt @@ -43,5 +43,7 @@ enum class AvatarSize(val dp: Dp) { RoomInviteItem(52.dp), InviteSender(16.dp), + EditRoomDetails(70.dp), + NotificationsOptIn(32.dp), } diff --git a/libraries/eventformatter/impl/src/main/res/values-de/translations.xml b/libraries/eventformatter/impl/src/main/res/values-de/translations.xml index 48d41bed39..a7773e49c7 100644 --- a/libraries/eventformatter/impl/src/main/res/values-de/translations.xml +++ b/libraries/eventformatter/impl/src/main/res/values-de/translations.xml @@ -1,5 +1,57 @@ + "(Avatar wurde auch geändert)" + "%1$s hat den Avatar geändert" + "Sie haben Ihren Avatar geändert" + "%1$s hat den Anzeigenamen von %2$s auf %3$s geändert" + "Sie haben Ihren Anzeigenamen von %1$s auf %2$s geändert" + "%1$s hat den Anzeigenamen entfernt (war %2$s)" + "Sie haben Ihren Anzeigenamen entfernt (war %1$s)" + "%1$s setzen ihren Anzeigenamen auf %2$s" + "Sie haben Ihren Anzeigenamen zu %1$s geändert" + "%1$s hat den Raum-Avatar geändert" + "Sie haben den Raum-Avatar geändert" + "%1$s hat den Raum-Avatar entfernt" + "Sie haben den Raum-Avatar entfernt" + "%1$s hat %2$s gesperrt" + "Sie haben %1$s gesperrt" "%1$s hat den Raum erstellt" - "Du hast den Raum erstellt" + "Sie haben den Raum erstellt" + "%1$s hat %2$s eingeladen" + "%1$s hat die Einladung angenommen" + "Sie haben die Einladung angenommen" + "Sie haben %1$s eingeladen" + "%1$s hat dich eingeladen" + "%1$s hat den Raum betreten" + "Sie haben den Raum betreten" + "%1$s hat angefragt beizutreten" + "%1$s hat %2$s den Beitritt erlaubt" + "%1$s hat Ihnen den Betritt erlaubt" + "Sie haben angefragt beizutreten" + "%1$s hat die Beitrittsanfrage von %2$s abgelehnt" + "Sie haben die Beitrittsanfrage von %1$s abgelehnt" + "%1$s hat Ihre Beitrittsanfrage abgelehnt" + "%1$s ist nicht mehr an einem Beitritt interessiert" + "Sie haben Ihre Beitrittsanfrage zurückgezogen" + "%1$s hat den Raum verlassen" + "Sie haben den Raum verlassen" + "%1$s hat den Raumnamen geändert in: %2$s" + "Sie haben den Raumnamen geändert in: %1$s" + "%1$s hat den Raumnamen entfernt" + "Sie haben den Raumnamen entfernt" + "%1$s hat die Einladung abgelehnt" + "Sie haben die Einladung abgelehnt" + "%1$s hat %2$s entfernt" + "Sie haben %1$s entfernt" + "%1$s hat eine Einladung an %2$s gesendet, dem Raum beizutreten" + "Sie haben eine Einladung an %1$s gesendet, dem Raum beizutreten" + "%1$s hat die Einladung an %2$s zum Betreten des Raums zurückgezogen" + "Sie haben die Einladung an %1$s zum Betreten des Raums zurückgezogen" + "%1$s hat das Thema geändert in: %2$s" + "Sie haben das Thema geändert in: %1$s" + "%1$s hat das Raumthema entfernt" + "Sie haben das Raumthema entfernt" + "%1$s hat die Sperre für %2$s aufgehoben" + "Sie haben die Sperre für %1$s aufgehoben" + "%1$s hat eine unbekannte Raumänderung vorgenommen" diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClient.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClient.kt index 611cb4303c..fc215e4780 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClient.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClient.kt @@ -47,6 +47,9 @@ interface MatrixClient : Closeable { suspend fun createDM(userId: UserId): Result suspend fun getProfile(userId: UserId): Result suspend fun searchUsers(searchTerm: String, limit: Long): Result + suspend fun setDisplayName(displayName: String): Result + suspend fun uploadAvatar(mimeType: String, data: ByteArray): Result + suspend fun removeAvatar(): Result fun syncService(): SyncService fun sessionVerificationService(): SessionVerificationService fun pushersService(): PushersService diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt index c4a3ab3ef2..d99c60b286 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt @@ -276,6 +276,23 @@ class RustMatrixClient constructor( } } + override suspend fun setDisplayName(displayName: String): Result = + withContext(sessionDispatcher) { + runCatching { client.setDisplayName(displayName) } + } + + @OptIn(ExperimentalUnsignedTypes::class) + override suspend fun uploadAvatar(mimeType: String, data: ByteArray): Result = + withContext(sessionDispatcher) { + runCatching { client.uploadAvatar(mimeType, data.toUByteArray().toList()) } + } + + override suspend fun removeAvatar(): Result = + withContext(sessionDispatcher) { + runCatching { client.removeAvatar() } + } + + override fun syncService(): SyncService = rustSyncService override fun sessionVerificationService(): SessionVerificationService = verificationService diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomSummaryListProcessor.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomSummaryListProcessor.kt index e2bfd85f95..99b9c17968 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomSummaryListProcessor.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomSummaryListProcessor.kt @@ -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) } } } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/MatrixTimelineDiffProcessor.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/MatrixTimelineDiffProcessor.kt index fd880e7fe3..38c4ef8fb6 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/MatrixTimelineDiffProcessor.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/MatrixTimelineDiffProcessor.kt @@ -98,6 +98,9 @@ internal class MatrixTimelineDiffProcessor( TimelineChange.CLEAR -> { clear() } + TimelineChange.TRUNCATE -> { + // Not supported + } } } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/tracing/RustTracingTree.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/tracing/RustTracingTree.kt index 275994081d..f97b08a018 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/tracing/RustTracingTree.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/tracing/RustTracingTree.kt @@ -49,7 +49,7 @@ internal class RustTracingTree(private val retrieveFromStackTrace: Boolean) : Ti line = location.line, level = logLevel, target = Target.ELEMENT.filter, - message = message, + message = if (tag != null) "[$tag] $message" else message, ) } diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt index 660c1e268a..67a36f0db7 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt @@ -58,6 +58,13 @@ class FakeMatrixClient( private val accountManagementUrlString: Result = 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 = Result.success(Unit) private var unignoreUserResult: Result = Result.success(Unit) private var createRoomResult: Result = Result.success(A_ROOM_ID) @@ -69,6 +76,9 @@ class FakeMatrixClient( private val searchUserResults = mutableMapOf>() private val getProfileResults = mutableMapOf>() private var uploadMediaResult: Result = Result.success(AN_AVATAR_URL) + private var setDisplayNameResult: Result = Result.success(Unit) + private var uploadAvatarResult: Result = Result.success(Unit) + private var removeAvatarResult: Result = 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 { 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 = simulateLongTask { + setDisplayNameCalled = true + return setDisplayNameResult + } + + override suspend fun uploadAvatar(mimeType: String, data: ByteArray): Result = simulateLongTask { + uploadAvatarCalled = true + return uploadAvatarResult + } + + override suspend fun removeAvatar(): Result = 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) { uploadMediaResult = result } + + fun givenSetDisplayNameResult(result: Result) { + setDisplayNameResult = result + } + + fun givenUploadAvatarResult(result: Result) { + uploadAvatarResult = result + } + + fun givenRemoveAvatarResult(result: Result) { + removeAvatarResult = result + } } diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/EditableAvatarView.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/EditableAvatarView.kt new file mode 100644 index 0000000000..f97227be7e --- /dev/null +++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/EditableAvatarView.kt @@ -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, + ) + } + } + } +} diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/MatrixUserProvider.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/MatrixUserProvider.kt index 923afd94ad..1240b4c1b5 100644 --- a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/MatrixUserProvider.kt +++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/MatrixUserProvider.kt @@ -28,9 +28,14 @@ open class MatrixUserProvider : PreviewParameterProvider { ) } -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( diff --git a/libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/notifications/NotificationDrawerManager.kt b/libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/notifications/NotificationDrawerManager.kt index 9a778195fa..ecdf32f906 100644 --- a/libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/notifications/NotificationDrawerManager.kt +++ b/libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/notifications/NotificationDrawerManager.kt @@ -21,5 +21,5 @@ import io.element.android.libraries.matrix.api.core.SessionId interface NotificationDrawerManager { fun clearMembershipNotificationForSession(sessionId: SessionId) - fun clearMembershipNotificationForRoom(sessionId: SessionId, roomId: RoomId) + fun clearMembershipNotificationForRoom(sessionId: SessionId, roomId: RoomId, doRender: Boolean) } diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/PushersManager.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/PushersManager.kt index f3afb940cd..39ff4323a5 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/PushersManager.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/PushersManager.kt @@ -24,7 +24,6 @@ import io.element.android.libraries.matrix.api.core.EventId 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( diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotificationDrawerManager.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotificationDrawerManager.kt index 91b3987251..318b7cbd49 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotificationDrawerManager.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotificationDrawerManager.kt @@ -20,6 +20,7 @@ import io.element.android.libraries.androidutils.throttler.FirstThrottler 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 diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotifiableEventProcessor.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotifiableEventProcessor.kt index 50f1b88783..7f4c04da7b 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotifiableEventProcessor.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotifiableEventProcessor.kt @@ -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> +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 } diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotifiableEventResolver.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotifiableEventResolver.kt index e5af7785db..c93d517e89 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotifiableEventResolver.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotifiableEventResolver.kt @@ -36,7 +36,6 @@ import io.element.android.libraries.matrix.api.timeline.item.event.TextMessageTy 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. diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationBroadcastReceiver.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationBroadcastReceiver.kt index 59a763bd55..8f27d8692c 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationBroadcastReceiver.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationBroadcastReceiver.kt @@ -24,11 +24,10 @@ import io.element.android.libraries.core.log.logger.LoggerTag 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().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) } } diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationEventPersistence.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationEventPersistence.kt index 613a8d2bb7..d1aee8c0b2 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationEventPersistence.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationEventPersistence.kt @@ -22,7 +22,6 @@ import io.element.android.libraries.androidutils.file.safeDelete 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, diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationRenderer.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationRenderer.kt index a6179b3ec8..03241bbdb8 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationRenderer.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationRenderer.kt @@ -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), diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationState.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationState.kt index 4737e891aa..0d8731548d 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationState.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationState.kt @@ -39,11 +39,10 @@ class NotificationState( ) { fun updateQueuedEvents( - drawerManager: DefaultNotificationDrawerManager, - action: DefaultNotificationDrawerManager.(NotificationEventQueue, List>) -> T + action: (NotificationEventQueue, List>) -> T ): T { return synchronized(queuedEvents) { - action(drawerManager, queuedEvents, renderedEvents) + action(queuedEvents, renderedEvents) } } diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/DefaultPushHandler.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/DefaultPushHandler.kt index 7c5d24c31a..6c9138fb15 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/DefaultPushHandler.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/DefaultPushHandler.kt @@ -24,7 +24,6 @@ import io.element.android.libraries.core.meta.BuildMeta 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") diff --git a/libraries/push/impl/src/main/res/values-de/translations.xml b/libraries/push/impl/src/main/res/values-de/translations.xml index 496071416d..48c14e8247 100644 --- a/libraries/push/impl/src/main/res/values-de/translations.xml +++ b/libraries/push/impl/src/main/res/values-de/translations.xml @@ -1,7 +1,52 @@ + "Anruf" + "Auf Ereignisse achten" + "Laute Benachrichtigungen" + "Stumme Benachrichtigungen" + "** Fehler beim Senden - bitte Raum öffnen" + "Beitreten" + "Ablehnen" + "Sie wurden zu einem Chat eingeladen" + "Neue Nachrichten" + "Reagiert mit %1$s" + "Als gelesen markieren" + "Sie wurden eingeladen, den Raum zu betreten" + "Ich" + "Sie sehen sich die Benachrichtigung an! Klicken Sie hier!" "%1$s: %2$s" "%1$s: %2$s %3$s" "%1$s und %2$s" + "%1$s in %2$s" + "%1$s in %2$s und %3$s" + + "%1$s: %2$d Nachricht" + "%1$s: %2$d Nachrichten" + + + "%d Mitteilung" + "%d Mitteilungen" + + + "%d Einladung" + "%d Einladungen" + + + "%d neue Nachricht" + "%d neue Nachrichten" + + + "%d ungelesene gemeldete Nachricht" + "%d ungelesene gemeldete Nachrichten" + + + "%d Raum" + "%d Räume" + + "Wählen Sie aus, wie Sie Benachrichtigungen erhalten möchten" + "Hintergrundsynchronisation" + "Google-Dienste" + "Keine gültigen Google Play-Dienste gefunden. Benachrichtigungen funktionieren möglicherweise nicht richtig." + "Benachrichtigung" "Schnelle Antwort" diff --git a/libraries/push/test/src/main/kotlin/io/element/android/libraries/push/test/notifications/FakeNotificationDrawerManager.kt b/libraries/push/test/src/main/kotlin/io/element/android/libraries/push/test/notifications/FakeNotificationDrawerManager.kt index 1531d2df48..702e46c3ae 100644 --- a/libraries/push/test/src/main/kotlin/io/element/android/libraries/push/test/notifications/FakeNotificationDrawerManager.kt +++ b/libraries/push/test/src/main/kotlin/io/element/android/libraries/push/test/notifications/FakeNotificationDrawerManager.kt @@ -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 } } diff --git a/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/pushproviders/firebase/FirebaseNewTokenHandler.kt b/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/pushproviders/firebase/FirebaseNewTokenHandler.kt index dc938bd141..3e077841a4 100644 --- a/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/pushproviders/firebase/FirebaseNewTokenHandler.kt +++ b/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/pushproviders/firebase/FirebaseNewTokenHandler.kt @@ -26,7 +26,7 @@ import io.element.android.libraries.sessionstorage.api.toUserList 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. diff --git a/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/pushproviders/firebase/FirebasePushProvider.kt b/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/pushproviders/firebase/FirebasePushProvider.kt index 5d496b39ca..63611a0ed9 100644 --- a/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/pushproviders/firebase/FirebasePushProvider.kt +++ b/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/pushproviders/firebase/FirebasePushProvider.kt @@ -26,7 +26,7 @@ import io.element.android.libraries.pushproviders.api.PusherSubscriber 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( diff --git a/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/pushproviders/firebase/VectorFirebaseMessagingService.kt b/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/pushproviders/firebase/VectorFirebaseMessagingService.kt index 56ac65a338..3d251f6e64 100644 --- a/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/pushproviders/firebase/VectorFirebaseMessagingService.kt +++ b/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/pushproviders/firebase/VectorFirebaseMessagingService.kt @@ -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 diff --git a/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/pushproviders/unifiedpush/UnifiedPushNewGatewayHandler.kt b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/pushproviders/unifiedpush/UnifiedPushNewGatewayHandler.kt index 1a6cdb90c0..c10bc814d7 100644 --- a/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/pushproviders/unifiedpush/UnifiedPushNewGatewayHandler.kt +++ b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/pushproviders/unifiedpush/UnifiedPushNewGatewayHandler.kt @@ -24,7 +24,7 @@ import io.element.android.libraries.pushstore.api.clientsecret.PushClientSecret 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. diff --git a/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/pushproviders/unifiedpush/VectorUnifiedPushMessagingReceiver.kt b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/pushproviders/unifiedpush/VectorUnifiedPushMessagingReceiver.kt index e2006f61cc..05400e87f2 100644 --- a/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/pushproviders/unifiedpush/VectorUnifiedPushMessagingReceiver.kt +++ b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/pushproviders/unifiedpush/VectorUnifiedPushMessagingReceiver.kt @@ -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 diff --git a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/SoftKeyboardEffect.kt b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/SoftKeyboardEffect.kt new file mode 100644 index 0000000000..96b48dca6e --- /dev/null +++ b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/SoftKeyboardEffect.kt @@ -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 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() + } + } +} diff --git a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/TextComposer.kt b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/TextComposer.kt index d8906a49b0..40bf27dd5f 100644 --- a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/TextComposer.kt +++ b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/TextComposer.kt @@ -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, ) } diff --git a/libraries/textcomposer/impl/src/main/res/values-de/translations.xml b/libraries/textcomposer/impl/src/main/res/values-de/translations.xml new file mode 100644 index 0000000000..6b75a1c9a7 --- /dev/null +++ b/libraries/textcomposer/impl/src/main/res/values-de/translations.xml @@ -0,0 +1,4 @@ + + + "Anhang hinzufügen" + diff --git a/libraries/ui-strings/src/main/res/values-cs/translations.xml b/libraries/ui-strings/src/main/res/values-cs/translations.xml index 271b614dc4..0ff43bceb7 100644 --- a/libraries/ui-strings/src/main/res/values-cs/translations.xml +++ b/libraries/ui-strings/src/main/res/values-cs/translations.xml @@ -195,9 +195,9 @@ "Další nastavení" "Halsové a video hovory" "Neshoda konfigurace" - "Zjednodušili jsme nastavení oznámení, abychom usnadnili hledání možností. + "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." "Přímé zprávy" diff --git a/libraries/ui-strings/src/main/res/values-de/translations.xml b/libraries/ui-strings/src/main/res/values-de/translations.xml index 4028ece8ed..d1af3b9cd1 100644 --- a/libraries/ui-strings/src/main/res/values-de/translations.xml +++ b/libraries/ui-strings/src/main/res/values-de/translations.xml @@ -1,65 +1,265 @@ + "Passwort verbergen" "Nur Erwähnungen" "Stummgeschaltet" + "Umfrage" + "Umfrage beendet" + "Dateien senden" + "Passwort anzeigen" + "Benutzermenü" + "Akzeptieren" + "Zurück" "Abbrechen" + "Foto auswählen" + "Löschen" + "Schließen" + "Verifizierung abschließen" "Bestätigen" "Weiter" "Kopieren" + "Link kopieren" + "Link zur Nachricht kopieren" + "Erstellen" "Raum erstellen" + "Ablehnen" + "Deaktivieren" "Erledigt" "Bearbeiten" "Aktivieren" "Umfrage beenden" + "Passwort vergessen?" + "Weiter" "Einladen" + "Freunde einladen" + "Freunde einladen %1$s" + "Laden Sie Personen in %1$s ein" + "Einladungen" "Mehr erfahren" "Verlassen" "Raum verlassen" + "Konto verwalten" + "Geräte verwalten" "Weiter" + "Nein" "Nicht jetzt" "OK" + "Öffnen mit" "Schnelle Antwort" "Zitat" + "Reagieren" "Entfernen" - "Antwort" + "Antworten" + "Im Thread antworten" "Fehler melden" "Inhalt melden" "Erneut versuchen" "Entschlüsselung wiederholen" + "Speichern" + "Suchen" "Senden" + "Nachricht senden" + "Teilen" + "Link teilen" + "Überspringen" "Start" "Chat starten" - "Überprüfung starten" + "Verifizierung starten" + "Tippen Sie, um die Karte zu laden" + "Foto machen" "Quelle anzeigen" + "Ja" + "Über" + "Nutzungsrichtlinie" + "Erweiterte Einstellungen" + "Analysedaten" + "Audio" + "Blasen" + "Copyright" + "Raum wird erstellt…" + "Raum verlassen" "Dekodierungsfehler" + "Entwickleroptionen" "(bearbeitet)" "Bearbeitung" + "* %1$s %2$s" "Verschlüsselung aktiviert" "Fehler" "Datei" + "Datei wurde unter Downloads gespeichert" + "Nachricht weiterleiten" "GIF" "Bild" + "Als Antwort auf %1$s" + "Diese Matrix-ID kann nicht gefunden werden, daher wird die Einladung möglicherweise nicht empfangen." + "Raum verlassen" "Link in die Zwischenablage kopiert" "Laden…" "Nachricht" + "Nachrichtenlayout" "Nachricht entfernt" + "Modern" + "Stummschalten" + "Keine Ergebnisse" + "Offline" "Passwort" - "Menschen" + "Personen" "Permalink" + "Stimmen insgesamt: %1$s" + "Die Ergebnisse werden nach Ende der Umfrage angezeigt" + "Datenschutz­erklärung" + "Reaktion" "Reaktionen" + "Wird erneuert…" + "%1$s antworten" + "Einen Fehler melden" + "Bericht eingereicht" + "Rich-Text-Editor" + "Raumname" + "z.B. Ihr Projektname" + "Nach jemandem suchen" + "Suchergebnisse" + "Sicherheit" + "Wählen Sie Ihren Server aus" + "Wird gesendet…" + "Server wird nicht unterstützt" + "Server-URL" "Einstellungen" + "Geteilter Standort" + "Chat wird gestartet…" "Sticker" + "Erfolg" "Vorschläge" + "Synchronisieren" "Text" + "Hinweise von Drittanbietern" + "Thread" "Thema" + "Worum geht es in diesem Raum?" + "Entschlüsselung nicht möglich" + "Einladungen konnten nicht an einen oder mehrere Benutzer gesendet werden." + "Einladung(en) konnte(n) nicht gesendet werden" + "Stummschaltung aufheben" "Nicht unterstütztes Ereignis" "Benutzername" + "Verifizierung abgebrochen" + "Verifizierung abgeschlossen" "Video" "Warten…" + "Bestätigung" + "Warnung" + "Aktivitäten" + "Flaggen" + "Essen & Trinken" + "Tiere & Natur" + "Objekte" + "Smileys & Menschen" + "Reisen & Orte" + "Symbole" + "Fehler beim Erstellen des Permalinks" + "%1$s konnte die Karte nicht laden. Bitte versuchen Sie es später erneut." + "Fehler beim Laden der Nachrichten" + "%1$s konnte nicht auf Ihren Standort zugreifen. Bitte versuchen Sie es später erneut." + "%1$s hat keine Erlaubnis, auf Ihren Standort zuzugreifen. Sie können den Zugriff in den Einstellungen aktivieren." + "%1$s hat keine Erlaubnis, auf Ihren Standort zuzugreifen. Aktivieren Sie unten den Zugriff." + "Einige Nachrichten wurden nicht gesendet" + "Entschuldigung, es ist ein Fehler aufgetreten" + "🔐️ Begleite mich auf %1$s" + "Hey, sprechen Sie mit mir auf %1$s: %2$s" + "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." + "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." + "Sind Sie sicher, dass Sie den Raum verlassen wollen?" + "%1$s Android" + + "%1$d Mitglied" + "%1$d Mitglieder" + + + "%d Stimme" + "%d Stimmen" + + "Schütteln Sie heftig zum Melden von Fehlern" + "Sie scheinen das Telefon aus Frustration zu schütteln. Möchten Sie den Bildschirm für den Fehlerbericht öffnen?" + "Diese Meldung wird an den Administrator Ihres Homeservers weitergeleitet. Dieser kann keine verschlüsselten Nachrichten lesen." + "Grund für die Meldung dieses Inhalts" + "Aufzählungsliste umschalten" "Formatierungsoptionen schließen" + "Codeblock umschalten" + "Nachricht…" "Einen Link erstellen" "Link bearbeiten" + "Fettes Format anwenden" + "Kursives Format anwenden" + "Durchgestrichenes Format anwenden" + "Unterstreichungsformat anwenden" + "Vollbildmodus umschalten" + "Einrückung" + "Inline-Codeformat anwenden" + "Link setzen" + "Nummerierte Liste umschalten" + "Optionen zum Verfassen öffnen" + "Vorschlag umschalten" + "Link entfernen" + "Ohne Einrückung" "Link" + "Dies ist der Anfang von %1$s." + "Dies ist der Anfang dieses Gesprächs." + "Neu" + "Analysedaten teilen" + "Anzeigename" + "Ihr Anzeigename" + "Ein unbekannter Fehler ist aufgetreten und die Informationen konnten nicht geändert werden." + "Profil kann nicht aktualisiert werden" + "Profil bearbeiten" + "Profil wird aktualisiert…" + "Medienauswahl fehlgeschlagen, bitte versuchen Sie es erneut." + "Fehler beim Verarbeiten des hochgeladenen Mediums. Bitte versuchen Sie es erneut." + "Das Hochladen der Medien ist fehlgeschlagen. Bitte versuchen Sie es erneut." + "Zusätzliche Einstellungen" + "Audio- und Videoanrufe" + "Konfiguration stimmt nicht überein" + "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." + "Direkte Chats" + "Benutzerdefinierte Einstellung pro Chat" + "Beim Aktualisieren der Benachrichtigungseinstellungen ist ein Fehler aufgetreten." + "Alle Nachrichten" + "Nur Erwähnungen und Schlüsselwörter" + "Bei direkten Chats, benachrichtigen Sie mich bei" + "Bei Gruppenchats benachrichtigen Sie mich bei" + "Benachrichtigungen auf diesem Gerät aktivieren" + "Die Konfiguration wurde nicht korrigiert, bitte versuchen Sie es erneut." + "Gruppenchats" + "Erwähnungen" + "Alle" + "Erwähnungen" + "Benachrichtige mich bei" + "Benachrichtige mich bei @room" + "Um Benachrichtigungen zu erhalten, ändern Sie bitte Ihre %1$s." + "Systemeinstellungen" + "Systembenachrichtigungen deaktiviert" + "Benachrichtigungen" + "Prüfen Sie, ob Sie alle aktuellen und zukünftigen Nachrichten dieses Benutzers ausblenden möchten" + "Konto und Geräte" + "Standort teilen" + "Meinen Standort teilen" + "In Apple Maps öffnen" + "In Google Maps öffnen" + "In OpenStreetMap öffnen" + "Diesen Standort teilen" + "Standort" + "Rageshake" + "Erkennungsschwelle" + "Allgemein" + "Version: %1$s (%2$s)" + "en" "Fehler" + "Erfolg" + "Teilen Sie anonyme Nutzungsdaten, um uns bei der Identifizierung von Problemen zu helfen." + "Sie können alle unsere Bedingungen lesen%1$s." "hier" + "Benutzer sperren" diff --git a/libraries/ui-strings/src/main/res/values-fr/translations.xml b/libraries/ui-strings/src/main/res/values-fr/translations.xml index 4978cdb68c..f6dc4f5ac0 100644 --- a/libraries/ui-strings/src/main/res/values-fr/translations.xml +++ b/libraries/ui-strings/src/main/res/values-fr/translations.xml @@ -3,6 +3,8 @@ "Masquer le mot de passe" "Mentions uniquement" "En sourdine" + "Sondage" + "Sondage terminé" "Envoyer des fichiers" "Afficher le mot de passe" "Menu utilisateur" @@ -204,6 +206,12 @@ "Ceci est le début de cette conversation." "Nouveau" "Partagez des données de statistiques d\'utilisation" + "Nom d\'affichage" + "Votre nom d\'affichage" + "Une erreur inconnue s\'est produite et les informations n\'ont pas pu être modifiées." + "Impossible de mettre à jour le profil" + "Modifier le profil" + "Mise à jour du profil…" "Échec de la sélection du média, veuillez réessayer." "Échec du traitement des médias à télécharger, veuillez réessayer." "Échec du téléchargement du média, veuillez réessayer." diff --git a/libraries/ui-strings/src/main/res/values-ru/translations.xml b/libraries/ui-strings/src/main/res/values-ru/translations.xml index d37e28dc7a..798515fa6a 100644 --- a/libraries/ui-strings/src/main/res/values-ru/translations.xml +++ b/libraries/ui-strings/src/main/res/values-ru/translations.xml @@ -194,9 +194,9 @@ "Дополнительные параметры" "Аудио и видео звонки" "Несоответствие конфигурации" - "Мы упростили настройки уведомлений, чтобы упростить поиск опций. + "Мы упростили настройки уведомлений, чтобы упростить поиск опций. -Некоторые пользовательские настройки, выбранные вами ранее, не отображаются в данном меню, но они все еще активны. +Некоторые пользовательские настройки, выбранные вами ранее, не отображаются в данном меню, но они все еще активны. Если вы продолжите, некоторые настройки могут быть изменены." "Прямые чаты" diff --git a/libraries/ui-strings/src/main/res/values-sk/translations.xml b/libraries/ui-strings/src/main/res/values-sk/translations.xml index 162ca8f9d8..53fc0a1045 100644 --- a/libraries/ui-strings/src/main/res/values-sk/translations.xml +++ b/libraries/ui-strings/src/main/res/values-sk/translations.xml @@ -3,6 +3,8 @@ "Skryť heslo" "Iba zmienky" "Stlmené" + "Anketa" + "Ukončená anketa" "Odoslať súbory" "Zobraziť heslo" "Používateľské menu" @@ -36,6 +38,8 @@ "Zistiť viac" "Opustiť" "Opustiť miestnosť" + "Spravovať účet" + "Spravovať zariadenia" "Ďalej" "Nie" "Teraz nie" @@ -46,6 +50,7 @@ "Reagovať" "Odstrániť" "Odpovedať" + "Odpovedať vo vlákne" "Nahlásiť chybu" "Nahlásiť obsah" "Skúsiť znova" @@ -66,6 +71,7 @@ "Áno" "O aplikácii" "Zásady prijateľného používania" + "Pokročilé nastavenia" "Analytika" "Zvuk" "Bubliny" @@ -84,6 +90,7 @@ "Preposlať správu" "GIF" "Obrázok" + "V odpovedi na %1$s" "Toto Matrix ID sa nedá nájsť, takže pozvánka nemusí byť prijatá." "Opustenie miestnosti" "Odkaz bol skopírovaný do schránky" @@ -101,11 +108,13 @@ "Celkový počet hlasov: %1$s" "Výsledky sa zobrazia po ukončení ankety" "Zásady ochrany osobných údajov" + "Reakcia" "Reakcie" "Obnovuje sa…" "Odpoveď na %1$s" "Nahlásiť chybu" "Nahlásenie bolo odoslané" + "Rozšírený textový editor" "Názov miestnosti" "napr. názov vášho projektu" "Vyhľadať niekoho" @@ -124,6 +133,7 @@ "Synchronizuje sa" "Text" "Oznámenia tretích strán" + "Vlákno" "Téma" "O čom je táto miestnosť?" "Nie je možné dešifrovať" @@ -191,12 +201,19 @@ "Prepnúť číslovaný zoznam" "Otvoriť možnosti písania" "Prepnúť citáciu" + "Odstrániť odkaz" "Zrušiť odsadenie" "Odkaz" "Toto je začiatok %1$s." "Toto je začiatok tejto konverzácie." "Nové" "Zdieľať analytické údaje" + "Zobrazované meno" + "Vaše zobrazované meno" + "Vyskytla sa neznáma chyba a informácie nebolo možné zmeniť." + "Nepodarilo sa aktualizovať profil" + "Upraviť profil" + "Aktualizácia profilu…" "Nepodarilo sa vybrať médium, skúste to prosím znova." "Nepodarilo sa spracovať médiá na odoslanie, skúste to prosím znova." "Nepodarilo sa nahrať médiá, skúste to prosím znova." diff --git a/libraries/ui-strings/src/main/res/values/localazy.xml b/libraries/ui-strings/src/main/res/values/localazy.xml index 0cd0b7df0e..6509f3787e 100644 --- a/libraries/ui-strings/src/main/res/values/localazy.xml +++ b/libraries/ui-strings/src/main/res/values/localazy.xml @@ -3,6 +3,8 @@ "Hide password" "Mentions only" "Muted" + "Poll" + "Ended poll" "Send files" "Show password" "User menu" @@ -208,6 +210,7 @@ "Your display name" "An unknown error was encountered and the information couldn\'t be changed." "Unable to update profile" + "Edit profile" "Updating profile…" "Failed selecting media, please try again." "Failed processing media to upload, please try again." diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemPollView-D-13_14_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemPollView-D-13_14_null_0,NEXUS_5,1.0,en].png index 8ba3fa59a7..18764bef11 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemPollView-D-13_14_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemPollView-D-13_14_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:801f832469346524fdce0b5ad8654c3405daf0f21ae0601c62dc7f148b576ce8 -size 49074 +oid sha256:188c362ebd8bc32a47b66a080331db7643cd97714f2d4e952d7bec8c11520dcd +size 49026 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemPollView-D-13_14_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemPollView-D-13_14_null_1,NEXUS_5,1.0,en].png index 2af13b5df7..1117d239f3 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemPollView-D-13_14_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemPollView-D-13_14_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:944eb2ef8abf2e8f1715d063020767940c80b40690aae1129f69d02ddafac9d0 -size 51029 +oid sha256:9de6ab591cb02f6545218a2606d031f4f93c82e71e99523eacaf77ffa78fadb1 +size 50940 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemPollView-N-13_15_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemPollView-N-13_15_null_0,NEXUS_5,1.0,en].png index 0e2fb91a11..8bae8c65fa 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemPollView-N-13_15_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemPollView-N-13_15_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:adf6f3f79b9d8f62172171dd8a172bff1958bc4698df4586bf83c73fe4c6c6f3 -size 46198 +oid sha256:b4b00894025844927932e790a1738c85cfbb61e61a81dfbbbd7e342f38f40b99 +size 46061 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemPollView-N-13_15_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemPollView-N-13_15_null_1,NEXUS_5,1.0,en].png index c06bfdad7b..5f8c7437c9 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemPollView-N-13_15_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemPollView-N-13_15_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:38183d1d69e36c2570259c987be079d09ecfc0a0cba7508fc43d33e95c574121 -size 48368 +oid sha256:bd88ed3aeb9a20f148e914c4a5d4554220a1fecd8e8a8fe87400de78ff4bf248 +size 48237 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemEventTimestampBelow_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemEventTimestampBelow_0_null,NEXUS_5,1.0,en].png index ea0825524c..0b071c5d9d 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemEventTimestampBelow_0_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemEventTimestampBelow_0_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:54a4fe8e7f968bf487168a517baa0d9c9f6fc5b2354f4eff45f56de77b043f8f -size 56535 +oid sha256:5490f2501c6ef257f926fc2f4bd9d94ef4e6e3017d4da290e2199eaeaa2ac5b5 +size 56571 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerDisclosedNotSelected-D-0_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerDisclosedNotSelected-D-0_0_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..b40e0b863b --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerDisclosedNotSelected-D-0_0_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:045a09f485856b01e1084a91fefca0ccd7f9e7ef7470a22a4db0a60e95fb8667 +size 23119 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerDisclosedNotSelected-N-0_1_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerDisclosedNotSelected-N-0_1_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..320f918fd3 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerDisclosedNotSelected-N-0_1_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:31a8f8e06cc101bcdccb973f3a003238cc093342f04613975cb9512072b051c2 +size 21619 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerDisclosedNotSelected_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerDisclosedNotSelected_0_null,NEXUS_5,1.0,en].png deleted file mode 100644 index 7a81cb7428..0000000000 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerDisclosedNotSelected_0_null,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:9f97a2f591619aa2dd914f79a2bdfabea3e4e8239e6d80f29400e2c02c6ae21d -size 39250 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerDisclosedSelected-D-1_1_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerDisclosedSelected-D-1_1_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..b74ef6f2e3 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerDisclosedSelected-D-1_1_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1abb2b1c635b5a6666ce7de0ffbe10c753d32c6f0f2e95e8cf17d481bfa5228b +size 23024 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerDisclosedSelected-N-1_2_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerDisclosedSelected-N-1_2_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..e8e5307f60 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerDisclosedSelected-N-1_2_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:609279d1f2e25b86b8ae5a598bb6602a43a6714fa8e98a4625c349470d9d7e85 +size 21345 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerDisclosedSelected_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerDisclosedSelected_0_null,NEXUS_5,1.0,en].png deleted file mode 100644 index 5e990f7141..0000000000 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerDisclosedSelected_0_null,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:03b4bfab1cbbbedd9219423834f3bdf47f40e20c0da791be2c892cb2af86cf7d -size 38694 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerEndedSelected-D-6_6_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerEndedSelected-D-6_6_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..ed8c54d817 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerEndedSelected-D-6_6_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:08550f84b603d0c0424d63e7aef09856a048b7a862288ac43911673019ae7a5d +size 23075 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerEndedSelected-N-6_7_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerEndedSelected-N-6_7_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..e63b4b34ba --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerEndedSelected-N-6_7_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7e6000321200788598744d82d8b11c9f2d6eb509bf8c9c0bf362c10d1b27b9ae +size 21474 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerEndedSelected_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerEndedSelected_0_null,NEXUS_5,1.0,en].png deleted file mode 100644 index 7ebe08ccd8..0000000000 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerEndedSelected_0_null,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:1672ff3a807b387b1f780920111c19484261b36884f1f68d72e108c90a4104d2 -size 38948 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerEndedWinnerNotSelected-D-4_4_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerEndedWinnerNotSelected-D-4_4_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..1ae62e8a37 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerEndedWinnerNotSelected-D-4_4_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:013bfb3d3bd78d8db9f0ac51b11a41c2b82daaa28f87cdfd866260d3fb40bf0d +size 23125 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerEndedWinnerNotSelected-N-4_5_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerEndedWinnerNotSelected-N-4_5_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..243da80797 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerEndedWinnerNotSelected-N-4_5_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7c039ab6649ff160b540256dacba1037b15c5907d9994a041cbab3be0ee5cacd +size 21283 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerEndedWinnerNotSelected_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerEndedWinnerNotSelected_0_null,NEXUS_5,1.0,en].png deleted file mode 100644 index 96596d6000..0000000000 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerEndedWinnerNotSelected_0_null,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:43da602c904210df0112a1af831cd2a2f7a7c5109d26605efad464a3c5b58995 -size 38864 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerEndedWinnerSelected-D-5_5_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerEndedWinnerSelected-D-5_5_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..e9142acb1d --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerEndedWinnerSelected-D-5_5_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a99ff857afe6478e579c96b984e1abdbf981638e6964a7d2c94b53153aff078d +size 23135 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerEndedWinnerSelected-N-5_6_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerEndedWinnerSelected-N-5_6_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..f0115f6311 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerEndedWinnerSelected-N-5_6_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d97ba3288f5fcdaaed917a994df9273d3e82d5a6e0c7978044dbaf03152e3269 +size 21270 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerEndedWinnerSelected_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerEndedWinnerSelected_0_null,NEXUS_5,1.0,en].png deleted file mode 100644 index 5396c8592b..0000000000 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerEndedWinnerSelected_0_null,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:bf806b2490fc8a1df278bde94535cb1b5d38f66531df85d6fd1251e6671d9f56 -size 38756 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerUndisclosedNotSelected-D-2_2_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerUndisclosedNotSelected-D-2_2_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..6875a6a28b --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerUndisclosedNotSelected-D-2_2_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:05af414578ab61e5b24b6e0a4895e0b52c3566625fd47f00b32f38b416dbe7fa +size 21498 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerUndisclosedNotSelected-N-2_3_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerUndisclosedNotSelected-N-2_3_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..99fde7c140 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerUndisclosedNotSelected-N-2_3_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9ccde06f5825c37c1c58ef0d9aee44f7b65425988386a97ba744360d8c22e470 +size 19563 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerUndisclosedNotSelected_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerUndisclosedNotSelected_0_null,NEXUS_5,1.0,en].png deleted file mode 100644 index a9c192a685..0000000000 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerUndisclosedNotSelected_0_null,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:eeaa924482fa2fc29079f7f0d4bddacb96567a2133808bed8b49be2ad7afe80f -size 36481 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerUndisclosedSelected-D-3_3_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerUndisclosedSelected-D-3_3_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..ae98097be2 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerUndisclosedSelected-D-3_3_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ab9d7637c2c64d39beaa96244e348f39fc9b42fcebf54016fd27b4fd8e0a8c69 +size 21352 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerUndisclosedSelected-N-3_4_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerUndisclosedSelected-N-3_4_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..aee5375e86 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerUndisclosedSelected-N-3_4_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:16ea9da926c330f56bde968e90f8df1bc015bf8d55853edc21edfaf3f793adfe +size 19266 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerUndisclosedSelected_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerUndisclosedSelected_0_null,NEXUS_5,1.0,en].png deleted file mode 100644 index b16b55dc1c..0000000000 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerUndisclosedSelected_0_null,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:de340de9f0f07a44c9d2c1af196abc059d32f62fc857214cdf2c1d1e1951d267 -size 35915 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollContentDisclosed-D-1_1_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollContentDisclosed-D-1_1_null,NEXUS_5,1.0,en].png deleted file mode 100644 index 8ba3fa59a7..0000000000 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollContentDisclosed-D-1_1_null,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:801f832469346524fdce0b5ad8654c3405daf0f21ae0601c62dc7f148b576ce8 -size 49074 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollContentDisclosed-D-8_8_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollContentDisclosed-D-8_8_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..18764bef11 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollContentDisclosed-D-8_8_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:188c362ebd8bc32a47b66a080331db7643cd97714f2d4e952d7bec8c11520dcd +size 49026 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollContentDisclosed-N-1_2_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollContentDisclosed-N-1_2_null,NEXUS_5,1.0,en].png deleted file mode 100644 index 0e2fb91a11..0000000000 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollContentDisclosed-N-1_2_null,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:adf6f3f79b9d8f62172171dd8a172bff1958bc4698df4586bf83c73fe4c6c6f3 -size 46198 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollContentDisclosed-N-8_9_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollContentDisclosed-N-8_9_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..8bae8c65fa --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollContentDisclosed-N-8_9_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b4b00894025844927932e790a1738c85cfbb61e61a81dfbbbd7e342f38f40b99 +size 46061 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollContentEnded-D-2_2_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollContentEnded-D-2_2_null,NEXUS_5,1.0,en].png deleted file mode 100644 index e54dcafe51..0000000000 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollContentEnded-D-2_2_null,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:7d0674f2bac8e4912bb1faec069be0e86e3b796f61e6217494cfe42cce89dca2 -size 49143 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollContentEnded-D-9_9_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollContentEnded-D-9_9_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..c8e001c9af --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollContentEnded-D-9_9_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:88230f28f7761d8f76bc5a50dbc4ba08a694eaf0d64207257f5196c741fb7b52 +size 49078 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollContentEnded-N-2_3_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollContentEnded-N-2_3_null,NEXUS_5,1.0,en].png deleted file mode 100644 index 2a9c20d9c2..0000000000 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollContentEnded-N-2_3_null,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:f54d112dc90c4e2402eeab5fe8e649ffc397e208c395a19f37889038206c8179 -size 45927 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollContentEnded-N-9_10_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollContentEnded-N-9_10_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..495cb4c484 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollContentEnded-N-9_10_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a6e31f773b884608499081ab3c7a27297edfbf8af76f502d9076b4753956e2db +size 45929 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollContentUndisclosed-D-0_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollContentUndisclosed-D-0_0_null,NEXUS_5,1.0,en].png deleted file mode 100644 index 05fb3ba861..0000000000 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollContentUndisclosed-D-0_0_null,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:4fe5ffa68f8ea13ae4343a4fd915c58c1ff9157433248ac93ffa8ab6d8ecbbdf -size 47223 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollContentUndisclosed-D-7_7_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollContentUndisclosed-D-7_7_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..b813fd723d --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollContentUndisclosed-D-7_7_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cc814fc52a37c0724ac5a45a0337af25a2371ed8c4a0d4ce4f899bfb0a6c2ef5 +size 47138 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollContentUndisclosed-N-0_1_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollContentUndisclosed-N-0_1_null,NEXUS_5,1.0,en].png deleted file mode 100644 index 59cc1e7ce4..0000000000 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollContentUndisclosed-N-0_1_null,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:137bce4fc589d2275239c84ebea5a3fc07f90a31a8dbb0a5efb830a9433aa437 -size 43650 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollContentUndisclosed-N-7_8_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollContentUndisclosed-N-7_8_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..eecf7113c0 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollContentUndisclosed-N-7_8_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f5f8f51b498725d57c6086741ff07123c504c81c8c2bfafd483130afc2559b35 +size 43471 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.user.editprofile_null_EditUserProfileView-D-3_4_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.user.editprofile_null_EditUserProfileView-D-3_4_null_0,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..a030c7b043 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.user.editprofile_null_EditUserProfileView-D-3_4_null_0,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:717f9e1f59a958df1a2fef727e33280805a9c50e55c4d78d6ba16b428594589b +size 22646 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.user.editprofile_null_EditUserProfileView-N-3_5_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.user.editprofile_null_EditUserProfileView-N-3_5_null_0,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..c547d18192 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.user.editprofile_null_EditUserProfileView-N-3_5_null_0,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:727a8edc3a35b431583c53f73049c7e6e4e0e8d4cf998fb134d74ac86e077856 +size 21119 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.edit_null_RoomDetailsEditViewDark_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.edit_null_RoomDetailsEditViewDark_0_null_0,NEXUS_5,1.0,en].png index e0211eacbe..e7b0671305 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.edit_null_RoomDetailsEditViewDark_0_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.edit_null_RoomDetailsEditViewDark_0_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:19ac99f81f3e72f0492d33278a9e549763493ea7ff111412821c62d934a6b9f6 -size 29522 +oid sha256:5b9122206068d76d4e169dbd364d75e28c2fb5102ff2749340f829d15b02b124 +size 29053 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.edit_null_RoomDetailsEditViewDark_0_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.edit_null_RoomDetailsEditViewDark_0_null_1,NEXUS_5,1.0,en].png index e548c3c81f..096101e331 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.edit_null_RoomDetailsEditViewDark_0_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.edit_null_RoomDetailsEditViewDark_0_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d15154d8ad933acf9a52a73fb650d28489d780cae71b44c7edf10b682cec1ff9 -size 23143 +oid sha256:821f02fa92efca6f5912bbc8675f113e45f78654a0ec78bc30abc0b44c2ab0ed +size 22724 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.edit_null_RoomDetailsEditViewDark_0_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.edit_null_RoomDetailsEditViewDark_0_null_3,NEXUS_5,1.0,en].png index 487800c8dd..26474b93d6 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.edit_null_RoomDetailsEditViewDark_0_null_3,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.edit_null_RoomDetailsEditViewDark_0_null_3,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0958e2a99818be0125b700df470a944a3202e0336d440b073cc73ccc6305173f -size 28482 +oid sha256:840b6dfc827adbf183e04c68c8c4978cb87cd356141127a7e548229234359a0e +size 27984 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.edit_null_RoomDetailsEditViewDark_0_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.edit_null_RoomDetailsEditViewDark_0_null_4,NEXUS_5,1.0,en].png index 241c7e1400..116c775557 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.edit_null_RoomDetailsEditViewDark_0_null_4,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.edit_null_RoomDetailsEditViewDark_0_null_4,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:da07b8de2fd24e9e6bc7fea309e4d5fce68f2143316a5940a41f4270e9d48646 -size 28099 +oid sha256:7e27c699d975a911fbac4cd1456258ad1305d3bfbea7f059f4e736892d33d4c6 +size 28749 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.edit_null_RoomDetailsEditViewDark_0_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.edit_null_RoomDetailsEditViewDark_0_null_5,NEXUS_5,1.0,en].png index 1de6e8de96..3b3cb18ea2 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.edit_null_RoomDetailsEditViewDark_0_null_5,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.edit_null_RoomDetailsEditViewDark_0_null_5,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1a7e0644fff4f219712719b4837cf4e5db5c8cf4c9ec1e21fcd1362859a155f3 -size 28607 +oid sha256:2362fecce2c14174679dbda32e9f163ffb23498ef6b15e4846590460faab1011 +size 28142 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.edit_null_RoomDetailsEditViewDark_0_null_6,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.edit_null_RoomDetailsEditViewDark_0_null_6,NEXUS_5,1.0,en].png index c078ce120e..fb78e13908 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.edit_null_RoomDetailsEditViewDark_0_null_6,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.edit_null_RoomDetailsEditViewDark_0_null_6,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8444587bfa6ac65c6464b102bbac0bf679fecc0b193cc664540f39518b30c25d -size 24785 +oid sha256:9ad8c77df2595c643f3a394a406592328268f81a5f6bd14ebd1687f95773f305 +size 24396 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.edit_null_RoomDetailsEditViewLight_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.edit_null_RoomDetailsEditViewLight_0_null_0,NEXUS_5,1.0,en].png index dc1ba40727..104d4e7042 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.edit_null_RoomDetailsEditViewLight_0_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.edit_null_RoomDetailsEditViewLight_0_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:22b0a03bec31400bd88a51c160bf63132f0c8f7e1cf02041a901778c3f1e27ef -size 30856 +oid sha256:a2fa1c83e3ecddef2b489a8ce48db4195953fccd42534d2641bef619d5eb8bd1 +size 30325 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.edit_null_RoomDetailsEditViewLight_0_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.edit_null_RoomDetailsEditViewLight_0_null_1,NEXUS_5,1.0,en].png index cc49e4274d..1a66b0af31 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.edit_null_RoomDetailsEditViewLight_0_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.edit_null_RoomDetailsEditViewLight_0_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8308dc746a7a51a7bffa49cbf1436097565a175298073afe3d94558d77307510 -size 24145 +oid sha256:5ea00ab4789b78416b938057b7cf5df117042000894b3000536384e61284ffd0 +size 23652 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.edit_null_RoomDetailsEditViewLight_0_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.edit_null_RoomDetailsEditViewLight_0_null_3,NEXUS_5,1.0,en].png index ce4af21fcf..dfa5a058c1 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.edit_null_RoomDetailsEditViewLight_0_null_3,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.edit_null_RoomDetailsEditViewLight_0_null_3,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:32f3030c3bfb1bc9ed4c4378698df073cb29285477538c81592c38668953ffc8 -size 30562 +oid sha256:a9018f1e5339d1ad9fa6deab845e1365215d727bc68f243cccc907ead233a6e1 +size 30041 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.edit_null_RoomDetailsEditViewLight_0_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.edit_null_RoomDetailsEditViewLight_0_null_4,NEXUS_5,1.0,en].png index 090bda7bbf..bd9e15f3a5 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.edit_null_RoomDetailsEditViewLight_0_null_4,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.edit_null_RoomDetailsEditViewLight_0_null_4,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0587640a3ffb707c94d25da38d9f327500c3d3a96d0b57ba92a3c4201014fc11 -size 29448 +oid sha256:4c4af6710212228b3597d2e6cfaed5f6577e6aea90fb27240a7687d87f6f7ad7 +size 30043 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.edit_null_RoomDetailsEditViewLight_0_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.edit_null_RoomDetailsEditViewLight_0_null_5,NEXUS_5,1.0,en].png index 03d72aa037..357b55f52d 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.edit_null_RoomDetailsEditViewLight_0_null_5,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.edit_null_RoomDetailsEditViewLight_0_null_5,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1017fcd62a71f7a2a82dbee3ee2a5233747eb3c888c4b6302f65d9aa02c5492c -size 30673 +oid sha256:baee8e594977d8ae84c71b12e764005c7f04d34e57c80fce92dfa7431a8a6392 +size 30258 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.edit_null_RoomDetailsEditViewLight_0_null_6,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.edit_null_RoomDetailsEditViewLight_0_null_6,NEXUS_5,1.0,en].png index acd3d83443..abcd30f771 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.edit_null_RoomDetailsEditViewLight_0_null_6,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.edit_null_RoomDetailsEditViewLight_0_null_6,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4e01d7e598c771260bf4ef91d0ae99081e80f318c1e0300cda3dc1ce780c80ee -size 28078 +oid sha256:b5f05df46a49a9d1de0495655ca3828727c0c7d00afd11abbd643c8fc6712c03 +size 27667 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_null_RoomSummaryRowDark_0_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_null_RoomSummaryRowDark_0_null_2,NEXUS_5,1.0,en].png index e35b511282..4ad70e1e7d 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_null_RoomSummaryRowDark_0_null_2,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_null_RoomSummaryRowDark_0_null_2,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1dd44adb59ebe1ac95bc9483c61d09cc2b59307b0e2f86883f1639ae35819b0f -size 12544 +oid sha256:a4451ed5c83109d1a36d262b9d6cca1df4f65998fa2933e77b30ffeec2a1688e +size 12980 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_null_RoomSummaryRowLight_0_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_null_RoomSummaryRowLight_0_null_2,NEXUS_5,1.0,en].png index ec64e498e6..6b4a39b0f8 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_null_RoomSummaryRowLight_0_null_2,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_null_RoomSummaryRowLight_0_null_2,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f3d6f98e42089d2b8b1992a3aeb8c1c91847c3975d3759c00dc75bc85fb0ba5d -size 12195 +oid sha256:496059d16235c52562acf31c209d917b3eff6142324e07476e01a732d9aaf816 +size 12661 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_null_Avatars_Avatar_0_null_42,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_null_Avatars_Avatar_0_null_42,NEXUS_5,1.0,en].png index d69da80047..cb0e6b6fc9 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_null_Avatars_Avatar_0_null_42,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_null_Avatars_Avatar_0_null_42,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:820b23e9b9175b48461a4e7e359c2440b8c45cb598f512ea360519b0815621fc -size 18259 +oid sha256:eac20dcc3e285cde5f3d2515d65416162362de196940b3c208886934592070e4 +size 21346 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_null_Avatars_Avatar_0_null_43,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_null_Avatars_Avatar_0_null_43,NEXUS_5,1.0,en].png index 4cf9f8f839..f939fb7b9e 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_null_Avatars_Avatar_0_null_43,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_null_Avatars_Avatar_0_null_43,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3ce4abbf622711cf6a106cb6c74ed729c6e609d329579b7b00ba8c1581b4b953 -size 17463 +oid sha256:96d3a1b71b8372ca80b667ae57bf9f87b5e8167684ea62c8f71d90994113929f +size 19530 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_null_Avatars_Avatar_0_null_44,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_null_Avatars_Avatar_0_null_44,NEXUS_5,1.0,en].png index a475b2e7d1..f64b9f6a2d 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_null_Avatars_Avatar_0_null_44,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_null_Avatars_Avatar_0_null_44,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f0cb196c59e3bbd845f0e110150ac358ca78c9c08c212e184423ead5d841f67c -size 20219 +oid sha256:1f3a728b5791495710209e5b308dda4b38defded1a44d00bbd69fb1c45877218 +size 25254 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_null_Avatars_Avatar_0_null_45,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_null_Avatars_Avatar_0_null_45,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..d69da80047 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_null_Avatars_Avatar_0_null_45,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:820b23e9b9175b48461a4e7e359c2440b8c45cb598f512ea360519b0815621fc +size 18259 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_null_Avatars_Avatar_0_null_46,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_null_Avatars_Avatar_0_null_46,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..4cf9f8f839 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_null_Avatars_Avatar_0_null_46,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3ce4abbf622711cf6a106cb6c74ed729c6e609d329579b7b00ba8c1581b4b953 +size 17463 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_null_Avatars_Avatar_0_null_47,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_null_Avatars_Avatar_0_null_47,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..a475b2e7d1 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_null_Avatars_Avatar_0_null_47,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f0cb196c59e3bbd845f0e110150ac358ca78c9c08c212e184423ead5d841f67c +size 20219 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components_null_LabelledOutlinedTextField-D_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components_null_LabelledOutlinedTextField-D_0_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..94e3fe87de --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components_null_LabelledOutlinedTextField-D_0_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2edcff9494f8dfe74edeb988be57c205d98954887fc885375b850f76271df147 +size 15636 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components_null_LabelledOutlinedTextField-N_1_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components_null_LabelledOutlinedTextField-N_1_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..f7a31d7941 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components_null_LabelledOutlinedTextField-N_1_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0aaa6be5eed7879e03fc81d054f3caaf9cd5660f616be9bf47247e8d2503f473 +size 14725 diff --git a/tools/localazy/config.json b/tools/localazy/config.json index c2cb5cef3e..3a53671c05 100644 --- a/tools/localazy/config.json +++ b/tools/localazy/config.json @@ -129,6 +129,12 @@ "screen_create_poll_.*" ] }, + { + "name": ":features:preferences:impl", + "includeRegex": [ + "screen_edit_profile_.*" + ] + }, { "name": ":features:call", "includeRegex": [