-
Notifications
You must be signed in to change notification settings - Fork 751
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Start DM on first message (UI) #6051
Changes from 18 commits
c9ab092
ba3d350
26d1a12
10d683a
7ea2d0a
e86f919
b144bac
a46d7ed
554d35f
398e98a
d42a3da
3f087eb
33a19c1
71320e4
c7db896
7415623
0dad4cb
c25edfa
8a5a47c
f4b50f1
0d9cd2b
e89bb0e
2b6bfc1
414dc52
68bd55f
fb87d31
a10a8ce
fdb9ed8
fca4df3
53eb852
b8cdf9d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
Create DM room only on first message - Design implementation & debug feature flag |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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()}" | ||
} | ||
Florian14 marked this conversation as resolved.
Show resolved
Hide resolved
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,265 @@ | ||
/* | ||
* 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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I do not think we need to wait in this case. We are not waiting for a sync response including the new room. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe just replacing with this line can help (I let you do some test):
|
||
try { | ||
awaitNotEmptyResult(realmConfiguration, TimeUnit.MINUTES.toMillis(1L)) { realm -> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. for my understanding, are we relying on another task to asynchronously update the room entity? if possible it would be safer to sequentially run the tasks rather than rely on a timeout (is this a common pattern in the sdk?) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @ouchadam I agree with you, I'd like to remove it but I have a non-systematic but recurrent crash when I try to navigate to the local room (in the stacktrace, it seems that we try to navigate to un unknown room). It's a bit confusing because the awaitTransaction seems ok, I can log the entity before returning the roomId from the task. Did I miss something else? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The second task is waiting to get the room from the sync but in the current case, the room is created locally and put in DB, so we should not have to wait for it 😕 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sorry for the cross conversations, I did not see @ouchadam 's comments at first sight. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
It continues crashing... |
||
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 roomMemberContentsByUser = HashMap<String, RoomMemberContent?>() | ||
|
||
for (event in eventList) { | ||
if (event.eventId == null || event.senderId == null || event.type == null) { | ||
continue | ||
} | ||
|
||
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 | ||
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 { | ||
Florian14 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
return Event( | ||
type = type, | ||
senderId = userId, | ||
stateKey = stateKey, | ||
content = content, | ||
originServerTs = clock.epochMillis(), | ||
eventId = UUID.randomUUID().toString() | ||
Florian14 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
) | ||
} | ||
|
||
/** | ||
* 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 | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
thinking out loud about about the api design~ not a blocker!
from a consumer api perspective, are local rooms semantically the same as normal rooms? would we handle interacting with rooms differently if they're normal vs local?
I'm wondering if
CreateRoomParams
could have aisLocal
option and reusingcreateRoom
andleaveRoom
🤔