From c9ab0927f06309f56f71b460321c54abadbd9c47 Mon Sep 17 00:00:00 2001
From: Florian Renaud <florianr@element.io>
Date: Wed, 11 May 2022 16:19:33 +0200
Subject: [PATCH 01/26] Start DM - Add feature flag

---
 .../debug/features/DebugFeaturesStateFactory.kt       |  5 +++++
 .../features/debug/features/DebugVectorFeatures.kt    |  4 ++++
 .../java/im/vector/app/features/VectorFeatures.kt     |  2 ++
 .../createdirect/CreateDirectRoomViewModel.kt         | 11 +++++++++--
 4 files changed, 20 insertions(+), 2 deletions(-)

diff --git a/vector/src/debug/java/im/vector/app/features/debug/features/DebugFeaturesStateFactory.kt b/vector/src/debug/java/im/vector/app/features/debug/features/DebugFeaturesStateFactory.kt
index 248d9d232b..5506c2497c 100644
--- a/vector/src/debug/java/im/vector/app/features/debug/features/DebugFeaturesStateFactory.kt
+++ b/vector/src/debug/java/im/vector/app/features/debug/features/DebugFeaturesStateFactory.kt
@@ -70,6 +70,11 @@ class DebugFeaturesStateFactory @Inject constructor(
                                 key = DebugFeatureKeys.allowExternalUnifiedPushDistributors,
                                 factory = VectorFeatures::allowExternalUnifiedPushDistributors
                         ),
+                        createBooleanFeature(
+                                label = "Start DM on first message",
+                                key = DebugFeatureKeys.startDmOnFirstMsg,
+                                factory = VectorFeatures::shouldStartDmOnFirstMessage
+                        ),
                 )
         )
     }
