Merge pull request #1665 from vector-im/feature/fix_small_issues

Feature/fix small issues
This commit is contained in:
Benoit Marty 2020-07-11 21:58:11 +02:00 committed by GitHub
commit 9c402d4d40
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
93 changed files with 643 additions and 884 deletions

View file

@ -13,6 +13,11 @@ Improvements 🙌:
- Set up SSSS from security settings (#1567)
Bugfix 🐛:
- Integration Manager: Wrong URL to review terms if URL in config contains path (#1606)
- Regression Composer does not grow, crops out text (#1650)
- Bug / Unwanted draft (#698)
- All users seems to be able to see the enable encryption option in room settings (#1341)
- Leave room only leaves the current version (#1656)
- Regression | Share action menu do not work (#1647)
- verification issues on transition (#1555)
- Fix issue when restoring keys backup using recovery key

View file

@ -30,6 +30,7 @@ import im.vector.matrix.android.api.session.group.model.GroupSummary
import im.vector.matrix.android.api.session.identity.ThreePid
import im.vector.matrix.android.api.session.pushers.Pusher
import im.vector.matrix.android.api.session.room.RoomSummaryQueryParams
import im.vector.matrix.android.api.session.room.members.ChangeMembershipState
import im.vector.matrix.android.api.session.room.model.RoomSummary
import im.vector.matrix.android.api.session.room.model.create.CreateRoomParams
import im.vector.matrix.android.api.session.sync.SyncState
@ -173,6 +174,10 @@ class RxSession(private val session: Session) {
}
}
fun liveRoomChangeMembershipState(): Observable<Map<String, ChangeMembershipState>> {
return session.getChangeMembershipsLive().asObservable()
}
fun liveSecretSynchronisationInfo(): Observable<SecretsSynchronisationInfo> {
return Observable.combineLatest<List<UserAccountData>, Optional<MXCrossSigningInfo>, Optional<PrivateKeysInfo>, SecretsSynchronisationInfo>(
liveAccountData(setOf(MASTER_KEY_SSSS_NAME, USER_SIGNING_KEY_SSSS_NAME, SELF_SIGNING_KEY_SSSS_NAME, KEYBACKUP_SECRET_SSSS_NAME)),

View file

@ -39,5 +39,10 @@ data class UnsignedData(
* Optional. The previous content for this event. If there is no previous content, this key will be missing.
*/
@Json(name = "prev_content") val prevContent: Map<String, Any>? = null,
@Json(name = "m.relations") val relations: AggregatedRelations? = null
@Json(name = "m.relations") val relations: AggregatedRelations? = null,
/**
* Optional. The eventId of the previous state event being replaced.
*/
@Json(name = "replaces_state") val replacesState: String? = null
)

View file

@ -34,13 +34,6 @@ interface RoomDirectoryService {
publicRoomsParams: PublicRoomsParams,
callback: MatrixCallback<PublicRoomsResponse>): Cancelable
/**
* Join a room by id, or room alias
*/
fun joinRoom(roomIdOrAlias: String,
reason: String? = null,
callback: MatrixCallback<Unit>): Cancelable
/**
* Fetches the overall metadata about protocols supported by the homeserver.
* Includes both the available protocols and all fields required for queries against each protocol.

View file

@ -18,6 +18,7 @@ package im.vector.matrix.android.api.session.room
import androidx.lifecycle.LiveData
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.session.room.members.ChangeMembershipState
import im.vector.matrix.android.api.session.room.model.RoomSummary
import im.vector.matrix.android.api.session.room.model.create.CreateRoomParams
import im.vector.matrix.android.api.util.Cancelable
@ -104,5 +105,13 @@ interface RoomService {
searchOnServer: Boolean,
callback: MatrixCallback<Optional<String>>): Cancelable
/**
* Return a live data of all local changes membership that happened since the session has been opened.
* It allows you to track this in your client to known what is currently being processed by the SDK.
* It won't know anything about change being done in other client.
* Keys are roomId or roomAlias, depending of what you used as parameter for the join/leave action
*/
fun getChangeMembershipsLive(): LiveData<Map<String, ChangeMembershipState>>
fun getExistingDirectRoomWithUser(otherUserId: String) : Room?
}

View file

@ -28,6 +28,7 @@ fun roomSummaryQueryParams(init: (RoomSummaryQueryParams.Builder.() -> Unit) = {
* [im.vector.matrix.android.api.session.room.Room] and [im.vector.matrix.android.api.session.room.RoomService]
*/
data class RoomSummaryQueryParams(
val roomId: QueryStringValue,
val displayName: QueryStringValue,
val canonicalAlias: QueryStringValue,
val memberships: List<Membership>
@ -35,11 +36,13 @@ data class RoomSummaryQueryParams(
class Builder {
var roomId: QueryStringValue = QueryStringValue.IsNotEmpty
var displayName: QueryStringValue = QueryStringValue.IsNotEmpty
var canonicalAlias: QueryStringValue = QueryStringValue.NoCondition
var memberships: List<Membership> = Membership.all()
fun build() = RoomSummaryQueryParams(
roomId = roomId,
displayName = displayName,
canonicalAlias = canonicalAlias,
memberships = memberships

View file

@ -0,0 +1,33 @@
/*
* Copyright (c) 2020 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.members
sealed class ChangeMembershipState() {
object Unknown : ChangeMembershipState()
object Joining : ChangeMembershipState()
data class FailedJoining(val throwable: Throwable) : ChangeMembershipState()
object Joined : ChangeMembershipState()
object Leaving : ChangeMembershipState()
data class FailedLeaving(val throwable: Throwable) : ChangeMembershipState()
object Left : ChangeMembershipState()
fun isInProgress() = this is Joining || this is Leaving
fun isSuccessful() = this is Joined || this is Left
fun isFailed() = this is FailedJoining || this is FailedLeaving
}

View file

@ -17,7 +17,6 @@
package im.vector.matrix.android.api.session.room.powerlevels
import im.vector.matrix.android.api.session.events.model.EventType
import im.vector.matrix.android.api.session.room.model.PowerLevelsContent
/**
@ -124,59 +123,4 @@ class PowerLevelsHelper(private val powerLevelsContent: PowerLevelsContent) {
else -> Role.Moderator.value
}
}
/**
* Check if user have the necessary power level to change room name
* @param userId the id of the user to check for.
* @return true if able to change room name
*/
fun isUserAbleToChangeRoomName(userId: String): Boolean {
val powerLevel = getUserPowerLevelValue(userId)
val minPowerLevel = powerLevelsContent.events[EventType.STATE_ROOM_NAME] ?: powerLevelsContent.stateDefault
return powerLevel >= minPowerLevel
}
/**
* Check if user have the necessary power level to change room topic
* @param userId the id of the user to check for.
* @return true if able to change room topic
*/
fun isUserAbleToChangeRoomTopic(userId: String): Boolean {
val powerLevel = getUserPowerLevelValue(userId)
val minPowerLevel = powerLevelsContent.events[EventType.STATE_ROOM_TOPIC] ?: powerLevelsContent.stateDefault
return powerLevel >= minPowerLevel
}
/**
* Check if user have the necessary power level to change room canonical alias
* @param userId the id of the user to check for.
* @return true if able to change room canonical alias
*/
fun isUserAbleToChangeRoomCanonicalAlias(userId: String): Boolean {
val powerLevel = getUserPowerLevelValue(userId)
val minPowerLevel = powerLevelsContent.events[EventType.STATE_ROOM_CANONICAL_ALIAS] ?: powerLevelsContent.stateDefault
return powerLevel >= minPowerLevel
}
/**
* Check if user have the necessary power level to change room history readability
* @param userId the id of the user to check for.
* @return true if able to change room history readability
*/
fun isUserAbleToChangeRoomHistoryReadability(userId: String): Boolean {
val powerLevel = getUserPowerLevelValue(userId)
val minPowerLevel = powerLevelsContent.events[EventType.STATE_ROOM_HISTORY_VISIBILITY] ?: powerLevelsContent.stateDefault
return powerLevel >= minPowerLevel
}
/**
* Check if user have the necessary power level to change room avatar
* @param userId the id of the user to check for.
* @return true if able to change room avatar
*/
fun isUserAbleToChangeRoomAvatar(userId: String): Boolean {
val powerLevel = getUserPowerLevelValue(userId)
val minPowerLevel = powerLevelsContent.events[EventType.STATE_ROOM_AVATAR] ?: powerLevelsContent.stateDefault
return powerLevel >= minPowerLevel
}
}

View file

@ -72,7 +72,7 @@ internal class EventInsertLiveObserver @Inject constructor(@SessionDatabase real
it.process(realm, domainEvent)
}
}
realm.where(EventInsertEntity::class.java).findAll().deleteAllFromRealm()
realm.delete(EventInsertEntity::class.java)
}
}
}
@ -88,8 +88,8 @@ internal class EventInsertLiveObserver @Inject constructor(@SessionDatabase real
forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain
)
} catch (e: MXCryptoError) {
Timber.v("Call service: Failed to decrypt event")
// TODO -> we should keep track of this and retry, or aggregation will be broken
Timber.v("Failed to decrypt event")
// TODO -> we should keep track of this and retry, or some processing will never be handled
}
}
}

View file

@ -123,17 +123,18 @@ private fun computeIsUnique(
realm: Realm,
roomId: String,
isLastForward: Boolean,
myRoomMemberContent: RoomMemberContent,
senderRoomMemberContent: RoomMemberContent,
roomMemberContentsByUser: Map<String, RoomMemberContent?>
): Boolean {
val isHistoricalUnique = roomMemberContentsByUser.values.find {
it != myRoomMemberContent && it?.displayName == myRoomMemberContent.displayName
it != senderRoomMemberContent && it?.displayName == senderRoomMemberContent.displayName
} == null
return if (isLastForward) {
val isLiveUnique = RoomMemberSummaryEntity
.where(realm, roomId)
.equalTo(RoomMemberSummaryEntityFields.DISPLAY_NAME, myRoomMemberContent.displayName)
.findAll().none {
.equalTo(RoomMemberSummaryEntityFields.DISPLAY_NAME, senderRoomMemberContent.displayName)
.findAll()
.none {
!roomMemberContentsByUser.containsKey(it.userId)
}
isHistoricalUnique && isLiveUnique

View file

@ -24,7 +24,7 @@ import io.realm.annotations.PrimaryKey
internal open class RoomMemberSummaryEntity(@PrimaryKey var primaryKey: String = "",
@Index var userId: String = "",
@Index var roomId: String = "",
var displayName: String? = null,
@Index var displayName: String? = null,
var avatarUrl: String? = null,
var reason: String? = null,
var isDirect: Boolean = false

View file

@ -24,13 +24,11 @@ import im.vector.matrix.android.api.session.room.model.thirdparty.ThirdPartyProt
import im.vector.matrix.android.api.util.Cancelable
import im.vector.matrix.android.internal.session.room.directory.GetPublicRoomTask
import im.vector.matrix.android.internal.session.room.directory.GetThirdPartyProtocolsTask
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 javax.inject.Inject
internal class DefaultRoomDirectoryService @Inject constructor(private val getPublicRoomTask: GetPublicRoomTask,
private val joinRoomTask: JoinRoomTask,
private val getThirdPartyProtocolsTask: GetThirdPartyProtocolsTask,
private val taskExecutor: TaskExecutor) : RoomDirectoryService {
@ -44,14 +42,6 @@ internal class DefaultRoomDirectoryService @Inject constructor(private val getPu
.executeBy(taskExecutor)
}
override fun joinRoom(roomIdOrAlias: String, reason: String?, callback: MatrixCallback<Unit>): Cancelable {
return joinRoomTask
.configureWith(JoinRoomTask.Params(roomIdOrAlias, reason)) {
this.callback = callback
}
.executeBy(taskExecutor)
}
override fun getThirdPartyProtocol(callback: MatrixCallback<Map<String, ThirdPartyProtocol>>): Cancelable {
return getThirdPartyProtocolsTask
.configureWith {

View file

@ -21,12 +21,14 @@ 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.RoomSummaryQueryParams
import im.vector.matrix.android.api.session.room.members.ChangeMembershipState
import im.vector.matrix.android.api.session.room.model.RoomSummary
import im.vector.matrix.android.api.session.room.model.create.CreateRoomParams
import im.vector.matrix.android.api.util.Cancelable
import im.vector.matrix.android.api.util.Optional
import im.vector.matrix.android.internal.session.room.alias.GetRoomIdByAliasTask
import im.vector.matrix.android.internal.session.room.create.CreateRoomTask
import im.vector.matrix.android.internal.session.room.membership.RoomChangeMembershipStateDataSource
import im.vector.matrix.android.internal.session.room.membership.joining.JoinRoomTask
import im.vector.matrix.android.internal.session.room.read.MarkAllRoomsReadTask
import im.vector.matrix.android.internal.session.room.summary.RoomSummaryDataSource
@ -43,6 +45,7 @@ internal class DefaultRoomService @Inject constructor(
private val roomIdByAliasTask: GetRoomIdByAliasTask,
private val roomGetter: RoomGetter,
private val roomSummaryDataSource: RoomSummaryDataSource,
private val roomChangeMembershipStateDataSource: RoomChangeMembershipStateDataSource,
private val taskExecutor: TaskExecutor
) : RoomService {
@ -111,4 +114,8 @@ internal class DefaultRoomService @Inject constructor(
}
.executeBy(taskExecutor)
}
override fun getChangeMembershipsLive(): LiveData<Map<String, ChangeMembershipState>> {
return roomChangeMembershipStateDataSource.getLiveStates()
}
}

View file

@ -109,7 +109,7 @@ internal class EventRelationsAggregationProcessor @Inject constructor(@UserId pr
return
}
val isLocalEcho = LocalEcho.isLocalEchoId(event.eventId ?: "")
when (event.getClearType()) {
when (event.type) {
EventType.REACTION -> {
// we got a reaction!!
Timber.v("###REACTION in room $roomId , reaction eventID ${event.eventId}")
@ -161,7 +161,6 @@ internal class EventRelationsAggregationProcessor @Inject constructor(@UserId pr
if (encryptedEventContent?.relatesTo?.type == RelationType.REPLACE
|| encryptedEventContent?.relatesTo?.type == RelationType.RESPONSE
) {
// we need to decrypt if needed
event.getClearContent().toModel<MessageContent>()?.let {
if (encryptedEventContent.relatesTo.type == RelationType.REPLACE) {
Timber.v("###REPLACE in room $roomId for event ${event.eventId}")

View file

@ -0,0 +1,67 @@
/*
* Copyright (c) 2020 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.membership
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import im.vector.matrix.android.api.session.room.members.ChangeMembershipState
import im.vector.matrix.android.api.session.room.model.Membership
import im.vector.matrix.android.internal.session.SessionScope
import javax.inject.Inject
/**
* This class holds information about rooms that current user is joining or leaving.
*/
@SessionScope
internal class RoomChangeMembershipStateDataSource @Inject constructor() {
private val mutableLiveStates = MutableLiveData<Map<String, ChangeMembershipState>>(emptyMap())
private val states = HashMap<String, ChangeMembershipState>()
/**
* This will update local states to be synced with the server.
*/
fun setMembershipFromSync(roomId: String, membership: Membership) {
if (states.containsKey(roomId)) {
val newState = membership.toMembershipChangeState()
updateState(roomId, newState)
}
}
fun updateState(roomId: String, state: ChangeMembershipState) {
states[roomId] = state
mutableLiveStates.postValue(states.toMap())
}
fun getLiveStates(): LiveData<Map<String, ChangeMembershipState>> {
return mutableLiveStates
}
fun getState(roomId: String): ChangeMembershipState {
return states.getOrElse(roomId) {
ChangeMembershipState.Unknown
}
}
private fun Membership.toMembershipChangeState(): ChangeMembershipState {
return when {
this == Membership.JOIN -> ChangeMembershipState.Joined
this.isLeft() -> ChangeMembershipState.Left
else -> ChangeMembershipState.Unknown
}
}
}

View file

@ -30,8 +30,15 @@ internal class RoomMemberEventHandler @Inject constructor() {
if (event.type != EventType.STATE_ROOM_MEMBER) {
return false
}
val roomMember = event.content.toModel<RoomMemberContent>() ?: return false
val userId = event.stateKey ?: return false
val roomMember = event.content.toModel<RoomMemberContent>()
return handle(realm, roomId, userId, roomMember)
}
fun handle(realm: Realm, roomId: String, userId: String, roomMember: RoomMemberContent?): Boolean {
if (roomMember == null) {
return false
}
val roomMemberEntity = RoomMemberEntityFactory.create(roomId, userId, roomMember)
realm.insertOrUpdate(roomMemberEntity)
if (roomMember.membership.isActive()) {

View file

@ -17,6 +17,7 @@
package im.vector.matrix.android.internal.session.room.membership.joining
import im.vector.matrix.android.api.session.room.failure.JoinRoomFailure
import im.vector.matrix.android.api.session.room.members.ChangeMembershipState
import im.vector.matrix.android.api.session.room.model.create.JoinRoomResponse
import im.vector.matrix.android.internal.database.awaitNotEmptyResult
import im.vector.matrix.android.internal.database.model.RoomEntity
@ -24,6 +25,7 @@ import im.vector.matrix.android.internal.database.model.RoomEntityFields
import im.vector.matrix.android.internal.di.SessionDatabase
import im.vector.matrix.android.internal.network.executeRequest
import im.vector.matrix.android.internal.session.room.RoomAPI
import im.vector.matrix.android.internal.session.room.membership.RoomChangeMembershipStateDataSource
import im.vector.matrix.android.internal.session.room.read.SetReadMarkersTask
import im.vector.matrix.android.internal.task.Task
import io.realm.RealmConfiguration
@ -45,12 +47,19 @@ internal class DefaultJoinRoomTask @Inject constructor(
private val readMarkersTask: SetReadMarkersTask,
@SessionDatabase
private val realmConfiguration: RealmConfiguration,
private val roomChangeMembershipStateDataSource: RoomChangeMembershipStateDataSource,
private val eventBus: EventBus
) : JoinRoomTask {
override suspend fun execute(params: JoinRoomTask.Params) {
val joinRoomResponse = executeRequest<JoinRoomResponse>(eventBus) {
apiCall = roomAPI.join(params.roomIdOrAlias, params.viaServers, mapOf("reason" to params.reason))
roomChangeMembershipStateDataSource.updateState(params.roomIdOrAlias, ChangeMembershipState.Joining)
val joinRoomResponse = try {
executeRequest<JoinRoomResponse>(eventBus) {
apiCall = roomAPI.join(params.roomIdOrAlias, params.viaServers, mapOf("reason" to params.reason))
}
} catch (failure: Throwable) {
roomChangeMembershipStateDataSource.updateState(params.roomIdOrAlias, ChangeMembershipState.FailedJoining(failure))
throw failure
}
// Wait for room to come back from the sync (but it can maybe be in the DB is the sync response is received before)
val roomId = joinRoomResponse.roomId

View file

@ -16,10 +16,19 @@
package im.vector.matrix.android.internal.session.room.membership.leaving
import im.vector.matrix.android.api.query.QueryStringValue
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.members.ChangeMembershipState
import im.vector.matrix.android.api.session.room.model.create.RoomCreateContent
import im.vector.matrix.android.internal.network.executeRequest
import im.vector.matrix.android.internal.session.room.RoomAPI
import im.vector.matrix.android.internal.session.room.membership.RoomChangeMembershipStateDataSource
import im.vector.matrix.android.internal.session.room.state.StateEventDataSource
import im.vector.matrix.android.internal.session.room.summary.RoomSummaryDataSource
import im.vector.matrix.android.internal.task.Task
import org.greenrobot.eventbus.EventBus
import timber.log.Timber
import javax.inject.Inject
internal interface LeaveRoomTask : Task<LeaveRoomTask.Params, Unit> {
@ -31,12 +40,40 @@ internal interface LeaveRoomTask : Task<LeaveRoomTask.Params, Unit> {
internal class DefaultLeaveRoomTask @Inject constructor(
private val roomAPI: RoomAPI,
private val eventBus: EventBus
private val eventBus: EventBus,
private val stateEventDataSource: StateEventDataSource,
private val roomSummaryDataSource: RoomSummaryDataSource,
private val roomChangeMembershipStateDataSource: RoomChangeMembershipStateDataSource
) : LeaveRoomTask {
override suspend fun execute(params: LeaveRoomTask.Params) {
return executeRequest(eventBus) {
apiCall = roomAPI.leave(params.roomId, mapOf("reason" to params.reason))
leaveRoom(params.roomId, params.reason)
}
private suspend fun leaveRoom(roomId: String, reason: String?) {
val roomSummary = roomSummaryDataSource.getRoomSummary(roomId)
if (roomSummary?.membership?.isActive() == false) {
Timber.v("Room $roomId is not joined so can't be left")
return
}
roomChangeMembershipStateDataSource.updateState(roomId, ChangeMembershipState.Leaving)
val roomCreateStateEvent = stateEventDataSource.getStateEvent(
roomId = roomId,
eventType = EventType.STATE_ROOM_CREATE,
stateKey = QueryStringValue.NoCondition
)
// Server is not cleaning predecessor rooms, so we also try to left them
val predecessorRoomId = roomCreateStateEvent?.getClearContent()?.toModel<RoomCreateContent>()?.predecessor?.roomId
if (predecessorRoomId != null) {
leaveRoom(predecessorRoomId, reason)
}
try {
executeRequest<Unit>(eventBus) {
apiCall = roomAPI.leave(roomId, mapOf("reason" to reason))
}
} catch (failure: Throwable) {
roomChangeMembershipStateDataSource.updateState(roomId, ChangeMembershipState.FailedLeaving(failure))
throw failure
}
}
}

View file

@ -100,6 +100,7 @@ internal class RoomSummaryDataSource @Inject constructor(@SessionDatabase privat
private fun roomSummariesQuery(realm: Realm, queryParams: RoomSummaryQueryParams): RealmQuery<RoomSummaryEntity> {
val query = RoomSummaryEntity.where(realm)
query.process(RoomSummaryEntityFields.ROOM_ID, queryParams.roomId)
query.process(RoomSummaryEntityFields.DISPLAY_NAME, queryParams.displayName)
query.process(RoomSummaryEntityFields.CANONICAL_ALIAS, queryParams.canonicalAlias)
query.process(RoomSummaryEntityFields.MEMBERSHIP_STR, queryParams.memberships)

View file

@ -349,7 +349,7 @@ internal class DefaultTimeline(
updateState(Timeline.Direction.FORWARDS) {
it.copy(
hasMoreInCache = firstBuiltEvent == null || firstBuiltEvent.displayIndex < firstCacheEvent?.displayIndex ?: Int.MIN_VALUE,
hasMoreInCache = firstBuiltEvent != null && firstBuiltEvent.displayIndex < firstCacheEvent?.displayIndex ?: Int.MIN_VALUE,
hasReachedEnd = chunkEntity?.isLastForward ?: false
)
}
@ -369,6 +369,9 @@ internal class DefaultTimeline(
private fun paginateInternal(startDisplayIndex: Int?,
direction: Timeline.Direction,
count: Int): Boolean {
if (count == 0) {
return false
}
updateState(direction) { it.copy(requestedPaginationCount = count, isPaginating = true) }
val builtCount = buildTimelineEvents(startDisplayIndex, direction, count.toLong())
val shouldFetchMore = builtCount < count && !hasReachedEnd(direction)

View file

@ -241,12 +241,13 @@ internal class TokenChunkEventPersistor @Inject constructor(@SessionDatabase pri
chunksToDelete.add(it)
}
}
val shouldUpdateSummary = chunksToDelete.isNotEmpty() && currentChunk.isLastForward && direction == PaginationDirection.FORWARDS
chunksToDelete.forEach {
it.deleteOnCascade()
}
val roomSummaryEntity = RoomSummaryEntity.getOrCreate(realm, roomId)
val shouldUpdateSummary = roomSummaryEntity.latestPreviewableEvent == null
|| (chunksToDelete.isNotEmpty() && currentChunk.isLastForward && direction == PaginationDirection.FORWARDS)
if (shouldUpdateSummary) {
val roomSummaryEntity = RoomSummaryEntity.getOrCreate(realm, roomId)
val latestPreviewableEvent = TimelineEventEntity.latestEvent(
realm,
roomId,

View file

@ -31,7 +31,7 @@ import im.vector.matrix.android.internal.crypto.algorithms.olm.OlmDecryptionResu
import im.vector.matrix.android.internal.database.helper.addOrUpdate
import im.vector.matrix.android.internal.database.helper.addTimelineEvent
import im.vector.matrix.android.internal.database.helper.deleteOnCascade
import im.vector.matrix.android.internal.database.mapper.ContentMapper
import im.vector.matrix.android.internal.database.mapper.asDomain
import im.vector.matrix.android.internal.database.mapper.toEntity
import im.vector.matrix.android.internal.database.model.ChunkEntity
import im.vector.matrix.android.internal.database.model.CurrentStateEventEntity
@ -48,6 +48,7 @@ import im.vector.matrix.android.internal.di.MoshiProvider
import im.vector.matrix.android.internal.di.UserId
import im.vector.matrix.android.internal.session.DefaultInitialSyncProgressService
import im.vector.matrix.android.internal.session.mapWithProgress
import im.vector.matrix.android.internal.session.room.membership.RoomChangeMembershipStateDataSource
import im.vector.matrix.android.internal.session.room.membership.RoomMemberEventHandler
import im.vector.matrix.android.internal.session.room.read.FullyReadContent
import im.vector.matrix.android.internal.session.room.summary.RoomSummaryUpdater
@ -73,6 +74,7 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
private val cryptoService: DefaultCryptoService,
private val roomMemberEventHandler: RoomMemberEventHandler,
private val roomTypingUsersHandler: RoomTypingUsersHandler,
private val roomChangeMembershipStateDataSource: RoomChangeMembershipStateDataSource,
@UserId private val userId: String,
private val eventBus: EventBus,
private val timelineEventDecryptor: TimelineEventDecryptor) {
@ -185,6 +187,7 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
} != null
roomTypingUsersHandler.handle(realm, roomId, ephemeralResult)
roomChangeMembershipStateDataSource.setMembershipFromSync(roomId, Membership.JOIN)
roomSummaryUpdater.update(
realm,
roomId,
@ -221,6 +224,7 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
val inviterEvent = roomSync.inviteState?.events?.lastOrNull {
it.type == EventType.STATE_ROOM_MEMBER
}
roomChangeMembershipStateDataSource.setMembershipFromSync(roomId, Membership.INVITE)
roomSummaryUpdater.update(realm, roomId, Membership.INVITE, updateMembers = true, inviterId = inviterEvent?.senderId)
return roomEntity
}
@ -263,6 +267,8 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
val membership = leftMember?.membership ?: Membership.LEAVE
roomEntity.membership = membership
roomEntity.chunks.deleteAllFromRealm()
roomTypingUsersHandler.handle(realm, roomId, null)
roomChangeMembershipStateDataSource.setMembershipFromSync(roomId, Membership.LEAVE)
roomSummaryUpdater.update(realm, roomId, membership, roomSync.summary, roomSync.unreadNotifications)
return roomEntity
}
@ -307,14 +313,15 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
root = eventEntity
}
if (event.type == EventType.STATE_ROOM_MEMBER) {
roomMemberContentsByUser[event.stateKey] = event.content.toModel()
roomMemberEventHandler.handle(realm, roomEntity.roomId, event)
val fixedContent = event.getFixedRoomMemberContent()
roomMemberContentsByUser[event.stateKey] = fixedContent
roomMemberEventHandler.handle(realm, roomEntity.roomId, event.stateKey, fixedContent)
}
}
roomMemberContentsByUser.getOrPut(event.senderId) {
// If we don't have any new state on this user, get it from db
val rootStateEvent = CurrentStateEventEntity.getOrNull(realm, roomId, event.senderId, EventType.STATE_ROOM_MEMBER)?.root
ContentMapper.map(rootStateEvent?.content).toModel()
rootStateEvent?.asDomain()?.getFixedRoomMemberContent()
}
chunkEntity.addTimelineEvent(roomId, eventEntity, PaginationDirection.FORWARDS, roomMemberContentsByUser)
@ -405,4 +412,18 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
}
}
}
private fun Event.getFixedRoomMemberContent(): RoomMemberContent? {
val content = content.toModel<RoomMemberContent>()
// if user is leaving, we should grab his last name and avatar from prevContent
return if (content?.membership?.isLeft() == true) {
val prevContent = resolvedPrevContent().toModel<RoomMemberContent>()
content.copy(
displayName = prevContent?.displayName,
avatarUrl = prevContent?.avatarUrl
)
} else {
content
}
}
}

View file

@ -290,7 +290,8 @@ dependencies {
implementation 'androidx.appcompat:appcompat:1.1.0'
implementation "androidx.fragment:fragment:$fragment_version"
implementation "androidx.fragment:fragment-ktx:$fragment_version"
implementation 'androidx.constraintlayout:constraintlayout:2.0.0-beta7'
// Keep at 2.0.0-beta4 at the moment, as updating is breaking some UI
implementation 'androidx.constraintlayout:constraintlayout:2.0.0-beta4'
implementation 'androidx.core:core-ktx:1.3.0'
implementation "org.threeten:threetenbp:1.4.0:no-tzdb"

View file

@ -23,21 +23,21 @@ import androidx.fragment.app.FragmentTransaction
import im.vector.riotx.core.platform.VectorBaseActivity
fun VectorBaseActivity.addFragment(frameId: Int, fragment: Fragment) {
supportFragmentManager.commitTransactionNow { add(frameId, fragment) }
supportFragmentManager.commitTransaction { add(frameId, fragment) }
}
fun <T : Fragment> VectorBaseActivity.addFragment(frameId: Int, fragmentClass: Class<T>, params: Parcelable? = null, tag: String? = null) {
supportFragmentManager.commitTransactionNow {
supportFragmentManager.commitTransaction {
add(frameId, fragmentClass, params.toMvRxBundle(), tag)
}
}
fun VectorBaseActivity.replaceFragment(frameId: Int, fragment: Fragment, tag: String? = null) {
supportFragmentManager.commitTransactionNow { replace(frameId, fragment, tag) }
supportFragmentManager.commitTransaction { replace(frameId, fragment, tag) }
}
fun <T : Fragment> VectorBaseActivity.replaceFragment(frameId: Int, fragmentClass: Class<T>, params: Parcelable? = null, tag: String? = null) {
supportFragmentManager.commitTransactionNow {
supportFragmentManager.commitTransaction {
replace(frameId, fragmentClass, params.toMvRxBundle(), tag)
}
}

View file

@ -27,21 +27,21 @@ import java.util.Date
import java.util.Locale
fun VectorBaseFragment.addFragment(frameId: Int, fragment: Fragment) {
parentFragmentManager.commitTransactionNow { add(frameId, fragment) }
parentFragmentManager.commitTransaction { add(frameId, fragment) }
}
fun <T : Fragment> VectorBaseFragment.addFragment(frameId: Int, fragmentClass: Class<T>, params: Parcelable? = null, tag: String? = null) {
parentFragmentManager.commitTransactionNow {
parentFragmentManager.commitTransaction {
add(frameId, fragmentClass, params.toMvRxBundle(), tag)
}
}
fun VectorBaseFragment.replaceFragment(frameId: Int, fragment: Fragment) {
parentFragmentManager.commitTransactionNow { replace(frameId, fragment) }
parentFragmentManager.commitTransaction { replace(frameId, fragment) }
}
fun <T : Fragment> VectorBaseFragment.replaceFragment(frameId: Int, fragmentClass: Class<T>, params: Parcelable? = null, tag: String? = null) {
parentFragmentManager.commitTransactionNow {
parentFragmentManager.commitTransaction {
replace(frameId, fragmentClass, params.toMvRxBundle(), tag)
}
}
@ -57,21 +57,21 @@ fun <T : Fragment> VectorBaseFragment.addFragmentToBackstack(frameId: Int, fragm
}
fun VectorBaseFragment.addChildFragment(frameId: Int, fragment: Fragment, tag: String? = null) {
childFragmentManager.commitTransactionNow { add(frameId, fragment, tag) }
childFragmentManager.commitTransaction { add(frameId, fragment, tag) }
}
fun <T : Fragment> VectorBaseFragment.addChildFragment(frameId: Int, fragmentClass: Class<T>, params: Parcelable? = null, tag: String? = null) {
childFragmentManager.commitTransactionNow {
childFragmentManager.commitTransaction {
add(frameId, fragmentClass, params.toMvRxBundle(), tag)
}
}
fun VectorBaseFragment.replaceChildFragment(frameId: Int, fragment: Fragment, tag: String? = null) {
childFragmentManager.commitTransactionNow { replace(frameId, fragment, tag) }
childFragmentManager.commitTransaction { replace(frameId, fragment, tag) }
}
fun <T : Fragment> VectorBaseFragment.replaceChildFragment(frameId: Int, fragmentClass: Class<T>, params: Parcelable? = null, tag: String? = null) {
childFragmentManager.commitTransactionNow {
childFragmentManager.commitTransaction {
replace(frameId, fragmentClass, params.toMvRxBundle(), tag)
}
}

View file

@ -1,421 +0,0 @@
/*
* Copyright (C) 2011 Micah Hainline
* Copyright (C) 2012 Triposo
* Copyright (C) 2013 Paul Imhoff
* Copyright (C) 2014 Shahin Yousefi
* Copyright 2020 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.Canvas
import android.graphics.Color
import android.text.Layout
import android.text.Spannable
import android.text.SpannableString
import android.text.SpannableStringBuilder
import android.text.Spanned
import android.text.StaticLayout
import android.text.TextUtils.TruncateAt
import android.text.TextUtils.concat
import android.text.TextUtils.copySpansFrom
import android.text.TextUtils.indexOf
import android.text.TextUtils.lastIndexOf
import android.text.TextUtils.substring
import android.text.style.ForegroundColorSpan
import android.util.AttributeSet
import androidx.appcompat.widget.AppCompatTextView
import androidx.core.content.withStyledAttributes
import timber.log.Timber
import java.util.ArrayList
import java.util.regex.Pattern
/*
* Imported from https://gist.github.com/hateum/d2095575b441007d62b8
*
* Use it in your layout to avoid this issue: https://issuetracker.google.com/issues/121092510
*/
/**
* A [android.widget.TextView] that ellipsizes more intelligently.
* This class supports ellipsizing multiline text through setting `android:ellipsize`
* and `android:maxLines`.
*
*
* Note: [TruncateAt.MARQUEE] ellipsizing type is not supported.
* This as to be used to get rid of the StaticLayout issue with maxLines and ellipsize causing some performance issues.
*/
class EllipsizingTextView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyle: Int = android.R.attr.textViewStyle)
: AppCompatTextView(context, attrs, defStyle) {
private val ELLIPSIS = SpannableString("\u2026")
private val ellipsizeListeners: MutableList<EllipsizeListener> = ArrayList()
private var ellipsizeStrategy: EllipsizeStrategy? = null
var isEllipsized = false
private set
private var isStale = false
private var programmaticChange = false
private var fullText: CharSequence? = null
private var maxLines = 0
private var lineSpacingMult = 1.0f
private var lineAddVertPad = 0.0f
/**
* The end punctuation which will be removed when appending [.ELLIPSIS].
*/
private var mEndPunctPattern: Pattern? = null
fun setEndPunctuationPattern(pattern: Pattern?) {
mEndPunctPattern = pattern
}
fun addEllipsizeListener(listener: EllipsizeListener) {
ellipsizeListeners.add(listener)
}
fun removeEllipsizeListener(listener: EllipsizeListener) {
ellipsizeListeners.remove(listener)
}
/**
* @return The maximum number of lines displayed in this [android.widget.TextView].
*/
override fun getMaxLines(): Int {
return maxLines
}
override fun setMaxLines(maxLines: Int) {
super.setMaxLines(maxLines)
this.maxLines = maxLines
isStale = true
}
/**
* Determines if the last fully visible line is being ellipsized.
*
* @return `true` if the last fully visible line is being ellipsized;
* otherwise, returns `false`.
*/
fun ellipsizingLastFullyVisibleLine(): Boolean {
return maxLines == Int.MAX_VALUE
}
override fun setLineSpacing(add: Float, mult: Float) {
lineAddVertPad = add
lineSpacingMult = mult
super.setLineSpacing(add, mult)
}
override fun setText(text: CharSequence?, type: BufferType) {
if (!programmaticChange) {
fullText = if (text is Spanned) text else text
isStale = true
}
super.setText(text, type)
}
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
super.onSizeChanged(w, h, oldw, oldh)
if (ellipsizingLastFullyVisibleLine()) {
isStale = true
}
}
override fun setPadding(left: Int, top: Int, right: Int, bottom: Int) {
super.setPadding(left, top, right, bottom)
if (ellipsizingLastFullyVisibleLine()) {
isStale = true
}
}
override fun onDraw(canvas: Canvas) {
if (isStale) {
resetText()
}
super.onDraw(canvas)
}
/**
* Sets the ellipsized text if appropriate.
*/
private fun resetText() {
val maxLines = maxLines
var workingText = fullText
var ellipsized = false
if (maxLines != -1) {
if (ellipsizeStrategy == null) setEllipsize(null)
workingText = ellipsizeStrategy!!.processText(fullText)
ellipsized = !ellipsizeStrategy!!.isInLayout(fullText)
}
if (workingText != text) {
programmaticChange = true
text = try {
workingText
} finally {
programmaticChange = false
}
}
isStale = false
if (ellipsized != isEllipsized) {
isEllipsized = ellipsized
for (listener in ellipsizeListeners) {
listener.ellipsizeStateChanged(ellipsized)
}
}
}
/**
* Causes words in the text that are longer than the view is wide to be ellipsized
* instead of broken in the middle. Use `null` to turn off ellipsizing.
*
*
* Note: Method does nothing for [TruncateAt.MARQUEE]
* ellipsizing type.
*
* @param where part of text to ellipsize
*/
override fun setEllipsize(where: TruncateAt?) {
if (where == null) {
ellipsizeStrategy = EllipsizeNoneStrategy()
return
}
ellipsizeStrategy = when (where) {
TruncateAt.END -> EllipsizeEndStrategy()
TruncateAt.START -> EllipsizeStartStrategy()
TruncateAt.MIDDLE -> EllipsizeMiddleStrategy()
TruncateAt.MARQUEE -> EllipsizeNoneStrategy()
else -> EllipsizeNoneStrategy()
}
}
/**
* A listener that notifies when the ellipsize state has changed.
*/
interface EllipsizeListener {
fun ellipsizeStateChanged(ellipsized: Boolean)
}
/**
* A base class for an ellipsize strategy.
*/
private abstract inner class EllipsizeStrategy {
/**
* Returns ellipsized text if the text does not fit inside of the layout;
* otherwise, returns the full text.
*
* @param text text to process
* @return Ellipsized text if the text does not fit inside of the layout;
* otherwise, returns the full text.
*/
fun processText(text: CharSequence?): CharSequence? {
return if (!isInLayout(text)) createEllipsizedText(text) else text
}
/**
* Determines if the text fits inside of the layout.
*
* @param text text to fit
* @return `true` if the text fits inside of the layout;
* otherwise, returns `false`.
*/
fun isInLayout(text: CharSequence?): Boolean {
val layout = createWorkingLayout(text)
return layout.lineCount <= linesCount
}
/**
* Creates a working layout with the given text.
*
* @param workingText text to create layout with
* @return [android.text.Layout] with the given text.
*/
@Suppress("DEPRECATION")
protected fun createWorkingLayout(workingText: CharSequence?): Layout {
return StaticLayout(
workingText ?: "",
paint,
width - compoundPaddingLeft - compoundPaddingRight,
Layout.Alignment.ALIGN_NORMAL,
lineSpacingMult,
lineAddVertPad,
false
)
}
/**
* Get how many lines of text we are allowed to display.
*/
protected val linesCount: Int
get() = if (ellipsizingLastFullyVisibleLine()) {
val fullyVisibleLinesCount = fullyVisibleLinesCount
if (fullyVisibleLinesCount == -1) 1 else fullyVisibleLinesCount
} else {
maxLines
}
/**
* Get how many lines of text we can display so their full height is visible.
*/
protected val fullyVisibleLinesCount: Int
get() {
val layout = createWorkingLayout("")
val height = height - compoundPaddingTop - compoundPaddingBottom
val lineHeight = layout.getLineBottom(0)
return height / lineHeight
}
/**
* Creates ellipsized text from the given text.
*
* @param fullText text to ellipsize
* @return Ellipsized text
*/
protected abstract fun createEllipsizedText(fullText: CharSequence?): CharSequence?
}
/**
* An [EllipsizingTextView.EllipsizeStrategy] that
* does not ellipsize text.
*/
private inner class EllipsizeNoneStrategy : EllipsizeStrategy() {
override fun createEllipsizedText(fullText: CharSequence?): CharSequence? {
return fullText
}
}
/**
* An [EllipsizingTextView.EllipsizeStrategy] that
* ellipsizes text at the end.
*/
private inner class EllipsizeEndStrategy : EllipsizeStrategy() {
override fun createEllipsizedText(fullText: CharSequence?): CharSequence? {
val layout = createWorkingLayout(fullText)
val cutOffIndex = try {
layout.getLineEnd(maxLines - 1)
} catch (exception: IndexOutOfBoundsException) {
// Not sure to understand why this is happening
Timber.e(exception, "IndexOutOfBoundsException, maxLine: $maxLines")
0
}
val textLength = fullText!!.length
var cutOffLength = textLength - cutOffIndex
if (cutOffLength < ELLIPSIS.length) cutOffLength = ELLIPSIS.length
var workingText: CharSequence = substring(fullText, 0, textLength - cutOffLength).trim()
while (!isInLayout(concat(stripEndPunctuation(workingText), ELLIPSIS))) {
val lastSpace = lastIndexOf(workingText, ' ')
if (lastSpace == -1) {
break
}
workingText = substring(workingText, 0, lastSpace).trim()
}
workingText = concat(stripEndPunctuation(workingText), ELLIPSIS)
val dest = SpannableStringBuilder(workingText)
if (fullText is Spanned) {
copySpansFrom(fullText as Spanned?, 0, workingText.length, null, dest, 0)
}
return dest
}
/**
* Strips the end punctuation from a given text according to [.mEndPunctPattern].
*
* @param workingText text to strip end punctuation from
* @return Text without end punctuation.
*/
fun stripEndPunctuation(workingText: CharSequence): String {
return mEndPunctPattern!!.matcher(workingText).replaceFirst("")
}
}
/**
* An [EllipsizingTextView.EllipsizeStrategy] that
* ellipsizes text at the start.
*/
private inner class EllipsizeStartStrategy : EllipsizeStrategy() {
override fun createEllipsizedText(fullText: CharSequence?): CharSequence? {
val layout = createWorkingLayout(fullText)
val cutOffIndex = layout.getLineEnd(maxLines - 1)
val textLength = fullText!!.length
var cutOffLength = textLength - cutOffIndex
if (cutOffLength < ELLIPSIS.length) cutOffLength = ELLIPSIS.length
var workingText: CharSequence = substring(fullText, cutOffLength, textLength).trim()
while (!isInLayout(concat(ELLIPSIS, workingText))) {
val firstSpace = indexOf(workingText, ' ')
if (firstSpace == -1) {
break
}
workingText = substring(workingText, firstSpace, workingText.length).trim()
}
workingText = concat(ELLIPSIS, workingText)
val dest = SpannableStringBuilder(workingText)
if (fullText is Spanned) {
copySpansFrom(fullText as Spanned?, textLength - workingText.length,
textLength, null, dest, 0)
}
return dest
}
}
/**
* An [EllipsizingTextView.EllipsizeStrategy] that
* ellipsizes text in the middle.
*/
private inner class EllipsizeMiddleStrategy : EllipsizeStrategy() {
override fun createEllipsizedText(fullText: CharSequence?): CharSequence? {
val layout = createWorkingLayout(fullText)
val cutOffIndex = layout.getLineEnd(maxLines - 1)
val textLength = fullText!!.length
var cutOffLength = textLength - cutOffIndex
if (cutOffLength < ELLIPSIS.length) cutOffLength = ELLIPSIS.length
cutOffLength += cutOffIndex % 2 // Make it even.
var firstPart = substring(
fullText, 0, textLength / 2 - cutOffLength / 2).trim()
var secondPart = substring(
fullText, textLength / 2 + cutOffLength / 2, textLength).trim()
while (!isInLayout(concat(firstPart, ELLIPSIS, secondPart))) {
val lastSpaceFirstPart = firstPart.lastIndexOf(' ')
val firstSpaceSecondPart = secondPart.indexOf(' ')
if (lastSpaceFirstPart == -1 || firstSpaceSecondPart == -1) break
firstPart = firstPart.substring(0, lastSpaceFirstPart).trim()
secondPart = secondPart.substring(firstSpaceSecondPart, secondPart.length).trim()
}
val firstDest = SpannableStringBuilder(firstPart)
val secondDest = SpannableStringBuilder(secondPart)
if (fullText is Spanned) {
copySpansFrom(fullText as Spanned?, 0, firstPart.length,
null, firstDest, 0)
copySpansFrom(fullText as Spanned?, textLength - secondPart.length,
textLength, null, secondDest, 0)
}
return concat(firstDest, ELLIPSIS, secondDest)
}
}
companion object {
const val ELLIPSIZE_ALPHA = 0x88
private val DEFAULT_END_PUNCTUATION = Pattern.compile("[.!?,;:\u2026]*$", Pattern.DOTALL)
}
init {
context.withStyledAttributes(attrs, intArrayOf(android.R.attr.maxLines, android.R.attr.ellipsize), defStyle) {
maxLines = getInt(0, Int.MAX_VALUE)
}
setEndPunctuationPattern(DEFAULT_END_PUNCTUATION)
val currentTextColor = currentTextColor
val ellipsizeColor = Color.argb(ELLIPSIZE_ALPHA, Color.red(currentTextColor), Color.green(currentTextColor), Color.blue(currentTextColor))
ELLIPSIS.setSpan(ForegroundColorSpan(ellipsizeColor), 0, ELLIPSIS.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
}
}

View file

@ -19,13 +19,11 @@ package im.vector.riotx.core.ui.views
import android.content.Context
import android.util.AttributeSet
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.content.edit
import androidx.core.view.isVisible
import androidx.preference.PreferenceManager
import androidx.transition.TransitionManager
import butterknife.BindView
import butterknife.ButterKnife
import butterknife.OnClick
@ -80,8 +78,6 @@ class KeysBackupBanner @JvmOverloads constructor(
state = newState
hideAll()
val parent = parent as ViewGroup
TransitionManager.beginDelayedTransition(parent)
when (newState) {
State.Initial -> renderInitial()
State.Hidden -> renderHidden()

View file

@ -31,7 +31,7 @@ import im.vector.matrix.android.api.session.group.model.GroupSummary
import im.vector.matrix.android.api.util.toMatrixItem
import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo
import im.vector.riotx.R
import im.vector.riotx.core.extensions.commitTransactionNow
import im.vector.riotx.core.extensions.commitTransaction
import im.vector.riotx.core.glide.GlideApp
import im.vector.riotx.core.platform.ToolbarConfigurable
import im.vector.riotx.core.platform.VectorBaseActivity
@ -254,7 +254,7 @@ class HomeDetailFragment @Inject constructor(
private fun updateSelectedFragment(displayMode: RoomListDisplayMode) {
val fragmentTag = "FRAGMENT_TAG_${displayMode.name}"
val fragmentToShow = childFragmentManager.findFragmentByTag(fragmentTag)
childFragmentManager.commitTransactionNow {
childFragmentManager.commitTransaction {
childFragmentManager.fragments
.filter { it != fragmentToShow }
.forEach {

View file

@ -20,7 +20,6 @@ import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.floatingactionbutton.FloatingActionButton
import im.vector.riotx.core.utils.Debouncer
import timber.log.Timber
/**
* Show or hide the jumpToBottomView, depending on the scrolling and if the timeline is displaying the more recent event
@ -67,7 +66,6 @@ class JumpToBottomViewVisibilityManager(
}
private fun maybeShowJumpToBottomViewVisibility() {
Timber.v("First visible: ${layoutManager.findFirstCompletelyVisibleItemPosition()}")
if (layoutManager.findFirstVisibleItemPosition() != 0) {
jumpToBottomView.show()
} else {

View file

@ -69,6 +69,7 @@ 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.file.FileService
import im.vector.matrix.android.api.session.room.model.Membership
import im.vector.matrix.android.api.session.room.model.RoomSummary
import im.vector.matrix.android.api.session.room.model.message.MessageContent
import im.vector.matrix.android.api.session.room.model.message.MessageFormat
import im.vector.matrix.android.api.session.room.model.message.MessageImageInfoContent
@ -636,7 +637,7 @@ class RoomDetailFragment @Inject constructor(
val document = parser.parse(messageContent.formattedBody ?: messageContent.body)
formattedBody = eventHtmlRenderer.render(document)
}
composerLayout.composerRelatedMessageContent.text = formattedBody ?: nonFormattedBody
composerLayout.composerRelatedMessageContent.text = (formattedBody ?: nonFormattedBody)
updateComposerText(defaultContent)
@ -853,12 +854,14 @@ class RoomDetailFragment @Inject constructor(
}
override fun invalidate() = withState(roomDetailViewModel) { state ->
renderRoomSummary(state)
invalidateOptionsMenu()
val summary = state.asyncRoomSummary()
renderToolbar(summary, state.typingMessage)
val inviter = state.asyncInviter()
if (summary?.membership == Membership.JOIN) {
roomWidgetsBannerView.render(state.activeRoomWidgets())
jumpToBottomView.count = summary.notificationCount
jumpToBottomView.drawBadge = summary.hasUnreadMessages
scrollOnHighlightedEventCallback.timeline = roomDetailViewModel.timeline
timelineEventController.update(state)
inviteView.visibility = View.GONE
@ -880,7 +883,7 @@ class RoomDetailFragment @Inject constructor(
}
} else if (summary?.membership == Membership.INVITE && inviter != null) {
inviteView.visibility = View.VISIBLE
inviteView.render(inviter, VectorInviteView.Mode.LARGE)
inviteView.render(inviter, VectorInviteView.Mode.LARGE, state.changeMembershipState)
// Intercept click event
inviteView.setOnClickListener { }
} else if (state.asyncInviter.complete) {
@ -888,15 +891,15 @@ class RoomDetailFragment @Inject constructor(
}
}
private fun renderRoomSummary(state: RoomDetailViewState) {
state.asyncRoomSummary()?.let { roomSummary ->
private fun renderToolbar(roomSummary: RoomSummary?, typingMessage: String?) {
if (roomSummary == null) {
roomToolbarContentView.isClickable = false
} else {
roomToolbarContentView.isClickable = roomSummary.membership == Membership.JOIN
roomToolbarTitleView.text = roomSummary.displayName
avatarRenderer.render(roomSummary.toMatrixItem(), roomToolbarAvatarImageView)
renderSubTitle(state.typingMessage, roomSummary.topic)
jumpToBottomView.count = roomSummary.notificationCount
jumpToBottomView.drawBadge = roomSummary.hasUnreadMessages
renderSubTitle(typingMessage, roomSummary.topic)
roomToolbarDecorationImageView.let {
it.setImageResource(roomSummary.roomEncryptionTrustLevel.toImageRes())
it.isVisible = roomSummary.roomEncryptionTrustLevel != null

View file

@ -40,6 +40,7 @@ import im.vector.matrix.android.api.session.events.model.toContent
import im.vector.matrix.android.api.session.events.model.toModel
import im.vector.matrix.android.api.session.file.FileService
import im.vector.matrix.android.api.session.homeserver.HomeServerCapabilities
import im.vector.matrix.android.api.session.room.members.ChangeMembershipState
import im.vector.matrix.android.api.session.room.members.roomMemberQueryParams
import im.vector.matrix.android.api.session.room.model.Membership
import im.vector.matrix.android.api.session.room.model.PowerLevelsContent
@ -166,6 +167,7 @@ class RoomDetailViewModel @AssistedInject constructor(
timeline.start()
timeline.addListener(this)
observeRoomSummary()
observeMembershipChanges()
observeSummaryState()
getUnreadState()
observeSyncState()
@ -405,17 +407,22 @@ class RoomDetailViewModel @AssistedInject constructor(
private fun isIntegrationEnabled() = session.integrationManagerService().isIntegrationEnabled()
fun isMenuItemVisible(@IdRes itemId: Int) = when (itemId) {
R.id.clear_message_queue ->
// For now always disable when not in developer mode, worker cancellation is not working properly
timeline.pendingEventCount() > 0 && vectorPreferences.developerMode()
R.id.resend_all -> timeline.failedToDeliverEventCount() > 0
R.id.clear_all -> timeline.failedToDeliverEventCount() > 0
R.id.open_matrix_apps -> true
R.id.voice_call,
R.id.video_call -> room.canStartCall() && webRtcPeerConnectionManager.currentCall == null
R.id.hangup_call -> webRtcPeerConnectionManager.currentCall != null
else -> false
fun isMenuItemVisible(@IdRes itemId: Int): Boolean = com.airbnb.mvrx.withState(this) { state ->
if (state.asyncRoomSummary()?.membership != Membership.JOIN) {
return@withState false
}
when (itemId) {
R.id.clear_message_queue ->
// For now always disable when not in developer mode, worker cancellation is not working properly
timeline.pendingEventCount() > 0 && vectorPreferences.developerMode()
R.id.resend_all -> timeline.failedToDeliverEventCount() > 0
R.id.clear_all -> timeline.failedToDeliverEventCount() > 0
R.id.open_matrix_apps -> true
R.id.voice_call,
R.id.video_call -> room.canStartCall() && webRtcPeerConnectionManager.currentCall == null
R.id.hangup_call -> webRtcPeerConnectionManager.currentCall != null
else -> false
}
}
// PRIVATE METHODS *****************************************************************************
@ -624,7 +631,7 @@ class RoomDetailViewModel @AssistedInject constructor(
}
private fun handleJoinToAnotherRoomSlashCommand(command: ParsedCommand.JoinRoom) {
session.joinRoom(command.roomAlias, command.reason, object : MatrixCallback<Unit> {
session.joinRoom(command.roomAlias, command.reason, emptyList(), object : MatrixCallback<Unit> {
override fun onSuccess(data: Unit) {
session.getRoomSummary(command.roomAlias)
?.roomId
@ -846,17 +853,14 @@ class RoomDetailViewModel @AssistedInject constructor(
}
}
private fun handleExitSpecialMode(action: RoomDetailAction.ExitSpecialMode) {
setState { copy(sendMode = SendMode.REGULAR(action.text)) }
withState { state ->
// For edit, just delete the current draft
if (state.sendMode is SendMode.EDIT) {
room.deleteDraft(NoOpMatrixCallback())
} else {
// Save a new draft and keep the previously entered text
room.saveDraft(UserDraft.REGULAR(action.text), NoOpMatrixCallback())
}
private fun handleExitSpecialMode(action: RoomDetailAction.ExitSpecialMode) = withState {
if (it.sendMode is SendMode.EDIT) {
room.deleteDraft(NoOpMatrixCallback())
} else {
// Save a new draft and keep the previously entered text
room.saveDraft(UserDraft.REGULAR(action.text), NoOpMatrixCallback())
}
setState { copy(sendMode = SendMode.REGULAR(action.text)) }
}
private fun handleOpenOrDownloadFile(action: RoomDetailAction.DownloadOrOpen) {
@ -1145,6 +1149,19 @@ class RoomDetailViewModel @AssistedInject constructor(
}
}
private fun observeMembershipChanges() {
session.rx()
.liveRoomChangeMembershipState()
.map {
it[initialState.roomId] ?: ChangeMembershipState.Unknown
}
.distinctUntilChanged()
.subscribe {
setState { copy(changeMembershipState = it) }
}
.disposeOnClear()
}
private fun observeSummaryState() {
asyncSubscribe(RoomDetailViewState::asyncRoomSummary) { summary ->
roomSummaryHolder.set(summary)

View file

@ -20,6 +20,7 @@ import com.airbnb.mvrx.Async
import com.airbnb.mvrx.MvRxState
import com.airbnb.mvrx.Uninitialized
import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.api.session.room.members.ChangeMembershipState
import im.vector.matrix.android.api.session.room.model.RoomMemberSummary
import im.vector.matrix.android.api.session.room.model.RoomSummary
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
@ -64,6 +65,7 @@ data class RoomDetailViewState(
val highlightedEventId: String? = null,
val unreadState: UnreadState = UnreadState.Unknown,
val canShowJumpToReadMarker: Boolean = true,
val changeMembershipState: ChangeMembershipState = ChangeMembershipState.Unknown,
val canSendMessage: Boolean = true
) : MvRxState {

View file

@ -95,7 +95,7 @@ class NoticeEventFormatter @Inject constructor(private val activeSessionDataSour
private fun formatRoomPowerLevels(event: Event, disambiguatedDisplayName: String): CharSequence? {
val powerLevelsContent: PowerLevelsContent = event.getClearContent().toModel() ?: return null
val previousPowerLevelsContent: PowerLevelsContent = event.prevContent.toModel() ?: return null
val previousPowerLevelsContent: PowerLevelsContent = event.resolvedPrevContent().toModel() ?: return null
val userIds = HashSet<String>()
userIds.addAll(powerLevelsContent.users.keys)
userIds.addAll(previousPowerLevelsContent.users.keys)
@ -123,7 +123,7 @@ class NoticeEventFormatter @Inject constructor(private val activeSessionDataSour
private fun formatWidgetEvent(event: Event, disambiguatedDisplayName: String): CharSequence? {
val widgetContent: WidgetContent = event.getClearContent().toModel() ?: return null
val previousWidgetContent: WidgetContent? = event.prevContent.toModel()
val previousWidgetContent: WidgetContent? = event.resolvedPrevContent().toModel()
return if (widgetContent.isActive()) {
val widgetName = widgetContent.getHumanName()
if (previousWidgetContent?.isActive().orFalse()) {
@ -297,7 +297,7 @@ class NoticeEventFormatter @Inject constructor(private val activeSessionDataSour
private fun formatRoomMemberEvent(event: Event, senderName: String?): String? {
val eventContent: RoomMemberContent? = event.getClearContent().toModel()
val prevEventContent: RoomMemberContent? = event.prevContent.toModel()
val prevEventContent: RoomMemberContent? = event.resolvedPrevContent().toModel()
val isMembershipEvent = prevEventContent?.membership != eventContent?.membership
return if (isMembershipEvent) {
buildMembershipNotice(event, senderName, eventContent, prevEventContent)
@ -308,7 +308,7 @@ class NoticeEventFormatter @Inject constructor(private val activeSessionDataSour
private fun formatRoomAliasesEvent(event: Event, senderName: String?): String? {
val eventContent: RoomAliasesContent? = event.getClearContent().toModel()
val prevEventContent: RoomAliasesContent? = event.unsignedData?.prevContent?.toModel()
val prevEventContent: RoomAliasesContent? = event.resolvedPrevContent()?.toModel()
val addedAliases = eventContent?.aliases.orEmpty() - prevEventContent?.aliases.orEmpty()
val removedAliases = prevEventContent?.aliases.orEmpty() - eventContent?.aliases.orEmpty()

View file

@ -19,9 +19,9 @@ package im.vector.riotx.features.home.room.list
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import androidx.core.view.isVisible
import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass
import im.vector.matrix.android.api.session.room.members.ChangeMembershipState
import im.vector.matrix.android.api.util.MatrixItem
import im.vector.riotx.R
import im.vector.riotx.core.epoxy.VectorEpoxyHolder
@ -29,6 +29,7 @@ import im.vector.riotx.core.epoxy.VectorEpoxyModel
import im.vector.riotx.core.extensions.setTextOrHide
import im.vector.riotx.core.platform.ButtonStateView
import im.vector.riotx.features.home.AvatarRenderer
import im.vector.riotx.features.invite.InviteButtonStateBinder
@EpoxyModelClass(layout = R.layout.item_room_invitation)
abstract class RoomInvitationItem : VectorEpoxyModel<RoomInvitationItem.Holder>() {
@ -37,53 +38,36 @@ abstract class RoomInvitationItem : VectorEpoxyModel<RoomInvitationItem.Holder>(
@EpoxyAttribute lateinit var matrixItem: MatrixItem
@EpoxyAttribute var secondLine: CharSequence? = null
@EpoxyAttribute var listener: (() -> Unit)? = null
@EpoxyAttribute var invitationAcceptInProgress: Boolean = false
@EpoxyAttribute var invitationAcceptInError: Boolean = false
@EpoxyAttribute var invitationRejectInProgress: Boolean = false
@EpoxyAttribute var invitationRejectInError: Boolean = false
@EpoxyAttribute lateinit var changeMembershipState: ChangeMembershipState
@EpoxyAttribute var acceptListener: (() -> Unit)? = null
@EpoxyAttribute var rejectListener: (() -> Unit)? = null
private val acceptCallback = object : ButtonStateView.Callback {
override fun onButtonClicked() {
acceptListener?.invoke()
}
override fun onRetryClicked() {
acceptListener?.invoke()
}
}
private val rejectCallback = object : ButtonStateView.Callback {
override fun onButtonClicked() {
rejectListener?.invoke()
}
override fun onRetryClicked() {
rejectListener?.invoke()
}
}
override fun bind(holder: Holder) {
super.bind(holder)
holder.rootView.setOnClickListener { listener?.invoke() }
// When a request is in progress (accept or reject), we only use the accept State button
val requestInProgress = invitationAcceptInProgress || invitationRejectInProgress
when {
requestInProgress -> holder.acceptView.render(ButtonStateView.State.Loading)
invitationAcceptInError -> holder.acceptView.render(ButtonStateView.State.Error)
else -> holder.acceptView.render(ButtonStateView.State.Button)
}
// ButtonStateView.State.Loaded not used because roomSummary will not be displayed as a room invitation anymore
holder.acceptView.callback = object : ButtonStateView.Callback {
override fun onButtonClicked() {
acceptListener?.invoke()
}
override fun onRetryClicked() {
acceptListener?.invoke()
}
}
holder.rejectView.isVisible = !requestInProgress
when {
invitationRejectInError -> holder.rejectView.render(ButtonStateView.State.Error)
else -> holder.rejectView.render(ButtonStateView.State.Button)
}
holder.rejectView.callback = object : ButtonStateView.Callback {
override fun onButtonClicked() {
rejectListener?.invoke()
}
override fun onRetryClicked() {
rejectListener?.invoke()
}
}
holder.acceptView.callback = acceptCallback
holder.rejectView.callback = rejectCallback
InviteButtonStateBinder.bind(holder.acceptView, holder.rejectView, changeMembershipState)
holder.titleView.text = matrixItem.getBestName()
holder.subtitleView.setTextOrHide(secondLine)
avatarRenderer.render(matrixItem, holder.avatarImageView)

View file

@ -21,10 +21,12 @@ import com.airbnb.mvrx.MvRxViewModelFactory
import com.airbnb.mvrx.ViewModelContext
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.NoOpMatrixCallback
import im.vector.matrix.android.api.extensions.orFalse
import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.room.model.Membership
import im.vector.matrix.android.api.session.room.model.RoomSummary
import im.vector.matrix.android.api.session.room.model.tag.RoomTag
import im.vector.matrix.rx.rx
import im.vector.riotx.core.extensions.exhaustive
import im.vector.riotx.core.platform.VectorViewModel
import im.vector.riotx.core.utils.DataSource
@ -55,6 +57,7 @@ class RoomListViewModel @Inject constructor(initialState: RoomListViewState,
init {
observeRoomSummaries()
observeMembershipChanges()
}
override fun handle(action: RoomListAction) {
@ -102,37 +105,19 @@ class RoomListViewModel @Inject constructor(initialState: RoomListViewState,
.observeOn(Schedulers.computation())
.map { buildRoomSummaries(it) }
.execute { async ->
val invitedRooms = async()?.get(RoomCategory.INVITE)?.map { it.roomId }.orEmpty()
val remainingJoining = joiningRoomsIds.intersect(invitedRooms)
val remainingJoinErrors = joiningErrorRoomsIds.intersect(invitedRooms)
val remainingRejecting = rejectingRoomsIds.intersect(invitedRooms)
val remainingRejectErrors = rejectingErrorRoomsIds.intersect(invitedRooms)
copy(
asyncFilteredRooms = async,
joiningRoomsIds = remainingJoining,
joiningErrorRoomsIds = remainingJoinErrors,
rejectingRoomsIds = remainingRejecting,
rejectingErrorRoomsIds = remainingRejectErrors
)
copy(asyncFilteredRooms = async)
}
}
private fun handleAcceptInvitation(action: RoomListAction.AcceptInvitation) = withState { state ->
val roomId = action.roomSummary.roomId
if (state.joiningRoomsIds.contains(roomId) || state.rejectingRoomsIds.contains(roomId)) {
val roomMembershipChange = state.roomMembershipChanges[roomId]
if (roomMembershipChange?.isInProgress().orFalse()) {
// Request already sent, should not happen
Timber.w("Try to join an already joining room. Should not happen")
return@withState
}
setState {
copy(
joiningRoomsIds = joiningRoomsIds + roomId,
rejectingErrorRoomsIds = rejectingErrorRoomsIds - roomId
)
}
session.getRoom(roomId)?.join(callback = 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.
@ -142,32 +127,19 @@ class RoomListViewModel @Inject constructor(initialState: RoomListViewState,
override fun onFailure(failure: Throwable) {
// Notify the user
_viewEvents.post(RoomListViewEvents.Failure(failure))
setState {
copy(
joiningRoomsIds = joiningRoomsIds - roomId,
joiningErrorRoomsIds = joiningErrorRoomsIds + roomId
)
}
}
})
}
private fun handleRejectInvitation(action: RoomListAction.RejectInvitation) = withState { state ->
val roomId = action.roomSummary.roomId
if (state.joiningRoomsIds.contains(roomId) || state.rejectingRoomsIds.contains(roomId)) {
val roomMembershipChange = state.roomMembershipChanges[roomId]
if (roomMembershipChange?.isInProgress().orFalse()) {
// Request already sent, should not happen
Timber.w("Try to reject an already rejecting room. Should not happen")
Timber.w("Try to left an already leaving or joining room. Should not happen")
return@withState
}
setState {
copy(
rejectingRoomsIds = rejectingRoomsIds + roomId,
joiningErrorRoomsIds = joiningErrorRoomsIds - roomId
)
}
session.getRoom(roomId)?.leave(null, object : MatrixCallback<Unit> {
override fun onSuccess(data: Unit) {
// We do not update the rejectingRoomsIds here, because, the room is not rejected yet regarding the sync data.
@ -179,12 +151,6 @@ class RoomListViewModel @Inject constructor(initialState: RoomListViewState,
override fun onFailure(failure: Throwable) {
// Notify the user
_viewEvents.post(RoomListViewEvents.Failure(failure))
setState {
copy(
rejectingRoomsIds = rejectingRoomsIds - roomId,
rejectingErrorRoomsIds = rejectingErrorRoomsIds + roomId
)
}
}
})
}
@ -235,6 +201,16 @@ class RoomListViewModel @Inject constructor(initialState: RoomListViewState,
})
}
private fun observeMembershipChanges() {
session.rx()
.liveRoomChangeMembershipState()
.subscribe {
Timber.v("ChangeMembership states: $it")
setState { copy(roomMembershipChanges = it) }
}
.disposeOnClear()
}
private fun buildRoomSummaries(rooms: List<RoomSummary>): RoomSummaries {
// Set up init size on directChats and groupRooms as they are the biggest ones
val invites = ArrayList<RoomSummary>()

View file

@ -20,6 +20,7 @@ import androidx.annotation.StringRes
import com.airbnb.mvrx.Async
import com.airbnb.mvrx.MvRxState
import com.airbnb.mvrx.Uninitialized
import im.vector.matrix.android.api.session.room.members.ChangeMembershipState
import im.vector.matrix.android.api.session.room.model.Membership
import im.vector.matrix.android.api.session.room.model.RoomSummary
import im.vector.riotx.R
@ -30,14 +31,7 @@ data class RoomListViewState(
val asyncRooms: Async<List<RoomSummary>> = Uninitialized,
val roomFilter: String = "",
val asyncFilteredRooms: Async<RoomSummaries> = Uninitialized,
// List of roomIds that the user wants to join
val joiningRoomsIds: Set<String> = emptySet(),
// List of roomIds that the user wants to join, but an error occurred
val joiningErrorRoomsIds: Set<String> = emptySet(),
// List of roomIds that the user wants to join
val rejectingRoomsIds: Set<String> = emptySet(),
// List of roomIds that the user wants to reject, but an error occurred
val rejectingErrorRoomsIds: Set<String> = emptySet(),
val roomMembershipChanges: Map<String, ChangeMembershipState> = emptyMap(),
val isInviteExpanded: Boolean = true,
val isFavouriteRoomsExpanded: Boolean = true,
val isDirectRoomsExpanded: Boolean = true,

View file

@ -18,6 +18,7 @@ package im.vector.riotx.features.home.room.list
import androidx.annotation.StringRes
import com.airbnb.epoxy.EpoxyController
import im.vector.matrix.android.api.session.room.members.ChangeMembershipState
import im.vector.matrix.android.api.session.room.model.Membership
import im.vector.matrix.android.api.session.room.model.RoomSummary
import im.vector.riotx.R
@ -72,10 +73,7 @@ class RoomSummaryController @Inject constructor(private val stringProvider: Stri
.filter { it.membership == Membership.JOIN && roomListNameFilter.test(it) }
buildRoomModels(filteredSummaries,
viewState.joiningRoomsIds,
viewState.joiningErrorRoomsIds,
viewState.rejectingRoomsIds,
viewState.rejectingErrorRoomsIds,
viewState.roomMembershipChanges,
emptySet())
addFilterFooter(viewState)
@ -94,10 +92,7 @@ class RoomSummaryController @Inject constructor(private val stringProvider: Stri
}
if (isExpanded) {
buildRoomModels(summaries,
viewState.joiningRoomsIds,
viewState.joiningErrorRoomsIds,
viewState.rejectingRoomsIds,
viewState.rejectingErrorRoomsIds,
viewState.roomMembershipChanges,
emptySet())
// Never set showHelp to true for invitation
if (category != RoomCategory.INVITE) {
@ -153,18 +148,12 @@ class RoomSummaryController @Inject constructor(private val stringProvider: Stri
}
private fun buildRoomModels(summaries: List<RoomSummary>,
joiningRoomsIds: Set<String>,
joiningErrorRoomsIds: Set<String>,
rejectingRoomsIds: Set<String>,
rejectingErrorRoomsIds: Set<String>,
roomChangedMembershipStates: Map<String, ChangeMembershipState>,
selectedRoomIds: Set<String>) {
summaries.forEach { roomSummary ->
roomSummaryItemFactory
.create(roomSummary,
joiningRoomsIds,
joiningErrorRoomsIds,
rejectingRoomsIds,
rejectingErrorRoomsIds,
roomChangedMembershipStates,
selectedRoomIds,
listener)
.addTo(this)

View file

@ -17,6 +17,7 @@
package im.vector.riotx.features.home.room.list
import android.view.View
import im.vector.matrix.android.api.session.room.members.ChangeMembershipState
import im.vector.matrix.android.api.session.room.model.Membership
import im.vector.matrix.android.api.session.room.model.RoomSummary
import im.vector.matrix.android.api.util.toMatrixItem
@ -39,23 +40,20 @@ class RoomSummaryItemFactory @Inject constructor(private val displayableEventFor
private val avatarRenderer: AvatarRenderer) {
fun create(roomSummary: RoomSummary,
joiningRoomsIds: Set<String>,
joiningErrorRoomsIds: Set<String>,
rejectingRoomsIds: Set<String>,
rejectingErrorRoomsIds: Set<String>,
roomChangeMembershipStates: Map<String, ChangeMembershipState>,
selectedRoomIds: Set<String>,
listener: RoomSummaryController.Listener?): VectorEpoxyModel<*> {
return when (roomSummary.membership) {
Membership.INVITE -> createInvitationItem(roomSummary, joiningRoomsIds, joiningErrorRoomsIds, rejectingRoomsIds, rejectingErrorRoomsIds, listener)
Membership.INVITE -> {
val changeMembershipState = roomChangeMembershipStates[roomSummary.roomId] ?: ChangeMembershipState.Unknown
createInvitationItem(roomSummary, changeMembershipState, listener)
}
else -> createRoomItem(roomSummary, selectedRoomIds, listener?.let { it::onRoomClicked }, listener?.let { it::onRoomLongClicked })
}
}
fun createInvitationItem(roomSummary: RoomSummary,
joiningRoomsIds: Set<String>,
joiningErrorRoomsIds: Set<String>,
rejectingRoomsIds: Set<String>,
rejectingErrorRoomsIds: Set<String>,
private fun createInvitationItem(roomSummary: RoomSummary,
changeMembershipState: ChangeMembershipState,
listener: RoomSummaryController.Listener?): VectorEpoxyModel<*> {
val secondLine = if (roomSummary.isDirect) {
roomSummary.inviterId
@ -70,10 +68,7 @@ class RoomSummaryItemFactory @Inject constructor(private val displayableEventFor
.avatarRenderer(avatarRenderer)
.matrixItem(roomSummary.toMatrixItem())
.secondLine(secondLine)
.invitationAcceptInProgress(joiningRoomsIds.contains(roomSummary.roomId))
.invitationAcceptInError(joiningErrorRoomsIds.contains(roomSummary.roomId))
.invitationRejectInProgress(rejectingRoomsIds.contains(roomSummary.roomId))
.invitationRejectInError(rejectingErrorRoomsIds.contains(roomSummary.roomId))
.changeMembershipState(changeMembershipState)
.acceptListener { listener?.onAcceptRoomInvitation(roomSummary) }
.rejectListener { listener?.onRejectRoomInvitation(roomSummary) }
.listener { listener?.onRoomClicked(roomSummary) }

View file

@ -0,0 +1,48 @@
/*
* Copyright (c) 2020 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.invite
import androidx.core.view.isInvisible
import im.vector.matrix.android.api.session.room.members.ChangeMembershipState
import im.vector.riotx.core.platform.ButtonStateView
object InviteButtonStateBinder {
fun bind(
acceptView: ButtonStateView,
rejectView: ButtonStateView,
changeMembershipState: ChangeMembershipState
) {
// When a request is in progress (accept or reject), we only use the accept State button
// We check for isSuccessful, otherwise we get a glitch the time room summaries get rebuilt
val requestInProgress = changeMembershipState.isInProgress() || changeMembershipState.isSuccessful()
when {
requestInProgress -> acceptView.render(ButtonStateView.State.Loading)
changeMembershipState is ChangeMembershipState.FailedJoining -> acceptView.render(ButtonStateView.State.Error)
else -> acceptView.render(ButtonStateView.State.Button)
}
// ButtonStateView.State.Loaded not used because roomSummary will not be displayed as a room invitation anymore
rejectView.isInvisible = requestInProgress
when {
changeMembershipState is ChangeMembershipState.FailedLeaving -> rejectView.render(ButtonStateView.State.Error)
else -> rejectView.render(ButtonStateView.State.Button)
}
}
}

View file

@ -21,10 +21,12 @@ import android.util.AttributeSet
import android.view.View
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.view.updateLayoutParams
import im.vector.matrix.android.api.session.room.members.ChangeMembershipState
import im.vector.matrix.android.api.session.user.model.User
import im.vector.matrix.android.api.util.toMatrixItem
import im.vector.riotx.R
import im.vector.riotx.core.di.HasScreenInjector
import im.vector.riotx.core.platform.ButtonStateView
import im.vector.riotx.features.home.AvatarRenderer
import kotlinx.android.synthetic.main.vector_invite_view.view.*
import javax.inject.Inject
@ -50,11 +52,28 @@ class VectorInviteView @JvmOverloads constructor(context: Context, attrs: Attrib
context.injector().inject(this)
}
View.inflate(context, R.layout.vector_invite_view, this)
inviteRejectView.setOnClickListener { callback?.onRejectInvite() }
inviteAcceptView.setOnClickListener { callback?.onAcceptInvite() }
inviteAcceptView.callback = object : ButtonStateView.Callback {
override fun onButtonClicked() {
callback?.onAcceptInvite()
}
override fun onRetryClicked() {
callback?.onAcceptInvite()
}
}
inviteRejectView.callback = object : ButtonStateView.Callback {
override fun onButtonClicked() {
callback?.onRejectInvite()
}
override fun onRetryClicked() {
callback?.onRejectInvite()
}
}
}
fun render(sender: User, mode: Mode = Mode.LARGE) {
fun render(sender: User, mode: Mode = Mode.LARGE, changeMembershipState: ChangeMembershipState) {
if (mode == Mode.LARGE) {
updateLayoutParams { height = LayoutParams.MATCH_CONSTRAINT }
avatarRenderer.render(sender.toMatrixItem(), inviteAvatarView)
@ -68,5 +87,6 @@ class VectorInviteView @JvmOverloads constructor(context: Context, attrs: Attrib
inviteNameView.visibility = View.GONE
inviteLabelView.text = context.getString(R.string.invited_by, sender.userId)
}
InviteButtonStateBinder.bind(inviteAcceptView, inviteRejectView, changeMembershipState)
}
}

View file

@ -28,6 +28,7 @@ import androidx.core.view.ViewCompat
import androidx.fragment.app.Fragment
import im.vector.matrix.android.api.session.crypto.verification.IncomingSasVerificationTransaction
import im.vector.matrix.android.api.session.room.model.roomdirectory.PublicRoom
import im.vector.matrix.android.api.session.room.model.thirdparty.RoomDirectoryData
import im.vector.matrix.android.api.session.terms.TermsService
import im.vector.matrix.android.api.session.widgets.model.Widget
import im.vector.matrix.android.api.util.MatrixItem
@ -178,8 +179,8 @@ class DefaultNavigator @Inject constructor(
activity.finish()
}
override fun openRoomPreview(publicRoom: PublicRoom, context: Context) {
val intent = RoomPreviewActivity.getIntent(context, publicRoom)
override fun openRoomPreview(context: Context, publicRoom: PublicRoom, roomDirectoryData: RoomDirectoryData) {
val intent = RoomPreviewActivity.getIntent(context, publicRoom, roomDirectoryData)
context.startActivity(intent)
}

View file

@ -22,6 +22,7 @@ import android.view.View
import androidx.core.util.Pair
import androidx.fragment.app.Fragment
import im.vector.matrix.android.api.session.room.model.roomdirectory.PublicRoom
import im.vector.matrix.android.api.session.room.model.thirdparty.RoomDirectoryData
import im.vector.matrix.android.api.session.terms.TermsService
import im.vector.matrix.android.api.session.widgets.model.Widget
import im.vector.matrix.android.api.util.MatrixItem
@ -49,7 +50,7 @@ interface Navigator {
fun openNotJoinedRoom(context: Context, roomIdOrAlias: String?, eventId: String? = null, buildTask: Boolean = false)
fun openRoomPreview(publicRoom: PublicRoom, context: Context)
fun openRoomPreview(context: Context, publicRoom: PublicRoom, roomDirectoryData: RoomDirectoryData)
fun openCreateRoom(context: Context, initialName: String = "")

View file

@ -21,6 +21,7 @@ import com.airbnb.epoxy.VisibilityState
import com.airbnb.mvrx.Fail
import com.airbnb.mvrx.Incomplete
import com.airbnb.mvrx.Success
import im.vector.matrix.android.api.session.room.members.ChangeMembershipState
import im.vector.matrix.android.api.session.room.model.roomdirectory.PublicRoom
import im.vector.matrix.android.api.util.toMatrixItem
import im.vector.riotx.R
@ -89,13 +90,14 @@ class PublicRoomsController @Inject constructor(private val stringProvider: Stri
roomTopic(publicRoom.topic)
nbOfMembers(publicRoom.numJoinedMembers)
val roomChangeMembership = viewState.changeMembershipStates[publicRoom.roomId] ?: ChangeMembershipState.Unknown
val isJoined = viewState.joinedRoomsIds.contains(publicRoom.roomId) || roomChangeMembership is ChangeMembershipState.Joined
val joinState = when {
viewState.joinedRoomsIds.contains(publicRoom.roomId) -> JoinState.JOINED
viewState.joiningRoomsIds.contains(publicRoom.roomId) -> JoinState.JOINING
viewState.joiningErrorRoomsIds.contains(publicRoom.roomId) -> JoinState.JOINING_ERROR
else -> JoinState.NOT_JOINED
isJoined -> JoinState.JOINED
roomChangeMembership is ChangeMembershipState.Joining -> JoinState.JOINING
roomChangeMembership is ChangeMembershipState.FailedJoining -> JoinState.JOINING_ERROR
else -> JoinState.NOT_JOINED
}
joinState(joinState)
joinListener {

View file

@ -114,26 +114,22 @@ class PublicRoomsFragment @Inject constructor(
override fun onPublicRoomClicked(publicRoom: PublicRoom, joinState: JoinState) {
Timber.v("PublicRoomClicked: $publicRoom")
when (joinState) {
JoinState.JOINED -> {
navigator.openRoom(requireActivity(), publicRoom.roomId)
}
JoinState.NOT_JOINED,
JoinState.JOINING_ERROR -> {
// ROOM PREVIEW
navigator.openRoomPreview(publicRoom, requireActivity())
}
else -> {
Snackbar.make(publicRoomsCoordinator, getString(R.string.please_wait), Snackbar.LENGTH_SHORT)
.show()
withState(viewModel) { state ->
when (joinState) {
JoinState.JOINED -> {
navigator.openRoom(requireActivity(), publicRoom.roomId)
}
else -> {
// ROOM PREVIEW
navigator.openRoomPreview(requireActivity(), publicRoom, state.roomDirectoryData)
}
}
}
}
override fun onPublicRoomJoin(publicRoom: PublicRoom) {
Timber.v("PublicRoomJoinClicked: $publicRoom")
viewModel.handle(RoomDirectoryAction.JoinRoom(publicRoom.getPrimaryAlias(), publicRoom.roomId))
viewModel.handle(RoomDirectoryAction.JoinRoom(publicRoom.roomId))
}
override fun loadMore() {

View file

@ -19,7 +19,9 @@ package im.vector.riotx.features.roomdirectory
import com.airbnb.mvrx.Async
import com.airbnb.mvrx.MvRxState
import com.airbnb.mvrx.Uninitialized
import im.vector.matrix.android.api.session.room.members.ChangeMembershipState
import im.vector.matrix.android.api.session.room.model.roomdirectory.PublicRoom
import im.vector.matrix.android.api.session.room.model.thirdparty.RoomDirectoryData
data class PublicRoomsViewState(
// The current filter
@ -30,11 +32,9 @@ data class PublicRoomsViewState(
val asyncPublicRoomsRequest: Async<List<PublicRoom>> = Uninitialized,
// True if more result are available server side
val hasMore: Boolean = false,
// Set of roomIds that the user wants to join
val joiningRoomsIds: Set<String> = emptySet(),
// Set of roomIds that the user wants to join, but an error occurred
val joiningErrorRoomsIds: Set<String> = emptySet(),
// Set of joined roomId,
val joinedRoomsIds: Set<String> = emptySet(),
val roomDirectoryDisplayName: String? = null
// keys are room alias or roomId
val changeMembershipStates: Map<String, ChangeMembershipState> = emptyMap(),
val roomDirectoryData: RoomDirectoryData = RoomDirectoryData()
) : MvRxState

View file

@ -23,5 +23,5 @@ sealed class RoomDirectoryAction : VectorViewModelAction {
data class SetRoomDirectoryData(val roomDirectoryData: RoomDirectoryData) : RoomDirectoryAction()
data class FilterWith(val filter: String) : RoomDirectoryAction()
object LoadMore : RoomDirectoryAction()
data class JoinRoom(val roomAlias: String?, val roomId: String) : RoomDirectoryAction()
data class JoinRoom(val roomId: String) : RoomDirectoryAction()
}

View file

@ -26,6 +26,7 @@ import com.airbnb.mvrx.appendAt
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.extensions.orFalse
import im.vector.matrix.android.api.failure.Failure
import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.room.model.Membership
@ -63,18 +64,10 @@ class RoomDirectoryViewModel @AssistedInject constructor(@Assisted initialState:
private var currentTask: Cancelable? = null
// Default RoomDirectoryData
private var roomDirectoryData = RoomDirectoryData()
init {
setState {
copy(
roomDirectoryDisplayName = roomDirectoryData.displayName
)
}
// Observe joined room (from the sync)
observeJoinedRooms()
observeMembershipChanges()
}
private fun observeJoinedRooms() {
@ -91,18 +84,21 @@ class RoomDirectoryViewModel @AssistedInject constructor(@Assisted initialState:
?: emptySet()
setState {
copy(
joinedRoomsIds = joinedRoomIds,
// Remove (newly) joined room id from the joining room list
joiningRoomsIds = joiningRoomsIds.toMutableSet().apply { removeAll(joinedRoomIds) },
// Remove (newly) joined room id from the joining room list in error
joiningErrorRoomsIds = joiningErrorRoomsIds.toMutableSet().apply { removeAll(joinedRoomIds) }
)
copy(joinedRoomsIds = joinedRoomIds)
}
}
.disposeOnClear()
}
private fun observeMembershipChanges() {
session.rx()
.liveRoomChangeMembershipState()
.subscribe {
setState { copy(changeMembershipStates = it) }
}
.disposeOnClear()
}
override fun handle(action: RoomDirectoryAction) {
when (action) {
is RoomDirectoryAction.SetRoomDirectoryData -> setRoomDirectoryData(action)
@ -112,15 +108,15 @@ class RoomDirectoryViewModel @AssistedInject constructor(@Assisted initialState:
}
}
private fun setRoomDirectoryData(action: RoomDirectoryAction.SetRoomDirectoryData) {
if (this.roomDirectoryData == action.roomDirectoryData) {
return
private fun setRoomDirectoryData(action: RoomDirectoryAction.SetRoomDirectoryData) = withState {
if (it.roomDirectoryData == action.roomDirectoryData) {
return@withState
}
setState {
copy(roomDirectoryData = action.roomDirectoryData)
}
this.roomDirectoryData = action.roomDirectoryData
reset("")
load("")
load("", action.roomDirectoryData)
}
private fun filterWith(action: RoomDirectoryAction.FilterWith) = withState { state ->
@ -128,7 +124,7 @@ class RoomDirectoryViewModel @AssistedInject constructor(@Assisted initialState:
currentTask?.cancel()
reset(action.filter)
load(action.filter)
load(action.filter, state.roomDirectoryData)
}
}
@ -141,7 +137,6 @@ class RoomDirectoryViewModel @AssistedInject constructor(@Assisted initialState:
publicRooms = emptyList(),
asyncPublicRoomsRequest = Loading(),
hasMore = false,
roomDirectoryDisplayName = roomDirectoryData.displayName,
currentFilter = newFilter
)
}
@ -154,12 +149,11 @@ class RoomDirectoryViewModel @AssistedInject constructor(@Assisted initialState:
asyncPublicRoomsRequest = Loading()
)
}
load(state.currentFilter)
load(state.currentFilter, state.roomDirectoryData)
}
}
private fun load(filter: String) {
private fun load(filter: String, roomDirectoryData: RoomDirectoryData) {
currentTask = session.getPublicRooms(roomDirectoryData.homeServer,
PublicRoomsParams(
limit = PUBLIC_ROOMS_LIMIT,
@ -204,19 +198,16 @@ class RoomDirectoryViewModel @AssistedInject constructor(@Assisted initialState:
}
private fun joinRoom(action: RoomDirectoryAction.JoinRoom) = withState { state ->
if (state.joiningRoomsIds.contains(action.roomId)) {
val roomMembershipChange = state.changeMembershipStates[action.roomId]
if (roomMembershipChange?.isInProgress().orFalse()) {
// Request already sent, should not happen
Timber.w("Try to join an already joining room. Should not happen")
return@withState
}
setState {
copy(
joiningRoomsIds = joiningRoomsIds.toMutableSet().apply { add(action.roomId) }
)
}
session.joinRoom(action.roomAlias ?: action.roomId, callback = object : MatrixCallback<Unit> {
val viaServers = state.roomDirectoryData.homeServer?.let {
listOf(it)
} ?: emptyList()
session.joinRoom(action.roomId, viaServers = viaServers, callback = 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
@ -225,20 +216,12 @@ class RoomDirectoryViewModel @AssistedInject constructor(@Assisted initialState:
override fun onFailure(failure: Throwable) {
// Notify the user
_viewEvents.post(RoomDirectoryViewEvents.Failure(failure))
setState {
copy(
joiningRoomsIds = joiningRoomsIds.toMutableSet().apply { remove(action.roomId) },
joiningErrorRoomsIds = joiningErrorRoomsIds.toMutableSet().apply { add(action.roomId) }
)
}
}
})
}
override fun onCleared() {
super.onCleared()
currentTask?.cancel()
}
}

View file

@ -19,5 +19,5 @@ package im.vector.riotx.features.roomdirectory.roompreview
import im.vector.riotx.core.platform.VectorViewModelAction
sealed class RoomPreviewAction : VectorViewModelAction {
data class Join(val roomAlias: String?) : RoomPreviewAction()
object Join : RoomPreviewAction()
}

View file

@ -21,6 +21,7 @@ import android.content.Intent
import android.os.Parcelable
import androidx.appcompat.widget.Toolbar
import im.vector.matrix.android.api.session.room.model.roomdirectory.PublicRoom
import im.vector.matrix.android.api.session.room.model.thirdparty.RoomDirectoryData
import im.vector.matrix.android.api.util.MatrixItem
import im.vector.riotx.R
import im.vector.riotx.core.extensions.addFragment
@ -35,7 +36,8 @@ data class RoomPreviewData(
val roomAlias: String?,
val topic: String?,
val worldReadable: Boolean,
val avatarUrl: String?
val avatarUrl: String?,
val homeServer: String?
) : Parcelable {
val matrixItem: MatrixItem
get() = MatrixItem.RoomItem(roomId, roomName ?: roomAlias, avatarUrl)
@ -46,7 +48,7 @@ class RoomPreviewActivity : VectorBaseActivity(), ToolbarConfigurable {
companion object {
private const val ARG = "ARG"
fun getIntent(context: Context, publicRoom: PublicRoom): Intent {
fun getIntent(context: Context, publicRoom: PublicRoom, roomDirectoryData: RoomDirectoryData): Intent {
return Intent(context, RoomPreviewActivity::class.java).apply {
putExtra(ARG, RoomPreviewData(
roomId = publicRoom.roomId,
@ -54,7 +56,8 @@ class RoomPreviewActivity : VectorBaseActivity(), ToolbarConfigurable {
roomAlias = publicRoom.getPrimaryAlias(),
topic = publicRoom.topic,
worldReadable = publicRoom.worldReadable,
avatarUrl = publicRoom.avatarUrl
avatarUrl = publicRoom.avatarUrl,
homeServer = roomDirectoryData.homeServer
))
}
}

View file

@ -65,7 +65,7 @@ class RoomPreviewNoPreviewFragment @Inject constructor(
roomPreviewNoPreviewJoin.callback = object : ButtonStateView.Callback {
override fun onButtonClicked() {
roomPreviewViewModel.handle(RoomPreviewAction.Join(roomPreviewData.roomAlias))
roomPreviewViewModel.handle(RoomPreviewAction.Join)
}
override fun onRetryClicked() {

View file

@ -22,7 +22,9 @@ import com.airbnb.mvrx.ViewModelContext
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.query.QueryStringValue
import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.room.members.ChangeMembershipState
import im.vector.matrix.android.api.session.room.model.Membership
import im.vector.matrix.android.api.session.room.roomSummaryQueryParams
import im.vector.matrix.rx.rx
@ -32,7 +34,7 @@ import im.vector.riotx.core.platform.VectorViewModel
import im.vector.riotx.features.roomdirectory.JoinState
import timber.log.Timber
class RoomPreviewViewModel @AssistedInject constructor(@Assisted initialState: RoomPreviewViewState,
class RoomPreviewViewModel @AssistedInject constructor(@Assisted private val initialState: RoomPreviewViewState,
private val session: Session)
: VectorViewModel<RoomPreviewViewState, RoomPreviewAction, EmptyViewEvents>(initialState) {
@ -52,30 +54,41 @@ class RoomPreviewViewModel @AssistedInject constructor(@Assisted initialState: R
init {
// Observe joined room (from the sync)
observeJoinedRooms()
observeRoomSummary()
observeMembershipChanges()
}
private fun observeJoinedRooms() {
private fun observeRoomSummary() {
val queryParams = roomSummaryQueryParams {
memberships = listOf(Membership.JOIN)
roomId = QueryStringValue.Equals(initialState.roomId)
}
session
.rx()
.liveRoomSummaries(queryParams)
.subscribe { list ->
withState { state ->
val isRoomJoined = list
?.map { it.roomId }
?.toList()
?.contains(state.roomId) == true
val isRoomJoined = list.any {
it.membership == Membership.JOIN
}
if (isRoomJoined) {
setState { copy(roomJoinState = JoinState.JOINED) }
}
}
.disposeOnClear()
}
if (isRoomJoined) {
setState {
copy(
roomJoinState = JoinState.JOINED
)
}
}
private fun observeMembershipChanges() {
session.rx()
.liveRoomChangeMembershipState()
.subscribe {
val changeMembership = it[initialState.roomId] ?: ChangeMembershipState.Unknown
val joinState = when (changeMembership) {
is ChangeMembershipState.Joining -> JoinState.JOINING
is ChangeMembershipState.FailedJoining -> JoinState.JOINING_ERROR
// Other cases are handled by room summary
else -> null
}
if (joinState != null) {
setState { copy(roomJoinState = joinState) }
}
}
.disposeOnClear()
@ -83,37 +96,27 @@ class RoomPreviewViewModel @AssistedInject constructor(@Assisted initialState: R
override fun handle(action: RoomPreviewAction) {
when (action) {
is RoomPreviewAction.Join -> handleJoinRoom(action)
is RoomPreviewAction.Join -> handleJoinRoom()
}.exhaustive
}
private fun handleJoinRoom(action: RoomPreviewAction.Join) = withState { state ->
private fun handleJoinRoom() = withState { state ->
if (state.roomJoinState == JoinState.JOINING) {
// Request already sent, should not happen
Timber.w("Try to join an already joining room. Should not happen")
return@withState
}
setState {
copy(
roomJoinState = JoinState.JOINING,
lastError = null
)
}
session.joinRoom(action.roomAlias ?: state.roomId, callback = object : MatrixCallback<Unit> {
val viaServers = state.homeServer?.let {
listOf(it)
} ?: emptyList()
session.joinRoom(state.roomId, viaServers = viaServers, callback = 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
}
override fun onFailure(failure: Throwable) {
setState {
copy(
roomJoinState = JoinState.JOINING_ERROR,
lastError = failure
)
}
setState { copy(lastError = failure) }
}
})
}

View file

@ -22,11 +22,21 @@ import im.vector.riotx.features.roomdirectory.JoinState
data class RoomPreviewViewState(
// The room id
val roomId: String = "",
val roomAlias: String? = null,
/**
* The server name (might be null)
* Set null when the server is the current user's home server.
*/
val homeServer: String? = null,
// Current state of the room in preview
val roomJoinState: JoinState = JoinState.NOT_JOINED,
// Last error of join room request
val lastError: Throwable? = null
) : MvRxState {
constructor(args: RoomPreviewData) : this(roomId = args.roomId)
constructor(args: RoomPreviewData) : this(
roomId = args.roomId,
roomAlias = args.roomAlias,
homeServer = args.homeServer
)
}

View file

@ -112,7 +112,6 @@ class RoomProfileFragment @Inject constructor(
when (it) {
is RoomProfileViewEvents.Loading -> showLoading(it.message)
is RoomProfileViewEvents.Failure -> showFailure(it.throwable)
is RoomProfileViewEvents.OnLeaveRoomSuccess -> onLeaveRoom()
is RoomProfileViewEvents.ShareRoomProfile -> onShareRoomProfile(it.permalink)
RoomProfileViewEvents.OnChangeAvatarSuccess -> dismissLoadingDialog()
}.exhaustive

View file

@ -25,7 +25,6 @@ sealed class RoomProfileViewEvents : VectorViewEvents {
data class Loading(val message: CharSequence? = null) : RoomProfileViewEvents()
data class Failure(val throwable: Throwable) : RoomProfileViewEvents()
object OnLeaveRoomSuccess : RoomProfileViewEvents()
object OnChangeAvatarSuccess : RoomProfileViewEvents()
data class ShareRoomProfile(val permalink: String) : RoomProfileViewEvents()
}

View file

@ -25,6 +25,7 @@ import com.squareup.inject.assisted.AssistedInject
import im.vector.matrix.android.api.MatrixCallback
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.EventType
import im.vector.matrix.android.api.session.room.powerlevels.PowerLevelsHelper
import im.vector.matrix.rx.rx
import im.vector.matrix.rx.unwrap
@ -71,7 +72,9 @@ class RoomProfileViewModel @AssistedInject constructor(@Assisted private val ini
powerLevelsContentLive
.subscribe {
val powerLevelsHelper = PowerLevelsHelper(it)
setState { copy(canChangeAvatar = powerLevelsHelper.isUserAbleToChangeRoomAvatar(session.myUserId)) }
setState {
copy(canChangeAvatar = powerLevelsHelper.isUserAllowedToSend(session.myUserId, true, EventType.STATE_ROOM_AVATAR))
}
}
.disposeOnClear()
}
@ -95,7 +98,7 @@ class RoomProfileViewModel @AssistedInject constructor(@Assisted private val ini
_viewEvents.post(RoomProfileViewEvents.Loading(stringProvider.getString(R.string.room_profile_leaving_room)))
room.leave(null, object : MatrixCallback<Unit> {
override fun onSuccess(data: Unit) {
_viewEvents.post(RoomProfileViewEvents.OnLeaveRoomSuccess)
// Do nothing, we will be closing the room automatically when it will get back from sync
}
override fun onFailure(failure: Throwable) {

View file

@ -20,6 +20,7 @@ import com.airbnb.epoxy.TypedEpoxyController
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.RoomHistoryVisibilityContent
import im.vector.matrix.android.api.session.room.model.RoomSummary
import im.vector.riotx.R
import im.vector.riotx.core.epoxy.profiles.buildProfileAction
import im.vector.riotx.core.epoxy.profiles.buildProfileSection
@ -104,6 +105,13 @@ class RoomSettingsController @Inject constructor(
action = { if (data.actionPermissions.canChangeHistoryReadability) callback?.onHistoryVisibilityClicked() }
)
buildEncryptionAction(data.actionPermissions, roomSummary)
}
private fun buildEncryptionAction(actionPermissions: RoomSettingsViewState.ActionPermissions, roomSummary: RoomSummary) {
if (!actionPermissions.canEnableEncryption) {
return
}
if (roomSummary.isEncrypted) {
buildProfileAction(
id = "encryption",

View file

@ -101,10 +101,13 @@ class RoomSettingsViewModel @AssistedInject constructor(@Assisted initialState:
.subscribe {
val powerLevelsHelper = PowerLevelsHelper(it)
val permissions = RoomSettingsViewState.ActionPermissions(
canChangeName = powerLevelsHelper.isUserAbleToChangeRoomName(session.myUserId),
canChangeTopic = powerLevelsHelper.isUserAbleToChangeRoomTopic(session.myUserId),
canChangeCanonicalAlias = powerLevelsHelper.isUserAbleToChangeRoomCanonicalAlias(session.myUserId),
canChangeHistoryReadability = powerLevelsHelper.isUserAbleToChangeRoomHistoryReadability(session.myUserId)
canChangeName = powerLevelsHelper.isUserAllowedToSend(session.myUserId, true, EventType.STATE_ROOM_NAME),
canChangeTopic = powerLevelsHelper.isUserAllowedToSend(session.myUserId, true, EventType.STATE_ROOM_TOPIC),
canChangeCanonicalAlias = powerLevelsHelper.isUserAllowedToSend(session.myUserId, true,
EventType.STATE_ROOM_CANONICAL_ALIAS),
canChangeHistoryReadability = powerLevelsHelper.isUserAllowedToSend(session.myUserId, true,
EventType.STATE_ROOM_HISTORY_VISIBILITY),
canEnableEncryption = powerLevelsHelper.isUserAllowedToSend(session.myUserId, true, EventType.STATE_ROOM_ENCRYPTION)
)
setState { copy(actionPermissions = permissions) }
}

View file

@ -43,6 +43,7 @@ data class RoomSettingsViewState(
val canChangeName: Boolean = false,
val canChangeTopic: Boolean = false,
val canChangeCanonicalAlias: Boolean = false,
val canChangeHistoryReadability: Boolean = false
val canChangeHistoryReadability: Boolean = false,
val canEnableEncryption: Boolean = false
)
}

View file

@ -16,6 +16,7 @@
package im.vector.riotx.features.widgets
import android.net.Uri
import androidx.lifecycle.viewModelScope
import com.airbnb.mvrx.ActivityViewModelContext
import com.airbnb.mvrx.Fail
@ -236,7 +237,9 @@ class WidgetViewModel @AssistedInject constructor(@Assisted val initialState: Wi
_viewEvents.post(WidgetViewEvents.OnURLFormatted(formattedUrl))
} catch (failure: Throwable) {
if (failure is WidgetManagementFailure.TermsNotSignedException) {
_viewEvents.post(WidgetViewEvents.DisplayTerms(initialState.baseUrl, failure.token))
// Terms for IM shouldn't have path appended
val displayTermsBaseUrl = Uri.parse(initialState.baseUrl).buildUpon().path("").toString()
_viewEvents.post(WidgetViewEvents.DisplayTerms(displayTermsBaseUrl, failure.token))
}
setState { copy(formattedURL = Fail(failure)) }
}

View file

@ -60,7 +60,7 @@
app:layout_constraintTop_toTopOf="parent"
tools:text="@tools:sample/first_names" />
<im.vector.riotx.core.platform.EllipsizingTextView
<TextView
android:id="@+id/composer_related_message_preview"
android:layout_width="0dp"
android:layout_height="match_parent"

View file

@ -36,7 +36,7 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<im.vector.riotx.core.platform.EllipsizingTextView
<TextView
android:id="@+id/createDirectRoomTitle"
android:layout_width="0dp"
android:layout_height="wrap_content"

View file

@ -37,7 +37,7 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<im.vector.riotx.core.platform.EllipsizingTextView
<TextView
android:id="@+id/createDirectRoomTitle"
android:layout_width="0dp"
android:layout_height="wrap_content"

View file

@ -33,7 +33,7 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<im.vector.riotx.core.platform.EllipsizingTextView
<TextView
android:id="@+id/createRoomTitle"
android:layout_width="0dp"
android:layout_height="wrap_content"

View file

@ -28,7 +28,7 @@
android:contentDescription="@string/a11y_open_drawer"
tools:src="@tools:sample/avatars" />
<im.vector.riotx.core.platform.EllipsizingTextView
<TextView
android:id="@+id/groupToolbarTitleView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"

View file

@ -36,7 +36,7 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<im.vector.riotx.core.platform.EllipsizingTextView
<TextView
android:id="@+id/knownUsersTitle"
android:layout_width="0dp"
android:layout_height="wrap_content"

View file

@ -71,7 +71,7 @@
tools:ignore="MissingConstraints"
tools:src="@drawable/ic_shield_trusted" />
<im.vector.riotx.core.platform.EllipsizingTextView
<TextView
android:id="@+id/matrixProfileToolbarTitleView"
android:layout_width="0dp"
android:layout_height="wrap_content"

View file

@ -43,7 +43,7 @@
tools:ignore="MissingConstraints"
tools:src="@drawable/ic_shield_trusted" />
<im.vector.riotx.core.platform.EllipsizingTextView
<TextView
android:id="@+id/roomToolbarTitleView"
android:layout_width="0dp"
android:layout_height="wrap_content"
@ -60,7 +60,7 @@
app:layout_constraintTop_toTopOf="parent"
tools:text="@sample/matrix.json/data/roomName" />
<im.vector.riotx.core.platform.EllipsizingTextView
<TextView
android:id="@+id/roomToolbarSubtitleView"
android:layout_width="0dp"
android:layout_height="wrap_content"

View file

@ -33,7 +33,7 @@
app:layout_constraintTop_toTopOf="parent"
tools:src="@tools:sample/avatars" />
<im.vector.riotx.core.platform.EllipsizingTextView
<TextView
android:id="@+id/roomPreviewNoPreviewToolbarTitle"
android:layout_width="0dp"
android:layout_height="wrap_content"

View file

@ -33,7 +33,7 @@
app:layout_constraintTop_toTopOf="parent"
tools:src="@tools:sample/avatars" />
<im.vector.riotx.core.platform.EllipsizingTextView
<TextView
android:id="@+id/roomSettingsToolbarTitleView"
android:layout_width="0dp"
android:layout_height="wrap_content"

View file

@ -48,7 +48,7 @@
tools:ignore="MissingConstraints"
tools:src="@drawable/ic_shield_trusted" />
<im.vector.riotx.core.platform.EllipsizingTextView
<TextView
android:id="@+id/roomUploadsToolbarTitleView"
android:layout_width="0dp"
android:layout_height="wrap_content"

View file

@ -37,7 +37,7 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<im.vector.riotx.core.platform.EllipsizingTextView
<TextView
android:id="@+id/userDirectoryTitle"
android:layout_width="0dp"
android:layout_height="wrap_content"

View file

@ -37,7 +37,7 @@
android:textStyle="bold"
tools:text="name" />
<im.vector.riotx.core.platform.EllipsizingTextView
<TextView
android:id="@+id/itemAutocompleteEmojiSubname"
android:layout_width="wrap_content"
android:layout_height="wrap_content"

View file

@ -36,7 +36,7 @@
app:layout_constraintTop_toTopOf="parent"
tools:src="@drawable/ic_room_actions_notifications_all" />
<im.vector.riotx.core.platform.EllipsizingTextView
<TextView
android:id="@+id/actionTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"

View file

@ -21,7 +21,7 @@
app:layout_constraintVertical_bias="0"
tools:src="@tools:sample/avatars" />
<im.vector.riotx.core.platform.EllipsizingTextView
<TextView
android:id="@+id/bottom_sheet_message_preview_sender"
android:layout_width="0dp"
android:layout_height="wrap_content"
@ -41,7 +41,7 @@
app:layout_constraintTop_toTopOf="@id/bottom_sheet_message_preview_avatar"
tools:text="@tools:sample/full_names" />
<im.vector.riotx.core.platform.EllipsizingTextView
<TextView
android:id="@+id/bottom_sheet_message_preview_body"
android:layout_width="0dp"
android:layout_height="wrap_content"

View file

@ -25,7 +25,7 @@
app:layout_constraintVertical_bias="0"
tools:src="@tools:sample/avatars" />
<im.vector.riotx.core.platform.EllipsizingTextView
<TextView
android:id="@+id/bottomSheetRoomPreviewName"
android:layout_width="0dp"
android:layout_height="wrap_content"

View file

@ -36,7 +36,7 @@
android:visibility="visible" />
</FrameLayout>
<im.vector.riotx.core.platform.EllipsizingTextView
<TextView
android:id="@+id/createDirectRoomUserName"
android:layout_width="0dp"
android:layout_height="wrap_content"
@ -54,7 +54,7 @@
app:layout_constraintTop_toTopOf="parent"
tools:text="@tools:sample/full_names" />
<im.vector.riotx.core.platform.EllipsizingTextView
<TextView
android:id="@+id/createDirectRoomUserID"
android:layout_width="0dp"
android:layout_height="wrap_content"

View file

@ -55,7 +55,7 @@
android:textColor="?riotx_text_secondary"
android:textSize="12sp" />
<im.vector.riotx.core.platform.EllipsizingTextView
<TextView
android:id="@+id/itemDeviceId"
android:layout_width="match_parent"
android:layout_height="wrap_content"

View file

@ -8,7 +8,7 @@
android:foreground="?attr/selectableItemBackground"
android:minHeight="@dimen/item_form_min_height">
<im.vector.riotx.core.platform.EllipsizingTextView
<TextView
android:id="@+id/formSwitchTitle"
android:layout_width="0dp"
android:layout_height="wrap_content"

View file

@ -23,7 +23,7 @@
app:layout_constraintTop_toTopOf="parent"
tools:src="@tools:sample/avatars" />
<im.vector.riotx.core.platform.EllipsizingTextView
<TextView
android:id="@+id/groupNameView"
android:layout_width="0dp"
android:layout_height="wrap_content"

View file

@ -36,7 +36,7 @@
android:visibility="visible" />
</FrameLayout>
<im.vector.riotx.core.platform.EllipsizingTextView
<TextView
android:id="@+id/knownUserName"
android:layout_width="0dp"
android:layout_height="wrap_content"
@ -54,7 +54,7 @@
app:layout_constraintTop_toTopOf="parent"
tools:text="@tools:sample/full_names" />
<im.vector.riotx.core.platform.EllipsizingTextView
<TextView
android:id="@+id/knownUserID"
android:layout_width="0dp"
android:layout_height="wrap_content"

View file

@ -27,7 +27,7 @@
tools:src="@drawable/ic_room_profile_notification"
tools:visibility="visible" />
<im.vector.riotx.core.platform.EllipsizingTextView
<TextView
android:id="@+id/actionTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
@ -47,7 +47,7 @@
app:layout_goneMarginStart="0dp"
tools:text="@string/room_profile_section_security_learn_more" />
<im.vector.riotx.core.platform.EllipsizingTextView
<TextView
android:id="@+id/actionSubtitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"

View file

@ -35,7 +35,7 @@
tools:ignore="MissingConstraints"
tools:src="@drawable/ic_shield_trusted" />
<im.vector.riotx.core.platform.EllipsizingTextView
<TextView
android:id="@+id/matrixItemTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
@ -55,7 +55,7 @@
app:layout_goneMarginStart="0dp"
tools:text="@sample/matrix.json/data/displayName" />
<im.vector.riotx.core.platform.EllipsizingTextView
<TextView
android:id="@+id/matrixItemSubtitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"

View file

@ -23,7 +23,7 @@
app:layout_constraintTop_toTopOf="parent"
tools:src="@tools:sample/avatars" />
<im.vector.riotx.core.platform.EllipsizingTextView
<TextView
android:id="@+id/itemPublicRoomName"
android:layout_width="0dp"
android:layout_height="wrap_content"

View file

@ -66,7 +66,7 @@
app:layout_constraintTop_toBottomOf="@id/roomAvatarContainer"
tools:layout_marginStart="20dp" />
<im.vector.riotx.core.platform.EllipsizingTextView
<TextView
android:id="@+id/roomNameView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
@ -138,7 +138,7 @@
app:layout_constraintTop_toTopOf="@+id/roomNameView"
tools:text="@tools:sample/date/hhmm" />
<im.vector.riotx.core.platform.EllipsizingTextView
<TextView
android:id="@+id/roomLastEventView"
android:layout_width="0dp"
android:layout_height="wrap_content"
@ -154,7 +154,7 @@
app:layout_constraintTop_toBottomOf="@+id/roomNameView"
tools:text="@sample/matrix.json/data/message" />
<im.vector.riotx.core.platform.EllipsizingTextView
<TextView
android:id="@+id/roomTypingView"
android:layout_width="0dp"
android:layout_height="wrap_content"

View file

@ -17,7 +17,7 @@
android:paddingRight="@dimen/layout_horizontal_margin"
android:paddingBottom="4dp">
<im.vector.riotx.core.platform.EllipsizingTextView
<TextView
android:id="@+id/roomCategoryTitleView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"

View file

@ -24,7 +24,7 @@
app:layout_constraintTop_toTopOf="parent"
tools:src="@tools:sample/avatars" />
<im.vector.riotx.core.platform.EllipsizingTextView
<TextView
android:id="@+id/itemRoomDirectoryName"
android:layout_width="0dp"
android:layout_height="wrap_content"
@ -43,7 +43,7 @@
app:layout_constraintVertical_chainStyle="packed"
tools:text="@tools:sample/lorem/random" />
<im.vector.riotx.core.platform.EllipsizingTextView
<TextView
android:id="@+id/itemRoomDirectoryDescription"
android:layout_width="0dp"
android:layout_height="wrap_content"

View file

@ -30,7 +30,7 @@
app:layout_constraintTop_toBottomOf="@+id/roomInvitationAvatarImageView"
tools:layout_marginStart="20dp" />
<im.vector.riotx.core.platform.EllipsizingTextView
<TextView
android:id="@+id/roomInvitationNameView"
android:layout_width="0dp"
android:layout_height="wrap_content"
@ -54,7 +54,7 @@
app:layout_constraintTop_toTopOf="parent"
tools:text="@sample/matrix.json/data/displayName" />
<im.vector.riotx.core.platform.EllipsizingTextView
<TextView
android:id="@+id/roomInvitationSubTitle"
android:layout_width="0dp"
android:layout_height="wrap_content"

View file

@ -23,7 +23,7 @@
android:layout_marginTop="4dp"
tools:src="@tools:sample/avatars" />
<im.vector.riotx.core.platform.EllipsizingTextView
<TextView
android:id="@+id/messageMemberNameView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"

View file

@ -18,7 +18,7 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<im.vector.riotx.core.platform.EllipsizingTextView
<TextView
android:id="@+id/uploadsFileTitle"
android:layout_width="0dp"
android:layout_height="wrap_content"
@ -35,7 +35,7 @@
app:layout_constraintVertical_chainStyle="packed"
tools:text="Filename.file" />
<im.vector.riotx.core.platform.EllipsizingTextView
<TextView
android:id="@+id/uploadsFileSubtitle"
android:layout_width="0dp"
android:layout_height="wrap_content"

View file

@ -20,7 +20,7 @@
app:layout_constraintTop_toTopOf="parent"
tools:src="@tools:sample/avatars" />
<im.vector.riotx.core.platform.EllipsizingTextView
<TextView
android:id="@+id/itemUserId"
android:layout_width="0dp"
android:layout_height="wrap_content"
@ -38,7 +38,7 @@
app:layout_constraintVertical_chainStyle="packed"
tools:text="@sample/matrix.json/data/mxid" />
<im.vector.riotx.core.platform.EllipsizingTextView
<TextView
android:id="@+id/itemUserName"
android:layout_width="0dp"
android:layout_height="wrap_content"

View file

@ -55,7 +55,7 @@
tools:text="@tools:sample/first_names"
tools:visibility="gone" />
<im.vector.riotx.core.platform.EllipsizingTextView
<TextView
android:id="@+id/composer_related_message_preview"
android:layout_width="0dp"
android:layout_height="wrap_content"

View file

@ -57,34 +57,35 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/inviteIdentifierView" />
<com.google.android.material.button.MaterialButton
android:id="@+id/inviteRejectView"
style="@style/VectorButtonStyleText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:layout_marginEnd="4dp"
android:layout_marginRight="4dp"
android:minWidth="120dp"
android:text="@string/reject"
app:layout_constraintEnd_toStartOf="@+id/inviteAcceptView"
app:layout_constraintHorizontal_chainStyle="packed"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/inviteLabelView" />
<com.google.android.material.button.MaterialButton
<im.vector.riotx.core.platform.ButtonStateView
android:id="@+id/inviteAcceptView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
android:layout_marginLeft="4dp"
android:minWidth="120dp"
android:text="@string/accept"
app:bsv_button_text="@string/accept"
app:bsv_loaded_image_src="@drawable/ic_tick"
app:bsv_use_flat_button="false"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_chainStyle="packed"
app:layout_constraintStart_toEndOf="@+id/inviteRejectView"
app:layout_constraintTop_toTopOf="@id/inviteRejectView" />
<im.vector.riotx.core.platform.ButtonStateView
android:id="@+id/inviteRejectView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:layout_marginEnd="4dp"
android:minWidth="120dp"
app:bsv_button_text="@string/reject"
app:bsv_loaded_image_src="@drawable/ic_tick"
app:bsv_use_flat_button="true"
app:layout_constraintEnd_toStartOf="@+id/inviteAcceptView"
app:layout_constraintHorizontal_chainStyle="packed"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/inviteLabelView"/>
<Space
android:layout_width="match_parent"
android:layout_height="16dp"

View file

@ -29,6 +29,7 @@
android:layout_width="32dp"
android:layout_height="32dp"
android:layout_gravity="center"
android:indeterminateOnly="true"
android:scaleType="center"
tools:layout_gravity="center_horizontal"
tools:layout_marginTop="80dp" />