forked from GitHub-Mirror/riotX-android
Room update: start handling tombstone and room create events [WIP]
This commit is contained in:
parent
6176520805
commit
9e5c70dda3
@ -34,5 +34,6 @@ data class RoomSummary(
|
|||||||
val notificationCount: Int = 0,
|
val notificationCount: Int = 0,
|
||||||
val highlightCount: Int = 0,
|
val highlightCount: Int = 0,
|
||||||
val tags: List<RoomTag> = emptyList(),
|
val tags: List<RoomTag> = emptyList(),
|
||||||
val membership: Membership = Membership.NONE
|
val membership: Membership = Membership.NONE,
|
||||||
|
val isVersioned: Boolean = false
|
||||||
)
|
)
|
@ -0,0 +1,42 @@
|
|||||||
|
/*
|
||||||
|
*
|
||||||
|
* * Copyright 2019 New Vector Ltd
|
||||||
|
* *
|
||||||
|
* * Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* * you may not use this file except in compliance with the License.
|
||||||
|
* * You may obtain a copy of the License at
|
||||||
|
* *
|
||||||
|
* * http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
* *
|
||||||
|
* * Unless required by applicable law or agreed to in writing, software
|
||||||
|
* * distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* * See the License for the specific language governing permissions and
|
||||||
|
* * limitations under the License.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
package im.vector.matrix.android.api.session.room.model.create
|
||||||
|
|
||||||
|
import com.squareup.moshi.Json
|
||||||
|
import com.squareup.moshi.JsonClass
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Content of a m.room.create type event
|
||||||
|
*/
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
|
data class RoomCreateContent(
|
||||||
|
@Json(name = "creator") val creator: String,
|
||||||
|
@Json(name = "room_version") val roomVersion: String?,
|
||||||
|
@Json(name = "predecessor") val predecessor: Predecessor?
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A link to an old room in case of room versioning
|
||||||
|
*/
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
|
data class Predecessor(
|
||||||
|
@Json(name = "room_id") val roomId: String,
|
||||||
|
@Json(name = "event_id") val eventId: String
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,31 @@
|
|||||||
|
/*
|
||||||
|
*
|
||||||
|
* * Copyright 2019 New Vector Ltd
|
||||||
|
* *
|
||||||
|
* * Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* * you may not use this file except in compliance with the License.
|
||||||
|
* * You may obtain a copy of the License at
|
||||||
|
* *
|
||||||
|
* * http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
* *
|
||||||
|
* * Unless required by applicable law or agreed to in writing, software
|
||||||
|
* * distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* * See the License for the specific language governing permissions and
|
||||||
|
* * limitations under the License.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
package im.vector.matrix.android.api.session.room.model.tombstone
|
||||||
|
|
||||||
|
import com.squareup.moshi.Json
|
||||||
|
import com.squareup.moshi.JsonClass
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class to contains Tombstone information
|
||||||
|
*/
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
|
data class RoomTombstoneContent(
|
||||||
|
@Json(name = "body") val body: String? = null,
|
||||||
|
@Json(name = "replacement_room") val replacementRoom: String
|
||||||
|
)
|
@ -17,6 +17,7 @@
|
|||||||
package im.vector.matrix.android.api.session.room.state
|
package im.vector.matrix.android.api.session.room.state
|
||||||
|
|
||||||
import im.vector.matrix.android.api.MatrixCallback
|
import im.vector.matrix.android.api.MatrixCallback
|
||||||
|
import im.vector.matrix.android.api.session.events.model.Event
|
||||||
|
|
||||||
interface StateService {
|
interface StateService {
|
||||||
|
|
||||||
@ -25,4 +26,6 @@ interface StateService {
|
|||||||
*/
|
*/
|
||||||
fun updateTopic(topic: String, callback: MatrixCallback<Unit>)
|
fun updateTopic(topic: String, callback: MatrixCallback<Unit>)
|
||||||
|
|
||||||
|
fun getStateEvent(eventType: String): Event?
|
||||||
|
|
||||||
}
|
}
|
@ -61,7 +61,8 @@ internal class RoomSummaryMapper @Inject constructor(
|
|||||||
highlightCount = roomSummaryEntity.highlightCount,
|
highlightCount = roomSummaryEntity.highlightCount,
|
||||||
notificationCount = roomSummaryEntity.notificationCount,
|
notificationCount = roomSummaryEntity.notificationCount,
|
||||||
tags = tags,
|
tags = tags,
|
||||||
membership = roomSummaryEntity.membership
|
membership = roomSummaryEntity.membership,
|
||||||
|
isVersioned = roomSummaryEntity.isVersioned
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -35,7 +35,8 @@ internal open class RoomSummaryEntity(@PrimaryKey var roomId: String = "",
|
|||||||
var otherMemberIds: RealmList<String> = RealmList(),
|
var otherMemberIds: RealmList<String> = RealmList(),
|
||||||
var notificationCount: Int = 0,
|
var notificationCount: Int = 0,
|
||||||
var highlightCount: Int = 0,
|
var highlightCount: Int = 0,
|
||||||
var tags: RealmList<RoomTagEntity> = RealmList()
|
var tags: RealmList<RoomTagEntity> = RealmList(),
|
||||||
|
var isVersioned: Boolean = false
|
||||||
) : RealmObject() {
|
) : RealmObject() {
|
||||||
|
|
||||||
private var membershipStr: String = Membership.NONE.name
|
private var membershipStr: String = Membership.NONE.name
|
||||||
|
@ -37,6 +37,7 @@ import im.vector.matrix.android.internal.network.AccessTokenInterceptor
|
|||||||
import im.vector.matrix.android.internal.network.RetrofitFactory
|
import im.vector.matrix.android.internal.network.RetrofitFactory
|
||||||
import im.vector.matrix.android.internal.session.group.GroupSummaryUpdater
|
import im.vector.matrix.android.internal.session.group.GroupSummaryUpdater
|
||||||
import im.vector.matrix.android.internal.session.room.EventRelationsAggregationUpdater
|
import im.vector.matrix.android.internal.session.room.EventRelationsAggregationUpdater
|
||||||
|
import im.vector.matrix.android.internal.session.room.tombstone.RoomTombstoneEventLiveObserver
|
||||||
import im.vector.matrix.android.internal.session.room.prune.EventsPruner
|
import im.vector.matrix.android.internal.session.room.prune.EventsPruner
|
||||||
import im.vector.matrix.android.internal.util.md5
|
import im.vector.matrix.android.internal.util.md5
|
||||||
import io.realm.RealmConfiguration
|
import io.realm.RealmConfiguration
|
||||||
@ -128,6 +129,11 @@ internal abstract class SessionModule {
|
|||||||
@IntoSet
|
@IntoSet
|
||||||
abstract fun bindEventRelationsAggregationUpdater(groupSummaryUpdater: EventRelationsAggregationUpdater): LiveEntityObserver
|
abstract fun bindEventRelationsAggregationUpdater(groupSummaryUpdater: EventRelationsAggregationUpdater): LiveEntityObserver
|
||||||
|
|
||||||
|
@Binds
|
||||||
|
@IntoSet
|
||||||
|
abstract fun bindRoomCreateEventLiveObserver(roomTombstoneEventLiveObserver: RoomTombstoneEventLiveObserver): LiveEntityObserver
|
||||||
|
|
||||||
|
|
||||||
@Binds
|
@Binds
|
||||||
abstract fun bindInitialSyncProgressService(initialSyncProgressService: DefaultInitialSyncProgressService): InitialSyncProgressService
|
abstract fun bindInitialSyncProgressService(initialSyncProgressService: DefaultInitialSyncProgressService): InitialSyncProgressService
|
||||||
|
|
||||||
|
@ -54,7 +54,11 @@ internal class DefaultRoomService @Inject constructor(private val monarchy: Mona
|
|||||||
|
|
||||||
override fun liveRoomSummaries(): LiveData<List<RoomSummary>> {
|
override fun liveRoomSummaries(): LiveData<List<RoomSummary>> {
|
||||||
return monarchy.findAllMappedWithChanges(
|
return monarchy.findAllMappedWithChanges(
|
||||||
{ realm -> RoomSummaryEntity.where(realm).isNotEmpty(RoomSummaryEntityFields.DISPLAY_NAME) },
|
{ realm ->
|
||||||
|
RoomSummaryEntity.where(realm)
|
||||||
|
.isNotEmpty(RoomSummaryEntityFields.DISPLAY_NAME)
|
||||||
|
.notEqualTo(RoomSummaryEntityFields.IS_VERSIONED, true)
|
||||||
|
},
|
||||||
{ roomSummaryMapper.map(it) }
|
{ roomSummaryMapper.map(it) }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -22,6 +22,7 @@ import im.vector.matrix.android.api.auth.data.Credentials
|
|||||||
import im.vector.matrix.android.api.session.crypto.CryptoService
|
import im.vector.matrix.android.api.session.crypto.CryptoService
|
||||||
import im.vector.matrix.android.api.session.room.Room
|
import im.vector.matrix.android.api.session.room.Room
|
||||||
import im.vector.matrix.android.internal.database.mapper.RoomSummaryMapper
|
import im.vector.matrix.android.internal.database.mapper.RoomSummaryMapper
|
||||||
|
import im.vector.matrix.android.internal.di.SessionDatabase
|
||||||
import im.vector.matrix.android.internal.session.room.membership.DefaultMembershipService
|
import im.vector.matrix.android.internal.session.room.membership.DefaultMembershipService
|
||||||
import im.vector.matrix.android.internal.session.room.membership.LoadRoomMembersTask
|
import im.vector.matrix.android.internal.session.room.membership.LoadRoomMembersTask
|
||||||
import im.vector.matrix.android.internal.session.room.membership.joining.InviteTask
|
import im.vector.matrix.android.internal.session.room.membership.joining.InviteTask
|
||||||
@ -40,11 +41,14 @@ import im.vector.matrix.android.internal.session.room.timeline.DefaultTimelineSe
|
|||||||
import im.vector.matrix.android.internal.session.room.timeline.GetContextOfEventTask
|
import im.vector.matrix.android.internal.session.room.timeline.GetContextOfEventTask
|
||||||
import im.vector.matrix.android.internal.session.room.timeline.PaginationTask
|
import im.vector.matrix.android.internal.session.room.timeline.PaginationTask
|
||||||
import im.vector.matrix.android.internal.task.TaskExecutor
|
import im.vector.matrix.android.internal.task.TaskExecutor
|
||||||
|
import io.realm.RealmConfiguration
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
internal class RoomFactory @Inject constructor(private val context: Context,
|
internal class RoomFactory @Inject constructor(private val context: Context,
|
||||||
private val credentials: Credentials,
|
private val credentials: Credentials,
|
||||||
private val monarchy: Monarchy,
|
private val monarchy: Monarchy,
|
||||||
|
@SessionDatabase
|
||||||
|
private val realmConfiguration: RealmConfiguration,
|
||||||
private val eventFactory: LocalEchoEventFactory,
|
private val eventFactory: LocalEchoEventFactory,
|
||||||
private val roomSummaryMapper: RoomSummaryMapper,
|
private val roomSummaryMapper: RoomSummaryMapper,
|
||||||
private val taskExecutor: TaskExecutor,
|
private val taskExecutor: TaskExecutor,
|
||||||
@ -63,7 +67,7 @@ internal class RoomFactory @Inject constructor(private val context: Context,
|
|||||||
fun create(roomId: String): Room {
|
fun create(roomId: String): Room {
|
||||||
val timelineService = DefaultTimelineService(roomId, monarchy, taskExecutor, contextOfEventTask, cryptoService, paginationTask)
|
val timelineService = DefaultTimelineService(roomId, monarchy, taskExecutor, contextOfEventTask, cryptoService, paginationTask)
|
||||||
val sendService = DefaultSendService(context, credentials, roomId, eventFactory, cryptoService, monarchy)
|
val sendService = DefaultSendService(context, credentials, roomId, eventFactory, cryptoService, monarchy)
|
||||||
val stateService = DefaultStateService(roomId, taskExecutor, sendStateTask)
|
val stateService = DefaultStateService(roomId, realmConfiguration, taskExecutor, sendStateTask)
|
||||||
val roomMembersService = DefaultMembershipService(roomId, monarchy, taskExecutor, loadRoomMembersTask, inviteTask, joinRoomTask, leaveRoomTask)
|
val roomMembersService = DefaultMembershipService(roomId, monarchy, taskExecutor, loadRoomMembersTask, inviteTask, joinRoomTask, leaveRoomTask)
|
||||||
val readService = DefaultReadService(roomId, monarchy, taskExecutor, setReadMarkersTask, credentials)
|
val readService = DefaultReadService(roomId, monarchy, taskExecutor, setReadMarkersTask, credentials)
|
||||||
val relationService = DefaultRelationService(context,
|
val relationService = DefaultRelationService(context,
|
||||||
|
@ -17,16 +17,32 @@
|
|||||||
package im.vector.matrix.android.internal.session.room.state
|
package im.vector.matrix.android.internal.session.room.state
|
||||||
|
|
||||||
import im.vector.matrix.android.api.MatrixCallback
|
import im.vector.matrix.android.api.MatrixCallback
|
||||||
|
import im.vector.matrix.android.api.session.events.model.Event
|
||||||
import im.vector.matrix.android.api.session.events.model.EventType
|
import im.vector.matrix.android.api.session.events.model.EventType
|
||||||
import im.vector.matrix.android.api.session.room.state.StateService
|
import im.vector.matrix.android.api.session.room.state.StateService
|
||||||
|
import im.vector.matrix.android.internal.database.mapper.asDomain
|
||||||
|
import im.vector.matrix.android.internal.database.model.EventEntity
|
||||||
|
import im.vector.matrix.android.internal.database.query.prev
|
||||||
|
import im.vector.matrix.android.internal.database.query.where
|
||||||
|
import im.vector.matrix.android.internal.di.SessionDatabase
|
||||||
import im.vector.matrix.android.internal.task.TaskExecutor
|
import im.vector.matrix.android.internal.task.TaskExecutor
|
||||||
import im.vector.matrix.android.internal.task.configureWith
|
import im.vector.matrix.android.internal.task.configureWith
|
||||||
|
import io.realm.Realm
|
||||||
|
import io.realm.RealmConfiguration
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
internal class DefaultStateService @Inject constructor(private val roomId: String,
|
internal class DefaultStateService @Inject constructor(private val roomId: String,
|
||||||
|
@SessionDatabase
|
||||||
|
private val realmConfiguration: RealmConfiguration,
|
||||||
private val taskExecutor: TaskExecutor,
|
private val taskExecutor: TaskExecutor,
|
||||||
private val sendStateTask: SendStateTask) : StateService {
|
private val sendStateTask: SendStateTask) : StateService {
|
||||||
|
|
||||||
|
override fun getStateEvent(eventType: String): Event? {
|
||||||
|
return Realm.getInstance(realmConfiguration).use { realm ->
|
||||||
|
EventEntity.where(realm, roomId, eventType).prev()?.asDomain()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun updateTopic(topic: String, callback: MatrixCallback<Unit>) {
|
override fun updateTopic(topic: String, callback: MatrixCallback<Unit>) {
|
||||||
val params = SendStateTask.Params(roomId,
|
val params = SendStateTask.Params(roomId,
|
||||||
EventType.STATE_ROOM_TOPIC,
|
EventType.STATE_ROOM_TOPIC,
|
||||||
@ -39,4 +55,6 @@ internal class DefaultStateService @Inject constructor(private val roomId: Strin
|
|||||||
.dispatchTo(callback)
|
.dispatchTo(callback)
|
||||||
.executeBy(taskExecutor)
|
.executeBy(taskExecutor)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
@ -0,0 +1,75 @@
|
|||||||
|
/*
|
||||||
|
*
|
||||||
|
* * Copyright 2019 New Vector Ltd
|
||||||
|
* *
|
||||||
|
* * Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* * you may not use this file except in compliance with the License.
|
||||||
|
* * You may obtain a copy of the License at
|
||||||
|
* *
|
||||||
|
* * http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
* *
|
||||||
|
* * Unless required by applicable law or agreed to in writing, software
|
||||||
|
* * distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* * See the License for the specific language governing permissions and
|
||||||
|
* * limitations under the License.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
package im.vector.matrix.android.internal.session.room.tombstone
|
||||||
|
|
||||||
|
import com.zhuinden.monarchy.Monarchy
|
||||||
|
import im.vector.matrix.android.api.session.events.model.Event
|
||||||
|
import im.vector.matrix.android.api.session.events.model.EventType
|
||||||
|
import im.vector.matrix.android.api.session.events.model.toModel
|
||||||
|
import im.vector.matrix.android.api.session.room.model.tombstone.RoomTombstoneContent
|
||||||
|
import im.vector.matrix.android.internal.database.RealmLiveEntityObserver
|
||||||
|
import im.vector.matrix.android.internal.database.mapper.asDomain
|
||||||
|
import im.vector.matrix.android.internal.database.model.EventEntity
|
||||||
|
import im.vector.matrix.android.internal.database.model.RoomSummaryEntity
|
||||||
|
import im.vector.matrix.android.internal.database.query.types
|
||||||
|
import im.vector.matrix.android.internal.database.query.where
|
||||||
|
import im.vector.matrix.android.internal.di.SessionDatabase
|
||||||
|
import io.realm.OrderedCollectionChangeSet
|
||||||
|
import io.realm.Realm
|
||||||
|
import io.realm.RealmConfiguration
|
||||||
|
import io.realm.RealmResults
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
internal class RoomTombstoneEventLiveObserver @Inject constructor(@SessionDatabase
|
||||||
|
realmConfiguration: RealmConfiguration)
|
||||||
|
: RealmLiveEntityObserver<EventEntity>(realmConfiguration) {
|
||||||
|
|
||||||
|
override val query = Monarchy.Query<EventEntity> {
|
||||||
|
EventEntity.types(it, listOf(EventType.STATE_ROOM_TOMBSTONE))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onChange(results: RealmResults<EventEntity>, changeSet: OrderedCollectionChangeSet) {
|
||||||
|
changeSet.insertions
|
||||||
|
.asSequence()
|
||||||
|
.mapNotNull {
|
||||||
|
results[it]?.asDomain()
|
||||||
|
}
|
||||||
|
.toList()
|
||||||
|
.also {
|
||||||
|
handleRoomTombstoneEvents(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleRoomTombstoneEvents(tombstoneEvents: List<Event>) = Realm.getInstance(realmConfiguration).use {
|
||||||
|
it.executeTransactionAsync { realm ->
|
||||||
|
for (event in tombstoneEvents) {
|
||||||
|
if (event.roomId == null) continue
|
||||||
|
val createRoomContent = event.getClearContent().toModel<RoomTombstoneContent>()
|
||||||
|
if (createRoomContent?.replacementRoom == null) continue
|
||||||
|
|
||||||
|
val predecessorRoomSummary = RoomSummaryEntity.where(realm, event.roomId).findFirst()
|
||||||
|
?: RoomSummaryEntity(event.roomId)
|
||||||
|
predecessorRoomSummary.isVersioned = true
|
||||||
|
realm.insertOrUpdate(predecessorRoomSummary)
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -146,8 +146,7 @@ internal class RoomSyncHandler @Inject constructor(private val monarchy: Monarch
|
|||||||
roomEntity,
|
roomEntity,
|
||||||
roomSync.timeline.events,
|
roomSync.timeline.events,
|
||||||
roomSync.timeline.prevToken,
|
roomSync.timeline.prevToken,
|
||||||
roomSync.timeline.limited,
|
roomSync.timeline.limited
|
||||||
0
|
|
||||||
)
|
)
|
||||||
roomEntity.addOrUpdate(chunkEntity)
|
roomEntity.addOrUpdate(chunkEntity)
|
||||||
}
|
}
|
||||||
@ -195,15 +194,18 @@ internal class RoomSyncHandler @Inject constructor(private val monarchy: Monarch
|
|||||||
roomEntity: RoomEntity,
|
roomEntity: RoomEntity,
|
||||||
eventList: List<Event>,
|
eventList: List<Event>,
|
||||||
prevToken: String? = null,
|
prevToken: String? = null,
|
||||||
isLimited: Boolean = true,
|
isLimited: Boolean = true): ChunkEntity {
|
||||||
stateIndexOffset: Int = 0): ChunkEntity {
|
|
||||||
|
|
||||||
val lastChunk = ChunkEntity.findLastLiveChunkFromRoom(realm, roomEntity.roomId)
|
val lastChunk = ChunkEntity.findLastLiveChunkFromRoom(realm, roomEntity.roomId)
|
||||||
|
var stateIndexOffset = 0
|
||||||
val chunkEntity = if (!isLimited && lastChunk != null) {
|
val chunkEntity = if (!isLimited && lastChunk != null) {
|
||||||
lastChunk
|
lastChunk
|
||||||
} else {
|
} else {
|
||||||
realm.createObject<ChunkEntity>().apply { this.prevToken = prevToken }
|
realm.createObject<ChunkEntity>().apply { this.prevToken = prevToken }
|
||||||
}
|
}
|
||||||
|
if (isLimited && lastChunk != null) {
|
||||||
|
stateIndexOffset = lastChunk.lastStateIndex(PaginationDirection.FORWARDS)
|
||||||
|
}
|
||||||
lastChunk?.isLastForward = false
|
lastChunk?.isLastForward = false
|
||||||
chunkEntity.isLastForward = true
|
chunkEntity.isLastForward = true
|
||||||
|
|
||||||
|
@ -31,6 +31,7 @@
|
|||||||
<string name="notice_room_visibility_world_readable">anyone.</string>
|
<string name="notice_room_visibility_world_readable">anyone.</string>
|
||||||
<string name="notice_room_visibility_unknown">unknown (%s).</string>
|
<string name="notice_room_visibility_unknown">unknown (%s).</string>
|
||||||
<string name="notice_end_to_end">%1$s turned on end-to-end encryption (%2$s)</string>
|
<string name="notice_end_to_end">%1$s turned on end-to-end encryption (%2$s)</string>
|
||||||
|
<string name="notice_room_update">%s upgraded this room.</string>
|
||||||
|
|
||||||
<string name="notice_requested_voip_conference">%1$s requested a VoIP conference</string>
|
<string name="notice_requested_voip_conference">%1$s requested a VoIP conference</string>
|
||||||
<string name="notice_voip_started">VoIP conference started</string>
|
<string name="notice_voip_started">VoIP conference started</string>
|
||||||
|
@ -244,7 +244,8 @@ class RoomDetailFragment :
|
|||||||
composerLayout.collapse()
|
composerLayout.collapse()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun enterSpecialMode(event: TimelineEvent, @DrawableRes iconRes: Int, useText: Boolean) {
|
private fun enterSpecialMode(event: TimelineEvent, @DrawableRes
|
||||||
|
iconRes: Int, useText: Boolean) {
|
||||||
commandAutocompletePolicy.enabled = false
|
commandAutocompletePolicy.enabled = false
|
||||||
//switch to expanded bar
|
//switch to expanded bar
|
||||||
composerLayout.composerRelatedMessageTitle.apply {
|
composerLayout.composerRelatedMessageTitle.apply {
|
||||||
@ -533,7 +534,13 @@ class RoomDetailFragment :
|
|||||||
} else if (state.asyncInviter.complete) {
|
} else if (state.asyncInviter.complete) {
|
||||||
vectorBaseActivity.finish()
|
vectorBaseActivity.finish()
|
||||||
}
|
}
|
||||||
composerLayout.setRoomEncrypted(state.isEncrypted)
|
if (state.tombstoneContent == null) {
|
||||||
|
composerLayout.visibility = View.VISIBLE
|
||||||
|
composerLayout.setRoomEncrypted(state.isEncrypted)
|
||||||
|
} else {
|
||||||
|
composerLayout.visibility = View.GONE
|
||||||
|
showSnackWithMessage("TOMBSTONED", duration = Snackbar.LENGTH_INDEFINITE)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun renderRoomSummary(state: RoomDetailViewState) {
|
private fun renderRoomSummary(state: RoomDetailViewState) {
|
||||||
|
@ -30,12 +30,14 @@ import com.squareup.inject.assisted.AssistedInject
|
|||||||
import im.vector.matrix.android.api.MatrixCallback
|
import im.vector.matrix.android.api.MatrixCallback
|
||||||
import im.vector.matrix.android.api.session.Session
|
import im.vector.matrix.android.api.session.Session
|
||||||
import im.vector.matrix.android.api.session.content.ContentAttachmentData
|
import im.vector.matrix.android.api.session.content.ContentAttachmentData
|
||||||
|
import im.vector.matrix.android.api.session.events.model.EventType
|
||||||
import im.vector.matrix.android.api.session.events.model.toModel
|
import im.vector.matrix.android.api.session.events.model.toModel
|
||||||
import im.vector.matrix.android.api.session.file.FileService
|
import im.vector.matrix.android.api.session.file.FileService
|
||||||
import im.vector.matrix.android.api.session.room.model.Membership
|
import im.vector.matrix.android.api.session.room.model.Membership
|
||||||
import im.vector.matrix.android.api.session.room.model.message.MessageContent
|
import im.vector.matrix.android.api.session.room.model.message.MessageContent
|
||||||
import im.vector.matrix.android.api.session.room.model.message.MessageType
|
import im.vector.matrix.android.api.session.room.model.message.MessageType
|
||||||
import im.vector.matrix.android.api.session.room.model.message.getFileUrl
|
import im.vector.matrix.android.api.session.room.model.message.getFileUrl
|
||||||
|
import im.vector.matrix.android.api.session.room.model.tombstone.RoomTombstoneContent
|
||||||
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
|
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
|
||||||
import im.vector.matrix.android.internal.crypto.attachments.toElementToDecrypt
|
import im.vector.matrix.android.internal.crypto.attachments.toElementToDecrypt
|
||||||
import im.vector.matrix.android.internal.crypto.model.event.EncryptedEventContent
|
import im.vector.matrix.android.internal.crypto.model.event.EncryptedEventContent
|
||||||
@ -94,7 +96,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
|
|||||||
init {
|
init {
|
||||||
observeRoomSummary()
|
observeRoomSummary()
|
||||||
observeEventDisplayedActions()
|
observeEventDisplayedActions()
|
||||||
observeInvitationState()
|
observeSummaryState()
|
||||||
cancelableBag += room.loadRoomMembersIfNeeded()
|
cancelableBag += room.loadRoomMembersIfNeeded()
|
||||||
timeline.start()
|
timeline.start()
|
||||||
setState { copy(timeline = this@RoomDetailViewModel.timeline) }
|
setState { copy(timeline = this@RoomDetailViewModel.timeline) }
|
||||||
@ -547,7 +549,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun observeInvitationState() {
|
private fun observeSummaryState() {
|
||||||
asyncSubscribe(RoomDetailViewState::asyncRoomSummary) { summary ->
|
asyncSubscribe(RoomDetailViewState::asyncRoomSummary) { summary ->
|
||||||
if (summary.membership == Membership.INVITE) {
|
if (summary.membership == Membership.INVITE) {
|
||||||
summary.latestEvent?.root?.senderId?.let { senderId ->
|
summary.latestEvent?.root?.senderId?.let { senderId ->
|
||||||
@ -556,6 +558,13 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
|
|||||||
setState { copy(asyncInviter = Success(it)) }
|
setState { copy(asyncInviter = Success(it)) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (summary.isVersioned) {
|
||||||
|
room.getStateEvent(EventType.STATE_ROOM_TOMBSTONE)
|
||||||
|
?.getClearContent()
|
||||||
|
?.toModel<RoomTombstoneContent>()?.also {
|
||||||
|
setState { copy(tombstoneContent = it) }
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -20,6 +20,7 @@ import com.airbnb.mvrx.Async
|
|||||||
import com.airbnb.mvrx.MvRxState
|
import com.airbnb.mvrx.MvRxState
|
||||||
import com.airbnb.mvrx.Uninitialized
|
import com.airbnb.mvrx.Uninitialized
|
||||||
import im.vector.matrix.android.api.session.room.model.RoomSummary
|
import im.vector.matrix.android.api.session.room.model.RoomSummary
|
||||||
|
import im.vector.matrix.android.api.session.room.model.tombstone.RoomTombstoneContent
|
||||||
import im.vector.matrix.android.api.session.room.timeline.Timeline
|
import im.vector.matrix.android.api.session.room.timeline.Timeline
|
||||||
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
|
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
|
||||||
import im.vector.matrix.android.api.session.user.model.User
|
import im.vector.matrix.android.api.session.user.model.User
|
||||||
@ -46,7 +47,8 @@ data class RoomDetailViewState(
|
|||||||
val asyncInviter: Async<User> = Uninitialized,
|
val asyncInviter: Async<User> = Uninitialized,
|
||||||
val asyncRoomSummary: Async<RoomSummary> = Uninitialized,
|
val asyncRoomSummary: Async<RoomSummary> = Uninitialized,
|
||||||
val sendMode: SendMode = SendMode.REGULAR,
|
val sendMode: SendMode = SendMode.REGULAR,
|
||||||
val isEncrypted: Boolean = false
|
val isEncrypted: Boolean = false,
|
||||||
|
val tombstoneContent: RoomTombstoneContent? = null
|
||||||
) : MvRxState {
|
) : MvRxState {
|
||||||
|
|
||||||
constructor(args: RoomDetailArgs) : this(roomId = args.roomId, eventId = args.eventId)
|
constructor(args: RoomDetailArgs) : this(roomId = args.roomId, eventId = args.eventId)
|
||||||
|
@ -0,0 +1,61 @@
|
|||||||
|
/*
|
||||||
|
*
|
||||||
|
* * Copyright 2019 New Vector Ltd
|
||||||
|
* *
|
||||||
|
* * Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* * you may not use this file except in compliance with the License.
|
||||||
|
* * You may obtain a copy of the License at
|
||||||
|
* *
|
||||||
|
* * http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
* *
|
||||||
|
* * Unless required by applicable law or agreed to in writing, software
|
||||||
|
* * distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* * See the License for the specific language governing permissions and
|
||||||
|
* * limitations under the License.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
package im.vector.riotx.features.home.room.detail.timeline.factory
|
||||||
|
|
||||||
|
import im.vector.matrix.android.api.permalinks.MatrixPermalinkSpan
|
||||||
|
import im.vector.matrix.android.api.permalinks.PermalinkFactory
|
||||||
|
import im.vector.matrix.android.api.session.events.model.toModel
|
||||||
|
import im.vector.matrix.android.api.session.room.model.create.RoomCreateContent
|
||||||
|
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
|
||||||
|
import im.vector.riotx.R
|
||||||
|
import im.vector.riotx.core.resources.ColorProvider
|
||||||
|
import im.vector.riotx.core.resources.StringProvider
|
||||||
|
import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController
|
||||||
|
import im.vector.riotx.features.home.room.detail.timeline.item.RoomCreateItem
|
||||||
|
import im.vector.riotx.features.home.room.detail.timeline.item.RoomCreateItem_
|
||||||
|
import me.gujun.android.span.span
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
class RoomCreateItemFactory @Inject constructor(private val colorProvider: ColorProvider,
|
||||||
|
private val stringProvider: StringProvider) {
|
||||||
|
|
||||||
|
fun create(event: TimelineEvent, callback: TimelineEventController.Callback?): RoomCreateItem? {
|
||||||
|
val createRoomContent = event.root.getClearContent().toModel<RoomCreateContent>()
|
||||||
|
?: return null
|
||||||
|
val predecessor = createRoomContent.predecessor ?: return null
|
||||||
|
val roomLink = PermalinkFactory.createPermalink(predecessor.roomId) ?: return null
|
||||||
|
val urlSpan = MatrixPermalinkSpan(roomLink, object : MatrixPermalinkSpan.Callback {
|
||||||
|
override fun onUrlClicked(url: String) {
|
||||||
|
callback?.onUrlClicked(roomLink)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
val textColorInt = colorProvider.getColor(R.color.riot_primary_text_color_light)
|
||||||
|
val text = span {
|
||||||
|
text = stringProvider.getString(R.string.room_tombstone_continuation_description)
|
||||||
|
append("\n")
|
||||||
|
append(
|
||||||
|
stringProvider.getString(R.string.room_tombstone_predecessor_link)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return RoomCreateItem_()
|
||||||
|
.text(text)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
@ -33,6 +33,7 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me
|
|||||||
private val encryptedItemFactory: EncryptedItemFactory,
|
private val encryptedItemFactory: EncryptedItemFactory,
|
||||||
private val noticeItemFactory: NoticeItemFactory,
|
private val noticeItemFactory: NoticeItemFactory,
|
||||||
private val defaultItemFactory: DefaultItemFactory,
|
private val defaultItemFactory: DefaultItemFactory,
|
||||||
|
private val roomCreateItemFactory: RoomCreateItemFactory,
|
||||||
private val avatarRenderer: AvatarRenderer) {
|
private val avatarRenderer: AvatarRenderer) {
|
||||||
|
|
||||||
fun create(event: TimelineEvent,
|
fun create(event: TimelineEvent,
|
||||||
@ -45,6 +46,7 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me
|
|||||||
when (event.root.getClearType()) {
|
when (event.root.getClearType()) {
|
||||||
EventType.MESSAGE -> messageItemFactory.create(event, nextEvent, highlight, callback)
|
EventType.MESSAGE -> messageItemFactory.create(event, nextEvent, highlight, callback)
|
||||||
// State and call
|
// State and call
|
||||||
|
EventType.STATE_ROOM_TOMBSTONE,
|
||||||
EventType.STATE_ROOM_NAME,
|
EventType.STATE_ROOM_NAME,
|
||||||
EventType.STATE_ROOM_TOPIC,
|
EventType.STATE_ROOM_TOPIC,
|
||||||
EventType.STATE_ROOM_MEMBER,
|
EventType.STATE_ROOM_MEMBER,
|
||||||
@ -52,7 +54,8 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me
|
|||||||
EventType.CALL_INVITE,
|
EventType.CALL_INVITE,
|
||||||
EventType.CALL_HANGUP,
|
EventType.CALL_HANGUP,
|
||||||
EventType.CALL_ANSWER -> noticeItemFactory.create(event, highlight, callback)
|
EventType.CALL_ANSWER -> noticeItemFactory.create(event, highlight, callback)
|
||||||
|
// State room create
|
||||||
|
EventType.STATE_ROOM_CREATE -> roomCreateItemFactory.create(event, callback)
|
||||||
// Crypto
|
// Crypto
|
||||||
EventType.ENCRYPTION -> encryptionItemFactory.create(event, highlight, callback)
|
EventType.ENCRYPTION -> encryptionItemFactory.create(event, highlight, callback)
|
||||||
EventType.ENCRYPTED -> {
|
EventType.ENCRYPTED -> {
|
||||||
@ -66,8 +69,7 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me
|
|||||||
|
|
||||||
// Unhandled event types (yet)
|
// Unhandled event types (yet)
|
||||||
EventType.STATE_ROOM_THIRD_PARTY_INVITE,
|
EventType.STATE_ROOM_THIRD_PARTY_INVITE,
|
||||||
EventType.STICKER,
|
EventType.STICKER -> defaultItemFactory.create(event, highlight)
|
||||||
EventType.STATE_ROOM_CREATE -> defaultItemFactory.create(event, highlight)
|
|
||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
//These are just for debug to display hidden event, they should be filtered out in normal mode
|
//These are just for debug to display hidden event, they should be filtered out in normal mode
|
||||||
|
@ -22,6 +22,7 @@ import im.vector.matrix.android.api.session.events.model.EventType
|
|||||||
import im.vector.matrix.android.api.session.events.model.toModel
|
import im.vector.matrix.android.api.session.events.model.toModel
|
||||||
import im.vector.matrix.android.api.session.room.model.*
|
import im.vector.matrix.android.api.session.room.model.*
|
||||||
import im.vector.matrix.android.api.session.room.model.call.CallInviteContent
|
import im.vector.matrix.android.api.session.room.model.call.CallInviteContent
|
||||||
|
import im.vector.matrix.android.api.session.room.model.tombstone.RoomTombstoneContent
|
||||||
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
|
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
|
||||||
import im.vector.riotx.R
|
import im.vector.riotx.R
|
||||||
import im.vector.riotx.core.resources.StringProvider
|
import im.vector.riotx.core.resources.StringProvider
|
||||||
@ -37,6 +38,7 @@ class NoticeEventFormatter @Inject constructor(private val stringProvider: Strin
|
|||||||
EventType.STATE_ROOM_TOPIC -> formatRoomTopicEvent(timelineEvent.root, timelineEvent.getDisambiguatedDisplayName())
|
EventType.STATE_ROOM_TOPIC -> formatRoomTopicEvent(timelineEvent.root, timelineEvent.getDisambiguatedDisplayName())
|
||||||
EventType.STATE_ROOM_MEMBER -> formatRoomMemberEvent(timelineEvent.root, timelineEvent.senderName())
|
EventType.STATE_ROOM_MEMBER -> formatRoomMemberEvent(timelineEvent.root, timelineEvent.senderName())
|
||||||
EventType.STATE_HISTORY_VISIBILITY -> formatRoomHistoryVisibilityEvent(timelineEvent.root, timelineEvent.getDisambiguatedDisplayName())
|
EventType.STATE_HISTORY_VISIBILITY -> formatRoomHistoryVisibilityEvent(timelineEvent.root, timelineEvent.getDisambiguatedDisplayName())
|
||||||
|
EventType.STATE_ROOM_TOMBSTONE -> formatRoomTombstoneEvent(timelineEvent.root, timelineEvent.getDisambiguatedDisplayName())
|
||||||
EventType.CALL_INVITE,
|
EventType.CALL_INVITE,
|
||||||
EventType.CALL_HANGUP,
|
EventType.CALL_HANGUP,
|
||||||
EventType.CALL_ANSWER -> formatCallEvent(timelineEvent.root, timelineEvent.getDisambiguatedDisplayName())
|
EventType.CALL_ANSWER -> formatCallEvent(timelineEvent.root, timelineEvent.getDisambiguatedDisplayName())
|
||||||
@ -56,6 +58,7 @@ class NoticeEventFormatter @Inject constructor(private val stringProvider: Strin
|
|||||||
EventType.CALL_INVITE,
|
EventType.CALL_INVITE,
|
||||||
EventType.CALL_HANGUP,
|
EventType.CALL_HANGUP,
|
||||||
EventType.CALL_ANSWER -> formatCallEvent(event, senderName)
|
EventType.CALL_ANSWER -> formatCallEvent(event, senderName)
|
||||||
|
EventType.STATE_ROOM_TOMBSTONE -> formatRoomTombstoneEvent(event, senderName)
|
||||||
else -> {
|
else -> {
|
||||||
Timber.v("Type $type not handled by this formatter")
|
Timber.v("Type $type not handled by this formatter")
|
||||||
null
|
null
|
||||||
@ -72,6 +75,10 @@ class NoticeEventFormatter @Inject constructor(private val stringProvider: Strin
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun formatRoomTombstoneEvent(event: Event, senderName: String?): CharSequence? {
|
||||||
|
return stringProvider.getString(R.string.notice_room_update, senderName)
|
||||||
|
}
|
||||||
|
|
||||||
private fun formatRoomTopicEvent(event: Event, senderName: String?): CharSequence? {
|
private fun formatRoomTopicEvent(event: Event, senderName: String?): CharSequence? {
|
||||||
val content = event.getClearContent().toModel<RoomTopicContent>() ?: return null
|
val content = event.getClearContent().toModel<RoomTopicContent>() ?: return null
|
||||||
return if (content.topic.isNullOrEmpty()) {
|
return if (content.topic.isNullOrEmpty()) {
|
||||||
|
@ -39,7 +39,8 @@ object TimelineDisplayableEvents {
|
|||||||
EventType.ENCRYPTION,
|
EventType.ENCRYPTION,
|
||||||
EventType.STATE_ROOM_THIRD_PARTY_INVITE,
|
EventType.STATE_ROOM_THIRD_PARTY_INVITE,
|
||||||
EventType.STICKER,
|
EventType.STICKER,
|
||||||
EventType.STATE_ROOM_CREATE
|
EventType.STATE_ROOM_CREATE,
|
||||||
|
EventType.STATE_ROOM_TOMBSTONE
|
||||||
)
|
)
|
||||||
|
|
||||||
val DEBUG_DISPLAYABLE_TYPES = DISPLAYABLE_TYPES + listOf(
|
val DEBUG_DISPLAYABLE_TYPES = DISPLAYABLE_TYPES + listOf(
|
||||||
|
@ -0,0 +1,41 @@
|
|||||||
|
/*
|
||||||
|
*
|
||||||
|
* * Copyright 2019 New Vector Ltd
|
||||||
|
* *
|
||||||
|
* * Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* * you may not use this file except in compliance with the License.
|
||||||
|
* * You may obtain a copy of the License at
|
||||||
|
* *
|
||||||
|
* * http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
* *
|
||||||
|
* * Unless required by applicable law or agreed to in writing, software
|
||||||
|
* * distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* * See the License for the specific language governing permissions and
|
||||||
|
* * limitations under the License.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
package im.vector.riotx.features.home.room.detail.timeline.item
|
||||||
|
|
||||||
|
import android.widget.TextView
|
||||||
|
import com.airbnb.epoxy.EpoxyAttribute
|
||||||
|
import com.airbnb.epoxy.EpoxyModelClass
|
||||||
|
import com.airbnb.epoxy.EpoxyModelWithHolder
|
||||||
|
import im.vector.riotx.R
|
||||||
|
import im.vector.riotx.core.epoxy.VectorEpoxyHolder
|
||||||
|
import im.vector.riotx.core.epoxy.VectorEpoxyModel
|
||||||
|
|
||||||
|
@EpoxyModelClass(layout = R.layout.item_timeline_event_create)
|
||||||
|
abstract class RoomCreateItem : VectorEpoxyModel<RoomCreateItem.Holder>() {
|
||||||
|
|
||||||
|
@EpoxyAttribute lateinit var text: CharSequence
|
||||||
|
|
||||||
|
override fun bind(holder: Holder) {
|
||||||
|
holder.description.text = text
|
||||||
|
}
|
||||||
|
|
||||||
|
class Holder : VectorEpoxyHolder() {
|
||||||
|
val description by bind<TextView>(R.id.roomCreateItemDescription)
|
||||||
|
}
|
||||||
|
}
|
@ -101,4 +101,5 @@
|
|||||||
app:layout_constraintTop_toBottomOf="@+id/roomToolbar"
|
app:layout_constraintTop_toBottomOf="@+id/roomToolbar"
|
||||||
tools:visibility="visible" />
|
tools:visibility="visible" />
|
||||||
|
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
24
vector/src/main/res/layout/item_timeline_event_create.xml
Normal file
24
vector/src/main/res/layout/item_timeline_event_create.xml
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/roomCreateItemDescription"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="16dp"
|
||||||
|
android:layout_marginLeft="16dp"
|
||||||
|
android:layout_marginTop="16dp"
|
||||||
|
android:layout_marginBottom="16dp"
|
||||||
|
android:background="@drawable/bg_tombstone_predecessor"
|
||||||
|
android:drawableStart="@drawable/error"
|
||||||
|
android:drawableLeft="@drawable/error"
|
||||||
|
android:drawablePadding="16dp"
|
||||||
|
android:gravity="center|start"
|
||||||
|
android:minHeight="80dp"
|
||||||
|
android:padding="16dp"
|
||||||
|
tools:text="This room is continuation…" />
|
||||||
|
|
||||||
|
</FrameLayout>
|
Loading…
Reference in New Issue
Block a user