diff --git a/vector/src/debug/java/im/vector/app/features/debug/features/DebugVectorFeatures.kt b/vector/src/debug/java/im/vector/app/features/debug/features/DebugVectorFeatures.kt
index 919cc6635e..617284d9e4 100644
--- a/vector/src/debug/java/im/vector/app/features/debug/features/DebugVectorFeatures.kt
+++ b/vector/src/debug/java/im/vector/app/features/debug/features/DebugVectorFeatures.kt
@@ -66,6 +66,9 @@ class DebugVectorFeatures(
     override fun isScreenSharingEnabled(): Boolean = read(DebugFeatureKeys.screenSharing)
             ?: vectorFeatures.isScreenSharingEnabled()
 
+    override fun shouldStartDmOnFirstMessage(): Boolean = read(DebugFeatureKeys.startDmOnFirstMsg)
+            ?: vectorFeatures.shouldStartDmOnFirstMessage()
+
     fun <T> override(value: T?, key: Preferences.Key<T>) = updatePreferences {
         if (value == null) {
             it.remove(key)
@@ -123,4 +126,5 @@ object DebugFeatureKeys {
     val allowExternalUnifiedPushDistributors = booleanPreferencesKey("allow-external-unified-push-distributors")
     val liveLocationSharing = booleanPreferencesKey("live-location-sharing")
     val screenSharing = booleanPreferencesKey("screen-sharing")
+    val startDmOnFirstMsg = booleanPreferencesKey("start-dm-on-first-msg")
 }
diff --git a/vector/src/main/java/im/vector/app/features/VectorFeatures.kt b/vector/src/main/java/im/vector/app/features/VectorFeatures.kt
index 6fe4beff95..ad49916a07 100644
--- a/vector/src/main/java/im/vector/app/features/VectorFeatures.kt
+++ b/vector/src/main/java/im/vector/app/features/VectorFeatures.kt
@@ -30,6 +30,7 @@ interface VectorFeatures {
     fun isOnboardingCombinedLoginEnabled(): Boolean
     fun allowExternalUnifiedPushDistributors(): Boolean
     fun isScreenSharingEnabled(): Boolean
+    fun shouldStartDmOnFirstMessage(): Boolean
 
     enum class OnboardingVariant {
         LEGACY,
@@ -48,4 +49,5 @@ class DefaultVectorFeatures : VectorFeatures {
     override fun isOnboardingCombinedLoginEnabled() = false
     override fun allowExternalUnifiedPushDistributors(): Boolean = Config.ALLOW_EXTERNAL_UNIFIED_PUSH_DISTRIBUTORS
     override fun isScreenSharingEnabled(): Boolean = true
+    override fun shouldStartDmOnFirstMessage(): Boolean = false
 }
diff --git a/vector/src/main/java/im/vector/app/features/createdirect/CreateDirectRoomViewModel.kt b/vector/src/main/java/im/vector/app/features/createdirect/CreateDirectRoomViewModel.kt
index 8374f9d513..5574ce3e63 100644
--- a/vector/src/main/java/im/vector/app/features/createdirect/CreateDirectRoomViewModel.kt
+++ b/vector/src/main/java/im/vector/app/features/createdirect/CreateDirectRoomViewModel.kt
@@ -26,6 +26,7 @@ import im.vector.app.core.di.MavericksAssistedViewModelFactory
 import im.vector.app.core.di.hiltMavericksViewModelFactory
 import im.vector.app.core.mvrx.runCatchingToAsync
 import im.vector.app.core.platform.VectorViewModel
+import im.vector.app.features.VectorFeatures
 import im.vector.app.features.analytics.AnalyticsTracker
 import im.vector.app.features.analytics.plan.CreatedRoom
 import im.vector.app.features.raw.wellknown.getElementWellknown
@@ -46,7 +47,8 @@ class CreateDirectRoomViewModel @AssistedInject constructor(
         @Assisted initialState: CreateDirectRoomViewState,
         private val rawService: RawService,
         val session: Session,
-        val analyticsTracker: AnalyticsTracker
+        val analyticsTracker: AnalyticsTracker,
+        val vectorFeatures: VectorFeatures
 ) :
         VectorViewModel<CreateDirectRoomViewState, CreateDirectRoomAction, CreateDirectRoomViewEvents>(initialState) {
 
@@ -124,7 +126,12 @@ class CreateDirectRoomViewModel @AssistedInject constructor(
                     }
 
             val result = runCatchingToAsync {
-                session.roomService().createRoom(roomParams)
+                if (vectorFeatures.shouldStartDmOnFirstMessage()) {
+                    // Todo: Prepare direct room creation
+                    throw Throwable("Start DM on first message is not implemented yet.")
+                } else {
+                    session.roomService().createRoom(roomParams)
+                }
             }
             analyticsTracker.capture(CreatedRoom(isDM = roomParams.isDirect.orFalse()))
 

From ba3d3501274bd7c2f372f54a87ee04f3527a4f81 Mon Sep 17 00:00:00 2001
From: Florian Renaud <florianr@element.io>
Date: Wed, 11 May 2022 16:20:27 +0200
Subject: [PATCH 02/26] Fix VectorFeature pref key

---
 .../vector/app/features/debug/features/DebugVectorFeatures.kt   | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/vector/src/debug/java/im/vector/app/features/debug/features/DebugVectorFeatures.kt b/vector/src/debug/java/im/vector/app/features/debug/features/DebugVectorFeatures.kt
index 617284d9e4..7f74737719 100644
--- a/vector/src/debug/java/im/vector/app/features/debug/features/DebugVectorFeatures.kt
+++ b/vector/src/debug/java/im/vector/app/features/debug/features/DebugVectorFeatures.kt
@@ -119,7 +119,7 @@ private fun <T : Enum<T>> enumPreferencesKey(type: KClass<T>) = stringPreference
 object DebugFeatureKeys {
     val onboardingAlreadyHaveAnAccount = booleanPreferencesKey("onboarding-already-have-an-account")
     val onboardingSplashCarousel = booleanPreferencesKey("onboarding-splash-carousel")
-    val onboardingUseCase = booleanPreferencesKey("onboarding-splash-carousel")
+    val onboardingUseCase = booleanPreferencesKey("onboarding-use-case")
     val onboardingPersonalize = booleanPreferencesKey("onboarding-personalize")
     val onboardingCombinedRegister = booleanPreferencesKey("onboarding-combined-register")
     val onboardingCombinedLogin = booleanPreferencesKey("onboarding-combined-login")

From 26d1a12b74955df839ef513b6987d5d520662ec9 Mon Sep 17 00:00:00 2001
From: Florian Renaud <florianr@element.io>
Date: Wed, 16 Mar 2022 17:43:29 +0100
Subject: [PATCH 03/26] Start DM - Rename action button to "go"

---
 vector/src/main/res/menu/vector_create_direct_room.xml | 2 +-
 vector/src/main/res/values/strings.xml                 | 1 +
 2 files changed, 2 insertions(+), 1 deletion(-)

diff --git a/vector/src/main/res/menu/vector_create_direct_room.xml b/vector/src/main/res/menu/vector_create_direct_room.xml
index 8c6eab1c52..10b2adf23d 100755
--- a/vector/src/main/res/menu/vector_create_direct_room.xml
+++ b/vector/src/main/res/menu/vector_create_direct_room.xml
@@ -4,7 +4,7 @@
 
     <item
         android:id="@+id/action_create_direct_room"
-        android:title="@string/create_room_action_create"
+        android:title="@string/create_room_action_go"
         app:showAsAction="always" />
 
 </menu>
diff --git a/vector/src/main/res/values/strings.xml b/vector/src/main/res/values/strings.xml
index f60da53c09..b507ed473f 100644
--- a/vector/src/main/res/values/strings.xml
+++ b/vector/src/main/res/values/strings.xml
@@ -1623,6 +1623,7 @@
 
     <!-- Create room screen -->
     <string name="create_room_action_create">"CREATE"</string>
+    <string name="create_room_action_go">Go</string>
     <string name="create_room_name_section">"Room name"</string>
     <string name="create_room_name_hint">"Name"</string>
     <string name="create_room_topic_section">"Room topic (optional)"</string>

From 10d683ad5d1bf931b7578cfa558108826c7fb407 Mon Sep 17 00:00:00 2001
From: Florian Renaud <florianr@element.io>
Date: Thu, 17 Mar 2022 10:37:04 +0100
Subject: [PATCH 04/26] Start DM - display a local room before creating the
 real one

Add CreateLocalRoomTask interface and remove DI annotations
---
 .../sdk/api/session/room/RoomService.kt       |   7 +
 .../room/model/localecho/RoomLocalEcho.kt     |  28 ++
 .../session/room/DefaultRoomService.kt        |   6 +
 .../sdk/internal/session/room/RoomModule.kt   |   5 +
 .../room/create/CreateLocalRoomTask.kt        | 267 ++++++++++++++++++
 .../session/room/create/CreateRoomBody.kt     |  17 ++
 .../session/room/create/CreateRoomTask.kt     |  29 +-
 .../session/room/timeline/DefaultTimeline.kt  |   2 +
 .../createdirect/CreateDirectRoomAction.kt    |   4 +-
 .../createdirect/CreateDirectRoomActivity.kt  |   2 +-
 .../createdirect/CreateDirectRoomViewModel.kt |  20 +-
 .../createdirect/CreateDirectRoomViewState.kt |   2 +
 12 files changed, 352 insertions(+), 37 deletions(-)
 create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/localecho/RoomLocalEcho.kt
 create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateLocalRoomTask.kt

diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/RoomService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/RoomService.kt
index 5dfb8961e3..41a215636d 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/RoomService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/RoomService.kt
@@ -40,6 +40,13 @@ interface RoomService {
      */
     suspend fun createRoom(createRoomParams: CreateRoomParams): String
 
+    /**
+     * Create a room locally.
+     * This room will not be synchronized with the server and will not come back from the sync, so all the events related to this room will be generated
+     * locally.
+     */
+    suspend fun createLocalRoom(createRoomParams: CreateRoomParams): String
+
     /**
      * Create a direct room asynchronously. This is a facility method to create a direct room with the necessary parameters.
      */
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/localecho/RoomLocalEcho.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/localecho/RoomLocalEcho.kt
new file mode 100644
index 0000000000..a9804c6dac
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/localecho/RoomLocalEcho.kt
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2020 The Matrix.org Foundation C.I.C.
+ *
+ * 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 org.matrix.android.sdk.api.session.room.model.localecho
+
+import java.util.UUID
+
+object RoomLocalEcho {
+
+    private const val PREFIX = "!local."
+
+    fun isLocalEchoId(roomId: String) = roomId.startsWith(PREFIX)
+
+    fun createLocalEchoId() = "${PREFIX}${UUID.randomUUID()}"
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoomService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoomService.kt
index 5e6d052443..cc5e935f87 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoomService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoomService.kt
@@ -43,6 +43,7 @@ import org.matrix.android.sdk.internal.database.model.RoomMemberSummaryEntityFie
 import org.matrix.android.sdk.internal.di.SessionDatabase
 import org.matrix.android.sdk.internal.session.room.alias.DeleteRoomAliasTask
 import org.matrix.android.sdk.internal.session.room.alias.GetRoomIdByAliasTask
+import org.matrix.android.sdk.internal.session.room.create.CreateLocalRoomTask
 import org.matrix.android.sdk.internal.session.room.create.CreateRoomTask
 import org.matrix.android.sdk.internal.session.room.membership.RoomChangeMembershipStateDataSource
 import org.matrix.android.sdk.internal.session.room.membership.RoomMemberHelper
@@ -60,6 +61,7 @@ import javax.inject.Inject
 internal class DefaultRoomService @Inject constructor(
         @SessionDatabase private val monarchy: Monarchy,
         private val createRoomTask: CreateRoomTask,
+        private val createLocalRoomTask: CreateLocalRoomTask,
         private val joinRoomTask: JoinRoomTask,
         private val markAllRoomsReadTask: MarkAllRoomsReadTask,
         private val updateBreadcrumbsTask: UpdateBreadcrumbsTask,
@@ -78,6 +80,10 @@ internal class DefaultRoomService @Inject constructor(
         return createRoomTask.executeRetry(createRoomParams, 3)
     }
 
+    override suspend fun createLocalRoom(createRoomParams: CreateRoomParams): String {
+        return createLocalRoomTask.execute(createRoomParams)
+    }
+
     override fun getRoom(roomId: String): Room? {
         return roomGetter.getRoom(roomId)
     }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomModule.kt
index f3845f1f15..bb92287ead 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomModule.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomModule.kt
@@ -43,7 +43,9 @@ import org.matrix.android.sdk.internal.session.room.alias.DefaultGetRoomLocalAli
 import org.matrix.android.sdk.internal.session.room.alias.DeleteRoomAliasTask
 import org.matrix.android.sdk.internal.session.room.alias.GetRoomIdByAliasTask
 import org.matrix.android.sdk.internal.session.room.alias.GetRoomLocalAliasesTask
+import org.matrix.android.sdk.internal.session.room.create.CreateLocalRoomTask
 import org.matrix.android.sdk.internal.session.room.create.CreateRoomTask
+import org.matrix.android.sdk.internal.session.room.create.DefaultCreateLocalRoomTask
 import org.matrix.android.sdk.internal.session.room.create.DefaultCreateRoomTask
 import org.matrix.android.sdk.internal.session.room.directory.DefaultGetPublicRoomTask
 import org.matrix.android.sdk.internal.session.room.directory.DefaultGetRoomDirectoryVisibilityTask
@@ -192,6 +194,9 @@ internal abstract class RoomModule {
     @Binds
     abstract fun bindCreateRoomTask(task: DefaultCreateRoomTask): CreateRoomTask
 
+    @Binds
+    abstract fun bindCreateLocalRoomTask(task: DefaultCreateLocalRoomTask): CreateLocalRoomTask
+
     @Binds
     abstract fun bindGetPublicRoomTask(task: DefaultGetPublicRoomTask): GetPublicRoomTask
 
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateLocalRoomTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateLocalRoomTask.kt
new file mode 100644
index 0000000000..e650039d75
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateLocalRoomTask.kt
@@ -0,0 +1,267 @@
+/*
+ * Copyright 2022 The Matrix.org Foundation C.I.C.
+ *
+ * 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 org.matrix.android.sdk.internal.session.room.create
+
+import com.zhuinden.monarchy.Monarchy
+import io.realm.Realm
+import io.realm.RealmConfiguration
+import io.realm.kotlin.createObject
+import kotlinx.coroutines.TimeoutCancellationException
+import org.matrix.android.sdk.api.extensions.orFalse
+import org.matrix.android.sdk.api.extensions.tryOrNull
+import org.matrix.android.sdk.api.session.events.model.Content
+import org.matrix.android.sdk.api.session.events.model.Event
+import org.matrix.android.sdk.api.session.events.model.EventType
+import org.matrix.android.sdk.api.session.events.model.toContent
+import org.matrix.android.sdk.api.session.room.failure.CreateRoomFailure
+import org.matrix.android.sdk.api.session.room.model.GuestAccess
+import org.matrix.android.sdk.api.session.room.model.Membership
+import org.matrix.android.sdk.api.session.room.model.RoomDirectoryVisibility
+import org.matrix.android.sdk.api.session.room.model.RoomHistoryVisibility
+import org.matrix.android.sdk.api.session.room.model.RoomMemberContent
+import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams
+import org.matrix.android.sdk.api.session.room.model.create.RoomCreateContent
+import org.matrix.android.sdk.api.session.room.model.localecho.RoomLocalEcho
+import org.matrix.android.sdk.api.session.room.send.SendState
+import org.matrix.android.sdk.api.session.sync.model.RoomSyncSummary
+import org.matrix.android.sdk.api.session.user.UserService
+import org.matrix.android.sdk.api.session.user.model.User
+import org.matrix.android.sdk.internal.database.awaitNotEmptyResult
+import org.matrix.android.sdk.internal.database.helper.addTimelineEvent
+import org.matrix.android.sdk.internal.database.mapper.asDomain
+import org.matrix.android.sdk.internal.database.mapper.toEntity
+import org.matrix.android.sdk.internal.database.model.ChunkEntity
+import org.matrix.android.sdk.internal.database.model.CurrentStateEventEntity
+import org.matrix.android.sdk.internal.database.model.EventInsertType
+import org.matrix.android.sdk.internal.database.model.RoomEntity
+import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity
+import org.matrix.android.sdk.internal.database.model.RoomSummaryEntityFields
+import org.matrix.android.sdk.internal.database.query.copyToRealmOrIgnore
+import org.matrix.android.sdk.internal.database.query.getOrCreate
+import org.matrix.android.sdk.internal.database.query.getOrNull
+import org.matrix.android.sdk.internal.di.SessionDatabase
+import org.matrix.android.sdk.internal.di.UserId
+import org.matrix.android.sdk.internal.session.events.getFixedRoomMemberContent
+import org.matrix.android.sdk.internal.session.room.membership.RoomMemberEventHandler
+import org.matrix.android.sdk.internal.session.room.summary.RoomSummaryUpdater
+import org.matrix.android.sdk.internal.session.room.timeline.PaginationDirection
+import org.matrix.android.sdk.internal.task.Task
+import org.matrix.android.sdk.internal.util.awaitTransaction
+import org.matrix.android.sdk.internal.util.time.Clock
+import java.util.UUID
+import java.util.concurrent.TimeUnit
+import javax.inject.Inject
+
+internal interface CreateLocalRoomTask : Task<CreateRoomParams, String>
+
+internal class DefaultCreateLocalRoomTask @Inject constructor(
+        @UserId private val userId: String,
+        @SessionDatabase private val monarchy: Monarchy,
+        private val roomMemberEventHandler: RoomMemberEventHandler,
+        private val roomSummaryUpdater: RoomSummaryUpdater,
+        @SessionDatabase private val realmConfiguration: RealmConfiguration,
+        private val createRoomBodyBuilder: CreateRoomBodyBuilder,
+        private val userService: UserService,
+        private val clock: Clock,
+) : CreateLocalRoomTask {
+
+    override suspend fun execute(params: CreateRoomParams): String {
+        val createRoomBody = createRoomBodyBuilder.build(params.withDefault())
+        val roomId = RoomLocalEcho.createLocalEchoId()
+        monarchy.awaitTransaction { realm ->
+            createLocalRoomEntity(realm, roomId, createRoomBody)
+            createLocalRoomSummaryEntity(realm, roomId, createRoomBody)
+        }
+
+        // Wait for room to be created in DB
+        try {
+            awaitNotEmptyResult(realmConfiguration, TimeUnit.MINUTES.toMillis(1L)) { realm ->
+                realm.where(RoomSummaryEntity::class.java)
+                        .equalTo(RoomSummaryEntityFields.ROOM_ID, roomId)
+                        .equalTo(RoomSummaryEntityFields.MEMBERSHIP_STR, Membership.JOIN.name)
+            }
+        } catch (exception: TimeoutCancellationException) {
+            throw CreateRoomFailure.CreatedWithTimeout(roomId)
+        }
+
+        return roomId
+    }
+
+    /**
+     * Create a local room entity from the given room creation params.
+     * This will also generate and store in database the chunk and the events related to the room params in order to retrieve and display the local room.
+     */
+    private suspend fun createLocalRoomEntity(realm: Realm, roomId: String, createRoomBody: CreateRoomBody) {
+        RoomEntity.getOrCreate(realm, roomId).apply {
+            membership = Membership.JOIN
+            chunks.add(createLocalRoomChunk(realm, roomId, createRoomBody))
+        }
+    }
+
+    private fun createLocalRoomSummaryEntity(realm: Realm, roomId: String, createRoomBody: CreateRoomBody) {
+        val otherUserId = createRoomBody.getDirectUserId()
+        if (otherUserId != null) {
+            RoomSummaryEntity.getOrCreate(realm, roomId).apply {
+                isDirect = true
+                directUserId = otherUserId
+            }
+        }
+        roomSummaryUpdater.update(
+                realm = realm,
+                roomId = roomId,
+                membership = Membership.JOIN,
+                roomSummary = RoomSyncSummary(
+                        heroes = createRoomBody.invitedUserIds.orEmpty().take(5),
+                        joinedMembersCount = 1,
+                        invitedMembersCount = createRoomBody.invitedUserIds?.size ?: 0
+                ),
+                updateMembers = !createRoomBody.invitedUserIds.isNullOrEmpty()
+        )
+    }
+
+    /**
+     * Create a single chunk containing the necessary events to display the local room.
+     *
+     * @param realm the current instance of realm
+     * @param roomId the id of the local room
+     * @param createRoomBody the room creation params
+     *
+     * @return a chunk entity
+     */
+    private suspend fun createLocalRoomChunk(realm: Realm, roomId: String, createRoomBody: CreateRoomBody): ChunkEntity {
+        val chunkEntity = realm.createObject<ChunkEntity>().apply {
+            isLastBackward = true
+            isLastForward = true
+        }
+
+        val eventList = createLocalRoomEvents(createRoomBody)
+        val eventIds = ArrayList<String>(eventList.size)
+        val roomMemberContentsByUser = HashMap<String, RoomMemberContent?>()
+
+        for (event in eventList) {
+            if (event.eventId == null || event.senderId == null || event.type == null) {
+                continue
+            }
+
+            eventIds.add(event.eventId)
+
+            val eventEntity = event.toEntity(roomId, SendState.SYNCED, null).copyToRealmOrIgnore(realm, EventInsertType.INCREMENTAL_SYNC)
+            if (event.stateKey != null) {
+                CurrentStateEventEntity.getOrCreate(realm, roomId, event.stateKey, event.type).apply {
+                    eventId = event.eventId
+                    root = eventEntity
+                }
+                if (event.type == EventType.STATE_ROOM_MEMBER) {
+                    roomMemberContentsByUser[event.stateKey] = event.getFixedRoomMemberContent()
+                    roomMemberEventHandler.handle(realm, roomId, event, false)
+                }
+            }
+
+            roomMemberContentsByUser.getOrPut(event.senderId) {
+                // If we don't have any new state on this user, get it from db
+                val rootStateEvent = CurrentStateEventEntity.getOrNull(realm, roomId, event.senderId, EventType.STATE_ROOM_MEMBER)?.root
+                rootStateEvent?.asDomain()?.getFixedRoomMemberContent()
+            }
+
+            chunkEntity.addTimelineEvent(
+                    roomId = roomId,
+                    eventEntity = eventEntity,
+                    direction = PaginationDirection.FORWARDS,
+                    roomMemberContentsByUser = roomMemberContentsByUser
+            )
+        }
+
+        return chunkEntity
+    }
+
+    /**
+     * Build the list of the events related to the room creation params.
+     *
+     * @param createRoomBody the room creation params
+     *
+     * @return the list of events
+     */
+    private suspend fun createLocalRoomEvents(createRoomBody: CreateRoomBody): List<Event> {
+        val myUser = userService.getUser(userId) ?: User(userId)
+        val invitedUsers = createRoomBody.invitedUserIds.orEmpty()
+                .mapNotNull { tryOrNull { userService.resolveUser(it) } }
+
+        val createRoomEvent = createFakeEvent(
+                type = EventType.STATE_ROOM_CREATE,
+                content = RoomCreateContent(
+                        creator = userId
+                ).toContent()
+        )
+        val myRoomMemberEvent = createFakeEvent(
+                type = EventType.STATE_ROOM_MEMBER,
+                content = RoomMemberContent(
+                        membership = Membership.JOIN,
+                        displayName = myUser.displayName,
+                        avatarUrl = myUser.avatarUrl
+                ).toContent(),
+                stateKey = userId
+        )
+        val roomMemberEvents = invitedUsers.map {
+            createFakeEvent(
+                    type = EventType.STATE_ROOM_MEMBER,
+                    content = RoomMemberContent(
+                            isDirect = createRoomBody.isDirect.orFalse(),
+                            membership = Membership.INVITE,
+                            displayName = it.displayName,
+                            avatarUrl = it.avatarUrl
+                    ).toContent(),
+                    stateKey = it.userId
+            )
+        }
+
+        return buildList {
+            add(createRoomEvent)
+            add(myRoomMemberEvent)
+            addAll(createRoomBody.initialStates.orEmpty().map { createFakeEvent(it.type, it.content, it.stateKey) })
+            addAll(roomMemberEvents)
+        }
+    }
+
+    /**
+     * Generate a local event from the given parameters.
+     *
+     * @param type the event type, see [EventType]
+     * @param content the content of the Event
+     * @param stateKey the stateKey, if any
+     *
+     * @return a fake event
+     */
+    private fun createFakeEvent(type: String?, content: Content?, stateKey: String? = ""): Event {
+        return Event(
+                type = type,
+                senderId = userId,
+                stateKey = stateKey,
+                content = content,
+                originServerTs = clock.epochMillis(),
+                eventId = UUID.randomUUID().toString()
+        )
+    }
+
+    /**
+     * Setup default values to the CreateRoomParams as the room is created locally (the default values will not be defined by the server).
+     */
+    private fun CreateRoomParams.withDefault() = this.apply {
+        if (visibility == null) visibility = RoomDirectoryVisibility.PRIVATE
+        if (historyVisibility == null) historyVisibility = RoomHistoryVisibility.SHARED
+        if (guestAccess == null) guestAccess = GuestAccess.Forbidden
+    }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateRoomBody.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateRoomBody.kt
index cffa632768..b326c3618c 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateRoomBody.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateRoomBody.kt
@@ -120,3 +120,20 @@ internal data class CreateRoomBody(
         @Json(name = "room_version")
         val roomVersion: String?
 )
+
+/**
+ * Tells if the created room can be a direct chat one.
+ *
+ * @return true if it is a direct chat
+ */
+private fun CreateRoomBody.isDirect(): Boolean {
+    return preset == CreateRoomPreset.PRESET_TRUSTED_PRIVATE_CHAT && isDirect == true
+}
+
+internal fun CreateRoomBody.getDirectUserId(): String? {
+    return if (isDirect()) {
+        invitedUserIds?.firstOrNull()
+                ?: invite3pids?.firstOrNull()?.address
+                ?: throw IllegalStateException("You can't create a direct room without an invitedUser")
+    } else null
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateRoomTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateRoomTask.kt
index 6dd2c91048..d76640573f 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateRoomTask.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateRoomTask.kt
@@ -62,11 +62,6 @@ internal class DefaultCreateRoomTask @Inject constructor(
 ) : CreateRoomTask {
 
     override suspend fun execute(params: CreateRoomParams): String {
-        val otherUserId = if (params.isDirect()) {
-            params.getFirstInvitedUserId()
-                    ?: throw IllegalStateException("You can't create a direct room without an invitedUser")
-        } else null
-
         if (params.preset == CreateRoomPreset.PRESET_PUBLIC_CHAT) {
             try {
                 aliasAvailabilityChecker.check(params.roomAliasName)
@@ -111,14 +106,13 @@ internal class DefaultCreateRoomTask @Inject constructor(
             RoomSummaryEntity.where(it, roomId).findFirst()?.lastActivityTime = clock.epochMillis()
         }
 
-        if (otherUserId != null) {
-            handleDirectChatCreation(roomId, otherUserId)
-        }
+        handleDirectChatCreation(roomId, createRoomBody.getDirectUserId())
         setReadMarkers(roomId)
         return roomId
     }
 
-    private suspend fun handleDirectChatCreation(roomId: String, otherUserId: String) {
+    private suspend fun handleDirectChatCreation(roomId: String, otherUserId: String?) {
+        otherUserId ?: return // This is not a direct room
         monarchy.awaitTransaction { realm ->
             RoomSummaryEntity.where(realm, roomId).findFirst()?.apply {
                 this.directUserId = otherUserId
@@ -133,21 +127,4 @@ internal class DefaultCreateRoomTask @Inject constructor(
         val setReadMarkerParams = SetReadMarkersTask.Params(roomId, forceReadReceipt = true, forceReadMarker = true)
         return readMarkersTask.execute(setReadMarkerParams)
     }
-
-    /**
-     * Tells if the created room can be a direct chat one.
-     *
-     * @return true if it is a direct chat
-     */
-    private fun CreateRoomParams.isDirect(): Boolean {
-        return preset == CreateRoomPreset.PRESET_TRUSTED_PRIVATE_CHAT &&
-                isDirect == true
-    }
-
-    /**
-     * @return the first invited user id
-     */
-    private fun CreateRoomParams.getFirstInvitedUserId(): String? {
-        return invitedUserIds.firstOrNull() ?: invite3pids.firstOrNull()?.value
-    }
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt
index 44a786a95d..05379a1a7b 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt
@@ -32,6 +32,7 @@ import kotlinx.coroutines.withContext
 import okhttp3.internal.closeQuietly
 import org.matrix.android.sdk.api.MatrixCoroutineDispatchers
 import org.matrix.android.sdk.api.extensions.tryOrNull
+import org.matrix.android.sdk.api.session.room.model.localecho.RoomLocalEcho
 import org.matrix.android.sdk.api.session.room.timeline.Timeline
 import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
 import org.matrix.android.sdk.api.session.room.timeline.TimelineSettings
@@ -383,6 +384,7 @@ internal class DefaultTimeline(
     }
 
     private suspend fun loadRoomMembersIfNeeded() {
+        if (RoomLocalEcho.isLocalEchoId(roomId)) return
         val loadRoomMembersParam = LoadRoomMembersTask.Params(roomId)
         try {
             loadRoomMembersTask.execute(loadRoomMembersParam)
diff --git a/vector/src/main/java/im/vector/app/features/createdirect/CreateDirectRoomAction.kt b/vector/src/main/java/im/vector/app/features/createdirect/CreateDirectRoomAction.kt
index 83c7f0a13b..b5657598ee 100644
--- a/vector/src/main/java/im/vector/app/features/createdirect/CreateDirectRoomAction.kt
+++ b/vector/src/main/java/im/vector/app/features/createdirect/CreateDirectRoomAction.kt
@@ -20,10 +20,12 @@ import im.vector.app.core.platform.VectorViewModelAction
 import im.vector.app.features.userdirectory.PendingSelection
 
 sealed class CreateDirectRoomAction : VectorViewModelAction {
-    data class CreateRoomAndInviteSelectedUsers(
+    data class PrepareRoomWithSelectedUsers(
             val selections: Set<PendingSelection>
     ) : CreateDirectRoomAction()
 
+    object CreateRoomAndInviteSelectedUsers : CreateDirectRoomAction()
+
     data class QrScannedAction(
             val result: String
     ) : CreateDirectRoomAction()
diff --git a/vector/src/main/java/im/vector/app/features/createdirect/CreateDirectRoomActivity.kt b/vector/src/main/java/im/vector/app/features/createdirect/CreateDirectRoomActivity.kt
index 6292217b67..377acd2271 100644
--- a/vector/src/main/java/im/vector/app/features/createdirect/CreateDirectRoomActivity.kt
+++ b/vector/src/main/java/im/vector/app/features/createdirect/CreateDirectRoomActivity.kt
@@ -161,7 +161,7 @@ class CreateDirectRoomActivity : SimpleFragmentActivity() {
 
     private fun onMenuItemSelected(action: UserListSharedAction.OnMenuItemSelected) {
         if (action.itemId == R.id.action_create_direct_room) {
-            viewModel.handle(CreateDirectRoomAction.CreateRoomAndInviteSelectedUsers(action.selections))
+            viewModel.handle(CreateDirectRoomAction.PrepareRoomWithSelectedUsers(action.selections))
         }
     }
 
diff --git a/vector/src/main/java/im/vector/app/features/createdirect/CreateDirectRoomViewModel.kt b/vector/src/main/java/im/vector/app/features/createdirect/CreateDirectRoomViewModel.kt
index 5574ce3e63..b306cb6e03 100644
--- a/vector/src/main/java/im/vector/app/features/createdirect/CreateDirectRoomViewModel.kt
+++ b/vector/src/main/java/im/vector/app/features/createdirect/CreateDirectRoomViewModel.kt
@@ -61,7 +61,8 @@ class CreateDirectRoomViewModel @AssistedInject constructor(
 
     override fun handle(action: CreateDirectRoomAction) {
         when (action) {
-            is CreateDirectRoomAction.CreateRoomAndInviteSelectedUsers -> onSubmitInvitees(action.selections)
+            is CreateDirectRoomAction.PrepareRoomWithSelectedUsers -> onSubmitInvitees(action.selections)
+            is CreateDirectRoomAction.CreateRoomAndInviteSelectedUsers -> onCreateRoomWithInvitees()
             is CreateDirectRoomAction.QrScannedAction -> onCodeParsed(action)
         }
     }
@@ -96,16 +97,18 @@ class CreateDirectRoomViewModel @AssistedInject constructor(
         }
         if (existingRoomId != null) {
             // Do not create a new DM, just tell that the creation is successful by passing the existing roomId
-            setState {
-                copy(createAndInviteState = Success(existingRoomId))
-            }
+            setState { copy(createAndInviteState = Success(existingRoomId)) }
         } else {
-            // Create the DM
-            createRoomAndInviteSelectedUsers(selections)
+            createLocalRoomWithSelectedUsers(selections)
         }
     }
 
-    private fun createRoomAndInviteSelectedUsers(selections: Set<PendingSelection>) {
+    private fun onCreateRoomWithInvitees() {
+        // Create the DM
+        withState { createLocalRoomWithSelectedUsers(it.pendingSelections) }
+    }
+
+    private fun createLocalRoomWithSelectedUsers(selections: Set<PendingSelection>) {
         setState { copy(createAndInviteState = Loading()) }
 
         viewModelScope.launch(Dispatchers.IO) {
@@ -127,8 +130,7 @@ class CreateDirectRoomViewModel @AssistedInject constructor(
 
             val result = runCatchingToAsync {
                 if (vectorFeatures.shouldStartDmOnFirstMessage()) {
-                    // Todo: Prepare direct room creation
-                    throw Throwable("Start DM on first message is not implemented yet.")
+                    session.roomService().createLocalRoom(roomParams)
                 } else {
                     session.roomService().createRoom(roomParams)
                 }
diff --git a/vector/src/main/java/im/vector/app/features/createdirect/CreateDirectRoomViewState.kt b/vector/src/main/java/im/vector/app/features/createdirect/CreateDirectRoomViewState.kt
index 41366a7110..33360ac20c 100644
--- a/vector/src/main/java/im/vector/app/features/createdirect/CreateDirectRoomViewState.kt
+++ b/vector/src/main/java/im/vector/app/features/createdirect/CreateDirectRoomViewState.kt
@@ -19,7 +19,9 @@ package im.vector.app.features.createdirect
 import com.airbnb.mvrx.Async
 import com.airbnb.mvrx.MavericksState
 import com.airbnb.mvrx.Uninitialized
+import im.vector.app.features.userdirectory.PendingSelection
 
 data class CreateDirectRoomViewState(
+        val pendingSelections: Set<PendingSelection> = emptySet(),
         val createAndInviteState: Async<String> = Uninitialized
 ) : MavericksState

From 7ea2d0a86da9da63a9bab8ce8d33a66416649e94 Mon Sep 17 00:00:00 2001
From: Florian Renaud <florianr@element.io>
Date: Thu, 12 May 2022 11:32:31 +0200
Subject: [PATCH 05/26] Delete the local rooms when the room list is shown

---
 .../sdk/api/session/room/RoomService.kt       |  5 ++
 .../query/CurrentStateEventEntityQueries.kt   | 11 ++-
 .../session/room/DefaultRoomService.kt        |  6 ++
 .../sdk/internal/session/room/RoomModule.kt   |  5 ++
 .../room/delete/DeleteLocalRoomTask.kt        | 77 +++++++++++++++++++
 .../home/room/detail/RoomDetailViewState.kt   |  3 +
 .../home/room/detail/TimelineFragment.kt      | 61 +++++++++------
 .../home/room/detail/TimelineViewModel.kt     | 42 +++++-----
 .../home/room/list/RoomListFragment.kt        |  4 +
 .../home/room/list/RoomListViewModel.kt       | 29 +++++++
 .../home/room/list/RoomListViewState.kt       |  3 +-
 11 files changed, 201 insertions(+), 45 deletions(-)
 create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/delete/DeleteLocalRoomTask.kt

diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/RoomService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/RoomService.kt
index 41a215636d..90f3f323b2 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/RoomService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/RoomService.kt
@@ -47,6 +47,11 @@ interface RoomService {
      */
     suspend fun createLocalRoom(createRoomParams: CreateRoomParams): String
 
+    /**
+     * Delete a local room with all its related events.
+     */
+    suspend fun deleteLocalRoom(roomId: String)
+
     /**
      * Create a direct room asynchronously. This is a facility method to create a direct room with the necessary parameters.
      */
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/CurrentStateEventEntityQueries.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/CurrentStateEventEntityQueries.kt
index e0dbf2eee8..e17d07c584 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/CurrentStateEventEntityQueries.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/CurrentStateEventEntityQueries.kt
@@ -23,13 +23,20 @@ import io.realm.kotlin.createObject
 import org.matrix.android.sdk.internal.database.model.CurrentStateEventEntity
 import org.matrix.android.sdk.internal.database.model.CurrentStateEventEntityFields
 
+internal fun CurrentStateEventEntity.Companion.whereRoomId(
+        realm: Realm,
+        roomId: String
+): RealmQuery<CurrentStateEventEntity> {
+    return realm.where(CurrentStateEventEntity::class.java)
+            .equalTo(CurrentStateEventEntityFields.ROOM_ID, roomId)
+}
+
 internal fun CurrentStateEventEntity.Companion.whereType(
         realm: Realm,
         roomId: String,
         type: String
 ): RealmQuery<CurrentStateEventEntity> {
-    return realm.where(CurrentStateEventEntity::class.java)
-            .equalTo(CurrentStateEventEntityFields.ROOM_ID, roomId)
+    return whereRoomId(realm = realm, roomId = roomId)
             .equalTo(CurrentStateEventEntityFields.TYPE, type)
 }
 
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoomService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoomService.kt
index cc5e935f87..6fd4f752a8 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoomService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoomService.kt
@@ -45,6 +45,7 @@ import org.matrix.android.sdk.internal.session.room.alias.DeleteRoomAliasTask
 import org.matrix.android.sdk.internal.session.room.alias.GetRoomIdByAliasTask
 import org.matrix.android.sdk.internal.session.room.create.CreateLocalRoomTask
 import org.matrix.android.sdk.internal.session.room.create.CreateRoomTask
+import org.matrix.android.sdk.internal.session.room.delete.DeleteLocalRoomTask
 import org.matrix.android.sdk.internal.session.room.membership.RoomChangeMembershipStateDataSource
 import org.matrix.android.sdk.internal.session.room.membership.RoomMemberHelper
 import org.matrix.android.sdk.internal.session.room.membership.joining.JoinRoomTask
@@ -62,6 +63,7 @@ internal class DefaultRoomService @Inject constructor(
         @SessionDatabase private val monarchy: Monarchy,
         private val createRoomTask: CreateRoomTask,
         private val createLocalRoomTask: CreateLocalRoomTask,
+        private val deleteLocalRoomTask: DeleteLocalRoomTask,
         private val joinRoomTask: JoinRoomTask,
         private val markAllRoomsReadTask: MarkAllRoomsReadTask,
         private val updateBreadcrumbsTask: UpdateBreadcrumbsTask,
@@ -84,6 +86,10 @@ internal class DefaultRoomService @Inject constructor(
         return createLocalRoomTask.execute(createRoomParams)
     }
 
+    override suspend fun deleteLocalRoom(roomId: String) {
+        deleteLocalRoomTask.execute(DeleteLocalRoomTask.Params(roomId))
+    }
+
     override fun getRoom(roomId: String): Room? {
         return roomGetter.getRoom(roomId)
     }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomModule.kt
index bb92287ead..556e31356e 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomModule.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomModule.kt
@@ -47,6 +47,8 @@ import org.matrix.android.sdk.internal.session.room.create.CreateLocalRoomTask
 import org.matrix.android.sdk.internal.session.room.create.CreateRoomTask
 import org.matrix.android.sdk.internal.session.room.create.DefaultCreateLocalRoomTask
 import org.matrix.android.sdk.internal.session.room.create.DefaultCreateRoomTask
+import org.matrix.android.sdk.internal.session.room.delete.DefaultDeleteLocalRoomTask
+import org.matrix.android.sdk.internal.session.room.delete.DeleteLocalRoomTask
 import org.matrix.android.sdk.internal.session.room.directory.DefaultGetPublicRoomTask
 import org.matrix.android.sdk.internal.session.room.directory.DefaultGetRoomDirectoryVisibilityTask
 import org.matrix.android.sdk.internal.session.room.directory.DefaultSetRoomDirectoryVisibilityTask
@@ -197,6 +199,9 @@ internal abstract class RoomModule {
     @Binds
     abstract fun bindCreateLocalRoomTask(task: DefaultCreateLocalRoomTask): CreateLocalRoomTask
 
+    @Binds
+    abstract fun bindDeleteLocalRoomTask(task: DefaultDeleteLocalRoomTask): DeleteLocalRoomTask
+
     @Binds
     abstract fun bindGetPublicRoomTask(task: DefaultGetPublicRoomTask): GetPublicRoomTask
 
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/delete/DeleteLocalRoomTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/delete/DeleteLocalRoomTask.kt
new file mode 100644
index 0000000000..689fd24a97
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/delete/DeleteLocalRoomTask.kt
@@ -0,0 +1,77 @@
+/*
+ * Copyright 2022 The Matrix.org Foundation C.I.C.
+ *
+ * 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 org.matrix.android.sdk.internal.session.room.delete
+
+import com.zhuinden.monarchy.Monarchy
+import org.matrix.android.sdk.api.session.room.model.localecho.RoomLocalEcho
+import org.matrix.android.sdk.internal.database.model.ChunkEntity
+import org.matrix.android.sdk.internal.database.model.CurrentStateEventEntity
+import org.matrix.android.sdk.internal.database.model.EventEntity
+import org.matrix.android.sdk.internal.database.model.RoomEntity
+import org.matrix.android.sdk.internal.database.model.RoomMemberSummaryEntity
+import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity
+import org.matrix.android.sdk.internal.database.model.TimelineEventEntity
+import org.matrix.android.sdk.internal.database.query.where
+import org.matrix.android.sdk.internal.database.query.whereRoomId
+import org.matrix.android.sdk.internal.di.SessionDatabase
+import org.matrix.android.sdk.internal.session.room.delete.DeleteLocalRoomTask.Params
+import org.matrix.android.sdk.internal.task.Task
+import org.matrix.android.sdk.internal.util.awaitTransaction
+import timber.log.Timber
+import javax.inject.Inject
+
+internal interface DeleteLocalRoomTask : Task<Params, Unit> {
+    data class Params(val roomId: String)
+}
+
+internal class DefaultDeleteLocalRoomTask @Inject constructor(
+        @SessionDatabase private val monarchy: Monarchy,
+) : DeleteLocalRoomTask {
+
+    override suspend fun execute(params: Params) {
+        val roomId = params.roomId
+
+        if (RoomLocalEcho.isLocalEchoId(roomId)) {
+            monarchy.awaitTransaction { realm ->
+                Timber.i("## DeleteLocalRoomTask - delete local room id $roomId")
+                RoomMemberSummaryEntity.where(realm, roomId = roomId).findAll()
+                        ?.also { Timber.i("## DeleteLocalRoomTask - RoomMemberSummaryEntity - delete ${it.size} entries") }
+                        ?.deleteAllFromRealm()
+                CurrentStateEventEntity.whereRoomId(realm, roomId = roomId).findAll()
+                        ?.also { Timber.i("## DeleteLocalRoomTask - CurrentStateEventEntity - delete ${it.size} entries") }
+                        ?.deleteAllFromRealm()
+                EventEntity.whereRoomId(realm, roomId = roomId).findAll()
+                        ?.also { Timber.i("## DeleteLocalRoomTask - EventEntity - delete ${it.size} entries") }
+                        ?.deleteAllFromRealm()
+                TimelineEventEntity.whereRoomId(realm, roomId = roomId).findAll()
+                        ?.also { Timber.i("## DeleteLocalRoomTask - TimelineEventEntity - delete ${it.size} entries") }
+                        ?.deleteAllFromRealm()
+                ChunkEntity.where(realm, roomId = roomId).findAll()
+                        ?.also { Timber.i("## DeleteLocalRoomTask - RoomMemberSummaryEntity - delete ${it.size} entries") }
+                        ?.deleteAllFromRealm()
+                RoomSummaryEntity.where(realm, roomId = roomId).findAll()
+                        ?.also { Timber.i("## DeleteLocalRoomTask - RoomSummaryEntity - delete ${it.size} entries") }
+                        ?.deleteAllFromRealm()
+                RoomEntity.where(realm, roomId = roomId).findAll()
+                        ?.also { Timber.i("## DeleteLocalRoomTask - RoomEntity - delete ${it.size} entries") }
+                        ?.deleteAllFromRealm()
+            }
+        } else {
+            Timber.i("## DeleteLocalRoomTask - Failed to remove room id $roomId: not a local room")
+        }
+    }
+}
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewState.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewState.kt
index 47db50d0d4..3ec7166739 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewState.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewState.kt
@@ -25,6 +25,7 @@ import org.matrix.android.sdk.api.session.events.model.Event
 import org.matrix.android.sdk.api.session.room.members.ChangeMembershipState
 import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary
 import org.matrix.android.sdk.api.session.room.model.RoomSummary
+import org.matrix.android.sdk.api.session.room.model.localecho.RoomLocalEcho
 import org.matrix.android.sdk.api.session.room.sender.SenderInfo
 import org.matrix.android.sdk.api.session.sync.SyncRequestState
 import org.matrix.android.sdk.api.session.sync.SyncState
@@ -103,4 +104,6 @@ data class RoomDetailViewState(
     fun isDm() = asyncRoomSummary()?.isDirect == true
 
     fun isThreadTimeline() = rootThreadEventId != null
+
+    fun isLocalRoom() = RoomLocalEcho.isLocalEchoId(roomId)
 }
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt
index 2b99bc67ef..0e818acec3 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt
@@ -1695,31 +1695,41 @@ class TimelineFragment @Inject constructor(
     }
 
     private fun renderToolbar(roomSummary: RoomSummary?) {
-        if (!isThreadTimeLine()) {
-            views.includeRoomToolbar.roomToolbarContentView.isVisible = true
-            views.includeThreadToolbar.roomToolbarThreadConstraintLayout.isVisible = false
-            if (roomSummary == null) {
-                views.includeRoomToolbar.roomToolbarContentView.isClickable = false
-            } else {
-                views.includeRoomToolbar.roomToolbarContentView.isClickable = roomSummary.membership == Membership.JOIN
-                views.includeRoomToolbar.roomToolbarTitleView.text = roomSummary.displayName
-                avatarRenderer.render(roomSummary.toMatrixItem(), views.includeRoomToolbar.roomToolbarAvatarImageView)
-                val showPresence = roomSummary.isDirect
-                views.includeRoomToolbar.roomToolbarPresenceImageView.render(showPresence, roomSummary.directUserPresence)
-                val shieldView = if (showPresence) views.includeRoomToolbar.roomToolbarTitleShield else views.includeRoomToolbar.roomToolbarAvatarShield
-                shieldView.render(roomSummary.roomEncryptionTrustLevel)
-                views.includeRoomToolbar.roomToolbarPublicImageView.isVisible = roomSummary.isPublic && !roomSummary.isDirect
+        when {
+            isLocalRoom() -> {
+                views.includeRoomToolbar.roomToolbarContentView.isVisible = false
+                views.includeThreadToolbar.roomToolbarThreadConstraintLayout.isVisible = false
+                setupToolbar(views.roomToolbar)
+                        .setTitle(R.string.fab_menu_create_chat)
+                        .allowBack(useCross = true)
             }
-        } else {
-            views.includeRoomToolbar.roomToolbarContentView.isVisible = false
-            views.includeThreadToolbar.roomToolbarThreadConstraintLayout.isVisible = true
-            timelineArgs.threadTimelineArgs?.let {
-                val matrixItem = MatrixItem.RoomItem(it.roomId, it.displayName, it.avatarUrl)
-                avatarRenderer.render(matrixItem, views.includeThreadToolbar.roomToolbarThreadImageView)
-                views.includeThreadToolbar.roomToolbarThreadShieldImageView.render(it.roomEncryptionTrustLevel)
-                views.includeThreadToolbar.roomToolbarThreadSubtitleTextView.text = it.displayName
+            isThreadTimeLine()                -> {
+                views.includeRoomToolbar.roomToolbarContentView.isVisible = false
+                views.includeThreadToolbar.roomToolbarThreadConstraintLayout.isVisible = true
+                timelineArgs.threadTimelineArgs?.let {
+                    val matrixItem = MatrixItem.RoomItem(it.roomId, it.displayName, it.avatarUrl)
+                    avatarRenderer.render(matrixItem, views.includeThreadToolbar.roomToolbarThreadImageView)
+                    views.includeThreadToolbar.roomToolbarThreadShieldImageView.render(it.roomEncryptionTrustLevel)
+                    views.includeThreadToolbar.roomToolbarThreadSubtitleTextView.text = it.displayName
+                }
+                views.includeThreadToolbar.roomToolbarThreadTitleTextView.text = resources.getText(R.string.thread_timeline_title)
+            }
+            else -> {
+                views.includeRoomToolbar.roomToolbarContentView.isVisible = true
+                views.includeThreadToolbar.roomToolbarThreadConstraintLayout.isVisible = false
+                if (roomSummary == null) {
+                    views.includeRoomToolbar.roomToolbarContentView.isClickable = false
+                } else {
+                    views.includeRoomToolbar.roomToolbarContentView.isClickable = roomSummary.membership == Membership.JOIN
+                    views.includeRoomToolbar.roomToolbarTitleView.text = roomSummary.displayName
+                    avatarRenderer.render(roomSummary.toMatrixItem(), views.includeRoomToolbar.roomToolbarAvatarImageView)
+                    val showPresence = roomSummary.isDirect
+                    views.includeRoomToolbar.roomToolbarPresenceImageView.render(showPresence, roomSummary.directUserPresence)
+                    val shieldView = if (showPresence) views.includeRoomToolbar.roomToolbarTitleShield else views.includeRoomToolbar.roomToolbarAvatarShield
+                    shieldView.render(roomSummary.roomEncryptionTrustLevel)
+                    views.includeRoomToolbar.roomToolbarPublicImageView.isVisible = roomSummary.isPublic && !roomSummary.isDirect
+                }
             }
-            views.includeThreadToolbar.roomToolbarThreadTitleTextView.text = resources.getText(R.string.thread_timeline_title)
         }
     }
 
@@ -2658,6 +2668,11 @@ class TimelineFragment @Inject constructor(
      */
     private fun isThreadTimeLine(): Boolean = timelineArgs.threadTimelineArgs?.rootThreadEventId != null
 
+    /**
+     * Returns true if the current room is a local room, false otherwise.
+     */
+    private fun isLocalRoom(): Boolean = withState(timelineViewModel) { it.isLocalRoom() }
+
     /**
      * Returns the root thread event if we are in a thread room, otherwise returns null.
      */
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt
index 07b20b4914..6ad356e82d 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt
@@ -732,26 +732,30 @@ class TimelineViewModel @AssistedInject constructor(
             return@withState false
         }
 
-        if (initialState.isThreadTimeline()) {
-            when (itemId) {
-                R.id.menu_thread_timeline_view_in_room,
-                R.id.menu_thread_timeline_copy_link,
-                R.id.menu_thread_timeline_share -> true
-                else -> false
+        when {
+            initialState.isLocalRoom() -> false
+            initialState.isThreadTimeline() -> {
+                when (itemId) {
+                    R.id.menu_thread_timeline_view_in_room,
+                    R.id.menu_thread_timeline_copy_link,
+                    R.id.menu_thread_timeline_share -> true
+                    else -> false
+                }
             }
-        } else {
-            when (itemId) {
-                R.id.timeline_setting -> true
-                R.id.invite -> state.canInvite
-                R.id.open_matrix_apps -> true
-                R.id.voice_call -> state.isCallOptionAvailable()
-                R.id.video_call -> state.isCallOptionAvailable() || state.jitsiState.confId == null || state.jitsiState.hasJoined
-                // Show Join conference button only if there is an active conf id not joined. Otherwise fallback to default video disabled. ^
-                R.id.join_conference -> !state.isCallOptionAvailable() && state.jitsiState.confId != null && !state.jitsiState.hasJoined
-                R.id.search -> state.isSearchAvailable()
-                R.id.menu_timeline_thread_list -> vectorPreferences.areThreadMessagesEnabled()
-                R.id.dev_tools -> vectorPreferences.developerMode()
-                else -> false
+            else -> {
+                when (itemId) {
+                    R.id.timeline_setting -> true
+                    R.id.invite -> state.canInvite
+                    R.id.open_matrix_apps -> true
+                    R.id.voice_call -> state.isCallOptionAvailable()
+                    R.id.video_call -> state.isCallOptionAvailable() || state.jitsiState.confId == null || state.jitsiState.hasJoined
+                    // Show Join conference button only if there is an active conf id not joined. Otherwise fallback to default video disabled. ^
+                    R.id.join_conference -> !state.isCallOptionAvailable() && state.jitsiState.confId != null && !state.jitsiState.hasJoined
+                    R.id.search -> state.isSearchAvailable()
+                    R.id.menu_timeline_thread_list -> vectorPreferences.areThreadMessagesEnabled()
+                    R.id.dev_tools -> vectorPreferences.developerMode()
+                    else -> false
+                }
             }
         }
     }
diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/RoomListFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/list/RoomListFragment.kt
index 5539986118..c25fe546c3 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/list/RoomListFragment.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/list/RoomListFragment.kt
@@ -146,6 +146,10 @@ class RoomListFragment @Inject constructor(
                         (it.contentEpoxyController as? RoomSummaryPagedController)?.roomChangeMembershipStates = ms
                     }
         }
+        roomListViewModel.onEach(RoomListViewState::localRoomIds) {
+            // Local rooms should not exist anymore when the room list is shown
+            roomListViewModel.deleteLocalRooms(it)
+        }
     }
 
     private fun refreshCollapseStates() {
diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/RoomListViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/list/RoomListViewModel.kt
index 49467c0531..cd4bb06949 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/list/RoomListViewModel.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/list/RoomListViewModel.kt
@@ -48,7 +48,10 @@ import org.matrix.android.sdk.api.session.getRoom
 import org.matrix.android.sdk.api.session.getRoomSummary
 import org.matrix.android.sdk.api.session.room.UpdatableLivePageResult
 import org.matrix.android.sdk.api.session.room.members.ChangeMembershipState
+import org.matrix.android.sdk.api.session.room.model.Membership
+import org.matrix.android.sdk.api.session.room.model.localecho.RoomLocalEcho
 import org.matrix.android.sdk.api.session.room.model.tag.RoomTag
+import org.matrix.android.sdk.api.session.room.roomSummaryQueryParams
 import org.matrix.android.sdk.api.session.room.state.isPublic
 import org.matrix.android.sdk.api.util.toMatrixItem
 import org.matrix.android.sdk.flow.flow
@@ -96,6 +99,7 @@ class RoomListViewModel @AssistedInject constructor(
 
     init {
         observeMembershipChanges()
+        observeLocalRooms()
 
         appStateHandler.selectedRoomGroupingFlow
                 .distinctUntilChanged()
@@ -123,6 +127,23 @@ class RoomListViewModel @AssistedInject constructor(
                 }
     }
 
+    private fun observeLocalRooms() {
+        val queryParams = roomSummaryQueryParams {
+            memberships = listOf(Membership.JOIN)
+        }
+        session
+                .flow()
+                .liveRoomSummaries(queryParams)
+                .map { roomSummaries ->
+                    roomSummaries.mapNotNull { summary ->
+                        summary.roomId.takeIf { RoomLocalEcho.isLocalEchoId(it) }
+                    }.toSet()
+                }
+                .setOnEach { roomIds ->
+                    copy(localRoomIds = roomIds)
+                }
+    }
+
     companion object : MavericksViewModelFactory<RoomListViewModel, RoomListViewState> by hiltMavericksViewModelFactory()
 
     private val roomListSectionBuilder = if (appStateHandler.getCurrentRoomGroupingMethod() is RoomGroupingMethod.BySpace) {
@@ -173,6 +194,14 @@ class RoomListViewModel @AssistedInject constructor(
         return session.getRoom(roomId)?.stateService()?.isPublic().orFalse()
     }
 
+    fun deleteLocalRooms(roomsIds: Set<String>) {
+        viewModelScope.launch {
+            roomsIds.forEach {
+                session.roomService().deleteLocalRoom(it)
+            }
+        }
+    }
+
     // PRIVATE METHODS *****************************************************************************
 
     private fun handleSelectRoom(action: RoomListAction.SelectRoom) = withState {
diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/RoomListViewState.kt b/vector/src/main/java/im/vector/app/features/home/room/list/RoomListViewState.kt
index 46ff6c242b..9d386b679a 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/list/RoomListViewState.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/list/RoomListViewState.kt
@@ -30,7 +30,8 @@ data class RoomListViewState(
         val roomMembershipChanges: Map<String, ChangeMembershipState> = emptyMap(),
         val asyncSuggestedRooms: Async<List<SpaceChildInfo>> = Uninitialized,
         val currentUserName: String? = null,
-        val currentRoomGrouping: Async<RoomGroupingMethod> = Uninitialized
+        val currentRoomGrouping: Async<RoomGroupingMethod> = Uninitialized,
+        val localRoomIds: Set<String> = emptySet()
 ) : MavericksState {
 
     constructor(args: RoomListParams) : this(displayMode = args.displayMode)

From e86f9193dd61c08c80dffb8aea354bbeba2a1029 Mon Sep 17 00:00:00 2001
From: Florian Renaud <florianr@element.io>
Date: Fri, 13 May 2022 18:09:39 +0200
Subject: [PATCH 06/26] Display timeline from the top of the screen for local
 rooms

---
 .../app/features/home/room/detail/TimelineFragment.kt    | 9 ++++++---
 1 file changed, 6 insertions(+), 3 deletions(-)

diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt
index 0e818acec3..a3d342ef48 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt
@@ -1426,6 +1426,9 @@ class TimelineFragment @Inject constructor(
                 updateJumpToReadMarkerViewVisibility()
                 jumpToBottomViewVisibilityManager.maybeShowJumpToBottomViewVisibilityWithDelay()
             }
+        }.apply {
+            // For local rooms, pin the view's content to the top edge (the layout is reversed)
+            stackFromEnd = isLocalRoom()
         }
         val stateRestorer = LayoutManagerStateRestorer(layoutManager).register()
         scrollOnNewMessageCallback = ScrollOnNewMessageCallback(layoutManager, timelineEventController)
@@ -1696,14 +1699,14 @@ class TimelineFragment @Inject constructor(
 
     private fun renderToolbar(roomSummary: RoomSummary?) {
         when {
-            isLocalRoom() -> {
+            isLocalRoom()      -> {
                 views.includeRoomToolbar.roomToolbarContentView.isVisible = false
                 views.includeThreadToolbar.roomToolbarThreadConstraintLayout.isVisible = false
                 setupToolbar(views.roomToolbar)
                         .setTitle(R.string.fab_menu_create_chat)
                         .allowBack(useCross = true)
             }
-            isThreadTimeLine()                -> {
+            isThreadTimeLine() -> {
                 views.includeRoomToolbar.roomToolbarContentView.isVisible = false
                 views.includeThreadToolbar.roomToolbarThreadConstraintLayout.isVisible = true
                 timelineArgs.threadTimelineArgs?.let {
@@ -1714,7 +1717,7 @@ class TimelineFragment @Inject constructor(
                 }
                 views.includeThreadToolbar.roomToolbarThreadTitleTextView.text = resources.getText(R.string.thread_timeline_title)
             }
-            else -> {
+            else               -> {
                 views.includeRoomToolbar.roomToolbarContentView.isVisible = true
                 views.includeThreadToolbar.roomToolbarThreadConstraintLayout.isVisible = false
                 if (roomSummary == null) {

From b144bac578c721e2593d6f02556657d7fad7503c Mon Sep 17 00:00:00 2001
From: Florian Renaud <florianr@element.io>
Date: Fri, 13 May 2022 13:56:56 +0200
Subject: [PATCH 07/26] Update wordings for local room timeline

---
 .../timeline/factory/EncryptionItemFactory.kt | 16 +++++--
 .../factory/MergedHeaderItemFactory.kt        |  2 +
 .../detail/timeline/item/BasedMergedItem.kt   |  1 +
 .../timeline/item/MergedRoomCreationItem.kt   | 45 +++++++++++++------
 vector/src/main/res/values/strings.xml        |  4 +-
 5 files changed, 50 insertions(+), 18 deletions(-)

diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/EncryptionItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/EncryptionItemFactory.kt
index dd058197b4..5f32696334 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/EncryptionItemFactory.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/EncryptionItemFactory.kt
@@ -30,6 +30,7 @@ import org.matrix.android.sdk.api.session.Session
 import org.matrix.android.sdk.api.session.events.model.content.EncryptionEventContent
 import org.matrix.android.sdk.api.session.events.model.toModel
 import org.matrix.android.sdk.api.session.getRoomSummary
+import org.matrix.android.sdk.api.session.room.model.localecho.RoomLocalEcho
 import javax.inject.Inject
 
 class EncryptionItemFactory @Inject constructor(
@@ -55,12 +56,19 @@ class EncryptionItemFactory @Inject constructor(
         val description: String
         val shield: StatusTileTimelineItem.ShieldUIState
         if (isSafeAlgorithm) {
+            val isDirect = session.getRoomSummary(event.root.roomId.orEmpty())?.isDirect.orFalse()
             title = stringProvider.getString(R.string.encryption_enabled)
             description = stringProvider.getString(
-                    if (session.getRoomSummary(event.root.roomId ?: "")?.isDirect.orFalse()) {
-                        R.string.direct_room_encryption_enabled_tile_description
-                    } else {
-                        R.string.encryption_enabled_tile_description
+                    when {
+                        isDirect && RoomLocalEcho.isLocalEchoId(event.root.roomId.orEmpty()) -> {
+                            R.string.direct_room_encryption_enabled_tile_description_future
+                        }
+                        isDirect                                                             -> {
+                            R.string.direct_room_encryption_enabled_tile_description
+                        }
+                        else                                                                 -> {
+                            R.string.encryption_enabled_tile_description
+                        }
                     }
             )
             shield = StatusTileTimelineItem.ShieldUIState.BLACK
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MergedHeaderItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MergedHeaderItemFactory.kt
index 771e42b63c..800fc27e77 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MergedHeaderItemFactory.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MergedHeaderItemFactory.kt
@@ -117,6 +117,7 @@ class MergedHeaderItemFactory @Inject constructor(
                     highlighted = true
                 }
                 val data = BasedMergedItem.Data(
+                        roomId = mergedEvent.root.roomId,
                         userId = mergedEvent.root.senderId ?: "",
                         avatarUrl = mergedEvent.senderInfo.avatarUrl,
                         memberName = mergedEvent.senderInfo.disambiguatedDisplayName,
@@ -199,6 +200,7 @@ class MergedHeaderItemFactory @Inject constructor(
                             highlighted = true
                         }
                         val data = BasedMergedItem.Data(
+                                roomId = mergedEvent.root.roomId,
                                 userId = mergedEvent.root.senderId ?: "",
                                 avatarUrl = mergedEvent.senderInfo.avatarUrl,
                                 memberName = mergedEvent.senderInfo.disambiguatedDisplayName,
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/BasedMergedItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/BasedMergedItem.kt
index 1c56a0809e..4999036e9d 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/BasedMergedItem.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/BasedMergedItem.kt
@@ -54,6 +54,7 @@ abstract class BasedMergedItem<H : BasedMergedItem.Holder> : BaseEventItem<H>()
     }
 
     data class Data(
+            val roomId: String?,
             val localId: Long,
             val eventId: String,
             val userId: String,
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MergedRoomCreationItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MergedRoomCreationItem.kt
index f41c17d9e7..5b8d973a04 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MergedRoomCreationItem.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MergedRoomCreationItem.kt
@@ -40,6 +40,7 @@ import im.vector.app.features.home.room.detail.timeline.TimelineEventController
 import im.vector.app.features.home.room.detail.timeline.tools.linkify
 import me.gujun.android.span.span
 import org.matrix.android.sdk.api.session.room.model.RoomSummary
+import org.matrix.android.sdk.api.session.room.model.localecho.RoomLocalEcho
 import org.matrix.android.sdk.api.util.toMatrixItem
 
 @EpoxyModelClass(layout = R.layout.item_timeline_event_base_noinfo)
@@ -102,10 +103,16 @@ abstract class MergedRoomCreationItem : BasedMergedItem<MergedRoomCreationItem.H
             }
             if (attributes.isEncryptionAlgorithmSecure) {
                 holder.e2eTitleTextView.text = holder.expandView.resources.getString(R.string.encryption_enabled)
-                holder.e2eTitleDescriptionView.text = if (data?.isDirectRoom == true) {
-                    holder.expandView.resources.getString(R.string.direct_room_encryption_enabled_tile_description)
-                } else {
-                    holder.expandView.resources.getString(R.string.encryption_enabled_tile_description)
+                holder.e2eTitleDescriptionView.text = when {
+                    data?.isDirectRoom == true && RoomLocalEcho.isLocalEchoId(data.roomId.orEmpty()) -> {
+                        holder.expandView.resources.getString(R.string.direct_room_encryption_enabled_tile_description_future)
+                    }
+                    data?.isDirectRoom == true -> {
+                        holder.expandView.resources.getString(R.string.direct_room_encryption_enabled_tile_description)
+                    }
+                    else -> {
+                        holder.expandView.resources.getString(R.string.encryption_enabled_tile_description)
+                    }
                 }
                 holder.e2eTitleDescriptionView.textAlignment = View.TEXT_ALIGNMENT_CENTER
                 holder.e2eTitleTextView.setCompoundDrawablesWithIntrinsicBounds(
@@ -130,17 +137,29 @@ abstract class MergedRoomCreationItem : BasedMergedItem<MergedRoomCreationItem.H
         val roomDisplayName = roomSummary?.displayName
         holder.roomNameText.setTextOrHide(roomDisplayName)
         val isDirect = roomSummary?.isDirect == true
+        val isLocalRoom = RoomLocalEcho.isLocalEchoId(roomSummary?.roomId.orEmpty())
         val membersCount = roomSummary?.otherMemberIds?.size ?: 0
 
-        if (isDirect) {
-            holder.roomDescriptionText.text = holder.view.resources.getString(
-                    R.string.this_is_the_beginning_of_dm,
-                    roomSummary?.displayName ?: ""
-            )
-        } else if (roomDisplayName.isNullOrBlank() || roomSummary.name.isBlank()) {
-            holder.roomDescriptionText.text = holder.view.resources.getString(R.string.this_is_the_beginning_of_room_no_name)
-        } else {
-            holder.roomDescriptionText.text = holder.view.resources.getString(R.string.this_is_the_beginning_of_room, roomDisplayName)
+        when {
+            isDirect -> {
+                if (isLocalRoom) {
+                    holder.roomDescriptionText.text = holder.view.resources.getString(
+                            R.string.send_your_first_msg_to_invite,
+                            roomSummary?.displayName.orEmpty()
+                    )
+                } else {
+                    holder.roomDescriptionText.text = holder.view.resources.getString(
+                            R.string.this_is_the_beginning_of_dm,
+                            roomSummary?.displayName.orEmpty()
+                    )
+                }
+            }
+            roomDisplayName.isNullOrBlank() || roomSummary.name.isBlank() -> {
+                holder.roomDescriptionText.text = holder.view.resources.getString(R.string.this_is_the_beginning_of_room_no_name)
+            }
+            else -> {
+                holder.roomDescriptionText.text = holder.view.resources.getString(R.string.this_is_the_beginning_of_room, roomDisplayName)
+            }
         }
 
         val topic = roomSummary?.topic
diff --git a/vector/src/main/res/values/strings.xml b/vector/src/main/res/values/strings.xml
index b507ed473f..f684ccadad 100644
--- a/vector/src/main/res/values/strings.xml
+++ b/vector/src/main/res/values/strings.xml
@@ -2399,7 +2399,8 @@
 
     <string name="encryption_enabled">Encryption enabled</string>
     <string name="encryption_enabled_tile_description">Messages in this room are end-to-end encrypted. Learn more &amp; verify users in their profile.</string>
-    <string name="direct_room_encryption_enabled_tile_description">Messages in this room are end-to-end encrypted.</string>
+    <string name="direct_room_encryption_enabled_tile_description">Messages in this chat are end-to-end encrypted.</string>
+    <string name="direct_room_encryption_enabled_tile_description_future">Messages in this chat will be end-to-end encrypted.</string>
     <string name="encryption_not_enabled">Encryption not enabled</string>
     <string name="encryption_misconfigured">Encryption is misconfigured</string>
     <string name="encryption_unknown_algorithm_tile_description">The encryption used by this room is not supported</string>
@@ -2411,6 +2412,7 @@
     <string name="this_is_the_beginning_of_room">This is the beginning of %s.</string>
     <string name="this_is_the_beginning_of_room_no_name">This is the beginning of this conversation.</string>
     <string name="this_is_the_beginning_of_dm">This is the beginning of your direct message history with %s.</string>
+    <string name="send_your_first_msg_to_invite">Send your first message to invite %s to chat</string>
     <!-- First param will be replaced by the value of add_a_topic_link_text, that will be clickable-->
     <string name="room_created_summary_no_topic_creation_text">%s to let people know what this room is about.</string>
     <string name="add_a_topic_link_text">Add a topic</string>

From a46d7ed8dd17064abde7e78ec1a16baddfd90f5b Mon Sep 17 00:00:00 2001
From: Florian Renaud <florianr@element.io>
Date: Fri, 13 May 2022 17:48:30 +0200
Subject: [PATCH 08/26] Hide unwanted events from local room timeline

---
 .../detail/timeline/TimelineEventController.kt    | 15 +++++++++------
 .../helper/TimelineEventVisibilityHelper.kt       |  8 ++++++++
 .../timeline/item/MergedRoomCreationItem.kt       |  6 ++++++
 3 files changed, 23 insertions(+), 6 deletions(-)

diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt
index a02eaa3202..6ef4b21387 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt
@@ -69,6 +69,7 @@ import org.matrix.android.sdk.api.session.room.model.Membership
 import org.matrix.android.sdk.api.session.room.model.ReadReceipt
 import org.matrix.android.sdk.api.session.room.model.RoomMemberContent
 import org.matrix.android.sdk.api.session.room.model.RoomSummary
+import org.matrix.android.sdk.api.session.room.model.localecho.RoomLocalEcho
 import org.matrix.android.sdk.api.session.room.model.message.MessageAudioContent
 import org.matrix.android.sdk.api.session.room.model.message.MessageImageInfoContent
 import org.matrix.android.sdk.api.session.room.model.message.MessageVideoContent
@@ -603,12 +604,14 @@ class TimelineEventController @Inject constructor(
     }
 
     private fun wantsDateSeparator(event: TimelineEvent, nextEvent: TimelineEvent?): Boolean {
-        return if (hasReachedInvite && hasUTD) {
-            true
-        } else {
-            val date = event.root.localDateTime()
-            val nextDate = nextEvent?.root?.localDateTime()
-            date.toLocalDate() != nextDate?.toLocalDate()
+        return when {
+            RoomLocalEcho.isLocalEchoId(partialState.roomSummary?.roomId.orEmpty()) -> false
+            hasReachedInvite && hasUTD                                              -> true
+            else                                                                    -> {
+                val date = event.root.localDateTime()
+                val nextDate = nextEvent?.root?.localDateTime()
+                date.toLocalDate() != nextDate?.toLocalDate()
+            }
         }
     }
 
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineEventVisibilityHelper.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineEventVisibilityHelper.kt
index e4e0ea8352..488aebde13 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineEventVisibilityHelper.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineEventVisibilityHelper.kt
@@ -26,6 +26,7 @@ import org.matrix.android.sdk.api.session.events.model.isThread
 import org.matrix.android.sdk.api.session.events.model.toModel
 import org.matrix.android.sdk.api.session.room.model.Membership
 import org.matrix.android.sdk.api.session.room.model.RoomMemberContent
+import org.matrix.android.sdk.api.session.room.model.localecho.RoomLocalEcho
 import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
 import javax.inject.Inject
 
@@ -176,6 +177,13 @@ class TimelineEventVisibilityHelper @Inject constructor(private val userPreferen
             return true
         }
 
+        // Hide fake events for local rooms
+        if (RoomLocalEcho.isLocalEchoId(roomId) &&
+                root.getClearType() == EventType.STATE_ROOM_MEMBER ||
+                root.getClearType() == EventType.STATE_ROOM_HISTORY_VISIBILITY) {
+            return true
+        }
+
         // Allow only the the threads within the rootThreadEventId along with the root event
         if (userPreferencesProvider.areThreadMessagesEnabled() && isFromThreadTimeline) {
             return if (root.getRootThreadEventId() == rootThreadEventId) {
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MergedRoomCreationItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MergedRoomCreationItem.kt
index 5b8d973a04..5b7c4efc7f 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MergedRoomCreationItem.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MergedRoomCreationItem.kt
@@ -58,7 +58,12 @@ abstract class MergedRoomCreationItem : BasedMergedItem<MergedRoomCreationItem.H
         super.bind(holder)
 
         bindCreationSummaryTile(holder)
+        bindMergedViews(holder)
+    }
 
+    private fun bindMergedViews(holder: Holder) {
+        val isLocalRoom = RoomLocalEcho.isLocalEchoId(attributes.roomSummary?.roomId.orEmpty())
+        holder.mergedView.isVisible = !isLocalRoom
         if (attributes.isCollapsed) {
             // Take the oldest data
             val data = distinctMergeData.lastOrNull()
@@ -222,6 +227,7 @@ abstract class MergedRoomCreationItem : BasedMergedItem<MergedRoomCreationItem.H
     }
 
     class Holder : BasedMergedItem.Holder(STUB_ID) {
+        val mergedView by bind<View>(R.id.mergedSumContainer)
         val summaryView by bind<TextView>(R.id.itemNoticeTextView)
         val avatarView by bind<ImageView>(R.id.itemNoticeAvatarView)
         val encryptionTile by bind<ViewGroup>(R.id.creationEncryptionTile)

From 554d35fe96712e0faa64f5e83e66ff7a649cbf10 Mon Sep 17 00:00:00 2001
From: Florian Renaud <florianr@element.io>
Date: Sat, 21 May 2022 00:18:27 +0200
Subject: [PATCH 09/26] Add changelog

---
 changelog.d/5525.wip | 1 +
 1 file changed, 1 insertion(+)
 create mode 100644 changelog.d/5525.wip

diff --git a/changelog.d/5525.wip b/changelog.d/5525.wip
new file mode 100644
index 0000000000..c4ac0ff008
--- /dev/null
+++ b/changelog.d/5525.wip
@@ -0,0 +1 @@
+Create DM room only on first message - Design implementation & debug feature flag

From 398e98ae85a387eead479b695bf800fa65352fa3 Mon Sep 17 00:00:00 2001
From: Florian Renaud <florianr@element.io>
Date: Tue, 24 May 2022 00:32:16 +0200
Subject: [PATCH 10/26] Remove useless variable

---
 .../sdk/internal/session/room/create/CreateLocalRoomTask.kt    | 3 ---
 1 file changed, 3 deletions(-)

diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateLocalRoomTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateLocalRoomTask.kt
index e650039d75..1fa2cfe35b 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateLocalRoomTask.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateLocalRoomTask.kt
@@ -149,7 +149,6 @@ internal class DefaultCreateLocalRoomTask @Inject constructor(
         }
 
         val eventList = createLocalRoomEvents(createRoomBody)
-        val eventIds = ArrayList<String>(eventList.size)
         val roomMemberContentsByUser = HashMap<String, RoomMemberContent?>()
 
         for (event in eventList) {
@@ -157,8 +156,6 @@ internal class DefaultCreateLocalRoomTask @Inject constructor(
                 continue
             }
 
-            eventIds.add(event.eventId)
-
             val eventEntity = event.toEntity(roomId, SendState.SYNCED, null).copyToRealmOrIgnore(realm, EventInsertType.INCREMENTAL_SYNC)
             if (event.stateKey != null) {
                 CurrentStateEventEntity.getOrCreate(realm, roomId, event.stateKey, event.type).apply {

From d42a3da5b7b5dc664fd0f01f847fe5f0d013f07c Mon Sep 17 00:00:00 2001
From: Florian Renaud <florianr@element.io>
Date: Tue, 14 Jun 2022 15:16:17 +0200
Subject: [PATCH 11/26] Reduce code smell

---
 .../timeline/item/MergedRoomCreationItem.kt   | 82 +++++++++++--------
 1 file changed, 46 insertions(+), 36 deletions(-)

diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MergedRoomCreationItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MergedRoomCreationItem.kt
index 5b7c4efc7f..cd60057379 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MergedRoomCreationItem.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MergedRoomCreationItem.kt
@@ -16,6 +16,7 @@
 
 package im.vector.app.features.home.room.detail.timeline.item
 
+import android.content.res.Resources
 import android.text.SpannableString
 import android.text.method.MovementMethod
 import android.text.style.ClickableSpan
@@ -25,7 +26,6 @@ import android.widget.ImageView
 import android.widget.TextView
 import androidx.constraintlayout.widget.ConstraintLayout
 import androidx.core.content.ContextCompat
-import androidx.core.view.isGone
 import androidx.core.view.isVisible
 import androidx.core.view.updateLayoutParams
 import com.airbnb.epoxy.EpoxyAttribute
@@ -39,6 +39,7 @@ import im.vector.app.features.home.room.detail.RoomDetailAction
 import im.vector.app.features.home.room.detail.timeline.TimelineEventController
 import im.vector.app.features.home.room.detail.timeline.tools.linkify
 import me.gujun.android.span.span
+import org.matrix.android.sdk.api.extensions.orFalse
 import org.matrix.android.sdk.api.session.room.model.RoomSummary
 import org.matrix.android.sdk.api.session.room.model.localecho.RoomLocalEcho
 import org.matrix.android.sdk.api.util.toMatrixItem
@@ -62,44 +63,45 @@ abstract class MergedRoomCreationItem : BasedMergedItem<MergedRoomCreationItem.H
     }
 
     private fun bindMergedViews(holder: Holder) {
-        val isLocalRoom = RoomLocalEcho.isLocalEchoId(attributes.roomSummary?.roomId.orEmpty())
-        holder.mergedView.isVisible = !isLocalRoom
+        holder.mergedView.isVisible = !attributes.isLocalRoom
         if (attributes.isCollapsed) {
             // Take the oldest data
             val data = distinctMergeData.lastOrNull()
-
-            val createdFromCurrentUser = data?.userId == attributes.currentUserId
-            val summary = if (createdFromCurrentUser) {
-                if (data?.isDirectRoom == true) {
-                    holder.expandView.resources.getString(R.string.direct_room_created_summary_item_by_you)
-                } else {
-                    holder.expandView.resources.getString(R.string.room_created_summary_item_by_you)
-                }
-            } else {
-                if (data?.isDirectRoom == true) {
-                    holder.expandView.resources.getString(R.string.direct_room_created_summary_item, data.memberName)
-                } else {
-                    holder.expandView.resources.getString(R.string.room_created_summary_item, data?.memberName ?: data?.userId ?: "")
-                }
-            }
-            holder.summaryView.text = summary
+            holder.summaryView.text = getSummaryText(holder.expandView.resources, data)
             holder.summaryView.visibility = View.VISIBLE
-            holder.avatarView.visibility = View.VISIBLE
             if (data != null) {
                 holder.avatarView.visibility = View.VISIBLE
                 attributes.avatarRenderer.render(data.toMatrixItem(), holder.avatarView)
             } else {
                 holder.avatarView.visibility = View.GONE
             }
-
             bindEncryptionTile(holder, data)
         } else {
             holder.avatarView.visibility = View.INVISIBLE
             holder.summaryView.visibility = View.GONE
-            holder.encryptionTile.isGone = true
+            holder.encryptionTile.visibility = View.GONE
         }
     }
 
+    private fun getSummaryText(resources: Resources, data: Data?): String {
+        val createdFromCurrentUser = data?.userId == attributes.currentUserId
+        val isDirectRoom = data?.isDirectRoom.orFalse()
+        val summary = if (createdFromCurrentUser) {
+            if (isDirectRoom) {
+                resources.getString(R.string.direct_room_created_summary_item_by_you)
+            } else {
+                resources.getString(R.string.room_created_summary_item_by_you)
+            }
+        } else {
+            if (isDirectRoom) {
+                resources.getString(R.string.direct_room_created_summary_item, data?.memberName.orEmpty())
+            } else {
+                resources.getString(R.string.room_created_summary_item, data?.memberName.orEmpty())
+            }
+        }
+        return summary
+    }
+
     private fun bindEncryptionTile(holder: Holder, data: Data?) {
         if (attributes.hasEncryptionEvent) {
             holder.encryptionTile.isVisible = true
@@ -108,17 +110,7 @@ abstract class MergedRoomCreationItem : BasedMergedItem<MergedRoomCreationItem.H
             }
             if (attributes.isEncryptionAlgorithmSecure) {
                 holder.e2eTitleTextView.text = holder.expandView.resources.getString(R.string.encryption_enabled)
-                holder.e2eTitleDescriptionView.text = when {
-                    data?.isDirectRoom == true && RoomLocalEcho.isLocalEchoId(data.roomId.orEmpty()) -> {
-                        holder.expandView.resources.getString(R.string.direct_room_encryption_enabled_tile_description_future)
-                    }
-                    data?.isDirectRoom == true -> {
-                        holder.expandView.resources.getString(R.string.direct_room_encryption_enabled_tile_description)
-                    }
-                    else -> {
-                        holder.expandView.resources.getString(R.string.encryption_enabled_tile_description)
-                    }
-                }
+                holder.e2eTitleDescriptionView.text = getE2ESecureDescriptionText(holder.expandView.resources, data)
                 holder.e2eTitleDescriptionView.textAlignment = View.TEXT_ALIGNMENT_CENTER
                 holder.e2eTitleTextView.setCompoundDrawablesWithIntrinsicBounds(
                         ContextCompat.getDrawable(holder.view.context, R.drawable.ic_shield_black),
@@ -137,17 +129,32 @@ abstract class MergedRoomCreationItem : BasedMergedItem<MergedRoomCreationItem.H
         }
     }
 
+    private fun getE2ESecureDescriptionText(resources: Resources, data: Data?): String {
+        val isDirectRoom = data?.isDirectRoom.orFalse()
+        return when {
+            isDirectRoom -> {
+                if (attributes.isLocalRoom) {
+                    resources.getString(R.string.direct_room_encryption_enabled_tile_description_future)
+                } else {
+                    resources.getString(R.string.direct_room_encryption_enabled_tile_description)
+                }
+            }
+            else -> {
+                resources.getString(R.string.encryption_enabled_tile_description)
+            }
+        }
+    }
+
     private fun bindCreationSummaryTile(holder: Holder) {
         val roomSummary = attributes.roomSummary
         val roomDisplayName = roomSummary?.displayName
         holder.roomNameText.setTextOrHide(roomDisplayName)
         val isDirect = roomSummary?.isDirect == true
-        val isLocalRoom = RoomLocalEcho.isLocalEchoId(roomSummary?.roomId.orEmpty())
         val membersCount = roomSummary?.otherMemberIds?.size ?: 0
 
         when {
             isDirect -> {
-                if (isLocalRoom) {
+                if (attributes.isLocalRoom) {
                     holder.roomDescriptionText.text = holder.view.resources.getString(
                             R.string.send_your_first_msg_to_invite,
                             roomSummary?.displayName.orEmpty()
@@ -261,5 +268,8 @@ abstract class MergedRoomCreationItem : BasedMergedItem<MergedRoomCreationItem.H
             val canChangeAvatar: Boolean = false,
             val canChangeName: Boolean = false,
             val canChangeTopic: Boolean = false
-    ) : BasedMergedItem.Attributes
+    ) : BasedMergedItem.Attributes {
+
+        val isLocalRoom = RoomLocalEcho.isLocalEchoId(roomSummary?.roomId.orEmpty())
+    }
 }

From 71320e42d0a750bfe6357f3aa334b6d593b0642c Mon Sep 17 00:00:00 2001
From: Florian Renaud <florianr@element.io>
Date: Fri, 1 Jul 2022 09:33:21 +0200
Subject: [PATCH 12/26] Show date separator in local room timeline

---
 .../home/room/detail/timeline/TimelineEventController.kt         | 1 -
 1 file changed, 1 deletion(-)

diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt
index 6ef4b21387..5b81b327aa 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt
@@ -605,7 +605,6 @@ class TimelineEventController @Inject constructor(
 
     private fun wantsDateSeparator(event: TimelineEvent, nextEvent: TimelineEvent?): Boolean {
         return when {
-            RoomLocalEcho.isLocalEchoId(partialState.roomSummary?.roomId.orEmpty()) -> false
             hasReachedInvite && hasUTD                                              -> true
             else                                                                    -> {
                 val date = event.root.localDateTime()

From c7db89613c40add99d8b88242e5817e158997582 Mon Sep 17 00:00:00 2001
From: Florian Renaud <florianr@element.io>
Date: Thu, 30 Jun 2022 15:24:42 +0200
Subject: [PATCH 13/26] Split code in MergedRoomCreationItem

---
 .../timeline/item/MergedRoomCreationItem.kt   | 168 ++++++++++--------
 1 file changed, 93 insertions(+), 75 deletions(-)

diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MergedRoomCreationItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MergedRoomCreationItem.kt
index 6371440d1a..b5c24a4480 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MergedRoomCreationItem.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MergedRoomCreationItem.kt
@@ -16,7 +16,6 @@
 
 package im.vector.app.features.home.room.detail.timeline.item
 
-import android.content.res.Resources
 import android.text.SpannableString
 import android.text.method.MovementMethod
 import android.text.style.ClickableSpan
@@ -39,7 +38,6 @@ import im.vector.app.features.home.room.detail.RoomDetailAction
 import im.vector.app.features.home.room.detail.timeline.TimelineEventController
 import im.vector.app.features.home.room.detail.timeline.tools.linkify
 import me.gujun.android.span.span
-import org.matrix.android.sdk.api.extensions.orFalse
 import org.matrix.android.sdk.api.session.room.model.RoomSummary
 import org.matrix.android.sdk.api.session.room.model.localecho.RoomLocalEcho
 import org.matrix.android.sdk.api.util.toMatrixItem
@@ -53,6 +51,14 @@ abstract class MergedRoomCreationItem : BasedMergedItem<MergedRoomCreationItem.H
     @EpoxyAttribute(EpoxyAttribute.Option.DoNotHash)
     var movementMethod: MovementMethod? = null
 
+    private val roomSummary
+        get() = attributes.roomSummary
+
+    private val isDirectRoom
+        get() = distinctMergeData.lastOrNull()?.isDirectRoom
+                ?: roomSummary?.isDirect
+                ?: false
+
     override fun getViewStubId() = STUB_ID
 
     override fun bind(holder: Holder) {
@@ -67,7 +73,7 @@ abstract class MergedRoomCreationItem : BasedMergedItem<MergedRoomCreationItem.H
         if (attributes.isCollapsed) {
             // Take the oldest data
             val data = distinctMergeData.lastOrNull()
-            holder.summaryView.text = getSummaryText(holder.expandView.resources, data)
+            renderSummaryText(holder, data)
             holder.summaryView.visibility = View.VISIBLE
             if (data != null) {
                 holder.avatarView.visibility = View.VISIBLE
@@ -75,7 +81,7 @@ abstract class MergedRoomCreationItem : BasedMergedItem<MergedRoomCreationItem.H
             } else {
                 holder.avatarView.visibility = View.GONE
             }
-            bindEncryptionTile(holder, data)
+            bindEncryptionTile(holder)
         } else {
             holder.avatarView.visibility = View.INVISIBLE
             holder.summaryView.visibility = View.GONE
@@ -83,9 +89,9 @@ abstract class MergedRoomCreationItem : BasedMergedItem<MergedRoomCreationItem.H
         }
     }
 
-    private fun getSummaryText(resources: Resources, data: Data?): String {
+    private fun renderSummaryText(holder: Holder, data: Data?) {
+        val resources = holder.expandView.resources
         val createdFromCurrentUser = data?.userId == attributes.currentUserId
-        val isDirectRoom = data?.isDirectRoom.orFalse()
         val summary = if (createdFromCurrentUser) {
             if (isDirectRoom) {
                 resources.getString(R.string.direct_room_created_summary_item_by_you)
@@ -99,39 +105,28 @@ abstract class MergedRoomCreationItem : BasedMergedItem<MergedRoomCreationItem.H
                 resources.getString(R.string.room_created_summary_item, data?.memberName.orEmpty())
             }
         }
-        return summary
+        holder.summaryView.text = summary
     }
 
-    private fun bindEncryptionTile(holder: Holder, data: Data?) {
+    private fun bindEncryptionTile(holder: Holder) {
         if (attributes.hasEncryptionEvent) {
             holder.encryptionTile.isVisible = true
             holder.encryptionTile.updateLayoutParams<ConstraintLayout.LayoutParams> {
                 this.marginEnd = leftGuideline
             }
             if (attributes.isEncryptionAlgorithmSecure) {
-                holder.e2eTitleTextView.text = holder.expandView.resources.getString(R.string.encryption_enabled)
-                holder.e2eTitleDescriptionView.text = getE2ESecureDescriptionText(holder.expandView.resources, data)
-                holder.e2eTitleDescriptionView.textAlignment = View.TEXT_ALIGNMENT_CENTER
-                holder.e2eTitleTextView.setCompoundDrawablesWithIntrinsicBounds(
-                        ContextCompat.getDrawable(holder.view.context, R.drawable.ic_shield_black),
-                        null, null, null
-                )
+                renderE2ESecureTile(holder)
             } else {
-                holder.e2eTitleTextView.text = holder.expandView.resources.getString(R.string.encryption_not_enabled)
-                holder.e2eTitleDescriptionView.text = holder.expandView.resources.getString(R.string.encryption_unknown_algorithm_tile_description)
-                holder.e2eTitleTextView.setCompoundDrawablesWithIntrinsicBounds(
-                        ContextCompat.getDrawable(holder.view.context, R.drawable.ic_shield_warning),
-                        null, null, null
-                )
+                renderE2EUnsecureTile(holder)
             }
         } else {
             holder.encryptionTile.isVisible = false
         }
     }
 
-    private fun getE2ESecureDescriptionText(resources: Resources, data: Data?): String {
-        val isDirectRoom = data?.isDirectRoom.orFalse()
-        return when {
+    private fun renderE2ESecureTile(holder: Holder) {
+        val resources = holder.expandView.resources
+        val description = when {
             isDirectRoom -> {
                 if (attributes.isLocalRoom) {
                     resources.getString(R.string.direct_room_encryption_enabled_tile_description_future)
@@ -143,65 +138,36 @@ abstract class MergedRoomCreationItem : BasedMergedItem<MergedRoomCreationItem.H
                 resources.getString(R.string.encryption_enabled_tile_description)
             }
         }
+
+        holder.e2eTitleTextView.text = holder.expandView.resources.getString(R.string.encryption_enabled)
+        holder.e2eTitleTextView.setCompoundDrawablesWithIntrinsicBounds(
+                ContextCompat.getDrawable(holder.view.context, R.drawable.ic_shield_black),
+                null, null, null
+        )
+        holder.e2eTitleDescriptionView.text = description
+        holder.e2eTitleDescriptionView.textAlignment = View.TEXT_ALIGNMENT_CENTER
+    }
+
+    private fun renderE2EUnsecureTile(holder: Holder) {
+        holder.e2eTitleTextView.text = holder.expandView.resources.getString(R.string.encryption_not_enabled)
+        holder.e2eTitleDescriptionView.text = holder.expandView.resources.getString(R.string.encryption_unknown_algorithm_tile_description)
+        holder.e2eTitleTextView.setCompoundDrawablesWithIntrinsicBounds(
+                ContextCompat.getDrawable(holder.view.context, R.drawable.ic_shield_warning),
+                null, null, null
+        )
     }
 
     private fun bindCreationSummaryTile(holder: Holder) {
-        val roomSummary = attributes.roomSummary
         val roomDisplayName = roomSummary?.displayName
-        holder.roomNameText.setTextOrHide(roomDisplayName)
-        val isDirect = roomSummary?.isDirect == true
         val membersCount = roomSummary?.otherMemberIds?.size ?: 0
 
-        when {
-            isDirect -> {
-                if (attributes.isLocalRoom) {
-                    holder.roomDescriptionText.text = holder.view.resources.getString(
-                            R.string.send_your_first_msg_to_invite,
-                            roomSummary?.displayName.orEmpty()
-                    )
-                } else {
-                    holder.roomDescriptionText.text = holder.view.resources.getString(
-                            R.string.this_is_the_beginning_of_dm,
-                            roomSummary?.displayName.orEmpty()
-                    )
-                }
-            }
-            roomDisplayName.isNullOrBlank() || roomSummary.name.isBlank() -> {
-                holder.roomDescriptionText.text = holder.view.resources.getString(R.string.this_is_the_beginning_of_room_no_name)
-            }
-            else -> {
-                holder.roomDescriptionText.text = holder.view.resources.getString(R.string.this_is_the_beginning_of_room, roomDisplayName)
-            }
-        }
-
-        val topic = roomSummary?.topic
-        if (topic.isNullOrBlank()) {
-            // do not show hint for DMs or group DMs
-            val canSetTopic = attributes.canChangeTopic && !isDirect
-            if (canSetTopic) {
-                val addTopicLink = holder.view.resources.getString(R.string.add_a_topic_link_text)
-                val styledText = SpannableString(holder.view.resources.getString(R.string.room_created_summary_no_topic_creation_text, addTopicLink))
-                holder.roomTopicText.setTextOrHide(styledText.tappableMatchingText(addTopicLink, object : ClickableSpan() {
-                    override fun onClick(widget: View) {
-                        attributes.callback?.onTimelineItemAction(RoomDetailAction.QuickActionSetTopic)
-                    }
-                }))
-            }
-        } else {
-            holder.roomTopicText.setTextOrHide(
-                    span {
-                        span(holder.view.resources.getString(R.string.topic_prefix)) {
-                            textStyle = "bold"
-                        }
-                        +topic.linkify(attributes.callback)
-                    }
-            )
-        }
-        holder.roomTopicText.movementMethod = movementMethod
+        holder.roomNameText.setTextOrHide(roomDisplayName)
+        renderRoomDescription(holder)
+        renderRoomTopic(holder)
 
         val roomItem = roomSummary?.toMatrixItem()
         val shouldSetAvatar = attributes.canChangeAvatar &&
-                (roomSummary?.isDirect == false || (isDirect && membersCount >= 2)) &&
+                (roomSummary?.isDirect == false || (isDirectRoom && membersCount >= 2)) &&
                 roomItem?.avatarUrl.isNullOrBlank()
 
         holder.roomAvatarImageView.isVisible = roomItem != null
@@ -224,7 +190,7 @@ abstract class MergedRoomCreationItem : BasedMergedItem<MergedRoomCreationItem.H
             }
         }
 
-        val canInvite = attributes.canInvite && !isDirect
+        val canInvite = attributes.canInvite && !isDirectRoom
         holder.addPeopleButton.isVisible = canInvite
         if (canInvite) {
             holder.addPeopleButton.onClick {
@@ -233,6 +199,58 @@ abstract class MergedRoomCreationItem : BasedMergedItem<MergedRoomCreationItem.H
         }
     }
 
+    private fun renderRoomDescription(holder: Holder) {
+        val roomDisplayName = roomSummary?.displayName
+        when {
+            isDirectRoom -> {
+                if (attributes.isLocalRoom) {
+                    holder.roomDescriptionText.text = holder.view.resources.getString(
+                            R.string.send_your_first_msg_to_invite,
+                            roomSummary?.displayName.orEmpty()
+                    )
+                } else {
+                    holder.roomDescriptionText.text = holder.view.resources.getString(
+                            R.string.this_is_the_beginning_of_dm,
+                            roomSummary?.displayName.orEmpty()
+                    )
+                }
+            }
+            roomDisplayName.isNullOrBlank() || roomSummary?.name.isNullOrBlank() -> {
+                holder.roomDescriptionText.text = holder.view.resources.getString(R.string.this_is_the_beginning_of_room_no_name)
+            }
+            else -> {
+                holder.roomDescriptionText.text = holder.view.resources.getString(R.string.this_is_the_beginning_of_room, roomDisplayName)
+            }
+        }
+    }
+
+    private fun renderRoomTopic(holder: Holder) {
+        val topic = roomSummary?.topic
+        if (topic.isNullOrBlank()) {
+            // do not show hint for DMs or group DMs
+            val canSetTopic = attributes.canChangeTopic && !isDirectRoom
+            if (canSetTopic) {
+                val addTopicLink = holder.view.resources.getString(R.string.add_a_topic_link_text)
+                val styledText = SpannableString(holder.view.resources.getString(R.string.room_created_summary_no_topic_creation_text, addTopicLink))
+                holder.roomTopicText.setTextOrHide(styledText.tappableMatchingText(addTopicLink, object : ClickableSpan() {
+                    override fun onClick(widget: View) {
+                        attributes.callback?.onTimelineItemAction(RoomDetailAction.QuickActionSetTopic)
+                    }
+                }))
+            }
+        } else {
+            holder.roomTopicText.setTextOrHide(
+                    span {
+                        span(holder.view.resources.getString(R.string.topic_prefix)) {
+                            textStyle = "bold"
+                        }
+                        +topic.linkify(attributes.callback)
+                    }
+            )
+        }
+        holder.roomTopicText.movementMethod = movementMethod
+    }
+
     class Holder : BasedMergedItem.Holder(STUB_ID) {
         val mergedView by bind<View>(R.id.mergedSumContainer)
         val summaryView by bind<TextView>(R.id.itemNoticeTextView)

From 7415623c2fae0fce46e4f6c6c9616dadc5ae7513 Mon Sep 17 00:00:00 2001
From: Florian Renaud <florianr@element.io>
Date: Fri, 1 Jul 2022 00:19:41 +0200
Subject: [PATCH 14/26] Update room description style

---
 .../timeline/item/MergedRoomCreationItem.kt   | 24 ++++++++++---------
 1 file changed, 13 insertions(+), 11 deletions(-)

diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MergedRoomCreationItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MergedRoomCreationItem.kt
index b5c24a4480..47401c34e7 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MergedRoomCreationItem.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MergedRoomCreationItem.kt
@@ -27,6 +27,7 @@ import androidx.constraintlayout.widget.ConstraintLayout
 import androidx.core.content.ContextCompat
 import androidx.core.view.isVisible
 import androidx.core.view.updateLayoutParams
+import androidx.core.widget.TextViewCompat
 import com.airbnb.epoxy.EpoxyAttribute
 import com.airbnb.epoxy.EpoxyModelClass
 import im.vector.app.R
@@ -37,6 +38,7 @@ import im.vector.app.features.home.AvatarRenderer
 import im.vector.app.features.home.room.detail.RoomDetailAction
 import im.vector.app.features.home.room.detail.timeline.TimelineEventController
 import im.vector.app.features.home.room.detail.timeline.tools.linkify
+import im.vector.app.features.themes.ThemeUtils
 import me.gujun.android.span.span
 import org.matrix.android.sdk.api.session.room.model.RoomSummary
 import org.matrix.android.sdk.api.session.room.model.localecho.RoomLocalEcho
@@ -201,27 +203,27 @@ abstract class MergedRoomCreationItem : BasedMergedItem<MergedRoomCreationItem.H
 
     private fun renderRoomDescription(holder: Holder) {
         val roomDisplayName = roomSummary?.displayName
-        when {
+        val resources = holder.roomDescriptionText.resources
+        val description = when {
             isDirectRoom -> {
                 if (attributes.isLocalRoom) {
-                    holder.roomDescriptionText.text = holder.view.resources.getString(
-                            R.string.send_your_first_msg_to_invite,
-                            roomSummary?.displayName.orEmpty()
-                    )
+                    resources.getString(R.string.send_your_first_msg_to_invite, roomSummary?.displayName.orEmpty())
                 } else {
-                    holder.roomDescriptionText.text = holder.view.resources.getString(
-                            R.string.this_is_the_beginning_of_dm,
-                            roomSummary?.displayName.orEmpty()
-                    )
+                    resources.getString(R.string.this_is_the_beginning_of_dm, roomSummary?.displayName.orEmpty())
                 }
             }
             roomDisplayName.isNullOrBlank() || roomSummary?.name.isNullOrBlank() -> {
-                holder.roomDescriptionText.text = holder.view.resources.getString(R.string.this_is_the_beginning_of_room_no_name)
+                holder.view.resources.getString(R.string.this_is_the_beginning_of_room_no_name)
             }
             else -> {
-                holder.roomDescriptionText.text = holder.view.resources.getString(R.string.this_is_the_beginning_of_room, roomDisplayName)
+                holder.view.resources.getString(R.string.this_is_the_beginning_of_room, roomDisplayName)
             }
         }
+        holder.roomDescriptionText.text = description
+        if (isDirectRoom && attributes.isLocalRoom) {
+            TextViewCompat.setTextAppearance(holder.roomDescriptionText, R.style.TextAppearance_Vector_Subtitle)
+            holder.roomDescriptionText.setTextColor(ThemeUtils.getColor(holder.roomDescriptionText.context, R.attr.vctr_content_primary))
+        }
     }
 
     private fun renderRoomTopic(holder: Holder) {

From 0dad4cb02a111de71f9970b9103f00fd80b73394 Mon Sep 17 00:00:00 2001
From: Florian Renaud <florianr@element.io>
Date: Fri, 1 Jul 2022 09:57:45 +0200
Subject: [PATCH 15/26] Set current ts for local events age

---
 .../sdk/internal/session/room/create/CreateLocalRoomTask.kt    | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateLocalRoomTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateLocalRoomTask.kt
index 1fa2cfe35b..811d7ef5b6 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateLocalRoomTask.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateLocalRoomTask.kt
@@ -156,7 +156,8 @@ internal class DefaultCreateLocalRoomTask @Inject constructor(
                 continue
             }
 
-            val eventEntity = event.toEntity(roomId, SendState.SYNCED, null).copyToRealmOrIgnore(realm, EventInsertType.INCREMENTAL_SYNC)
+            val now = clock.epochMillis()
+            val eventEntity = event.toEntity(roomId, SendState.SYNCED, now).copyToRealmOrIgnore(realm, EventInsertType.INCREMENTAL_SYNC)
             if (event.stateKey != null) {
                 CurrentStateEventEntity.getOrCreate(realm, roomId, event.stateKey, event.type).apply {
                     eventId = event.eventId

From c25edfada40ec2f7ae8358a2437f158497df7739 Mon Sep 17 00:00:00 2001
From: Florian Renaud <florianr@element.io>
Date: Fri, 1 Jul 2022 10:09:26 +0200
Subject: [PATCH 16/26] Remove unused imports

---
 .../home/room/detail/timeline/TimelineEventController.kt     | 5 ++---
 1 file changed, 2 insertions(+), 3 deletions(-)

diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt
index 5b81b327aa..18c626bda8 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt
@@ -69,7 +69,6 @@ import org.matrix.android.sdk.api.session.room.model.Membership
 import org.matrix.android.sdk.api.session.room.model.ReadReceipt
 import org.matrix.android.sdk.api.session.room.model.RoomMemberContent
 import org.matrix.android.sdk.api.session.room.model.RoomSummary
-import org.matrix.android.sdk.api.session.room.model.localecho.RoomLocalEcho
 import org.matrix.android.sdk.api.session.room.model.message.MessageAudioContent
 import org.matrix.android.sdk.api.session.room.model.message.MessageImageInfoContent
 import org.matrix.android.sdk.api.session.room.model.message.MessageVideoContent
@@ -605,8 +604,8 @@ class TimelineEventController @Inject constructor(
 
     private fun wantsDateSeparator(event: TimelineEvent, nextEvent: TimelineEvent?): Boolean {
         return when {
-            hasReachedInvite && hasUTD                                              -> true
-            else                                                                    -> {
+            hasReachedInvite && hasUTD -> true
+            else -> {
                 val date = event.root.localDateTime()
                 val nextDate = nextEvent?.root?.localDateTime()
                 date.toLocalDate() != nextDate?.toLocalDate()

From 8a5a47c6a57863b87f62ca2e123fe619d1d9b612 Mon Sep 17 00:00:00 2001
From: Florian Renaud <florianr@element.io>
Date: Mon, 4 Jul 2022 17:38:32 +0200
Subject: [PATCH 17/26] Ensure that Realm is up to date before returning the
 roomId

---
 .../room/create/CreateLocalRoomTask.kt        | 19 ++-----------------
 1 file changed, 2 insertions(+), 17 deletions(-)

diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateLocalRoomTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateLocalRoomTask.kt
index 811d7ef5b6..7f0d3a63e3 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateLocalRoomTask.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateLocalRoomTask.kt
@@ -18,16 +18,13 @@ package org.matrix.android.sdk.internal.session.room.create
 
 import com.zhuinden.monarchy.Monarchy
 import io.realm.Realm
-import io.realm.RealmConfiguration
 import io.realm.kotlin.createObject
-import kotlinx.coroutines.TimeoutCancellationException
 import org.matrix.android.sdk.api.extensions.orFalse
 import org.matrix.android.sdk.api.extensions.tryOrNull
 import org.matrix.android.sdk.api.session.events.model.Content
 import org.matrix.android.sdk.api.session.events.model.Event
 import org.matrix.android.sdk.api.session.events.model.EventType
 import org.matrix.android.sdk.api.session.events.model.toContent
-import org.matrix.android.sdk.api.session.room.failure.CreateRoomFailure
 import org.matrix.android.sdk.api.session.room.model.GuestAccess
 import org.matrix.android.sdk.api.session.room.model.Membership
 import org.matrix.android.sdk.api.session.room.model.RoomDirectoryVisibility
@@ -40,7 +37,6 @@ import org.matrix.android.sdk.api.session.room.send.SendState
 import org.matrix.android.sdk.api.session.sync.model.RoomSyncSummary
 import org.matrix.android.sdk.api.session.user.UserService
 import org.matrix.android.sdk.api.session.user.model.User
-import org.matrix.android.sdk.internal.database.awaitNotEmptyResult
 import org.matrix.android.sdk.internal.database.helper.addTimelineEvent
 import org.matrix.android.sdk.internal.database.mapper.asDomain
 import org.matrix.android.sdk.internal.database.mapper.toEntity
@@ -49,7 +45,6 @@ import org.matrix.android.sdk.internal.database.model.CurrentStateEventEntity
 import org.matrix.android.sdk.internal.database.model.EventInsertType
 import org.matrix.android.sdk.internal.database.model.RoomEntity
 import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity
-import org.matrix.android.sdk.internal.database.model.RoomSummaryEntityFields
 import org.matrix.android.sdk.internal.database.query.copyToRealmOrIgnore
 import org.matrix.android.sdk.internal.database.query.getOrCreate
 import org.matrix.android.sdk.internal.database.query.getOrNull
@@ -63,7 +58,6 @@ import org.matrix.android.sdk.internal.task.Task
 import org.matrix.android.sdk.internal.util.awaitTransaction
 import org.matrix.android.sdk.internal.util.time.Clock
 import java.util.UUID
-import java.util.concurrent.TimeUnit
 import javax.inject.Inject
 
 internal interface CreateLocalRoomTask : Task<CreateRoomParams, String>
@@ -73,7 +67,6 @@ internal class DefaultCreateLocalRoomTask @Inject constructor(
         @SessionDatabase private val monarchy: Monarchy,
         private val roomMemberEventHandler: RoomMemberEventHandler,
         private val roomSummaryUpdater: RoomSummaryUpdater,
-        @SessionDatabase private val realmConfiguration: RealmConfiguration,
         private val createRoomBodyBuilder: CreateRoomBodyBuilder,
         private val userService: UserService,
         private val clock: Clock,
@@ -87,16 +80,8 @@ internal class DefaultCreateLocalRoomTask @Inject constructor(
             createLocalRoomSummaryEntity(realm, roomId, createRoomBody)
         }
 
-        // Wait for room to be created in DB
-        try {
-            awaitNotEmptyResult(realmConfiguration, TimeUnit.MINUTES.toMillis(1L)) { realm ->
-                realm.where(RoomSummaryEntity::class.java)
-                        .equalTo(RoomSummaryEntityFields.ROOM_ID, roomId)
-                        .equalTo(RoomSummaryEntityFields.MEMBERSHIP_STR, Membership.JOIN.name)
-            }
-        } catch (exception: TimeoutCancellationException) {
-            throw CreateRoomFailure.CreatedWithTimeout(roomId)
-        }
+        // Ensure that Realm is up to date before returning the roomId.
+        monarchy.doWithRealm { it.refresh() }
 
         return roomId
     }

From f4b50f1e0f7823550d708cba70ddf3b868ae1ad8 Mon Sep 17 00:00:00 2001
From: Florian Renaud <florianr@element.io>
Date: Mon, 4 Jul 2022 17:41:00 +0200
Subject: [PATCH 18/26] Fix logs when deleting local room

---
 .../sdk/internal/session/room/delete/DeleteLocalRoomTask.kt   | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/delete/DeleteLocalRoomTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/delete/DeleteLocalRoomTask.kt
index 689fd24a97..a15cb4393c 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/delete/DeleteLocalRoomTask.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/delete/DeleteLocalRoomTask.kt
@@ -61,7 +61,7 @@ internal class DefaultDeleteLocalRoomTask @Inject constructor(
                         ?.also { Timber.i("## DeleteLocalRoomTask - TimelineEventEntity - delete ${it.size} entries") }
                         ?.deleteAllFromRealm()
                 ChunkEntity.where(realm, roomId = roomId).findAll()
-                        ?.also { Timber.i("## DeleteLocalRoomTask - RoomMemberSummaryEntity - delete ${it.size} entries") }
+                        ?.also { Timber.i("## DeleteLocalRoomTask - ChunkEntity - delete ${it.size} entries") }
                         ?.deleteAllFromRealm()
                 RoomSummaryEntity.where(realm, roomId = roomId).findAll()
                         ?.also { Timber.i("## DeleteLocalRoomTask - RoomSummaryEntity - delete ${it.size} entries") }
@@ -71,7 +71,7 @@ internal class DefaultDeleteLocalRoomTask @Inject constructor(
                         ?.deleteAllFromRealm()
             }
         } else {
-            Timber.i("## DeleteLocalRoomTask - Failed to remove room id $roomId: not a local room")
+            Timber.i("## DeleteLocalRoomTask - Failed to remove room with id $roomId: not a local room")
         }
     }
 }

From 0d9cd2b3a3bbf4e2b49fc3ff01102a548bd16e81 Mon Sep 17 00:00:00 2001
From: Florian Renaud <florianr@element.io>
Date: Mon, 4 Jul 2022 17:47:48 +0200
Subject: [PATCH 19/26] Delete local room related entities with cascade

---
 .../sdk/internal/session/room/delete/DeleteLocalRoomTask.kt  | 5 +++--
 1 file changed, 3 insertions(+), 2 deletions(-)

diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/delete/DeleteLocalRoomTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/delete/DeleteLocalRoomTask.kt
index a15cb4393c..936c94e520 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/delete/DeleteLocalRoomTask.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/delete/DeleteLocalRoomTask.kt
@@ -25,6 +25,7 @@ import org.matrix.android.sdk.internal.database.model.RoomEntity
 import org.matrix.android.sdk.internal.database.model.RoomMemberSummaryEntity
 import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity
 import org.matrix.android.sdk.internal.database.model.TimelineEventEntity
+import org.matrix.android.sdk.internal.database.model.deleteOnCascade
 import org.matrix.android.sdk.internal.database.query.where
 import org.matrix.android.sdk.internal.database.query.whereRoomId
 import org.matrix.android.sdk.internal.di.SessionDatabase
@@ -59,10 +60,10 @@ internal class DefaultDeleteLocalRoomTask @Inject constructor(
                         ?.deleteAllFromRealm()
                 TimelineEventEntity.whereRoomId(realm, roomId = roomId).findAll()
                         ?.also { Timber.i("## DeleteLocalRoomTask - TimelineEventEntity - delete ${it.size} entries") }
-                        ?.deleteAllFromRealm()
+                        ?.forEach { it.deleteOnCascade(true) }
                 ChunkEntity.where(realm, roomId = roomId).findAll()
                         ?.also { Timber.i("## DeleteLocalRoomTask - ChunkEntity - delete ${it.size} entries") }
-                        ?.deleteAllFromRealm()
+                        ?.forEach { it.deleteOnCascade(deleteStateEvents = true, canDeleteRoot = true) }
                 RoomSummaryEntity.where(realm, roomId = roomId).findAll()
                         ?.also { Timber.i("## DeleteLocalRoomTask - RoomSummaryEntity - delete ${it.size} entries") }
                         ?.deleteAllFromRealm()

From e89bb0eea7c3c41828147fecc89384b7a32e1821 Mon Sep 17 00:00:00 2001
From: Florian Renaud <florianr@element.io>
Date: Mon, 4 Jul 2022 17:53:21 +0200
Subject: [PATCH 20/26] Set local room members as loaded

---
 .../sdk/internal/session/room/create/CreateLocalRoomTask.kt     | 2 ++
 .../sdk/internal/session/room/timeline/DefaultTimeline.kt       | 2 --
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateLocalRoomTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateLocalRoomTask.kt
index 7f0d3a63e3..eb35dd2387 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateLocalRoomTask.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateLocalRoomTask.kt
@@ -44,6 +44,7 @@ import org.matrix.android.sdk.internal.database.model.ChunkEntity
 import org.matrix.android.sdk.internal.database.model.CurrentStateEventEntity
 import org.matrix.android.sdk.internal.database.model.EventInsertType
 import org.matrix.android.sdk.internal.database.model.RoomEntity
+import org.matrix.android.sdk.internal.database.model.RoomMembersLoadStatusType
 import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity
 import org.matrix.android.sdk.internal.database.query.copyToRealmOrIgnore
 import org.matrix.android.sdk.internal.database.query.getOrCreate
@@ -94,6 +95,7 @@ internal class DefaultCreateLocalRoomTask @Inject constructor(
         RoomEntity.getOrCreate(realm, roomId).apply {
             membership = Membership.JOIN
             chunks.add(createLocalRoomChunk(realm, roomId, createRoomBody))
+            membersLoadStatus = RoomMembersLoadStatusType.LOADED
         }
     }
 
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt
index 6e72cbdb8f..d1eb8794bf 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt
@@ -34,7 +34,6 @@ import okhttp3.internal.closeQuietly
 import org.matrix.android.sdk.api.MatrixCoroutineDispatchers
 import org.matrix.android.sdk.api.extensions.tryOrNull
 import org.matrix.android.sdk.api.session.room.model.Membership
-import org.matrix.android.sdk.api.session.room.model.localecho.RoomLocalEcho
 import org.matrix.android.sdk.api.session.room.timeline.Timeline
 import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
 import org.matrix.android.sdk.api.session.room.timeline.TimelineSettings
@@ -390,7 +389,6 @@ internal class DefaultTimeline(
     }
 
     private suspend fun loadRoomMembersIfNeeded() {
-        if (RoomLocalEcho.isLocalEchoId(roomId)) return
         val loadRoomMembersParam = LoadRoomMembersTask.Params(roomId, excludeMembership = Membership.LEAVE)
         try {
             loadRoomMembersTask.execute(loadRoomMembersParam)

From 2b6bfc1ebc8a3ae89d68d23fde5c5cba7ff847bf Mon Sep 17 00:00:00 2001
From: Florian Renaud <florianr@element.io>
Date: Mon, 4 Jul 2022 18:00:30 +0200
Subject: [PATCH 21/26] Create local events using local echo

---
 .../session/room/create/CreateLocalRoomTask.kt     | 14 +++++++-------
 1 file changed, 7 insertions(+), 7 deletions(-)

diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateLocalRoomTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateLocalRoomTask.kt
index eb35dd2387..94c300c3c3 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateLocalRoomTask.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateLocalRoomTask.kt
@@ -24,6 +24,7 @@ import org.matrix.android.sdk.api.extensions.tryOrNull
 import org.matrix.android.sdk.api.session.events.model.Content
 import org.matrix.android.sdk.api.session.events.model.Event
 import org.matrix.android.sdk.api.session.events.model.EventType
+import org.matrix.android.sdk.api.session.events.model.LocalEcho
 import org.matrix.android.sdk.api.session.events.model.toContent
 import org.matrix.android.sdk.api.session.room.model.GuestAccess
 import org.matrix.android.sdk.api.session.room.model.Membership
@@ -58,7 +59,6 @@ import org.matrix.android.sdk.internal.session.room.timeline.PaginationDirection
 import org.matrix.android.sdk.internal.task.Task
 import org.matrix.android.sdk.internal.util.awaitTransaction
 import org.matrix.android.sdk.internal.util.time.Clock
-import java.util.UUID
 import javax.inject.Inject
 
 internal interface CreateLocalRoomTask : Task<CreateRoomParams, String>
@@ -185,13 +185,13 @@ internal class DefaultCreateLocalRoomTask @Inject constructor(
         val invitedUsers = createRoomBody.invitedUserIds.orEmpty()
                 .mapNotNull { tryOrNull { userService.resolveUser(it) } }
 
-        val createRoomEvent = createFakeEvent(
+        val createRoomEvent = createLocalEvent(
                 type = EventType.STATE_ROOM_CREATE,
                 content = RoomCreateContent(
                         creator = userId
                 ).toContent()
         )
-        val myRoomMemberEvent = createFakeEvent(
+        val myRoomMemberEvent = createLocalEvent(
                 type = EventType.STATE_ROOM_MEMBER,
                 content = RoomMemberContent(
                         membership = Membership.JOIN,
@@ -201,7 +201,7 @@ internal class DefaultCreateLocalRoomTask @Inject constructor(
                 stateKey = userId
         )
         val roomMemberEvents = invitedUsers.map {
-            createFakeEvent(
+            createLocalEvent(
                     type = EventType.STATE_ROOM_MEMBER,
                     content = RoomMemberContent(
                             isDirect = createRoomBody.isDirect.orFalse(),
@@ -216,7 +216,7 @@ internal class DefaultCreateLocalRoomTask @Inject constructor(
         return buildList {
             add(createRoomEvent)
             add(myRoomMemberEvent)
-            addAll(createRoomBody.initialStates.orEmpty().map { createFakeEvent(it.type, it.content, it.stateKey) })
+            addAll(createRoomBody.initialStates.orEmpty().map { createLocalEvent(it.type, it.content, it.stateKey) })
             addAll(roomMemberEvents)
         }
     }
@@ -230,14 +230,14 @@ internal class DefaultCreateLocalRoomTask @Inject constructor(
      *
      * @return a fake event
      */
-    private fun createFakeEvent(type: String?, content: Content?, stateKey: String? = ""): Event {
+    private fun createLocalEvent(type: String?, content: Content?, stateKey: String? = ""): Event {
         return Event(
                 type = type,
                 senderId = userId,
                 stateKey = stateKey,
                 content = content,
                 originServerTs = clock.epochMillis(),
-                eventId = UUID.randomUUID().toString()
+                eventId = LocalEcho.createLocalEchoId()
         )
     }
 

From 414dc52f7d56aee57dc4a380a8590a2229e01ae4 Mon Sep 17 00:00:00 2001
From: Florian Renaud <florianr@element.io>
Date: Mon, 4 Jul 2022 18:03:43 +0200
Subject: [PATCH 22/26] Fix copyright date

---
 .../sdk/api/session/room/model/localecho/RoomLocalEcho.kt       | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/localecho/RoomLocalEcho.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/localecho/RoomLocalEcho.kt
index a9804c6dac..e48baff5a2 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/localecho/RoomLocalEcho.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/localecho/RoomLocalEcho.kt
@@ -1,5 +1,5 @@
 /*
- * Copyright 2020 The Matrix.org Foundation C.I.C.
+ * Copyright 2022 The Matrix.org Foundation C.I.C.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.

From fb87d31ce479124a8cfcbeba109ae42d9e56fe1c Mon Sep 17 00:00:00 2001
From: Florian Renaud <florianr@element.io>
Date: Mon, 11 Jul 2022 15:24:48 +0200
Subject: [PATCH 23/26] Update the title of the local room timeline

---
 .../im/vector/app/features/home/room/detail/TimelineFragment.kt | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt
index d327bb48d4..25947cc22b 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt
@@ -1709,7 +1709,7 @@ class TimelineFragment @Inject constructor(
                 views.includeRoomToolbar.roomToolbarContentView.isVisible = false
                 views.includeThreadToolbar.roomToolbarThreadConstraintLayout.isVisible = false
                 setupToolbar(views.roomToolbar)
-                        .setTitle(R.string.fab_menu_create_chat)
+                        .setTitle(R.string.room_member_open_or_create_dm)
                         .allowBack(useCross = true)
             }
             isThreadTimeLine() -> {

From a10a8ce5ca8571e04fd11cef97b86560a06a0ab2 Mon Sep 17 00:00:00 2001
From: Florian Renaud <florianr@element.io>
Date: Mon, 11 Jul 2022 16:19:27 +0200
Subject: [PATCH 24/26] Add margin after the action button of the user list
 toolbar

---
 vector/src/main/res/layout/fragment_user_list.xml | 6 ++++--
 1 file changed, 4 insertions(+), 2 deletions(-)

diff --git a/vector/src/main/res/layout/fragment_user_list.xml b/vector/src/main/res/layout/fragment_user_list.xml
index b3149f05c5..989cb2feef 100644
--- a/vector/src/main/res/layout/fragment_user_list.xml
+++ b/vector/src/main/res/layout/fragment_user_list.xml
@@ -15,7 +15,9 @@
             android:id="@+id/userListToolbar"
             android:layout_width="match_parent"
             android:layout_height="?actionBarSize"
-            app:title="@string/fab_menu_create_chat"/>
+            android:paddingEnd="@dimen/layout_horizontal_margin"
+            app:title="@string/fab_menu_create_chat"
+            tools:ignore="RtlSymmetry" />
 
     </com.google.android.material.appbar.AppBarLayout>
 
@@ -94,4 +96,4 @@
         app:layout_constraintTop_toBottomOf="@id/userListE2EbyDefaultDisabled"
         tools:listitem="@layout/item_known_user" />
 
-</androidx.constraintlayout.widget.ConstraintLayout>
\ No newline at end of file
+</androidx.constraintlayout.widget.ConstraintLayout>

From fdb9ed80d4955a28b35aa708e39258edcbf5b588 Mon Sep 17 00:00:00 2001
From: Florian Renaud <florianr@element.io>
Date: Mon, 11 Jul 2022 16:22:49 +0200
Subject: [PATCH 25/26] Change method visibility

---
 .../sdk/api/session/room/model/localecho/RoomLocalEcho.kt    | 5 ++++-
 1 file changed, 4 insertions(+), 1 deletion(-)

diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/localecho/RoomLocalEcho.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/localecho/RoomLocalEcho.kt
index e48baff5a2..7ef0d63924 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/localecho/RoomLocalEcho.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/localecho/RoomLocalEcho.kt
@@ -22,7 +22,10 @@ object RoomLocalEcho {
 
     private const val PREFIX = "!local."
 
+    /**
+     * Tell whether the provider room id is a local id.
+     */
     fun isLocalEchoId(roomId: String) = roomId.startsWith(PREFIX)
 
-    fun createLocalEchoId() = "${PREFIX}${UUID.randomUUID()}"
+    internal fun createLocalEchoId() = "${PREFIX}${UUID.randomUUID()}"
 }

From fca4df352253a8feddc5d590771038c293ad9b10 Mon Sep 17 00:00:00 2001
From: Florian Renaud <florianr@element.io>
Date: Mon, 11 Jul 2022 16:44:32 +0200
Subject: [PATCH 26/26] Revert "Ensure that Realm is up to date before
 returning the roomId"

This reverts commit 8a5a47c6a57863b87f62ca2e123fe619d1d9b612.
---
 .../room/create/CreateLocalRoomTask.kt        | 19 +++++++++++++++++--
 1 file changed, 17 insertions(+), 2 deletions(-)

diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateLocalRoomTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateLocalRoomTask.kt
index 94c300c3c3..d57491a4c8 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateLocalRoomTask.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateLocalRoomTask.kt
@@ -18,7 +18,9 @@ package org.matrix.android.sdk.internal.session.room.create
 
 import com.zhuinden.monarchy.Monarchy
 import io.realm.Realm
+import io.realm.RealmConfiguration
 import io.realm.kotlin.createObject
+import kotlinx.coroutines.TimeoutCancellationException
 import org.matrix.android.sdk.api.extensions.orFalse
 import org.matrix.android.sdk.api.extensions.tryOrNull
 import org.matrix.android.sdk.api.session.events.model.Content
@@ -26,6 +28,7 @@ import org.matrix.android.sdk.api.session.events.model.Event
 import org.matrix.android.sdk.api.session.events.model.EventType
 import org.matrix.android.sdk.api.session.events.model.LocalEcho
 import org.matrix.android.sdk.api.session.events.model.toContent
+import org.matrix.android.sdk.api.session.room.failure.CreateRoomFailure
 import org.matrix.android.sdk.api.session.room.model.GuestAccess
 import org.matrix.android.sdk.api.session.room.model.Membership
 import org.matrix.android.sdk.api.session.room.model.RoomDirectoryVisibility
@@ -38,6 +41,7 @@ import org.matrix.android.sdk.api.session.room.send.SendState
 import org.matrix.android.sdk.api.session.sync.model.RoomSyncSummary
 import org.matrix.android.sdk.api.session.user.UserService
 import org.matrix.android.sdk.api.session.user.model.User
+import org.matrix.android.sdk.internal.database.awaitNotEmptyResult
 import org.matrix.android.sdk.internal.database.helper.addTimelineEvent
 import org.matrix.android.sdk.internal.database.mapper.asDomain
 import org.matrix.android.sdk.internal.database.mapper.toEntity
@@ -47,6 +51,7 @@ import org.matrix.android.sdk.internal.database.model.EventInsertType
 import org.matrix.android.sdk.internal.database.model.RoomEntity
 import org.matrix.android.sdk.internal.database.model.RoomMembersLoadStatusType
 import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity
+import org.matrix.android.sdk.internal.database.model.RoomSummaryEntityFields
 import org.matrix.android.sdk.internal.database.query.copyToRealmOrIgnore
 import org.matrix.android.sdk.internal.database.query.getOrCreate
 import org.matrix.android.sdk.internal.database.query.getOrNull
@@ -59,6 +64,7 @@ import org.matrix.android.sdk.internal.session.room.timeline.PaginationDirection
 import org.matrix.android.sdk.internal.task.Task
 import org.matrix.android.sdk.internal.util.awaitTransaction
 import org.matrix.android.sdk.internal.util.time.Clock
+import java.util.concurrent.TimeUnit
 import javax.inject.Inject
 
 internal interface CreateLocalRoomTask : Task<CreateRoomParams, String>
@@ -68,6 +74,7 @@ internal class DefaultCreateLocalRoomTask @Inject constructor(
         @SessionDatabase private val monarchy: Monarchy,
         private val roomMemberEventHandler: RoomMemberEventHandler,
         private val roomSummaryUpdater: RoomSummaryUpdater,
+        @SessionDatabase private val realmConfiguration: RealmConfiguration,
         private val createRoomBodyBuilder: CreateRoomBodyBuilder,
         private val userService: UserService,
         private val clock: Clock,
@@ -81,8 +88,16 @@ internal class DefaultCreateLocalRoomTask @Inject constructor(
             createLocalRoomSummaryEntity(realm, roomId, createRoomBody)
         }
 
-        // Ensure that Realm is up to date before returning the roomId.
-        monarchy.doWithRealm { it.refresh() }
+        // Wait for room to be created in DB
+        try {
+            awaitNotEmptyResult(realmConfiguration, TimeUnit.MINUTES.toMillis(1L)) { realm ->
+                realm.where(RoomSummaryEntity::class.java)
+                        .equalTo(RoomSummaryEntityFields.ROOM_ID, roomId)
+                        .equalTo(RoomSummaryEntityFields.MEMBERSHIP_STR, Membership.JOIN.name)
+            }
+        } catch (exception: TimeoutCancellationException) {
+            throw CreateRoomFailure.CreatedWithTimeout(roomId)
+        }
 
         return roomId
     }