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"
+ "Datenschutzerklä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": [