Merge pull request #449 from vector-im/feature/room_update

Feature/room upgrade
This commit is contained in:
ganfra 2019-07-31 15:34:38 +02:00 committed by GitHub
commit c300c50093
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
54 changed files with 1230 additions and 111 deletions

View File

@ -45,6 +45,10 @@ class RxRoom(private val room: Room) {
room.loadRoomMembersIfNeeded(MatrixCallbackSingle(it)).toSingle(it) room.loadRoomMembersIfNeeded(MatrixCallbackSingle(it)).toSingle(it)
} }


fun joinRoom(viaServers: List<String> = emptyList()): Single<Unit> = Single.create {
room.join(viaServers, MatrixCallbackSingle(it)).toSingle(it)
}

} }


fun Room.rx(): RxRoom { fun Room.rx(): RxRoom {

View File

@ -63,6 +63,10 @@ class RxSession(private val session: Session) {
session.searchUsersDirectory(search, limit, excludedUserIds, MatrixCallbackSingle(it)).toSingle(it) session.searchUsersDirectory(search, limit, excludedUserIds, MatrixCallbackSingle(it)).toSingle(it)
} }


fun joinRoom(roomId: String, viaServers: List<String> = emptyList()): Single<Unit> = Single.create {
session.joinRoom(roomId, viaServers, MatrixCallbackSingle(it)).toSingle(it)
}

} }


fun Session.rx(): RxSession { fun Session.rx(): RxSession {

View File

@ -123,9 +123,9 @@ object MatrixPatterns {
*/ */
fun isEventId(str: String?): Boolean { fun isEventId(str: String?): Boolean {
return str != null return str != null
&& (str matches PATTERN_CONTAIN_MATRIX_EVENT_IDENTIFIER && (str matches PATTERN_CONTAIN_MATRIX_EVENT_IDENTIFIER
|| str matches PATTERN_CONTAIN_MATRIX_EVENT_IDENTIFIER_V3 || str matches PATTERN_CONTAIN_MATRIX_EVENT_IDENTIFIER_V3
|| str matches PATTERN_CONTAIN_MATRIX_EVENT_IDENTIFIER_V4) || str matches PATTERN_CONTAIN_MATRIX_EVENT_IDENTIFIER_V4)
} }


/** /**
@ -137,4 +137,23 @@ object MatrixPatterns {
fun isGroupId(str: String?): Boolean { fun isGroupId(str: String?): Boolean {
return str != null && str matches PATTERN_CONTAIN_MATRIX_GROUP_IDENTIFIER 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)

}
} }

View File

@ -26,8 +26,13 @@ import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
data class MatrixError( data class MatrixError(
@Json(name = "errcode") val code: String, @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 { companion object {
const val FORBIDDEN = "M_FORBIDDEN" const val FORBIDDEN = "M_FORBIDDEN"
@ -55,5 +60,8 @@ data class MatrixError(
const val M_CONSENT_NOT_GIVEN = "M_CONSENT_NOT_GIVEN" const val M_CONSENT_NOT_GIVEN = "M_CONSENT_NOT_GIVEN"
const val RESOURCE_LIMIT_EXCEEDED = "M_RESOURCE_LIMIT_EXCEEDED" const val RESOURCE_LIMIT_EXCEEDED = "M_RESOURCE_LIMIT_EXCEEDED"
const val WRONG_ROOM_KEYS_VERSION = "M_WRONG_ROOM_KEYS_VERSION" const val WRONG_ROOM_KEYS_VERSION = "M_WRONG_ROOM_KEYS_VERSION"

// Possible value for "limit_type"
const val LIMIT_TYPE_MAU = "monthly_active_user"
} }
} }

View File

@ -32,6 +32,15 @@ interface RoomService {
*/ */
fun createRoom(createRoomParams: CreateRoomParams, callback: MatrixCallback<String>): Cancelable fun createRoom(createRoomParams: CreateRoomParams, callback: MatrixCallback<String>): Cancelable


/**
* 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<String> = emptyList(),
callback: MatrixCallback<Unit>): Cancelable

/** /**
* Get a room from a roomId * Get a room from a roomId
* @param roomId the roomId to look for. * @param roomId the roomId to look for.

View File

@ -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()

}

View File

@ -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()

}

View File

@ -57,7 +57,8 @@ interface MembershipService {
/** /**
* Join the room, or accept an invitation. * Join the room, or accept an invitation.
*/ */
fun join(callback: MatrixCallback<Unit>): Cancelable
fun join(viaServers: List<String> = emptyList(), callback: MatrixCallback<Unit>): Cancelable


/** /**
* Leave the room, or reject an invitation. * Leave the room, or reject an invitation.

View File

@ -34,5 +34,10 @@ data class RoomSummary(
val notificationCount: Int = 0, val notificationCount: Int = 0,
val highlightCount: Int = 0, val highlightCount: Int = 0,
val tags: List<RoomTag> = emptyList(), val tags: List<RoomTag> = emptyList(),
val membership: Membership = Membership.NONE val membership: Membership = Membership.NONE,
) val versioningState: VersioningState = VersioningState.NONE
) {

val isVersioned: Boolean
get() = versioningState != VersioningState.NONE
}

View File

@ -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
}

View File

@ -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
)

View File

@ -0,0 +1,32 @@
/*
* 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? = null,
@Json(name = "room_version") val roomVersion: String? = null,
@Json(name = "predecessor") val predecessor: Predecessor? = null
)


View File

@ -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.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?
)

View File

@ -17,6 +17,7 @@
package im.vector.matrix.android.api.session.room.state package im.vector.matrix.android.api.session.room.state


import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.session.events.model.Event


interface StateService { interface StateService {


@ -25,4 +26,6 @@ interface StateService {
*/ */
fun updateTopic(topic: String, callback: MatrixCallback<Unit>) fun updateTopic(topic: String, callback: MatrixCallback<Unit>)


fun getStateEvent(eventType: String): Event?

} }

View File

@ -16,45 +16,45 @@


package im.vector.matrix.android.internal.database package im.vector.matrix.android.internal.database


import android.os.Handler import im.vector.matrix.android.internal.util.createBackgroundHandler
import android.os.HandlerThread
import io.realm.* import io.realm.*
import timber.log.Timber
import java.util.concurrent.CountDownLatch import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicReference


private const val THREAD_NAME = "REALM_QUERY_LATCH"


class RealmQueryLatch<E : RealmObject>(private val realmConfiguration: RealmConfiguration, class RealmQueryLatch<E : RealmObject>(private val realmConfiguration: RealmConfiguration,
private val realmQueryBuilder: (Realm) -> RealmQuery<E>) { private val realmQueryBuilder: (Realm) -> RealmQuery<E>) {


@Throws(InterruptedException::class) private companion object {
fun await(timeout: Long = Long.MAX_VALUE, timeUnit: TimeUnit = TimeUnit.MILLISECONDS) { val QUERY_LATCH_HANDLER = createBackgroundHandler("REALM_QUERY_LATCH")
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()


@Throws(InterruptedException::class)
fun await(timeout: Long, timeUnit: TimeUnit) {
val realmRef = AtomicReference<Realm>()
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<RealmResults<E>> { result.addChangeListener(object : RealmChangeListener<RealmResults<E>> {
override fun onChange(t: RealmResults<E>) { override fun onChange(t: RealmResults<E>) {
if (t.isNotEmpty()) { if (t.isNotEmpty()) {
result.removeChangeListener(this) result.removeChangeListener(this)
realm.close()
latch.countDown() latch.countDown()
} }
} }
}) })
} }
handler.post(runnable)
try { try {
latch.await(timeout, timeUnit) latch.await(timeout, timeUnit)
} catch (exception: InterruptedException) { } catch (exception: InterruptedException) {
throw exception throw exception
} finally { } finally {
handlerThread.quit() QUERY_LATCH_HANDLER.post {
realmRef.getAndSet(null).close()
}
} }
} }



View File

@ -61,7 +61,8 @@ internal class RoomSummaryMapper @Inject constructor(
highlightCount = roomSummaryEntity.highlightCount, highlightCount = roomSummaryEntity.highlightCount,
notificationCount = roomSummaryEntity.notificationCount, notificationCount = roomSummaryEntity.notificationCount,
tags = tags, tags = tags,
membership = roomSummaryEntity.membership membership = roomSummaryEntity.membership,
versioningState = roomSummaryEntity.versioningState
) )
} }
} }

View File

@ -17,6 +17,7 @@
package im.vector.matrix.android.internal.database.model 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.Membership
import im.vector.matrix.android.api.session.room.model.VersioningState
import io.realm.RealmList import io.realm.RealmList
import io.realm.RealmObject import io.realm.RealmObject
import io.realm.annotations.Ignore import io.realm.annotations.Ignore
@ -40,12 +41,19 @@ internal open class RoomSummaryEntity(@PrimaryKey var roomId: String = "",
) : RealmObject() { ) : RealmObject() {


private var membershipStr: String = Membership.NONE.name private var membershipStr: String = Membership.NONE.name
private var versioningStateStr: String = VersioningState.NONE.name



@delegate:Ignore @delegate:Ignore
var membership: Membership by Delegates.observable(Membership.valueOf(membershipStr)) { _, _, newValue -> var membership: Membership by Delegates.observable(Membership.valueOf(membershipStr)) { _, _, newValue ->
membershipStr = newValue.name membershipStr = newValue.name
} }


@delegate:Ignore
var versioningState: VersioningState by Delegates.observable(VersioningState.valueOf(versioningStateStr)) { _, _, newValue ->
versioningStateStr = newValue.name
}

companion object companion object


} }

View File

