mirror of
https://github.com/element-hq/element-android
synced 2024-11-28 13:38:49 +03:00
Merge pull request #449 from vector-im/feature/room_update
Feature/room upgrade
This commit is contained in:
commit
c300c50093
54 changed files with 1230 additions and 111 deletions
|
@ -45,6 +45,10 @@ class RxRoom(private val room: Room) {
|
|||
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 {
|
||||
|
|
|
@ -63,6 +63,10 @@ class RxSession(private val session: Session) {
|
|||
session.searchUsersDirectory(search, limit, excludedUserIds, MatrixCallbackSingle(it)).toSingle(it)
|
||||
}
|
||||
|
||||
fun joinRoom(roomId: String, viaServers: List<String> = emptyList()): Single<Unit> = Single.create {
|
||||
session.joinRoom(roomId, viaServers, MatrixCallbackSingle(it)).toSingle(it)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
fun Session.rx(): RxSession {
|
||||
|
|
|
@ -123,9 +123,9 @@ object MatrixPatterns {
|
|||
*/
|
||||
fun isEventId(str: String?): Boolean {
|
||||
return str != null
|
||||
&& (str matches PATTERN_CONTAIN_MATRIX_EVENT_IDENTIFIER
|
||||
|| str matches PATTERN_CONTAIN_MATRIX_EVENT_IDENTIFIER_V3
|
||||
|| str matches PATTERN_CONTAIN_MATRIX_EVENT_IDENTIFIER_V4)
|
||||
&& (str matches PATTERN_CONTAIN_MATRIX_EVENT_IDENTIFIER
|
||||
|| str matches PATTERN_CONTAIN_MATRIX_EVENT_IDENTIFIER_V3
|
||||
|| str matches PATTERN_CONTAIN_MATRIX_EVENT_IDENTIFIER_V4)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -137,4 +137,23 @@ object MatrixPatterns {
|
|||
fun isGroupId(str: String?): Boolean {
|
||||
return str != null && str matches PATTERN_CONTAIN_MATRIX_GROUP_IDENTIFIER
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract server name from a matrix id
|
||||
*
|
||||
* @param matrixId
|
||||
* @return null if not found or if matrixId is null
|
||||
*/
|
||||
fun extractServerNameFromId(matrixId: String?): String? {
|
||||
if (matrixId == null) {
|
||||
return null
|
||||
}
|
||||
|
||||
val index = matrixId.lastIndexOf(":")
|
||||
|
||||
return if (index == -1) {
|
||||
null
|
||||
} else matrixId.substring(index + 1)
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -26,8 +26,13 @@ import com.squareup.moshi.JsonClass
|
|||
@JsonClass(generateAdapter = true)
|
||||
data class MatrixError(
|
||||
@Json(name = "errcode") val code: String,
|
||||
@Json(name = "error") val message: String
|
||||
) {
|
||||
@Json(name = "error") val message: String,
|
||||
|
||||
@Json(name = "consent_uri") val consentUri: String? = null,
|
||||
// RESOURCE_LIMIT_EXCEEDED data
|
||||
@Json(name = "limit_type") val limitType: String? = null,
|
||||
@Json(name = "admin_contact") val adminUri: String? = null) {
|
||||
|
||||
|
||||
companion object {
|
||||
const val FORBIDDEN = "M_FORBIDDEN"
|
||||
|
@ -55,5 +60,8 @@ data class MatrixError(
|
|||
const val M_CONSENT_NOT_GIVEN = "M_CONSENT_NOT_GIVEN"
|
||||
const val RESOURCE_LIMIT_EXCEEDED = "M_RESOURCE_LIMIT_EXCEEDED"
|
||||
const val WRONG_ROOM_KEYS_VERSION = "M_WRONG_ROOM_KEYS_VERSION"
|
||||
|
||||
// Possible value for "limit_type"
|
||||
const val LIMIT_TYPE_MAU = "monthly_active_user"
|
||||
}
|
||||
}
|
|
@ -32,6 +32,15 @@ interface RoomService {
|
|||
*/
|
||||
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
|
||||
* @param roomId the roomId to look for.
|
||||
|
|
|
@ -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()
|
||||
|
||||
}
|
|
@ -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()
|
||||
|
||||
}
|
|
@ -57,7 +57,8 @@ interface MembershipService {
|
|||
/**
|
||||
* 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.
|
||||
|
|
|
@ -34,5 +34,10 @@ data class RoomSummary(
|
|||
val notificationCount: Int = 0,
|
||||
val highlightCount: Int = 0,
|
||||
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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
)
|
|
@ -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
|
||||
)
|
||||
|
||||
|
|
@ -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?
|
||||
)
|
|
@ -17,6 +17,7 @@
|
|||
package im.vector.matrix.android.api.session.room.state
|
||||
|
||||
import im.vector.matrix.android.api.MatrixCallback
|
||||
import im.vector.matrix.android.api.session.events.model.Event
|
||||
|
||||
interface StateService {
|
||||
|
||||
|
@ -25,4 +26,6 @@ interface StateService {
|
|||
*/
|
||||
fun updateTopic(topic: String, callback: MatrixCallback<Unit>)
|
||||
|
||||
fun getStateEvent(eventType: String): Event?
|
||||
|
||||
}
|
|
@ -16,45 +16,45 @@
|
|||
|
||||
package im.vector.matrix.android.internal.database
|
||||
|
||||
import android.os.Handler
|
||||
import android.os.HandlerThread
|
||||
import im.vector.matrix.android.internal.util.createBackgroundHandler
|
||||
import io.realm.*
|
||||
import timber.log.Timber
|
||||
import java.util.concurrent.CountDownLatch
|
||||
import java.util.concurrent.TimeUnit
|
||||
import java.util.concurrent.atomic.AtomicReference
|
||||
|
||||
private const val THREAD_NAME = "REALM_QUERY_LATCH"
|
||||
|
||||
class RealmQueryLatch<E : RealmObject>(private val realmConfiguration: RealmConfiguration,
|
||||
private val realmQueryBuilder: (Realm) -> RealmQuery<E>) {
|
||||
|
||||
@Throws(InterruptedException::class)
|
||||
fun await(timeout: Long = Long.MAX_VALUE, timeUnit: TimeUnit = TimeUnit.MILLISECONDS) {
|
||||
val latch = CountDownLatch(1)
|
||||
val handlerThread = HandlerThread(THREAD_NAME + hashCode())
|
||||
handlerThread.start()
|
||||
val handler = Handler(handlerThread.looper)
|
||||
val runnable = Runnable {
|
||||
val realm = Realm.getInstance(realmConfiguration)
|
||||
val result = realmQueryBuilder(realm).findAllAsync()
|
||||
private companion object {
|
||||
val QUERY_LATCH_HANDLER = createBackgroundHandler("REALM_QUERY_LATCH")
|
||||
}
|
||||
|
||||
@Throws(InterruptedException::class)
|
||||
fun await(timeout: Long, timeUnit: TimeUnit) {
|
||||
val realmRef = AtomicReference<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>> {
|
||||
override fun onChange(t: RealmResults<E>) {
|
||||
if (t.isNotEmpty()) {
|
||||
result.removeChangeListener(this)
|
||||
realm.close()
|
||||
latch.countDown()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
handler.post(runnable)
|
||||
try {
|
||||
latch.await(timeout, timeUnit)
|
||||
} catch (exception: InterruptedException) {
|
||||
throw exception
|
||||
} finally {
|
||||
handlerThread.quit()
|
||||
QUERY_LATCH_HANDLER.post {
|
||||
realmRef.getAndSet(null).close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -61,7 +61,8 @@ internal class RoomSummaryMapper @Inject constructor(
|
|||
highlightCount = roomSummaryEntity.highlightCount,
|
||||
notificationCount = roomSummaryEntity.notificationCount,
|
||||
tags = tags,
|
||||
membership = roomSummaryEntity.membership
|
||||
membership = roomSummaryEntity.membership,
|
||||
versioningState = roomSummaryEntity.versioningState
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,6 +17,7 @@
|
|||
package im.vector.matrix.android.internal.database.model
|
||||
|
||||
import im.vector.matrix.android.api.session.room.model.Membership
|
||||
import im.vector.matrix.android.api.session.room.model.VersioningState
|
||||
import io.realm.RealmList
|
||||
import io.realm.RealmObject
|
||||
import io.realm.annotations.Ignore
|
||||
|
@ -40,12 +41,19 @@ internal open class RoomSummaryEntity(@PrimaryKey var roomId: String = "",
|
|||
) : RealmObject() {
|
||||
|
||||
private var membershipStr: String = Membership.NONE.name
|
||||
private var versioningStateStr: String = VersioningState.NONE.name
|
||||
|
||||
|
||||
@delegate:Ignore
|
||||
var membership: Membership by Delegates.observable(Membership.valueOf(membershipStr)) { _, _, newValue ->
|
||||
membershipStr = newValue.name
|
||||
}
|
||||
|
||||
@delegate:Ignore
|
||||
var versioningState: VersioningState by Delegates.observable(VersioningState.valueOf(versioningStateStr)) { _, _, newValue ->
|
||||
versioningStateStr = newValue.name
|
||||
}
|
||||
|
||||
companion object
|
||||
|
||||
}
|
|
@ -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.session.group.GroupSummaryUpdater
|
||||
import im.vector.matrix.android.internal.session.room.EventRelationsAggregationUpdater
|
||||
import im.vector.matrix.android.internal.session.room.create.RoomCreateEventLiveObserver
|
||||
import im.vector.matrix.android.internal.session.room.tombstone.RoomTombstoneEventLiveObserver
|
||||
import im.vector.matrix.android.internal.session.room.prune.EventsPruner
|
||||
import im.vector.matrix.android.internal.util.md5
|
||||
import io.realm.RealmConfiguration
|
||||
|
@ -128,6 +130,14 @@ internal abstract class SessionModule {
|
|||
@IntoSet
|
||||
abstract fun bindEventRelationsAggregationUpdater(groupSummaryUpdater: EventRelationsAggregationUpdater): LiveEntityObserver
|
||||
|
||||
@Binds
|
||||
@IntoSet
|
||||
abstract fun bindRoomTombstoneEventLiveObserver(roomTombstoneEventLiveObserver: RoomTombstoneEventLiveObserver): LiveEntityObserver
|
||||
|
||||
@Binds
|
||||
@IntoSet
|
||||
abstract fun bindRoomCreateEventLiveObserver(roomCreateEventLiveObserver: RoomCreateEventLiveObserver): LiveEntityObserver
|
||||
|
||||
@Binds
|
||||
abstract fun bindInitialSyncProgressService(initialSyncProgressService: DefaultInitialSyncProgressService): InitialSyncProgressService
|
||||
|
||||
|
|
|
@ -22,6 +22,7 @@ import im.vector.matrix.android.api.MatrixCallback
|
|||
import im.vector.matrix.android.api.session.room.Room
|
||||
import im.vector.matrix.android.api.session.room.RoomService
|
||||
import im.vector.matrix.android.api.session.room.model.RoomSummary
|
||||
import im.vector.matrix.android.api.session.room.model.VersioningState
|
||||
import im.vector.matrix.android.api.session.room.model.create.CreateRoomParams
|
||||
import im.vector.matrix.android.api.util.Cancelable
|
||||
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.query.where
|
||||
import im.vector.matrix.android.internal.session.room.create.CreateRoomTask
|
||||
import im.vector.matrix.android.internal.session.room.membership.joining.JoinRoomTask
|
||||
import im.vector.matrix.android.internal.task.TaskExecutor
|
||||
import im.vector.matrix.android.internal.task.configureWith
|
||||
import im.vector.matrix.android.internal.util.fetchManaged
|
||||
|
@ -38,6 +40,7 @@ import javax.inject.Inject
|
|||
internal class DefaultRoomService @Inject constructor(private val monarchy: Monarchy,
|
||||
private val roomSummaryMapper: RoomSummaryMapper,
|
||||
private val createRoomTask: CreateRoomTask,
|
||||
private val joinRoomTask: JoinRoomTask,
|
||||
private val roomFactory: RoomFactory,
|
||||
private val taskExecutor: TaskExecutor) : RoomService {
|
||||
|
||||
|
@ -55,8 +58,19 @@ internal class DefaultRoomService @Inject constructor(private val monarchy: Mona
|
|||
|
||||
override fun liveRoomSummaries(): LiveData<List<RoomSummary>> {
|
||||
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) }
|
||||
)
|
||||
}
|
||||
|
||||
override fun joinRoom(roomId: String, viaServers: List<String>, callback: MatrixCallback<Unit>): Cancelable {
|
||||
return joinRoomTask
|
||||
.configureWith(JoinRoomTask.Params(roomId, viaServers))
|
||||
.dispatchTo(callback)
|
||||
.executeBy(taskExecutor)
|
||||
}
|
||||
}
|
|
@ -218,6 +218,7 @@ internal interface RoomAPI {
|
|||
*/
|
||||
@POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/join")
|
||||
fun join(@Path("roomId") roomId: String,
|
||||
@Query("server_name") viaServers: List<String>,
|
||||
@Body params: Map<String, String>): Call<Unit>
|
||||
|
||||
/**
|
||||
|
|
|
@ -22,6 +22,7 @@ import im.vector.matrix.android.api.auth.data.Credentials
|
|||
import im.vector.matrix.android.api.session.crypto.CryptoService
|
||||
import im.vector.matrix.android.api.session.room.Room
|
||||
import im.vector.matrix.android.internal.database.mapper.RoomSummaryMapper
|
||||
import im.vector.matrix.android.internal.di.SessionDatabase
|
||||
import im.vector.matrix.android.internal.session.room.membership.DefaultMembershipService
|
||||
import im.vector.matrix.android.internal.session.room.membership.LoadRoomMembersTask
|
||||
import im.vector.matrix.android.internal.session.room.membership.joining.InviteTask
|
||||
|
@ -40,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.PaginationTask
|
||||
import im.vector.matrix.android.internal.task.TaskExecutor
|
||||
import io.realm.RealmConfiguration
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class RoomFactory @Inject constructor(private val context: Context,
|
||||
|
@ -63,7 +65,7 @@ internal class RoomFactory @Inject constructor(private val context: Context,
|
|||
fun create(roomId: String): Room {
|
||||
val timelineService = DefaultTimelineService(roomId, monarchy, taskExecutor, contextOfEventTask, cryptoService, paginationTask)
|
||||
val sendService = DefaultSendService(context, credentials, roomId, eventFactory, cryptoService, monarchy)
|
||||
val stateService = DefaultStateService(roomId, taskExecutor, sendStateTask)
|
||||
val stateService = DefaultStateService(roomId, monarchy.realmConfiguration, taskExecutor, sendStateTask)
|
||||
val roomMembersService = DefaultMembershipService(roomId, monarchy, taskExecutor, loadRoomMembersTask, inviteTask, joinRoomTask, leaveRoomTask)
|
||||
val readService = DefaultReadService(roomId, monarchy, taskExecutor, setReadMarkersTask, credentials)
|
||||
val relationService = DefaultRelationService(context,
|
||||
|
|
|
@ -17,7 +17,9 @@
|
|||
package im.vector.matrix.android.internal.session.room.create
|
||||
|
||||
import arrow.core.Try
|
||||
import arrow.core.recoverWith
|
||||
import com.zhuinden.monarchy.Monarchy
|
||||
import im.vector.matrix.android.api.session.room.failure.CreateRoomFailure
|
||||
import im.vector.matrix.android.api.session.room.model.create.CreateRoomParams
|
||||
import im.vector.matrix.android.api.session.room.model.create.CreateRoomResponse
|
||||
import im.vector.matrix.android.internal.database.RealmQueryLatch
|
||||
|
@ -57,9 +59,11 @@ internal class DefaultCreateRoomTask @Inject constructor(private val roomAPI: Ro
|
|||
realm.where(RoomEntity::class.java)
|
||||
.equalTo(RoomEntityFields.ROOM_ID, roomId)
|
||||
}
|
||||
Try {
|
||||
rql.await(timeout = 20L, timeUnit = TimeUnit.SECONDS)
|
||||
roomId
|
||||
try {
|
||||
rql.await(timeout = 1L, timeUnit = TimeUnit.MINUTES)
|
||||
Try.just(roomId)
|
||||
} catch (exception: Exception) {
|
||||
Try.raise<String>(CreateRoomFailure.CreatedWithTimeout)
|
||||
}
|
||||
}.flatMap { roomId ->
|
||||
if (params.isDirect()) {
|
||||
|
|
|
@ -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)
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -82,8 +82,8 @@ internal class DefaultMembershipService @Inject constructor(private val roomId:
|
|||
.executeBy(taskExecutor)
|
||||
}
|
||||
|
||||
override fun join(callback: MatrixCallback<Unit>): Cancelable {
|
||||
val params = JoinRoomTask.Params(roomId)
|
||||
override fun join(viaServers: List<String>, callback: MatrixCallback<Unit>): Cancelable {
|
||||
val params = JoinRoomTask.Params(roomId, viaServers)
|
||||
return joinTask.configureWith(params)
|
||||
.dispatchTo(callback)
|
||||
.executeBy(taskExecutor)
|
||||
|
|
|
@ -17,6 +17,8 @@
|
|||
package im.vector.matrix.android.internal.session.room.membership.joining
|
||||
|
||||
import arrow.core.Try
|
||||
import im.vector.matrix.android.api.session.room.failure.CreateRoomFailure
|
||||
import im.vector.matrix.android.api.session.room.failure.JoinRoomFailure
|
||||
import im.vector.matrix.android.internal.database.RealmQueryLatch
|
||||
import im.vector.matrix.android.internal.database.model.RoomEntity
|
||||
import im.vector.matrix.android.internal.database.model.RoomEntityFields
|
||||
|
@ -31,7 +33,8 @@ import javax.inject.Inject
|
|||
|
||||
internal interface JoinRoomTask : Task<JoinRoomTask.Params, Unit> {
|
||||
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> {
|
||||
return executeRequest<Unit> {
|
||||
apiCall = roomAPI.join(params.roomId, HashMap())
|
||||
apiCall = roomAPI.join(params.roomId, params.viaServers, HashMap())
|
||||
}.flatMap {
|
||||
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)
|
||||
|
@ -49,9 +52,11 @@ internal class DefaultJoinRoomTask @Inject constructor(private val roomAPI: Room
|
|||
realm.where(RoomEntity::class.java)
|
||||
.equalTo(RoomEntityFields.ROOM_ID, roomId)
|
||||
}
|
||||
Try {
|
||||
rql.await(20L, TimeUnit.SECONDS)
|
||||
roomId
|
||||
try {
|
||||
rql.await(timeout = 1L, timeUnit = TimeUnit.MINUTES)
|
||||
Try.just(roomId)
|
||||
} catch (exception: Exception) {
|
||||
Try.raise<String>(JoinRoomFailure.JoinedWithTimeout)
|
||||
}
|
||||
}.flatMap { roomId ->
|
||||
setReadMarkers(roomId)
|
||||
|
|
|
@ -17,16 +17,32 @@
|
|||
package im.vector.matrix.android.internal.session.room.state
|
||||
|
||||
import im.vector.matrix.android.api.MatrixCallback
|
||||
import im.vector.matrix.android.api.session.events.model.Event
|
||||
import im.vector.matrix.android.api.session.events.model.EventType
|
||||
import im.vector.matrix.android.api.session.room.state.StateService
|
||||
import im.vector.matrix.android.internal.database.mapper.asDomain
|
||||
import im.vector.matrix.android.internal.database.model.EventEntity
|
||||
import im.vector.matrix.android.internal.database.query.prev
|
||||
import im.vector.matrix.android.internal.database.query.where
|
||||
import im.vector.matrix.android.internal.di.SessionDatabase
|
||||
import im.vector.matrix.android.internal.task.TaskExecutor
|
||||
import im.vector.matrix.android.internal.task.configureWith
|
||||
import io.realm.Realm
|
||||
import io.realm.RealmConfiguration
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class DefaultStateService @Inject constructor(private val roomId: String,
|
||||
@SessionDatabase
|
||||
private val realmConfiguration: RealmConfiguration,
|
||||
private val taskExecutor: TaskExecutor,
|
||||
private val sendStateTask: SendStateTask) : StateService {
|
||||
|
||||
override fun getStateEvent(eventType: String): Event? {
|
||||
return Realm.getInstance(realmConfiguration).use { realm ->
|
||||
EventEntity.where(realm, roomId, eventType).prev()?.asDomain()
|
||||
}
|
||||
}
|
||||
|
||||
override fun updateTopic(topic: String, callback: MatrixCallback<Unit>) {
|
||||
val params = SendStateTask.Params(roomId,
|
||||
EventType.STATE_ROOM_TOPIC,
|
||||
|
@ -39,4 +55,6 @@ internal class DefaultStateService @Inject constructor(private val roomId: Strin
|
|||
.dispatchTo(callback)
|
||||
.executeBy(taskExecutor)
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -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)
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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.addOrUpdate
|
||||
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.mapper.asDomain
|
||||
import im.vector.matrix.android.internal.database.model.ChunkEntity
|
||||
|
@ -157,8 +158,7 @@ internal class RoomSyncHandler @Inject constructor(private val monarchy: Monarch
|
|||
roomEntity,
|
||||
roomSync.timeline.events,
|
||||
roomSync.timeline.prevToken,
|
||||
roomSync.timeline.limited,
|
||||
0
|
||||
roomSync.timeline.limited
|
||||
)
|
||||
roomEntity.addOrUpdate(chunkEntity)
|
||||
}
|
||||
|
@ -206,15 +206,18 @@ internal class RoomSyncHandler @Inject constructor(private val monarchy: Monarch
|
|||
roomEntity: RoomEntity,
|
||||
eventList: List<Event>,
|
||||
prevToken: String? = null,
|
||||
isLimited: Boolean = true,
|
||||
stateIndexOffset: Int = 0): ChunkEntity {
|
||||
isLimited: Boolean = true): ChunkEntity {
|
||||
|
||||
val lastChunk = ChunkEntity.findLastLiveChunkFromRoom(realm, roomEntity.roomId)
|
||||
var stateIndexOffset = 0
|
||||
val chunkEntity = if (!isLimited && lastChunk != null) {
|
||||
lastChunk
|
||||
} else {
|
||||
realm.createObject<ChunkEntity>().apply { this.prevToken = prevToken }
|
||||
}
|
||||
if (isLimited && lastChunk != null) {
|
||||
stateIndexOffset = lastChunk.lastStateIndex(PaginationDirection.FORWARDS)
|
||||
}
|
||||
lastChunk?.isLastForward = false
|
||||
chunkEntity.isLastForward = true
|
||||
|
||||
|
|
|
@ -31,6 +31,7 @@
|
|||
<string name="notice_room_visibility_world_readable">anyone.</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_room_update">%s upgraded this room.</string>
|
||||
|
||||
<string name="notice_requested_voip_conference">%1$s requested a VoIP conference</string>
|
||||
<string name="notice_voip_started">VoIP conference started</string>
|
||||
|
|
33
vector/src/debug/res/layout/view_notification_area.xml
Normal file
33
vector/src/debug/res/layout/view_notification_area.xml
Normal 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>
|
|
@ -30,12 +30,16 @@ class ErrorFormatter @Inject constructor(val stringProvider: StringProvider) {
|
|||
}
|
||||
|
||||
fun toHumanReadable(throwable: Throwable?): String {
|
||||
|
||||
return when (throwable) {
|
||||
null -> ""
|
||||
is Failure.NetworkConnection -> stringProvider.getString(R.string.error_no_network)
|
||||
is Failure.ServerError -> {
|
||||
throwable.error.message.takeIf { it.isNotEmpty() }
|
||||
?: throwable.error.code.takeIf { it.isNotEmpty() }
|
||||
?: stringProvider.getString(R.string.unknown_error)
|
||||
}
|
||||
else -> throwable.localizedMessage
|
||||
?: stringProvider.getString(R.string.unknown_error)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
|
@ -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>"
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -144,7 +144,6 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable {
|
|||
|
||||
override fun onNewIntent(intent: Intent?) {
|
||||
super.onNewIntent(intent)
|
||||
|
||||
if (intent?.hasExtra(EXTRA_CLEAR_EXISTING_NOTIFICATION) == true) {
|
||||
notificationDrawerManager.clearAllEvents()
|
||||
intent.removeExtra(EXTRA_CLEAR_EXISTING_NOTIFICATION)
|
||||
|
@ -193,7 +192,7 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable {
|
|||
bugReporter.openBugReportScreen(this, false)
|
||||
return true
|
||||
}
|
||||
R.id.menu_home_filter -> {
|
||||
R.id.menu_home_filter -> {
|
||||
navigator.openRoomsFiltering(this)
|
||||
return true
|
||||
}
|
||||
|
|
|
@ -29,6 +29,7 @@ import com.airbnb.mvrx.Fail
|
|||
import com.airbnb.mvrx.Loading
|
||||
import com.airbnb.mvrx.Success
|
||||
import com.airbnb.mvrx.viewModel
|
||||
import im.vector.matrix.android.api.session.room.failure.CreateRoomFailure
|
||||
import im.vector.riotx.R
|
||||
import im.vector.riotx.core.di.ScreenComponent
|
||||
import im.vector.riotx.core.error.ErrorFormatter
|
||||
|
@ -38,6 +39,7 @@ import im.vector.riotx.core.extensions.observeEvent
|
|||
import im.vector.riotx.core.platform.SimpleFragmentActivity
|
||||
import im.vector.riotx.core.platform.WaitingViewData
|
||||
import kotlinx.android.synthetic.main.activity.*
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
class CreateDirectRoomActivity : SimpleFragmentActivity() {
|
||||
|
@ -91,10 +93,13 @@ class CreateDirectRoomActivity : SimpleFragmentActivity() {
|
|||
|
||||
private fun renderCreationFailure(error: Throwable) {
|
||||
hideWaitingView()
|
||||
AlertDialog.Builder(this)
|
||||
.setMessage(errorFormatter.toHumanReadable(error))
|
||||
.setPositiveButton(R.string.ok) { dialog, id -> dialog.cancel() }
|
||||
.show()
|
||||
if (error is CreateRoomFailure.CreatedWithTimeout) {
|
||||
finish()
|
||||
} else
|
||||
AlertDialog.Builder(this)
|
||||
.setMessage(errorFormatter.toHumanReadable(error))
|
||||
.setPositiveButton(R.string.ok) { dialog, id -> dialog.cancel() }
|
||||
.show()
|
||||
}
|
||||
|
||||
private fun renderCreationSuccess(roomId: String?) {
|
||||
|
|
|
@ -17,6 +17,7 @@
|
|||
package im.vector.riotx.features.home.room.detail
|
||||
|
||||
import com.jaiselrahman.filepicker.model.MediaFile
|
||||
import im.vector.matrix.android.api.session.events.model.Event
|
||||
import im.vector.matrix.android.api.session.room.model.EditAggregatedSummary
|
||||
import im.vector.matrix.android.api.session.room.model.message.MessageFileContent
|
||||
import im.vector.matrix.android.api.session.room.timeline.Timeline
|
||||
|
@ -27,13 +28,14 @@ sealed class RoomDetailActions {
|
|||
data class SendMessage(val text: String, val autoMarkdown: Boolean) : RoomDetailActions()
|
||||
data class SendMedia(val mediaFiles: List<MediaFile>) : RoomDetailActions()
|
||||
data class EventDisplayed(val event: TimelineEvent) : RoomDetailActions()
|
||||
data class LoadMore(val direction: Timeline.Direction) : RoomDetailActions()
|
||||
data class LoadMoreTimelineEvents(val direction: Timeline.Direction) : RoomDetailActions()
|
||||
data class SendReaction(val reaction: String, val targetEventId: String) : RoomDetailActions()
|
||||
data class RedactAction(val targetEventId: String, val reason: String? = "") : RoomDetailActions()
|
||||
data class UndoReaction(val targetEventId: String, val key: String, val reason: String? = "") : RoomDetailActions()
|
||||
data class UpdateQuickReactAction(val targetEventId: String, val selectedReaction: String, val add: Boolean) : RoomDetailActions()
|
||||
data class NavigateToEvent(val eventId: String, val position: Int?) : RoomDetailActions()
|
||||
data class DownloadFile(val eventId: String, val messageFileContent: MessageFileContent) : RoomDetailActions()
|
||||
data class HandleTombstoneEvent(val event: Event): RoomDetailActions()
|
||||
object AcceptInvite : RoomDetailActions()
|
||||
object RejectInvite : RoomDetailActions()
|
||||
|
||||
|
|
|
@ -29,6 +29,7 @@ import im.vector.riotx.R
|
|||
import im.vector.riotx.core.extensions.replaceFragment
|
||||
import im.vector.riotx.core.platform.ToolbarConfigurable
|
||||
import im.vector.riotx.core.platform.VectorBaseActivity
|
||||
import kotlinx.android.synthetic.main.merge_overlay_waiting_view.*
|
||||
|
||||
class RoomDetailActivity : VectorBaseActivity(), ToolbarConfigurable {
|
||||
|
||||
|
@ -38,6 +39,7 @@ class RoomDetailActivity : VectorBaseActivity(), ToolbarConfigurable {
|
|||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
waitingView = waiting_view
|
||||
if (isFirstCreation()) {
|
||||
val roomDetailArgs: RoomDetailArgs = intent?.extras?.getParcelable(EXTRA_ROOM_DETAIL_ARGS)
|
||||
?: return
|
||||
|
|
|
@ -44,8 +44,7 @@ import androidx.recyclerview.widget.RecyclerView
|
|||
import butterknife.BindView
|
||||
import com.airbnb.epoxy.EpoxyModel
|
||||
import com.airbnb.epoxy.EpoxyVisibilityTracker
|
||||
import com.airbnb.mvrx.args
|
||||
import com.airbnb.mvrx.fragmentViewModel
|
||||
import com.airbnb.mvrx.*
|
||||
import com.github.piasy.biv.BigImageViewer
|
||||
import com.github.piasy.biv.loader.ImageLoader
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
|
@ -57,6 +56,7 @@ import com.otaliastudios.autocomplete.AutocompleteCallback
|
|||
import com.otaliastudios.autocomplete.CharPolicy
|
||||
import im.vector.matrix.android.api.permalinks.PermalinkFactory
|
||||
import im.vector.matrix.android.api.session.Session
|
||||
import im.vector.matrix.android.api.session.events.model.Event
|
||||
import im.vector.matrix.android.api.session.room.model.EditAggregatedSummary
|
||||
import im.vector.matrix.android.api.session.room.model.Membership
|
||||
import im.vector.matrix.android.api.session.room.model.message.*
|
||||
|
@ -75,6 +75,7 @@ import im.vector.riotx.core.extensions.observeEvent
|
|||
import im.vector.riotx.core.extensions.setTextOrHide
|
||||
import im.vector.riotx.core.files.addEntryToDownloadManager
|
||||
import im.vector.riotx.core.glide.GlideApp
|
||||
import im.vector.riotx.core.platform.NotificationAreaView
|
||||
import im.vector.riotx.core.platform.VectorBaseFragment
|
||||
import im.vector.riotx.core.utils.*
|
||||
import im.vector.riotx.features.autocomplete.command.AutocompleteCommandPresenter
|
||||
|
@ -107,6 +108,7 @@ import im.vector.riotx.features.themes.ThemeUtils
|
|||
import kotlinx.android.parcel.Parcelize
|
||||
import kotlinx.android.synthetic.main.fragment_room_detail.*
|
||||
import kotlinx.android.synthetic.main.merge_composer_layout.view.*
|
||||
import kotlinx.android.synthetic.main.merge_overlay_waiting_view.*
|
||||
import org.commonmark.parser.Parser
|
||||
import timber.log.Timber
|
||||
import java.io.File
|
||||
|
@ -204,6 +206,7 @@ class RoomDetailFragment :
|
|||
setupComposer()
|
||||
setupAttachmentButton()
|
||||
setupInviteView()
|
||||
setupNotificationView()
|
||||
roomDetailViewModel.subscribe { renderState(it) }
|
||||
textComposerViewModel.subscribe { renderTextComposerState(it) }
|
||||
roomDetailViewModel.sendMessageResultLiveData.observeEvent(this) { renderSendMessageResult(it) }
|
||||
|
@ -221,6 +224,10 @@ class RoomDetailFragment :
|
|||
scrollOnHighlightedEventCallback.scheduleScrollTo(it)
|
||||
}
|
||||
|
||||
roomDetailViewModel.selectSubscribe(this, RoomDetailViewState::tombstoneEventHandling, uniqueOnly("tombstoneEventHandling")) {
|
||||
renderTombstoneEventHandling(it)
|
||||
}
|
||||
|
||||
roomDetailViewModel.downloadedFileEvent.observeEvent(this) { downloadFileState ->
|
||||
if (downloadFileState.throwable != null) {
|
||||
requireActivity().toast(errorFormatter.toHumanReadable(downloadFileState.throwable))
|
||||
|
@ -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) {
|
||||
menu.forEach {
|
||||
it.isVisible = roomDetailViewModel.isMenuItemVisible(it.itemId)
|
||||
|
@ -266,7 +298,8 @@ class RoomDetailFragment :
|
|||
composerLayout.collapse()
|
||||
}
|
||||
|
||||
private fun enterSpecialMode(event: TimelineEvent, @DrawableRes iconRes: Int, useText: Boolean) {
|
||||
private fun enterSpecialMode(event: TimelineEvent, @DrawableRes
|
||||
iconRes: Int, useText: Boolean) {
|
||||
commandAutocompletePolicy.enabled = false
|
||||
//switch to expanded bar
|
||||
composerLayout.composerRelatedMessageTitle.apply {
|
||||
|
@ -280,17 +313,17 @@ class RoomDetailFragment :
|
|||
if (messageContent is MessageTextContent && messageContent.format == MessageType.FORMAT_MATRIX_HTML) {
|
||||
val parser = Parser.builder().build()
|
||||
val document = parser.parse(messageContent.formattedBody
|
||||
?: messageContent.body)
|
||||
?: messageContent.body)
|
||||
formattedBody = eventHtmlRenderer.render(document)
|
||||
}
|
||||
composerLayout.composerRelatedMessageContent.text = formattedBody
|
||||
?: nonFormattedBody
|
||||
?: nonFormattedBody
|
||||
|
||||
composerLayout.composerEditText.setText(if (useText) event.getTextEditableContent() else "")
|
||||
composerLayout.composerRelatedMessageActionIcon.setImageDrawable(ContextCompat.getDrawable(requireContext(), iconRes))
|
||||
|
||||
avatarRenderer.render(event.senderAvatar, event.root.senderId
|
||||
?: "", event.senderName, composerLayout.composerRelatedMessageAvatar)
|
||||
?: "", event.senderName, composerLayout.composerRelatedMessageAvatar)
|
||||
|
||||
composerLayout.composerEditText.setSelection(composerLayout.composerEditText.text.length)
|
||||
composerLayout.expand {
|
||||
|
@ -319,9 +352,9 @@ class RoomDetailFragment :
|
|||
REQUEST_FILES_REQUEST_CODE, TAKE_IMAGE_REQUEST_CODE -> handleMediaIntent(data)
|
||||
REACTION_SELECT_REQUEST_CODE -> {
|
||||
val eventId = data.getStringExtra(EmojiReactionPickerActivity.EXTRA_EVENT_ID)
|
||||
?: return
|
||||
?: return
|
||||
val reaction = data.getStringExtra(EmojiReactionPickerActivity.EXTRA_REACTION_RESULT)
|
||||
?: return
|
||||
?: return
|
||||
//TODO check if already reacted with that?
|
||||
roomDetailViewModel.process(RoomDetailActions.SendReaction(reaction, eventId))
|
||||
}
|
||||
|
@ -349,33 +382,33 @@ class RoomDetailFragment :
|
|||
|
||||
recyclerView.addOnScrollListener(
|
||||
EndlessRecyclerViewScrollListener(layoutManager, RoomDetailViewModel.PAGINATION_COUNT) { direction ->
|
||||
roomDetailViewModel.process(RoomDetailActions.LoadMore(direction))
|
||||
roomDetailViewModel.process(RoomDetailActions.LoadMoreTimelineEvents(direction))
|
||||
})
|
||||
recyclerView.setController(timelineEventController)
|
||||
timelineEventController.callback = this
|
||||
|
||||
if (VectorPreferences.swipeToReplyIsEnabled(requireContext())) {
|
||||
val swipeCallback = RoomMessageTouchHelperCallback(requireContext(),
|
||||
R.drawable.ic_reply,
|
||||
object : RoomMessageTouchHelperCallback.QuickReplayHandler {
|
||||
override fun performQuickReplyOnHolder(model: EpoxyModel<*>) {
|
||||
(model as? AbsMessageItem)?.informationData?.let {
|
||||
val eventId = it.eventId
|
||||
roomDetailViewModel.process(RoomDetailActions.EnterReplyMode(eventId))
|
||||
}
|
||||
}
|
||||
R.drawable.ic_reply,
|
||||
object : RoomMessageTouchHelperCallback.QuickReplayHandler {
|
||||
override fun performQuickReplyOnHolder(model: EpoxyModel<*>) {
|
||||
(model as? AbsMessageItem)?.informationData?.let {
|
||||
val eventId = it.eventId
|
||||
roomDetailViewModel.process(RoomDetailActions.EnterReplyMode(eventId))
|
||||
}
|
||||
}
|
||||
|
||||
override fun canSwipeModel(model: EpoxyModel<*>): Boolean {
|
||||
return when (model) {
|
||||
is MessageFileItem,
|
||||
is MessageImageVideoItem,
|
||||
is MessageTextItem -> {
|
||||
return (model as AbsMessageItem).informationData.sendState == SendState.SYNCED
|
||||
}
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
})
|
||||
override fun canSwipeModel(model: EpoxyModel<*>): Boolean {
|
||||
return when (model) {
|
||||
is MessageFileItem,
|
||||
is MessageImageVideoItem,
|
||||
is MessageTextItem -> {
|
||||
return (model as AbsMessageItem).informationData.sendState == SendState.SYNCED
|
||||
}
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
})
|
||||
val touchHelper = ItemTouchHelper(swipeCallback)
|
||||
touchHelper.attachToRecyclerView(recyclerView)
|
||||
}
|
||||
|
@ -541,7 +574,6 @@ class RoomDetailFragment :
|
|||
if (summary?.membership == Membership.JOIN) {
|
||||
timelineEventController.setTimeline(state.timeline, state.eventId)
|
||||
inviteView.visibility = View.GONE
|
||||
|
||||
val uid = session.myUserId
|
||||
val meMember = session.getRoom(state.roomId)?.getRoomMember(uid)
|
||||
avatarRenderer.render(meMember?.avatarUrl, uid, meMember?.displayName, composerLayout.composerAvatarImageView)
|
||||
|
@ -555,7 +587,14 @@ class RoomDetailFragment :
|
|||
} else if (state.asyncInviter.complete) {
|
||||
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) {
|
||||
|
@ -575,6 +614,26 @@ class RoomDetailFragment :
|
|||
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) {
|
||||
when (sendMessageResult) {
|
||||
is SendMessageResult.MessageSent,
|
||||
|
@ -608,7 +667,7 @@ class RoomDetailFragment :
|
|||
.show()
|
||||
}
|
||||
|
||||
// TimelineEventController.Callback ************************************************************
|
||||
// TimelineEventController.Callback ************************************************************
|
||||
|
||||
override fun onUrlClicked(url: String): Boolean {
|
||||
return permalinkHandler.launch(requireActivity(), url, object : NavigateToRoomInterceptor {
|
||||
|
@ -651,7 +710,7 @@ class RoomDetailFragment :
|
|||
val intent = ImageMediaViewerActivity.newIntent(vectorBaseActivity, mediaData, ViewCompat.getTransitionName(view))
|
||||
val bundle = ActivityOptionsCompat.makeSceneTransitionAnimation(
|
||||
requireActivity(), view, ViewCompat.getTransitionName(view)
|
||||
?: "").toBundle()
|
||||
?: "").toBundle()
|
||||
startActivity(intent, bundle)
|
||||
}
|
||||
|
||||
|
@ -731,6 +790,16 @@ class RoomDetailFragment :
|
|||
ViewEditHistoryBottomSheet.newInstance(roomDetailArgs.roomId, informationData)
|
||||
.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
|
||||
|
||||
override fun onQueryUsers(query: CharSequence?) {
|
||||
|
@ -745,7 +814,7 @@ class RoomDetailFragment :
|
|||
}
|
||||
MessageMenuViewModel.ACTION_VIEW_REACTIONS -> {
|
||||
val messageInformationData = actionData.data as? MessageInformationData
|
||||
?: return
|
||||
?: return
|
||||
ViewReactionBottomSheet.newInstance(roomDetailArgs.roomId, messageInformationData)
|
||||
.show(requireActivity().supportFragmentManager, "DISPLAY_REACTIONS")
|
||||
}
|
||||
|
@ -841,13 +910,13 @@ class RoomDetailFragment :
|
|||
}
|
||||
}
|
||||
|
||||
//utils
|
||||
//utils
|
||||
/**
|
||||
* Insert an user displayname in the message editor.
|
||||
*
|
||||
* @param text the text to insert.
|
||||
*/
|
||||
//TODO legacy, refactor
|
||||
//TODO legacy, refactor
|
||||
private fun insertUserDisplayNameInTextEditor(text: String?) {
|
||||
//TODO move logic outside of fragment
|
||||
if (null != text) {
|
||||
|
@ -898,7 +967,7 @@ class RoomDetailFragment :
|
|||
snack.show()
|
||||
}
|
||||
|
||||
// VectorInviteView.Callback
|
||||
// VectorInviteView.Callback
|
||||
|
||||
override fun onAcceptInvite() {
|
||||
notificationDrawerManager.clearMemberShipNotificationForRoom(roomDetailArgs.roomId)
|
||||
|
|
|
@ -29,8 +29,10 @@ import com.jakewharton.rxrelay2.BehaviorRelay
|
|||
import com.squareup.inject.assisted.Assisted
|
||||
import com.squareup.inject.assisted.AssistedInject
|
||||
import im.vector.matrix.android.api.MatrixCallback
|
||||
import im.vector.matrix.android.api.MatrixPatterns
|
||||
import im.vector.matrix.android.api.session.Session
|
||||
import im.vector.matrix.android.api.session.content.ContentAttachmentData
|
||||
import im.vector.matrix.android.api.session.events.model.EventType
|
||||
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.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.MessageType
|
||||
import im.vector.matrix.android.api.session.room.model.message.getFileUrl
|
||||
import im.vector.matrix.android.api.session.room.model.tombstone.RoomTombstoneContent
|
||||
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
|
||||
import im.vector.matrix.android.internal.crypto.attachments.toElementToDecrypt
|
||||
import im.vector.matrix.android.internal.crypto.model.event.EncryptedEventContent
|
||||
|
@ -100,7 +103,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
|
|||
init {
|
||||
observeRoomSummary()
|
||||
observeEventDisplayedActions()
|
||||
observeInvitationState()
|
||||
observeSummaryState()
|
||||
room.rx().loadRoomMembersIfNeeded().subscribeLogError().disposeOnClear()
|
||||
timeline.start()
|
||||
setState { copy(timeline = this@RoomDetailViewModel.timeline) }
|
||||
|
@ -111,7 +114,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
|
|||
is RoomDetailActions.SendMessage -> handleSendMessage(action)
|
||||
is RoomDetailActions.SendMedia -> handleSendMedia(action)
|
||||
is RoomDetailActions.EventDisplayed -> handleEventDisplayed(action)
|
||||
is RoomDetailActions.LoadMore -> handleLoadMore(action)
|
||||
is RoomDetailActions.LoadMoreTimelineEvents -> handleLoadMore(action)
|
||||
is RoomDetailActions.SendReaction -> handleSendReaction(action)
|
||||
is RoomDetailActions.AcceptInvite -> handleAcceptInvite()
|
||||
is RoomDetailActions.RejectInvite -> handleRejectInvite()
|
||||
|
@ -123,6 +126,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
|
|||
is RoomDetailActions.EnterReplyMode -> handleReplyAction(action)
|
||||
is RoomDetailActions.DownloadFile -> handleDownloadFile(action)
|
||||
is RoomDetailActions.NavigateToEvent -> handleNavigateToEvent(action)
|
||||
is RoomDetailActions.HandleTombstoneEvent -> handleTombstoneEvent(action)
|
||||
is RoomDetailActions.ResendMessage -> handleResendEvent(action)
|
||||
is RoomDetailActions.RemoveFailedEcho -> handleRemove(action)
|
||||
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) {
|
||||
setState {
|
||||
copy(
|
||||
|
@ -151,7 +181,6 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
|
|||
val nonBlockingPopAlert: LiveData<LiveEvent<Pair<Int, List<Any>>>>
|
||||
get() = _nonBlockingPopAlert
|
||||
|
||||
|
||||
private val _sendMessageResultLiveData = MutableLiveData<LiveEvent<SendMessageResult>>()
|
||||
val sendMessageResultLiveData: LiveData<LiveEvent<SendMessageResult>>
|
||||
get() = _sendMessageResultLiveData
|
||||
|
@ -263,12 +292,12 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
|
|||
} else {
|
||||
val messageContent: MessageContent? =
|
||||
state.sendMode.timelineEvent.annotations?.editSummary?.aggregatedContent.toModel()
|
||||
?: state.sendMode.timelineEvent.root.getClearContent().toModel()
|
||||
?: state.sendMode.timelineEvent.root.getClearContent().toModel()
|
||||
val existingBody = messageContent?.body ?: ""
|
||||
if (existingBody != action.text) {
|
||||
room.editTextMessage(state.sendMode.timelineEvent.root.eventId
|
||||
?: "", messageContent?.type
|
||||
?: MessageType.MSGTYPE_TEXT, action.text, action.autoMarkdown)
|
||||
?: "", messageContent?.type
|
||||
?: MessageType.MSGTYPE_TEXT, action.text, action.autoMarkdown)
|
||||
} else {
|
||||
Timber.w("Same message content, do not send edition")
|
||||
}
|
||||
|
@ -283,7 +312,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
|
|||
is SendMode.QUOTE -> {
|
||||
val messageContent: MessageContent? =
|
||||
state.sendMode.timelineEvent.annotations?.editSummary?.aggregatedContent.toModel()
|
||||
?: state.sendMode.timelineEvent.root.getClearContent().toModel()
|
||||
?: state.sendMode.timelineEvent.root.getClearContent().toModel()
|
||||
val textMsg = messageContent?.body
|
||||
|
||||
val finalText = legacyRiotQuoteText(textMsg, action.text)
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
|
@ -432,7 +461,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
|
|||
}
|
||||
|
||||
private fun handleAcceptInvite() {
|
||||
room.join(object : MatrixCallback<Unit> {})
|
||||
room.join(callback = object : MatrixCallback<Unit> {})
|
||||
}
|
||||
|
||||
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 ->
|
||||
if (summary.membership == Membership.INVITE) {
|
||||
summary.latestEvent?.root?.senderId?.let { senderId ->
|
||||
|
@ -620,6 +649,9 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
|
|||
setState { copy(asyncInviter = Success(it)) }
|
||||
}
|
||||
}
|
||||
room.getStateEvent(EventType.STATE_ROOM_TOMBSTONE)?.also {
|
||||
setState { copy(tombstoneEvent = it) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -19,7 +19,9 @@ package im.vector.riotx.features.home.room.detail
|
|||
import com.airbnb.mvrx.Async
|
||||
import com.airbnb.mvrx.MvRxState
|
||||
import com.airbnb.mvrx.Uninitialized
|
||||
import im.vector.matrix.android.api.session.events.model.Event
|
||||
import im.vector.matrix.android.api.session.room.model.RoomSummary
|
||||
import im.vector.matrix.android.api.session.room.model.tombstone.RoomTombstoneContent
|
||||
import im.vector.matrix.android.api.session.room.timeline.Timeline
|
||||
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
|
||||
import im.vector.matrix.android.api.session.user.model.User
|
||||
|
@ -46,7 +48,9 @@ data class RoomDetailViewState(
|
|||
val asyncInviter: Async<User> = Uninitialized,
|
||||
val asyncRoomSummary: Async<RoomSummary> = Uninitialized,
|
||||
val sendMode: SendMode = SendMode.REGULAR,
|
||||
val isEncrypted: Boolean = false
|
||||
val isEncrypted: Boolean = false,
|
||||
val tombstoneEvent: Event? = null,
|
||||
val tombstoneEventHandling: Async<String> = Uninitialized
|
||||
) : MvRxState {
|
||||
|
||||
constructor(args: RoomDetailArgs) : this(roomId = args.roomId, eventId = args.eventId)
|
||||
|
|
|
@ -54,6 +54,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Tim
|
|||
|
||||
interface Callback : ReactionPillCallback, AvatarCallback, BaseCallback, UrlClickCallback {
|
||||
fun onEventVisible(event: TimelineEvent)
|
||||
fun onRoomCreateLinkClicked(url: String)
|
||||
fun onEncryptedMessageClicked(informationData: MessageInformationData, view: View)
|
||||
fun onImageMessageClicked(messageImageContent: MessageImageContent, mediaData: ImageContentRenderer.Data, view: View)
|
||||
fun onVideoMessageClicked(messageVideoContent: MessageVideoContent, mediaData: VideoContentRenderer.Data, view: View)
|
||||
|
@ -158,7 +159,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Tim
|
|||
synchronized(modelCache) {
|
||||
for (i in 0 until modelCache.size) {
|
||||
if (modelCache[i]?.eventId == eventIdToHighlight
|
||||
|| modelCache[i]?.eventId == this.eventIdToHighlight) {
|
||||
|| modelCache[i]?.eventId == this.eventIdToHighlight) {
|
||||
modelCache[i] = null
|
||||
}
|
||||
}
|
||||
|
@ -219,8 +220,8 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Tim
|
|||
// Should be build if not cached or if cached but contains mergedHeader or formattedDay
|
||||
// We then are sure we always have items up to date.
|
||||
if (modelCache[position] == null
|
||||
|| modelCache[position]?.mergedHeaderModel != null
|
||||
|| modelCache[position]?.formattedDayModel != null) {
|
||||
|| modelCache[position]?.mergedHeaderModel != null
|
||||
|| modelCache[position]?.formattedDayModel != null) {
|
||||
modelCache[position] = buildItemModels(position, currentSnapshot)
|
||||
}
|
||||
}
|
||||
|
@ -294,7 +295,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Tim
|
|||
// => handle case where paginating from mergeable events and we get more
|
||||
val previousCollapseStateKey = mergedEventIds.intersect(mergeItemCollapseStates.keys).firstOrNull()
|
||||
val initialCollapseState = mergeItemCollapseStates.remove(previousCollapseStateKey)
|
||||
?: true
|
||||
?: true
|
||||
val isCollapsed = mergeItemCollapseStates.getOrPut(event.localId) { initialCollapseState }
|
||||
if (isCollapsed) {
|
||||
collapsedEventIds.addAll(mergedEventIds)
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -33,6 +33,7 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me
|
|||
private val encryptedItemFactory: EncryptedItemFactory,
|
||||
private val noticeItemFactory: NoticeItemFactory,
|
||||
private val defaultItemFactory: DefaultItemFactory,
|
||||
private val roomCreateItemFactory: RoomCreateItemFactory,
|
||||
private val avatarRenderer: AvatarRenderer) {
|
||||
|
||||
fun create(event: TimelineEvent,
|
||||
|
@ -45,6 +46,7 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me
|
|||
when (event.root.getClearType()) {
|
||||
EventType.MESSAGE -> messageItemFactory.create(event, nextEvent, highlight, callback)
|
||||
// State and call
|
||||
EventType.STATE_ROOM_TOMBSTONE,
|
||||
EventType.STATE_ROOM_NAME,
|
||||
EventType.STATE_ROOM_TOPIC,
|
||||
EventType.STATE_ROOM_MEMBER,
|
||||
|
@ -52,7 +54,8 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me
|
|||
EventType.CALL_INVITE,
|
||||
EventType.CALL_HANGUP,
|
||||
EventType.CALL_ANSWER -> noticeItemFactory.create(event, highlight, callback)
|
||||
|
||||
// State room create
|
||||
EventType.STATE_ROOM_CREATE -> roomCreateItemFactory.create(event, callback)
|
||||
// Crypto
|
||||
EventType.ENCRYPTION -> encryptionItemFactory.create(event, highlight, callback)
|
||||
EventType.ENCRYPTED -> {
|
||||
|
@ -66,8 +69,7 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me
|
|||
|
||||
// Unhandled event types (yet)
|
||||
EventType.STATE_ROOM_THIRD_PARTY_INVITE,
|
||||
EventType.STICKER,
|
||||
EventType.STATE_ROOM_CREATE -> defaultItemFactory.create(event, highlight)
|
||||
EventType.STICKER -> defaultItemFactory.create(event, highlight)
|
||||
|
||||
else -> {
|
||||
//These are just for debug to display hidden event, they should be filtered out in normal mode
|
||||
|
|
|
@ -22,6 +22,7 @@ import im.vector.matrix.android.api.session.events.model.EventType
|
|||
import im.vector.matrix.android.api.session.events.model.toModel
|
||||
import im.vector.matrix.android.api.session.room.model.*
|
||||
import im.vector.matrix.android.api.session.room.model.call.CallInviteContent
|
||||
import im.vector.matrix.android.api.session.room.model.tombstone.RoomTombstoneContent
|
||||
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
|
||||
import im.vector.riotx.R
|
||||
import im.vector.riotx.core.resources.StringProvider
|
||||
|
@ -37,6 +38,7 @@ class NoticeEventFormatter @Inject constructor(private val stringProvider: Strin
|
|||
EventType.STATE_ROOM_TOPIC -> formatRoomTopicEvent(timelineEvent.root, timelineEvent.getDisambiguatedDisplayName())
|
||||
EventType.STATE_ROOM_MEMBER -> formatRoomMemberEvent(timelineEvent.root, timelineEvent.senderName())
|
||||
EventType.STATE_HISTORY_VISIBILITY -> formatRoomHistoryVisibilityEvent(timelineEvent.root, timelineEvent.getDisambiguatedDisplayName())
|
||||
EventType.STATE_ROOM_TOMBSTONE -> formatRoomTombstoneEvent(timelineEvent.root, timelineEvent.getDisambiguatedDisplayName())
|
||||
EventType.CALL_INVITE,
|
||||
EventType.CALL_HANGUP,
|
||||
EventType.CALL_ANSWER -> formatCallEvent(timelineEvent.root, timelineEvent.getDisambiguatedDisplayName())
|
||||
|
@ -56,6 +58,7 @@ class NoticeEventFormatter @Inject constructor(private val stringProvider: Strin
|
|||
EventType.CALL_INVITE,
|
||||
EventType.CALL_HANGUP,
|
||||
EventType.CALL_ANSWER -> formatCallEvent(event, senderName)
|
||||
EventType.STATE_ROOM_TOMBSTONE -> formatRoomTombstoneEvent(event, senderName)
|
||||
else -> {
|
||||
Timber.v("Type $type not handled by this formatter")
|
||||
null
|
||||
|
@ -72,6 +75,10 @@ class NoticeEventFormatter @Inject constructor(private val stringProvider: Strin
|
|||
}
|
||||
}
|
||||
|
||||
private fun formatRoomTombstoneEvent(event: Event, senderName: String?): CharSequence? {
|
||||
return stringProvider.getString(R.string.notice_room_update, senderName)
|
||||
}
|
||||
|
||||
private fun formatRoomTopicEvent(event: Event, senderName: String?): CharSequence? {
|
||||
val content = event.getClearContent().toModel<RoomTopicContent>() ?: return null
|
||||
return if (content.topic.isNullOrEmpty()) {
|
||||
|
|
|
@ -39,7 +39,8 @@ object TimelineDisplayableEvents {
|
|||
EventType.ENCRYPTION,
|
||||
EventType.STATE_ROOM_THIRD_PARTY_INVITE,
|
||||
EventType.STICKER,
|
||||
EventType.STATE_ROOM_CREATE
|
||||
EventType.STATE_ROOM_CREATE,
|
||||
EventType.STATE_ROOM_TOMBSTONE
|
||||
)
|
||||
|
||||
val DEBUG_DISPLAYABLE_TYPES = DISPLAYABLE_TYPES + listOf(
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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) {
|
||||
// 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
|
||||
|
|
|
@ -75,7 +75,7 @@ class NotificationBroadcastReceiver : BroadcastReceiver() {
|
|||
private fun handleJoinRoom(roomId: String) {
|
||||
activeSessionHolder.getSafeActiveSession()?.let { session ->
|
||||
session.getRoom(roomId)
|
||||
?.join(object : MatrixCallback<Unit> {})
|
||||
?.join(emptyList(), object : MatrixCallback<Unit> {})
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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) {
|
||||
// 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
|
||||
|
|
|
@ -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) {
|
||||
// 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
|
||||
|
|
|
@ -1,8 +1,14 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/roomDetailContainer"
|
||||
android:layout_width="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>
|
|
@ -74,12 +74,18 @@
|
|||
android:id="@+id/recyclerView"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
app:layout_constraintBottom_toTopOf="@+id/composerLayout"
|
||||
app:layout_constraintBottom_toTopOf="@+id/recyclerViewBarrier"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/roomToolbar"
|
||||
tools:listitem="@layout/item_timeline_event_base" />
|
||||
|
||||
<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
|
||||
android:id="@+id/composerLayout"
|
||||
|
@ -89,6 +95,16 @@
|
|||
app:layout_constraintEnd_toEndOf="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
|
||||
android:id="@+id/inviteView"
|
||||
android:layout_width="0dp"
|
||||
|
@ -101,4 +117,5 @@
|
|||
app:layout_constraintTop_toBottomOf="@+id/roomToolbar"
|
||||
tools:visibility="visible" />
|
||||
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
23
vector/src/main/res/layout/item_timeline_event_create.xml
Normal file
23
vector/src/main/res/layout/item_timeline_event_create.xml
Normal 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>
|
|
@ -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_start_search">"Start typing to get results"</string>
|
||||
<string name="direct_room_filter_hint">"Filter by username or ID…"</string>
|
||||
|
||||
<string name="joining_room">"Joining room…"</string>
|
||||
</resources>
|
Loading…
Reference in a new issue