fix linkify

<!-- Please read [CONTRIBUTING.md](https://github.com/element-hq/element-x-android/blob/develop/CONTRIBUTING.md) before submitting your pull request -->

## Type of change

- [ ] Feature
- [ ] Bugfix
- [ ] Technical
- [ ] Other :

## Content

<!-- Describe shortly what has been changed -->
Ensure message with body and without formatted body got working links.
Also ensure email addresses are linkified.

## Motivation and context

<!-- Provide link to the corresponding issue if applicable or explain the context -->
Closes #2038

## Screenshots / GIFs

<!--
We have screenshot tests in the project, so attaching screenshots to a PR is not mandatory, as far as there
is a Composable Preview covering the changes. In this case, the change will appear in the file diff.
Note that all the UI composables should be covered by a Composable Preview.

Providing a video of the change is still very useful for the reviewer and for the history of the project.

You can use a table like this to show screenshots comparison.
Uncomment this markdown table below and edit the last line `|||`:
|copy screenshot of before here|copy screenshot of after here|

|Before|After|
|-|-|
|||
 -->

## Tests

<!-- Explain how you tested your development -->

- Send a message with just a raw link and nothing else
- Observe that with this change the link in the message is rendered as a link and is clickable

## Tested devices

- [ ] Physical
- [ ] Emulator
- OS version(s):

## Checklist

<!-- Depending on the Pull Request content, it can be acceptable if some of the following checkboxes stay unchecked. -->

- [ ] Changes have been tested on an Android device or Android emulator with API 23
- [ ] UI change has been tested on both light and dark themes
- [ ] Accessibility has been taken into account. See https://github.com/element-hq/element-x-android/blob/develop/CONTRIBUTING.md#accessibility
- [ ] Pull request is based on the develop branch
- [ ] Pull request includes a new file under ./changelog.d. See https://github.com/element-hq/element-x-android/blob/develop/CONTRIBUTING.md#changelog
- [ ] Pull request includes screenshots or videos if containing UI changes
- [ ] Pull request includes a [sign off](https://matrix-org.github.io/synapse/latest/development/contributing_guide.html#sign-off)
- [ ] You've made a self review of your PR

Messages are not linkified anymore

Messages are not linkified anymore
- send a message containing a link
- observe that the link is not rendered correctly and not clickable

<img width="345" alt="image" src="https://github.com/element-hq/element-x-android/assets/3940906/20ed590a-9f25-45f9-9b26-fde10176a145">

diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentMessageFactory.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentMessageFactory.kt
index 77e514fa608..69c453bb44d 100644
--- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentMessageFactory.kt
+++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentMessageFactory.kt
@@ -21,6 +21,7 @@ import android.text.style.URLSpan
 import android.text.util.Linkify
 import androidx.core.text.buildSpannedString
 import androidx.core.text.getSpans
+import androidx.core.text.toSpannable
 import androidx.core.text.util.LinkifyCompat
 import io.element.android.features.location.api.Location
 import io.element.android.features.messages.api.timeline.HtmlConverterProvider
@@ -68,12 +69,15 @@ class TimelineItemContentMessageFactory @Inject constructor(

     suspend fun create(content: MessageContent, senderDisplayName: String, eventId: EventId?): TimelineItemEventContent {
         return when (val messageType = content.type) {
-            is EmoteMessageType -> TimelineItemEmoteContent(
-                body = "* $senderDisplayName ${messageType.body}",
-                htmlDocument = messageType.formatted?.toHtmlDocument(prefix = "* $senderDisplayName"),
-                formattedBody = parseHtml(messageType.formatted, prefix = "* $senderDisplayName"),
-                isEdited = content.isEdited,
-            )
+            is EmoteMessageType -> {
+                val emoteBody = "* $senderDisplayName ${messageType.body}"
+                TimelineItemEmoteContent(
+                    body = emoteBody,
+                    htmlDocument = messageType.formatted?.toHtmlDocument(prefix = "* $senderDisplayName"),
+                    formattedBody = parseHtml(messageType.formatted, prefix = "* $senderDisplayName") ?: emoteBody.withLinks(),
+                    isEdited = content.isEdited,
+                )
+            }
             is ImageMessageType -> {
                 val aspectRatio = aspectRatioOf(messageType.info?.width, messageType.info?.height)
                 TimelineItemImageContent(
@@ -171,21 +175,21 @@ class TimelineItemContentMessageFactory @Inject constructor(
             is NoticeMessageType -> TimelineItemNoticeContent(
                 body = messageType.body,
                 htmlDocument = messageType.formatted?.toHtmlDocument(),
-                formattedBody = parseHtml(messageType.formatted),
+                formattedBody = parseHtml(messageType.formatted) ?: messageType.body.withLinks(),
                 isEdited = content.isEdited,
             )
             is TextMessageType -> {
                 TimelineItemTextContent(
                     body = messageType.body,
                     htmlDocument = messageType.formatted?.toHtmlDocument(),
-                    formattedBody = parseHtml(messageType.formatted),
+                    formattedBody = parseHtml(messageType.formatted) ?: messageType.body.withLinks(),
                     isEdited = content.isEdited,
                 )
             }
             is OtherMessageType -> TimelineItemTextContent(
                 body = messageType.body,
                 htmlDocument = null,
-                formattedBody = null,
+                formattedBody = messageType.body.withLinks(),
                 isEdited = content.isEdited,
             )
         }
@@ -226,7 +230,7 @@ class TimelineItemContentMessageFactory @Inject constructor(
             Pair(start, end)
         }
         // Find and set as URLSpans any links present in the text
-        LinkifyCompat.addLinks(this, Linkify.WEB_URLS or Linkify.PHONE_NUMBERS)
+        LinkifyCompat.addLinks(this, Linkify.WEB_URLS or Linkify.PHONE_NUMBERS or Linkify.EMAIL_ADDRESSES)
         // Restore old spans if they don't conflict with the new ones
         for ((urlSpan, location) in oldURLSpans) {
             val (start, end) = location
@@ -237,3 +241,11 @@ class TimelineItemContentMessageFactory @Inject constructor(
         return this
     }
 }
+
+@Suppress("USELESS_ELVIS")
+private fun String.withLinks(): CharSequence? {
+    /* Note: toSpannable() can return null when running unit tests */
+    val spannable = toSpannable() ?: return null
+    val addedLinks = LinkifyCompat.addLinks(spannable, Linkify.WEB_URLS or Linkify.PHONE_NUMBERS or Linkify.EMAIL_ADDRESSES)
+    return spannable.takeIf { addedLinks }
+}
diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/ClickableLinkText.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/ClickableLinkText.kt
index 105757b431c..701d2945baf 100644
--- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/ClickableLinkText.kt
+++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/ClickableLinkText.kt
@@ -142,7 +142,7 @@ fun ClickableLinkText(
 fun AnnotatedString.linkify(linkStyle: SpanStyle): AnnotatedString {
     val original = this
     val spannable = SpannableString(this.text)
-    LinkifyCompat.addLinks(spannable, Linkify.WEB_URLS or Linkify.PHONE_NUMBERS)
+    LinkifyCompat.addLinks(spannable, Linkify.WEB_URLS or Linkify.PHONE_NUMBERS or Linkify.EMAIL_ADDRESSES)

     val spans = spannable.getSpans(0, spannable.length, URLSpan::class.java)
     return buildAnnotatedString {

diff --git a/appnav/src/test/kotlin/io/element/android/appnav/loggedin/LoggedInPresenterTest.kt b/appnav/src/test/kotlin/io/element/android/appnav/loggedin/LoggedInPresenterTest.kt
index fcfba8f7a1b..d7b951fbbc4 100644
--- a/appnav/src/test/kotlin/io/element/android/appnav/loggedin/LoggedInPresenterTest.kt
+++ b/appnav/src/test/kotlin/io/element/android/appnav/loggedin/LoggedInPresenterTest.kt
@@ -60,8 +60,9 @@ class LoggedInPresenterTest {
         }.test {
             val initialState = awaitItem()
             assertThat(initialState.showSyncSpinner).isFalse()
+            roomListService.postSyncIndicator(RoomListService.SyncIndicator.Show)
             consumeItemsUntilPredicate { it.showSyncSpinner }
-            roomListService.postState(RoomListService.State.Running)
+            roomListService.postSyncIndicator(RoomListService.SyncIndicator.Hide)
             consumeItemsUntilPredicate { !it.showSyncSpinner }
         }
     }
diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentMessageFactoryTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentMessageFactoryTest.kt
index 5945ccbee2f..0fe79b89635 100644
--- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentMessageFactoryTest.kt
+++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentMessageFactoryTest.kt
@@ -17,8 +17,9 @@
 package io.element.android.features.messages.impl.timeline.factories.event

 import android.text.SpannableString
-import android.text.SpannableStringBuilder
+import android.text.Spanned
 import android.text.style.URLSpan
+import androidx.core.text.buildSpannedString
 import androidx.core.text.inSpans
 import com.google.common.truth.Truth.assertThat
 import io.element.android.features.location.api.Location
@@ -144,9 +145,40 @@ class TimelineItemContentMessageFactoryTest {
         assertThat(result).isEqualTo(expected)
     }

+    @Test
+    fun `test create TextMessageType with simple link`() = runTest {
+        val sut = createTimelineItemContentMessageFactory()
+        val result = sut.create(
+            content = createMessageContent(type = TextMessageType("https://www.example.org", null)),
+            senderDisplayName = "Bob",
+            eventId = AN_EVENT_ID,
+        ) as TimelineItemTextContent
+        val expected = TimelineItemTextContent(
+            body = "https://www.example.org",
+            htmlDocument = null,
+            plainText = "https://www.example.org",
+            isEdited = false,
+            formattedBody = buildSpannedString {
+                inSpans(URLSpan("https://www.example.org")) {
+                    append("https://www.example.org")
+                }
+            }
+        )
+        assertThat(result.body).isEqualTo(expected.body)
+        assertThat(result.htmlDocument).isEqualTo(expected.htmlDocument)
+        assertThat(result.plainText).isEqualTo(expected.plainText)
+        assertThat(result.isEdited).isEqualTo(expected.isEdited)
+        assertThat(result.formattedBody).isInstanceOf(Spanned::class.java)
+        val spanned = result.formattedBody as Spanned
+        assertThat(spanned.toString()).isEqualTo("https://www.example.org")
+        val urlSpans = spanned.getSpans(0, spanned.length, URLSpan::class.java)
+        assertThat(urlSpans).hasLength(1)
+        assertThat(urlSpans[0].url).isEqualTo("https://www.example.org")
+    }
+
     @Test
     fun `test create TextMessageType with HTML formatted body`() = runTest {
-        val expected = SpannableStringBuilder().apply {
+        val expected = buildSpannedString {
             append("link to ")
             inSpans(URLSpan("https://matrix.org")) {
                 append("https://matrix.org")
@@ -160,10 +192,12 @@ class TimelineItemContentMessageFactoryTest {
             htmlConverterTransform = { expected }
         )
         val result = sut.create(
-            content = createMessageContent(type = TextMessageType(
-                body = "body",
-                formatted = FormattedBody(MessageFormat.HTML, expected.toString())
-            )),
+            content = createMessageContent(
+                type = TextMessageType(
+                    body = "body",
+                    formatted = FormattedBody(MessageFormat.HTML, expected.toString())
+                )
+            ),
             senderDisplayName = "Bob",
             eventId = AN_EVENT_ID,
         )
@@ -176,10 +210,12 @@ class TimelineItemContentMessageFactoryTest {
             htmlConverterTransform = { it }
         )
         val result = sut.create(
-            content = createMessageContent(type = TextMessageType(
-                body = "body",
-                formatted = FormattedBody(MessageFormat.UNKNOWN, "formatted")
-            )),
+            content = createMessageContent(
+                type = TextMessageType(
+                    body = "body",
+                    formatted = FormattedBody(MessageFormat.UNKNOWN, "formatted")
+                )
+            ),
             senderDisplayName = "Bob",
             eventId = AN_EVENT_ID,
         )
@@ -520,10 +556,12 @@ class TimelineItemContentMessageFactoryTest {
     fun `test create NoticeMessageType with HTML formatted body`() = runTest {
         val sut = createTimelineItemContentMessageFactory()
         val result = sut.create(
-            content = createMessageContent(type = NoticeMessageType(
-                body = "body",
-                formatted = FormattedBody(MessageFormat.HTML, "formatted")
-            )),
+            content = createMessageContent(
+                type = NoticeMessageType(
+                    body = "body",
+                    formatted = FormattedBody(MessageFormat.HTML, "formatted")
+                )
+            ),
             senderDisplayName = "Bob",
             eventId = AN_EVENT_ID,
         )
@@ -552,10 +590,12 @@ class TimelineItemContentMessageFactoryTest {
     fun `test create EmoteMessageType with HTML formatted body`() = runTest {
         val sut = createTimelineItemContentMessageFactory()
         val result = sut.create(
-            content = createMessageContent(type = EmoteMessageType(
-                body = "body",
-                formatted = FormattedBody(MessageFormat.HTML, "formatted")
-            )),
+            content = createMessageContent(
+                type = EmoteMessageType(
+                    body = "body",
+                    formatted = FormattedBody(MessageFormat.HTML, "formatted")
+                )
+            ),
             senderDisplayName = "Bob",
             eventId = AN_EVENT_ID,
         )
diff --git a/features/poll/impl/src/test/kotlin/io/element/android/features/poll/impl/history/PollHistoryPresenterTest.kt b/features/poll/impl/src/test/kotlin/io/element/android/features/poll/impl/history/PollHistoryPresenterTest.kt
index 172ec5fa28f..39494e1f349 100644
--- a/features/poll/impl/src/test/kotlin/io/element/android/features/poll/impl/history/PollHistoryPresenterTest.kt
+++ b/features/poll/impl/src/test/kotlin/io/element/android/features/poll/impl/history/PollHistoryPresenterTest.kt
@@ -52,14 +52,15 @@ class PollHistoryPresenterTest {
     @get:Rule
     val warmUpRule = WarmUpRule()

-    private val room = FakeMatrixRoom(
-        matrixTimeline = aPollTimeline(
-            polls = mapOf(
-                AN_EVENT_ID to anOngoingPollContent(),
-                AN_EVENT_ID_2 to anEndedPollContent()
-            )
+    private val timeline = aPollTimeline(
+        polls = mapOf(
+            AN_EVENT_ID to anOngoingPollContent(),
+            AN_EVENT_ID_2 to anEndedPollContent()
         )
     )
+    private val room = FakeMatrixRoom(
+        matrixTimeline = timeline
+    )

     @Test
     fun `present - initial states`() = runTest {
@@ -134,10 +135,14 @@ class PollHistoryPresenterTest {
             presenter.present()
         }.test {
             consumeItemsUntilPredicate {
-                it.pollHistoryItems.size == 2 && !it.isLoading
-            }.last().also { state ->
-                state.eventSink(PollHistoryEvents.LoadMore)
+                it.pollHistoryItems.size == 2
+            }
+            timeline.updatePaginationState {
+                copy(isBackPaginating = false)
             }
+            val loadedState = awaitItem()
+            assertThat(loadedState.isLoading).isFalse()
+            loadedState.eventSink(PollHistoryEvents.LoadMore)
             consumeItemsUntilPredicate {
                 it.isLoading
             }
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 8746670e03d..3e32ed675f8 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
@@ -37,6 +37,7 @@ import io.element.android.libraries.permissions.test.FakePermissionsPresenter
 import io.element.android.libraries.permissions.test.FakePermissionsPresenterFactory
 import io.element.android.tests.testutils.WarmUpRule
 import io.element.android.tests.testutils.consumeItemsUntilPredicate
+import io.element.android.tests.testutils.consumeItemsUntilTimeout
 import io.mockk.every
 import io.mockk.mockk
 import io.mockk.mockkStatic
@@ -292,11 +293,10 @@ class EditUserProfilePresenterTest {
             val initialState = awaitItem()
             initialState.eventSink(EditUserProfileEvents.UpdateDisplayName("   Name   "))
             initialState.eventSink(EditUserProfileEvents.Save)
-            consumeItemsUntilPredicate { matrixClient.setDisplayNameCalled && !matrixClient.removeAvatarCalled && !matrixClient.uploadAvatarCalled }
+            consumeItemsUntilTimeout()
             assertThat(matrixClient.setDisplayNameCalled).isFalse()
             assertThat(matrixClient.uploadAvatarCalled).isFalse()
             assertThat(matrixClient.removeAvatarCalled).isFalse()
-            cancelAndIgnoreRemainingEvents()
         }
     }

diff --git a/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListPresenterTests.kt b/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListPresenterTests.kt
index bf7c085ac73..45734567621 100644
--- a/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListPresenterTests.kt
+++ b/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListPresenterTests.kt
@@ -192,17 +192,22 @@ class RoomListPresenterTests {
             presenter.present()
         }.test {
             roomListService.postAllRooms(listOf(aRoomSummaryFilled()))
-            val loadedState = consumeItemsUntilPredicate { state -> state.roomList.size == 1 }.last()
+            skipItems(3)
+            val loadedState = awaitItem()
             // Test filtering with result
+            assertThat(loadedState.roomList.size).isEqualTo(1)
             loadedState.eventSink.invoke(RoomListEvents.UpdateFilter(A_ROOM_NAME.substring(0, 3)))
-            val withFilteredRoomState = consumeItemsUntilPredicate { state -> state.filteredRoomList.size == 1 }.last()
+            skipItems(1)
+            val withFilteredRoomState = awaitItem()
+            assertThat(withFilteredRoomState.filteredRoomList.size).isEqualTo(1)
             assertThat(withFilteredRoomState.filter).isEqualTo(A_ROOM_NAME.substring(0, 3))
             assertThat(withFilteredRoomState.filteredRoomList.size).isEqualTo(1)
             assertThat(withFilteredRoomState.filteredRoomList.first())
                 .isEqualTo(aRoomListRoomSummary)
             // Test filtering without result
             withFilteredRoomState.eventSink.invoke(RoomListEvents.UpdateFilter("tada"))
-            val withNotFilteredRoomState = consumeItemsUntilPredicate { state -> state.filteredRoomList.size == 0 }.last()
+            skipItems(1)
+            val withNotFilteredRoomState = awaitItem()
             assertThat(withNotFilteredRoomState.filter).isEqualTo("tada")
             assertThat(withNotFilteredRoomState.filteredRoomList).isEmpty()
             scope.cancel()
diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/roomlist/FakeRoomListService.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/roomlist/FakeRoomListService.kt
index 5c7d0983cdd..073a314e9ff 100644
--- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/roomlist/FakeRoomListService.kt
+++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/roomlist/FakeRoomListService.kt
@@ -52,6 +52,10 @@ class FakeRoomListService : RoomListService {
         roomListStateFlow.emit(state)
     }

+    suspend fun postSyncIndicator(value: RoomListService.SyncIndicator) {
+        syncIndicatorStateFlow.emit(value)
+    }
+
     var latestSlidingSyncRange: IntRange? = null
         private set

diff --git a/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/ReceiveTurbine.kt b/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/ReceiveTurbine.kt
index 3e47dd63cec..0c8bd89951a 100644
--- a/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/ReceiveTurbine.kt
+++ b/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/ReceiveTurbine.kt
@@ -19,7 +19,7 @@ package io.element.android.tests.testutils
 import app.cash.turbine.Event
 import app.cash.turbine.ReceiveTurbine
 import app.cash.turbine.withTurbineTimeout
-import io.element.android.libraries.core.data.tryOrNull
+import io.element.android.libraries.core.bool.orFalse
 import kotlin.time.Duration
 import kotlin.time.Duration.Companion.milliseconds

@@ -29,7 +29,7 @@ import kotlin.time.Duration.Companion.milliseconds
  * @return the list of consumed items.
  */
 suspend fun <T : Any> ReceiveTurbine<T>.consumeItemsUntilTimeout(timeout: Duration = 100.milliseconds): List<T> {
-    return consumeItemsUntilPredicate(timeout) { false }
+    return consumeItemsUntilPredicate(timeout, ignoreTimeoutError = true) { false }
 }

 /**
@@ -49,22 +49,29 @@ suspend fun <T : Any> ReceiveTurbine<T>.awaitLastSequentialItem(): T {
  */
 suspend fun <T : Any> ReceiveTurbine<T>.consumeItemsUntilPredicate(
     timeout: Duration = 100.milliseconds,
+    ignoreTimeoutError: Boolean = false,
     predicate: (T) -> Boolean,
 ): List<T> {
     val items = ArrayList<T>()
-    tryOrNull {
-        var foundItemOrFinished = false
-        while (!foundItemOrFinished) {
+    var exitLoop = false
+    try {
+        while (!exitLoop) {
             when (val event = withTurbineTimeout(timeout) { awaitEvent() }) {
                 is Event.Item<T> -> {
                     items.add(event.value)
-                    if (predicate(event.value)) {
-                        foundItemOrFinished = true
-                    }
+                    exitLoop = predicate(event.value)
                 }
-                Event.Complete, is Event.Error -> foundItemOrFinished = true
+                Event.Complete -> error("Unexpected complete")
+                is Event.Error -> throw event.throwable
             }
         }
+    } catch (assertionError: AssertionError) {
+        // TurbineAssertionError is internal :/, so rely on the message
+        if (assertionError.message?.startsWith("No value produced in").orFalse() && ignoreTimeoutError) {
+            // Timeout, ignore
+        } else {
+            throw assertionError
+        }
     }
     return items
 }


Scroll to bottom if the new event is from me, even if sent from another session

This fixes the issue that the timeline does not scroll when sending an attachment to the room, since there is no local echo (yet) for such event.

<!-- Please read [CONTRIBUTING.md](https://github.com/element-hq/element-x-android/blob/develop/CONTRIBUTING.md) before submitting your pull request -->

## Type of change

- [ ] Feature
- [ ] Bugfix
- [ ] Technical
- [ ] Other :

## Content

<!-- Describe shortly what has been changed -->
Finally implement [this](https://github.com/element-hq/element-x-android/pull/1879#discussion_r1407421943).

## Motivation and context

<!-- Provide link to the corresponding issue if applicable or explain the context -->
Fixes #1877 (this [comment](https://github.com/element-hq/element-x-android/issues/1877#issuecomment-1849544205)) for image, or whatever is sent by the user.

## Screenshots / GIFs

<!--
We have screenshot tests in the project, so attaching screenshots to a PR is not mandatory, as far as there
is a Composable Preview covering the changes. In this case, the change will appear in the file diff.
Note that all the UI composables should be covered by a Composable Preview.

Providing a video of the change is still very useful for the reviewer and for the history of the project.

You can use a table like this to show screenshots comparison.
Uncomment this markdown table below and edit the last line `|||`:
|copy screenshot of before here|copy screenshot of after here|

|Before|After|
|-|-|
|||
 -->

## Tests

<!-- Explain how you tested your development -->

- Step 1
- Step 2
- Step ...

## Tested devices

- [ ] Physical
- [ ] Emulator
- OS version(s):

## Checklist

<!-- Depending on the Pull Request content, it can be acceptable if some of the following checkboxes stay unchecked. -->

- [ ] Changes have been tested on an Android device or Android emulator with API 23
- [ ] UI change has been tested on both light and dark themes
- [ ] Accessibility has been taken into account. See https://github.com/element-hq/element-x-android/blob/develop/CONTRIBUTING.md#accessibility
- [ ] Pull request is based on the develop branch
- [ ] Pull request includes a new file under ./changelog.d. See https://github.com/element-hq/element-x-android/blob/develop/CONTRIBUTING.md#changelog
- [ ] Pull request includes screenshots or videos if containing UI changes
- [ ] Pull request includes a [sign off](https://matrix-org.github.io/synapse/latest/development/contributing_guide.html#sign-off)
- [ ] You've made a self review of your PR

Scroll to end of timeline when sending a new message
As a user that has send a message I want the timeline to scroll to that message at the end of the timeline so that I can have a look at what I just send.

If I may chime in, this sounds like it would make reading message backlogs and sending replies as I'm reading much more inconvenient, unless there's a reliable "back to last (un)read" feature coming too, to quickly jump back to where I was right before that automatic scroll.
> unless there's a reliable "back to last (un)read" feature coming too

I think you can (or, rather, will be able to) click on the message you replied to in order to go back up.
Only working for text-messages. Should have the same behaviour w. any message.
> Only working for text-messages. Should have the same behaviour w. any message.

It's not working for images for instance since there is no local echo support when sending images right now on the SDK.
So it's linked to https://github.com/matrix-org/matrix-rust-sdk/issues/1732
Can't you just scroll down when the message has been send?
This sounds like a bad idea for a media which can take time to get back from sync. And scrolling to the bottom as soon as we hit the send button is not great neither to me. So not sure what we can do on this for now.
The app could also move to the bottom of the timeline as soon as there is a new message sent by the user (with the side effect that it will scroll even if the user has sent the message from another session - which could be declared "acceptable" I think).
EIX uses the stick at bottom even for incoming messages
@VolkerJunginger will come back to the solution we should use on both platforms.
I have tested what's the behavior on WhatsApp:
- WhatsApp mobile: scroll to the history
- send a message from WhatsApp Desktop (same account)
The timeline is scrolling to live on WhatsApp mobile.

This is the same if you do it with exchange the 2 WhatsApp clients.

I guess we can do the same on Element then, but I let @VolkerJunginger confirm this.
The WhatsApp behaviour is the same we did on iOS, right? If so lets go with this for now.
Not sure this is exactly what is done on iOS. @pixlwave do you if it automatically scrolls if you receive a message you sent from another session?
Yep @ganfra is correct, we don't scroll for the user's other sessions. We basically send a signal to [scroll to bottom](https://github.com/element-hq/element-x-ios/blob/669141b1193fd2cde06b24fe77ca5c1673dc12ef/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift#L306-L312) whenever the app queues a [text message](https://github.com/element-hq/element-x-ios/blob/669141b1193fd2cde06b24fe77ca5c1673dc12ef/ElementX/Sources/Services/Timeline/TimelineProxy.swift#L347) or finished sending some form of [media](https://github.com/element-hq/element-x-ios/blob/669141b1193fd2cde06b24fe77ca5c1673dc12ef/ElementX/Sources/Services/Timeline/TimelineProxy.swift#L240).

diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenter.kt
index 799f6aa1686..718c32c67c9 100644
--- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenter.kt
+++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenter.kt
@@ -213,7 +213,8 @@ class TimelinePresenter @AssistedInject constructor(
             newMostRecentItemId != prevMostRecentItemIdValue
         if (hasNewEvent) {
             val newMostRecentEvent = newMostRecentItem as? TimelineItem.Event
-            val fromMe = newMostRecentEvent?.localSendState != null
+            // Scroll to bottom if the new event is from me, even if sent from another device
+            val fromMe = newMostRecentEvent?.isMine == true
             newEventState.value = if (fromMe) {
                 NewEventState.FromMe
             } else {

diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenterTest.kt
index 4316750a6a2..eaf61896fb1 100644
--- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenterTest.kt
+++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenterTest.kt
@@ -37,7 +37,6 @@ import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
 import io.element.android.libraries.matrix.api.timeline.MatrixTimeline
 import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
 import io.element.android.libraries.matrix.api.timeline.item.event.EventReaction
-import io.element.android.libraries.matrix.api.timeline.item.event.LocalEventSendState
 import io.element.android.libraries.matrix.api.timeline.item.event.ReactionSender
 import io.element.android.libraries.matrix.api.timeline.item.virtual.VirtualTimelineItem
 import io.element.android.libraries.matrix.test.AN_EVENT_ID
@@ -205,7 +204,7 @@ class TimelinePresenterTest {
             consumeItemsUntilPredicate { it.timelineItems.size == 1 }
             // Mimics sending a message, and assert newEventState is FromMe
             timeline.updateTimelineItems { items ->
-                val event = anEventTimelineItem(content = aMessageContent(), localSendState = LocalEventSendState.Sent(AN_EVENT_ID))
+                val event = anEventTimelineItem(content = aMessageContent(), isOwn = true)
                 items + listOf(MatrixTimelineItem.Event(1, event))
             }
             consumeItemsUntilPredicate { it.timelineItems.size == 2 }
@@ -366,7 +365,7 @@ class TimelinePresenterTest {
         messagesNavigator: FakeMessagesNavigator = FakeMessagesNavigator(),
         endPollAction: EndPollAction = FakeEndPollAction(),
         sendPollResponseAction: SendPollResponseAction = FakeSendPollResponseAction(),
-        ): TimelinePresenter {
+    ): TimelinePresenter {
         return TimelinePresenter(
             timelineItemsFactory = timelineItemsFactory,
             room = FakeMatrixRoom(matrixTimeline = timeline),


<!-- Please read [CONTRIBUTING.md](https://github.com/element-hq/element-x-android/blob/develop/CONTRIBUTING.md) before submitting your pull request -->

## Type of change

- [ ] Feature
- [X] Bugfix
- [ ] Technical
- [ ] Other :

## Content

<!-- Describe shortly what has been changed -->
Fix title of sign out screen regarding the different states

## Motivation and context

<!-- Provide link to the corresponding issue if applicable or explain the context -->
Closes #2009

## Screenshots / GIFs

<!--
We have screenshot tests in the project, so attaching screenshots to a PR is not mandatory, as far as there
is a Composable Preview covering the changes. In this case, the change will appear in the file diff.
Note that all the UI composables should be covered by a Composable Preview.

Providing a video of the change is still very useful for the reviewer and for the history of the project.

You can use a table like this to show screenshots comparison.
Uncomment this markdown table below and edit the last line `|||`:
|copy screenshot of before here|copy screenshot of after here|

|Before|After|
|-|-|
|||
 -->

## Tests

<!-- Explain how you tested your development -->

- Step 1
- Step 2
- Step ...

## Tested devices

- [ ] Physical
- [ ] Emulator
- OS version(s):

## Checklist

<!-- Depending on the Pull Request content, it can be acceptable if some of the following checkboxes stay unchecked. -->

- [ ] Changes have been tested on an Android device or Android emulator with API 23
- [ ] UI change has been tested on both light and dark themes
- [ ] Accessibility has been taken into account. See https://github.com/element-hq/element-x-android/blob/develop/CONTRIBUTING.md#accessibility
- [ ] Pull request is based on the develop branch
- [ ] Pull request includes a new file under ./changelog.d. See https://github.com/element-hq/element-x-android/blob/develop/CONTRIBUTING.md#changelog
- [ ] Pull request includes screenshots or videos if containing UI changes
- [ ] Pull request includes a [sign off](https://matrix-org.github.io/synapse/latest/development/contributing_guide.html#sign-off)
- [ ] You've made a self review of your PR

On logout it's saying that backup is off but it's not
Logout screen:

And settings

RS https://github.com/element-hq/element-x-android-rageshakes/issues/1199

./gradlew :features:logout:impl:testDebugUnitTest --tests "io.element.android.features.logout.impl.LogoutPresenterTest"

diff --git a/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/LogoutPresenter.kt b/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/LogoutPresenter.kt
index ff76bde7764..b8d8772104d 100644
--- a/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/LogoutPresenter.kt
+++ b/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/LogoutPresenter.kt
@@ -28,9 +28,11 @@ 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.bool.orTrue
 import io.element.android.libraries.featureflag.api.FeatureFlagService
 import io.element.android.libraries.featureflag.api.FeatureFlags
 import io.element.android.libraries.matrix.api.MatrixClient
+import io.element.android.libraries.matrix.api.encryption.BackupState
 import io.element.android.libraries.matrix.api.encryption.BackupUploadState
 import io.element.android.libraries.matrix.api.encryption.EncryptionService
 import kotlinx.coroutines.CoroutineScope
@@ -70,6 +72,19 @@ class LogoutPresenter @Inject constructor(
             isLastSession = encryptionService.isLastDevice().getOrNull() ?: false
         }

+        val backupState by encryptionService.backupStateStateFlow.collectAsState()
+        val recoveryState by encryptionService.recoveryStateStateFlow.collectAsState()
+
+        val doesBackupExistOnServerAction: MutableState<Async<Boolean>> = remember {
+            mutableStateOf(Async.Uninitialized)
+        }
+
+        LaunchedEffect(backupState) {
+            if (backupState == BackupState.UNKNOWN) {
+                getKeyBackupStatus(doesBackupExistOnServerAction)
+            }
+        }
+
         fun handleEvents(event: LogoutEvents) {
             when (event) {
                 is LogoutEvents.Logout -> {
@@ -89,6 +104,9 @@ class LogoutPresenter @Inject constructor(

         return LogoutState(
             isLastSession = isLastSession,
+            backupState = backupState,
+            doesBackupExistOnServer = doesBackupExistOnServerAction.value.dataOrNull().orTrue(),
+            recoveryState = recoveryState,
             backupUploadState = backupUploadState,
             showConfirmationDialog = showLogoutDialog,
             logoutAction = logoutAction.value,
@@ -96,6 +114,12 @@ class LogoutPresenter @Inject constructor(
         )
     }

+    private fun CoroutineScope.getKeyBackupStatus(action: MutableState<Async<Boolean>>) = launch {
+        suspend {
+            encryptionService.doesBackupExistOnServer().getOrThrow()
+        }.runCatchingUpdatingState(action)
+    }
+
     private fun CoroutineScope.logout(
         logoutAction: MutableState<Async<String?>>,
         ignoreSdkError: Boolean,
diff --git a/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/LogoutState.kt b/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/LogoutState.kt
index 1672640ab71..abade32b963 100644
--- a/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/LogoutState.kt
+++ b/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/LogoutState.kt
@@ -17,10 +17,15 @@
 package io.element.android.features.logout.impl

 import io.element.android.libraries.architecture.Async
+import io.element.android.libraries.matrix.api.encryption.BackupState
 import io.element.android.libraries.matrix.api.encryption.BackupUploadState
+import io.element.android.libraries.matrix.api.encryption.RecoveryState

 data class LogoutState(
     val isLastSession: Boolean,
+    val backupState: BackupState,
+    val doesBackupExistOnServer: Boolean,
+    val recoveryState: RecoveryState,
     val backupUploadState: BackupUploadState,
     val showConfirmationDialog: Boolean,
     val logoutAction: Async<String?>,
diff --git a/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/LogoutStateProvider.kt b/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/LogoutStateProvider.kt
index ddf0f30340c..8c9482105d9 100644
--- a/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/LogoutStateProvider.kt
+++ b/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/LogoutStateProvider.kt
@@ -18,7 +18,9 @@ package io.element.android.features.logout.impl

 import androidx.compose.ui.tooling.preview.PreviewParameterProvider
 import io.element.android.libraries.architecture.Async
+import io.element.android.libraries.matrix.api.encryption.BackupState
 import io.element.android.libraries.matrix.api.encryption.BackupUploadState
+import io.element.android.libraries.matrix.api.encryption.RecoveryState
 import io.element.android.libraries.matrix.api.encryption.SteadyStateException

 open class LogoutStateProvider : PreviewParameterProvider<LogoutState> {
@@ -32,16 +34,26 @@ open class LogoutStateProvider : PreviewParameterProvider<LogoutState> {
             aLogoutState(logoutAction = Async.Loading()),
             aLogoutState(logoutAction = Async.Failure(Exception("Failed to logout"))),
             aLogoutState(backupUploadState = BackupUploadState.SteadyException(SteadyStateException.Connection("No network"))),
+            // Last session no recovery
+            aLogoutState(isLastSession = true, recoveryState = RecoveryState.DISABLED),
+            // Last session no backup
+            aLogoutState(isLastSession = true, backupState = BackupState.UNKNOWN, doesBackupExistOnServer = false),
         )
 }

 fun aLogoutState(
     isLastSession: Boolean = false,
+    backupState: BackupState = BackupState.ENABLED,
+    doesBackupExistOnServer: Boolean = true,
+    recoveryState: RecoveryState = RecoveryState.ENABLED,
     backupUploadState: BackupUploadState = BackupUploadState.Unknown,
     showConfirmationDialog: Boolean = false,
     logoutAction: Async<String?> = Async.Uninitialized,
 ) = LogoutState(
     isLastSession = isLastSession,
+    backupState = backupState,
+    doesBackupExistOnServer = doesBackupExistOnServer,
+    recoveryState = recoveryState,
     backupUploadState = backupUploadState,
     showConfirmationDialog = showConfirmationDialog,
     logoutAction = logoutAction,
diff --git a/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/LogoutView.kt b/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/LogoutView.kt
index fd559642a58..d3d4805f827 100644
--- a/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/LogoutView.kt
+++ b/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/LogoutView.kt
@@ -43,7 +43,9 @@ import io.element.android.libraries.designsystem.theme.components.OutlinedButton
 import io.element.android.libraries.designsystem.theme.components.Text
 import io.element.android.libraries.designsystem.theme.progressIndicatorTrackColor
 import io.element.android.libraries.designsystem.utils.CommonDrawables
+import io.element.android.libraries.matrix.api.encryption.BackupState
 import io.element.android.libraries.matrix.api.encryption.BackupUploadState
+import io.element.android.libraries.matrix.api.encryption.RecoveryState
 import io.element.android.libraries.matrix.api.encryption.SteadyStateException
 import io.element.android.libraries.testtags.TestTags
 import io.element.android.libraries.testtags.testTag
@@ -120,7 +122,15 @@ fun LogoutView(
 private fun title(state: LogoutState): String {
     return when {
         state.backupUploadState.isBackingUp() -> stringResource(id = R.string.screen_signout_key_backup_ongoing_title)
-        state.isLastSession -> stringResource(id = R.string.screen_signout_key_backup_disabled_title)
+        state.isLastSession -> {
+            if (state.recoveryState != RecoveryState.ENABLED) {
+                stringResource(id = R.string.screen_signout_recovery_disabled_title)
+            } else if (state.backupState == BackupState.UNKNOWN && state.doesBackupExistOnServer.not()) {
+                stringResource(id = R.string.screen_signout_key_backup_disabled_title)
+            } else {
+                stringResource(id = R.string.screen_signout_save_recovery_key_title)
+            }
+        }
         else -> stringResource(CommonStrings.action_signout)
     }
 }
@@ -138,10 +148,10 @@ private fun subtitle(state: LogoutState): String? {

 private fun BackupUploadState.isBackingUp(): Boolean {
     return when (this) {
-        BackupUploadState.Unknown,
         BackupUploadState.Waiting,
         is BackupUploadState.Uploading -> true
         is BackupUploadState.SteadyException -> exception is SteadyStateException.Connection
+        BackupUploadState.Unknown,
         BackupUploadState.Done,
         BackupUploadState.Error -> false
     }

diff --git a/features/logout/impl/src/test/kotlin/io/element/android/features/logout/impl/LogoutPresenterTest.kt b/features/logout/impl/src/test/kotlin/io/element/android/features/logout/impl/LogoutPresenterTest.kt
index 9b06e71ba35..75401c290ae 100644
--- a/features/logout/impl/src/test/kotlin/io/element/android/features/logout/impl/LogoutPresenterTest.kt
+++ b/features/logout/impl/src/test/kotlin/io/element/android/features/logout/impl/LogoutPresenterTest.kt
@@ -24,8 +24,10 @@ import io.element.android.libraries.architecture.Async
 import io.element.android.libraries.featureflag.api.FeatureFlags
 import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
 import io.element.android.libraries.matrix.api.MatrixClient
+import io.element.android.libraries.matrix.api.encryption.BackupState
 import io.element.android.libraries.matrix.api.encryption.BackupUploadState
 import io.element.android.libraries.matrix.api.encryption.EncryptionService
+import io.element.android.libraries.matrix.api.encryption.RecoveryState
 import io.element.android.libraries.matrix.test.A_THROWABLE
 import io.element.android.libraries.matrix.test.FakeMatrixClient
 import io.element.android.libraries.matrix.test.encryption.FakeEncryptionService
@@ -50,6 +52,9 @@ class LogoutPresenterTest {
         }.test {
             val initialState = awaitLastSequentialItem()
             assertThat(initialState.isLastSession).isFalse()
+            assertThat(initialState.backupState).isEqualTo(BackupState.UNKNOWN)
+            assertThat(initialState.doesBackupExistOnServer).isTrue()
+            assertThat(initialState.recoveryState).isEqualTo(RecoveryState.UNKNOWN)
             assertThat(initialState.backupUploadState).isEqualTo(BackupUploadState.Unknown)
             assertThat(initialState.showConfirmationDialog).isFalse()
             assertThat(initialState.logoutAction).isEqualTo(Async.Uninitialized)
@@ -93,14 +98,15 @@ class LogoutPresenterTest {
         moleculeFlow(RecompositionMode.Immediate) {
             presenter.present()
         }.test {
-            skipItems(1)
             val initialState = awaitItem()
             assertThat(initialState.isLastSession).isFalse()
             assertThat(initialState.backupUploadState).isEqualTo(BackupUploadState.Unknown)
             assertThat(initialState.showConfirmationDialog).isFalse()
             assertThat(initialState.logoutAction).isEqualTo(Async.Uninitialized)
+            skipItems(1)
             val waitingState = awaitItem()
             assertThat(waitingState.backupUploadState).isEqualTo(BackupUploadState.Waiting)
+            skipItems(1)
             val uploadingState = awaitItem()
             assertThat(uploadingState.backupUploadState).isEqualTo(BackupUploadState.Uploading(backedUpCount = 1, totalCount = 2))
             val doneState = awaitItem()
@@ -155,7 +161,8 @@ class LogoutPresenterTest {
         moleculeFlow(RecompositionMode.Immediate) {
             presenter.present()
         }.test {
-            val initialState = awaitLastSequentialItem()
+            skipItems(1)
+            val initialState = awaitItem()
             initialState.eventSink.invoke(LogoutEvents.Logout(ignoreSdkError = false))
             val confirmationState = awaitItem()
             assertThat(confirmationState.showConfirmationDialog).isTrue()
@@ -164,6 +171,7 @@ class LogoutPresenterTest {
             val loadingState = awaitItem()
             assertThat(loadingState.showConfirmationDialog).isFalse()
             assertThat(loadingState.logoutAction).isInstanceOf(Async.Loading::class.java)
+            skipItems(1)
             val errorState = awaitItem()
             assertThat(errorState.logoutAction).isEqualTo(Async.Failure<LogoutState>(A_THROWABLE))
             errorState.eventSink.invoke(LogoutEvents.CloseDialogs)
@@ -183,7 +191,8 @@ class LogoutPresenterTest {
         moleculeFlow(RecompositionMode.Immediate) {
             presenter.present()
         }.test {
-            val initialState = awaitLastSequentialItem()
+            skipItems(1)
+            val initialState = awaitItem()
             initialState.eventSink.invoke(LogoutEvents.Logout(ignoreSdkError = false))
             val confirmationState = awaitItem()
             assertThat(confirmationState.showConfirmationDialog).isTrue()
@@ -192,6 +201,7 @@ class LogoutPresenterTest {
             val loadingState = awaitItem()
             assertThat(loadingState.showConfirmationDialog).isFalse()
             assertThat(loadingState.logoutAction).isInstanceOf(Async.Loading::class.java)
+            skipItems(1)
             val errorState = awaitItem()
             assertThat(errorState.logoutAction).isEqualTo(Async.Failure<LogoutState>(A_THROWABLE))
             errorState.eventSink.invoke(LogoutEvents.Logout(ignoreSdkError = true))
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_LogoutView_null_LogoutView-Day-0_1_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_LogoutView_null_LogoutView-Day-0_1_null_0,NEXUS_5,1.0,en].png
index f043cebc47a..c930a5e4862 100644
--- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_LogoutView_null_LogoutView-Day-0_1_null_0,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_LogoutView_null_LogoutView-Day-0_1_null_0,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
 version https://git-lfs.github.com/spec/v1
-oid sha256:0566f93f37532625a0c647992f25c2cbae9e944ab51629ad1c02322d28bad72e
-size 29115
+oid sha256:78edb2f0cbb3a7c975999aa31f90e5a82e7b73a21f2923e420f364da3c0118fd
+size 13706
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_LogoutView_null_LogoutView-Day-0_1_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_LogoutView_null_LogoutView-Day-0_1_null_1,NEXUS_5,1.0,en].png
index ff2ed0846ef..4bd4efb2270 100644
--- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_LogoutView_null_LogoutView-Day-0_1_null_1,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_LogoutView_null_LogoutView-Day-0_1_null_1,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
 version https://git-lfs.github.com/spec/v1
-oid sha256:5845034f94d3e3452bdda9e04d8609385154b3462ac98abb267819a7d55e1672
-size 33099
+oid sha256:186d3f45a3e8b294e523ede9787563399d97b26d7181ad687afcd8b48f8be506
+size 41247
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_LogoutView_null_LogoutView-Day-0_1_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_LogoutView_null_LogoutView-Day-0_1_null_3,NEXUS_5,1.0,en].png
index 00eb708f14c..4bd4efb2270 100644
--- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_LogoutView_null_LogoutView-Day-0_1_null_3,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_LogoutView_null_LogoutView-Day-0_1_null_3,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
 version https://git-lfs.github.com/spec/v1
-oid sha256:2c2f057c86716407a4cd3491612f57b9d4b800cf4ef749c2e6dce3e12fbbf632
-size 37972
+oid sha256:186d3f45a3e8b294e523ede9787563399d97b26d7181ad687afcd8b48f8be506
+size 41247
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_LogoutView_null_LogoutView-Day-0_1_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_LogoutView_null_LogoutView-Day-0_1_null_4,NEXUS_5,1.0,en].png
index 2504ca146c4..4e3d0eab1c6 100644
--- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_LogoutView_null_LogoutView-Day-0_1_null_4,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_LogoutView_null_LogoutView-Day-0_1_null_4,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
 version https://git-lfs.github.com/spec/v1
-oid sha256:475436dfdc616c2c5111e9dfbbf058e57fa12b05c82064e9a778d56cdeb05923
-size 34939
+oid sha256:2d8dfacb3767fe7e9de7996cd0fb9bcb7f208e49498bfdc7b599546669b7b396
+size 27632
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_LogoutView_null_LogoutView-Day-0_1_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_LogoutView_null_LogoutView-Day-0_1_null_5,NEXUS_5,1.0,en].png
index 4c27781a84d..e9f9d8d5dbd 100644
--- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_LogoutView_null_LogoutView-Day-0_1_null_5,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_LogoutView_null_LogoutView-Day-0_1_null_5,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
 version https://git-lfs.github.com/spec/v1
-oid sha256:a7001f904e0b190b8846cf0d440a972d1a6097fc168caee135469a2dbb39788c
-size 31590
+oid sha256:51528fe646b2ac7f679861af28443b22fe1a710586ac70aa730d98cf7c1e8856
+size 21842
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_LogoutView_null_LogoutView-Day-0_1_null_6,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_LogoutView_null_LogoutView-Day-0_1_null_6,NEXUS_5,1.0,en].png
index 2b5e0b9c758..72f4c9d66e6 100644
--- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_LogoutView_null_LogoutView-Day-0_1_null_6,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_LogoutView_null_LogoutView-Day-0_1_null_6,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
 version https://git-lfs.github.com/spec/v1
-oid sha256:77fe1e9484c72dc9572a810941cc83ecd208aa080d59417e409c69edbea978d5
-size 33484
+oid sha256:47f4ac4fb71f8a4adc3890101244c4ee9fbbf87d0da0e0381a85792b79896a5f
+size 26372
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_LogoutView_null_LogoutView-Day-0_1_null_8,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_LogoutView_null_LogoutView-Day-0_1_null_8,NEXUS_5,1.0,en].png
new file mode 100644
index 00000000000..b23d4ec9216
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_LogoutView_null_LogoutView-Day-0_1_null_8,NEXUS_5,1.0,en].png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:87010d4aae66e1030d4b9557e4f065df777324495459c08513040e6793d2d00d
+size 36675
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_LogoutView_null_LogoutView-Day-0_1_null_9,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_LogoutView_null_LogoutView-Day-0_1_null_9,NEXUS_5,1.0,en].png
new file mode 100644
index 00000000000..00eb708f14c
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_LogoutView_null_LogoutView-Day-0_1_null_9,NEXUS_5,1.0,en].png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:2c2f057c86716407a4cd3491612f57b9d4b800cf4ef749c2e6dce3e12fbbf632
+size 37972
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_LogoutView_null_LogoutView-Night-0_2_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_LogoutView_null_LogoutView-Night-0_2_null_0,NEXUS_5,1.0,en].png
index acf219bae25..5915ad7d403 100644
--- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_LogoutView_null_LogoutView-Night-0_2_null_0,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_LogoutView_null_LogoutView-Night-0_2_null_0,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
 version https://git-lfs.github.com/spec/v1
-oid sha256:ccdef8b261183673b56d0405b0a2b233eddc0537af600f9071d1b0b0bb495e3b
-size 27390
+oid sha256:dd5acfa31ca9994634286aa59bdb67d86e438165296164db82e152cd56868585
+size 13044
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_LogoutView_null_LogoutView-Night-0_2_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_LogoutView_null_LogoutView-Night-0_2_null_1,NEXUS_5,1.0,en].png
index a0bf0b2a3c1..b28650d35e0 100644
--- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_LogoutView_null_LogoutView-Night-0_2_null_1,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_LogoutView_null_LogoutView-Night-0_2_null_1,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
 version https://git-lfs.github.com/spec/v1
-oid sha256:dbbd4c82b8f8a1c283074c51968b7e097595721cb25ba2612b9fff5e2c1a5c07
-size 30999
+oid sha256:49df4ac1a7ec108308b82d0fdb6f0e0d8ca2063960070fa9865562e59774ec21
+size 38623
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_LogoutView_null_LogoutView-Night-0_2_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_LogoutView_null_LogoutView-Night-0_2_null_3,NEXUS_5,1.0,en].png
index 3a2db9c9ab2..b28650d35e0 100644
--- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_LogoutView_null_LogoutView-Night-0_2_null_3,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_LogoutView_null_LogoutView-Night-0_2_null_3,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
 version https://git-lfs.github.com/spec/v1
-oid sha256:52bf6e6334dd36e2e39f6957a60c7d54839caffdf47a32a0bb463124fc2c27d4
-size 35820
+oid sha256:49df4ac1a7ec108308b82d0fdb6f0e0d8ca2063960070fa9865562e59774ec21
+size 38623
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_LogoutView_null_LogoutView-Night-0_2_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_LogoutView_null_LogoutView-Night-0_2_null_4,NEXUS_5,1.0,en].png
index eefa148ed56..19068829313 100644
--- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_LogoutView_null_LogoutView-Night-0_2_null_4,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_LogoutView_null_LogoutView-Night-0_2_null_4,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
 version https://git-lfs.github.com/spec/v1
-oid sha256:426c68f5506c8f8a203a3d017130bebd556146b241d044d4ba9160b8029a4fd6
-size 30490
+oid sha256:51af5a971e3bb5958646ec1866ba41fd4d46e281713573b005bc71b4762ec7b4
+size 24085
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_LogoutView_null_LogoutView-Night-0_2_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_LogoutView_null_LogoutView-Night-0_2_null_5,NEXUS_5,1.0,en].png
index 02eab2ed181..2c7916bd2b9 100644
--- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_LogoutView_null_LogoutView-Night-0_2_null_5,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_LogoutView_null_LogoutView-Night-0_2_null_5,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
 version https://git-lfs.github.com/spec/v1
-oid sha256:277730c5d3e711f28b8405f90b004268343845a1b528860cfbffcf47c7e39086
-size 28511
+oid sha256:6efe41b16360b4cfc891a5ffc99ff147ea5c5324341d5f8b8a2f574d360b5aad
+size 19117
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_LogoutView_null_LogoutView-Night-0_2_null_6,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_LogoutView_null_LogoutView-Night-0_2_null_6,NEXUS_5,1.0,en].png
index 8bdee04c642..8de5498d315 100644
--- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_LogoutView_null_LogoutView-Night-0_2_null_6,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_LogoutView_null_LogoutView-Night-0_2_null_6,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
 version https://git-lfs.github.com/spec/v1
-oid sha256:f3814c6d92191053e08d6c6b7c4ad7c52cdf868795bd50771d03c08885d2c298
-size 29271
+oid sha256:6a2a05a5d4f528302ca1a6f8bd799418f97d511bb808dadbfe806077ab2a9bab
+size 22756
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_LogoutView_null_LogoutView-Night-0_2_null_8,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_LogoutView_null_LogoutView-Night-0_2_null_8,NEXUS_5,1.0,en].png
new file mode 100644
index 00000000000..b61e420d857
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_LogoutView_null_LogoutView-Night-0_2_null_8,NEXUS_5,1.0,en].png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:3961ac676e29ac3d6f8dede3aed1c0c22d400e6a95ca8c4333691102f1616c42
+size 34922
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_LogoutView_null_LogoutView-Night-0_2_null_9,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_LogoutView_null_LogoutView-Night-0_2_null_9,NEXUS_5,1.0,en].png
new file mode 100644
index 00000000000..3a2db9c9ab2
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_LogoutView_null_LogoutView-Night-0_2_null_9,NEXUS_5,1.0,en].png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:52bf6e6334dd36e2e39f6957a60c7d54839caffdf47a32a0bb463124fc2c27d4
+size 35820