@ -37,6 +37,8 @@ import im.vector.matrix.android.internal.network.AccessTokenInterceptor
import im.vector.matrix.android.internal.network.RetrofitFactory import im.vector.matrix.android.internal.network.RetrofitFactory
import im.vector.matrix.android.internal.session.group.GroupSummaryUpdater import im.vector.matrix.android.internal.session.group.GroupSummaryUpdater
import im.vector.matrix.android.internal.session.room.EventRelationsAggregationUpdater import im.vector.matrix.android.internal.session.room.EventRelationsAggregationUpdater
import im.vector.matrix.android.internal.session.room.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.session.room.prune.EventsPruner
import im.vector.matrix.android.internal.util.md5 import im.vector.matrix.android.internal.util.md5
import io.realm.RealmConfiguration import io.realm.RealmConfiguration
@ -128,6 +130,14 @@ internal abstract class SessionModule {
@IntoSet @IntoSet
abstract fun bindEventRelationsAggregationUpdater(groupSummaryUpdater: EventRelationsAggregationUpdater): LiveEntityObserver abstract fun bindEventRelationsAggregationUpdater(groupSummaryUpdater: EventRelationsAggregationUpdater): LiveEntityObserver


@Binds
@IntoSet
abstract fun bindRoomTombstoneEventLiveObserver(roomTombstoneEventLiveObserver: RoomTombstoneEventLiveObserver): LiveEntityObserver

@Binds
@IntoSet
abstract fun bindRoomCreateEventLiveObserver(roomCreateEventLiveObserver: RoomCreateEventLiveObserver): LiveEntityObserver

@Binds @Binds
abstract fun bindInitialSyncProgressService(initialSyncProgressService: DefaultInitialSyncProgressService): InitialSyncProgressService abstract fun bindInitialSyncProgressService(initialSyncProgressService: DefaultInitialSyncProgressService): InitialSyncProgressService



View File

@ -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.Room
import im.vector.matrix.android.api.session.room.RoomService 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.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.api.session.room.model.create.CreateRoomParams
import im.vector.matrix.android.api.util.Cancelable import im.vector.matrix.android.api.util.Cancelable
import im.vector.matrix.android.internal.database.mapper.RoomSummaryMapper import im.vector.matrix.android.internal.database.mapper.RoomSummaryMapper
@ -30,6 +31,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.model.RoomSummaryEntityFields
import im.vector.matrix.android.internal.database.query.where 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.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.TaskExecutor
import im.vector.matrix.android.internal.task.configureWith import im.vector.matrix.android.internal.task.configureWith
import im.vector.matrix.android.internal.util.fetchManaged import im.vector.matrix.android.internal.util.fetchManaged
@ -38,6 +40,7 @@ import javax.inject.Inject
internal class DefaultRoomService @Inject constructor(private val monarchy: Monarchy, internal class DefaultRoomService @Inject constructor(private val monarchy: Monarchy,
private val roomSummaryMapper: RoomSummaryMapper, private val roomSummaryMapper: RoomSummaryMapper,
private val createRoomTask: CreateRoomTask, private val createRoomTask: CreateRoomTask,
private val joinRoomTask: JoinRoomTask,
private val roomFactory: RoomFactory, private val roomFactory: RoomFactory,
private val taskExecutor: TaskExecutor) : RoomService { private val taskExecutor: TaskExecutor) : RoomService {


@ -55,8 +58,19 @@ internal class DefaultRoomService @Inject constructor(private val monarchy: Mona


override fun liveRoomSummaries(): LiveData<List<RoomSummary>> { override fun liveRoomSummaries(): LiveData<List<RoomSummary>> {
return monarchy.findAllMappedWithChanges( return monarchy.findAllMappedWithChanges(
{ realm -> RoomSummaryEntity.where(realm).isNotEmpty(RoomSummaryEntityFields.DISPLAY_NAME) }, { realm ->
RoomSummaryEntity.where(realm)
.isNotEmpty(RoomSummaryEntityFields.DISPLAY_NAME)
.notEqualTo(RoomSummaryEntityFields.VERSIONING_STATE_STR, VersioningState.UPGRADED_ROOM_JOINED.name)
},
{ roomSummaryMapper.map(it) } { roomSummaryMapper.map(it) }
) )
} }

override fun joinRoom(roomId: String, viaServers: List<String>, callback: MatrixCallback<Unit>): Cancelable {
return joinRoomTask
.configureWith(JoinRoomTask.Params(roomId, viaServers))
.dispatchTo(callback)
.executeBy(taskExecutor)
}
} }

View File

@ -218,6 +218,7 @@ internal interface RoomAPI {
*/ */
@POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/join") @POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/join")
fun join(@Path("roomId") roomId: String, fun join(@Path("roomId") roomId: String,
@Query("server_name") viaServers: List<String>,
@Body params: Map<String, String>): Call<Unit> @Body params: Map<String, String>): Call<Unit>


/** /**

View File

@ -22,6 +22,7 @@ import im.vector.matrix.android.api.auth.data.Credentials
import im.vector.matrix.android.api.session.crypto.CryptoService import im.vector.matrix.android.api.session.crypto.CryptoService
import im.vector.matrix.android.api.session.room.Room import im.vector.matrix.android.api.session.room.Room
import im.vector.matrix.android.internal.database.mapper.RoomSummaryMapper import im.vector.matrix.android.internal.database.mapper.RoomSummaryMapper
import im.vector.matrix.android.internal.di.SessionDatabase
import im.vector.matrix.android.internal.session.room.membership.DefaultMembershipService import im.vector.matrix.android.internal.session.room.membership.DefaultMembershipService
import im.vector.matrix.android.internal.session.room.membership.LoadRoomMembersTask import im.vector.matrix.android.internal.session.room.membership.LoadRoomMembersTask
import im.vector.matrix.android.internal.session.room.membership.joining.InviteTask import im.vector.matrix.android.internal.session.room.membership.joining.InviteTask
@ -40,6 +41,7 @@ import im.vector.matrix.android.internal.session.room.timeline.DefaultTimelineSe
import im.vector.matrix.android.internal.session.room.timeline.GetContextOfEventTask import im.vector.matrix.android.internal.session.room.timeline.GetContextOfEventTask
import im.vector.matrix.android.internal.session.room.timeline.PaginationTask import im.vector.matrix.android.internal.session.room.timeline.PaginationTask
import im.vector.matrix.android.internal.task.TaskExecutor import im.vector.matrix.android.internal.task.TaskExecutor
import io.realm.RealmConfiguration
import javax.inject.Inject import javax.inject.Inject


internal class RoomFactory @Inject constructor(private val context: Context, internal class RoomFactory @Inject constructor(private val context: Context,
@ -63,7 +65,7 @@ internal class RoomFactory @Inject constructor(private val context: Context,
fun create(roomId: String): Room { fun create(roomId: String): Room {
val timelineService = DefaultTimelineService(roomId, monarchy, taskExecutor, contextOfEventTask, cryptoService, paginationTask) val timelineService = DefaultTimelineService(roomId, monarchy, taskExecutor, contextOfEventTask, cryptoService, paginationTask)
val sendService = DefaultSendService(context, credentials, roomId, eventFactory, cryptoService, monarchy) val sendService = DefaultSendService(context, credentials, roomId, eventFactory, cryptoService, monarchy)
val stateService = DefaultStateService(roomId, taskExecutor, sendStateTask) val stateService = DefaultStateService(roomId, monarchy.realmConfiguration, taskExecutor, sendStateTask)
val roomMembersService = DefaultMembershipService(roomId, monarchy, taskExecutor, loadRoomMembersTask, inviteTask, joinRoomTask, leaveRoomTask) val roomMembersService = DefaultMembershipService(roomId, monarchy, taskExecutor, loadRoomMembersTask, inviteTask, joinRoomTask, leaveRoomTask)
val readService = DefaultReadService(roomId, monarchy, taskExecutor, setReadMarkersTask, credentials) val readService = DefaultReadService(roomId, monarchy, taskExecutor, setReadMarkersTask, credentials)
val relationService = DefaultRelationService(context, val relationService = DefaultRelationService(context,

View File

@ -17,7 +17,9 @@
package im.vector.matrix.android.internal.session.room.create package im.vector.matrix.android.internal.session.room.create


import arrow.core.Try import arrow.core.Try
import arrow.core.recoverWith
import com.zhuinden.monarchy.Monarchy 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.CreateRoomParams
import im.vector.matrix.android.api.session.room.model.create.CreateRoomResponse import im.vector.matrix.android.api.session.room.model.create.CreateRoomResponse
import im.vector.matrix.android.internal.database.RealmQueryLatch 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) realm.where(RoomEntity::class.java)
.equalTo(RoomEntityFields.ROOM_ID, roomId) .equalTo(RoomEntityFields.ROOM_ID, roomId)
} }
Try { try {
rql.await(timeout = 20L, timeUnit = TimeUnit.SECONDS) rql.await(timeout = 1L, timeUnit = TimeUnit.MINUTES)
roomId Try.just(roomId)
} catch (exception: Exception) {
Try.raise<String>(CreateRoomFailure.CreatedWithTimeout)
} }
}.flatMap { roomId -> }.flatMap { roomId ->
if (params.isDirect()) { if (params.isDirect()) {

View File

@ -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<EventEntity>(realmConfiguration) {

override val query = Monarchy.Query<EventEntity> {
EventEntity.types(it, listOf(EventType.STATE_ROOM_CREATE))
}

override fun onChange(results: RealmResults<EventEntity>, changeSet: OrderedCollectionChangeSet) {
changeSet.insertions
.asSequence()
.mapNotNull {
results[it]?.asDomain()
}
.toList()
.also {
handleRoomCreateEvents(it)
}
}

private fun handleRoomCreateEvents(createEvents: List<Event>) = Realm.getInstance(realmConfiguration).use {
it.executeTransactionAsync { realm ->
for (event in createEvents) {
val createRoomContent = event.getClearContent().toModel<RoomCreateContent>()
val predecessorRoomId = createRoomContent?.predecessor?.roomId ?: continue

val predecessorRoomSummary = RoomSummaryEntity.where(realm, predecessorRoomId).findFirst()
?: RoomSummaryEntity(predecessorRoomId)
predecessorRoomSummary.versioningState = VersioningState.UPGRADED_ROOM_JOINED
realm.insertOrUpdate(predecessorRoomSummary)

}
}
}

}

View File

@ -82,8 +82,8 @@ internal class DefaultMembershipService @Inject constructor(private val roomId:
.executeBy(taskExecutor) .executeBy(taskExecutor)
} }


override fun join(callback: MatrixCallback<Unit>): Cancelable { override fun join(viaServers: List<String>, callback: MatrixCallback<Unit>): Cancelable {
val params = JoinRoomTask.Params(roomId) val params = JoinRoomTask.Params(roomId, viaServers)
return joinTask.configureWith(params) return joinTask.configureWith(params)
.dispatchTo(callback) .dispatchTo(callback)
.executeBy(taskExecutor) .executeBy(taskExecutor)

View File

@ -17,6 +17,8 @@
package im.vector.matrix.android.internal.session.room.membership.joining package im.vector.matrix.android.internal.session.room.membership.joining


import arrow.core.Try 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.RealmQueryLatch
import im.vector.matrix.android.internal.database.model.RoomEntity import im.vector.matrix.android.internal.database.model.RoomEntity
import im.vector.matrix.android.internal.database.model.RoomEntityFields import im.vector.matrix.android.internal.database.model.RoomEntityFields
@ -31,7 +33,8 @@ import javax.inject.Inject


internal interface JoinRoomTask : Task<JoinRoomTask.Params, Unit> { internal interface JoinRoomTask : Task<JoinRoomTask.Params, Unit> {
data class Params( data class Params(
val roomId: String val roomId: String,
val viaServers: List<String> = emptyList()
) )
} }


@ -41,7 +44,7 @@ internal class DefaultJoinRoomTask @Inject constructor(private val roomAPI: Room


override suspend fun execute(params: JoinRoomTask.Params): Try<Unit> { override suspend fun execute(params: JoinRoomTask.Params): Try<Unit> {
return executeRequest<Unit> { return executeRequest<Unit> {
apiCall = roomAPI.join(params.roomId, HashMap()) apiCall = roomAPI.join(params.roomId, params.viaServers, HashMap())
}.flatMap { }.flatMap {
val roomId = params.roomId val roomId = params.roomId
// Wait for room to come back from the sync (but it can maybe be in the DB is the sync response is received before) // Wait for room to come back from the sync (but it can maybe be in the DB is the sync response is received before)
@ -49,9 +52,11 @@ internal class DefaultJoinRoomTask @Inject constructor(private val roomAPI: Room
realm.where(RoomEntity::class.java) realm.where(RoomEntity::class.java)
.equalTo(RoomEntityFields.ROOM_ID, roomId) .equalTo(RoomEntityFields.ROOM_ID, roomId)
} }
Try { try {
rql.await(20L, TimeUnit.SECONDS) rql.await(timeout = 1L, timeUnit = TimeUnit.MINUTES)
roomId Try.just(roomId)
} catch (exception: Exception) {
Try.raise<String>(JoinRoomFailure.JoinedWithTimeout)
} }
}.flatMap { roomId -> }.flatMap { roomId ->
setReadMarkers(roomId) setReadMarkers(roomId)

View File

@ -17,16 +17,32 @@
package im.vector.matrix.android.internal.session.room.state package im.vector.matrix.android.internal.session.room.state


import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.api.session.events.model.EventType import im.vector.matrix.android.api.session.events.model.EventType
import im.vector.matrix.android.api.session.room.state.StateService import im.vector.matrix.android.api.session.room.state.StateService
import im.vector.matrix.android.internal.database.mapper.asDomain
import im.vector.matrix.android.internal.database.model.EventEntity
import im.vector.matrix.android.internal.database.query.prev
import im.vector.matrix.android.internal.database.query.where
import im.vector.matrix.android.internal.di.SessionDatabase
import im.vector.matrix.android.internal.task.TaskExecutor import im.vector.matrix.android.internal.task.TaskExecutor
import im.vector.matrix.android.internal.task.configureWith import im.vector.matrix.android.internal.task.configureWith
import io.realm.Realm
import io.realm.RealmConfiguration
import javax.inject.Inject import javax.inject.Inject


internal class DefaultStateService @Inject constructor(private val roomId: String, internal class DefaultStateService @Inject constructor(private val roomId: String,
@SessionDatabase
private val realmConfiguration: RealmConfiguration,
private val taskExecutor: TaskExecutor, private val taskExecutor: TaskExecutor,
private val sendStateTask: SendStateTask) : StateService { private val sendStateTask: SendStateTask) : StateService {


override fun getStateEvent(eventType: String): Event? {
return Realm.getInstance(realmConfiguration).use { realm ->
EventEntity.where(realm, roomId, eventType).prev()?.asDomain()
}
}

override fun updateTopic(topic: String, callback: MatrixCallback<Unit>) { override fun updateTopic(topic: String, callback: MatrixCallback<Unit>) {
val params = SendStateTask.Params(roomId, val params = SendStateTask.Params(roomId,
EventType.STATE_ROOM_TOPIC, EventType.STATE_ROOM_TOPIC,
@ -39,4 +55,6 @@ internal class DefaultStateService @Inject constructor(private val roomId: Strin
.dispatchTo(callback) .dispatchTo(callback)
.executeBy(taskExecutor) .executeBy(taskExecutor)
} }


} }

View File

@ -0,0 +1,76 @@
/*
* 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.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
import im.vector.matrix.android.internal.database.model.EventEntity
import im.vector.matrix.android.internal.database.model.RoomSummaryEntity
import im.vector.matrix.android.internal.database.query.types
import im.vector.matrix.android.internal.database.query.where
import im.vector.matrix.android.internal.di.SessionDatabase
import io.realm.OrderedCollectionChangeSet
import io.realm.Realm
import io.realm.RealmConfiguration
import io.realm.RealmResults
import javax.inject.Inject

internal class RoomTombstoneEventLiveObserver @Inject constructor(@SessionDatabase
realmConfiguration: RealmConfiguration)
: RealmLiveEntityObserver<EventEntity>(realmConfiguration) {

override val query = Monarchy.Query<EventEntity> {
EventEntity.types(it, listOf(EventType.STATE_ROOM_TOMBSTONE))
}

override fun onChange(results: RealmResults<EventEntity>, changeSet: OrderedCollectionChangeSet) {
changeSet.insertions
.asSequence()
.mapNotNull {
results[it]?.asDomain()
}
.toList()
.also {
handleRoomTombstoneEvents(it)
}
}

private fun handleRoomTombstoneEvents(tombstoneEvents: List<Event>) = Realm.getInstance(realmConfiguration).use {
it.executeTransactionAsync { realm ->
for (event in tombstoneEvents) {
if (event.roomId == null) continue
val createRoomContent = event.getClearContent().toModel<RoomTombstoneContent>()
if (createRoomContent?.replacementRoom == null) continue

val predecessorRoomSummary = RoomSummaryEntity.where(realm, event.roomId).findFirst()
?: RoomSummaryEntity(event.roomId)
if (predecessorRoomSummary.versioningState == VersioningState.NONE) {
predecessorRoomSummary.versioningState = VersioningState.UPGRADED_ROOM_NOT_JOINED
}
realm.insertOrUpdate(predecessorRoomSummary)

}
}
}

}

View File

@ -29,6 +29,7 @@ import im.vector.matrix.android.internal.crypto.CryptoManager
import im.vector.matrix.android.internal.database.helper.add import im.vector.matrix.android.internal.database.helper.add
import im.vector.matrix.android.internal.database.helper.addOrUpdate import im.vector.matrix.android.internal.database.helper.addOrUpdate
import im.vector.matrix.android.internal.database.helper.addStateEvent import im.vector.matrix.android.internal.database.helper.addStateEvent
import im.vector.matrix.android.internal.database.helper.lastStateIndex
import im.vector.matrix.android.internal.database.helper.updateSenderDataFor import im.vector.matrix.android.internal.database.helper.updateSenderDataFor
import im.vector.matrix.android.internal.database.mapper.asDomain import im.vector.matrix.android.internal.database.mapper.asDomain
import im.vector.matrix.android.internal.database.model.ChunkEntity import im.vector.matrix.android.internal.database.model.ChunkEntity
@ -157,8 +158,7 @@ internal class RoomSyncHandler @Inject constructor(private val monarchy: Monarch
roomEntity, roomEntity,
roomSync.timeline.events, roomSync.timeline.events,
roomSync.timeline.prevToken, roomSync.timeline.prevToken,
roomSync.timeline.limited, roomSync.timeline.limited
0
) )
roomEntity.addOrUpdate(chunkEntity) roomEntity.addOrUpdate(chunkEntity)
} }
@ -206,15 +206,18 @@ internal class RoomSyncHandler @Inject constructor(private val monarchy: Monarch
roomEntity: RoomEntity, roomEntity: RoomEntity,
eventList: List<Event>, eventList: List<Event>,
prevToken: String? = null, prevToken: String? = null,
isLimited: Boolean = true, isLimited: Boolean = true): ChunkEntity {
stateIndexOffset: Int = 0): ChunkEntity {


val lastChunk = ChunkEntity.findLastLiveChunkFromRoom(realm, roomEntity.roomId) val lastChunk = ChunkEntity.findLastLiveChunkFromRoom(realm, roomEntity.roomId)
var stateIndexOffset = 0
val chunkEntity = if (!isLimited && lastChunk != null) { val chunkEntity = if (!isLimited && lastChunk != null) {
lastChunk lastChunk
} else { } else {
realm.createObject<ChunkEntity>().apply { this.prevToken = prevToken } realm.createObject<ChunkEntity>().apply { this.prevToken = prevToken }
} }
if (isLimited && lastChunk != null) {
stateIndexOffset = lastChunk.lastStateIndex(PaginationDirection.FORWARDS)
}
lastChunk?.isLastForward = false lastChunk?.isLastForward = false
chunkEntity.isLastForward = true chunkEntity.isLastForward = true



View File

@ -31,6 +31,7 @@
<string name="notice_room_visibility_world_readable">anyone.</string> <string name="notice_room_visibility_world_readable">anyone.</string>
<string name="notice_room_visibility_unknown">unknown (%s).</string> <string name="notice_room_visibility_unknown">unknown (%s).</string>
<string name="notice_end_to_end">%1$s turned on end-to-end encryption (%2$s)</string> <string name="notice_end_to_end">%1$s turned on end-to-end encryption (%2$s)</string>
<string name="notice_room_update">%s upgraded this room.</string>


<string name="notice_requested_voip_conference">%1$s requested a VoIP conference</string> <string name="notice_requested_voip_conference">%1$s requested a VoIP conference</string>
<string name="notice_voip_started">VoIP conference started</string> <string name="notice_voip_started">VoIP conference started</string>

View File

@ -0,0 +1,33 @@
<?xml version="1.0" encoding="utf-8"?>

<merge xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="42dp"
android:paddingTop="8dp"
android:paddingBottom="8dp"
tools:background="@color/vector_fuchsia_color"
tools:parentTag="android.widget.RelativeLayout">

<ImageView
android:id="@+id/room_notification_icon"
android:layout_width="32dp"
android:layout_height="32dp"
android:layout_centerVertical="true"
android:layout_marginStart="24dp"
android:padding="5dp"
tools:src="@drawable/vector_typing" />

<TextView
android:id="@+id/room_notification_message"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:layout_marginStart="64dp"
android:layout_marginEnd="16dp"
android:accessibilityLiveRegion="polite"
android:textColor="?attr/vctr_room_notification_text_color"
tools:text="a text here" />

</merge>

View File

@ -30,12 +30,16 @@ class ErrorFormatter @Inject constructor(val stringProvider: StringProvider) {
} }


fun toHumanReadable(throwable: Throwable?): String { fun toHumanReadable(throwable: Throwable?): String {

return when (throwable) { return when (throwable) {
null -> "" null -> ""
is Failure.NetworkConnection -> stringProvider.getString(R.string.error_no_network) 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 else -> throwable.localizedMessage
?: stringProvider.getString(R.string.unknown_error)
} }

} }
} }

View File

@ -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 "<a href=\"$uri\">$contactStr</a>"
}
}

View File

@ -0,0 +1,318 @@
/*
* 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 butterknife.BindView
import butterknife.ButterKnife
import im.vector.matrix.android.api.failure.MatrixError
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.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) {
visibility = View.VISIBLE
imageView.setImageResource(R.drawable.error)
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?.onTombstoneEventClicked(state.tombstoneEvent) }
}
}
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 tombstoneEvent: Event) : 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 onTombstoneEventClicked(tombstoneEvent: Event)
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"
}
}

View File

@ -144,7 +144,6 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable {


override fun onNewIntent(intent: Intent?) { override fun onNewIntent(intent: Intent?) {
super.onNewIntent(intent) super.onNewIntent(intent)

if (intent?.hasExtra(EXTRA_CLEAR_EXISTING_NOTIFICATION) == true) { if (intent?.hasExtra(EXTRA_CLEAR_EXISTING_NOTIFICATION) == true) {
notificationDrawerManager.clearAllEvents() notificationDrawerManager.clearAllEvents()
intent.removeExtra(EXTRA_CLEAR_EXISTING_NOTIFICATION) intent.removeExtra(EXTRA_CLEAR_EXISTING_NOTIFICATION)
@ -193,7 +192,7 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable {
bugReporter.openBugReportScreen(this, false) bugReporter.openBugReportScreen(this, false)
return true return true
} }
R.id.menu_home_filter -> { R.id.menu_home_filter -> {
navigator.openRoomsFiltering(this) navigator.openRoomsFiltering(this)
return true return true
} }

View File

@ -29,6 +29,7 @@ import com.airbnb.mvrx.Fail
import com.airbnb.mvrx.Loading import com.airbnb.mvrx.Loading
import com.airbnb.mvrx.Success import com.airbnb.mvrx.Success
import com.airbnb.mvrx.viewModel import com.airbnb.mvrx.viewModel
import im.vector.matrix.android.api.session.room.failure.CreateRoomFailure
import im.vector.riotx.R import im.vector.riotx.R
import im.vector.riotx.core.di.ScreenComponent import im.vector.riotx.core.di.ScreenComponent
import im.vector.riotx.core.error.ErrorFormatter 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.SimpleFragmentActivity
import im.vector.riotx.core.platform.WaitingViewData import im.vector.riotx.core.platform.WaitingViewData
import kotlinx.android.synthetic.main.activity.* import kotlinx.android.synthetic.main.activity.*
import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject


class CreateDirectRoomActivity : SimpleFragmentActivity() { class CreateDirectRoomActivity : SimpleFragmentActivity() {
@ -91,10 +93,13 @@ class CreateDirectRoomActivity : SimpleFragmentActivity() {


private fun renderCreationFailure(error: Throwable) { private fun renderCreationFailure(error: Throwable) {
hideWaitingView() hideWaitingView()
AlertDialog.Builder(this) if (error is CreateRoomFailure.CreatedWithTimeout) {
.setMessage(errorFormatter.toHumanReadable(error)) finish()
.setPositiveButton(R.string.ok) { dialog, id -> dialog.cancel() } } else
.show() AlertDialog.Builder(this)
.setMessage(errorFormatter.toHumanReadable(error))
.setPositiveButton(R.string.ok) { dialog, id -> dialog.cancel() }
.show()
} }


private fun renderCreationSuccess(roomId: String?) { private fun renderCreationSuccess(roomId: String?) {

View File

@ -17,6 +17,7 @@
package im.vector.riotx.features.home.room.detail package im.vector.riotx.features.home.room.detail


import com.jaiselrahman.filepicker.model.MediaFile 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.EditAggregatedSummary
import im.vector.matrix.android.api.session.room.model.message.MessageFileContent import im.vector.matrix.android.api.session.room.model.message.MessageFileContent
import im.vector.matrix.android.api.session.room.timeline.Timeline 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 SendMessage(val text: String, val autoMarkdown: Boolean) : RoomDetailActions()
data class SendMedia(val mediaFiles: List<MediaFile>) : RoomDetailActions() data class SendMedia(val mediaFiles: List<MediaFile>) : RoomDetailActions()
data class EventDisplayed(val event: TimelineEvent) : 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 SendReaction(val reaction: String, val targetEventId: String) : RoomDetailActions()
data class RedactAction(val targetEventId: String, val reason: 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 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 UpdateQuickReactAction(val targetEventId: String, val selectedReaction: String, val add: Boolean) : RoomDetailActions()
data class NavigateToEvent(val eventId: String, val position: Int?) : RoomDetailActions() data class NavigateToEvent(val eventId: String, val position: Int?) : RoomDetailActions()
data class DownloadFile(val eventId: String, val messageFileContent: MessageFileContent) : RoomDetailActions() data class DownloadFile(val eventId: String, val messageFileContent: MessageFileContent) : RoomDetailActions()
data class HandleTombstoneEvent(val event: Event): RoomDetailActions()
object AcceptInvite : RoomDetailActions() object AcceptInvite : RoomDetailActions()
object RejectInvite : RoomDetailActions() object RejectInvite : RoomDetailActions()



View File

@ -29,6 +29,7 @@ import im.vector.riotx.R
import im.vector.riotx.core.extensions.replaceFragment import im.vector.riotx.core.extensions.replaceFragment
import im.vector.riotx.core.platform.ToolbarConfigurable import im.vector.riotx.core.platform.ToolbarConfigurable
import im.vector.riotx.core.platform.VectorBaseActivity import im.vector.riotx.core.platform.VectorBaseActivity
import kotlinx.android.synthetic.main.merge_overlay_waiting_view.*


class RoomDetailActivity : VectorBaseActivity(), ToolbarConfigurable { class RoomDetailActivity : VectorBaseActivity(), ToolbarConfigurable {


@ -38,6 +39,7 @@ class RoomDetailActivity : VectorBaseActivity(), ToolbarConfigurable {


override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
waitingView = waiting_view
if (isFirstCreation()) { if (isFirstCreation()) {
val roomDetailArgs: RoomDetailArgs = intent?.extras?.getParcelable(EXTRA_ROOM_DETAIL_ARGS) val roomDetailArgs: RoomDetailArgs = intent?.extras?.getParcelable(EXTRA_ROOM_DETAIL_ARGS)
?: return ?: return

View File

@ -44,8 +44,7 @@ import androidx.recyclerview.widget.RecyclerView
import butterknife.BindView import butterknife.BindView
import com.airbnb.epoxy.EpoxyModel import com.airbnb.epoxy.EpoxyModel
import com.airbnb.epoxy.EpoxyVisibilityTracker import com.airbnb.epoxy.EpoxyVisibilityTracker
import com.airbnb.mvrx.args import com.airbnb.mvrx.*
import com.airbnb.mvrx.fragmentViewModel
import com.github.piasy.biv.BigImageViewer import com.github.piasy.biv.BigImageViewer
import com.github.piasy.biv.loader.ImageLoader import com.github.piasy.biv.loader.ImageLoader
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
@ -57,6 +56,7 @@ import com.otaliastudios.autocomplete.AutocompleteCallback
import com.otaliastudios.autocomplete.CharPolicy import com.otaliastudios.autocomplete.CharPolicy
import im.vector.matrix.android.api.permalinks.PermalinkFactory import im.vector.matrix.android.api.permalinks.PermalinkFactory
import im.vector.matrix.android.api.session.Session 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.EditAggregatedSummary
import im.vector.matrix.android.api.session.room.model.Membership import im.vector.matrix.android.api.session.room.model.Membership
import im.vector.matrix.android.api.session.room.model.message.* import im.vector.matrix.android.api.session.room.model.message.*
@ -75,6 +75,7 @@ import im.vector.riotx.core.extensions.observeEvent
import im.vector.riotx.core.extensions.setTextOrHide import im.vector.riotx.core.extensions.setTextOrHide
import im.vector.riotx.core.files.addEntryToDownloadManager import im.vector.riotx.core.files.addEntryToDownloadManager
import im.vector.riotx.core.glide.GlideApp 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.platform.VectorBaseFragment
import im.vector.riotx.core.utils.* import im.vector.riotx.core.utils.*
import im.vector.riotx.features.autocomplete.command.AutocompleteCommandPresenter import im.vector.riotx.features.autocomplete.command.AutocompleteCommandPresenter
@ -107,6 +108,7 @@ import im.vector.riotx.features.themes.ThemeUtils
import kotlinx.android.parcel.Parcelize import kotlinx.android.parcel.Parcelize
import kotlinx.android.synthetic.main.fragment_room_detail.* import kotlinx.android.synthetic.main.fragment_room_detail.*
import kotlinx.android.synthetic.main.merge_composer_layout.view.* import kotlinx.android.synthetic.main.merge_composer_layout.view.*
import kotlinx.android.synthetic.main.merge_overlay_waiting_view.*
import org.commonmark.parser.Parser import org.commonmark.parser.Parser
import timber.log.Timber import timber.log.Timber
import java.io.File import java.io.File
@ -204,6 +206,7 @@ class RoomDetailFragment :
setupComposer() setupComposer()
setupAttachmentButton() setupAttachmentButton()
setupInviteView() setupInviteView()
setupNotificationView()
roomDetailViewModel.subscribe { renderState(it) } roomDetailViewModel.subscribe { renderState(it) }
textComposerViewModel.subscribe { renderTextComposerState(it) } textComposerViewModel.subscribe { renderTextComposerState(it) }
roomDetailViewModel.sendMessageResultLiveData.observeEvent(this) { renderSendMessageResult(it) } roomDetailViewModel.sendMessageResultLiveData.observeEvent(this) { renderSendMessageResult(it) }
@ -221,6 +224,10 @@ class RoomDetailFragment :
scrollOnHighlightedEventCallback.scheduleScrollTo(it) scrollOnHighlightedEventCallback.scheduleScrollTo(it)
} }


roomDetailViewModel.selectSubscribe(this, RoomDetailViewState::tombstoneEventHandling, uniqueOnly("tombstoneEventHandling")) {
renderTombstoneEventHandling(it)
}

roomDetailViewModel.downloadedFileEvent.observeEvent(this) { downloadFileState -> roomDetailViewModel.downloadedFileEvent.observeEvent(this) { downloadFileState ->
if (downloadFileState.throwable != null) { if (downloadFileState.throwable != null) {
requireActivity().toast(errorFormatter.toHumanReadable(downloadFileState.throwable)) requireActivity().toast(errorFormatter.toHumanReadable(downloadFileState.throwable))
@ -240,6 +247,31 @@ class RoomDetailFragment :
} }
} }


private fun setupNotificationView() {
notificationAreaView.delegate = object : NotificationAreaView.Delegate {

override fun onTombstoneEventClicked(tombstoneEvent: Event) {
roomDetailViewModel.process(RoomDetailActions.HandleTombstoneEvent(tombstoneEvent))
}

override fun resendUnsentEvents() {
vectorBaseActivity.notImplemented()
}

override fun deleteUnsentEvents() {
vectorBaseActivity.notImplemented()
}

override fun closeScreen() {
vectorBaseActivity.notImplemented()
}

override fun jumpToBottom() {
vectorBaseActivity.notImplemented()
}
}
}

override fun onPrepareOptionsMenu(menu: Menu) { override fun onPrepareOptionsMenu(menu: Menu) {
menu.forEach { menu.forEach {
it.isVisible = roomDetailViewModel.isMenuItemVisible(it.itemId) it.isVisible = roomDetailViewModel.isMenuItemVisible(it.itemId)
@ -266,7 +298,8 @@ class RoomDetailFragment :
composerLayout.collapse() composerLayout.collapse()
} }


private fun enterSpecialMode(event: TimelineEvent, @DrawableRes iconRes: Int, useText: Boolean) { private fun enterSpecialMode(event: TimelineEvent, @DrawableRes
iconRes: Int, useText: Boolean) {
commandAutocompletePolicy.enabled = false commandAutocompletePolicy.enabled = false
//switch to expanded bar //switch to expanded bar
composerLayout.composerRelatedMessageTitle.apply { composerLayout.composerRelatedMessageTitle.apply {
@ -280,17 +313,17 @@ class RoomDetailFragment :
if (messageContent is MessageTextContent && messageContent.format == MessageType.FORMAT_MATRIX_HTML) { if (messageContent is MessageTextContent && messageContent.format == MessageType.FORMAT_MATRIX_HTML) {
val parser = Parser.builder().build() val parser = Parser.builder().build()
val document = parser.parse(messageContent.formattedBody val document = parser.parse(messageContent.formattedBody
?: messageContent.body) ?: messageContent.body)
formattedBody = eventHtmlRenderer.render(document) formattedBody = eventHtmlRenderer.render(document)
} }
composerLayout.composerRelatedMessageContent.text = formattedBody composerLayout.composerRelatedMessageContent.text = formattedBody
?: nonFormattedBody ?: nonFormattedBody


composerLayout.composerEditText.setText(if (useText) event.getTextEditableContent() else "") composerLayout.composerEditText.setText(if (useText) event.getTextEditableContent() else "")
composerLayout.composerRelatedMessageActionIcon.setImageDrawable(ContextCompat.getDrawable(requireContext(), iconRes)) composerLayout.composerRelatedMessageActionIcon.setImageDrawable(ContextCompat.getDrawable(requireContext(), iconRes))


avatarRenderer.render(event.senderAvatar, event.root.senderId avatarRenderer.render(event.senderAvatar, event.root.senderId
?: "", event.senderName, composerLayout.composerRelatedMessageAvatar) ?: "", event.senderName, composerLayout.composerRelatedMessageAvatar)


composerLayout.composerEditText.setSelection(composerLayout.composerEditText.text.length) composerLayout.composerEditText.setSelection(composerLayout.composerEditText.text.length)
composerLayout.expand { composerLayout.expand {
@ -319,9 +352,9 @@ class RoomDetailFragment :
REQUEST_FILES_REQUEST_CODE, TAKE_IMAGE_REQUEST_CODE -> handleMediaIntent(data) REQUEST_FILES_REQUEST_CODE, TAKE_IMAGE_REQUEST_CODE -> handleMediaIntent(data)
REACTION_SELECT_REQUEST_CODE -> { REACTION_SELECT_REQUEST_CODE -> {
val eventId = data.getStringExtra(EmojiReactionPickerActivity.EXTRA_EVENT_ID) val eventId = data.getStringExtra(EmojiReactionPickerActivity.EXTRA_EVENT_ID)
?: return ?: return
val reaction = data.getStringExtra(EmojiReactionPickerActivity.EXTRA_REACTION_RESULT) val reaction = data.getStringExtra(EmojiReactionPickerActivity.EXTRA_REACTION_RESULT)
?: return ?: return
//TODO check if already reacted with that? //TODO check if already reacted with that?
roomDetailViewModel.process(RoomDetailActions.SendReaction(reaction, eventId)) roomDetailViewModel.process(RoomDetailActions.SendReaction(reaction, eventId))
} }
@ -349,33 +382,33 @@ class RoomDetailFragment :


recyclerView.addOnScrollListener( recyclerView.addOnScrollListener(
EndlessRecyclerViewScrollListener(layoutManager, RoomDetailViewModel.PAGINATION_COUNT) { direction -> EndlessRecyclerViewScrollListener(layoutManager, RoomDetailViewModel.PAGINATION_COUNT) { direction ->
roomDetailViewModel.process(RoomDetailActions.LoadMore(direction)) roomDetailViewModel.process(RoomDetailActions.LoadMoreTimelineEvents(direction))
}) })
recyclerView.setController(timelineEventController) recyclerView.setController(timelineEventController)
timelineEventController.callback = this timelineEventController.callback = this


if (VectorPreferences.swipeToReplyIsEnabled(requireContext())) { if (VectorPreferences.swipeToReplyIsEnabled(requireContext())) {
val swipeCallback = RoomMessageTouchHelperCallback(requireContext(), val swipeCallback = RoomMessageTouchHelperCallback(requireContext(),
R.drawable.ic_reply, R.drawable.ic_reply,
object : RoomMessageTouchHelperCallback.QuickReplayHandler { object : RoomMessageTouchHelperCallback.QuickReplayHandler {
override fun performQuickReplyOnHolder(model: EpoxyModel<*>) { override fun performQuickReplyOnHolder(model: EpoxyModel<*>) {
(model as? AbsMessageItem)?.informationData?.let { (model as? AbsMessageItem)?.informationData?.let {
val eventId = it.eventId val eventId = it.eventId
roomDetailViewModel.process(RoomDetailActions.EnterReplyMode(eventId)) roomDetailViewModel.process(RoomDetailActions.EnterReplyMode(eventId))
} }
} }


override fun canSwipeModel(model: EpoxyModel<*>): Boolean { override fun canSwipeModel(model: EpoxyModel<*>): Boolean {
return when (model) { return when (model) {
is MessageFileItem, is MessageFileItem,
is MessageImageVideoItem, is MessageImageVideoItem,
is MessageTextItem -> { is MessageTextItem -> {
return (model as AbsMessageItem).informationData.sendState == SendState.SYNCED return (model as AbsMessageItem).informationData.sendState == SendState.SYNCED
} }
else -> false else -> false
} }
} }
}) })
val touchHelper = ItemTouchHelper(swipeCallback) val touchHelper = ItemTouchHelper(swipeCallback)
touchHelper.attachToRecyclerView(recyclerView) touchHelper.attachToRecyclerView(recyclerView)
} }
@ -541,7 +574,6 @@ class RoomDetailFragment :
if (summary?.membership == Membership.JOIN) { if (summary?.membership == Membership.JOIN) {
timelineEventController.setTimeline(state.timeline, state.eventId) timelineEventController.setTimeline(state.timeline, state.eventId)
inviteView.visibility = View.GONE inviteView.visibility = View.GONE

val uid = session.myUserId val uid = session.myUserId
val meMember = session.getRoom(state.roomId)?.getRoomMember(uid) val meMember = session.getRoom(state.roomId)?.getRoomMember(uid)
avatarRenderer.render(meMember?.avatarUrl, uid, meMember?.displayName, composerLayout.composerAvatarImageView) avatarRenderer.render(meMember?.avatarUrl, uid, meMember?.displayName, composerLayout.composerAvatarImageView)
@ -555,7 +587,14 @@ class RoomDetailFragment :
} else if (state.asyncInviter.complete) { } else if (state.asyncInviter.complete) {
vectorBaseActivity.finish() vectorBaseActivity.finish()
} }
composerLayout.setRoomEncrypted(state.isEncrypted) 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.tombstoneEvent))
}
} }


private fun renderRoomSummary(state: RoomDetailViewState) { private fun renderRoomSummary(state: RoomDetailViewState) {
@ -575,6 +614,26 @@ class RoomDetailFragment :
autocompleteUserPresenter.render(state.asyncUsers) autocompleteUserPresenter.render(state.asyncUsers)
} }


private fun renderTombstoneEventHandling(async: Async<String>) {
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.joining_room)
}
is Success -> {
navigator.openRoom(vectorBaseActivity, async())
vectorBaseActivity.finish()
}
is Fail -> {
vectorBaseActivity.hideWaitingView()
vectorBaseActivity.toast(errorFormatter.toHumanReadable(async.error))
}
}
}


private fun renderSendMessageResult(sendMessageResult: SendMessageResult) { private fun renderSendMessageResult(sendMessageResult: SendMessageResult) {
when (sendMessageResult) { when (sendMessageResult) {
is SendMessageResult.MessageSent, is SendMessageResult.MessageSent,
@ -608,7 +667,7 @@ class RoomDetailFragment :
.show() .show()
} }


// TimelineEventController.Callback ************************************************************ // TimelineEventController.Callback ************************************************************


override fun onUrlClicked(url: String): Boolean { override fun onUrlClicked(url: String): Boolean {
return permalinkHandler.launch(requireActivity(), url, object : NavigateToRoomInterceptor { return permalinkHandler.launch(requireActivity(), url, object : NavigateToRoomInterceptor {
@ -651,7 +710,7 @@ class RoomDetailFragment :
val intent = ImageMediaViewerActivity.newIntent(vectorBaseActivity, mediaData, ViewCompat.getTransitionName(view)) val intent = ImageMediaViewerActivity.newIntent(vectorBaseActivity, mediaData, ViewCompat.getTransitionName(view))
val bundle = ActivityOptionsCompat.makeSceneTransitionAnimation( val bundle = ActivityOptionsCompat.makeSceneTransitionAnimation(
requireActivity(), view, ViewCompat.getTransitionName(view) requireActivity(), view, ViewCompat.getTransitionName(view)
?: "").toBundle() ?: "").toBundle()
startActivity(intent, bundle) startActivity(intent, bundle)
} }


@ -731,6 +790,16 @@ class RoomDetailFragment :
ViewEditHistoryBottomSheet.newInstance(roomDetailArgs.roomId, informationData) ViewEditHistoryBottomSheet.newInstance(roomDetailArgs.roomId, informationData)
.show(requireActivity().supportFragmentManager, "DISPLAY_EDITS") .show(requireActivity().supportFragmentManager, "DISPLAY_EDITS")
} }

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 // AutocompleteUserPresenter.Callback


override fun onQueryUsers(query: CharSequence?) { override fun onQueryUsers(query: CharSequence?) {
@ -745,7 +814,7 @@ class RoomDetailFragment :
} }
MessageMenuViewModel.ACTION_VIEW_REACTIONS -> { MessageMenuViewModel.ACTION_VIEW_REACTIONS -> {
val messageInformationData = actionData.data as? MessageInformationData val messageInformationData = actionData.data as? MessageInformationData
?: return ?: return
ViewReactionBottomSheet.newInstance(roomDetailArgs.roomId, messageInformationData) ViewReactionBottomSheet.newInstance(roomDetailArgs.roomId, messageInformationData)
.show(requireActivity().supportFragmentManager, "DISPLAY_REACTIONS") .show(requireActivity().supportFragmentManager, "DISPLAY_REACTIONS")
} }
@ -841,13 +910,13 @@ class RoomDetailFragment :
} }
} }


//utils //utils
/** /**
* Insert an user displayname in the message editor. * Insert an user displayname in the message editor.
* *
* @param text the text to insert. * @param text the text to insert.
*/ */
//TODO legacy, refactor //TODO legacy, refactor
private fun insertUserDisplayNameInTextEditor(text: String?) { private fun insertUserDisplayNameInTextEditor(text: String?) {
//TODO move logic outside of fragment //TODO move logic outside of fragment
if (null != text) { if (null != text) {
@ -898,7 +967,7 @@ class RoomDetailFragment :
snack.show() snack.show()
} }


// VectorInviteView.Callback // VectorInviteView.Callback


override fun onAcceptInvite() { override fun onAcceptInvite() {
notificationDrawerManager.clearMemberShipNotificationForRoom(roomDetailArgs.roomId) notificationDrawerManager.clearMemberShipNotificationForRoom(roomDetailArgs.roomId)

View File

@ -29,8 +29,10 @@ import com.jakewharton.rxrelay2.BehaviorRelay
import com.squareup.inject.assisted.Assisted import com.squareup.inject.assisted.Assisted
import com.squareup.inject.assisted.AssistedInject import com.squareup.inject.assisted.AssistedInject
import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.MatrixPatterns
import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.content.ContentAttachmentData import im.vector.matrix.android.api.session.content.ContentAttachmentData
import im.vector.matrix.android.api.session.events.model.EventType
import im.vector.matrix.android.api.session.events.model.isImageMessage import im.vector.matrix.android.api.session.events.model.isImageMessage
import im.vector.matrix.android.api.session.events.model.isTextMessage import im.vector.matrix.android.api.session.events.model.isTextMessage
import im.vector.matrix.android.api.session.events.model.toModel import im.vector.matrix.android.api.session.events.model.toModel
@ -39,6 +41,7 @@ import im.vector.matrix.android.api.session.room.model.Membership
import im.vector.matrix.android.api.session.room.model.message.MessageContent import im.vector.matrix.android.api.session.room.model.message.MessageContent
import im.vector.matrix.android.api.session.room.model.message.MessageType import im.vector.matrix.android.api.session.room.model.message.MessageType
import im.vector.matrix.android.api.session.room.model.message.getFileUrl import im.vector.matrix.android.api.session.room.model.message.getFileUrl
import im.vector.matrix.android.api.session.room.model.tombstone.RoomTombstoneContent
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.matrix.android.internal.crypto.attachments.toElementToDecrypt import im.vector.matrix.android.internal.crypto.attachments.toElementToDecrypt
import im.vector.matrix.android.internal.crypto.model.event.EncryptedEventContent import im.vector.matrix.android.internal.crypto.model.event.EncryptedEventContent
@ -100,7 +103,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
init { init {
observeRoomSummary() observeRoomSummary()
observeEventDisplayedActions() observeEventDisplayedActions()
observeInvitationState() observeSummaryState()
room.rx().loadRoomMembersIfNeeded().subscribeLogError().disposeOnClear() room.rx().loadRoomMembersIfNeeded().subscribeLogError().disposeOnClear()
timeline.start() timeline.start()
setState { copy(timeline = this@RoomDetailViewModel.timeline) } setState { copy(timeline = this@RoomDetailViewModel.timeline) }
@ -111,7 +114,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
is RoomDetailActions.SendMessage -> handleSendMessage(action) is RoomDetailActions.SendMessage -> handleSendMessage(action)
is RoomDetailActions.SendMedia -> handleSendMedia(action) is RoomDetailActions.SendMedia -> handleSendMedia(action)
is RoomDetailActions.EventDisplayed -> handleEventDisplayed(action) is RoomDetailActions.EventDisplayed -> handleEventDisplayed(action)
is RoomDetailActions.LoadMore -> handleLoadMore(action) is RoomDetailActions.LoadMoreTimelineEvents -> handleLoadMore(action)
is RoomDetailActions.SendReaction -> handleSendReaction(action) is RoomDetailActions.SendReaction -> handleSendReaction(action)
is RoomDetailActions.AcceptInvite -> handleAcceptInvite() is RoomDetailActions.AcceptInvite -> handleAcceptInvite()
is RoomDetailActions.RejectInvite -> handleRejectInvite() is RoomDetailActions.RejectInvite -> handleRejectInvite()
@ -123,6 +126,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
is RoomDetailActions.EnterReplyMode -> handleReplyAction(action) is RoomDetailActions.EnterReplyMode -> handleReplyAction(action)
is RoomDetailActions.DownloadFile -> handleDownloadFile(action) is RoomDetailActions.DownloadFile -> handleDownloadFile(action)
is RoomDetailActions.NavigateToEvent -> handleNavigateToEvent(action) is RoomDetailActions.NavigateToEvent -> handleNavigateToEvent(action)
is RoomDetailActions.HandleTombstoneEvent -> handleTombstoneEvent(action)
is RoomDetailActions.ResendMessage -> handleResendEvent(action) is RoomDetailActions.ResendMessage -> handleResendEvent(action)
is RoomDetailActions.RemoveFailedEcho -> handleRemove(action) is RoomDetailActions.RemoveFailedEcho -> handleRemove(action)
is RoomDetailActions.ClearSendQueue -> handleClearSendQueue() is RoomDetailActions.ClearSendQueue -> handleClearSendQueue()
@ -131,6 +135,32 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
} }
} }


private fun handleTombstoneEvent(action: RoomDetailActions.HandleTombstoneEvent) {
val tombstoneContent = action.event.getClearContent().toModel<RoomTombstoneContent>()
?: return

val roomId = tombstoneContent.replacementRoom ?: ""
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 {
if (it.isNullOrBlank()) {
emptyList()
} else {
listOf(it)
}
}
session.rx()
.joinRoom(roomId, viaServer)
.map { roomId }
.execute {
copy(tombstoneEventHandling = it)
}
}

}

private fun enterEditMode(event: TimelineEvent) { private fun enterEditMode(event: TimelineEvent) {
setState { setState {
copy( copy(
@ -151,7 +181,6 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
val nonBlockingPopAlert: LiveData<LiveEvent<Pair<Int, List<Any>>>> val nonBlockingPopAlert: LiveData<LiveEvent<Pair<Int, List<Any>>>>
get() = _nonBlockingPopAlert get() = _nonBlockingPopAlert



private val _sendMessageResultLiveData = MutableLiveData<LiveEvent<SendMessageResult>>() private val _sendMessageResultLiveData = MutableLiveData<LiveEvent<SendMessageResult>>()
val sendMessageResultLiveData: LiveData<LiveEvent<SendMessageResult>> val sendMessageResultLiveData: LiveData<LiveEvent<SendMessageResult>>
get() = _sendMessageResultLiveData get() = _sendMessageResultLiveData
@ -263,12 +292,12 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
} else { } else {
val messageContent: MessageContent? = val messageContent: MessageContent? =
state.sendMode.timelineEvent.annotations?.editSummary?.aggregatedContent.toModel() state.sendMode.timelineEvent.annotations?.editSummary?.aggregatedContent.toModel()
?: state.sendMode.timelineEvent.root.getClearContent().toModel() ?: state.sendMode.timelineEvent.root.getClearContent().toModel()
val existingBody = messageContent?.body ?: "" val existingBody = messageContent?.body ?: ""
if (existingBody != action.text) { if (existingBody != action.text) {
room.editTextMessage(state.sendMode.timelineEvent.root.eventId room.editTextMessage(state.sendMode.timelineEvent.root.eventId
?: "", messageContent?.type ?: "", messageContent?.type
?: MessageType.MSGTYPE_TEXT, action.text, action.autoMarkdown) ?: MessageType.MSGTYPE_TEXT, action.text, action.autoMarkdown)
} else { } else {
Timber.w("Same message content, do not send edition") Timber.w("Same message content, do not send edition")
} }
@ -283,7 +312,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
is SendMode.QUOTE -> { is SendMode.QUOTE -> {
val messageContent: MessageContent? = val messageContent: MessageContent? =
state.sendMode.timelineEvent.annotations?.editSummary?.aggregatedContent.toModel() state.sendMode.timelineEvent.annotations?.editSummary?.aggregatedContent.toModel()
?: state.sendMode.timelineEvent.root.getClearContent().toModel() ?: state.sendMode.timelineEvent.root.getClearContent().toModel()
val textMsg = messageContent?.body val textMsg = messageContent?.body


val finalText = legacyRiotQuoteText(textMsg, action.text) val finalText = legacyRiotQuoteText(textMsg, action.text)
@ -423,7 +452,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) timeline.paginate(action.direction, PAGINATION_COUNT)
} }


@ -432,7 +461,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
} }


private fun handleAcceptInvite() { private fun handleAcceptInvite() {
room.join(object : MatrixCallback<Unit> {}) room.join(callback = object : MatrixCallback<Unit> {})
} }


private fun handleEditAction(action: RoomDetailActions.EnterEditMode) { private fun handleEditAction(action: RoomDetailActions.EnterEditMode) {
@ -611,7 +640,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
} }
} }


private fun observeInvitationState() { private fun observeSummaryState() {
asyncSubscribe(RoomDetailViewState::asyncRoomSummary) { summary -> asyncSubscribe(RoomDetailViewState::asyncRoomSummary) { summary ->
if (summary.membership == Membership.INVITE) { if (summary.membership == Membership.INVITE) {
summary.latestEvent?.root?.senderId?.let { senderId -> summary.latestEvent?.root?.senderId?.let { senderId ->
@ -620,6 +649,9 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
setState { copy(asyncInviter = Success(it)) } setState { copy(asyncInviter = Success(it)) }
} }
} }
room.getStateEvent(EventType.STATE_ROOM_TOMBSTONE)?.also {
setState { copy(tombstoneEvent = it) }
}
} }
} }



View File

@ -19,7 +19,9 @@ package im.vector.riotx.features.home.room.detail
import com.airbnb.mvrx.Async import com.airbnb.mvrx.Async
import com.airbnb.mvrx.MvRxState import com.airbnb.mvrx.MvRxState
import com.airbnb.mvrx.Uninitialized import com.airbnb.mvrx.Uninitialized
import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.api.session.room.model.RoomSummary import im.vector.matrix.android.api.session.room.model.RoomSummary
import im.vector.matrix.android.api.session.room.model.tombstone.RoomTombstoneContent
import im.vector.matrix.android.api.session.room.timeline.Timeline import im.vector.matrix.android.api.session.room.timeline.Timeline
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.matrix.android.api.session.user.model.User import im.vector.matrix.android.api.session.user.model.User
@ -46,7 +48,9 @@ data class RoomDetailViewState(
val asyncInviter: Async<User> = Uninitialized, val asyncInviter: Async<User> = Uninitialized,
val asyncRoomSummary: Async<RoomSummary> = Uninitialized, val asyncRoomSummary: Async<RoomSummary> = Uninitialized,
val sendMode: SendMode = SendMode.REGULAR, val sendMode: SendMode = SendMode.REGULAR,
val isEncrypted: Boolean = false val isEncrypted: Boolean = false,
val tombstoneEvent: Event? = null,
val tombstoneEventHandling: Async<String> = Uninitialized
) : MvRxState { ) : MvRxState {


constructor(args: RoomDetailArgs) : this(roomId = args.roomId, eventId = args.eventId) constructor(args: RoomDetailArgs) : this(roomId = args.roomId, eventId = args.eventId)

View File

@ -54,6 +54,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Tim


interface Callback : ReactionPillCallback, AvatarCallback, BaseCallback, UrlClickCallback { interface Callback : ReactionPillCallback, AvatarCallback, BaseCallback, UrlClickCallback {
fun onEventVisible(event: TimelineEvent) fun onEventVisible(event: TimelineEvent)
fun onRoomCreateLinkClicked(url: String)
fun onEncryptedMessageClicked(informationData: MessageInformationData, view: View) fun onEncryptedMessageClicked(informationData: MessageInformationData, view: View)
fun onImageMessageClicked(messageImageContent: MessageImageContent, mediaData: ImageContentRenderer.Data, view: View) fun onImageMessageClicked(messageImageContent: MessageImageContent, mediaData: ImageContentRenderer.Data, view: View)
fun onVideoMessageClicked(messageVideoContent: MessageVideoContent, mediaData: VideoContentRenderer.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) { synchronized(modelCache) {
for (i in 0 until modelCache.size) { for (i in 0 until modelCache.size) {
if (modelCache[i]?.eventId == eventIdToHighlight if (modelCache[i]?.eventId == eventIdToHighlight
|| modelCache[i]?.eventId == this.eventIdToHighlight) { || modelCache[i]?.eventId == this.eventIdToHighlight) {
modelCache[i] = null 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 // 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. // We then are sure we always have items up to date.
if (modelCache[position] == null if (modelCache[position] == null
|| modelCache[position]?.mergedHeaderModel != null || modelCache[position]?.mergedHeaderModel != null
|| modelCache[position]?.formattedDayModel != null) { || modelCache[position]?.formattedDayModel != null) {
modelCache[position] = buildItemModels(position, currentSnapshot) 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 // => handle case where paginating from mergeable events and we get more
val previousCollapseStateKey = mergedEventIds.intersect(mergeItemCollapseStates.keys).firstOrNull() val previousCollapseStateKey = mergedEventIds.intersect(mergeItemCollapseStates.keys).firstOrNull()
val initialCollapseState = mergeItemCollapseStates.remove(previousCollapseStateKey) val initialCollapseState = mergeItemCollapseStates.remove(previousCollapseStateKey)
?: true ?: true
val isCollapsed = mergeItemCollapseStates.getOrPut(event.localId) { initialCollapseState } val isCollapsed = mergeItemCollapseStates.getOrPut(event.localId) { initialCollapseState }
if (isCollapsed) { if (isCollapsed) {
collapsedEventIds.addAll(mergedEventIds) collapsedEventIds.addAll(mergedEventIds)

View File

@ -0,0 +1,53 @@
/*
* 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.PermalinkFactory
import im.vector.matrix.android.api.session.events.model.toModel
import im.vector.matrix.android.api.session.room.model.create.RoomCreateContent
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.riotx.R
import im.vector.riotx.core.resources.ColorProvider
import im.vector.riotx.core.resources.StringProvider
import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController
import im.vector.riotx.features.home.room.detail.timeline.item.RoomCreateItem
import im.vector.riotx.features.home.room.detail.timeline.item.RoomCreateItem_
import me.gujun.android.span.span
import javax.inject.Inject

class RoomCreateItemFactory @Inject constructor(private val colorProvider: ColorProvider,
private val stringProvider: StringProvider) {

fun create(event: TimelineEvent, callback: TimelineEventController.Callback?): RoomCreateItem? {
val createRoomContent = event.root.getClearContent().toModel<RoomCreateContent>()
?: return null
val 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"
span(stringProvider.getString(R.string.room_tombstone_predecessor_link)) {
textDecorationLine = "underline"
onClick = { callback?.onRoomCreateLinkClicked(roomLink) }
}
}
return RoomCreateItem_()
.text(text)
}


}

View File

@ -33,6 +33,7 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me
private val encryptedItemFactory: EncryptedItemFactory, private val encryptedItemFactory: EncryptedItemFactory,
private val noticeItemFactory: NoticeItemFactory, private val noticeItemFactory: NoticeItemFactory,
private val defaultItemFactory: DefaultItemFactory, private val defaultItemFactory: DefaultItemFactory,
private val roomCreateItemFactory: RoomCreateItemFactory,
private val avatarRenderer: AvatarRenderer) { private val avatarRenderer: AvatarRenderer) {


fun create(event: TimelineEvent, fun create(event: TimelineEvent,
@ -45,6 +46,7 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me
when (event.root.getClearType()) { when (event.root.getClearType()) {
EventType.MESSAGE -> messageItemFactory.create(event, nextEvent, highlight, callback) EventType.MESSAGE -> messageItemFactory.create(event, nextEvent, highlight, callback)
// State and call // State and call
EventType.STATE_ROOM_TOMBSTONE,
EventType.STATE_ROOM_NAME, EventType.STATE_ROOM_NAME,
EventType.STATE_ROOM_TOPIC, EventType.STATE_ROOM_TOPIC,
EventType.STATE_ROOM_MEMBER, EventType.STATE_ROOM_MEMBER,
@ -52,7 +54,8 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me
EventType.CALL_INVITE, EventType.CALL_INVITE,
EventType.CALL_HANGUP, EventType.CALL_HANGUP,
EventType.CALL_ANSWER -> noticeItemFactory.create(event, highlight, callback) EventType.CALL_ANSWER -> noticeItemFactory.create(event, highlight, callback)

// State room create
EventType.STATE_ROOM_CREATE -> roomCreateItemFactory.create(event, callback)
// Crypto // Crypto
EventType.ENCRYPTION -> encryptionItemFactory.create(event, highlight, callback) EventType.ENCRYPTION -> encryptionItemFactory.create(event, highlight, callback)
EventType.ENCRYPTED -> { EventType.ENCRYPTED -> {
@ -66,8 +69,7 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me


// Unhandled event types (yet) // Unhandled event types (yet)
EventType.STATE_ROOM_THIRD_PARTY_INVITE, EventType.STATE_ROOM_THIRD_PARTY_INVITE,
EventType.STICKER, EventType.STICKER -> defaultItemFactory.create(event, highlight)
EventType.STATE_ROOM_CREATE -> defaultItemFactory.create(event, highlight)


else -> { else -> {
//These are just for debug to display hidden event, they should be filtered out in normal mode //These are just for debug to display hidden event, they should be filtered out in normal mode

View File

@ -22,6 +22,7 @@ import im.vector.matrix.android.api.session.events.model.EventType
import im.vector.matrix.android.api.session.events.model.toModel import im.vector.matrix.android.api.session.events.model.toModel
import im.vector.matrix.android.api.session.room.model.* import im.vector.matrix.android.api.session.room.model.*
import im.vector.matrix.android.api.session.room.model.call.CallInviteContent import im.vector.matrix.android.api.session.room.model.call.CallInviteContent
import im.vector.matrix.android.api.session.room.model.tombstone.RoomTombstoneContent
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.riotx.R import im.vector.riotx.R
import im.vector.riotx.core.resources.StringProvider import im.vector.riotx.core.resources.StringProvider
@ -37,6 +38,7 @@ class NoticeEventFormatter @Inject constructor(private val stringProvider: Strin
EventType.STATE_ROOM_TOPIC -> formatRoomTopicEvent(timelineEvent.root, timelineEvent.getDisambiguatedDisplayName()) EventType.STATE_ROOM_TOPIC -> formatRoomTopicEvent(timelineEvent.root, timelineEvent.getDisambiguatedDisplayName())
EventType.STATE_ROOM_MEMBER -> formatRoomMemberEvent(timelineEvent.root, timelineEvent.senderName()) EventType.STATE_ROOM_MEMBER -> formatRoomMemberEvent(timelineEvent.root, timelineEvent.senderName())
EventType.STATE_HISTORY_VISIBILITY -> formatRoomHistoryVisibilityEvent(timelineEvent.root, timelineEvent.getDisambiguatedDisplayName()) EventType.STATE_HISTORY_VISIBILITY -> formatRoomHistoryVisibilityEvent(timelineEvent.root, timelineEvent.getDisambiguatedDisplayName())
EventType.STATE_ROOM_TOMBSTONE -> formatRoomTombstoneEvent(timelineEvent.root, timelineEvent.getDisambiguatedDisplayName())
EventType.CALL_INVITE, EventType.CALL_INVITE,
EventType.CALL_HANGUP, EventType.CALL_HANGUP,
EventType.CALL_ANSWER -> formatCallEvent(timelineEvent.root, timelineEvent.getDisambiguatedDisplayName()) EventType.CALL_ANSWER -> formatCallEvent(timelineEvent.root, timelineEvent.getDisambiguatedDisplayName())
@ -56,6 +58,7 @@ class NoticeEventFormatter @Inject constructor(private val stringProvider: Strin
EventType.CALL_INVITE, EventType.CALL_INVITE,
EventType.CALL_HANGUP, EventType.CALL_HANGUP,
EventType.CALL_ANSWER -> formatCallEvent(event, senderName) EventType.CALL_ANSWER -> formatCallEvent(event, senderName)
EventType.STATE_ROOM_TOMBSTONE -> formatRoomTombstoneEvent(event, senderName)
else -> { else -> {
Timber.v("Type $type not handled by this formatter") Timber.v("Type $type not handled by this formatter")
null null
@ -72,6 +75,10 @@ class NoticeEventFormatter @Inject constructor(private val stringProvider: Strin
} }
} }


private fun formatRoomTombstoneEvent(event: Event, senderName: String?): CharSequence? {
return stringProvider.getString(R.string.notice_room_update, senderName)
}

private fun formatRoomTopicEvent(event: Event, senderName: String?): CharSequence? { private fun formatRoomTopicEvent(event: Event, senderName: String?): CharSequence? {
val content = event.getClearContent().toModel<RoomTopicContent>() ?: return null val content = event.getClearContent().toModel<RoomTopicContent>() ?: return null
return if (content.topic.isNullOrEmpty()) { return if (content.topic.isNullOrEmpty()) {

View File

@ -39,7 +39,8 @@ object TimelineDisplayableEvents {
EventType.ENCRYPTION, EventType.ENCRYPTION,
EventType.STATE_ROOM_THIRD_PARTY_INVITE, EventType.STATE_ROOM_THIRD_PARTY_INVITE,
EventType.STICKER, EventType.STICKER,
EventType.STATE_ROOM_CREATE EventType.STATE_ROOM_CREATE,
EventType.STATE_ROOM_TOMBSTONE
) )


val DEBUG_DISPLAYABLE_TYPES = DISPLAYABLE_TYPES + listOf( val DEBUG_DISPLAYABLE_TYPES = DISPLAYABLE_TYPES + listOf(

View File

@ -0,0 +1,40 @@
/*
* 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 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<RoomCreateItem.Holder>() {

@EpoxyAttribute lateinit var text: CharSequence

override fun bind(holder: Holder) {
holder.description.movementMethod = BetterLinkMovementMethod.getInstance()
holder.description.text = text
}

class Holder : VectorEpoxyHolder() {
val description by bind<TextView>(R.id.roomCreateItemDescription)
}
}

View File

@ -135,7 +135,7 @@ class RoomListViewModel @AssistedInject constructor(@Assisted initialState: Room
) )
} }


session.getRoom(roomId)?.join(object : MatrixCallback<Unit> { session.getRoom(roomId)?.join(emptyList(), object : MatrixCallback<Unit> {
override fun onSuccess(data: Unit) { override fun onSuccess(data: Unit) {
// We do not update the joiningRoomsIds here, because, the room is not joined yet regarding the sync data. // 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 // Instead, we wait for the room to be joined

View File

@ -75,7 +75,7 @@ class NotificationBroadcastReceiver : BroadcastReceiver() {
private fun handleJoinRoom(roomId: String) { private fun handleJoinRoom(roomId: String) {
activeSessionHolder.getSafeActiveSession()?.let { session -> activeSessionHolder.getSafeActiveSession()?.let { session ->
session.getRoom(roomId) session.getRoom(roomId)
?.join(object : MatrixCallback<Unit> {}) ?.join(emptyList(), object : MatrixCallback<Unit> {})
} }
} }



View File

@ -200,7 +200,7 @@ class RoomDirectoryViewModel @AssistedInject constructor(@Assisted initialState:
) )
} }


session.joinRoom(publicRoom.roomId, object : MatrixCallback<Unit> { session.joinRoom(publicRoom.roomId, emptyList(), object : MatrixCallback<Unit> {
override fun onSuccess(data: Unit) { override fun onSuccess(data: Unit) {
// We do not update the joiningRoomsIds here, because, the room is not joined yet regarding the sync data. // 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 // Instead, we wait for the room to be joined

View File

@ -90,7 +90,7 @@ class RoomPreviewViewModel @AssistedInject constructor(@Assisted initialState: R
) )
} }


session.joinRoom(state.roomId, object : MatrixCallback<Unit> { session.joinRoom(state.roomId, emptyList(), object : MatrixCallback<Unit> {
override fun onSuccess(data: Unit) { override fun onSuccess(data: Unit) {
// We do not update the joiningRoomsIds here, because, the room is not joined yet regarding the sync data. // 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 // Instead, we wait for the room to be joined

View File

@ -1,8 +1,14 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>


<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/roomDetailContainer"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent">


<FrameLayout
android:id="@+id/roomDetailContainer"
android:layout_width="match_parent"
android:layout_height="match_parent" />

<include layout="@layout/merge_overlay_waiting_view" />

</FrameLayout> </FrameLayout>

View File

@ -74,12 +74,18 @@
android:id="@+id/recyclerView" android:id="@+id/recyclerView"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="0dp" android:layout_height="0dp"
app:layout_constraintBottom_toTopOf="@+id/composerLayout" app:layout_constraintBottom_toTopOf="@+id/recyclerViewBarrier"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/roomToolbar" app:layout_constraintTop_toBottomOf="@id/roomToolbar"
tools:listitem="@layout/item_timeline_event_base" /> tools:listitem="@layout/item_timeline_event_base" />


<androidx.constraintlayout.widget.Barrier
android:id="@+id/recyclerViewBarrier"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:barrierDirection="top"
app:constraint_referenced_ids="composerLayout,notificationAreaView" />


<im.vector.riotx.features.home.room.detail.composer.TextComposerView <im.vector.riotx.features.home.room.detail.composer.TextComposerView
android:id="@+id/composerLayout" android:id="@+id/composerLayout"
@ -89,6 +95,16 @@
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" /> app:layout_constraintStart_toStartOf="parent" />


<im.vector.riotx.core.platform.NotificationAreaView
android:id="@+id/notificationAreaView"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:paddingTop="16dp"
android:paddingBottom="16dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />

<im.vector.riotx.features.invite.VectorInviteView <im.vector.riotx.features.invite.VectorInviteView
android:id="@+id/inviteView" android:id="@+id/inviteView"
android:layout_width="0dp" android:layout_width="0dp"
@ -101,4 +117,5 @@
app:layout_constraintTop_toBottomOf="@+id/roomToolbar" app:layout_constraintTop_toBottomOf="@+id/roomToolbar"
tools:visibility="visible" /> tools:visibility="visible" />



</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content">

<TextView
android:id="@+id/roomCreateItemDescription"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginLeft="16dp"
android:layout_marginTop="16dp"
android:layout_marginBottom="16dp"
android:background="?attr/riotx_keys_backup_banner_accent_color"
android:drawableStart="@drawable/error"
android:drawablePadding="16dp"
android:gravity="center|start"
android:minHeight="80dp"
android:padding="16dp"
tools:text="This room is continuation…" />

</FrameLayout>

View File

@ -7,4 +7,6 @@
<string name="direct_room_no_known_users">"No result found, use Add by matrix ID to search on server."</string> <string name="direct_room_no_known_users">"No result found, use Add by matrix ID to search on server."</string>
<string name="direct_room_start_search">"Start typing to get results"</string> <string name="direct_room_start_search">"Start typing to get results"</string>
<string name="direct_room_filter_hint">"Filter by username or ID…"</string> <string name="direct_room_filter_hint">"Filter by username or ID…"</string>

<string name="joining_room">"Joining room…"</string>
</resources> </resources>