From 9e5c70dda317f11ced38c7f62630eb68bf378eb3 Mon Sep 17 00:00:00 2001 From: ganfra Date: Thu, 25 Jul 2019 19:34:39 +0200 Subject: [PATCH 1/5] Room update: start handling tombstone and room create events [WIP] --- .../api/session/room/model/RoomSummary.kt | 3 +- .../room/model/create/RoomCreateContent.kt | 42 +++++++++++ .../model/tombstone/RoomTombstoneContent.kt | 31 ++++++++ .../api/session/room/state/StateService.kt | 3 + .../database/mapper/RoomSummaryMapper.kt | 3 +- .../database/model/RoomSummaryEntity.kt | 3 +- .../android/internal/session/SessionModule.kt | 6 ++ .../session/room/DefaultRoomService.kt | 6 +- .../internal/session/room/RoomFactory.kt | 6 +- .../session/room/state/DefaultStateService.kt | 18 +++++ .../RoomTombstoneEventLiveObserver.kt | 75 +++++++++++++++++++ .../internal/session/sync/RoomSyncHandler.kt | 10 ++- .../src/main/res/values/strings.xml | 1 + .../home/room/detail/RoomDetailFragment.kt | 11 ++- .../home/room/detail/RoomDetailViewModel.kt | 13 +++- .../home/room/detail/RoomDetailViewState.kt | 4 +- .../timeline/factory/RoomCreateItemFactory.kt | 61 +++++++++++++++ .../timeline/factory/TimelineItemFactory.kt | 8 +- .../timeline/format/NoticeEventFormatter.kt | 7 ++ .../helper/TimelineDisplayableEvents.kt | 3 +- .../detail/timeline/item/RoomCreateItem.kt | 41 ++++++++++ .../main/res/layout/fragment_room_detail.xml | 1 + .../res/layout/item_timeline_event_create.xml | 24 ++++++ 23 files changed, 362 insertions(+), 18 deletions(-) create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/create/RoomCreateContent.kt create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/tombstone/RoomTombstoneContent.kt create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/tombstone/RoomTombstoneEventLiveObserver.kt create mode 100644 vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/RoomCreateItemFactory.kt create mode 100644 vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/RoomCreateItem.kt create mode 100644 vector/src/main/res/layout/item_timeline_event_create.xml diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/RoomSummary.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/RoomSummary.kt index b54f17dbd9..ce86df5c70 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/RoomSummary.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/RoomSummary.kt @@ -34,5 +34,6 @@ data class RoomSummary( val notificationCount: Int = 0, val highlightCount: Int = 0, val tags: List = emptyList(), - val membership: Membership = Membership.NONE + val membership: Membership = Membership.NONE, + val isVersioned: Boolean = false ) \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/create/RoomCreateContent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/create/RoomCreateContent.kt new file mode 100644 index 0000000000..3aa456e6f0 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/create/RoomCreateContent.kt @@ -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 +) + diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/tombstone/RoomTombstoneContent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/tombstone/RoomTombstoneContent.kt new file mode 100644 index 0000000000..f8f8df00db --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/tombstone/RoomTombstoneContent.kt @@ -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 +) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/state/StateService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/state/StateService.kt index 79443d0b30..21aae95bdf 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/state/StateService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/state/StateService.kt @@ -17,6 +17,7 @@ package im.vector.matrix.android.api.session.room.state import im.vector.matrix.android.api.MatrixCallback +import im.vector.matrix.android.api.session.events.model.Event interface StateService { @@ -25,4 +26,6 @@ interface StateService { */ fun updateTopic(topic: String, callback: MatrixCallback) + fun getStateEvent(eventType: String): Event? + } \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/RoomSummaryMapper.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/RoomSummaryMapper.kt index 5f946e4291..6b8cf567ca 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/RoomSummaryMapper.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/RoomSummaryMapper.kt @@ -61,7 +61,8 @@ internal class RoomSummaryMapper @Inject constructor( highlightCount = roomSummaryEntity.highlightCount, notificationCount = roomSummaryEntity.notificationCount, tags = tags, - membership = roomSummaryEntity.membership + membership = roomSummaryEntity.membership, + isVersioned = roomSummaryEntity.isVersioned ) } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/RoomSummaryEntity.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/RoomSummaryEntity.kt index c178711cb3..7470836161 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/RoomSummaryEntity.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/RoomSummaryEntity.kt @@ -35,7 +35,8 @@ internal open class RoomSummaryEntity(@PrimaryKey var roomId: String = "", var otherMemberIds: RealmList = RealmList(), var notificationCount: Int = 0, var highlightCount: Int = 0, - var tags: RealmList = RealmList() + var tags: RealmList = RealmList(), + var isVersioned: Boolean = false ) : RealmObject() { private var membershipStr: String = Membership.NONE.name diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionModule.kt index f2e61e8c2a..af0c4e0f8f 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionModule.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionModule.kt @@ -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.session.group.GroupSummaryUpdater 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.util.md5 import io.realm.RealmConfiguration @@ -128,6 +129,11 @@ internal abstract class SessionModule { @IntoSet abstract fun bindEventRelationsAggregationUpdater(groupSummaryUpdater: EventRelationsAggregationUpdater): LiveEntityObserver + @Binds + @IntoSet + abstract fun bindRoomCreateEventLiveObserver(roomTombstoneEventLiveObserver: RoomTombstoneEventLiveObserver): LiveEntityObserver + + @Binds abstract fun bindInitialSyncProgressService(initialSyncProgressService: DefaultInitialSyncProgressService): InitialSyncProgressService diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/DefaultRoomService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/DefaultRoomService.kt index 2c0f1ce90c..e07b47a28b 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/DefaultRoomService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/DefaultRoomService.kt @@ -54,7 +54,11 @@ internal class DefaultRoomService @Inject constructor(private val monarchy: Mona override fun liveRoomSummaries(): LiveData> { 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) } ) } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomFactory.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomFactory.kt index 98cf872b10..022133eaea 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomFactory.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomFactory.kt @@ -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.room.Room 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.LoadRoomMembersTask 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.PaginationTask import im.vector.matrix.android.internal.task.TaskExecutor +import io.realm.RealmConfiguration import javax.inject.Inject internal class RoomFactory @Inject constructor(private val context: Context, private val credentials: Credentials, private val monarchy: Monarchy, + @SessionDatabase + private val realmConfiguration: RealmConfiguration, private val eventFactory: LocalEchoEventFactory, private val roomSummaryMapper: RoomSummaryMapper, private val taskExecutor: TaskExecutor, @@ -63,7 +67,7 @@ internal class RoomFactory @Inject constructor(private val context: Context, fun create(roomId: String): Room { val timelineService = DefaultTimelineService(roomId, monarchy, taskExecutor, contextOfEventTask, cryptoService, paginationTask) 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 readService = DefaultReadService(roomId, monarchy, taskExecutor, setReadMarkersTask, credentials) val relationService = DefaultRelationService(context, diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/state/DefaultStateService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/state/DefaultStateService.kt index e51f9b466a..03beb8e7e2 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/state/DefaultStateService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/state/DefaultStateService.kt @@ -17,16 +17,32 @@ package im.vector.matrix.android.internal.session.room.state 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.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.configureWith +import io.realm.Realm +import io.realm.RealmConfiguration import javax.inject.Inject internal class DefaultStateService @Inject constructor(private val roomId: String, + @SessionDatabase + private val realmConfiguration: RealmConfiguration, private val taskExecutor: TaskExecutor, 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) { val params = SendStateTask.Params(roomId, EventType.STATE_ROOM_TOPIC, @@ -39,4 +55,6 @@ internal class DefaultStateService @Inject constructor(private val roomId: Strin .dispatchTo(callback) .executeBy(taskExecutor) } + + } \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/tombstone/RoomTombstoneEventLiveObserver.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/tombstone/RoomTombstoneEventLiveObserver.kt new file mode 100644 index 0000000000..8c713ac4d4 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/tombstone/RoomTombstoneEventLiveObserver.kt @@ -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(realmConfiguration) { + + override val query = Monarchy.Query { + EventEntity.types(it, listOf(EventType.STATE_ROOM_TOMBSTONE)) + } + + override fun onChange(results: RealmResults, changeSet: OrderedCollectionChangeSet) { + changeSet.insertions + .asSequence() + .mapNotNull { + results[it]?.asDomain() + } + .toList() + .also { + handleRoomTombstoneEvents(it) + } + } + + private fun handleRoomTombstoneEvents(tombstoneEvents: List) = Realm.getInstance(realmConfiguration).use { + it.executeTransactionAsync { realm -> + for (event in tombstoneEvents) { + if (event.roomId == null) continue + val createRoomContent = event.getClearContent().toModel() + if (createRoomContent?.replacementRoom == null) continue + + val predecessorRoomSummary = RoomSummaryEntity.where(realm, event.roomId).findFirst() + ?: RoomSummaryEntity(event.roomId) + predecessorRoomSummary.isVersioned = true + realm.insertOrUpdate(predecessorRoomSummary) + + } + } + } + +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/RoomSyncHandler.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/RoomSyncHandler.kt index 215321bd42..6c8c6a037e 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/RoomSyncHandler.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/RoomSyncHandler.kt @@ -146,8 +146,7 @@ internal class RoomSyncHandler @Inject constructor(private val monarchy: Monarch roomEntity, roomSync.timeline.events, roomSync.timeline.prevToken, - roomSync.timeline.limited, - 0 + roomSync.timeline.limited ) roomEntity.addOrUpdate(chunkEntity) } @@ -195,15 +194,18 @@ internal class RoomSyncHandler @Inject constructor(private val monarchy: Monarch roomEntity: RoomEntity, eventList: List, prevToken: String? = null, - isLimited: Boolean = true, - stateIndexOffset: Int = 0): ChunkEntity { + isLimited: Boolean = true): ChunkEntity { val lastChunk = ChunkEntity.findLastLiveChunkFromRoom(realm, roomEntity.roomId) + var stateIndexOffset = 0 val chunkEntity = if (!isLimited && lastChunk != null) { lastChunk } else { realm.createObject().apply { this.prevToken = prevToken } } + if (isLimited && lastChunk != null) { + stateIndexOffset = lastChunk.lastStateIndex(PaginationDirection.FORWARDS) + } lastChunk?.isLastForward = false chunkEntity.isLastForward = true diff --git a/matrix-sdk-android/src/main/res/values/strings.xml b/matrix-sdk-android/src/main/res/values/strings.xml index 5459cf9124..8e5fc6d690 100644 --- a/matrix-sdk-android/src/main/res/values/strings.xml +++ b/matrix-sdk-android/src/main/res/values/strings.xml @@ -31,6 +31,7 @@ anyone. unknown (%s). %1$s turned on end-to-end encryption (%2$s) + %s upgraded this room. %1$s requested a VoIP conference VoIP conference started diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt index cabb479575..204dfb5d74 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt @@ -244,7 +244,8 @@ class RoomDetailFragment : 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 //switch to expanded bar composerLayout.composerRelatedMessageTitle.apply { @@ -533,7 +534,13 @@ class RoomDetailFragment : } else if (state.asyncInviter.complete) { 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) { diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt index d38561fa88..5f6c17df77 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt @@ -30,12 +30,14 @@ import com.squareup.inject.assisted.AssistedInject import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.session.Session 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.file.FileService 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.MessageType 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.internal.crypto.attachments.toElementToDecrypt import im.vector.matrix.android.internal.crypto.model.event.EncryptedEventContent @@ -94,7 +96,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro init { observeRoomSummary() observeEventDisplayedActions() - observeInvitationState() + observeSummaryState() cancelableBag += room.loadRoomMembersIfNeeded() timeline.start() 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 -> if (summary.membership == Membership.INVITE) { summary.latestEvent?.root?.senderId?.let { senderId -> @@ -556,6 +558,13 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro setState { copy(asyncInviter = Success(it)) } } } + if (summary.isVersioned) { + room.getStateEvent(EventType.STATE_ROOM_TOMBSTONE) + ?.getClearContent() + ?.toModel()?.also { + setState { copy(tombstoneContent = it) } + } + } } } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewState.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewState.kt index 6317149116..c78e8dc3a9 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewState.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewState.kt @@ -20,6 +20,7 @@ import com.airbnb.mvrx.Async import com.airbnb.mvrx.MvRxState import com.airbnb.mvrx.Uninitialized 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.TimelineEvent import im.vector.matrix.android.api.session.user.model.User @@ -46,7 +47,8 @@ data class RoomDetailViewState( val asyncInviter: Async = Uninitialized, val asyncRoomSummary: Async = Uninitialized, val sendMode: SendMode = SendMode.REGULAR, - val isEncrypted: Boolean = false + val isEncrypted: Boolean = false, + val tombstoneContent: RoomTombstoneContent? = null ) : MvRxState { constructor(args: RoomDetailArgs) : this(roomId = args.roomId, eventId = args.eventId) diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/RoomCreateItemFactory.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/RoomCreateItemFactory.kt new file mode 100644 index 0000000000..21fe85d774 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/RoomCreateItemFactory.kt @@ -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() + ?: 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) + } + + +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/TimelineItemFactory.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/TimelineItemFactory.kt index 4a927b1979..a3b2250912 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/TimelineItemFactory.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/TimelineItemFactory.kt @@ -33,6 +33,7 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me private val encryptedItemFactory: EncryptedItemFactory, private val noticeItemFactory: NoticeItemFactory, private val defaultItemFactory: DefaultItemFactory, + private val roomCreateItemFactory: RoomCreateItemFactory, private val avatarRenderer: AvatarRenderer) { fun create(event: TimelineEvent, @@ -45,6 +46,7 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me when (event.root.getClearType()) { EventType.MESSAGE -> messageItemFactory.create(event, nextEvent, highlight, callback) // State and call + EventType.STATE_ROOM_TOMBSTONE, EventType.STATE_ROOM_NAME, EventType.STATE_ROOM_TOPIC, EventType.STATE_ROOM_MEMBER, @@ -52,7 +54,8 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me EventType.CALL_INVITE, EventType.CALL_HANGUP, EventType.CALL_ANSWER -> noticeItemFactory.create(event, highlight, callback) - + // State room create + EventType.STATE_ROOM_CREATE -> roomCreateItemFactory.create(event, callback) // Crypto EventType.ENCRYPTION -> encryptionItemFactory.create(event, highlight, callback) EventType.ENCRYPTED -> { @@ -66,8 +69,7 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me // Unhandled event types (yet) EventType.STATE_ROOM_THIRD_PARTY_INVITE, - EventType.STICKER, - EventType.STATE_ROOM_CREATE -> defaultItemFactory.create(event, highlight) + EventType.STICKER -> defaultItemFactory.create(event, highlight) else -> { //These are just for debug to display hidden event, they should be filtered out in normal mode diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/format/NoticeEventFormatter.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/format/NoticeEventFormatter.kt index 369af64ae2..2b1a263328 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/format/NoticeEventFormatter.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/format/NoticeEventFormatter.kt @@ -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.room.model.* 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.riotx.R 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_MEMBER -> formatRoomMemberEvent(timelineEvent.root, timelineEvent.senderName()) EventType.STATE_HISTORY_VISIBILITY -> formatRoomHistoryVisibilityEvent(timelineEvent.root, timelineEvent.getDisambiguatedDisplayName()) + EventType.STATE_ROOM_TOMBSTONE -> formatRoomTombstoneEvent(timelineEvent.root, timelineEvent.getDisambiguatedDisplayName()) EventType.CALL_INVITE, EventType.CALL_HANGUP, 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_HANGUP, EventType.CALL_ANSWER -> formatCallEvent(event, senderName) + EventType.STATE_ROOM_TOMBSTONE -> formatRoomTombstoneEvent(event, senderName) else -> { Timber.v("Type $type not handled by this formatter") 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? { val content = event.getClearContent().toModel() ?: return null return if (content.topic.isNullOrEmpty()) { diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/TimelineDisplayableEvents.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/TimelineDisplayableEvents.kt index 667de65a4b..fa0a71bde2 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/TimelineDisplayableEvents.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/TimelineDisplayableEvents.kt @@ -39,7 +39,8 @@ object TimelineDisplayableEvents { EventType.ENCRYPTION, EventType.STATE_ROOM_THIRD_PARTY_INVITE, EventType.STICKER, - EventType.STATE_ROOM_CREATE + EventType.STATE_ROOM_CREATE, + EventType.STATE_ROOM_TOMBSTONE ) val DEBUG_DISPLAYABLE_TYPES = DISPLAYABLE_TYPES + listOf( diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/RoomCreateItem.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/RoomCreateItem.kt new file mode 100644 index 0000000000..117f4bd2d0 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/RoomCreateItem.kt @@ -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() { + + @EpoxyAttribute lateinit var text: CharSequence + + override fun bind(holder: Holder) { + holder.description.text = text + } + + class Holder : VectorEpoxyHolder() { + val description by bind(R.id.roomCreateItemDescription) + } +} \ No newline at end of file diff --git a/vector/src/main/res/layout/fragment_room_detail.xml b/vector/src/main/res/layout/fragment_room_detail.xml index c0e3713942..f8cec43371 100644 --- a/vector/src/main/res/layout/fragment_room_detail.xml +++ b/vector/src/main/res/layout/fragment_room_detail.xml @@ -101,4 +101,5 @@ app:layout_constraintTop_toBottomOf="@+id/roomToolbar" tools:visibility="visible" /> + \ No newline at end of file diff --git a/vector/src/main/res/layout/item_timeline_event_create.xml b/vector/src/main/res/layout/item_timeline_event_create.xml new file mode 100644 index 0000000000..80dc7d6312 --- /dev/null +++ b/vector/src/main/res/layout/item_timeline_event_create.xml @@ -0,0 +1,24 @@ + + + + + + \ No newline at end of file From 9a1e16a170c65ae8167836c1e2f8e638202d8502 Mon Sep 17 00:00:00 2001 From: ganfra Date: Fri, 26 Jul 2019 14:51:14 +0200 Subject: [PATCH 2/5] Tombstone : add notification area and handle links --- .../matrix/android/api/failure/MatrixError.kt | 12 +- .../res/layout/view_notification_area.xml | 33 ++ .../core/error/ResourceLimitErrorFormatter.kt | 68 ++++ .../core/platform/NotificationAreaView.kt | 324 ++++++++++++++++++ .../home/room/detail/RoomDetailFragment.kt | 100 ++++-- .../timeline/TimelineEventController.kt | 9 +- .../timeline/factory/RoomCreateItemFactory.kt | 20 +- .../detail/timeline/item/RoomCreateItem.kt | 3 +- .../main/res/layout/fragment_room_detail.xml | 18 +- .../res/layout/item_timeline_event_create.xml | 3 +- 10 files changed, 539 insertions(+), 51 deletions(-) create mode 100644 vector/src/debug/res/layout/view_notification_area.xml create mode 100644 vector/src/main/java/im/vector/riotx/core/error/ResourceLimitErrorFormatter.kt create mode 100644 vector/src/main/java/im/vector/riotx/core/platform/NotificationAreaView.kt diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/failure/MatrixError.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/failure/MatrixError.kt index 1e87cfc1b2..7d433ba7ba 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/failure/MatrixError.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/failure/MatrixError.kt @@ -26,8 +26,13 @@ import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = true) data class MatrixError( @Json(name = "errcode") val code: String, - @Json(name = "error") val message: String -) { + @Json(name = "error") val message: String, + + @Json(name = "consent_uri") val consentUri: String? = null, + // RESOURCE_LIMIT_EXCEEDED data + @Json(name = "limit_type") val limitType: String? = null, + @Json(name = "admin_contact") val adminUri: String? = null) { + companion object { const val FORBIDDEN = "M_FORBIDDEN" @@ -55,5 +60,8 @@ data class MatrixError( const val M_CONSENT_NOT_GIVEN = "M_CONSENT_NOT_GIVEN" const val RESOURCE_LIMIT_EXCEEDED = "M_RESOURCE_LIMIT_EXCEEDED" const val WRONG_ROOM_KEYS_VERSION = "M_WRONG_ROOM_KEYS_VERSION" + + // Possible value for "limit_type" + const val LIMIT_TYPE_MAU = "monthly_active_user" } } \ No newline at end of file diff --git a/vector/src/debug/res/layout/view_notification_area.xml b/vector/src/debug/res/layout/view_notification_area.xml new file mode 100644 index 0000000000..8af520c2c7 --- /dev/null +++ b/vector/src/debug/res/layout/view_notification_area.xml @@ -0,0 +1,33 @@ + + + + + + + + + \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotx/core/error/ResourceLimitErrorFormatter.kt b/vector/src/main/java/im/vector/riotx/core/error/ResourceLimitErrorFormatter.kt new file mode 100644 index 0000000000..b57014f142 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/core/error/ResourceLimitErrorFormatter.kt @@ -0,0 +1,68 @@ +/* + * 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.core.error + +import android.content.Context +import android.text.Html +import androidx.annotation.StringRes +import im.vector.matrix.android.api.failure.MatrixError +import im.vector.riotx.R +import me.gujun.android.span.span + +class ResourceLimitErrorFormatter(private val context: Context) { + + // 'hard' if the logged in user has been locked out, 'soft' if they haven't + sealed class Mode(@StringRes val mauErrorRes: Int, @StringRes val defaultErrorRes: Int, @StringRes val contactRes: Int) { + // User can still send message (will be used in a near future) + object Soft : Mode(R.string.resource_limit_soft_mau, R.string.resource_limit_soft_default, R.string.resource_limit_soft_contact) + + // User cannot send message anymore + object Hard : Mode(R.string.resource_limit_hard_mau, R.string.resource_limit_hard_default, R.string.resource_limit_hard_contact) + } + + fun format(matrixError: MatrixError, + mode: Mode, + separator: CharSequence = " ", + clickable: Boolean = false): CharSequence { + val error = if (MatrixError.LIMIT_TYPE_MAU == matrixError.limitType) { + context.getString(mode.mauErrorRes) + } else { + context.getString(mode.defaultErrorRes) + } + val contact = if (clickable && matrixError.adminUri != null) { + val contactSubString = uriAsLink(matrixError.adminUri!!) + val contactFullString = context.getString(mode.contactRes, contactSubString) + Html.fromHtml(contactFullString) + } else { + val contactSubString = context.getString(R.string.resource_limit_contact_admin) + context.getString(mode.contactRes, contactSubString) + } + return span { + text = error + } + .append(separator) + .append(contact) + } + + /** + * Create a HTML link with a uri + */ + private fun uriAsLink(uri: String): String { + val contactStr = context.getString(R.string.resource_limit_contact_admin) + return "$contactStr" + } +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotx/core/platform/NotificationAreaView.kt b/vector/src/main/java/im/vector/riotx/core/platform/NotificationAreaView.kt new file mode 100644 index 0000000000..6a9af14937 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/core/platform/NotificationAreaView.kt @@ -0,0 +1,324 @@ +/* + * 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.core.platform + +import android.content.Context +import android.graphics.Color +import android.text.SpannableString +import android.text.TextPaint +import android.text.TextUtils +import android.text.method.LinkMovementMethod +import android.text.style.ClickableSpan +import android.util.AttributeSet +import android.view.View +import android.widget.ImageView +import android.widget.RelativeLayout +import android.widget.TextView +import androidx.core.content.ContextCompat +import androidx.core.text.bold +import butterknife.BindView +import butterknife.ButterKnife +import im.vector.matrix.android.api.failure.MatrixError +import im.vector.matrix.android.api.permalinks.MatrixPermalinkSpan +import im.vector.matrix.android.api.permalinks.PermalinkFactory +import im.vector.matrix.android.api.session.room.model.tombstone.RoomTombstoneContent +import im.vector.riotx.R +import im.vector.riotx.core.error.ResourceLimitErrorFormatter +import im.vector.riotx.features.themes.ThemeUtils +import me.gujun.android.span.addSpan +import me.gujun.android.span.span +import me.saket.bettermovementmethod.BetterLinkMovementMethod +import timber.log.Timber + +/** + * The view used to show some information about the room + * It does have a unique render method + */ +class NotificationAreaView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0 +) : RelativeLayout(context, attrs, defStyleAttr) { + + @BindView(R.id.room_notification_icon) + lateinit var imageView: ImageView + @BindView(R.id.room_notification_message) + lateinit var messageView: TextView + + var delegate: Delegate? = null + private var state: State = State.Initial + + init { + setupView() + } + + /** + * This methods is responsible for rendering the view according to the newState + * + * @param newState the newState representing the view + */ + fun render(newState: State) { + if (newState == state) { + Timber.d("State unchanged") + return + } + Timber.d("Rendering $newState") + cleanUp() + state = newState + when (newState) { + is State.Default -> renderDefault() + is State.Hidden -> renderHidden() + is State.Tombstone -> renderTombstone(newState) + is State.ResourceLimitExceededError -> renderResourceLimitExceededError(newState) + is State.ConnectionError -> renderConnectionError() + is State.Typing -> renderTyping(newState) + is State.UnreadPreview -> renderUnreadPreview() + is State.ScrollToBottom -> renderScrollToBottom(newState) + is State.UnsentEvents -> renderUnsent(newState) + } + } + + // PRIVATE METHODS ***************************************************************************************************************************************** + + private fun setupView() { + inflate(context, R.layout.view_notification_area, this) + ButterKnife.bind(this) + } + + private fun cleanUp() { + messageView.setOnClickListener(null) + imageView.setOnClickListener(null) + setBackgroundColor(Color.TRANSPARENT) + messageView.text = null + imageView.setImageResource(0) + } + + private fun renderTombstone(state: State.Tombstone) { + val roomTombstoneContent = state.tombstoneContent + val roomLink = PermalinkFactory.createPermalink(roomTombstoneContent.replacementRoom) + ?: return + + visibility = View.VISIBLE + imageView.setImageResource(R.drawable.error) + val textColorInt = ThemeUtils.getColor(context, R.attr.vctr_message_text_color) + val message = span { + +resources.getString(R.string.room_tombstone_versioned_description) + +"\n" + span(resources.getString(R.string.room_tombstone_continuation_link)) { + textDecorationLine = "underline" + onClick = { delegate?.onUrlClicked(roomLink) } + } + } + messageView.movementMethod = BetterLinkMovementMethod.getInstance() + messageView.text = message + } + + private fun renderResourceLimitExceededError(state: State.ResourceLimitExceededError) { + visibility = View.VISIBLE + val resourceLimitErrorFormatter = ResourceLimitErrorFormatter(context) + val formatterMode: ResourceLimitErrorFormatter.Mode + val backgroundColor: Int + if (state.isSoft) { + backgroundColor = R.color.soft_resource_limit_exceeded + formatterMode = ResourceLimitErrorFormatter.Mode.Soft + } else { + backgroundColor = R.color.hard_resource_limit_exceeded + formatterMode = ResourceLimitErrorFormatter.Mode.Hard + } + val message = resourceLimitErrorFormatter.format(state.matrixError, formatterMode, clickable = true) + messageView.setTextColor(Color.WHITE) + messageView.text = message + messageView.movementMethod = LinkMovementMethod.getInstance() + messageView.setLinkTextColor(Color.WHITE) + setBackgroundColor(ContextCompat.getColor(context, backgroundColor)) + } + + private fun renderConnectionError() { + visibility = View.VISIBLE + imageView.setImageResource(R.drawable.error) + messageView.setTextColor(ContextCompat.getColor(context, R.color.vector_fuchsia_color)) + messageView.text = SpannableString(resources.getString(R.string.room_offline_notification)) + } + + private fun renderTyping(state: State.Typing) { + visibility = View.VISIBLE + imageView.setImageResource(R.drawable.vector_typing) + messageView.text = SpannableString(state.message) + messageView.setTextColor(ThemeUtils.getColor(context, R.attr.vctr_room_notification_text_color)) + } + + private fun renderUnreadPreview() { + visibility = View.VISIBLE + imageView.setImageResource(R.drawable.scrolldown) + messageView.setTextColor(ThemeUtils.getColor(context, R.attr.vctr_room_notification_text_color)) + imageView.setOnClickListener { delegate?.closeScreen() } + } + + private fun renderScrollToBottom(state: State.ScrollToBottom) { + visibility = View.VISIBLE + if (state.unreadCount > 0) { + imageView.setImageResource(R.drawable.newmessages) + messageView.setTextColor(ContextCompat.getColor(context, R.color.vector_fuchsia_color)) + messageView.text = SpannableString(resources.getQuantityString(R.plurals.room_new_messages_notification, state.unreadCount, state.unreadCount)) + } else { + imageView.setImageResource(R.drawable.scrolldown) + messageView.setTextColor(ThemeUtils.getColor(context, R.attr.vctr_room_notification_text_color)) + if (!TextUtils.isEmpty(state.message)) { + messageView.text = SpannableString(state.message) + } + } + messageView.setOnClickListener { delegate?.jumpToBottom() } + imageView.setOnClickListener { delegate?.jumpToBottom() } + } + + private fun renderUnsent(state: State.UnsentEvents) { + visibility = View.VISIBLE + imageView.setImageResource(R.drawable.error) + val cancelAll = resources.getString(R.string.room_prompt_cancel) + val resendAll = resources.getString(R.string.room_prompt_resend) + val messageRes = if (state.hasUnknownDeviceEvents) R.string.room_unknown_devices_messages_notification else R.string.room_unsent_messages_notification + val message = context.getString(messageRes, resendAll, cancelAll) + val cancelAllPos = message.indexOf(cancelAll) + val resendAllPos = message.indexOf(resendAll) + val spannableString = SpannableString(message) + // cancelAllPos should always be > 0 but a GA crash reported here + if (cancelAllPos >= 0) { + spannableString.setSpan(CancelAllClickableSpan(), cancelAllPos, cancelAllPos + cancelAll.length, 0) + } + + // resendAllPos should always be > 0 but a GA crash reported here + if (resendAllPos >= 0) { + spannableString.setSpan(ResendAllClickableSpan(), resendAllPos, resendAllPos + resendAll.length, 0) + } + messageView.movementMethod = LinkMovementMethod.getInstance() + messageView.setTextColor(ContextCompat.getColor(context, R.color.vector_fuchsia_color)) + messageView.text = spannableString + } + + private fun renderDefault() { + visibility = View.GONE + } + + private fun renderHidden() { + visibility = View.GONE + } + + /** + * Track the cancel all click. + */ + private inner class CancelAllClickableSpan : ClickableSpan() { + override fun onClick(widget: View) { + delegate?.deleteUnsentEvents() + render(state) + } + + override fun updateDrawState(ds: TextPaint) { + super.updateDrawState(ds) + ds.color = ContextCompat.getColor(context, R.color.vector_fuchsia_color) + ds.bgColor = 0 + ds.isUnderlineText = true + } + } + + /** + * Track the resend all click. + */ + private inner class ResendAllClickableSpan : ClickableSpan() { + override fun onClick(widget: View) { + delegate?.resendUnsentEvents() + render(state) + } + + override fun updateDrawState(ds: TextPaint) { + super.updateDrawState(ds) + ds.color = ContextCompat.getColor(context, R.color.vector_fuchsia_color) + ds.bgColor = 0 + ds.isUnderlineText = true + } + } + + /** + * The state representing the view + * It can take one state at a time + * Priority of state is managed in {@link VectorRoomActivity.refreshNotificationsArea() } + */ + sealed class State { + + // Not yet rendered + object Initial : State() + + // View will be Invisible + object Default : State() + + // View will be Gone + object Hidden : State() + + // Resource limit exceeded error will be displayed (only hard for the moment) + data class ResourceLimitExceededError(val isSoft: Boolean, val matrixError: MatrixError) : State() + + // Server connection is lost + object ConnectionError : State() + + // The room is dead + data class Tombstone(val tombstoneContent: RoomTombstoneContent) : State() + + // Somebody is typing + data class Typing(val message: String) : State() + + // Some new messages are unread in preview + object UnreadPreview : State() + + // Some new messages are unread (grey or red) + data class ScrollToBottom(val unreadCount: Int, val message: String? = null) : State() + + // Some event has been unsent + data class UnsentEvents(val hasUndeliverableEvents: Boolean, val hasUnknownDeviceEvents: Boolean) : State() + } + + /** + * An interface to delegate some actions to another object + */ + interface Delegate { + fun onUrlClicked(url: String) + fun resendUnsentEvents() + fun deleteUnsentEvents() + fun closeScreen() + fun jumpToBottom() + } + + companion object { + /** + * Preference key. + */ + private const val SHOW_INFO_AREA_KEY = "SETTINGS_SHOW_INFO_AREA_KEY" + + /** + * Always show the info area. + */ + private const val SHOW_INFO_AREA_VALUE_ALWAYS = "always" + + /** + * Show the info area when it has messages or errors. + */ + private const val SHOW_INFO_AREA_VALUE_MESSAGES_AND_ERRORS = "messages_and_errors" + + /** + * Show the info area only when it has errors. + */ + private const val SHOW_INFO_AREA_VALUE_ONLY_ERRORS = "only_errors" + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt index 204dfb5d74..ab766e149d 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt @@ -76,6 +76,7 @@ import im.vector.riotx.core.extensions.observeEvent import im.vector.riotx.core.extensions.setTextOrHide import im.vector.riotx.core.files.addEntryToDownloadManager import im.vector.riotx.core.glide.GlideApp +import im.vector.riotx.core.platform.NotificationAreaView import im.vector.riotx.core.platform.VectorBaseFragment import im.vector.riotx.core.utils.* import im.vector.riotx.features.autocomplete.command.AutocompleteCommandPresenter @@ -203,6 +204,7 @@ class RoomDetailFragment : setupComposer() setupAttachmentButton() setupInviteView() + setupNotificationView() roomDetailViewModel.subscribe { renderState(it) } textComposerViewModel.subscribe { renderTextComposerState(it) } roomDetailViewModel.sendMessageResultLiveData.observeEvent(this) { renderSendMessageResult(it) } @@ -239,6 +241,36 @@ class RoomDetailFragment : } } + private fun setupNotificationView() { + notificationAreaView.delegate = object : NotificationAreaView.Delegate { + + override fun onUrlClicked(url: String) { + permalinkHandler.launch(requireActivity(), url, object : NavigateToRoomInterceptor { + override fun navToRoom(roomId: String, eventId: String?): Boolean { + requireActivity().finish() + return false + } + }) + } + + override fun resendUnsentEvents() { + TODO("not implemented") + } + + override fun deleteUnsentEvents() { + TODO("not implemented") + } + + override fun closeScreen() { + TODO("not implemented") + } + + override fun jumpToBottom() { + TODO("not implemented") + } + } + } + private fun exitSpecialMode() { commandAutocompletePolicy.enabled = true composerLayout.collapse() @@ -259,17 +291,17 @@ class RoomDetailFragment : if (messageContent is MessageTextContent && messageContent.format == MessageType.FORMAT_MATRIX_HTML) { val parser = Parser.builder().build() val document = parser.parse(messageContent.formattedBody - ?: messageContent.body) + ?: messageContent.body) formattedBody = eventHtmlRenderer.render(document) } composerLayout.composerRelatedMessageContent.text = formattedBody - ?: nonFormattedBody + ?: nonFormattedBody composerLayout.composerEditText.setText(if (useText) event.getTextEditableContent() else "") composerLayout.composerRelatedMessageActionIcon.setImageDrawable(ContextCompat.getDrawable(requireContext(), iconRes)) avatarRenderer.render(event.senderAvatar, event.root.senderId - ?: "", event.senderName, composerLayout.composerRelatedMessageAvatar) + ?: "", event.senderName, composerLayout.composerRelatedMessageAvatar) composerLayout.composerEditText.setSelection(composerLayout.composerEditText.text.length) composerLayout.expand { @@ -298,9 +330,9 @@ class RoomDetailFragment : REQUEST_FILES_REQUEST_CODE, TAKE_IMAGE_REQUEST_CODE -> handleMediaIntent(data) REACTION_SELECT_REQUEST_CODE -> { val eventId = data.getStringExtra(EmojiReactionPickerActivity.EXTRA_EVENT_ID) - ?: return + ?: return val reaction = data.getStringExtra(EmojiReactionPickerActivity.EXTRA_REACTION_RESULT) - ?: return + ?: return //TODO check if already reacted with that? roomDetailViewModel.process(RoomDetailActions.SendReaction(reaction, eventId)) } @@ -335,26 +367,26 @@ class RoomDetailFragment : if (VectorPreferences.swipeToReplyIsEnabled(requireContext())) { val swipeCallback = RoomMessageTouchHelperCallback(requireContext(), - R.drawable.ic_reply, - object : RoomMessageTouchHelperCallback.QuickReplayHandler { - override fun performQuickReplyOnHolder(model: EpoxyModel<*>) { - (model as? AbsMessageItem)?.informationData?.let { - val eventId = it.eventId - roomDetailViewModel.process(RoomDetailActions.EnterReplyMode(eventId)) - } - } + R.drawable.ic_reply, + object : RoomMessageTouchHelperCallback.QuickReplayHandler { + override fun performQuickReplyOnHolder(model: EpoxyModel<*>) { + (model as? AbsMessageItem)?.informationData?.let { + val eventId = it.eventId + roomDetailViewModel.process(RoomDetailActions.EnterReplyMode(eventId)) + } + } - override fun canSwipeModel(model: EpoxyModel<*>): Boolean { - return when (model) { - is MessageFileItem, - is MessageImageVideoItem, - is MessageTextItem -> { - return (model as AbsMessageItem).informationData.sendState == SendState.SYNCED - } - else -> false - } - } - }) + override fun canSwipeModel(model: EpoxyModel<*>): Boolean { + return when (model) { + is MessageFileItem, + is MessageImageVideoItem, + is MessageTextItem -> { + return (model as AbsMessageItem).informationData.sendState == SendState.SYNCED + } + else -> false + } + } + }) val touchHelper = ItemTouchHelper(swipeCallback) touchHelper.attachToRecyclerView(recyclerView) } @@ -534,12 +566,14 @@ class RoomDetailFragment : } else if (state.asyncInviter.complete) { vectorBaseActivity.finish() } + if (state.tombstoneContent == null) { composerLayout.visibility = View.VISIBLE composerLayout.setRoomEncrypted(state.isEncrypted) + notificationAreaView.render(NotificationAreaView.State.Hidden) } else { composerLayout.visibility = View.GONE - showSnackWithMessage("TOMBSTONED", duration = Snackbar.LENGTH_INDEFINITE) + notificationAreaView.render(NotificationAreaView.State.Tombstone(state.tombstoneContent)) } } @@ -636,7 +670,7 @@ class RoomDetailFragment : val intent = ImageMediaViewerActivity.newIntent(vectorBaseActivity, mediaData, ViewCompat.getTransitionName(view)) val bundle = ActivityOptionsCompat.makeSceneTransitionAnimation( requireActivity(), view, ViewCompat.getTransitionName(view) - ?: "").toBundle() + ?: "").toBundle() startActivity(intent, bundle) } @@ -716,7 +750,17 @@ class RoomDetailFragment : ViewEditHistoryBottomSheet.newInstance(roomDetailArgs.roomId, informationData) .show(requireActivity().supportFragmentManager, "DISPLAY_EDITS") } -// AutocompleteUserPresenter.Callback + + override fun onRoomCreateLinkClicked(url: String) { + permalinkHandler.launch(requireContext(), url, object : NavigateToRoomInterceptor { + override fun navToRoom(roomId: String, eventId: String?): Boolean { + requireActivity().finish() + return false + } + }) + } + + // AutocompleteUserPresenter.Callback override fun onQueryUsers(query: CharSequence?) { textComposerViewModel.process(TextComposerActions.QueryUsers(query)) @@ -730,7 +774,7 @@ class RoomDetailFragment : } MessageMenuViewModel.ACTION_VIEW_REACTIONS -> { val messageInformationData = actionData.data as? MessageInformationData - ?: return + ?: return ViewReactionBottomSheet.newInstance(roomDetailArgs.roomId, messageInformationData) .show(requireActivity().supportFragmentManager, "DISPLAY_REACTIONS") } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/TimelineEventController.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/TimelineEventController.kt index 0b78f8153f..e8268bace6 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/TimelineEventController.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/TimelineEventController.kt @@ -54,6 +54,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Tim interface Callback : ReactionPillCallback, AvatarCallback, BaseCallback, UrlClickCallback { fun onEventVisible(event: TimelineEvent) + fun onRoomCreateLinkClicked(url: String) fun onEncryptedMessageClicked(informationData: MessageInformationData, view: View) fun onImageMessageClicked(messageImageContent: MessageImageContent, mediaData: ImageContentRenderer.Data, view: View) fun onVideoMessageClicked(messageVideoContent: MessageVideoContent, mediaData: VideoContentRenderer.Data, view: View) @@ -158,7 +159,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Tim synchronized(modelCache) { for (i in 0 until modelCache.size) { if (modelCache[i]?.eventId == eventIdToHighlight - || modelCache[i]?.eventId == this.eventIdToHighlight) { + || modelCache[i]?.eventId == this.eventIdToHighlight) { modelCache[i] = null } } @@ -219,8 +220,8 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Tim // Should be build if not cached or if cached but contains mergedHeader or formattedDay // We then are sure we always have items up to date. if (modelCache[position] == null - || modelCache[position]?.mergedHeaderModel != null - || modelCache[position]?.formattedDayModel != null) { + || modelCache[position]?.mergedHeaderModel != null + || modelCache[position]?.formattedDayModel != null) { modelCache[position] = buildItemModels(position, currentSnapshot) } } @@ -294,7 +295,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Tim // => handle case where paginating from mergeable events and we get more val previousCollapseStateKey = mergedEventIds.intersect(mergeItemCollapseStates.keys).firstOrNull() val initialCollapseState = mergeItemCollapseStates.remove(previousCollapseStateKey) - ?: true + ?: true val isCollapsed = mergeItemCollapseStates.getOrPut(event.localId) { initialCollapseState } if (isCollapsed) { collapsedEventIds.addAll(mergedEventIds) diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/RoomCreateItemFactory.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/RoomCreateItemFactory.kt index 21fe85d774..e32e2746cf 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/RoomCreateItemFactory.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/RoomCreateItemFactory.kt @@ -18,7 +18,6 @@ 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 @@ -37,21 +36,16 @@ class RoomCreateItemFactory @Inject constructor(private val colorProvider: Color fun create(event: TimelineEvent, callback: TimelineEventController.Callback?): RoomCreateItem? { val createRoomContent = event.root.getClearContent().toModel() - ?: return null + ?: 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) - ) + +stringProvider.getString(R.string.room_tombstone_continuation_description) + +"\n" + span(stringProvider.getString(R.string.room_tombstone_predecessor_link)) { + textDecorationLine = "underline" + onClick = { callback?.onRoomCreateLinkClicked(roomLink) } + } } return RoomCreateItem_() .text(text) diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/RoomCreateItem.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/RoomCreateItem.kt index 117f4bd2d0..3e5ef30d90 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/RoomCreateItem.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/RoomCreateItem.kt @@ -21,10 +21,10 @@ 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 +import me.saket.bettermovementmethod.BetterLinkMovementMethod @EpoxyModelClass(layout = R.layout.item_timeline_event_create) abstract class RoomCreateItem : VectorEpoxyModel() { @@ -32,6 +32,7 @@ abstract class RoomCreateItem : VectorEpoxyModel() { @EpoxyAttribute lateinit var text: CharSequence override fun bind(holder: Holder) { + holder.description.movementMethod = BetterLinkMovementMethod.getInstance() holder.description.text = text } diff --git a/vector/src/main/res/layout/fragment_room_detail.xml b/vector/src/main/res/layout/fragment_room_detail.xml index f8cec43371..ae38b51008 100644 --- a/vector/src/main/res/layout/fragment_room_detail.xml +++ b/vector/src/main/res/layout/fragment_room_detail.xml @@ -74,12 +74,18 @@ android:id="@+id/recyclerView" android:layout_width="0dp" android:layout_height="0dp" - app:layout_constraintBottom_toTopOf="@+id/composerLayout" + app:layout_constraintBottom_toTopOf="@+id/recyclerViewBarrier" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/roomToolbar" tools:listitem="@layout/item_timeline_event_base" /> + + + Date: Fri, 26 Jul 2019 19:17:12 +0200 Subject: [PATCH 3/5] Tombstone : handle joining viaserver params --- .../matrix/android/api/MatrixPatterns.kt | 25 ++++++- .../api/session/room/RoomDirectoryService.kt | 5 -- .../android/api/session/room/RoomService.kt | 9 +++ .../session/room/members/MembershipService.kt | 2 +- .../api/session/room/model/RoomSummary.kt | 8 +- .../api/session/room/model/VersioningState.kt | 23 ++++++ .../database/mapper/RoomSummaryMapper.kt | 2 +- .../database/model/RoomSummaryEntity.kt | 11 ++- .../android/internal/session/SessionModule.kt | 6 +- .../room/DefaultRoomDirectoryService.kt | 8 -- .../session/room/DefaultRoomService.kt | 12 ++- .../android/internal/session/room/RoomAPI.kt | 1 + .../create/RoomCreateEventLiveObserver.kt | 74 +++++++++++++++++++ .../membership/DefaultMembershipService.kt | 4 +- .../room/membership/joining/JoinRoomTask.kt | 5 +- .../RoomTombstoneEventLiveObserver.kt | 7 +- .../core/platform/NotificationAreaView.kt | 16 ++-- .../riotx/features/home/HomeActivity.kt | 3 +- .../home/room/detail/RoomDetailActions.kt | 4 +- .../home/room/detail/RoomDetailActivity.kt | 2 + .../home/room/detail/RoomDetailFragment.kt | 62 +++++++++++----- .../home/room/detail/RoomDetailViewModel.kt | 66 +++++++++++++---- .../home/room/detail/RoomDetailViewState.kt | 4 +- .../home/room/list/RoomListViewModel.kt | 2 +- .../NotificationBroadcastReceiver.kt | 2 +- .../roomdirectory/RoomDirectoryViewModel.kt | 2 +- .../roompreview/RoomPreviewViewModel.kt | 2 +- .../main/res/layout/activity_room_detail.xml | 8 +- 28 files changed, 293 insertions(+), 82 deletions(-) create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/VersioningState.kt create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/create/RoomCreateEventLiveObserver.kt diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/MatrixPatterns.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/MatrixPatterns.kt index 5cb7f4ca0c..20cae629ec 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/MatrixPatterns.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/MatrixPatterns.kt @@ -123,9 +123,9 @@ object MatrixPatterns { */ fun isEventId(str: String?): Boolean { return str != null - && (str matches PATTERN_CONTAIN_MATRIX_EVENT_IDENTIFIER - || str matches PATTERN_CONTAIN_MATRIX_EVENT_IDENTIFIER_V3 - || str matches PATTERN_CONTAIN_MATRIX_EVENT_IDENTIFIER_V4) + && (str matches PATTERN_CONTAIN_MATRIX_EVENT_IDENTIFIER + || str matches PATTERN_CONTAIN_MATRIX_EVENT_IDENTIFIER_V3 + || str matches PATTERN_CONTAIN_MATRIX_EVENT_IDENTIFIER_V4) } /** @@ -137,4 +137,23 @@ object MatrixPatterns { fun isGroupId(str: String?): Boolean { return str != null && str matches PATTERN_CONTAIN_MATRIX_GROUP_IDENTIFIER } + + /** + * Extract server name from a matrix id + * + * @param matrixId + * @return null if not found or if matrixId is null + */ + fun extractServerNameFromId(matrixId: String?): String? { + if (matrixId == null) { + return null + } + + val index = matrixId.lastIndexOf(":") + + return if (index == -1) { + null + } else matrixId.substring(index + 1) + + } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/RoomDirectoryService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/RoomDirectoryService.kt index 5b66ddd8e7..ad95fb57f1 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/RoomDirectoryService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/RoomDirectoryService.kt @@ -34,11 +34,6 @@ interface RoomDirectoryService { publicRoomsParams: PublicRoomsParams, callback: MatrixCallback): Cancelable - /** - * Join a room by id - */ - fun joinRoom(roomId: String, - callback: MatrixCallback) /** * Fetches the overall metadata about protocols supported by the homeserver. diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/RoomService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/RoomService.kt index fc0bf49955..02f6530427 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/RoomService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/RoomService.kt @@ -32,6 +32,15 @@ interface RoomService { fun createRoom(createRoomParams: CreateRoomParams, callback: MatrixCallback) + /** + * Join a room by id + * @param roomId the roomId of the room to join + * @param viaServers the servers to attempt to join the room through. One of the servers must be participating in the room. + */ + fun joinRoom(roomId: String, + viaServers: List = emptyList(), + callback: MatrixCallback) + /** * Get a room from a roomId * @param roomId the roomId to look for. diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/members/MembershipService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/members/MembershipService.kt index ca3b99b6bf..c629326a25 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/members/MembershipService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/members/MembershipService.kt @@ -57,7 +57,7 @@ interface MembershipService { /** * Join the room, or accept an invitation. */ - fun join(callback: MatrixCallback) + fun join(viaServers: List = emptyList(), callback: MatrixCallback) /** * Leave the room, or reject an invitation. diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/RoomSummary.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/RoomSummary.kt index ce86df5c70..aae72dd41f 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/RoomSummary.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/RoomSummary.kt @@ -35,5 +35,9 @@ data class RoomSummary( val highlightCount: Int = 0, val tags: List = emptyList(), val membership: Membership = Membership.NONE, - val isVersioned: Boolean = false -) \ No newline at end of file + val versioningState: VersioningState = VersioningState.NONE +) { + + val isVersioned: Boolean + get() = versioningState != VersioningState.NONE +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/VersioningState.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/VersioningState.kt new file mode 100644 index 0000000000..f2753fbf35 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/VersioningState.kt @@ -0,0 +1,23 @@ +/* + * 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 + +enum class VersioningState { + NONE, + UPGRADED_ROOM_NOT_JOINED, + UPGRADED_ROOM_JOINED +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/RoomSummaryMapper.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/RoomSummaryMapper.kt index 6b8cf567ca..45328992e7 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/RoomSummaryMapper.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/RoomSummaryMapper.kt @@ -62,7 +62,7 @@ internal class RoomSummaryMapper @Inject constructor( notificationCount = roomSummaryEntity.notificationCount, tags = tags, membership = roomSummaryEntity.membership, - isVersioned = roomSummaryEntity.isVersioned + versioningState = roomSummaryEntity.versioningState ) } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/RoomSummaryEntity.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/RoomSummaryEntity.kt index 7470836161..ce1554ba8a 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/RoomSummaryEntity.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/RoomSummaryEntity.kt @@ -17,6 +17,7 @@ package im.vector.matrix.android.internal.database.model import im.vector.matrix.android.api.session.room.model.Membership +import im.vector.matrix.android.api.session.room.model.VersioningState import io.realm.RealmList import io.realm.RealmObject import io.realm.annotations.Ignore @@ -35,17 +36,23 @@ internal open class RoomSummaryEntity(@PrimaryKey var roomId: String = "", var otherMemberIds: RealmList = RealmList(), var notificationCount: Int = 0, var highlightCount: Int = 0, - var tags: RealmList = RealmList(), - var isVersioned: Boolean = false + var tags: RealmList = RealmList() ) : RealmObject() { private var membershipStr: String = Membership.NONE.name + private var versioningStateStr: String = VersioningState.NONE.name + @delegate:Ignore var membership: Membership by Delegates.observable(Membership.valueOf(membershipStr)) { _, _, newValue -> membershipStr = newValue.name } + @delegate:Ignore + var versioningState: VersioningState by Delegates.observable(VersioningState.valueOf(versioningStateStr)) { _, _, newValue -> + versioningStateStr = newValue.name + } + companion object } \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionModule.kt index af0c4e0f8f..ca8461f196 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionModule.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionModule.kt @@ -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.session.group.GroupSummaryUpdater import im.vector.matrix.android.internal.session.room.EventRelationsAggregationUpdater +import im.vector.matrix.android.internal.session.room.create.RoomCreateEventLiveObserver 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.util.md5 @@ -131,8 +132,11 @@ internal abstract class SessionModule { @Binds @IntoSet - abstract fun bindRoomCreateEventLiveObserver(roomTombstoneEventLiveObserver: RoomTombstoneEventLiveObserver): LiveEntityObserver + abstract fun bindRoomTombstoneEventLiveObserver(roomTombstoneEventLiveObserver: RoomTombstoneEventLiveObserver): LiveEntityObserver + @Binds + @IntoSet + abstract fun bindRoomCreateEventLiveObserver(roomCreateEventLiveObserver: RoomCreateEventLiveObserver): LiveEntityObserver @Binds abstract fun bindInitialSyncProgressService(initialSyncProgressService: DefaultInitialSyncProgressService): InitialSyncProgressService diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/DefaultRoomDirectoryService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/DefaultRoomDirectoryService.kt index 0b13fa3c18..fa4c2bc304 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/DefaultRoomDirectoryService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/DefaultRoomDirectoryService.kt @@ -31,7 +31,6 @@ import im.vector.matrix.android.internal.task.toConfigurableTask import javax.inject.Inject internal class DefaultRoomDirectoryService @Inject constructor(private val getPublicRoomTask: GetPublicRoomTask, - private val joinRoomTask: JoinRoomTask, private val getThirdPartyProtocolsTask: GetThirdPartyProtocolsTask, private val taskExecutor: TaskExecutor) : RoomDirectoryService { @@ -44,13 +43,6 @@ internal class DefaultRoomDirectoryService @Inject constructor(private val getPu .executeBy(taskExecutor) } - override fun joinRoom(roomId: String, callback: MatrixCallback) { - joinRoomTask - .configureWith(JoinRoomTask.Params(roomId)) - .dispatchTo(callback) - .executeBy(taskExecutor) - } - override fun getThirdPartyProtocol(callback: MatrixCallback>) { getThirdPartyProtocolsTask .toConfigurableTask() diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/DefaultRoomService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/DefaultRoomService.kt index e07b47a28b..fe5799795b 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/DefaultRoomService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/DefaultRoomService.kt @@ -22,6 +22,7 @@ import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.session.room.Room import im.vector.matrix.android.api.session.room.RoomService import im.vector.matrix.android.api.session.room.model.RoomSummary +import im.vector.matrix.android.api.session.room.model.VersioningState import im.vector.matrix.android.api.session.room.model.create.CreateRoomParams import im.vector.matrix.android.internal.database.mapper.RoomSummaryMapper import im.vector.matrix.android.internal.database.model.RoomEntity @@ -29,6 +30,7 @@ import im.vector.matrix.android.internal.database.model.RoomSummaryEntity import im.vector.matrix.android.internal.database.model.RoomSummaryEntityFields import im.vector.matrix.android.internal.database.query.where import im.vector.matrix.android.internal.session.room.create.CreateRoomTask +import im.vector.matrix.android.internal.session.room.membership.joining.JoinRoomTask import im.vector.matrix.android.internal.task.TaskExecutor import im.vector.matrix.android.internal.task.configureWith import im.vector.matrix.android.internal.util.fetchManaged @@ -37,6 +39,7 @@ import javax.inject.Inject internal class DefaultRoomService @Inject constructor(private val monarchy: Monarchy, private val roomSummaryMapper: RoomSummaryMapper, private val createRoomTask: CreateRoomTask, + private val joinRoomTask: JoinRoomTask, private val roomFactory: RoomFactory, private val taskExecutor: TaskExecutor) : RoomService { @@ -57,9 +60,16 @@ internal class DefaultRoomService @Inject constructor(private val monarchy: Mona { realm -> RoomSummaryEntity.where(realm) .isNotEmpty(RoomSummaryEntityFields.DISPLAY_NAME) - .notEqualTo(RoomSummaryEntityFields.IS_VERSIONED, true) + .notEqualTo(RoomSummaryEntityFields.VERSIONING_STATE_STR, VersioningState.UPGRADED_ROOM_JOINED.name) }, { roomSummaryMapper.map(it) } ) } + + override fun joinRoom(roomId: String, viaServers: List, callback: MatrixCallback) { + joinRoomTask + .configureWith(JoinRoomTask.Params(roomId, viaServers)) + .dispatchTo(callback) + .executeBy(taskExecutor) + } } \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomAPI.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomAPI.kt index 361a935d2f..0a5dcea3cb 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomAPI.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomAPI.kt @@ -218,6 +218,7 @@ internal interface RoomAPI { */ @POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/join") fun join(@Path("roomId") roomId: String, + @Query("server_name") viaServers: List, @Body params: Map): Call /** diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/create/RoomCreateEventLiveObserver.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/create/RoomCreateEventLiveObserver.kt new file mode 100644 index 0000000000..c091e6f6ba --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/create/RoomCreateEventLiveObserver.kt @@ -0,0 +1,74 @@ +/* + * 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.create + +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.VersioningState +import im.vector.matrix.android.api.session.room.model.create.RoomCreateContent +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 RoomCreateEventLiveObserver @Inject constructor(@SessionDatabase + realmConfiguration: RealmConfiguration) + : RealmLiveEntityObserver(realmConfiguration) { + + override val query = Monarchy.Query { + EventEntity.types(it, listOf(EventType.STATE_ROOM_CREATE)) + } + + override fun onChange(results: RealmResults, changeSet: OrderedCollectionChangeSet) { + changeSet.insertions + .asSequence() + .mapNotNull { + results[it]?.asDomain() + } + .toList() + .also { + handleRoomCreateEvents(it) + } + } + + private fun handleRoomCreateEvents(createEvents: List) = Realm.getInstance(realmConfiguration).use { + it.executeTransactionAsync { realm -> + for (event in createEvents) { + val createRoomContent = event.getClearContent().toModel() + val predecessorRoomId = createRoomContent?.predecessor?.roomId ?: continue + + val predecessorRoomSummary = RoomSummaryEntity.where(realm, predecessorRoomId).findFirst() + ?: RoomSummaryEntity(predecessorRoomId) + predecessorRoomSummary.versioningState = VersioningState.UPGRADED_ROOM_JOINED + realm.insertOrUpdate(predecessorRoomSummary) + + } + } + } + +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/DefaultMembershipService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/DefaultMembershipService.kt index 01fb746133..be6804b66c 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/DefaultMembershipService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/DefaultMembershipService.kt @@ -80,8 +80,8 @@ internal class DefaultMembershipService @Inject constructor(private val roomId: .executeBy(taskExecutor) } - override fun join(callback: MatrixCallback) { - val params = JoinRoomTask.Params(roomId) + override fun join(viaServers: List, callback: MatrixCallback) { + val params = JoinRoomTask.Params(roomId, viaServers) joinTask.configureWith(params) .dispatchTo(callback) .executeBy(taskExecutor) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/joining/JoinRoomTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/joining/JoinRoomTask.kt index 96454cbfeb..7bd580693e 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/joining/JoinRoomTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/joining/JoinRoomTask.kt @@ -25,7 +25,8 @@ import javax.inject.Inject internal interface JoinRoomTask : Task { data class Params( - val roomId: String + val roomId: String, + val viaServers: List = emptyList() ) } @@ -33,7 +34,7 @@ internal class DefaultJoinRoomTask @Inject constructor(private val roomAPI: Room override suspend fun execute(params: JoinRoomTask.Params): Try { return executeRequest { - apiCall = roomAPI.join(params.roomId, HashMap()) + apiCall = roomAPI.join(params.roomId, params.viaServers, HashMap()) } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/tombstone/RoomTombstoneEventLiveObserver.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/tombstone/RoomTombstoneEventLiveObserver.kt index 8c713ac4d4..8664fe4fa7 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/tombstone/RoomTombstoneEventLiveObserver.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/tombstone/RoomTombstoneEventLiveObserver.kt @@ -22,6 +22,7 @@ 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.VersioningState 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 @@ -64,8 +65,10 @@ internal class RoomTombstoneEventLiveObserver @Inject constructor(@SessionDataba if (createRoomContent?.replacementRoom == null) continue val predecessorRoomSummary = RoomSummaryEntity.where(realm, event.roomId).findFirst() - ?: RoomSummaryEntity(event.roomId) - predecessorRoomSummary.isVersioned = true + ?: RoomSummaryEntity(event.roomId) + if (predecessorRoomSummary.versioningState == VersioningState.NONE) { + predecessorRoomSummary.versioningState = VersioningState.UPGRADED_ROOM_NOT_JOINED + } realm.insertOrUpdate(predecessorRoomSummary) } diff --git a/vector/src/main/java/im/vector/riotx/core/platform/NotificationAreaView.kt b/vector/src/main/java/im/vector/riotx/core/platform/NotificationAreaView.kt index 6a9af14937..a321fd1a20 100644 --- a/vector/src/main/java/im/vector/riotx/core/platform/NotificationAreaView.kt +++ b/vector/src/main/java/im/vector/riotx/core/platform/NotificationAreaView.kt @@ -29,17 +29,16 @@ import android.widget.ImageView import android.widget.RelativeLayout import android.widget.TextView import androidx.core.content.ContextCompat -import androidx.core.text.bold import butterknife.BindView import butterknife.ButterKnife import im.vector.matrix.android.api.failure.MatrixError -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.Event +import im.vector.matrix.android.api.session.events.model.toModel import im.vector.matrix.android.api.session.room.model.tombstone.RoomTombstoneContent import im.vector.riotx.R import im.vector.riotx.core.error.ResourceLimitErrorFormatter import im.vector.riotx.features.themes.ThemeUtils -import me.gujun.android.span.addSpan import me.gujun.android.span.span import me.saket.bettermovementmethod.BetterLinkMovementMethod import timber.log.Timber @@ -108,19 +107,14 @@ class NotificationAreaView @JvmOverloads constructor( } private fun renderTombstone(state: State.Tombstone) { - val roomTombstoneContent = state.tombstoneContent - val roomLink = PermalinkFactory.createPermalink(roomTombstoneContent.replacementRoom) - ?: return - visibility = View.VISIBLE imageView.setImageResource(R.drawable.error) - val textColorInt = ThemeUtils.getColor(context, R.attr.vctr_message_text_color) val message = span { +resources.getString(R.string.room_tombstone_versioned_description) +"\n" span(resources.getString(R.string.room_tombstone_continuation_link)) { textDecorationLine = "underline" - onClick = { delegate?.onUrlClicked(roomLink) } + onClick = { delegate?.onTombstoneEventClicked(state.tombstoneEvent) } } } messageView.movementMethod = BetterLinkMovementMethod.getInstance() @@ -274,7 +268,7 @@ class NotificationAreaView @JvmOverloads constructor( object ConnectionError : State() // The room is dead - data class Tombstone(val tombstoneContent: RoomTombstoneContent) : State() + data class Tombstone(val tombstoneEvent: Event) : State() // Somebody is typing data class Typing(val message: String) : State() @@ -293,7 +287,7 @@ class NotificationAreaView @JvmOverloads constructor( * An interface to delegate some actions to another object */ interface Delegate { - fun onUrlClicked(url: String) + fun onTombstoneEventClicked(tombstoneEvent: Event) fun resendUnsentEvents() fun deleteUnsentEvents() fun closeScreen() diff --git a/vector/src/main/java/im/vector/riotx/features/home/HomeActivity.kt b/vector/src/main/java/im/vector/riotx/features/home/HomeActivity.kt index 4ec2c0ad95..3157fb1e65 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/HomeActivity.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/HomeActivity.kt @@ -145,7 +145,6 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable { override fun onNewIntent(intent: Intent?) { super.onNewIntent(intent) - if (intent?.hasExtra(EXTRA_CLEAR_EXISTING_NOTIFICATION) == true) { notificationDrawerManager.clearAllEvents() intent.removeExtra(EXTRA_CLEAR_EXISTING_NOTIFICATION) @@ -194,7 +193,7 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable { bugReporter.openBugReportScreen(this, false) return true } - R.id.menu_home_filter -> { + R.id.menu_home_filter -> { navigator.openRoomsFiltering(this) return true } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailActions.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailActions.kt index ace0802e09..79b00b6911 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailActions.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailActions.kt @@ -17,6 +17,7 @@ package im.vector.riotx.features.home.room.detail import com.jaiselrahman.filepicker.model.MediaFile +import im.vector.matrix.android.api.session.events.model.Event import im.vector.matrix.android.api.session.room.model.EditAggregatedSummary import im.vector.matrix.android.api.session.room.model.message.MessageFileContent import im.vector.matrix.android.api.session.room.timeline.Timeline @@ -27,13 +28,14 @@ sealed class RoomDetailActions { data class SendMessage(val text: String, val autoMarkdown: Boolean) : RoomDetailActions() data class SendMedia(val mediaFiles: List) : RoomDetailActions() data class EventDisplayed(val event: TimelineEvent) : RoomDetailActions() - data class LoadMore(val direction: Timeline.Direction) : RoomDetailActions() + data class LoadMoreTimelineEvents(val direction: Timeline.Direction) : RoomDetailActions() data class SendReaction(val reaction: String, val targetEventId: String) : RoomDetailActions() data class RedactAction(val targetEventId: String, val reason: String? = "") : RoomDetailActions() data class UndoReaction(val targetEventId: String, val key: String, val reason: String? = "") : RoomDetailActions() data class UpdateQuickReactAction(val targetEventId: String, val selectedReaction: String, val add: Boolean) : RoomDetailActions() data class NavigateToEvent(val eventId: String, val position: Int?) : RoomDetailActions() data class DownloadFile(val eventId: String, val messageFileContent: MessageFileContent) : RoomDetailActions() + data class HandleTombstoneEvent(val event: Event): RoomDetailActions() object AcceptInvite : RoomDetailActions() object RejectInvite : RoomDetailActions() diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailActivity.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailActivity.kt index 6ad9a61f1a..80e943cb04 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailActivity.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailActivity.kt @@ -24,6 +24,7 @@ import im.vector.riotx.R import im.vector.riotx.core.extensions.replaceFragment import im.vector.riotx.core.platform.ToolbarConfigurable import im.vector.riotx.core.platform.VectorBaseActivity +import kotlinx.android.synthetic.main.merge_overlay_waiting_view.* class RoomDetailActivity : VectorBaseActivity(), ToolbarConfigurable { @@ -33,6 +34,7 @@ class RoomDetailActivity : VectorBaseActivity(), ToolbarConfigurable { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + waitingView = waiting_view if (isFirstCreation()) { val roomDetailArgs: RoomDetailArgs = intent?.extras?.getParcelable(EXTRA_ROOM_DETAIL_ARGS) ?: return diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt index ab766e149d..738b726c29 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt @@ -45,6 +45,12 @@ import androidx.recyclerview.widget.RecyclerView import butterknife.BindView import com.airbnb.epoxy.EpoxyModel import com.airbnb.epoxy.EpoxyVisibilityTracker +import com.airbnb.mvrx.Async +import com.airbnb.mvrx.DeliveryMode +import com.airbnb.mvrx.Fail +import com.airbnb.mvrx.Loading +import com.airbnb.mvrx.Success +import com.airbnb.mvrx.Uninitialized import com.airbnb.mvrx.args import com.airbnb.mvrx.fragmentViewModel import com.github.piasy.biv.BigImageViewer @@ -58,6 +64,7 @@ import com.otaliastudios.autocomplete.AutocompleteCallback import com.otaliastudios.autocomplete.CharPolicy import im.vector.matrix.android.api.permalinks.PermalinkFactory import im.vector.matrix.android.api.session.Session +import im.vector.matrix.android.api.session.events.model.Event import im.vector.matrix.android.api.session.room.model.EditAggregatedSummary import im.vector.matrix.android.api.session.room.model.Membership import im.vector.matrix.android.api.session.room.model.message.* @@ -73,6 +80,8 @@ import im.vector.riotx.core.epoxy.LayoutManagerStateRestorer import im.vector.riotx.core.error.ErrorFormatter import im.vector.riotx.core.extensions.hideKeyboard import im.vector.riotx.core.extensions.observeEvent +import im.vector.riotx.core.extensions.observeK +import im.vector.riotx.core.extensions.observeNotNull import im.vector.riotx.core.extensions.setTextOrHide import im.vector.riotx.core.files.addEntryToDownloadManager import im.vector.riotx.core.glide.GlideApp @@ -108,7 +117,9 @@ import im.vector.riotx.features.settings.VectorPreferences import im.vector.riotx.features.themes.ThemeUtils import kotlinx.android.parcel.Parcelize import kotlinx.android.synthetic.main.fragment_room_detail.* +import kotlinx.android.synthetic.main.item_loading.* import kotlinx.android.synthetic.main.merge_composer_layout.view.* +import kotlinx.android.synthetic.main.merge_overlay_waiting_view.* import org.commonmark.parser.Parser import timber.log.Timber import java.io.File @@ -222,6 +233,10 @@ class RoomDetailFragment : scrollOnHighlightedEventCallback.scheduleScrollTo(it) } + roomDetailViewModel.selectSubscribe(this, RoomDetailViewState::tombstoneEventHandling, uniqueOnly("tombstoneEventHandling")) { + renderTombstoneEventHandling(it) + } + roomDetailViewModel.downloadedFileEvent.observeEvent(this) { downloadFileState -> if (downloadFileState.throwable != null) { requireActivity().toast(errorFormatter.toHumanReadable(downloadFileState.throwable)) @@ -244,13 +259,8 @@ class RoomDetailFragment : private fun setupNotificationView() { notificationAreaView.delegate = object : NotificationAreaView.Delegate { - override fun onUrlClicked(url: String) { - permalinkHandler.launch(requireActivity(), url, object : NavigateToRoomInterceptor { - override fun navToRoom(roomId: String, eventId: String?): Boolean { - requireActivity().finish() - return false - } - }) + override fun onTombstoneEventClicked(tombstoneEvent: Event) { + roomDetailViewModel.process(RoomDetailActions.HandleTombstoneEvent(tombstoneEvent)) } override fun resendUnsentEvents() { @@ -360,7 +370,7 @@ class RoomDetailFragment : recyclerView.addOnScrollListener( EndlessRecyclerViewScrollListener(layoutManager, RoomDetailViewModel.PAGINATION_COUNT) { direction -> - roomDetailViewModel.process(RoomDetailActions.LoadMore(direction)) + roomDetailViewModel.process(RoomDetailActions.LoadMoreTimelineEvents(direction)) }) recyclerView.setController(timelineEventController) timelineEventController.callback = this @@ -552,7 +562,6 @@ class RoomDetailFragment : if (summary?.membership == Membership.JOIN) { timelineEventController.setTimeline(state.timeline, state.eventId) inviteView.visibility = View.GONE - val uid = session.myUserId val meMember = session.getRoom(state.roomId)?.getRoomMember(uid) avatarRenderer.render(meMember?.avatarUrl, uid, meMember?.displayName, composerLayout.composerAvatarImageView) @@ -566,14 +575,13 @@ class RoomDetailFragment : } else if (state.asyncInviter.complete) { vectorBaseActivity.finish() } - - if (state.tombstoneContent == null) { + if (state.tombstoneEvent == null) { composerLayout.visibility = View.VISIBLE composerLayout.setRoomEncrypted(state.isEncrypted) notificationAreaView.render(NotificationAreaView.State.Hidden) } else { composerLayout.visibility = View.GONE - notificationAreaView.render(NotificationAreaView.State.Tombstone(state.tombstoneContent)) + notificationAreaView.render(NotificationAreaView.State.Tombstone(state.tombstoneEvent)) } } @@ -594,6 +602,26 @@ class RoomDetailFragment : autocompleteUserPresenter.render(state.asyncUsers) } + private fun renderTombstoneEventHandling(async: Async) { + when (async) { + is Loading -> { + // TODO Better handling progress + vectorBaseActivity.showWaitingView() + vectorBaseActivity.waiting_view_status_text.visibility = View.VISIBLE + vectorBaseActivity.waiting_view_status_text.text = getString(R.string.join) + } + is Success -> { + navigator.openRoom(vectorBaseActivity, async()) + vectorBaseActivity.finish() + } + is Fail -> { + vectorBaseActivity.hideWaitingView() + vectorBaseActivity.toast(errorFormatter.toHumanReadable(async.error)) + } + } + } + + private fun renderSendMessageResult(sendMessageResult: SendMessageResult) { when (sendMessageResult) { is SendMessageResult.MessageSent, @@ -627,7 +655,7 @@ class RoomDetailFragment : .show() } - // TimelineEventController.Callback ************************************************************ +// TimelineEventController.Callback ************************************************************ override fun onUrlClicked(url: String): Boolean { return permalinkHandler.launch(requireActivity(), url, object : NavigateToRoomInterceptor { @@ -760,7 +788,7 @@ class RoomDetailFragment : }) } - // AutocompleteUserPresenter.Callback +// AutocompleteUserPresenter.Callback override fun onQueryUsers(query: CharSequence?) { textComposerViewModel.process(TextComposerActions.QueryUsers(query)) @@ -862,13 +890,13 @@ class RoomDetailFragment : } } - //utils +//utils /** * Insert an user displayname in the message editor. * * @param text the text to insert. */ - //TODO legacy, refactor +//TODO legacy, refactor private fun insertUserDisplayNameInTextEditor(text: String?) { //TODO move logic outside of fragment if (null != text) { @@ -919,7 +947,7 @@ class RoomDetailFragment : snack.show() } - // VectorInviteView.Callback +// VectorInviteView.Callback override fun onAcceptInvite() { notificationDrawerManager.clearMemberShipNotificationForRoom(roomDetailArgs.roomId) diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt index 5f6c17df77..aa5b5b5823 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt @@ -20,7 +20,11 @@ import android.net.Uri import android.text.TextUtils import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData +import arrow.core.success +import com.airbnb.mvrx.Async +import com.airbnb.mvrx.Fail import com.airbnb.mvrx.FragmentViewModelContext +import com.airbnb.mvrx.Loading import com.airbnb.mvrx.MvRxViewModelFactory import com.airbnb.mvrx.Success import com.airbnb.mvrx.ViewModelContext @@ -28,6 +32,7 @@ import com.jakewharton.rxrelay2.BehaviorRelay import com.squareup.inject.assisted.Assisted import com.squareup.inject.assisted.AssistedInject import im.vector.matrix.android.api.MatrixCallback +import im.vector.matrix.android.api.MatrixPatterns import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.content.ContentAttachmentData import im.vector.matrix.android.api.session.events.model.EventType @@ -49,7 +54,11 @@ import im.vector.riotx.core.utils.LiveEvent import im.vector.riotx.features.command.CommandParser import im.vector.riotx.features.command.ParsedCommand import im.vector.riotx.features.home.room.detail.timeline.helper.TimelineDisplayableEvents +import io.reactivex.Single import io.reactivex.rxkotlin.subscribeBy +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.withContext import org.commonmark.parser.Parser import org.commonmark.renderer.html.HtmlRenderer import timber.log.Timber @@ -107,7 +116,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro is RoomDetailActions.SendMessage -> handleSendMessage(action) is RoomDetailActions.SendMedia -> handleSendMedia(action) is RoomDetailActions.EventDisplayed -> handleEventDisplayed(action) - is RoomDetailActions.LoadMore -> handleLoadMore(action) + is RoomDetailActions.LoadMoreTimelineEvents -> handleLoadMore(action) is RoomDetailActions.SendReaction -> handleSendReaction(action) is RoomDetailActions.AcceptInvite -> handleAcceptInvite() is RoomDetailActions.RejectInvite -> handleRejectInvite() @@ -119,10 +128,42 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro is RoomDetailActions.EnterReplyMode -> handleReplyAction(action) is RoomDetailActions.DownloadFile -> handleDownloadFile(action) is RoomDetailActions.NavigateToEvent -> handleNavigateToEvent(action) + is RoomDetailActions.HandleTombstoneEvent -> handleTombstoneEvent(action) else -> Timber.e("Unhandled Action: $action") } } + private fun handleTombstoneEvent(action: RoomDetailActions.HandleTombstoneEvent) { + val tombstoneContent = action.event.getClearContent().toModel() + ?: return + + val roomId = tombstoneContent.replacementRoom + // TODO replace with rx flux + if (session.getRoom(roomId)?.roomSummary()?.membership == Membership.JOIN) { + setState { copy(tombstoneEventHandling = Success(roomId)) } + } else { + val viaServer = MatrixPatterns.extractServerNameFromId(action.event.senderId).let { + if (it.isNullOrBlank()) { + emptyList() + } else { + listOf(it) + } + } + setState { copy(tombstoneEventHandling = Loading()) } + session.joinRoom(roomId, viaServer, object : MatrixCallback { + override fun onSuccess(data: Unit) { + setState { copy(tombstoneEventHandling = Success(roomId)) } + + } + + override fun onFailure(failure: Throwable) { + setState { copy(tombstoneEventHandling = Fail(failure)) } + } + }) + } + + } + private fun enterEditMode(event: TimelineEvent) { setState { copy( @@ -143,7 +184,6 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro val nonBlockingPopAlert: LiveData>>> get() = _nonBlockingPopAlert - private val _sendMessageResultLiveData = MutableLiveData>() val sendMessageResultLiveData: LiveData> get() = _sendMessageResultLiveData @@ -232,7 +272,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro //is original event a reply? val inReplyTo = state.sendMode.timelineEvent.root.getClearContent().toModel()?.relatesTo?.inReplyTo?.eventId - ?: state.sendMode.timelineEvent.root.content.toModel()?.relatesTo?.inReplyTo?.eventId + ?: state.sendMode.timelineEvent.root.content.toModel()?.relatesTo?.inReplyTo?.eventId if (inReplyTo != null) { //TODO check if same content? room.getTimeLineEvent(inReplyTo)?.let { @@ -241,12 +281,12 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro } else { val messageContent: MessageContent? = state.sendMode.timelineEvent.annotations?.editSummary?.aggregatedContent.toModel() - ?: state.sendMode.timelineEvent.root.getClearContent().toModel() + ?: state.sendMode.timelineEvent.root.getClearContent().toModel() val existingBody = messageContent?.body ?: "" if (existingBody != action.text) { room.editTextMessage(state.sendMode.timelineEvent.root.eventId - ?: "", messageContent?.type - ?: MessageType.MSGTYPE_TEXT, action.text, action.autoMarkdown) + ?: "", messageContent?.type + ?: MessageType.MSGTYPE_TEXT, action.text, action.autoMarkdown) } else { Timber.w("Same message content, do not send edition") } @@ -261,7 +301,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro is SendMode.QUOTE -> { val messageContent: MessageContent? = state.sendMode.timelineEvent.annotations?.editSummary?.aggregatedContent.toModel() - ?: state.sendMode.timelineEvent.root.getClearContent().toModel() + ?: state.sendMode.timelineEvent.root.getClearContent().toModel() val textMsg = messageContent?.body val finalText = legacyRiotQuoteText(textMsg, action.text) @@ -401,7 +441,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro } } - private fun handleLoadMore(action: RoomDetailActions.LoadMore) { + private fun handleLoadMore(action: RoomDetailActions.LoadMoreTimelineEvents) { timeline.paginate(action.direction, PAGINATION_COUNT) } @@ -410,7 +450,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro } private fun handleAcceptInvite() { - room.join(object : MatrixCallback {}) + room.join(callback = object : MatrixCallback {}) } private fun handleEditAction(action: RoomDetailActions.EnterEditMode) { @@ -558,12 +598,8 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro setState { copy(asyncInviter = Success(it)) } } } - if (summary.isVersioned) { - room.getStateEvent(EventType.STATE_ROOM_TOMBSTONE) - ?.getClearContent() - ?.toModel()?.also { - setState { copy(tombstoneContent = it) } - } + room.getStateEvent(EventType.STATE_ROOM_TOMBSTONE)?.also { + setState { copy(tombstoneEvent = it) } } } } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewState.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewState.kt index c78e8dc3a9..77c1c63cb6 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewState.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewState.kt @@ -19,6 +19,7 @@ package im.vector.riotx.features.home.room.detail import com.airbnb.mvrx.Async import com.airbnb.mvrx.MvRxState import com.airbnb.mvrx.Uninitialized +import im.vector.matrix.android.api.session.events.model.Event 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 @@ -48,7 +49,8 @@ data class RoomDetailViewState( val asyncRoomSummary: Async = Uninitialized, val sendMode: SendMode = SendMode.REGULAR, val isEncrypted: Boolean = false, - val tombstoneContent: RoomTombstoneContent? = null + val tombstoneEvent: Event? = null, + val tombstoneEventHandling: Async = Uninitialized ) : MvRxState { constructor(args: RoomDetailArgs) : this(roomId = args.roomId, eventId = args.eventId) diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListViewModel.kt index a1ae4fdf8a..6ff4792624 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListViewModel.kt @@ -134,7 +134,7 @@ class RoomListViewModel @AssistedInject constructor(@Assisted initialState: Room ) } - session.getRoom(roomId)?.join(object : MatrixCallback { + session.getRoom(roomId)?.join(emptyList(), object : MatrixCallback { override fun onSuccess(data: Unit) { // We do not update the joiningRoomsIds here, because, the room is not joined yet regarding the sync data. // Instead, we wait for the room to be joined diff --git a/vector/src/main/java/im/vector/riotx/features/notifications/NotificationBroadcastReceiver.kt b/vector/src/main/java/im/vector/riotx/features/notifications/NotificationBroadcastReceiver.kt index ac6068b08d..638cf7d161 100644 --- a/vector/src/main/java/im/vector/riotx/features/notifications/NotificationBroadcastReceiver.kt +++ b/vector/src/main/java/im/vector/riotx/features/notifications/NotificationBroadcastReceiver.kt @@ -75,7 +75,7 @@ class NotificationBroadcastReceiver : BroadcastReceiver() { private fun handleJoinRoom(roomId: String) { activeSessionHolder.getSafeActiveSession()?.let { session -> session.getRoom(roomId) - ?.join(object : MatrixCallback {}) + ?.join(emptyList(), object : MatrixCallback {}) } } diff --git a/vector/src/main/java/im/vector/riotx/features/roomdirectory/RoomDirectoryViewModel.kt b/vector/src/main/java/im/vector/riotx/features/roomdirectory/RoomDirectoryViewModel.kt index c47e8bbdbf..24f2469a89 100644 --- a/vector/src/main/java/im/vector/riotx/features/roomdirectory/RoomDirectoryViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/roomdirectory/RoomDirectoryViewModel.kt @@ -199,7 +199,7 @@ class RoomDirectoryViewModel @AssistedInject constructor(@Assisted initialState: ) } - session.joinRoom(publicRoom.roomId, object : MatrixCallback { + session.joinRoom(publicRoom.roomId, emptyList(), object : MatrixCallback { override fun onSuccess(data: Unit) { // We do not update the joiningRoomsIds here, because, the room is not joined yet regarding the sync data. // Instead, we wait for the room to be joined diff --git a/vector/src/main/java/im/vector/riotx/features/roomdirectory/roompreview/RoomPreviewViewModel.kt b/vector/src/main/java/im/vector/riotx/features/roomdirectory/roompreview/RoomPreviewViewModel.kt index bf5fa74321..964f30d3ce 100644 --- a/vector/src/main/java/im/vector/riotx/features/roomdirectory/roompreview/RoomPreviewViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/roomdirectory/roompreview/RoomPreviewViewModel.kt @@ -90,7 +90,7 @@ class RoomPreviewViewModel @AssistedInject constructor(@Assisted initialState: R ) } - session.joinRoom(state.roomId, object : MatrixCallback { + session.joinRoom(state.roomId, emptyList(), object : MatrixCallback { override fun onSuccess(data: Unit) { // We do not update the joiningRoomsIds here, because, the room is not joined yet regarding the sync data. // Instead, we wait for the room to be joined diff --git a/vector/src/main/res/layout/activity_room_detail.xml b/vector/src/main/res/layout/activity_room_detail.xml index 1dae010e2f..a02ff1d1b8 100644 --- a/vector/src/main/res/layout/activity_room_detail.xml +++ b/vector/src/main/res/layout/activity_room_detail.xml @@ -1,8 +1,14 @@ + + + + \ No newline at end of file From dc4786ecf0b4f5719b8e9562090f5c0921618c68 Mon Sep 17 00:00:00 2001 From: ganfra Date: Tue, 30 Jul 2019 19:13:09 +0200 Subject: [PATCH 4/5] Room upgrade: add rx flux and handle failures more precisely --- .../main/java/im/vector/matrix/rx/RxRoom.kt | 4 +++ .../java/im/vector/matrix/rx/RxSession.kt | 4 +++ .../android/api/session/room/RoomService.kt | 2 +- .../session/room/failure/CreateRoomFailure.kt | 25 +++++++++++++++ .../session/room/failure/JoinRoomFailure.kt | 25 +++++++++++++++ .../internal/database/RealmQueryLatch.kt | 32 +++++++++---------- .../session/room/DefaultRoomService.kt | 4 +-- .../session/room/create/CreateRoomTask.kt | 10 ++++-- .../room/membership/joining/JoinRoomTask.kt | 10 ++++-- .../vector/riotx/core/error/ErrorFormatter.kt | 8 +++-- .../createdirect/CreateDirectRoomActivity.kt | 13 +++++--- .../home/room/detail/RoomDetailFragment.kt | 3 +- .../home/room/detail/RoomDetailViewModel.kt | 21 +++++------- vector/src/main/res/values/strings_riotX.xml | 2 ++ 14 files changed, 118 insertions(+), 45 deletions(-) create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/failure/CreateRoomFailure.kt create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/failure/JoinRoomFailure.kt diff --git a/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxRoom.kt b/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxRoom.kt index 419fb6c913..e62b7df57f 100644 --- a/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxRoom.kt +++ b/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxRoom.kt @@ -45,6 +45,10 @@ class RxRoom(private val room: Room) { room.loadRoomMembersIfNeeded(MatrixCallbackSingle(it)).toSingle(it) } + fun joinRoom(viaServers: List = emptyList()): Single = Single.create { + room.join(viaServers, MatrixCallbackSingle(it)).toSingle(it) + } + } fun Room.rx(): RxRoom { diff --git a/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxSession.kt b/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxSession.kt index 97661cebd1..f3fb06a45a 100644 --- a/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxSession.kt +++ b/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxSession.kt @@ -63,6 +63,10 @@ class RxSession(private val session: Session) { session.searchUsersDirectory(search, limit, excludedUserIds, MatrixCallbackSingle(it)).toSingle(it) } + fun joinRoom(roomId: String, viaServers: List = emptyList()): Single = Single.create { + session.joinRoom(roomId, viaServers, MatrixCallbackSingle(it)).toSingle(it) + } + } fun Session.rx(): RxSession { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/RoomService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/RoomService.kt index b3f060d706..7ec50bd2ca 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/RoomService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/RoomService.kt @@ -39,7 +39,7 @@ interface RoomService { */ fun joinRoom(roomId: String, viaServers: List = emptyList(), - callback: MatrixCallback) + callback: MatrixCallback): Cancelable /** * Get a room from a roomId diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/failure/CreateRoomFailure.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/failure/CreateRoomFailure.kt new file mode 100644 index 0000000000..086dc621ca --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/failure/CreateRoomFailure.kt @@ -0,0 +1,25 @@ +/* + * 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.failure + +import im.vector.matrix.android.api.failure.Failure + +sealed class CreateRoomFailure : Failure.FeatureFailure() { + + object CreatedWithTimeout: CreateRoomFailure() + +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/failure/JoinRoomFailure.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/failure/JoinRoomFailure.kt new file mode 100644 index 0000000000..4c7dd62ad6 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/failure/JoinRoomFailure.kt @@ -0,0 +1,25 @@ +/* + * 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.failure + +import im.vector.matrix.android.api.failure.Failure + +sealed class JoinRoomFailure : Failure.FeatureFailure() { + + object JoinedWithTimeout : JoinRoomFailure() + +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/RealmQueryLatch.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/RealmQueryLatch.kt index 1fc60d8098..3e3ffad45c 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/RealmQueryLatch.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/RealmQueryLatch.kt @@ -16,45 +16,45 @@ package im.vector.matrix.android.internal.database -import android.os.Handler -import android.os.HandlerThread +import im.vector.matrix.android.internal.util.createBackgroundHandler import io.realm.* -import timber.log.Timber import java.util.concurrent.CountDownLatch import java.util.concurrent.TimeUnit +import java.util.concurrent.atomic.AtomicReference -private const val THREAD_NAME = "REALM_QUERY_LATCH" class RealmQueryLatch(private val realmConfiguration: RealmConfiguration, private val realmQueryBuilder: (Realm) -> RealmQuery) { - @Throws(InterruptedException::class) - fun await(timeout: Long = Long.MAX_VALUE, timeUnit: TimeUnit = TimeUnit.MILLISECONDS) { - val latch = CountDownLatch(1) - val handlerThread = HandlerThread(THREAD_NAME + hashCode()) - handlerThread.start() - val handler = Handler(handlerThread.looper) - val runnable = Runnable { - val realm = Realm.getInstance(realmConfiguration) - val result = realmQueryBuilder(realm).findAllAsync() + private companion object { + val QUERY_LATCH_HANDLER = createBackgroundHandler("REALM_QUERY_LATCH") + } + @Throws(InterruptedException::class) + fun await(timeout: Long, timeUnit: TimeUnit) { + val realmRef = AtomicReference() + val latch = CountDownLatch(1) + QUERY_LATCH_HANDLER.post { + val realm = Realm.getInstance(realmConfiguration) + realmRef.set(realm) + val result = realmQueryBuilder(realm).findAllAsync() result.addChangeListener(object : RealmChangeListener> { override fun onChange(t: RealmResults) { if (t.isNotEmpty()) { result.removeChangeListener(this) - realm.close() latch.countDown() } } }) } - handler.post(runnable) try { latch.await(timeout, timeUnit) } catch (exception: InterruptedException) { throw exception } finally { - handlerThread.quit() + QUERY_LATCH_HANDLER.post { + realmRef.getAndSet(null).close() + } } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/DefaultRoomService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/DefaultRoomService.kt index 78c19af3e2..5a7f82b04b 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/DefaultRoomService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/DefaultRoomService.kt @@ -67,8 +67,8 @@ internal class DefaultRoomService @Inject constructor(private val monarchy: Mona ) } - override fun joinRoom(roomId: String, viaServers: List, callback: MatrixCallback) { - joinRoomTask + override fun joinRoom(roomId: String, viaServers: List, callback: MatrixCallback): Cancelable { + return joinRoomTask .configureWith(JoinRoomTask.Params(roomId, viaServers)) .dispatchTo(callback) .executeBy(taskExecutor) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/create/CreateRoomTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/create/CreateRoomTask.kt index f9cad783a6..6091f6b96c 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/create/CreateRoomTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/create/CreateRoomTask.kt @@ -17,7 +17,9 @@ package im.vector.matrix.android.internal.session.room.create import arrow.core.Try +import arrow.core.recoverWith import com.zhuinden.monarchy.Monarchy +import im.vector.matrix.android.api.session.room.failure.CreateRoomFailure import im.vector.matrix.android.api.session.room.model.create.CreateRoomParams import im.vector.matrix.android.api.session.room.model.create.CreateRoomResponse import im.vector.matrix.android.internal.database.RealmQueryLatch @@ -57,9 +59,11 @@ internal class DefaultCreateRoomTask @Inject constructor(private val roomAPI: Ro realm.where(RoomEntity::class.java) .equalTo(RoomEntityFields.ROOM_ID, roomId) } - Try { - rql.await(timeout = 20L, timeUnit = TimeUnit.SECONDS) - roomId + try { + rql.await(timeout = 1L, timeUnit = TimeUnit.MINUTES) + Try.just(roomId) + } catch (exception: Exception) { + Try.raise(CreateRoomFailure.CreatedWithTimeout) } }.flatMap { roomId -> if (params.isDirect()) { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/joining/JoinRoomTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/joining/JoinRoomTask.kt index a8e43236b4..bc76a211fc 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/joining/JoinRoomTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/joining/JoinRoomTask.kt @@ -17,6 +17,8 @@ package im.vector.matrix.android.internal.session.room.membership.joining import arrow.core.Try +import im.vector.matrix.android.api.session.room.failure.CreateRoomFailure +import im.vector.matrix.android.api.session.room.failure.JoinRoomFailure import im.vector.matrix.android.internal.database.RealmQueryLatch import im.vector.matrix.android.internal.database.model.RoomEntity import im.vector.matrix.android.internal.database.model.RoomEntityFields @@ -50,9 +52,11 @@ internal class DefaultJoinRoomTask @Inject constructor(private val roomAPI: Room realm.where(RoomEntity::class.java) .equalTo(RoomEntityFields.ROOM_ID, roomId) } - Try { - rql.await(20L, TimeUnit.SECONDS) - roomId + try { + rql.await(timeout = 1L, timeUnit = TimeUnit.MINUTES) + Try.just(roomId) + } catch (exception: Exception) { + Try.raise(JoinRoomFailure.JoinedWithTimeout) } }.flatMap { roomId -> setReadMarkers(roomId) diff --git a/vector/src/main/java/im/vector/riotx/core/error/ErrorFormatter.kt b/vector/src/main/java/im/vector/riotx/core/error/ErrorFormatter.kt index 7619d433d7..9d478f34e8 100644 --- a/vector/src/main/java/im/vector/riotx/core/error/ErrorFormatter.kt +++ b/vector/src/main/java/im/vector/riotx/core/error/ErrorFormatter.kt @@ -30,12 +30,16 @@ class ErrorFormatter @Inject constructor(val stringProvider: StringProvider) { } fun toHumanReadable(throwable: Throwable?): String { - return when (throwable) { null -> "" is Failure.NetworkConnection -> stringProvider.getString(R.string.error_no_network) + is Failure.ServerError -> { + throwable.error.message.takeIf { it.isNotEmpty() } + ?: throwable.error.code.takeIf { it.isNotEmpty() } + ?: stringProvider.getString(R.string.unknown_error) + } else -> throwable.localizedMessage + ?: stringProvider.getString(R.string.unknown_error) } - } } \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomActivity.kt b/vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomActivity.kt index 13bc93686f..8c40e5691d 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomActivity.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomActivity.kt @@ -29,6 +29,7 @@ import com.airbnb.mvrx.Fail import com.airbnb.mvrx.Loading import com.airbnb.mvrx.Success import com.airbnb.mvrx.viewModel +import im.vector.matrix.android.api.session.room.failure.CreateRoomFailure import im.vector.riotx.R import im.vector.riotx.core.di.ScreenComponent import im.vector.riotx.core.error.ErrorFormatter @@ -38,6 +39,7 @@ import im.vector.riotx.core.extensions.observeEvent import im.vector.riotx.core.platform.SimpleFragmentActivity import im.vector.riotx.core.platform.WaitingViewData import kotlinx.android.synthetic.main.activity.* +import timber.log.Timber import javax.inject.Inject class CreateDirectRoomActivity : SimpleFragmentActivity() { @@ -91,10 +93,13 @@ class CreateDirectRoomActivity : SimpleFragmentActivity() { private fun renderCreationFailure(error: Throwable) { hideWaitingView() - AlertDialog.Builder(this) - .setMessage(errorFormatter.toHumanReadable(error)) - .setPositiveButton(R.string.ok) { dialog, id -> dialog.cancel() } - .show() + if (error is CreateRoomFailure.CreatedWithTimeout) { + finish() + } else + AlertDialog.Builder(this) + .setMessage(errorFormatter.toHumanReadable(error)) + .setPositiveButton(R.string.ok) { dialog, id -> dialog.cancel() } + .show() } private fun renderCreationSuccess(roomId: String?) { diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt index 738b726c29..1742b405f3 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt @@ -65,6 +65,7 @@ import com.otaliastudios.autocomplete.CharPolicy import im.vector.matrix.android.api.permalinks.PermalinkFactory import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.events.model.Event +import im.vector.matrix.android.api.session.room.failure.JoinRoomFailure import im.vector.matrix.android.api.session.room.model.EditAggregatedSummary import im.vector.matrix.android.api.session.room.model.Membership import im.vector.matrix.android.api.session.room.model.message.* @@ -608,7 +609,7 @@ class RoomDetailFragment : // TODO Better handling progress vectorBaseActivity.showWaitingView() vectorBaseActivity.waiting_view_status_text.visibility = View.VISIBLE - vectorBaseActivity.waiting_view_status_text.text = getString(R.string.join) + vectorBaseActivity.waiting_view_status_text.text = getString(R.string.joining_room) } is Success -> { navigator.openRoom(vectorBaseActivity, async()) diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt index 983851ffc7..304e09f883 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt @@ -140,8 +140,8 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro ?: return val roomId = tombstoneContent.replacementRoom - // TODO replace with rx flux - if (session.getRoom(roomId)?.roomSummary()?.membership == Membership.JOIN) { + val isRoomJoined = session.getRoom(roomId)?.roomSummary()?.membership == Membership.JOIN + if (isRoomJoined) { setState { copy(tombstoneEventHandling = Success(roomId)) } } else { val viaServer = MatrixPatterns.extractServerNameFromId(action.event.senderId).let { @@ -151,17 +151,12 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro listOf(it) } } - setState { copy(tombstoneEventHandling = Loading()) } - session.joinRoom(roomId, viaServer, object : MatrixCallback { - override fun onSuccess(data: Unit) { - setState { copy(tombstoneEventHandling = Success(roomId)) } - - } - - override fun onFailure(failure: Throwable) { - setState { copy(tombstoneEventHandling = Fail(failure)) } - } - }) + session.rx() + .joinRoom(roomId, viaServer) + .map { roomId } + .execute { + copy(tombstoneEventHandling = it) + } } } diff --git a/vector/src/main/res/values/strings_riotX.xml b/vector/src/main/res/values/strings_riotX.xml index 45dc5c5308..6bcec316dd 100644 --- a/vector/src/main/res/values/strings_riotX.xml +++ b/vector/src/main/res/values/strings_riotX.xml @@ -7,4 +7,6 @@ "No result found, use Add by matrix ID to search on server." "Start typing to get results" "Filter by username or ID…" + + "Joining room…" \ No newline at end of file From 1a9256218275d74b77eef2fa73e5028bf41da795 Mon Sep 17 00:00:00 2001 From: ganfra Date: Wed, 31 Jul 2019 14:06:10 +0200 Subject: [PATCH 5/5] Clean code after review --- .../session/room/model/create/Predecessor.kt | 28 ++++++++++++++ .../room/model/create/RoomCreateContent.kt | 38 +++++++------------ .../model/tombstone/RoomTombstoneContent.kt | 27 ++++++------- .../internal/session/room/RoomFactory.kt | 4 +- .../create/RoomCreateEventLiveObserver.kt | 4 +- .../RoomTombstoneEventLiveObserver.kt | 24 ++++++------ .../home/room/detail/RoomDetailFragment.kt | 21 +++------- .../home/room/detail/RoomDetailViewModel.kt | 22 ++++------- .../timeline/factory/RoomCreateItemFactory.kt | 28 +++++++------- .../detail/timeline/item/RoomCreateItem.kt | 24 ++++++------ 10 files changed, 104 insertions(+), 116 deletions(-) create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/create/Predecessor.kt diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/create/Predecessor.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/create/Predecessor.kt new file mode 100644 index 0000000000..960f91306f --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/create/Predecessor.kt @@ -0,0 +1,28 @@ +/* + * 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 + +/** + * A link to an old room in case of room versioning + */ +@JsonClass(generateAdapter = true) +data class Predecessor( + @Json(name = "room_id") val roomId: String? = null, + @Json(name = "event_id") val eventId: String? = null +) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/create/RoomCreateContent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/create/RoomCreateContent.kt index 3aa456e6f0..afb318bce9 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/create/RoomCreateContent.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/create/RoomCreateContent.kt @@ -1,19 +1,17 @@ /* + * Copyright 2019 New Vector Ltd * - * * 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. + * 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 @@ -26,17 +24,9 @@ import com.squareup.moshi.JsonClass */ @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? + @Json(name = "creator") val creator: String? = null, + @Json(name = "room_version") val roomVersion: String? = null, + @Json(name = "predecessor") val predecessor: Predecessor? = null ) -/** - * 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 -) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/tombstone/RoomTombstoneContent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/tombstone/RoomTombstoneContent.kt index f8f8df00db..035e76d10f 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/tombstone/RoomTombstoneContent.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/tombstone/RoomTombstoneContent.kt @@ -1,21 +1,18 @@ /* + * Copyright 2019 New Vector Ltd * - * * 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. + * 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 @@ -27,5 +24,5 @@ import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = true) data class RoomTombstoneContent( @Json(name = "body") val body: String? = null, - @Json(name = "replacement_room") val replacementRoom: String + @Json(name = "replacement_room") val replacementRoom: String? ) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomFactory.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomFactory.kt index 022133eaea..a310cc3154 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomFactory.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomFactory.kt @@ -47,8 +47,6 @@ import javax.inject.Inject internal class RoomFactory @Inject constructor(private val context: Context, private val credentials: Credentials, private val monarchy: Monarchy, - @SessionDatabase - private val realmConfiguration: RealmConfiguration, private val eventFactory: LocalEchoEventFactory, private val roomSummaryMapper: RoomSummaryMapper, private val taskExecutor: TaskExecutor, @@ -67,7 +65,7 @@ internal class RoomFactory @Inject constructor(private val context: Context, fun create(roomId: String): Room { val timelineService = DefaultTimelineService(roomId, monarchy, taskExecutor, contextOfEventTask, cryptoService, paginationTask) val sendService = DefaultSendService(context, credentials, roomId, eventFactory, cryptoService, monarchy) - val stateService = DefaultStateService(roomId, realmConfiguration, taskExecutor, sendStateTask) + val stateService = DefaultStateService(roomId, monarchy.realmConfiguration, taskExecutor, sendStateTask) val roomMembersService = DefaultMembershipService(roomId, monarchy, taskExecutor, loadRoomMembersTask, inviteTask, joinRoomTask, leaveRoomTask) val readService = DefaultReadService(roomId, monarchy, taskExecutor, setReadMarkersTask, credentials) val relationService = DefaultRelationService(context, diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/create/RoomCreateEventLiveObserver.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/create/RoomCreateEventLiveObserver.kt index c091e6f6ba..9b652fe39d 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/create/RoomCreateEventLiveObserver.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/create/RoomCreateEventLiveObserver.kt @@ -5,7 +5,7 @@ * 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 + * 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, @@ -63,7 +63,7 @@ internal class RoomCreateEventLiveObserver @Inject constructor(@SessionDatabase val predecessorRoomId = createRoomContent?.predecessor?.roomId ?: continue val predecessorRoomSummary = RoomSummaryEntity.where(realm, predecessorRoomId).findFirst() - ?: RoomSummaryEntity(predecessorRoomId) + ?: RoomSummaryEntity(predecessorRoomId) predecessorRoomSummary.versioningState = VersioningState.UPGRADED_ROOM_JOINED realm.insertOrUpdate(predecessorRoomSummary) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/tombstone/RoomTombstoneEventLiveObserver.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/tombstone/RoomTombstoneEventLiveObserver.kt index 8664fe4fa7..d71b32ef8b 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/tombstone/RoomTombstoneEventLiveObserver.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/tombstone/RoomTombstoneEventLiveObserver.kt @@ -1,19 +1,17 @@ /* + * Copyright 2019 New Vector Ltd * - * * 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. + * 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 diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt index 1742b405f3..99ae9eac67 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt @@ -45,14 +45,7 @@ import androidx.recyclerview.widget.RecyclerView import butterknife.BindView import com.airbnb.epoxy.EpoxyModel import com.airbnb.epoxy.EpoxyVisibilityTracker -import com.airbnb.mvrx.Async -import com.airbnb.mvrx.DeliveryMode -import com.airbnb.mvrx.Fail -import com.airbnb.mvrx.Loading -import com.airbnb.mvrx.Success -import com.airbnb.mvrx.Uninitialized -import com.airbnb.mvrx.args -import com.airbnb.mvrx.fragmentViewModel +import com.airbnb.mvrx.* import com.github.piasy.biv.BigImageViewer import com.github.piasy.biv.loader.ImageLoader import com.google.android.material.snackbar.Snackbar @@ -65,7 +58,6 @@ import com.otaliastudios.autocomplete.CharPolicy import im.vector.matrix.android.api.permalinks.PermalinkFactory import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.events.model.Event -import im.vector.matrix.android.api.session.room.failure.JoinRoomFailure import im.vector.matrix.android.api.session.room.model.EditAggregatedSummary import im.vector.matrix.android.api.session.room.model.Membership import im.vector.matrix.android.api.session.room.model.message.* @@ -81,8 +73,6 @@ import im.vector.riotx.core.epoxy.LayoutManagerStateRestorer import im.vector.riotx.core.error.ErrorFormatter import im.vector.riotx.core.extensions.hideKeyboard import im.vector.riotx.core.extensions.observeEvent -import im.vector.riotx.core.extensions.observeK -import im.vector.riotx.core.extensions.observeNotNull import im.vector.riotx.core.extensions.setTextOrHide import im.vector.riotx.core.files.addEntryToDownloadManager import im.vector.riotx.core.glide.GlideApp @@ -118,7 +108,6 @@ import im.vector.riotx.features.settings.VectorPreferences import im.vector.riotx.features.themes.ThemeUtils import kotlinx.android.parcel.Parcelize import kotlinx.android.synthetic.main.fragment_room_detail.* -import kotlinx.android.synthetic.main.item_loading.* import kotlinx.android.synthetic.main.merge_composer_layout.view.* import kotlinx.android.synthetic.main.merge_overlay_waiting_view.* import org.commonmark.parser.Parser @@ -265,19 +254,19 @@ class RoomDetailFragment : } override fun resendUnsentEvents() { - TODO("not implemented") + vectorBaseActivity.notImplemented() } override fun deleteUnsentEvents() { - TODO("not implemented") + vectorBaseActivity.notImplemented() } override fun closeScreen() { - TODO("not implemented") + vectorBaseActivity.notImplemented() } override fun jumpToBottom() { - TODO("not implemented") + vectorBaseActivity.notImplemented() } } } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt index 304e09f883..d4732ad7f1 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt @@ -20,11 +20,7 @@ import android.net.Uri import android.text.TextUtils import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData -import arrow.core.success -import com.airbnb.mvrx.Async -import com.airbnb.mvrx.Fail import com.airbnb.mvrx.FragmentViewModelContext -import com.airbnb.mvrx.Loading import com.airbnb.mvrx.MvRxViewModelFactory import com.airbnb.mvrx.Success import com.airbnb.mvrx.ViewModelContext @@ -56,11 +52,7 @@ import im.vector.riotx.core.utils.subscribeLogError import im.vector.riotx.features.command.CommandParser import im.vector.riotx.features.command.ParsedCommand import im.vector.riotx.features.home.room.detail.timeline.helper.TimelineDisplayableEvents -import io.reactivex.Single import io.reactivex.rxkotlin.subscribeBy -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.GlobalScope -import kotlinx.coroutines.withContext import org.commonmark.parser.Parser import org.commonmark.renderer.html.HtmlRenderer import timber.log.Timber @@ -137,9 +129,9 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro private fun handleTombstoneEvent(action: RoomDetailActions.HandleTombstoneEvent) { val tombstoneContent = action.event.getClearContent().toModel() - ?: return + ?: return - val roomId = tombstoneContent.replacementRoom + val roomId = tombstoneContent.replacementRoom ?: "" val isRoomJoined = session.getRoom(roomId)?.roomSummary()?.membership == Membership.JOIN if (isRoomJoined) { setState { copy(tombstoneEventHandling = Success(roomId)) } @@ -269,7 +261,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro //is original event a reply? val inReplyTo = state.sendMode.timelineEvent.root.getClearContent().toModel()?.relatesTo?.inReplyTo?.eventId - ?: state.sendMode.timelineEvent.root.content.toModel()?.relatesTo?.inReplyTo?.eventId + ?: state.sendMode.timelineEvent.root.content.toModel()?.relatesTo?.inReplyTo?.eventId if (inReplyTo != null) { //TODO check if same content? room.getTimeLineEvent(inReplyTo)?.let { @@ -278,12 +270,12 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro } else { val messageContent: MessageContent? = state.sendMode.timelineEvent.annotations?.editSummary?.aggregatedContent.toModel() - ?: state.sendMode.timelineEvent.root.getClearContent().toModel() + ?: state.sendMode.timelineEvent.root.getClearContent().toModel() val existingBody = messageContent?.body ?: "" if (existingBody != action.text) { room.editTextMessage(state.sendMode.timelineEvent.root.eventId - ?: "", messageContent?.type - ?: MessageType.MSGTYPE_TEXT, action.text, action.autoMarkdown) + ?: "", messageContent?.type + ?: MessageType.MSGTYPE_TEXT, action.text, action.autoMarkdown) } else { Timber.w("Same message content, do not send edition") } @@ -298,7 +290,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro is SendMode.QUOTE -> { val messageContent: MessageContent? = state.sendMode.timelineEvent.annotations?.editSummary?.aggregatedContent.toModel() - ?: state.sendMode.timelineEvent.root.getClearContent().toModel() + ?: state.sendMode.timelineEvent.root.getClearContent().toModel() val textMsg = messageContent?.body val finalText = legacyRiotQuoteText(textMsg, action.text) diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/RoomCreateItemFactory.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/RoomCreateItemFactory.kt index e32e2746cf..ed503eafbe 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/RoomCreateItemFactory.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/RoomCreateItemFactory.kt @@ -1,19 +1,17 @@ /* + * Copyright 2019 New Vector Ltd * - * * 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. + * 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 @@ -37,8 +35,8 @@ class RoomCreateItemFactory @Inject constructor(private val colorProvider: Color fun create(event: TimelineEvent, callback: TimelineEventController.Callback?): RoomCreateItem? { val createRoomContent = event.root.getClearContent().toModel() ?: return null - val predecessor = createRoomContent.predecessor ?: return null - val roomLink = PermalinkFactory.createPermalink(predecessor.roomId) ?: return null + val predecessorId = createRoomContent.predecessor?.roomId ?: return null + val roomLink = PermalinkFactory.createPermalink(predecessorId) ?: return null val text = span { +stringProvider.getString(R.string.room_tombstone_continuation_description) +"\n" diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/RoomCreateItem.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/RoomCreateItem.kt index 3e5ef30d90..ff0e6a9967 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/RoomCreateItem.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/RoomCreateItem.kt @@ -1,19 +1,17 @@ /* + * Copyright 2019 New Vector Ltd * - * * 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. + * 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