From 9190880643ae6b140ba512e97c8bba5cc8ace5d7 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 14 Sep 2023 22:25:11 +0000 Subject: [PATCH 01/27] Update dependency com.google.gms:google-services to v4.4.0 --- build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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") } } From 50c7892eab19dc5c96be0db0bfe8689a3a4fa7c8 Mon Sep 17 00:00:00 2001 From: Marco Romano Date: Thu, 14 Sep 2023 13:45:01 +0200 Subject: [PATCH 02/27] Make PollContentView a11y friendly MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Improves a bit how screen readers read polls in the timeline. - Adds a few `contentDescription` so that talkback reads “poll” or “ended poll” before the poll question. - Changes the compose structure of the answers so that they are properly scanned by the screen reader. This meant getting rid of the `IconToggleButton` which was made redundant by the use of the `selectable`. --- .../features/poll/api/PollAnswerView.kt | 94 ++++++++----------- .../features/poll/api/PollContentView.kt | 55 ++++++----- .../src/main/res/values/localazy.xml | 2 + 3 files changed, 74 insertions(+), 77 deletions(-) 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/libraries/ui-strings/src/main/res/values/localazy.xml b/libraries/ui-strings/src/main/res/values/localazy.xml index 2355e4b603..dfb095d05a 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" From 626ee7fefc85b6d47a677435303af300df6d69e9 Mon Sep 17 00:00:00 2001 From: ElementBot Date: Fri, 15 Sep 2023 08:09:51 +0000 Subject: [PATCH 03/27] Update screenshots --- ...ll_TimelineItemPollView-D-13_14_null_0,NEXUS_5,1.0,en].png | 4 ++-- ...ll_TimelineItemPollView-D-13_14_null_1,NEXUS_5,1.0,en].png | 4 ++-- ...ll_TimelineItemPollView-N-13_15_null_0,NEXUS_5,1.0,en].png | 4 ++-- ...ll_TimelineItemPollView-N-13_15_null_1,NEXUS_5,1.0,en].png | 4 ++-- ...TimelineItemEventTimestampBelow_0_null,NEXUS_5,1.0,en].png | 4 ++-- ...lAnswerDisclosedNotSelected-D-0_0_null,NEXUS_5,1.0,en].png | 3 +++ ...lAnswerDisclosedNotSelected-N-0_1_null,NEXUS_5,1.0,en].png | 3 +++ ..._PollAnswerDisclosedNotSelected_0_null,NEXUS_5,1.0,en].png | 3 --- ...PollAnswerDisclosedSelected-D-1_1_null,NEXUS_5,1.0,en].png | 3 +++ ...PollAnswerDisclosedSelected-N-1_2_null,NEXUS_5,1.0,en].png | 3 +++ ...ull_PollAnswerDisclosedSelected_0_null,NEXUS_5,1.0,en].png | 3 --- ...ull_PollAnswerEndedSelected-D-6_6_null,NEXUS_5,1.0,en].png | 3 +++ ...ull_PollAnswerEndedSelected-N-6_7_null,NEXUS_5,1.0,en].png | 3 +++ ...pi_null_PollAnswerEndedSelected_0_null,NEXUS_5,1.0,en].png | 3 --- ...nswerEndedWinnerNotSelected-D-4_4_null,NEXUS_5,1.0,en].png | 3 +++ ...nswerEndedWinnerNotSelected-N-4_5_null,NEXUS_5,1.0,en].png | 3 +++ ...ollAnswerEndedWinnerNotSelected_0_null,NEXUS_5,1.0,en].png | 3 --- ...llAnswerEndedWinnerSelected-D-5_5_null,NEXUS_5,1.0,en].png | 3 +++ ...llAnswerEndedWinnerSelected-N-5_6_null,NEXUS_5,1.0,en].png | 3 +++ ...l_PollAnswerEndedWinnerSelected_0_null,NEXUS_5,1.0,en].png | 3 --- ...nswerUndisclosedNotSelected-D-2_2_null,NEXUS_5,1.0,en].png | 3 +++ ...nswerUndisclosedNotSelected-N-2_3_null,NEXUS_5,1.0,en].png | 3 +++ ...ollAnswerUndisclosedNotSelected_0_null,NEXUS_5,1.0,en].png | 3 --- ...llAnswerUndisclosedSelected-D-3_3_null,NEXUS_5,1.0,en].png | 3 +++ ...llAnswerUndisclosedSelected-N-3_4_null,NEXUS_5,1.0,en].png | 3 +++ ...l_PollAnswerUndisclosedSelected_0_null,NEXUS_5,1.0,en].png | 3 --- ...i_null_PollContentDisclosed-D-1_1_null,NEXUS_5,1.0,en].png | 3 --- ...i_null_PollContentDisclosed-D-8_8_null,NEXUS_5,1.0,en].png | 3 +++ ...i_null_PollContentDisclosed-N-1_2_null,NEXUS_5,1.0,en].png | 3 --- ...i_null_PollContentDisclosed-N-8_9_null,NEXUS_5,1.0,en].png | 3 +++ ...l.api_null_PollContentEnded-D-2_2_null,NEXUS_5,1.0,en].png | 3 --- ...l.api_null_PollContentEnded-D-9_9_null,NEXUS_5,1.0,en].png | 3 +++ ...l.api_null_PollContentEnded-N-2_3_null,NEXUS_5,1.0,en].png | 3 --- ....api_null_PollContentEnded-N-9_10_null,NEXUS_5,1.0,en].png | 3 +++ ...null_PollContentUndisclosed-D-0_0_null,NEXUS_5,1.0,en].png | 3 --- ...null_PollContentUndisclosed-D-7_7_null,NEXUS_5,1.0,en].png | 3 +++ ...null_PollContentUndisclosed-N-0_1_null,NEXUS_5,1.0,en].png | 3 --- ...null_PollContentUndisclosed-N-7_8_null,NEXUS_5,1.0,en].png | 3 +++ 38 files changed, 70 insertions(+), 49 deletions(-) create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerDisclosedNotSelected-D-0_0_null,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerDisclosedNotSelected-N-0_1_null,NEXUS_5,1.0,en].png delete mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerDisclosedNotSelected_0_null,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerDisclosedSelected-D-1_1_null,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerDisclosedSelected-N-1_2_null,NEXUS_5,1.0,en].png delete mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerDisclosedSelected_0_null,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerEndedSelected-D-6_6_null,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerEndedSelected-N-6_7_null,NEXUS_5,1.0,en].png delete mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerEndedSelected_0_null,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerEndedWinnerNotSelected-D-4_4_null,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerEndedWinnerNotSelected-N-4_5_null,NEXUS_5,1.0,en].png delete mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerEndedWinnerNotSelected_0_null,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerEndedWinnerSelected-D-5_5_null,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerEndedWinnerSelected-N-5_6_null,NEXUS_5,1.0,en].png delete mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerEndedWinnerSelected_0_null,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerUndisclosedNotSelected-D-2_2_null,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerUndisclosedNotSelected-N-2_3_null,NEXUS_5,1.0,en].png delete mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerUndisclosedNotSelected_0_null,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerUndisclosedSelected-D-3_3_null,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerUndisclosedSelected-N-3_4_null,NEXUS_5,1.0,en].png delete mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerUndisclosedSelected_0_null,NEXUS_5,1.0,en].png delete mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollContentDisclosed-D-1_1_null,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollContentDisclosed-D-8_8_null,NEXUS_5,1.0,en].png delete mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollContentDisclosed-N-1_2_null,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollContentDisclosed-N-8_9_null,NEXUS_5,1.0,en].png delete mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollContentEnded-D-2_2_null,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollContentEnded-D-9_9_null,NEXUS_5,1.0,en].png delete mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollContentEnded-N-2_3_null,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollContentEnded-N-9_10_null,NEXUS_5,1.0,en].png delete mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollContentUndisclosed-D-0_0_null,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollContentUndisclosed-D-7_7_null,NEXUS_5,1.0,en].png delete mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollContentUndisclosed-N-0_1_null,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollContentUndisclosed-N-7_8_null,NEXUS_5,1.0,en].png 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 From c64b38cf0495de77455a16c8c0a664f7c739f560 Mon Sep 17 00:00:00 2001 From: jonnyandrew Date: Fri, 15 Sep 2023 11:11:43 +0100 Subject: [PATCH 04/27] Fix placeholder spilling onto multiple lines --- .../io/element/android/libraries/textcomposer/TextComposer.kt | 2 ++ 1 file changed, 2 insertions(+) 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 b7c2b8ea40..8ac4ddf48f 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 @@ -274,6 +274,8 @@ private fun TextInput( style = defaultTypography.copy( color = ElementTheme.colors.textDisabled, ), + maxLines = 1, + overflow = TextOverflow.Ellipsis, ) } From 9583171aa26f8556ba824dd7cdd58098e7434af7 Mon Sep 17 00:00:00 2001 From: jonnyandrew Date: Fri, 15 Sep 2023 11:14:30 +0100 Subject: [PATCH 05/27] Add changelog --- changelog.d/1347.bugfix | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/1347.bugfix 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 From 149677a2e6848e1603a1977e2a3c88a93672a569 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Mart=C3=ADn?= Date: Wed, 13 Sep 2023 15:59:05 +0200 Subject: [PATCH 06/27] Add preference screen for user profile --- features/preferences/impl/build.gradle.kts | 4 +- .../preferences/impl/PreferencesFlowNode.kt | 11 + .../impl/root/PreferencesRootNode.kt | 8 +- .../impl/root/PreferencesRootView.kt | 10 +- .../impl/user/screen/UserPreferencesEvents.kt | 26 ++ .../impl/user/screen/UserPreferencesNode.kt | 46 +++ .../user/screen/UserPreferencesPresenter.kt | 146 ++++++++++ .../impl/user/screen/UserPreferencesState.kt | 33 +++ .../screen/UserPreferencesStateProvider.kt | 40 +++ .../impl/user/screen/UserPreferencesView.kt | 271 ++++++++++++++++++ .../libraries/matrix/api/MatrixClient.kt | 3 + libraries/matrix/impl/build.gradle.kts | 4 +- .../libraries/matrix/impl/RustMatrixClient.kt | 17 ++ 13 files changed, 614 insertions(+), 5 deletions(-) create mode 100644 features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/screen/UserPreferencesEvents.kt create mode 100644 features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/screen/UserPreferencesNode.kt create mode 100644 features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/screen/UserPreferencesPresenter.kt create mode 100644 features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/screen/UserPreferencesState.kt create mode 100644 features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/screen/UserPreferencesStateProvider.kt create mode 100644 features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/screen/UserPreferencesView.kt diff --git a/features/preferences/impl/build.gradle.kts b/features/preferences/impl/build.gradle.kts index 34a33ff80b..4d0403809d 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) 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..0ee29dd1f9 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,6 +38,7 @@ 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.screen.UserPreferencesNode import io.element.android.libraries.architecture.BackstackNode import io.element.android.libraries.architecture.animation.rememberDefaultTransitionHandler import io.element.android.libraries.architecture.createNode @@ -81,6 +82,9 @@ class PreferencesFlowNode @AssistedInject constructor( @Parcelize data class EditDefaultNotificationSetting(val isOneToOne: Boolean) : NavTarget + + @Parcelize + data object UserProfile : NavTarget } override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node { @@ -114,6 +118,10 @@ class PreferencesFlowNode @AssistedInject constructor( override fun onOpenAdvancedSettings() { backstack.push(NavTarget.AdvancedSettings) } + + override fun onOpenUserProfile() { + backstack.push(NavTarget.UserProfile) + } } createNode(buildContext, plugins = listOf(callback)) } @@ -149,6 +157,9 @@ class PreferencesFlowNode @AssistedInject constructor( NavTarget.AdvancedSettings -> { createNode(buildContext) } + NavTarget.UserProfile -> { + createNode(buildContext) + } } } 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..3a12ba4478 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 @@ -47,6 +47,7 @@ class PreferencesRootNode @AssistedInject constructor( fun onOpenDeveloperSettings() fun onOpenNotificationSettings() fun onOpenAdvancedSettings() + fun onOpenUserProfile() } private fun onOpenBugReport() { @@ -91,6 +92,10 @@ class PreferencesRootNode @AssistedInject constructor( plugins().forEach { it.onOpenNotificationSettings() } } + private fun onOpenUserProfile() { + plugins().forEach { it.onOpenUserProfile() } + } + @Composable override fun View(modifier: Modifier) { val state = presenter.present() @@ -108,7 +113,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..d8a05b0db8 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: () -> 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 { + 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/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/screen/UserPreferencesEvents.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/screen/UserPreferencesEvents.kt new file mode 100644 index 0000000000..c54ea98afc --- /dev/null +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/screen/UserPreferencesEvents.kt @@ -0,0 +1,26 @@ +/* + * 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.screen + +import io.element.android.libraries.matrix.ui.media.AvatarAction + +sealed interface UserPreferencesEvents { + data class HandleAvatarAction(val action: AvatarAction) : UserPreferencesEvents + data class UpdateDisplayName(val name: String) : UserPreferencesEvents + data object Save : UserPreferencesEvents + data object CancelSaveChanges : UserPreferencesEvents +} diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/screen/UserPreferencesNode.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/screen/UserPreferencesNode.kt new file mode 100644 index 0000000000..eaccb6fbbd --- /dev/null +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/screen/UserPreferencesNode.kt @@ -0,0 +1,46 @@ +/* + * 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.screen + +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.di.SessionScope + +@ContributesNode(SessionScope::class) +class UserPreferencesNode @AssistedInject constructor( + @Assisted buildContext: BuildContext, + @Assisted plugins: List, + private val presenter: UserPreferencesPresenter, +) : Node(buildContext, plugins = plugins) { + + @Composable + override fun View(modifier: Modifier) { + val state = presenter.present() + UserPreferencesView( + state = state, + onBackPressed = ::navigateUp, + onProfileEdited = ::navigateUp, // TODO: check if something else is needed + modifier = modifier + ) + } +} diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/screen/UserPreferencesPresenter.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/screen/UserPreferencesPresenter.kt new file mode 100644 index 0000000000..49e28144bb --- /dev/null +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/screen/UserPreferencesPresenter.kt @@ -0,0 +1,146 @@ +/* + * 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.screen + +import android.net.Uri +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +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 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.api.user.getCurrentUser +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 +import javax.inject.Inject + +class UserPreferencesPresenter @Inject constructor( + private val matrixClient: MatrixClient, + private val mediaPickerProvider: PickerProvider, + private val mediaPreProcessor: MediaPreProcessor, +) : Presenter { + + @Composable + override fun present(): UserPreferencesState { + var currentUser by remember { mutableStateOf(null) } + var userAvatarUri by rememberSaveable(currentUser) { mutableStateOf(currentUser?.avatarUrl?.let { Uri.parse(it) }) } + var userDisplayName by rememberSaveable(currentUser) { mutableStateOf(currentUser?.displayName) } + val cameraPhotoPicker = mediaPickerProvider.registerCameraPhotoPicker( + onResult = { uri -> if (uri != null) userAvatarUri = uri } + ) + val galleryImagePicker = mediaPickerProvider.registerGalleryImagePicker( + onResult = { uri -> if (uri != null) userAvatarUri = uri } + ) + + LaunchedEffect(Unit) { + currentUser = matrixClient.getCurrentUser() + } + + 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: UserPreferencesEvents) { + when (event) { + is UserPreferencesEvents.Save -> currentUser?.let { + localCoroutineScope.saveChanges(userDisplayName, userAvatarUri, it, saveAction) + } + is UserPreferencesEvents.HandleAvatarAction -> { + when (event.action) { + AvatarAction.ChoosePhoto -> galleryImagePicker.launch() + AvatarAction.TakePhoto -> cameraPhotoPicker.launch() + AvatarAction.Remove -> userAvatarUri = null + } + } + + is UserPreferencesEvents.UpdateDisplayName -> userDisplayName = event.name + UserPreferencesEvents.CancelSaveChanges -> saveAction.value = Async.Uninitialized + } + } + + val canSave = remember(userDisplayName, userAvatarUri, currentUser) { + val hasProfileChanged = hasDisplayNameChanged(userDisplayName, currentUser) + || hasAvatarUrlChanged(userAvatarUri, currentUser) + !userDisplayName.isNullOrBlank() && hasProfileChanged + } + + return UserPreferencesState( + userId = currentUser?.userId, + displayName = userDisplayName.orEmpty(), + userAvatarUrl = userAvatarUri, + avatarActions = avatarActions, + saveButtonEnabled = canSave && saveAction.value !is Async.Loading, + saveAction = saveAction.value, + eventSink = ::handleEvents + ) + } + + private fun hasDisplayNameChanged(name: String?, currentUser: MatrixUser?) = name?.trim() != currentUser?.displayName?.trim() + private fun hasAvatarUrlChanged(avatarUri: Uri?, currentUser: MatrixUser?) = avatarUri?.toString()?.trim() != currentUser?.avatarUrl?.trim() + + private fun CoroutineScope.saveChanges(name: String?, avatarUri: Uri?, currentUser: MatrixUser, action: MutableState>) = launch { + matrixClient.getCurrentUser() + 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() + } + } + } +} diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/screen/UserPreferencesState.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/screen/UserPreferencesState.kt new file mode 100644 index 0000000000..ee70194638 --- /dev/null +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/screen/UserPreferencesState.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.screen + +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 UserPreferencesState( + val userId: UserId?, + val displayName: String, + val userAvatarUrl: Uri?, + val avatarActions: ImmutableList, + val saveButtonEnabled: Boolean, + val saveAction: Async, + val eventSink: (UserPreferencesEvents) -> Unit +) diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/screen/UserPreferencesStateProvider.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/screen/UserPreferencesStateProvider.kt new file mode 100644 index 0000000000..2c87c591c6 --- /dev/null +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/screen/UserPreferencesStateProvider.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.screen + +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 UserPreferencesStateProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + aUserPreferencesState(), + // Add other states here + ) +} + +fun aUserPreferencesState() = UserPreferencesState( + 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/screen/UserPreferencesView.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/screen/UserPreferencesView.kt new file mode 100644 index 0000000000..aba122cef4 --- /dev/null +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/screen/UserPreferencesView.kt @@ -0,0 +1,271 @@ +/* + * 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.screen + +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.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 +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.unit.dp +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.theme.ElementTheme +import io.element.android.libraries.ui.strings.CommonStrings +import kotlinx.coroutines.launch + +@OptIn(ExperimentalMaterialApi::class, ExperimentalMaterial3Api::class) +@Composable +fun UserPreferencesView( + state: UserPreferencesState, + 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 = "Edit profile", + style = ElementTheme.typography.aliasScreenTitle, + ) + }, + navigationIcon = { BackButton(onClick = onBackPressed) }, + actions = { + TextButton( + text = stringResource(CommonStrings.action_save), + enabled = state.saveButtonEnabled, + onClick = { + focusManager.clearFocus() + state.eventSink(UserPreferencesEvents.Save) + }, + ) + } + ) + }, + ) { padding -> + Column( + modifier = Modifier + .padding(padding) + .padding(horizontal = 16.dp) + .navigationBarsPadding() + .imePadding() + .verticalScroll(rememberScrollState()) + ) { + Spacer(modifier = Modifier.height(24.dp)) + EditableAvatarView(state, ::onAvatarClicked) + Spacer(modifier = Modifier.height(60.dp)) + + LabelledTextField( + label = "Display name", + value = state.displayName, + placeholder = stringResource(CommonStrings.common_room_name_placeholder), + singleLine = true, + onValueChange = { state.eventSink(UserPreferencesEvents.UpdateDisplayName(it)) }, + ) + } + + AvatarActionBottomSheet( + actions = state.avatarActions, + modalBottomSheetState = itemActionsBottomSheetState, + onActionSelected = { state.eventSink(UserPreferencesEvents.HandleAvatarAction(it)) } + ) + + when (state.saveAction) { + is Async.Loading -> { + ProgressDialog(text = "Updating profile...") + } + + is Async.Failure -> { + ErrorDialog( + content = "Error updating profile", + onDismiss = { state.eventSink(UserPreferencesEvents.CancelSaveChanges) }, + ) + } + + is Async.Success -> { + LaunchedEffect(state.saveAction) { + onProfileEdited() + } + } + + else -> Unit + } + } +} + +@Composable +private fun EditableAvatarView( + state: UserPreferencesState, + onAvatarClicked: () -> Unit, + modifier: Modifier = Modifier, +) { + Column(modifier = modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally) { + Box( + modifier = Modifier + .size(70.dp) + .clickable(onClick = onAvatarClicked) + ) { + // TODO this might be able to be simplified into a single component once send/receive media is done + when (state.userAvatarUrl?.scheme) { + null, "mxc" -> { + Avatar( + avatarData = AvatarData(state.userId.toString(), state.displayName, state.userAvatarUrl?.toString(), size = AvatarSize.RoomHeader), + modifier = Modifier.fillMaxSize(), + ) + } + + else -> { + UnsavedAvatar( + avatarUri = state.userAvatarUrl, + 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, + ) + } + } + } +} + +@Composable +private fun LabelledReadOnlyField( + title: String, + value: String, + modifier: Modifier = Modifier +) { + Column( + modifier = modifier, + verticalArrangement = Arrangement.spacedBy(8.dp), + ) { + Text( + modifier = Modifier.padding(horizontal = 16.dp), + style = ElementTheme.typography.fontBodyMdRegular, + color = MaterialTheme.colorScheme.primary, + text = title, + ) + + Text( + modifier = Modifier.padding(start = 16.dp, end = 16.dp, top = 8.dp, bottom = 12.dp), + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.primary, + text = value, + ) + } +} + +private fun Modifier.clearFocusOnTap(focusManager: FocusManager): Modifier = + pointerInput(Unit) { + detectTapGestures(onTap = { + focusManager.clearFocus() + }) + } + +@Preview +@Composable +fun UserPreferencesViewLightPreview(@PreviewParameter(UserPreferencesStateProvider::class) state: UserPreferencesState) = + ElementPreviewLight { ContentToPreview(state) } + +@Preview +@Composable +fun UserPreferencesViewDarkPreview(@PreviewParameter(UserPreferencesStateProvider::class) state: UserPreferencesState) = + ElementPreviewDark { ContentToPreview(state) } + +@Composable +private fun ContentToPreview(state: UserPreferencesState) { + UserPreferencesView( + onBackPressed = {}, + onProfileEdited = {}, + state = state, + ) +} 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/build.gradle.kts b/libraries/matrix/impl/build.gradle.kts index a2b616f989..97eb7b6864 100644 --- a/libraries/matrix/impl/build.gradle.kts +++ b/libraries/matrix/impl/build.gradle.kts @@ -29,8 +29,8 @@ anvil { } dependencies { - // implementation(projects.libraries.rustsdk) - implementation(libs.matrix.sdk) + implementation(projects.libraries.rustsdk) +// implementation(libs.matrix.sdk) implementation(projects.libraries.di) implementation(projects.libraries.androidutils) implementation(projects.libraries.network) 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 From 2ccafec5645684ec663b194fe7f70b2971d93067 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Mart=C3=ADn?= Date: Fri, 15 Sep 2023 14:04:21 +0200 Subject: [PATCH 07/27] Changes: - Improve UI to match designs. - Extract `EditableAvatarView` component. - Create `LabelledOutlinedTextField`. - Get strings from Localazy. --- .../preferences/impl/PreferencesFlowNode.kt | 14 +- .../impl/root/PreferencesRootNode.kt | 7 +- .../impl/root/PreferencesRootView.kt | 4 +- .../EditUserProfileEvents.kt} | 12 +- .../EditUserProfileNode.kt} | 20 ++- .../EditUserProfilePresenter.kt} | 53 +++---- .../EditUserProfileState.kt} | 6 +- .../EditUserProfileStateProvider.kt} | 10 +- .../EditUserProfileView.kt} | 149 +++++------------- .../impl/src/main/res/values/localazy.xml | 9 ++ .../impl/edit/RoomDetailsEditView.kt | 76 ++------- .../components/LabelledOutlinedTextField.kt | 91 +++++++++++ .../components/avatar/AvatarSize.kt | 2 + libraries/matrix/impl/build.gradle.kts | 4 +- .../ui/components/EditableAvatarView.kt | 97 ++++++++++++ tools/localazy/config.json | 6 + 16 files changed, 325 insertions(+), 235 deletions(-) rename features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/{screen/UserPreferencesEvents.kt => editprofile/EditUserProfileEvents.kt} (64%) rename features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/{screen/UserPreferencesNode.kt => editprofile/EditUserProfileNode.kt} (69%) rename features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/{screen/UserPreferencesPresenter.kt => editprofile/EditUserProfilePresenter.kt} (77%) rename features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/{screen/UserPreferencesState.kt => editprofile/EditUserProfileState.kt} (87%) rename features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/{screen/UserPreferencesStateProvider.kt => editprofile/EditUserProfileStateProvider.kt} (78%) rename features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/{screen/UserPreferencesView.kt => editprofile/EditUserProfileView.kt} (57%) create mode 100644 features/preferences/impl/src/main/res/values/localazy.xml create mode 100644 libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/LabelledOutlinedTextField.kt create mode 100644 libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/EditableAvatarView.kt 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 0ee29dd1f9..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,11 +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.screen.UserPreferencesNode +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) @@ -84,7 +85,7 @@ class PreferencesFlowNode @AssistedInject constructor( data class EditDefaultNotificationSetting(val isOneToOne: Boolean) : NavTarget @Parcelize - data object UserProfile : NavTarget + data class UserProfile(val matrixUser: MatrixUser) : NavTarget } override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node { @@ -119,8 +120,8 @@ class PreferencesFlowNode @AssistedInject constructor( backstack.push(NavTarget.AdvancedSettings) } - override fun onOpenUserProfile() { - backstack.push(NavTarget.UserProfile) + override fun onOpenUserProfile(matrixUser: MatrixUser) { + backstack.push(NavTarget.UserProfile(matrixUser)) } } createNode(buildContext, plugins = listOf(callback)) @@ -157,8 +158,9 @@ class PreferencesFlowNode @AssistedInject constructor( NavTarget.AdvancedSettings -> { createNode(buildContext) } - NavTarget.UserProfile -> { - 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 3a12ba4478..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,7 +48,7 @@ class PreferencesRootNode @AssistedInject constructor( fun onOpenDeveloperSettings() fun onOpenNotificationSettings() fun onOpenAdvancedSettings() - fun onOpenUserProfile() + fun onOpenUserProfile(matrixUser: MatrixUser) } private fun onOpenBugReport() { @@ -92,8 +93,8 @@ class PreferencesRootNode @AssistedInject constructor( plugins().forEach { it.onOpenNotificationSettings() } } - private fun onOpenUserProfile() { - plugins().forEach { it.onOpenUserProfile() } + private fun onOpenUserProfile(matrixUser: MatrixUser) { + plugins().forEach { it.onOpenUserProfile(matrixUser) } } @Composable 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 d8a05b0db8..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 @@ -63,7 +63,7 @@ fun PreferencesRootView( onOpenAdvancedSettings: () -> Unit, onSuccessLogout: (logoutUrlResult: String?) -> Unit, onOpenNotificationSettings: () -> Unit, - onOpenUserProfile: () -> Unit, + onOpenUserProfile: (MatrixUser) -> Unit, modifier: Modifier = Modifier, ) { val snackbarHostState = rememberSnackbarHostState(snackbarMessage = state.snackbarMessage) @@ -77,7 +77,7 @@ fun PreferencesRootView( ) { UserPreferences( modifier = Modifier.clickable { - onOpenUserProfile() + state.myUser?.let(onOpenUserProfile) }, user = state.myUser, ) diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/screen/UserPreferencesEvents.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/editprofile/EditUserProfileEvents.kt similarity index 64% rename from features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/screen/UserPreferencesEvents.kt rename to features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/editprofile/EditUserProfileEvents.kt index c54ea98afc..5c53ec23c4 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/screen/UserPreferencesEvents.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/editprofile/EditUserProfileEvents.kt @@ -14,13 +14,13 @@ * limitations under the License. */ -package io.element.android.features.preferences.impl.user.screen +package io.element.android.features.preferences.impl.user.editprofile import io.element.android.libraries.matrix.ui.media.AvatarAction -sealed interface UserPreferencesEvents { - data class HandleAvatarAction(val action: AvatarAction) : UserPreferencesEvents - data class UpdateDisplayName(val name: String) : UserPreferencesEvents - data object Save : UserPreferencesEvents - data object CancelSaveChanges : UserPreferencesEvents +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/screen/UserPreferencesNode.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/editprofile/EditUserProfileNode.kt similarity index 69% rename from features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/screen/UserPreferencesNode.kt rename to features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/editprofile/EditUserProfileNode.kt index eaccb6fbbd..738ae4ec6d 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/screen/UserPreferencesNode.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/editprofile/EditUserProfileNode.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.features.preferences.impl.user.screen +package io.element.android.features.preferences.impl.user.editprofile import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier @@ -24,22 +24,32 @@ 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 UserPreferencesNode @AssistedInject constructor( +class EditUserProfileNode @AssistedInject constructor( @Assisted buildContext: BuildContext, @Assisted plugins: List, - private val presenter: UserPreferencesPresenter, + 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() - UserPreferencesView( + EditUserProfileView( state = state, onBackPressed = ::navigateUp, - onProfileEdited = ::navigateUp, // TODO: check if something else is needed + onProfileEdited = ::navigateUp, modifier = modifier ) } diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/screen/UserPreferencesPresenter.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/editprofile/EditUserProfilePresenter.kt similarity index 77% rename from features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/screen/UserPreferencesPresenter.kt rename to features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/editprofile/EditUserProfilePresenter.kt index 49e28144bb..6557079de3 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/screen/UserPreferencesPresenter.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/editprofile/EditUserProfilePresenter.kt @@ -14,19 +14,19 @@ * limitations under the License. */ -package io.element.android.features.preferences.impl.user.screen +package io.element.android.features.preferences.impl.user.editprofile import android.net.Uri import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect 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 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 @@ -41,19 +41,23 @@ import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch import timber.log.Timber -import javax.inject.Inject -class UserPreferencesPresenter @Inject constructor( +class EditUserProfilePresenter @AssistedInject constructor( + @Assisted private val matrixUser: MatrixUser, private val matrixClient: MatrixClient, private val mediaPickerProvider: PickerProvider, private val mediaPreProcessor: MediaPreProcessor, -) : Presenter { +) : Presenter { + + @AssistedFactory + interface Factory { + fun create(matrixUser: MatrixUser): EditUserProfilePresenter + } @Composable - override fun present(): UserPreferencesState { - var currentUser by remember { mutableStateOf(null) } - var userAvatarUri by rememberSaveable(currentUser) { mutableStateOf(currentUser?.avatarUrl?.let { Uri.parse(it) }) } - var userDisplayName by rememberSaveable(currentUser) { mutableStateOf(currentUser?.displayName) } + override fun present(): EditUserProfileState { + var userAvatarUri = remember { matrixUser.avatarUrl?.let { Uri.parse(it) } } + var userDisplayName = remember { matrixUser.displayName } val cameraPhotoPicker = mediaPickerProvider.registerCameraPhotoPicker( onResult = { uri -> if (uri != null) userAvatarUri = uri } ) @@ -61,10 +65,6 @@ class UserPreferencesPresenter @Inject constructor( onResult = { uri -> if (uri != null) userAvatarUri = uri } ) - LaunchedEffect(Unit) { - currentUser = matrixClient.getCurrentUser() - } - val avatarActions by remember(userAvatarUri) { derivedStateOf { listOfNotNull( @@ -77,12 +77,10 @@ class UserPreferencesPresenter @Inject constructor( val saveAction: MutableState> = remember { mutableStateOf(Async.Uninitialized) } val localCoroutineScope = rememberCoroutineScope() - fun handleEvents(event: UserPreferencesEvents) { + fun handleEvents(event: EditUserProfileEvents) { when (event) { - is UserPreferencesEvents.Save -> currentUser?.let { - localCoroutineScope.saveChanges(userDisplayName, userAvatarUri, it, saveAction) - } - is UserPreferencesEvents.HandleAvatarAction -> { + is EditUserProfileEvents.Save -> localCoroutineScope.saveChanges(userDisplayName, userAvatarUri, matrixUser, saveAction) + is EditUserProfileEvents.HandleAvatarAction -> { when (event.action) { AvatarAction.ChoosePhoto -> galleryImagePicker.launch() AvatarAction.TakePhoto -> cameraPhotoPicker.launch() @@ -90,19 +88,19 @@ class UserPreferencesPresenter @Inject constructor( } } - is UserPreferencesEvents.UpdateDisplayName -> userDisplayName = event.name - UserPreferencesEvents.CancelSaveChanges -> saveAction.value = Async.Uninitialized + is EditUserProfileEvents.UpdateDisplayName -> userDisplayName = event.name + EditUserProfileEvents.CancelSaveChanges -> saveAction.value = Async.Uninitialized } } - val canSave = remember(userDisplayName, userAvatarUri, currentUser) { - val hasProfileChanged = hasDisplayNameChanged(userDisplayName, currentUser) - || hasAvatarUrlChanged(userAvatarUri, currentUser) + val canSave = remember(userDisplayName, userAvatarUri) { + val hasProfileChanged = hasDisplayNameChanged(userDisplayName, matrixUser) + || hasAvatarUrlChanged(userAvatarUri, matrixUser) !userDisplayName.isNullOrBlank() && hasProfileChanged } - return UserPreferencesState( - userId = currentUser?.userId, + return EditUserProfileState( + userId = matrixUser.userId, displayName = userDisplayName.orEmpty(), userAvatarUrl = userAvatarUri, avatarActions = avatarActions, @@ -116,7 +114,6 @@ class UserPreferencesPresenter @Inject constructor( private fun hasAvatarUrlChanged(avatarUri: Uri?, currentUser: MatrixUser?) = avatarUri?.toString()?.trim() != currentUser?.avatarUrl?.trim() private fun CoroutineScope.saveChanges(name: String?, avatarUri: Uri?, currentUser: MatrixUser, action: MutableState>) = launch { - matrixClient.getCurrentUser() val results = mutableListOf>() suspend { if (!name.isNullOrEmpty() && name.trim() != currentUser.displayName.orEmpty().trim()) { diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/screen/UserPreferencesState.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/editprofile/EditUserProfileState.kt similarity index 87% rename from features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/screen/UserPreferencesState.kt rename to features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/editprofile/EditUserProfileState.kt index ee70194638..87668e6f45 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/screen/UserPreferencesState.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/editprofile/EditUserProfileState.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.features.preferences.impl.user.screen +package io.element.android.features.preferences.impl.user.editprofile import android.net.Uri import io.element.android.libraries.architecture.Async @@ -22,12 +22,12 @@ 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 UserPreferencesState( +data class EditUserProfileState( val userId: UserId?, val displayName: String, val userAvatarUrl: Uri?, val avatarActions: ImmutableList, val saveButtonEnabled: Boolean, val saveAction: Async, - val eventSink: (UserPreferencesEvents) -> Unit + val eventSink: (EditUserProfileEvents) -> Unit ) diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/screen/UserPreferencesStateProvider.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/editprofile/EditUserProfileStateProvider.kt similarity index 78% rename from features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/screen/UserPreferencesStateProvider.kt rename to features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/editprofile/EditUserProfileStateProvider.kt index 2c87c591c6..5e4ccb95cb 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/screen/UserPreferencesStateProvider.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/editprofile/EditUserProfileStateProvider.kt @@ -14,22 +14,22 @@ * limitations under the License. */ -package io.element.android.features.preferences.impl.user.screen +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 UserPreferencesStateProvider : PreviewParameterProvider { - override val values: Sequence +open class EditUserProfileStateProvider : PreviewParameterProvider { + override val values: Sequence get() = sequenceOf( - aUserPreferencesState(), + aEditUserProfileState(), // Add other states here ) } -fun aUserPreferencesState() = UserPreferencesState( +fun aEditUserProfileState() = EditUserProfileState( userId = UserId("@john.doe:matrix.org"), displayName = "John Doe", userAvatarUrl = null, diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/screen/UserPreferencesView.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/editprofile/EditUserProfileView.kt similarity index 57% rename from features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/screen/UserPreferencesView.kt rename to features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/editprofile/EditUserProfileView.kt index aba122cef4..02423470a9 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/screen/UserPreferencesView.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/editprofile/EditUserProfileView.kt @@ -14,71 +14,59 @@ * limitations under the License. */ -package io.element.android.features.preferences.impl.user.screen +package io.element.android.features.preferences.impl.user.editprofile -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.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 import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview 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.LabelledTextField +import io.element.android.libraries.designsystem.components.LabelledOutlinedTextField 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 @OptIn(ExperimentalMaterialApi::class, ExperimentalMaterial3Api::class) @Composable -fun UserPreferencesView( - state: UserPreferencesState, +fun EditUserProfileView( + state: EditUserProfileState, onBackPressed: () -> Unit, onProfileEdited: () -> Unit, modifier: Modifier = Modifier, @@ -102,7 +90,7 @@ fun UserPreferencesView( TopAppBar( title = { Text( - text = "Edit profile", + text = stringResource(R.string.screen_edit_profile_title), style = ElementTheme.typography.aliasScreenTitle, ) }, @@ -113,7 +101,7 @@ fun UserPreferencesView( enabled = state.saveButtonEnabled, onClick = { focusManager.clearFocus() - state.eventSink(UserPreferencesEvents.Save) + state.eventSink(EditUserProfileEvents.Save) }, ) } @@ -129,33 +117,50 @@ fun UserPreferencesView( .verticalScroll(rememberScrollState()) ) { Spacer(modifier = Modifier.height(24.dp)) - EditableAvatarView(state, ::onAvatarClicked) - Spacer(modifier = Modifier.height(60.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)) - LabelledTextField( - label = "Display name", + 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(UserPreferencesEvents.UpdateDisplayName(it)) }, + onValueChange = { state.eventSink(EditUserProfileEvents.UpdateDisplayName(it)) }, ) } AvatarActionBottomSheet( actions = state.avatarActions, modalBottomSheetState = itemActionsBottomSheetState, - onActionSelected = { state.eventSink(UserPreferencesEvents.HandleAvatarAction(it)) } + onActionSelected = { state.eventSink(EditUserProfileEvents.HandleAvatarAction(it)) } ) when (state.saveAction) { is Async.Loading -> { - ProgressDialog(text = "Updating profile...") + ProgressDialog(text = stringResource(R.string.screen_edit_profile_updating_details)) } is Async.Failure -> { ErrorDialog( - content = "Error updating profile", - onDismiss = { state.eventSink(UserPreferencesEvents.CancelSaveChanges) }, + title = stringResource(R.string.screen_edit_profile_error_title), + content = stringResource(R.string.screen_edit_profile_error), + onDismiss = { state.eventSink(EditUserProfileEvents.CancelSaveChanges) }, ) } @@ -170,82 +175,8 @@ fun UserPreferencesView( } } -@Composable -private fun EditableAvatarView( - state: UserPreferencesState, - onAvatarClicked: () -> Unit, - modifier: Modifier = Modifier, -) { - Column(modifier = modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally) { - Box( - modifier = Modifier - .size(70.dp) - .clickable(onClick = onAvatarClicked) - ) { - // TODO this might be able to be simplified into a single component once send/receive media is done - when (state.userAvatarUrl?.scheme) { - null, "mxc" -> { - Avatar( - avatarData = AvatarData(state.userId.toString(), state.displayName, state.userAvatarUrl?.toString(), size = AvatarSize.RoomHeader), - modifier = Modifier.fillMaxSize(), - ) - } - - else -> { - UnsavedAvatar( - avatarUri = state.userAvatarUrl, - 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, - ) - } - } - } -} - -@Composable -private fun LabelledReadOnlyField( - title: String, - value: String, - modifier: Modifier = Modifier -) { - Column( - modifier = modifier, - verticalArrangement = Arrangement.spacedBy(8.dp), - ) { - Text( - modifier = Modifier.padding(horizontal = 16.dp), - style = ElementTheme.typography.fontBodyMdRegular, - color = MaterialTheme.colorScheme.primary, - text = title, - ) - - Text( - modifier = Modifier.padding(start = 16.dp, end = 16.dp, top = 8.dp, bottom = 12.dp), - style = MaterialTheme.typography.bodyLarge, - color = MaterialTheme.colorScheme.primary, - text = value, - ) - } -} - private fun Modifier.clearFocusOnTap(focusManager: FocusManager): Modifier = - pointerInput(Unit) { + this.pointerInput(Unit) { detectTapGestures(onTap = { focusManager.clearFocus() }) @@ -253,17 +184,17 @@ private fun Modifier.clearFocusOnTap(focusManager: FocusManager): Modifier = @Preview @Composable -fun UserPreferencesViewLightPreview(@PreviewParameter(UserPreferencesStateProvider::class) state: UserPreferencesState) = +fun EditUserProfileViewLightPreview(@PreviewParameter(EditUserProfileStateProvider::class) state: EditUserProfileState) = ElementPreviewLight { ContentToPreview(state) } @Preview @Composable -fun UserPreferencesViewDarkPreview(@PreviewParameter(UserPreferencesStateProvider::class) state: UserPreferencesState) = +fun EditUserProfileViewDarkPreview(@PreviewParameter(EditUserProfileStateProvider::class) state: EditUserProfileState) = ElementPreviewDark { ContentToPreview(state) } @Composable -private fun ContentToPreview(state: UserPreferencesState) { - UserPreferencesView( +private fun ContentToPreview(state: EditUserProfileState) { + 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/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..4553161d73 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, @@ -279,7 +223,7 @@ private fun LabelledReadOnlyField( } private fun Modifier.clearFocusOnTap(focusManager: FocusManager): Modifier = - pointerInput(Unit) { + this.pointerInput(Unit) { detectTapGestures(onTap = { focusManager.clearFocus() }) 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..a273acf25c --- /dev/null +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/LabelledOutlinedTextField.kt @@ -0,0 +1,91 @@ +/* + * 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.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import io.element.android.libraries.designsystem.preview.ElementPreviewDark +import io.element.android.libraries.designsystem.preview.ElementPreviewLight +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, + ) + } +} + +@Preview +@Composable +internal fun LabelledOutlinedTextFieldLightPreview() = ElementPreviewLight { ContentToPreview() } + +@Preview +@Composable +internal fun LabelledOutlinedTextFieldDarkPreview() = ElementPreviewDark { ContentToPreview() } + +@Composable +private fun ContentToPreview() { + 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/matrix/impl/build.gradle.kts b/libraries/matrix/impl/build.gradle.kts index 97eb7b6864..88cc929e79 100644 --- a/libraries/matrix/impl/build.gradle.kts +++ b/libraries/matrix/impl/build.gradle.kts @@ -29,8 +29,8 @@ anvil { } dependencies { - implementation(projects.libraries.rustsdk) -// implementation(libs.matrix.sdk) +// implementation(projects.libraries.rustsdk) + implementation(libs.matrix.sdk) implementation(projects.libraries.di) implementation(projects.libraries.androidutils) implementation(projects.libraries.network) 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/tools/localazy/config.json b/tools/localazy/config.json index b92e02f670..a69b7d2273 100644 --- a/tools/localazy/config.json +++ b/tools/localazy/config.json @@ -128,6 +128,12 @@ "includeRegex": [ "screen_create_poll_.*" ] + }, + { + "name": ":features:preferences:impl", + "includeRegex": [ + "screen_edit_profile_.*" + ] } ] } From 60104822cd15a72fe20f5b89fa3f4feadc1c9dbb Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 15 Sep 2023 14:45:46 +0200 Subject: [PATCH 08/27] Notification: log roomId and eventId --- .../push/impl/notifications/NotificationBroadcastReceiver.kt | 3 ++- .../android/libraries/push/impl/push/DefaultPushHandler.kt | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) 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..b2ccebbb05 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 @@ -41,10 +41,11 @@ 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") + Timber.tag(loggerTag.value).d("received: ${intent.action} ${intent.data}") 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("received for: ${roomId?.value}/${eventId?.value}") when (intent.action) { actionIds.smartReply -> handleSmartReply(intent, context) 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..b9ca4a5cca 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 @@ -67,7 +67,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") From 1c56d95eab1b0dcc1cb9144953ec49b559a268e5 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 15 Sep 2023 15:42:01 +0200 Subject: [PATCH 09/27] Do not render notification if the user has dismiss the notification. It should not change anything, just avoid doing useless notification rendering. --- .../invitelist/impl/InviteListPresenter.kt | 4 +- .../NotificationDrawerManager.kt | 2 +- .../DefaultNotificationDrawerManager.kt | 53 ++++++++++++------- .../NotificationBroadcastReceiver.kt | 14 ++--- .../FakeNotificationDrawerManager.kt | 2 +- 5 files changed, 44 insertions(+), 31 deletions(-) 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/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/notifications/DefaultNotificationDrawerManager.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotificationDrawerManager.kt index 91b3987251..db198bd138 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 @@ -89,7 +89,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( @@ -132,7 +136,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 +144,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 +154,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 +164,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 +179,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 +192,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 +203,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,21 +220,24 @@ class DefaultNotificationDrawerManager @Inject constructor( } } - private fun updateEvents(action: DefaultNotificationDrawerManager.(NotificationEventQueue) -> Unit) { + private fun updateEvents( + doRender: Boolean, + action: DefaultNotificationDrawerManager.(NotificationEventQueue) -> Unit, + ) { notificationState.updateQueuedEvents(this) { 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") 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") @@ -234,7 +245,7 @@ class DefaultNotificationDrawerManager @Inject constructor( } } - private suspend fun refreshNotificationDrawerBg() { + private suspend fun refreshNotificationDrawerBg(doRender: Boolean) { Timber.v("refreshNotificationDrawerBg()") val eventsToRender = notificationState.updateQueuedEvents(this) { queuedEvents, renderedEvents -> notifiableEventProcessor.process(queuedEvents.rawEvents(), renderedEvents).also { @@ -246,7 +257,9 @@ class DefaultNotificationDrawerManager @Inject constructor( Timber.d("Skipping notification update due to event list not changing") } else { notificationState.clearAndAddRenderedEvents(eventsToRender) - renderEvents(eventsToRender) + if (doRender) { + renderEvents(eventsToRender) + } persistEvents() } } 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 b2ccebbb05..2709f3840c 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 @@ -50,26 +50,26 @@ class NotificationBroadcastReceiver : BroadcastReceiver() { 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/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 } } From 9a461f4b74cb86523f99e36547978193ac5610f8 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 15 Sep 2023 15:11:22 +0200 Subject: [PATCH 10/27] No need to force `DefaultNotificationDrawerManager` receiver for this private fun. --- .../push/impl/notifications/DefaultNotificationDrawerManager.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 db198bd138..0fbd28aa3b 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 @@ -222,7 +222,7 @@ class DefaultNotificationDrawerManager @Inject constructor( private fun updateEvents( doRender: Boolean, - action: DefaultNotificationDrawerManager.(NotificationEventQueue) -> Unit, + action: (NotificationEventQueue) -> Unit, ) { notificationState.updateQueuedEvents(this) { queuedEvents, _ -> action(queuedEvents) From 020839f17e8c01362f544a5f9a5968adedf36294 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 15 Sep 2023 15:22:25 +0200 Subject: [PATCH 11/27] Simplify again the code. --- .../impl/notifications/DefaultNotificationDrawerManager.kt | 4 ++-- .../libraries/push/impl/notifications/NotificationState.kt | 5 ++--- 2 files changed, 4 insertions(+), 5 deletions(-) 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 0fbd28aa3b..211d8d935a 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 @@ -224,7 +224,7 @@ class DefaultNotificationDrawerManager @Inject constructor( doRender: Boolean, action: (NotificationEventQueue) -> Unit, ) { - notificationState.updateQueuedEvents(this) { queuedEvents, _ -> + notificationState.updateQueuedEvents { queuedEvents, _ -> action(queuedEvents) } coroutineScope.refreshNotificationDrawer(doRender) @@ -247,7 +247,7 @@ class DefaultNotificationDrawerManager @Inject constructor( private suspend fun refreshNotificationDrawerBg(doRender: Boolean) { Timber.v("refreshNotificationDrawerBg()") - val eventsToRender = notificationState.updateQueuedEvents(this) { queuedEvents, renderedEvents -> + val eventsToRender = notificationState.updateQueuedEvents { queuedEvents, renderedEvents -> notifiableEventProcessor.process(queuedEvents.rawEvents(), renderedEvents).also { queuedEvents.clearAndAdd(it.onlyKeptEvents()) } 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) } } From 7d080ea3373f18f9b99f4375e19c7d4232da88e5 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 15 Sep 2023 15:57:39 +0200 Subject: [PATCH 12/27] Add Timber tags. --- .../notifications/NotifiableEventProcessor.kt | 10 +++++--- .../notifications/NotifiableEventResolver.kt | 4 ++-- .../notifications/NotificationRenderer.kt | 24 +++++++++++-------- 3 files changed, 23 insertions(+), 15 deletions(-) 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..709dfd14e8 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,7 +16,9 @@ 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.log.notificationLoggerTag 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 @@ -29,6 +31,8 @@ import javax.inject.Inject private typealias ProcessedEvents = List> +private val loggerTag = LoggerTag("NotifiableEventProcessor", notificationLoggerTag) + class NotifiableEventProcessor @Inject constructor( private val outdatedDetector: OutdatedEventDetector, private val appNavigationStateService: AppNavigationStateService, @@ -45,10 +49,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 +62,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..66ca7a0abd 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,7 @@ 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.log.notificationLoggerTag 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 +47,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", 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/NotificationRenderer.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationRenderer.kt index a6179b3ec8..69292e5c6b 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,8 +16,10 @@ 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.log.notificationLoggerTag 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 @@ -26,6 +28,8 @@ import io.element.android.libraries.push.impl.notifications.model.SimpleNotifiab import timber.log.Timber import javax.inject.Inject +private val loggerTag = LoggerTag("NotificationRenderer", notificationLoggerTag) + class NotificationRenderer @Inject constructor( private val notificationIdProvider: NotificationIdProvider, private val notificationDisplayer: NotificationDisplayer, @@ -54,7 +58,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 +68,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 +88,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 +108,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 +128,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 +147,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), From e73f96eb6adb1f9922ac7f5568a744a804209965 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 6 Sep 2023 16:06:47 +0200 Subject: [PATCH 13/27] Avoid logging warning why attempting to delete unexisting file. --- .../io/element/android/libraries/androidutils/file/File.kt | 1 + 1 file changed, 1 insertion(+) 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") From d3231487aa554b2c1af205d416cc65f336c215bd Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 15 Sep 2023 16:42:04 +0200 Subject: [PATCH 14/27] Log param. --- .../impl/notifications/DefaultNotificationDrawerManager.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 211d8d935a..9621663e98 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 @@ -233,7 +233,7 @@ class DefaultNotificationDrawerManager @Inject constructor( private fun CoroutineScope.refreshNotificationDrawer(doRender: Boolean) = launch { // Implement last throttler val canHandle = firstThrottler.canHandle() - Timber.v("refreshNotificationDrawer(), delay: ${canHandle.waitMillis()} ms") + Timber.v("refreshNotificationDrawer($doRender), delay: ${canHandle.waitMillis()} ms") withContext(dispatchers.io) { delay(canHandle.waitMillis()) try { @@ -246,7 +246,7 @@ class DefaultNotificationDrawerManager @Inject constructor( } private suspend fun refreshNotificationDrawerBg(doRender: Boolean) { - Timber.v("refreshNotificationDrawerBg()") + Timber.v("refreshNotificationDrawerBg($doRender)") val eventsToRender = notificationState.updateQueuedEvents { queuedEvents, renderedEvents -> notifiableEventProcessor.process(queuedEvents.rawEvents(), renderedEvents).also { queuedEvents.clearAndAdd(it.onlyKeptEvents()) From b9317ee2aa313b3c4d62205cb5730f8dd49b1d50 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 15 Sep 2023 16:43:24 +0200 Subject: [PATCH 15/27] Add log tag --- .../DefaultNotificationDrawerManager.kt | 20 +++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) 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 9621663e98..09ed8f1194 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 @@ -30,6 +31,7 @@ import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.matrix.api.core.ThreadId import io.element.android.libraries.matrix.api.user.MatrixUser import io.element.android.libraries.push.api.notifications.NotificationDrawerManager +import io.element.android.libraries.push.impl.log.notificationLoggerTag import io.element.android.libraries.push.impl.notifications.model.NotifiableEvent import io.element.android.services.appnavstate.api.AppNavigationStateService import io.element.android.services.appnavstate.api.NavigationState @@ -41,6 +43,8 @@ import kotlinx.coroutines.withContext import timber.log.Timber import javax.inject.Inject +private val loggerTag = LoggerTag("DefaultNotificationDrawerManager", 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. @@ -116,13 +120,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 } @@ -233,20 +237,20 @@ class DefaultNotificationDrawerManager @Inject constructor( private fun CoroutineScope.refreshNotificationDrawer(doRender: Boolean) = launch { // Implement last throttler val canHandle = firstThrottler.canHandle() - Timber.v("refreshNotificationDrawer($doRender), delay: ${canHandle.waitMillis()} ms") + Timber.tag(loggerTag.value).v("refreshNotificationDrawer($doRender), delay: ${canHandle.waitMillis()} ms") withContext(dispatchers.io) { delay(canHandle.waitMillis()) try { 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(doRender: Boolean) { - Timber.v("refreshNotificationDrawerBg($doRender)") + Timber.tag(loggerTag.value).v("refreshNotificationDrawerBg($doRender)") val eventsToRender = notificationState.updateQueuedEvents { queuedEvents, renderedEvents -> notifiableEventProcessor.process(queuedEvents.rawEvents(), renderedEvents).also { queuedEvents.clearAndAdd(it.onlyKeptEvents()) @@ -254,7 +258,7 @@ class DefaultNotificationDrawerManager @Inject constructor( } 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) if (doRender) { @@ -278,7 +282,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 From 2e9cc25759d1111cf6698b0d545ac783572187ef Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 15 Sep 2023 16:52:46 +0200 Subject: [PATCH 16/27] Log the tag if present. --- .../android/libraries/matrix/impl/tracing/RustTracingTree.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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, ) } From 0d204f5e98d5e343e84dfcb62ddfffbf65782c05 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 15 Sep 2023 17:03:32 +0200 Subject: [PATCH 17/27] Share tag with other modules. --- .../libraries/core/log/logger/LoggerTag.kt | 6 ++--- .../libraries/push/impl/PushersManager.kt | 3 +-- .../libraries/push/impl/log/LoggerTag.kt | 22 ------------------- .../DefaultNotificationDrawerManager.kt | 3 +-- .../notifications/NotifiableEventProcessor.kt | 3 +-- .../notifications/NotifiableEventResolver.kt | 3 +-- .../NotificationBroadcastReceiver.kt | 6 ++--- .../NotificationEventPersistence.kt | 3 +-- .../notifications/NotificationRenderer.kt | 3 +-- .../push/impl/push/DefaultPushHandler.kt | 3 +-- .../firebase/FirebaseNewTokenHandler.kt | 2 +- .../firebase/FirebasePushProvider.kt | 2 +- .../VectorFirebaseMessagingService.kt | 2 +- .../UnifiedPushNewGatewayHandler.kt | 2 +- .../VectorUnifiedPushMessagingReceiver.kt | 2 +- 15 files changed, 16 insertions(+), 49 deletions(-) delete mode 100644 libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/log/LoggerTag.kt 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/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/log/LoggerTag.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/log/LoggerTag.kt deleted file mode 100644 index 3fa613d097..0000000000 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/log/LoggerTag.kt +++ /dev/null @@ -1,22 +0,0 @@ -/* - * 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.push.impl.log - -import io.element.android.libraries.core.log.logger.LoggerTag - -internal val pushLoggerTag = LoggerTag("Push") -internal val notificationLoggerTag = LoggerTag("Notification", pushLoggerTag) 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 09ed8f1194..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 @@ -31,7 +31,6 @@ import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.matrix.api.core.ThreadId import io.element.android.libraries.matrix.api.user.MatrixUser import io.element.android.libraries.push.api.notifications.NotificationDrawerManager -import io.element.android.libraries.push.impl.log.notificationLoggerTag import io.element.android.libraries.push.impl.notifications.model.NotifiableEvent import io.element.android.services.appnavstate.api.AppNavigationStateService import io.element.android.services.appnavstate.api.NavigationState @@ -43,7 +42,7 @@ import kotlinx.coroutines.withContext import timber.log.Timber import javax.inject.Inject -private val loggerTag = LoggerTag("DefaultNotificationDrawerManager", notificationLoggerTag) +private val loggerTag = LoggerTag("DefaultNotificationDrawerManager", LoggerTag.NotificationLoggerTag) /** * The NotificationDrawerManager receives notification events as they arrived (from event stream or fcm) and 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 709dfd14e8..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 @@ -18,7 +18,6 @@ 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.log.notificationLoggerTag 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 @@ -31,7 +30,7 @@ import javax.inject.Inject private typealias ProcessedEvents = List> -private val loggerTag = LoggerTag("NotifiableEventProcessor", notificationLoggerTag) +private val loggerTag = LoggerTag("NotifiableEventProcessor", LoggerTag.NotificationLoggerTag) class NotifiableEventProcessor @Inject constructor( private val outdatedDetector: OutdatedEventDetector, 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 66ca7a0abd..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.notificationLoggerTag 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", notificationLoggerTag) +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 2709f3840c..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,11 +40,10 @@ class NotificationBroadcastReceiver : BroadcastReceiver() { override fun onReceive(context: Context?, intent: Intent?) { if (intent == null || context == null) return context.bindings().inject(this) - Timber.tag(loggerTag.value).d("received: ${intent.action} ${intent.data}") 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("received for: ${roomId?.value}/${eventId?.value}") + Timber.tag(loggerTag.value).d("onReceive: ${intent.action} ${intent.data} for: ${roomId?.value}/${eventId?.value}") when (intent.action) { actionIds.smartReply -> handleSmartReply(intent, context) 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 69292e5c6b..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 @@ -19,7 +19,6 @@ 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.log.notificationLoggerTag 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 @@ -28,7 +27,7 @@ import io.element.android.libraries.push.impl.notifications.model.SimpleNotifiab import timber.log.Timber import javax.inject.Inject -private val loggerTag = LoggerTag("NotificationRenderer", notificationLoggerTag) +private val loggerTag = LoggerTag("NotificationRenderer", LoggerTag.NotificationLoggerTag) class NotificationRenderer @Inject constructor( private val notificationIdProvider: NotificationIdProvider, 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 b9ca4a5cca..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( 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 From 98bb91cd2733e1d829b4778e96825511807ca3ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Mart=C3=ADn?= Date: Fri, 15 Sep 2023 16:00:36 +0200 Subject: [PATCH 18/27] Create tests --- features/preferences/impl/build.gradle.kts | 3 + .../editprofile/EditUserProfilePresenter.kt | 11 +- .../EditUserProfilePresenterTest.kt | 494 ++++++++++++++++++ .../libraries/matrix/test/FakeMatrixClient.kt | 37 ++ .../ui/components/MatrixUserProvider.kt | 7 +- 5 files changed, 546 insertions(+), 6 deletions(-) create mode 100644 features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/user/editprofile/EditUserProfilePresenterTest.kt diff --git a/features/preferences/impl/build.gradle.kts b/features/preferences/impl/build.gradle.kts index 4d0403809d..b28a068708 100644 --- a/features/preferences/impl/build.gradle.kts +++ b/features/preferences/impl/build.gradle.kts @@ -66,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/user/editprofile/EditUserProfilePresenter.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/editprofile/EditUserProfilePresenter.kt index 6557079de3..edbb2467f2 100644 --- 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 @@ -24,6 +24,8 @@ 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 dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject @@ -33,7 +35,6 @@ 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.api.user.getCurrentUser 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 @@ -56,8 +57,8 @@ class EditUserProfilePresenter @AssistedInject constructor( @Composable override fun present(): EditUserProfileState { - var userAvatarUri = remember { matrixUser.avatarUrl?.let { Uri.parse(it) } } - var userDisplayName = remember { matrixUser.displayName } + 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 } ) @@ -106,7 +107,7 @@ class EditUserProfilePresenter @AssistedInject constructor( avatarActions = avatarActions, saveButtonEnabled = canSave && saveAction.value !is Async.Loading, saveAction = saveAction.value, - eventSink = ::handleEvents + eventSink = { handleEvents(it) }, ) } @@ -138,6 +139,6 @@ class EditUserProfilePresenter @AssistedInject constructor( } else { matrixClient.removeAvatar().getOrThrow() } - } + }.onFailure { it.printStackTrace() } } } 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..c377265689 --- /dev/null +++ b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/user/editprofile/EditUserProfilePresenterTest.kt @@ -0,0 +1,494 @@ +/* + * 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.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 anEditUserProfilePresenter( + 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 room info`() = runTest { + val user = aMatrixUser(avatarUrl = AN_AVATAR_URL) + val presenter = anEditUserProfilePresenter(matrixUser = user) + + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + assertThat(initialState.userId).isEqualTo(user.userId.value) + assertThat(initialState.displayName).isEqualTo(user.displayName) + assertThat(initialState.userAvatarUrl).isEqualTo(userAvatarUri) + assertThat(initialState.avatarActions).containsExactly( + AvatarAction.ChoosePhoto, + AvatarAction.TakePhoto, + AvatarAction.Remove + ) + assertThat(initialState.saveButtonEnabled).isEqualTo(false) + 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 = anEditUserProfilePresenter(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 = anEditUserProfilePresenter(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 = anEditUserProfilePresenter(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 = anEditUserProfilePresenter(matrixUser = user) + + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + assertThat(initialState.saveButtonEnabled).isEqualTo(false) + + // Once a change is made, the save button is enabled + initialState.eventSink(EditUserProfileEvents.UpdateDisplayName("Name II")) + awaitItem().apply { + assertThat(saveButtonEnabled).isEqualTo(true) + } + + // If it's reverted then the save disables again + initialState.eventSink(EditUserProfileEvents.UpdateDisplayName("Name")) + awaitItem().apply { + assertThat(saveButtonEnabled).isEqualTo(false) + } + + // Make a change... + initialState.eventSink(EditUserProfileEvents.HandleAvatarAction(AvatarAction.Remove)) + awaitItem().apply { + assertThat(saveButtonEnabled).isEqualTo(true) + } + + // Revert it... + initialState.eventSink(EditUserProfileEvents.HandleAvatarAction(AvatarAction.ChoosePhoto)) + awaitItem().apply { + assertThat(saveButtonEnabled).isEqualTo(false) + } + } + } + + @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 = anEditUserProfilePresenter(matrixUser = user) + + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + assertThat(initialState.saveButtonEnabled).isEqualTo(false) + + // Once a change is made, the save button is enabled + initialState.eventSink(EditUserProfileEvents.UpdateDisplayName("Name II")) + awaitItem().apply { + assertThat(saveButtonEnabled).isEqualTo(true) + } + + // If it's reverted then the save disables again + initialState.eventSink(EditUserProfileEvents.UpdateDisplayName("fallback")) + awaitItem().apply { + assertThat(saveButtonEnabled).isEqualTo(false) + } + + // Make a change... + initialState.eventSink(EditUserProfileEvents.HandleAvatarAction(AvatarAction.ChoosePhoto)) + awaitItem().apply { + assertThat(saveButtonEnabled).isEqualTo(true) + } + + // Revert it... + initialState.eventSink(EditUserProfileEvents.HandleAvatarAction(AvatarAction.Remove)) + awaitItem().apply { + assertThat(saveButtonEnabled).isEqualTo(false) + } + } + } + + @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 = anEditUserProfilePresenter( + 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) + skipItems(5) + + assertThat(matrixClient.setDisplayNameCalled).isTrue() + assertThat(matrixClient.removeAvatarCalled).isTrue() + assertThat(matrixClient.uploadAvatarCalled).isFalse() + + cancelAndIgnoreRemainingEvents() + } + } + + @Test + fun `present - save doesn't 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 = anEditUserProfilePresenter( + matrixClient = matrixClient, + matrixUser = user + ) + + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + + initialState.eventSink(EditUserProfileEvents.UpdateDisplayName(" Name ")) + initialState.eventSink(EditUserProfileEvents.Save) + + assertThat(matrixClient.setDisplayNameCalled).isTrue() + assertThat(matrixClient.uploadAvatarCalled).isFalse() + assertThat(matrixClient.removeAvatarCalled).isFalse() + + cancelAndIgnoreRemainingEvents() + } + } + + @Test + fun `present - save doesn't 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 = anEditUserProfilePresenter( + 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 = anEditUserProfilePresenter( + matrixClient = matrixClient, + matrixUser = user + ) + + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + + initialState.eventSink(EditUserProfileEvents.HandleAvatarAction(AvatarAction.ChoosePhoto)) + initialState.eventSink(EditUserProfileEvents.Save) + skipItems(2) + + 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 = anEditUserProfilePresenter( + 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 = anEditUserProfilePresenter(matrixUser = user) + + 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 = anEditUserProfilePresenter(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/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..ce518730c4 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] @@ -141,6 +151,21 @@ class FakeMatrixClient( return uploadMediaResult } + override suspend fun setDisplayName(displayName: String): Result { + setDisplayNameCalled = true + return setDisplayNameResult + } + + override suspend fun uploadAvatar(mimeType: String, data: ByteArray): Result { + uploadAvatarCalled = true + return uploadAvatarResult + } + + override suspend fun removeAvatar(): Result { + removeAvatarCalled = true + return removeAvatarResult + } + override fun sessionVerificationService(): SessionVerificationService = sessionVerificationService override fun pushersService(): PushersService = pushersService @@ -197,4 +222,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/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( From 6d7e51c2e0de9a47ce277372238cbaf9cd5b0c8a Mon Sep 17 00:00:00 2001 From: ElementBot Date: Fri, 15 Sep 2023 15:48:51 +0000 Subject: [PATCH 19/27] Update screenshots --- ...nents_null_RoomSummaryRowDark_0_null_2,NEXUS_5,1.0,en].png | 4 ++-- ...ents_null_RoomSummaryRowLight_0_null_2,NEXUS_5,1.0,en].png | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) 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 From f7f9a78101022468026a0c11a0e3361300e659e5 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 15 Sep 2023 18:20:44 +0200 Subject: [PATCH 20/27] Cleanup and compact code. Also prefer usage of DayNightPreview. --- .../editprofile/EditUserProfilePresenter.kt | 6 +- .../user/editprofile/EditUserProfileView.kt | 36 +++---- .../EditUserProfilePresenterTest.kt | 95 +++---------------- .../impl/edit/RoomDetailsEditView.kt | 2 +- .../components/LabelledOutlinedTextField.kt | 17 +--- libraries/matrix/impl/build.gradle.kts | 2 +- 6 files changed, 36 insertions(+), 122 deletions(-) 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 index edbb2467f2..5d3821931e 100644 --- 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 @@ -95,8 +95,8 @@ class EditUserProfilePresenter @AssistedInject constructor( } val canSave = remember(userDisplayName, userAvatarUri) { - val hasProfileChanged = hasDisplayNameChanged(userDisplayName, matrixUser) - || hasAvatarUrlChanged(userAvatarUri, matrixUser) + val hasProfileChanged = hasDisplayNameChanged(userDisplayName, matrixUser) || + hasAvatarUrlChanged(userAvatarUri, matrixUser) !userDisplayName.isNullOrBlank() && hasProfileChanged } @@ -139,6 +139,6 @@ class EditUserProfilePresenter @AssistedInject constructor( } else { matrixClient.removeAvatar().getOrThrow() } - }.onFailure { it.printStackTrace() } + }.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/EditUserProfileView.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/editprofile/EditUserProfileView.kt index 02423470a9..5b921c047c 100644 --- 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 @@ -40,7 +40,6 @@ 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.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp import io.element.android.features.preferences.impl.R @@ -50,8 +49,8 @@ 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.ElementPreviewDark -import io.element.android.libraries.designsystem.preview.ElementPreviewLight +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 @@ -135,7 +134,6 @@ fun EditUserProfileView( ) } Spacer(modifier = Modifier.height(40.dp)) - LabelledOutlinedTextField( label = stringResource(R.string.screen_edit_profile_display_name), value = state.displayName, @@ -155,7 +153,6 @@ fun EditUserProfileView( 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), @@ -163,40 +160,31 @@ fun EditUserProfileView( onDismiss = { state.eventSink(EditUserProfileEvents.CancelSaveChanges) }, ) } - is Async.Success -> { LaunchedEffect(state.saveAction) { onProfileEdited() } } - else -> Unit } } } private fun Modifier.clearFocusOnTap(focusManager: FocusManager): Modifier = - this.pointerInput(Unit) { + pointerInput(Unit) { detectTapGestures(onTap = { focusManager.clearFocus() }) } -@Preview -@Composable -fun EditUserProfileViewLightPreview(@PreviewParameter(EditUserProfileStateProvider::class) state: EditUserProfileState) = - ElementPreviewLight { ContentToPreview(state) } - -@Preview +@DayNightPreviews @Composable -fun EditUserProfileViewDarkPreview(@PreviewParameter(EditUserProfileStateProvider::class) state: EditUserProfileState) = - ElementPreviewDark { ContentToPreview(state) } +internal fun EditUserProfileViewPreview(@PreviewParameter(EditUserProfileStateProvider::class) state: EditUserProfileState) = + ElementPreview { + EditUserProfileView( + onBackPressed = {}, + onProfileEdited = {}, + state = state, + ) + } -@Composable -private fun ContentToPreview(state: EditUserProfileState) { - EditUserProfileView( - onBackPressed = {}, - onProfileEdited = {}, - state = state, - ) -} 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 index c377265689..d3580a82b1 100644 --- 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 @@ -74,7 +74,7 @@ class EditUserProfilePresenterTest { unmockkAll() } - private fun anEditUserProfilePresenter( + private fun createEditUserProfilePresenter( matrixClient: MatrixClient = FakeMatrixClient(), matrixUser: MatrixUser = aMatrixUser(), ): EditUserProfilePresenter { @@ -89,8 +89,7 @@ class EditUserProfilePresenterTest { @Test fun `present - initial state is created from room info`() = runTest { val user = aMatrixUser(avatarUrl = AN_AVATAR_URL) - val presenter = anEditUserProfilePresenter(matrixUser = user) - + val presenter = createEditUserProfilePresenter(matrixUser = user) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -111,27 +110,23 @@ class EditUserProfilePresenterTest { @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 = anEditUserProfilePresenter(matrixUser = user) - + 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") @@ -143,17 +138,13 @@ class EditUserProfilePresenterTest { @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 = anEditUserProfilePresenter(matrixUser = user) - + 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) @@ -164,17 +155,13 @@ class EditUserProfilePresenterTest { @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 = anEditUserProfilePresenter(matrixUser = user) - + 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) @@ -185,35 +172,28 @@ class EditUserProfilePresenterTest { @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 = anEditUserProfilePresenter(matrixUser = user) - + val presenter = createEditUserProfilePresenter(matrixUser = user) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { val initialState = awaitItem() assertThat(initialState.saveButtonEnabled).isEqualTo(false) - // Once a change is made, the save button is enabled initialState.eventSink(EditUserProfileEvents.UpdateDisplayName("Name II")) awaitItem().apply { assertThat(saveButtonEnabled).isEqualTo(true) } - // If it's reverted then the save disables again initialState.eventSink(EditUserProfileEvents.UpdateDisplayName("Name")) awaitItem().apply { assertThat(saveButtonEnabled).isEqualTo(false) } - // Make a change... initialState.eventSink(EditUserProfileEvents.HandleAvatarAction(AvatarAction.Remove)) awaitItem().apply { assertThat(saveButtonEnabled).isEqualTo(true) } - // Revert it... initialState.eventSink(EditUserProfileEvents.HandleAvatarAction(AvatarAction.ChoosePhoto)) awaitItem().apply { @@ -225,35 +205,28 @@ class EditUserProfilePresenterTest { @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 = anEditUserProfilePresenter(matrixUser = user) - + val presenter = createEditUserProfilePresenter(matrixUser = user) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { val initialState = awaitItem() assertThat(initialState.saveButtonEnabled).isEqualTo(false) - // Once a change is made, the save button is enabled initialState.eventSink(EditUserProfileEvents.UpdateDisplayName("Name II")) awaitItem().apply { assertThat(saveButtonEnabled).isEqualTo(true) } - // If it's reverted then the save disables again initialState.eventSink(EditUserProfileEvents.UpdateDisplayName("fallback")) awaitItem().apply { assertThat(saveButtonEnabled).isEqualTo(false) } - // Make a change... initialState.eventSink(EditUserProfileEvents.HandleAvatarAction(AvatarAction.ChoosePhoto)) awaitItem().apply { assertThat(saveButtonEnabled).isEqualTo(true) } - // Revert it... initialState.eventSink(EditUserProfileEvents.HandleAvatarAction(AvatarAction.Remove)) awaitItem().apply { @@ -266,26 +239,21 @@ class EditUserProfilePresenterTest { 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 = anEditUserProfilePresenter( + 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) skipItems(5) - assertThat(matrixClient.setDisplayNameCalled).isTrue() assertThat(matrixClient.removeAvatarCalled).isTrue() assertThat(matrixClient.uploadAvatarCalled).isFalse() - cancelAndIgnoreRemainingEvents() } } @@ -294,24 +262,19 @@ class EditUserProfilePresenterTest { fun `present - save doesn't 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 = anEditUserProfilePresenter( + 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) - assertThat(matrixClient.setDisplayNameCalled).isTrue() assertThat(matrixClient.uploadAvatarCalled).isFalse() assertThat(matrixClient.removeAvatarCalled).isFalse() - cancelAndIgnoreRemainingEvents() } } @@ -320,24 +283,19 @@ class EditUserProfilePresenterTest { fun `present - save doesn't 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 = anEditUserProfilePresenter( + 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() } } @@ -346,23 +304,18 @@ class EditUserProfilePresenterTest { 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 = anEditUserProfilePresenter( + 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) skipItems(2) - assertThat(matrixClient.uploadAvatarCalled).isTrue() } } @@ -371,26 +324,20 @@ class EditUserProfilePresenterTest { 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 = anEditUserProfilePresenter( + 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) } } @@ -401,7 +348,6 @@ class EditUserProfilePresenterTest { val matrixClient = FakeMatrixClient().apply { givenSetDisplayNameResult(Result.failure(Throwable("!"))) } - saveAndAssertFailure(user, matrixClient, EditUserProfileEvents.UpdateDisplayName("New name")) } @@ -411,61 +357,49 @@ class EditUserProfilePresenterTest { 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 = anEditUserProfilePresenter(matrixUser = user) - + val presenter = createEditUserProfilePresenter(matrixUser = user) 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 = anEditUserProfilePresenter(matrixUser = matrixUser, matrixClient = matrixClient) - + 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) } @@ -476,7 +410,6 @@ class EditUserProfilePresenterTest { val processedFile: File = mockk { every { readBytes() } returns fakeFileContents } - fakePickerProvider.givenResult(anotherAvatarUri) fakeMediaPreProcessor.givenResult( Result.success( 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 4553161d73..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 @@ -223,7 +223,7 @@ private fun LabelledReadOnlyField( } private fun Modifier.clearFocusOnTap(focusManager: FocusManager): Modifier = - this.pointerInput(Unit) { + pointerInput(Unit) { detectTapGestures(onTap = { focusManager.clearFocus() }) 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 index a273acf25c..4939cac38d 100644 --- 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 @@ -24,10 +24,9 @@ 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.tooling.preview.Preview import androidx.compose.ui.unit.dp -import io.element.android.libraries.designsystem.preview.ElementPreviewDark -import io.element.android.libraries.designsystem.preview.ElementPreviewLight +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 @@ -66,16 +65,9 @@ fun LabelledOutlinedTextField( } } -@Preview +@DayNightPreviews @Composable -internal fun LabelledOutlinedTextFieldLightPreview() = ElementPreviewLight { ContentToPreview() } - -@Preview -@Composable -internal fun LabelledOutlinedTextFieldDarkPreview() = ElementPreviewDark { ContentToPreview() } - -@Composable -private fun ContentToPreview() { +internal fun LabelledOutlinedTextFieldPreview() = ElementPreview { Column { LabelledOutlinedTextField( label = "Room name", @@ -89,3 +81,4 @@ private fun ContentToPreview() { ) } } + diff --git a/libraries/matrix/impl/build.gradle.kts b/libraries/matrix/impl/build.gradle.kts index 88cc929e79..a2b616f989 100644 --- a/libraries/matrix/impl/build.gradle.kts +++ b/libraries/matrix/impl/build.gradle.kts @@ -29,7 +29,7 @@ anvil { } dependencies { -// implementation(projects.libraries.rustsdk) + // implementation(projects.libraries.rustsdk) implementation(libs.matrix.sdk) implementation(projects.libraries.di) implementation(projects.libraries.androidutils) From 27e567e6f4b63d11ec659a9c1920343e4d60673e Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 15 Sep 2023 19:24:43 +0200 Subject: [PATCH 21/27] Fix the test. --- .../editprofile/EditUserProfilePresenter.kt | 9 +++- .../EditUserProfilePresenterTest.kt | 42 ++++++++++--------- .../libraries/matrix/test/FakeMatrixClient.kt | 7 ++-- 3 files changed, 33 insertions(+), 25 deletions(-) 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 index 5d3821931e..793c4840d7 100644 --- 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 @@ -26,6 +26,7 @@ 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 @@ -111,8 +112,12 @@ class EditUserProfilePresenter @AssistedInject constructor( ) } - private fun hasDisplayNameChanged(name: String?, currentUser: MatrixUser?) = name?.trim() != currentUser?.displayName?.trim() - private fun hasAvatarUrlChanged(avatarUri: Uri?, currentUser: MatrixUser?) = avatarUri?.toString()?.trim() != currentUser?.avatarUrl?.trim() + 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>() 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 index d3580a82b1..beece60c9a 100644 --- 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 @@ -33,6 +33,7 @@ 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 @@ -87,14 +88,14 @@ class EditUserProfilePresenterTest { } @Test - fun `present - initial state is created from room info`() = runTest { + 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.value) + assertThat(initialState.userId).isEqualTo(user.userId) assertThat(initialState.displayName).isEqualTo(user.displayName) assertThat(initialState.userAvatarUrl).isEqualTo(userAvatarUri) assertThat(initialState.avatarActions).containsExactly( @@ -102,7 +103,7 @@ class EditUserProfilePresenterTest { AvatarAction.TakePhoto, AvatarAction.Remove ) - assertThat(initialState.saveButtonEnabled).isEqualTo(false) + assertThat(initialState.saveButtonEnabled).isFalse() assertThat(initialState.saveAction).isInstanceOf(Async.Uninitialized::class.java) } } @@ -178,26 +179,26 @@ class EditUserProfilePresenterTest { presenter.present() }.test { val initialState = awaitItem() - assertThat(initialState.saveButtonEnabled).isEqualTo(false) + assertThat(initialState.saveButtonEnabled).isFalse() // Once a change is made, the save button is enabled initialState.eventSink(EditUserProfileEvents.UpdateDisplayName("Name II")) awaitItem().apply { - assertThat(saveButtonEnabled).isEqualTo(true) + assertThat(saveButtonEnabled).isTrue() } // If it's reverted then the save disables again initialState.eventSink(EditUserProfileEvents.UpdateDisplayName("Name")) awaitItem().apply { - assertThat(saveButtonEnabled).isEqualTo(false) + assertThat(saveButtonEnabled).isFalse() } // Make a change... initialState.eventSink(EditUserProfileEvents.HandleAvatarAction(AvatarAction.Remove)) awaitItem().apply { - assertThat(saveButtonEnabled).isEqualTo(true) + assertThat(saveButtonEnabled).isTrue() } // Revert it... initialState.eventSink(EditUserProfileEvents.HandleAvatarAction(AvatarAction.ChoosePhoto)) awaitItem().apply { - assertThat(saveButtonEnabled).isEqualTo(false) + assertThat(saveButtonEnabled).isFalse() } } } @@ -211,26 +212,26 @@ class EditUserProfilePresenterTest { presenter.present() }.test { val initialState = awaitItem() - assertThat(initialState.saveButtonEnabled).isEqualTo(false) + assertThat(initialState.saveButtonEnabled).isFalse() // Once a change is made, the save button is enabled initialState.eventSink(EditUserProfileEvents.UpdateDisplayName("Name II")) awaitItem().apply { - assertThat(saveButtonEnabled).isEqualTo(true) + assertThat(saveButtonEnabled).isTrue() } // If it's reverted then the save disables again - initialState.eventSink(EditUserProfileEvents.UpdateDisplayName("fallback")) + initialState.eventSink(EditUserProfileEvents.UpdateDisplayName("Name")) awaitItem().apply { - assertThat(saveButtonEnabled).isEqualTo(false) + assertThat(saveButtonEnabled).isFalse() } // Make a change... initialState.eventSink(EditUserProfileEvents.HandleAvatarAction(AvatarAction.ChoosePhoto)) awaitItem().apply { - assertThat(saveButtonEnabled).isEqualTo(true) + assertThat(saveButtonEnabled).isTrue() } // Revert it... initialState.eventSink(EditUserProfileEvents.HandleAvatarAction(AvatarAction.Remove)) awaitItem().apply { - assertThat(saveButtonEnabled).isEqualTo(false) + assertThat(saveButtonEnabled).isFalse() } } } @@ -250,7 +251,7 @@ class EditUserProfilePresenterTest { initialState.eventSink(EditUserProfileEvents.UpdateDisplayName("New name")) initialState.eventSink(EditUserProfileEvents.HandleAvatarAction(AvatarAction.Remove)) initialState.eventSink(EditUserProfileEvents.Save) - skipItems(5) + consumeItemsUntilPredicate { matrixClient.setDisplayNameCalled && matrixClient.removeAvatarCalled && !matrixClient.uploadAvatarCalled } assertThat(matrixClient.setDisplayNameCalled).isTrue() assertThat(matrixClient.removeAvatarCalled).isTrue() assertThat(matrixClient.uploadAvatarCalled).isFalse() @@ -259,7 +260,7 @@ class EditUserProfilePresenterTest { } @Test - fun `present - save doesn't change room details if they're the same trimmed`() = runTest { + 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( @@ -272,7 +273,8 @@ class EditUserProfilePresenterTest { val initialState = awaitItem() initialState.eventSink(EditUserProfileEvents.UpdateDisplayName(" Name ")) initialState.eventSink(EditUserProfileEvents.Save) - assertThat(matrixClient.setDisplayNameCalled).isTrue() + consumeItemsUntilPredicate { matrixClient.setDisplayNameCalled && !matrixClient.removeAvatarCalled && !matrixClient.uploadAvatarCalled } + assertThat(matrixClient.setDisplayNameCalled).isFalse() assertThat(matrixClient.uploadAvatarCalled).isFalse() assertThat(matrixClient.removeAvatarCalled).isFalse() cancelAndIgnoreRemainingEvents() @@ -280,7 +282,7 @@ class EditUserProfilePresenterTest { } @Test - fun `present - save doesn't change name if it's now empty`() = runTest { + 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( @@ -315,7 +317,7 @@ class EditUserProfilePresenterTest { val initialState = awaitItem() initialState.eventSink(EditUserProfileEvents.HandleAvatarAction(AvatarAction.ChoosePhoto)) initialState.eventSink(EditUserProfileEvents.Save) - skipItems(2) + consumeItemsUntilPredicate { matrixClient.uploadAvatarCalled } assertThat(matrixClient.uploadAvatarCalled).isTrue() } } @@ -377,7 +379,7 @@ class EditUserProfilePresenterTest { val matrixClient = FakeMatrixClient().apply { givenSetDisplayNameResult(Result.failure(Throwable("!"))) } - val presenter = createEditUserProfilePresenter(matrixUser = user) + val presenter = createEditUserProfilePresenter(matrixUser = user, matrixClient = matrixClient) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { 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 ce518730c4..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 @@ -143,6 +143,7 @@ class FakeMatrixClient( override suspend fun getAccountManagementUrl(action: AccountManagementAction?): Result { return accountManagementUrlString } + override suspend fun uploadMedia( mimeType: String, data: ByteArray, @@ -151,17 +152,17 @@ class FakeMatrixClient( return uploadMediaResult } - override suspend fun setDisplayName(displayName: String): Result { + override suspend fun setDisplayName(displayName: String): Result = simulateLongTask { setDisplayNameCalled = true return setDisplayNameResult } - override suspend fun uploadAvatar(mimeType: String, data: ByteArray): Result { + override suspend fun uploadAvatar(mimeType: String, data: ByteArray): Result = simulateLongTask { uploadAvatarCalled = true return uploadAvatarResult } - override suspend fun removeAvatar(): Result { + override suspend fun removeAvatar(): Result = simulateLongTask { removeAvatarCalled = true return removeAvatarResult } From 24fb8dac58ceafc3d1317b6b3a79a5cd54e4d846 Mon Sep 17 00:00:00 2001 From: ElementBot Date: Fri, 15 Sep 2023 17:40:19 +0000 Subject: [PATCH 22/27] Update screenshots --- ..._null_EditUserProfileView-D-3_4_null_0,NEXUS_5,1.0,en].png | 3 +++ ..._null_EditUserProfileView-N-3_5_null_0,NEXUS_5,1.0,en].png | 3 +++ ..._null_RoomDetailsEditViewDark_0_null_0,NEXUS_5,1.0,en].png | 4 ++-- ..._null_RoomDetailsEditViewDark_0_null_1,NEXUS_5,1.0,en].png | 4 ++-- ..._null_RoomDetailsEditViewDark_0_null_3,NEXUS_5,1.0,en].png | 4 ++-- ..._null_RoomDetailsEditViewDark_0_null_4,NEXUS_5,1.0,en].png | 4 ++-- ..._null_RoomDetailsEditViewDark_0_null_5,NEXUS_5,1.0,en].png | 4 ++-- ..._null_RoomDetailsEditViewDark_0_null_6,NEXUS_5,1.0,en].png | 4 ++-- ...null_RoomDetailsEditViewLight_0_null_0,NEXUS_5,1.0,en].png | 4 ++-- ...null_RoomDetailsEditViewLight_0_null_1,NEXUS_5,1.0,en].png | 4 ++-- ...null_RoomDetailsEditViewLight_0_null_3,NEXUS_5,1.0,en].png | 4 ++-- ...null_RoomDetailsEditViewLight_0_null_4,NEXUS_5,1.0,en].png | 4 ++-- ...null_RoomDetailsEditViewLight_0_null_5,NEXUS_5,1.0,en].png | 4 ++-- ...null_RoomDetailsEditViewLight_0_null_6,NEXUS_5,1.0,en].png | 4 ++-- ...s.avatar_null_Avatars_Avatar_0_null_42,NEXUS_5,1.0,en].png | 4 ++-- ...s.avatar_null_Avatars_Avatar_0_null_43,NEXUS_5,1.0,en].png | 4 ++-- ...s.avatar_null_Avatars_Avatar_0_null_44,NEXUS_5,1.0,en].png | 4 ++-- ...s.avatar_null_Avatars_Avatar_0_null_45,NEXUS_5,1.0,en].png | 3 +++ ...s.avatar_null_Avatars_Avatar_0_null_46,NEXUS_5,1.0,en].png | 3 +++ ...s.avatar_null_Avatars_Avatar_0_null_47,NEXUS_5,1.0,en].png | 3 +++ ...ull_LabelledOutlinedTextField-D_0_null,NEXUS_5,1.0,en].png | 3 +++ ...ull_LabelledOutlinedTextField-N_1_null,NEXUS_5,1.0,en].png | 3 +++ 22 files changed, 51 insertions(+), 30 deletions(-) create mode 100644 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 create mode 100644 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 create mode 100644 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 create mode 100644 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 create mode 100644 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 create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components_null_LabelledOutlinedTextField-D_0_null,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components_null_LabelledOutlinedTextField-N_1_null,NEXUS_5,1.0,en].png 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[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 From 25c1cfffcab5f7522c2415b72ccc91979846d813 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 15 Sep 2023 22:34:32 +0000 Subject: [PATCH 23/27] Update dependency com.google.firebase:firebase-bom to v32.3.1 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index b164dfbb95..69d3988f06 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" } From 519464fe5de805e6064468e685e1bb777200bce4 Mon Sep 17 00:00:00 2001 From: jonnyandrew Date: Sat, 16 Sep 2023 08:40:56 +0100 Subject: [PATCH 24/27] [Rich text editor] Ensure keyboard opens for reply and text formatting modes (#1337) --- changelog.d/1337.bugfix | 1 + .../android/libraries/androidutils/ui/View.kt | 24 ++++++++ .../textcomposer/SoftKeyboardEffect.kt | 55 +++++++++++++++++++ .../libraries/textcomposer/TextComposer.kt | 17 ++---- 4 files changed, 84 insertions(+), 13 deletions(-) create mode 100644 changelog.d/1337.bugfix create mode 100644 libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/SoftKeyboardEffect.kt 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/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/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 df51d5d1ef..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 From d2f9b02bbf0eaf21b2a9c5b4c534930be30f9587 Mon Sep 17 00:00:00 2001 From: bmarty Date: Mon, 18 Sep 2023 00:09:19 +0000 Subject: [PATCH 25/27] Sync Strings from Localazy --- .../src/main/res/values-de/translations.xml | 6 + .../src/main/res/values-de/translations.xml | 6 + .../src/main/res/values-sk/translations.xml | 6 + .../src/main/res/values-de/translations.xml | 11 + .../src/main/res/values-de/translations.xml | 10 +- .../src/main/res/values-de/translations.xml | 9 + .../src/main/res/values-de/translations.xml | 39 ++++ .../src/main/res/values-de/translations.xml | 8 + .../src/main/res/values-de/translations.xml | 37 ++++ .../src/main/res/values-de/translations.xml | 10 + .../src/main/res/values-de/translations.xml | 3 +- .../src/main/res/values-de/translations.xml | 5 + .../src/main/res/values-de/translations.xml | 15 ++ .../src/main/res/values-de/translations.xml | 46 +++- .../src/main/res/values-de/translations.xml | 9 + .../src/main/res/values-de/translations.xml | 15 ++ .../src/main/res/values-de/translations.xml | 4 + .../src/main/res/values-de/translations.xml | 54 ++++- .../src/main/res/values-de/translations.xml | 45 ++++ .../src/main/res/values-de/translations.xml | 4 + .../src/main/res/values-cs/translations.xml | 4 +- .../src/main/res/values-de/translations.xml | 206 +++++++++++++++++- .../src/main/res/values-fr/translations.xml | 8 + .../src/main/res/values-ru/translations.xml | 4 +- .../src/main/res/values-sk/translations.xml | 17 ++ .../src/main/res/values/localazy.xml | 1 + 26 files changed, 571 insertions(+), 11 deletions(-) create mode 100644 features/call/src/main/res/values-de/translations.xml create mode 100644 features/call/src/main/res/values-sk/translations.xml create mode 100644 features/invitelist/impl/src/main/res/values-de/translations.xml create mode 100644 features/logout/api/src/main/res/values-de/translations.xml create mode 100644 features/onboarding/impl/src/main/res/values-de/translations.xml create mode 100644 features/rageshake/api/src/main/res/values-de/translations.xml create mode 100644 features/rageshake/impl/src/main/res/values-de/translations.xml create mode 100644 features/roomlist/impl/src/main/res/values-de/translations.xml create mode 100644 libraries/androidutils/src/main/res/values-de/translations.xml create mode 100644 libraries/textcomposer/impl/src/main/res/values-de/translations.xml 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/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/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/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/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/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/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/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/textcomposer/impl/src/main/res/values-de/translations.xml b/libraries/textcomposer/impl/src/main/res/values-de/translations.xml new file mode 100644 index 0000000000..6b75a1c9a7 --- /dev/null +++ b/libraries/textcomposer/impl/src/main/res/values-de/translations.xml @@ -0,0 +1,4 @@ + + + "Anhang hinzufügen" + diff --git a/libraries/ui-strings/src/main/res/values-cs/translations.xml b/libraries/ui-strings/src/main/res/values-cs/translations.xml index 271b614dc4..0ff43bceb7 100644 --- a/libraries/ui-strings/src/main/res/values-cs/translations.xml +++ b/libraries/ui-strings/src/main/res/values-cs/translations.xml @@ -195,9 +195,9 @@ "Další nastavení" "Halsové a video hovory" "Neshoda konfigurace" - "Zjednodušili jsme nastavení oznámení, abychom usnadnili hledání možností. + "Zjednodušili jsme nastavení oznámení, abychom usnadnili hledání možností. -Některá vlastní nastavení, která jste si vybrali v minulosti, se zde nezobrazují, ale jsou stále aktivní. +Některá vlastní nastavení, která jste si vybrali v minulosti, se zde nezobrazují, ale jsou stále aktivní. Pokud budete pokračovat, některá nastavení se mohou změnit." "Přímé zprávy" diff --git a/libraries/ui-strings/src/main/res/values-de/translations.xml b/libraries/ui-strings/src/main/res/values-de/translations.xml index 4028ece8ed..d1af3b9cd1 100644 --- a/libraries/ui-strings/src/main/res/values-de/translations.xml +++ b/libraries/ui-strings/src/main/res/values-de/translations.xml @@ -1,65 +1,265 @@ + "Passwort verbergen" "Nur Erwähnungen" "Stummgeschaltet" + "Umfrage" + "Umfrage beendet" + "Dateien senden" + "Passwort anzeigen" + "Benutzermenü" + "Akzeptieren" + "Zurück" "Abbrechen" + "Foto auswählen" + "Löschen" + "Schließen" + "Verifizierung abschließen" "Bestätigen" "Weiter" "Kopieren" + "Link kopieren" + "Link zur Nachricht kopieren" + "Erstellen" "Raum erstellen" + "Ablehnen" + "Deaktivieren" "Erledigt" "Bearbeiten" "Aktivieren" "Umfrage beenden" + "Passwort vergessen?" + "Weiter" "Einladen" + "Freunde einladen" + "Freunde einladen %1$s" + "Laden Sie Personen in %1$s ein" + "Einladungen" "Mehr erfahren" "Verlassen" "Raum verlassen" + "Konto verwalten" + "Geräte verwalten" "Weiter" + "Nein" "Nicht jetzt" "OK" + "Öffnen mit" "Schnelle Antwort" "Zitat" + "Reagieren" "Entfernen" - "Antwort" + "Antworten" + "Im Thread antworten" "Fehler melden" "Inhalt melden" "Erneut versuchen" "Entschlüsselung wiederholen" + "Speichern" + "Suchen" "Senden" + "Nachricht senden" + "Teilen" + "Link teilen" + "Überspringen" "Start" "Chat starten" - "Überprüfung starten" + "Verifizierung starten" + "Tippen Sie, um die Karte zu laden" + "Foto machen" "Quelle anzeigen" + "Ja" + "Über" + "Nutzungsrichtlinie" + "Erweiterte Einstellungen" + "Analysedaten" + "Audio" + "Blasen" + "Copyright" + "Raum wird erstellt…" + "Raum verlassen" "Dekodierungsfehler" + "Entwickleroptionen" "(bearbeitet)" "Bearbeitung" + "* %1$s %2$s" "Verschlüsselung aktiviert" "Fehler" "Datei" + "Datei wurde unter Downloads gespeichert" + "Nachricht weiterleiten" "GIF" "Bild" + "Als Antwort auf %1$s" + "Diese Matrix-ID kann nicht gefunden werden, daher wird die Einladung möglicherweise nicht empfangen." + "Raum verlassen" "Link in die Zwischenablage kopiert" "Laden…" "Nachricht" + "Nachrichtenlayout" "Nachricht entfernt" + "Modern" + "Stummschalten" + "Keine Ergebnisse" + "Offline" "Passwort" - "Menschen" + "Personen" "Permalink" + "Stimmen insgesamt: %1$s" + "Die Ergebnisse werden nach Ende der Umfrage angezeigt" + "Datenschutz­erklärung" + "Reaktion" "Reaktionen" + "Wird erneuert…" + "%1$s antworten" + "Einen Fehler melden" + "Bericht eingereicht" + "Rich-Text-Editor" + "Raumname" + "z.B. Ihr Projektname" + "Nach jemandem suchen" + "Suchergebnisse" + "Sicherheit" + "Wählen Sie Ihren Server aus" + "Wird gesendet…" + "Server wird nicht unterstützt" + "Server-URL" "Einstellungen" + "Geteilter Standort" + "Chat wird gestartet…" "Sticker" + "Erfolg" "Vorschläge" + "Synchronisieren" "Text" + "Hinweise von Drittanbietern" + "Thread" "Thema" + "Worum geht es in diesem Raum?" + "Entschlüsselung nicht möglich" + "Einladungen konnten nicht an einen oder mehrere Benutzer gesendet werden." + "Einladung(en) konnte(n) nicht gesendet werden" + "Stummschaltung aufheben" "Nicht unterstütztes Ereignis" "Benutzername" + "Verifizierung abgebrochen" + "Verifizierung abgeschlossen" "Video" "Warten…" + "Bestätigung" + "Warnung" + "Aktivitäten" + "Flaggen" + "Essen & Trinken" + "Tiere & Natur" + "Objekte" + "Smileys & Menschen" + "Reisen & Orte" + "Symbole" + "Fehler beim Erstellen des Permalinks" + "%1$s konnte die Karte nicht laden. Bitte versuchen Sie es später erneut." + "Fehler beim Laden der Nachrichten" + "%1$s konnte nicht auf Ihren Standort zugreifen. Bitte versuchen Sie es später erneut." + "%1$s hat keine Erlaubnis, auf Ihren Standort zuzugreifen. Sie können den Zugriff in den Einstellungen aktivieren." + "%1$s hat keine Erlaubnis, auf Ihren Standort zuzugreifen. Aktivieren Sie unten den Zugriff." + "Einige Nachrichten wurden nicht gesendet" + "Entschuldigung, es ist ein Fehler aufgetreten" + "🔐️ Begleite mich auf %1$s" + "Hey, sprechen Sie mit mir auf %1$s: %2$s" + "Sind Sie sicher, dass Sie diesen Raum verlassen möchten? Sie sind die einzige Person hier. Wenn Sie austreten, kann in Zukunft niemand mehr eintreten, auch Sie nicht." + "Sind Sie sicher, dass Sie diesen Raum verlassen möchten? Dieser Raum ist nicht öffentlich und Sie können ihm ohne Einladung nicht erneut beitreten." + "Sind Sie sicher, dass Sie den Raum verlassen wollen?" + "%1$s Android" + + "%1$d Mitglied" + "%1$d Mitglieder" + + + "%d Stimme" + "%d Stimmen" + + "Schütteln Sie heftig zum Melden von Fehlern" + "Sie scheinen das Telefon aus Frustration zu schütteln. Möchten Sie den Bildschirm für den Fehlerbericht öffnen?" + "Diese Meldung wird an den Administrator Ihres Homeservers weitergeleitet. Dieser kann keine verschlüsselten Nachrichten lesen." + "Grund für die Meldung dieses Inhalts" + "Aufzählungsliste umschalten" "Formatierungsoptionen schließen" + "Codeblock umschalten" + "Nachricht…" "Einen Link erstellen" "Link bearbeiten" + "Fettes Format anwenden" + "Kursives Format anwenden" + "Durchgestrichenes Format anwenden" + "Unterstreichungsformat anwenden" + "Vollbildmodus umschalten" + "Einrückung" + "Inline-Codeformat anwenden" + "Link setzen" + "Nummerierte Liste umschalten" + "Optionen zum Verfassen öffnen" + "Vorschlag umschalten" + "Link entfernen" + "Ohne Einrückung" "Link" + "Dies ist der Anfang von %1$s." + "Dies ist der Anfang dieses Gesprächs." + "Neu" + "Analysedaten teilen" + "Anzeigename" + "Ihr Anzeigename" + "Ein unbekannter Fehler ist aufgetreten und die Informationen konnten nicht geändert werden." + "Profil kann nicht aktualisiert werden" + "Profil bearbeiten" + "Profil wird aktualisiert…" + "Medienauswahl fehlgeschlagen, bitte versuchen Sie es erneut." + "Fehler beim Verarbeiten des hochgeladenen Mediums. Bitte versuchen Sie es erneut." + "Das Hochladen der Medien ist fehlgeschlagen. Bitte versuchen Sie es erneut." + "Zusätzliche Einstellungen" + "Audio- und Videoanrufe" + "Konfiguration stimmt nicht überein" + "Wir haben die Einstellungen für Benachrichtigungen vereinfacht, damit die Optionen leichter zu finden sind. + +Einige benutzerdefinierte Einstellungen, die Sie in der Vergangenheit gewählt haben, werden hier nicht angezeigt, sind aber immer noch aktiv. + +Wenn Sie fortfahren, können sich einige Ihrer Einstellungen ändern." + "Direkte Chats" + "Benutzerdefinierte Einstellung pro Chat" + "Beim Aktualisieren der Benachrichtigungseinstellungen ist ein Fehler aufgetreten." + "Alle Nachrichten" + "Nur Erwähnungen und Schlüsselwörter" + "Bei direkten Chats, benachrichtigen Sie mich bei" + "Bei Gruppenchats benachrichtigen Sie mich bei" + "Benachrichtigungen auf diesem Gerät aktivieren" + "Die Konfiguration wurde nicht korrigiert, bitte versuchen Sie es erneut." + "Gruppenchats" + "Erwähnungen" + "Alle" + "Erwähnungen" + "Benachrichtige mich bei" + "Benachrichtige mich bei @room" + "Um Benachrichtigungen zu erhalten, ändern Sie bitte Ihre %1$s." + "Systemeinstellungen" + "Systembenachrichtigungen deaktiviert" + "Benachrichtigungen" + "Prüfen Sie, ob Sie alle aktuellen und zukünftigen Nachrichten dieses Benutzers ausblenden möchten" + "Konto und Geräte" + "Standort teilen" + "Meinen Standort teilen" + "In Apple Maps öffnen" + "In Google Maps öffnen" + "In OpenStreetMap öffnen" + "Diesen Standort teilen" + "Standort" + "Rageshake" + "Erkennungsschwelle" + "Allgemein" + "Version: %1$s (%2$s)" + "en" "Fehler" + "Erfolg" + "Teilen Sie anonyme Nutzungsdaten, um uns bei der Identifizierung von Problemen zu helfen." + "Sie können alle unsere Bedingungen lesen%1$s." "hier" + "Benutzer sperren" diff --git a/libraries/ui-strings/src/main/res/values-fr/translations.xml b/libraries/ui-strings/src/main/res/values-fr/translations.xml index 4978cdb68c..f6dc4f5ac0 100644 --- a/libraries/ui-strings/src/main/res/values-fr/translations.xml +++ b/libraries/ui-strings/src/main/res/values-fr/translations.xml @@ -3,6 +3,8 @@ "Masquer le mot de passe" "Mentions uniquement" "En sourdine" + "Sondage" + "Sondage terminé" "Envoyer des fichiers" "Afficher le mot de passe" "Menu utilisateur" @@ -204,6 +206,12 @@ "Ceci est le début de cette conversation." "Nouveau" "Partagez des données de statistiques d\'utilisation" + "Nom d\'affichage" + "Votre nom d\'affichage" + "Une erreur inconnue s\'est produite et les informations n\'ont pas pu être modifiées." + "Impossible de mettre à jour le profil" + "Modifier le profil" + "Mise à jour du profil…" "Échec de la sélection du média, veuillez réessayer." "Échec du traitement des médias à télécharger, veuillez réessayer." "Échec du téléchargement du média, veuillez réessayer." diff --git a/libraries/ui-strings/src/main/res/values-ru/translations.xml b/libraries/ui-strings/src/main/res/values-ru/translations.xml index d37e28dc7a..798515fa6a 100644 --- a/libraries/ui-strings/src/main/res/values-ru/translations.xml +++ b/libraries/ui-strings/src/main/res/values-ru/translations.xml @@ -194,9 +194,9 @@ "Дополнительные параметры" "Аудио и видео звонки" "Несоответствие конфигурации" - "Мы упростили настройки уведомлений, чтобы упростить поиск опций. + "Мы упростили настройки уведомлений, чтобы упростить поиск опций. -Некоторые пользовательские настройки, выбранные вами ранее, не отображаются в данном меню, но они все еще активны. +Некоторые пользовательские настройки, выбранные вами ранее, не отображаются в данном меню, но они все еще активны. Если вы продолжите, некоторые настройки могут быть изменены." "Прямые чаты" diff --git a/libraries/ui-strings/src/main/res/values-sk/translations.xml b/libraries/ui-strings/src/main/res/values-sk/translations.xml index 162ca8f9d8..53fc0a1045 100644 --- a/libraries/ui-strings/src/main/res/values-sk/translations.xml +++ b/libraries/ui-strings/src/main/res/values-sk/translations.xml @@ -3,6 +3,8 @@ "Skryť heslo" "Iba zmienky" "Stlmené" + "Anketa" + "Ukončená anketa" "Odoslať súbory" "Zobraziť heslo" "Používateľské menu" @@ -36,6 +38,8 @@ "Zistiť viac" "Opustiť" "Opustiť miestnosť" + "Spravovať účet" + "Spravovať zariadenia" "Ďalej" "Nie" "Teraz nie" @@ -46,6 +50,7 @@ "Reagovať" "Odstrániť" "Odpovedať" + "Odpovedať vo vlákne" "Nahlásiť chybu" "Nahlásiť obsah" "Skúsiť znova" @@ -66,6 +71,7 @@ "Áno" "O aplikácii" "Zásady prijateľného používania" + "Pokročilé nastavenia" "Analytika" "Zvuk" "Bubliny" @@ -84,6 +90,7 @@ "Preposlať správu" "GIF" "Obrázok" + "V odpovedi na %1$s" "Toto Matrix ID sa nedá nájsť, takže pozvánka nemusí byť prijatá." "Opustenie miestnosti" "Odkaz bol skopírovaný do schránky" @@ -101,11 +108,13 @@ "Celkový počet hlasov: %1$s" "Výsledky sa zobrazia po ukončení ankety" "Zásady ochrany osobných údajov" + "Reakcia" "Reakcie" "Obnovuje sa…" "Odpoveď na %1$s" "Nahlásiť chybu" "Nahlásenie bolo odoslané" + "Rozšírený textový editor" "Názov miestnosti" "napr. názov vášho projektu" "Vyhľadať niekoho" @@ -124,6 +133,7 @@ "Synchronizuje sa" "Text" "Oznámenia tretích strán" + "Vlákno" "Téma" "O čom je táto miestnosť?" "Nie je možné dešifrovať" @@ -191,12 +201,19 @@ "Prepnúť číslovaný zoznam" "Otvoriť možnosti písania" "Prepnúť citáciu" + "Odstrániť odkaz" "Zrušiť odsadenie" "Odkaz" "Toto je začiatok %1$s." "Toto je začiatok tejto konverzácie." "Nové" "Zdieľať analytické údaje" + "Zobrazované meno" + "Vaše zobrazované meno" + "Vyskytla sa neznáma chyba a informácie nebolo možné zmeniť." + "Nepodarilo sa aktualizovať profil" + "Upraviť profil" + "Aktualizácia profilu…" "Nepodarilo sa vybrať médium, skúste to prosím znova." "Nepodarilo sa spracovať médiá na odoslanie, skúste to prosím znova." "Nepodarilo sa nahrať médiá, skúste to prosím znova." diff --git a/libraries/ui-strings/src/main/res/values/localazy.xml b/libraries/ui-strings/src/main/res/values/localazy.xml index 7c1df85553..6509f3787e 100644 --- a/libraries/ui-strings/src/main/res/values/localazy.xml +++ b/libraries/ui-strings/src/main/res/values/localazy.xml @@ -210,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." From bc68d47d1f3ccec82eec59ed43c692d92465dbae Mon Sep 17 00:00:00 2001 From: ganfra Date: Mon, 18 Sep 2023 10:29:52 +0200 Subject: [PATCH 26/27] Update rust-sdk to 0.1.54 --- gradle/libs.versions.toml | 2 +- .../libraries/matrix/impl/roomlist/RoomSummaryListProcessor.kt | 3 +++ .../matrix/impl/timeline/MatrixTimelineDiffProcessor.kt | 3 +++ 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index b164dfbb95..98e08f79f4 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -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/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 4bade9cc81..1ab5b93e69 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 @@ -99,6 +99,9 @@ class RoomSummaryListProcessor( RoomListEntriesUpdate.Clear -> { clear() } + is RoomListEntriesUpdate.Truncate -> { + subList(update.length.toInt(), size).clear() + } } } 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 + } } } From e73afad96cbc9416c30599896454e3185f0f6320 Mon Sep 17 00:00:00 2001 From: ganfra Date: Mon, 18 Sep 2023 10:56:29 +0200 Subject: [PATCH 27/27] RoomSummary: RoomListEntry.Invalidated should not build empty room summary --- .../libraries/matrix/impl/roomlist/RoomSummaryListProcessor.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 1ab5b93e69..321a17d809 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 @@ -110,7 +110,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) } } }