Merge branch 'develop' into feature/ons/device_manager_security_sessions

This commit is contained in:
Onuray Sahin 2022-09-20 17:11:51 +03:00
commit 45cf7dcd63
60 changed files with 819 additions and 294 deletions

View File

@ -31,7 +31,7 @@ jobs:
ui-tests:
name: UI Tests (Synapse)
needs: should-i-run
runs-on: macos-latest
runs-on: buildjet-4vcpu-ubuntu-2204
strategy:
fail-fast: false
matrix:

View File

@ -13,7 +13,10 @@ env:
jobs:
tests:
name: Runs all tests
runs-on: macos-latest # for the emulator
runs-on: buildjet-4vcpu-ubuntu-2204
strategy:
matrix:
api-level: [28]
# Allow all jobs on main and develop. Just one per PR.
concurrency:
group: ${{ github.ref == 'refs/heads/main' && format('unit-tests-main-{0}', github.sha) || github.ref == 'refs/heads/develop' && format('unit-tests-develop-{0}', github.sha) || format('unit-tests-{0}', github.ref) }}
@ -36,40 +39,70 @@ jobs:
httpPort: 8080
disableRateLimiting: true
public_baseurl: "http://10.0.2.2:8080/"
- name: AVD cache
uses: actions/cache@v3
id: avd-cache
with:
path: |
~/.android/avd/*
~/.android/adb*
key: avd-${{ matrix.api-level }}
- name: create AVD and generate snapshot for caching
if: steps.avd-cache.outputs.cache-hit != 'true'
uses: reactivecircus/android-emulator-runner@v2
with:
api-level: ${{ matrix.api-level }}
arch: x86
profile: Nexus 5X
force-avd-creation: true # Is set to false in the doc https://github.com/ReactiveCircus/android-emulator-runner
emulator-options: -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
disable-animations: true
script: echo "Generated AVD snapshot for caching."
- name: Run all the codecoverage tests at once
id: tests
uses: reactivecircus/android-emulator-runner@v2
continue-on-error: true
# continue-on-error: true
with:
api-level: 28
api-level: ${{ matrix.api-level }}
arch: x86
profile: Nexus 5X
force-avd-creation: false
emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
disable-animations: true
emulator-build: 7425822
# emulator-build: 7425822
script: |
./gradlew gatherGplayDebugStringTemplates $CI_GRADLE_ARG_PROPERTIES
./gradlew unitTestsWithCoverage $CI_GRADLE_ARG_PROPERTIES
./gradlew instrumentationTestsWithCoverage $CI_GRADLE_ARG_PROPERTIES
./gradlew generateCoverageReport $CI_GRADLE_ARG_PROPERTIES
# NB: continue-on-error marks steps.tests.conclusion = 'success' but leaves stes.tests.outcome = 'failure'
- name: Run all the codecoverage tests at once (retry if emulator failed)
uses: reactivecircus/android-emulator-runner@v2
if: always() && steps.tests.outcome == 'failure' # don't run if previous step succeeded.
# NB: continue-on-error marks steps.tests.conclusion = 'success' but leaves steps.tests.outcome = 'failure'
### - name: Run all the codecoverage tests at once (retry if emulator failed)
### uses: reactivecircus/android-emulator-runner@v2
### if: always() && steps.tests.outcome == 'failure' # don't run if previous step succeeded.
### with:
### api-level: 28
### arch: x86
### profile: Nexus 5X
### force-avd-creation: false
### emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
### disable-animations: true
### emulator-build: 7425822
### script: |
### ./gradlew gatherGplayDebugStringTemplates $CI_GRADLE_ARG_PROPERTIES
### ./gradlew unitTestsWithCoverage $CI_GRADLE_ARG_PROPERTIES
### ./gradlew instrumentationTestsWithCoverage $CI_GRADLE_ARG_PROPERTIES
### ./gradlew generateCoverageReport $CI_GRADLE_ARG_PROPERTIES
- name: Upload Integration Test Report Log
uses: actions/upload-artifact@v3
if: always()
with:
api-level: 28
arch: x86
profile: Nexus 5X
force-avd-creation: false
emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
disable-animations: true
emulator-build: 7425822
script: |
./gradlew gatherGplayDebugStringTemplates $CI_GRADLE_ARG_PROPERTIES
./gradlew unitTestsWithCoverage $CI_GRADLE_ARG_PROPERTIES
./gradlew instrumentationTestsWithCoverage $CI_GRADLE_ARG_PROPERTIES
./gradlew generateCoverageReport $CI_GRADLE_ARG_PROPERTIES
name: integration-test-error-results
path: |
*/build/outputs/androidTest-results/connected/
*/build/reports/androidTests/connected/
# we may have failed a previous step and retried, that's OK
- name: Publish results to Sonar

1
changelog.d/6970.wip Normal file
View File

@ -0,0 +1 @@
Create DM room only on first message - Add a spinner when sending the first message

1
changelog.d/7079.bugfix Normal file
View File

@ -0,0 +1 @@
Fixed problem when room list's scroll did jump after rooms placeholders were replaced with rooms summary items

1
changelog.d/7108.misc Normal file
View File

@ -0,0 +1 @@
Move some GitHub actions to buildjet runners, and remove the second attempt to run integration tests.

1
changelog.d/7153.wip Normal file
View File

@ -0,0 +1 @@
Create DM room only on first message - Handle the local rooms within the new AppLayout

1
changelog.d/7166.misc Normal file
View File

@ -0,0 +1 @@
New App Layout is now enabled by default! Go to the Settings > Labs to toggle this

View File

@ -25,6 +25,7 @@ import org.matrix.android.sdk.api.session.room.getStateEvent
import org.matrix.android.sdk.api.session.room.getTimelineEvent
import org.matrix.android.sdk.api.session.room.members.RoomMemberQueryParams
import org.matrix.android.sdk.api.session.room.model.EventAnnotationsSummary
import org.matrix.android.sdk.api.session.room.model.LocalRoomSummary
import org.matrix.android.sdk.api.session.room.model.ReadReceipt
import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary
import org.matrix.android.sdk.api.session.room.model.RoomSummary
@ -46,6 +47,13 @@ class FlowRoom(private val room: Room) {
}
}
fun liveLocalRoomSummary(): Flow<Optional<LocalRoomSummary>> {
return room.getLocalRoomSummaryLive().asFlow()
.startWith(room.coroutineDispatchers.io) {
room.localRoomSummary().toOptional()
}
}
fun liveRoomMembers(queryParams: RoomMemberQueryParams): Flow<List<RoomMemberSummary>> {
return room.membershipService().getRoomMembersLive(queryParams).asFlow()
.startWith(room.coroutineDispatchers.io) {

View File

@ -24,6 +24,7 @@ import org.matrix.android.sdk.api.session.room.call.RoomCallService
import org.matrix.android.sdk.api.session.room.crypto.RoomCryptoService
import org.matrix.android.sdk.api.session.room.location.LocationSharingService
import org.matrix.android.sdk.api.session.room.members.MembershipService
import org.matrix.android.sdk.api.session.room.model.LocalRoomSummary
import org.matrix.android.sdk.api.session.room.model.RoomSummary
import org.matrix.android.sdk.api.session.room.model.relation.RelationService
import org.matrix.android.sdk.api.session.room.notification.RoomPushRuleService
@ -60,11 +61,22 @@ interface Room {
*/
fun getRoomSummaryLive(): LiveData<Optional<RoomSummary>>
/**
* A live [LocalRoomSummary] associated with the room.
* You can observe this summary to get dynamic data from this room.
*/
fun getLocalRoomSummaryLive(): LiveData<Optional<LocalRoomSummary>>
/**
* A current snapshot of [RoomSummary] associated with the room.
*/
fun roomSummary(): RoomSummary?
/**
* A current snapshot of [LocalRoomSummary] associated with the room.
*/
fun localRoomSummary(): LocalRoomSummary?
/**
* Use this room as a Space, if the type is correct.
*/

View File

@ -22,6 +22,7 @@ import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.identity.model.SignInvitationResult
import org.matrix.android.sdk.api.session.room.alias.RoomAliasDescription
import org.matrix.android.sdk.api.session.room.members.ChangeMembershipState
import org.matrix.android.sdk.api.session.room.model.LocalRoomSummary
import org.matrix.android.sdk.api.session.room.model.Membership
import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary
import org.matrix.android.sdk.api.session.room.model.RoomSummary
@ -117,6 +118,12 @@ interface RoomService {
*/
fun getRoomSummaryLive(roomId: String): LiveData<Optional<RoomSummary>>
/**
* A live [LocalRoomSummary] associated with the room with id [roomId].
* You can observe this summary to get dynamic data from this room, even if the room is not joined yet
*/
fun getLocalRoomSummaryLive(roomId: String): LiveData<Optional<LocalRoomSummary>>
/**
* Get a snapshot list of room summaries.
* @return the immutable list of [RoomSummary]

View File

@ -0,0 +1,24 @@
/*
* Copyright 2022 The Matrix.org Foundation C.I.C.
*
* 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 org.matrix.android.sdk.api.session.room.model
enum class LocalRoomCreationState {
NOT_CREATED,
CREATING,
FAILURE,
CREATED
}

View File

@ -0,0 +1,46 @@
/*
* Copyright 2022 The Matrix.org Foundation C.I.C.
*
* 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 org.matrix.android.sdk.api.session.room.model
import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams
/**
* This class holds some data of a local room.
* It can be retrieved by [org.matrix.android.sdk.api.session.room.Room] and [org.matrix.android.sdk.api.session.room.RoomService]
*/
data class LocalRoomSummary(
/**
* The roomId of the room.
*/
val roomId: String,
/**
* The room summary of the room.
*/
val roomSummary: RoomSummary?,
/**
* The creation params attached to the room.
*/
val createRoomParams: CreateRoomParams?,
/**
* The roomId of the created room (ie. created on the server), if any.
*/
val replacementRoomId: String?,
/**
* The creation state of the room.
*/
val creationState: LocalRoomCreationState,
)

View File

@ -53,6 +53,7 @@ import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo033
import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo034
import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo035
import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo036
import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo037
import org.matrix.android.sdk.internal.util.Normalizer
import org.matrix.android.sdk.internal.util.database.MatrixRealmMigration
import javax.inject.Inject
@ -61,7 +62,7 @@ internal class RealmSessionStoreMigration @Inject constructor(
private val normalizer: Normalizer
) : MatrixRealmMigration(
dbName = "Session",
schemaVersion = 36L,
schemaVersion = 37L,
) {
/**
* Forces all RealmSessionStoreMigration instances to be equal.
@ -107,5 +108,6 @@ internal class RealmSessionStoreMigration @Inject constructor(
if (oldVersion < 34) MigrateSessionTo034(realm).perform()
if (oldVersion < 35) MigrateSessionTo035(realm).perform()
if (oldVersion < 36) MigrateSessionTo036(realm).perform()
if (oldVersion < 37) MigrateSessionTo037(realm).perform()
}
}

View File

@ -0,0 +1,36 @@
/*
* Copyright 2022 The Matrix.org Foundation C.I.C.
*
* 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 org.matrix.android.sdk.internal.database.mapper
import org.matrix.android.sdk.api.session.room.model.LocalRoomSummary
import org.matrix.android.sdk.internal.database.model.LocalRoomSummaryEntity
import javax.inject.Inject
internal class LocalRoomSummaryMapper @Inject constructor(
private val roomSummaryMapper: RoomSummaryMapper,
) {
fun map(localRoomSummaryEntity: LocalRoomSummaryEntity): LocalRoomSummary {
return LocalRoomSummary(
roomId = localRoomSummaryEntity.roomId,
roomSummary = localRoomSummaryEntity.roomSummaryEntity?.let { roomSummaryMapper.map(it) },
createRoomParams = localRoomSummaryEntity.createRoomParams,
replacementRoomId = localRoomSummaryEntity.replacementRoomId,
creationState = localRoomSummaryEntity.creationState
)
}
}

View File

@ -0,0 +1,34 @@
/*
* Copyright (c) 2022 The Matrix.org Foundation C.I.C.
*
* 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 org.matrix.android.sdk.internal.database.migration
import io.realm.DynamicRealm
import org.matrix.android.sdk.api.session.room.model.LocalRoomCreationState
import org.matrix.android.sdk.internal.database.model.LocalRoomSummaryEntityFields
import org.matrix.android.sdk.internal.util.database.RealmMigrator
internal class MigrateSessionTo037(realm: DynamicRealm) : RealmMigrator(realm, 37) {
override fun doMigrate(realm: DynamicRealm) {
realm.schema.get("LocalRoomSummaryEntity")
?.addField(LocalRoomSummaryEntityFields.REPLACEMENT_ROOM_ID, String::class.java)
?.addField(LocalRoomSummaryEntityFields.STATE_STR, String::class.java)
?.transform { obj ->
obj.set(LocalRoomSummaryEntityFields.STATE_STR, LocalRoomCreationState.NOT_CREATED.name)
}
}
}

View File

@ -18,15 +18,24 @@ package org.matrix.android.sdk.internal.database.model
import io.realm.RealmObject
import io.realm.annotations.PrimaryKey
import org.matrix.android.sdk.api.session.room.model.LocalRoomCreationState
import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams
import org.matrix.android.sdk.api.session.room.model.create.toJSONString
internal open class LocalRoomSummaryEntity(
@PrimaryKey var roomId: String = "",
var roomSummaryEntity: RoomSummaryEntity? = null,
private var createRoomParamsStr: String? = null
var replacementRoomId: String? = null,
) : RealmObject() {
private var stateStr: String = LocalRoomCreationState.NOT_CREATED.name
var creationState: LocalRoomCreationState
get() = LocalRoomCreationState.valueOf(stateStr)
set(value) {
stateStr = value.name
}
private var createRoomParamsStr: String? = null
var createRoomParams: CreateRoomParams?
get() {
return CreateRoomParams.fromJson(createRoomParamsStr)

View File

@ -22,10 +22,6 @@ import io.realm.kotlin.where
import org.matrix.android.sdk.internal.database.model.LocalRoomSummaryEntity
import org.matrix.android.sdk.internal.database.model.LocalRoomSummaryEntityFields
internal fun LocalRoomSummaryEntity.Companion.where(realm: Realm, roomId: String? = null): RealmQuery<LocalRoomSummaryEntity> {
val query = realm.where<LocalRoomSummaryEntity>()
if (roomId != null) {
query.equalTo(LocalRoomSummaryEntityFields.ROOM_ID, roomId)
}
return query
internal fun LocalRoomSummaryEntity.Companion.where(realm: Realm, roomId: String): RealmQuery<LocalRoomSummaryEntity> {
return realm.where<LocalRoomSummaryEntity>().equalTo(LocalRoomSummaryEntityFields.ROOM_ID, roomId)
}

View File

@ -33,6 +33,11 @@ internal fun ReadReceiptEntity.Companion.whereUserId(realm: Realm, userId: Strin
.equalTo(ReadReceiptEntityFields.USER_ID, userId)
}
internal fun ReadReceiptEntity.Companion.whereRoomId(realm: Realm, roomId: String): RealmQuery<ReadReceiptEntity> {
return realm.where<ReadReceiptEntity>()
.equalTo(ReadReceiptEntityFields.ROOM_ID, roomId)
}
internal fun ReadReceiptEntity.Companion.createUnmanaged(roomId: String, eventId: String, userId: String, originServerTs: Double): ReadReceiptEntity {
return ReadReceiptEntity().apply {
this.primaryKey = "${roomId}_$userId"

View File

@ -25,6 +25,7 @@ import org.matrix.android.sdk.api.session.room.call.RoomCallService
import org.matrix.android.sdk.api.session.room.crypto.RoomCryptoService
import org.matrix.android.sdk.api.session.room.location.LocationSharingService
import org.matrix.android.sdk.api.session.room.members.MembershipService
import org.matrix.android.sdk.api.session.room.model.LocalRoomSummary
import org.matrix.android.sdk.api.session.room.model.RoomSummary
import org.matrix.android.sdk.api.session.room.model.RoomType
import org.matrix.android.sdk.api.session.room.model.relation.RelationService
@ -82,6 +83,14 @@ internal class DefaultRoom(
return roomSummaryDataSource.getRoomSummary(roomId)
}
override fun getLocalRoomSummaryLive(): LiveData<Optional<LocalRoomSummary>> {
return roomSummaryDataSource.getLocalRoomSummaryLive(roomId)
}
override fun localRoomSummary(): LocalRoomSummary? {
return roomSummaryDataSource.getLocalRoomSummary(roomId)
}
override fun asSpace(): Space? {
if (roomSummary()?.roomType != RoomType.SPACE) return null
return DefaultSpace(this, roomSummaryDataSource, viaParameterFinder)

View File

@ -29,10 +29,12 @@ import org.matrix.android.sdk.api.session.room.RoomSummaryQueryParams
import org.matrix.android.sdk.api.session.room.UpdatableLivePageResult
import org.matrix.android.sdk.api.session.room.alias.RoomAliasDescription
import org.matrix.android.sdk.api.session.room.members.ChangeMembershipState
import org.matrix.android.sdk.api.session.room.model.LocalRoomSummary
import org.matrix.android.sdk.api.session.room.model.Membership
import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary
import org.matrix.android.sdk.api.session.room.model.RoomSummary
import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams
import org.matrix.android.sdk.api.session.room.model.localecho.RoomLocalEcho
import org.matrix.android.sdk.api.session.room.peeking.PeekResult
import org.matrix.android.sdk.api.session.room.roomSummaryQueryParams
import org.matrix.android.sdk.api.session.room.summary.RoomAggregateNotificationCount
@ -106,6 +108,10 @@ internal class DefaultRoomService @Inject constructor(
return roomSummaryDataSource.getRoomSummaryLive(roomId)
}
override fun getLocalRoomSummaryLive(roomId: String): LiveData<Optional<LocalRoomSummary>> {
return roomSummaryDataSource.getLocalRoomSummaryLive(roomId)
}
override fun getRoomSummaries(
queryParams: RoomSummaryQueryParams,
sortOrder: RoomSortOrder
@ -173,7 +179,10 @@ internal class DefaultRoomService @Inject constructor(
}
override suspend fun onRoomDisplayed(roomId: String) {
updateBreadcrumbsTask.execute(UpdateBreadcrumbsTask.Params(roomId))
// Do not add local rooms to the recent rooms list as they should not be known by the server
if (!RoomLocalEcho.isLocalEchoId(roomId)) {
updateBreadcrumbsTask.execute(UpdateBreadcrumbsTask.Params(roomId))
}
}
override suspend fun joinRoom(roomIdOrAlias: String, reason: String?, viaServers: List<String>) {

View File

@ -17,38 +17,23 @@
package org.matrix.android.sdk.internal.session.room.create
import com.zhuinden.monarchy.Monarchy
import io.realm.kotlin.where
import kotlinx.coroutines.TimeoutCancellationException
import org.matrix.android.sdk.api.extensions.orFalse
import org.matrix.android.sdk.api.query.QueryStringValue
import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.api.session.events.model.toContent
import org.matrix.android.sdk.api.session.events.model.toModel
import org.matrix.android.sdk.api.session.room.failure.CreateRoomFailure
import org.matrix.android.sdk.api.session.room.model.LocalRoomCreationState
import org.matrix.android.sdk.api.session.room.model.RoomSummary
import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams
import org.matrix.android.sdk.api.session.room.model.tombstone.RoomTombstoneContent
import org.matrix.android.sdk.api.session.room.send.SendState
import org.matrix.android.sdk.internal.database.awaitNotEmptyResult
import org.matrix.android.sdk.internal.database.mapper.toEntity
import org.matrix.android.sdk.internal.database.model.CurrentStateEventEntity
import org.matrix.android.sdk.internal.database.model.EventEntity
import org.matrix.android.sdk.internal.database.model.EventEntityFields
import org.matrix.android.sdk.internal.database.model.EventInsertType
import org.matrix.android.sdk.internal.database.model.LocalRoomSummaryEntity
import org.matrix.android.sdk.internal.database.model.LocalRoomSummaryEntityFields
import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity
import org.matrix.android.sdk.internal.database.model.RoomSummaryEntityFields
import org.matrix.android.sdk.internal.database.query.copyToRealmOrIgnore
import org.matrix.android.sdk.internal.database.query.getOrCreate
import org.matrix.android.sdk.internal.database.query.where
import org.matrix.android.sdk.internal.database.query.whereRoomId
import org.matrix.android.sdk.internal.di.SessionDatabase
import org.matrix.android.sdk.internal.di.UserId
import org.matrix.android.sdk.internal.session.room.state.StateEventDataSource
import org.matrix.android.sdk.internal.session.room.summary.RoomSummaryDataSource
import org.matrix.android.sdk.internal.task.Task
import org.matrix.android.sdk.internal.util.awaitTransaction
import org.matrix.android.sdk.internal.util.time.Clock
import java.util.UUID
import java.util.concurrent.TimeUnit
import javax.inject.Inject
@ -56,94 +41,100 @@ import javax.inject.Inject
* Create a room on the server from a local room.
* The configuration of the local room will be use to configure the new room.
* The potential local room members will also be invited to this new room.
*
* A local tombstone event will be created to indicate that the local room has been replacing by the new one.
*/
internal interface CreateRoomFromLocalRoomTask : Task<CreateRoomFromLocalRoomTask.Params, String> {
data class Params(val localRoomId: String)
}
internal class DefaultCreateRoomFromLocalRoomTask @Inject constructor(
@UserId private val userId: String,
@SessionDatabase private val monarchy: Monarchy,
private val createRoomTask: CreateRoomTask,
private val stateEventDataSource: StateEventDataSource,
private val clock: Clock,
private val roomSummaryDataSource: RoomSummaryDataSource,
) : CreateRoomFromLocalRoomTask {
private val realmConfiguration
get() = monarchy.realmConfiguration
override suspend fun execute(params: CreateRoomFromLocalRoomTask.Params): String {
val replacementRoomId = stateEventDataSource.getStateEvent(params.localRoomId, EventType.STATE_ROOM_TOMBSTONE, QueryStringValue.IsEmpty)
?.content.toModel<RoomTombstoneContent>()
?.replacementRoomId
val localRoomSummary = roomSummaryDataSource.getLocalRoomSummary(params.localRoomId)
?: error("## CreateRoomFromLocalRoomTask - Cannot retrieve LocalRoomSummary with roomId ${params.localRoomId}")
if (replacementRoomId != null) {
return replacementRoomId
// If a room has already been created for the given local room, return the existing roomId
if (localRoomSummary.replacementRoomId != null) {
return localRoomSummary.replacementRoomId
}
var createRoomParams: CreateRoomParams? = null
var isEncrypted = false
monarchy.doWithRealm { realm ->
realm.where<LocalRoomSummaryEntity>()
.equalTo(LocalRoomSummaryEntityFields.ROOM_ID, params.localRoomId)
.findFirst()
?.let {
createRoomParams = it.createRoomParams
isEncrypted = it.roomSummaryEntity?.isEncrypted.orFalse()
}
if (localRoomSummary.createRoomParams != null && localRoomSummary.roomSummary != null) {
return createRoom(params.localRoomId, localRoomSummary.roomSummary, localRoomSummary.createRoomParams)
} else {
error("## CreateRoomFromLocalRoomTask - Invalid LocalRoomSummary: $localRoomSummary")
}
val roomId = createRoomTask.execute(createRoomParams!!)
}
/**
* Create a room on the server for the given local room.
*
* @param localRoomId the local room identifier.
* @param localRoomSummary the RoomSummary of the local room.
* @param createRoomParams the CreateRoomParams object which was used to configure the local room.
*
* @return the identifier of the created room.
*/
private suspend fun createRoom(localRoomId: String, localRoomSummary: RoomSummary, createRoomParams: CreateRoomParams): String {
updateCreationState(localRoomId, LocalRoomCreationState.CREATING)
val replacementRoomId = runCatching {
createRoomTask.execute(createRoomParams)
}.fold(
{ it },
{
updateCreationState(localRoomId, LocalRoomCreationState.FAILURE)
throw it
}
)
updateReplacementRoomId(localRoomId, replacementRoomId)
waitForRoomEvents(replacementRoomId, localRoomSummary)
updateCreationState(localRoomId, LocalRoomCreationState.CREATED)
return replacementRoomId
}
/**
* Wait for all the room events before triggering the created state.
*
* @param replacementRoomId the identifier of the created room
* @param localRoomSummary the RoomSummary of the local room.
*/
private suspend fun waitForRoomEvents(replacementRoomId: String, localRoomSummary: RoomSummary) {
try {
// Wait for all the room events before triggering the replacement room
awaitNotEmptyResult(realmConfiguration, TimeUnit.MINUTES.toMillis(1L)) { realm ->
realm.where(RoomSummaryEntity::class.java)
.equalTo(RoomSummaryEntityFields.ROOM_ID, roomId)
.equalTo(RoomSummaryEntityFields.INVITED_MEMBERS_COUNT, createRoomParams?.invitedUserIds?.size ?: 0)
.equalTo(RoomSummaryEntityFields.ROOM_ID, replacementRoomId)
.equalTo(RoomSummaryEntityFields.INVITED_MEMBERS_COUNT, localRoomSummary.invitedMembersCount)
}
awaitNotEmptyResult(realmConfiguration, TimeUnit.MINUTES.toMillis(1L)) { realm ->
EventEntity.whereRoomId(realm, roomId)
EventEntity.whereRoomId(realm, replacementRoomId)
.equalTo(EventEntityFields.TYPE, EventType.STATE_ROOM_HISTORY_VISIBILITY)
}
if (isEncrypted) {
if (localRoomSummary.isEncrypted) {
awaitNotEmptyResult(realmConfiguration, TimeUnit.MINUTES.toMillis(1L)) { realm ->
EventEntity.whereRoomId(realm, roomId)
EventEntity.whereRoomId(realm, replacementRoomId)
.equalTo(EventEntityFields.TYPE, EventType.STATE_ROOM_ENCRYPTION)
}
}
} catch (exception: TimeoutCancellationException) {
throw CreateRoomFailure.CreatedWithTimeout(roomId)
updateCreationState(localRoomSummary.roomId, LocalRoomCreationState.FAILURE)
throw CreateRoomFailure.CreatedWithTimeout(replacementRoomId)
}
createTombstoneEvent(params, roomId)
return roomId
}
/**
* Create a Tombstone event to indicate that the local room has been replaced by a new one.
*/
private suspend fun createTombstoneEvent(params: CreateRoomFromLocalRoomTask.Params, roomId: String) {
val now = clock.epochMillis()
val event = Event(
type = EventType.STATE_ROOM_TOMBSTONE,
senderId = userId,
originServerTs = now,
stateKey = "",
eventId = UUID.randomUUID().toString(),
content = RoomTombstoneContent(
replacementRoomId = roomId
).toContent()
)
monarchy.awaitTransaction { realm ->
val eventEntity = event.toEntity(params.localRoomId, SendState.SYNCED, now).copyToRealmOrIgnore(realm, EventInsertType.INCREMENTAL_SYNC)
if (event.stateKey != null && event.type != null && event.eventId != null) {
CurrentStateEventEntity.getOrCreate(realm, params.localRoomId, event.stateKey, event.type).apply {
eventId = event.eventId
root = eventEntity
}
}
private fun updateCreationState(roomId: String, creationState: LocalRoomCreationState) {
monarchy.runTransactionSync { realm ->
LocalRoomSummaryEntity.where(realm, roomId).findFirst()?.creationState = creationState
}
}
private fun updateReplacementRoomId(localRoomId: String, replacementRoomId: String) {
monarchy.runTransactionSync { realm ->
LocalRoomSummaryEntity.where(realm, localRoomId).findFirst()?.replacementRoomId = replacementRoomId
}
}
}

View File

@ -22,12 +22,15 @@ import org.matrix.android.sdk.internal.database.model.ChunkEntity
import org.matrix.android.sdk.internal.database.model.CurrentStateEventEntity
import org.matrix.android.sdk.internal.database.model.EventEntity
import org.matrix.android.sdk.internal.database.model.LocalRoomSummaryEntity
import org.matrix.android.sdk.internal.database.model.ReadReceiptEntity
import org.matrix.android.sdk.internal.database.model.ReadReceiptsSummaryEntity
import org.matrix.android.sdk.internal.database.model.RoomEntity
import org.matrix.android.sdk.internal.database.model.RoomMemberSummaryEntity
import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity
import org.matrix.android.sdk.internal.database.model.TimelineEventEntity
import org.matrix.android.sdk.internal.database.model.deleteOnCascade
import org.matrix.android.sdk.internal.database.query.where
import org.matrix.android.sdk.internal.database.query.whereInRoom
import org.matrix.android.sdk.internal.database.query.whereRoomId
import org.matrix.android.sdk.internal.di.SessionDatabase
import org.matrix.android.sdk.internal.session.room.delete.DeleteLocalRoomTask.Params
@ -50,6 +53,12 @@ internal class DefaultDeleteLocalRoomTask @Inject constructor(
if (RoomLocalEcho.isLocalEchoId(roomId)) {
monarchy.awaitTransaction { realm ->
Timber.i("## DeleteLocalRoomTask - delete local room id $roomId")
ReadReceiptsSummaryEntity.whereInRoom(realm, roomId = roomId).findAll()
?.also { Timber.i("## DeleteLocalRoomTask - ReadReceiptsSummaryEntity - delete ${it.size} entries") }
?.deleteAllFromRealm()
ReadReceiptEntity.whereRoomId(realm, roomId = roomId).findAll()
?.also { Timber.i("## DeleteLocalRoomTask - ReadReceiptEntity - delete ${it.size} entries") }
?.deleteAllFromRealm()
RoomMemberSummaryEntity.where(realm, roomId = roomId).findAll()
?.also { Timber.i("## DeleteLocalRoomTask - RoomMemberSummaryEntity - delete ${it.size} entries") }
?.deleteAllFromRealm()

View File

@ -34,6 +34,7 @@ import org.matrix.android.sdk.api.session.room.ResultBoundaries
import org.matrix.android.sdk.api.session.room.RoomSortOrder
import org.matrix.android.sdk.api.session.room.RoomSummaryQueryParams
import org.matrix.android.sdk.api.session.room.UpdatableLivePageResult
import org.matrix.android.sdk.api.session.room.model.LocalRoomSummary
import org.matrix.android.sdk.api.session.room.model.Membership
import org.matrix.android.sdk.api.session.room.model.RoomSummary
import org.matrix.android.sdk.api.session.room.model.RoomType
@ -43,7 +44,9 @@ import org.matrix.android.sdk.api.session.room.summary.RoomAggregateNotification
import org.matrix.android.sdk.api.session.space.SpaceSummaryQueryParams
import org.matrix.android.sdk.api.util.Optional
import org.matrix.android.sdk.api.util.toOptional
import org.matrix.android.sdk.internal.database.mapper.LocalRoomSummaryMapper
import org.matrix.android.sdk.internal.database.mapper.RoomSummaryMapper
import org.matrix.android.sdk.internal.database.model.LocalRoomSummaryEntity
import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity
import org.matrix.android.sdk.internal.database.model.RoomSummaryEntityFields
import org.matrix.android.sdk.internal.database.query.findByAlias
@ -57,6 +60,7 @@ import javax.inject.Inject
internal class RoomSummaryDataSource @Inject constructor(
@SessionDatabase private val monarchy: Monarchy,
private val roomSummaryMapper: RoomSummaryMapper,
private val localRoomSummaryMapper: LocalRoomSummaryMapper,
private val queryStringValueProcessor: QueryStringValueProcessor,
) {
@ -95,6 +99,25 @@ internal class RoomSummaryDataSource @Inject constructor(
)
}
fun getLocalRoomSummary(roomId: String): LocalRoomSummary? {
return monarchy
.fetchCopyMap({
LocalRoomSummaryEntity.where(it, roomId).findFirst()
}, { entity, _ ->
localRoomSummaryMapper.map(entity)
})
}
fun getLocalRoomSummaryLive(roomId: String): LiveData<Optional<LocalRoomSummary>> {
val liveData = monarchy.findAllMappedWithChanges(
{ realm -> LocalRoomSummaryEntity.where(realm, roomId) },
{ localRoomSummaryMapper.map(it) }
)
return Transformations.map(liveData) { results ->
results.firstOrNull().toOptional()
}
}
fun getRoomSummariesLive(
queryParams: RoomSummaryQueryParams,
sortOrder: RoomSortOrder = RoomSortOrder.NONE

View File

@ -22,21 +22,22 @@ import io.mockk.coVerify
import io.mockk.every
import io.mockk.mockk
import io.mockk.mockkStatic
import io.mockk.spyk
import io.mockk.unmockkAll
import io.mockk.verify
import io.mockk.verifyOrder
import io.realm.kotlin.where
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest
import org.amshove.kluent.shouldBeEqualTo
import org.amshove.kluent.shouldBeNull
import org.junit.After
import org.junit.Before
import org.junit.Test
import org.matrix.android.sdk.api.query.QueryStringValue
import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.api.session.events.model.toContent
import org.matrix.android.sdk.api.session.events.model.toModel
import org.matrix.android.sdk.api.extensions.tryOrNull
import org.matrix.android.sdk.api.session.room.model.LocalRoomCreationState
import org.matrix.android.sdk.api.session.room.model.LocalRoomSummary
import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams
import org.matrix.android.sdk.api.session.room.model.tombstone.RoomTombstoneContent
import org.matrix.android.sdk.internal.database.awaitNotEmptyResult
import org.matrix.android.sdk.internal.database.model.CurrentStateEventEntity
import org.matrix.android.sdk.internal.database.model.EventEntity
@ -44,29 +45,24 @@ import org.matrix.android.sdk.internal.database.model.LocalRoomSummaryEntity
import org.matrix.android.sdk.internal.database.model.LocalRoomSummaryEntityFields
import org.matrix.android.sdk.internal.database.query.copyToRealmOrIgnore
import org.matrix.android.sdk.internal.database.query.getOrCreate
import org.matrix.android.sdk.internal.util.time.DefaultClock
import org.matrix.android.sdk.test.fakes.FakeMonarchy
import org.matrix.android.sdk.test.fakes.FakeStateEventDataSource
import org.matrix.android.sdk.test.fakes.FakeRoomSummaryDataSource
private const val A_LOCAL_ROOM_ID = "local.a-local-room-id"
private const val AN_EXISTING_ROOM_ID = "an-existing-room-id"
private const val A_ROOM_ID = "a-room-id"
private const val MY_USER_ID = "my-user-id"
@ExperimentalCoroutinesApi
internal class DefaultCreateRoomFromLocalRoomTaskTest {
private val fakeMonarchy = FakeMonarchy()
private val clock = DefaultClock()
private val createRoomTask = mockk<CreateRoomTask>()
private val fakeStateEventDataSource = FakeStateEventDataSource()
private val fakeRoomSummaryDataSource = FakeRoomSummaryDataSource()
private val defaultCreateRoomFromLocalRoomTask = DefaultCreateRoomFromLocalRoomTask(
userId = MY_USER_ID,
monarchy = fakeMonarchy.instance,
createRoomTask = createRoomTask,
stateEventDataSource = fakeStateEventDataSource.instance,
clock = clock
roomSummaryDataSource = fakeRoomSummaryDataSource.instance,
)
@Before
@ -91,13 +87,12 @@ internal class DefaultCreateRoomFromLocalRoomTaskTest {
@Test
fun `given a local room id when execute then the existing room id is kept`() = runTest {
// Given
givenATombstoneEvent(
Event(
roomId = A_LOCAL_ROOM_ID,
type = EventType.STATE_ROOM_TOMBSTONE,
stateKey = "",
content = RoomTombstoneContent(replacementRoomId = AN_EXISTING_ROOM_ID).toContent()
)
val aCreateRoomParams = mockk<CreateRoomParams>(relaxed = true)
givenALocalRoomSummary(aCreateRoomParams = aCreateRoomParams, aCreationState = LocalRoomCreationState.CREATED, aReplacementRoomId = AN_EXISTING_ROOM_ID)
val aLocalRoomSummaryEntity = givenALocalRoomSummaryEntity(
aCreateRoomParams = aCreateRoomParams,
aCreationState = LocalRoomCreationState.CREATED,
aReplacementRoomId = AN_EXISTING_ROOM_ID
)
// When
@ -105,20 +100,18 @@ internal class DefaultCreateRoomFromLocalRoomTaskTest {
val result = defaultCreateRoomFromLocalRoomTask.execute(params)
// Then
verifyTombstoneEvent(AN_EXISTING_ROOM_ID)
fakeRoomSummaryDataSource.verifyGetLocalRoomSummary(A_LOCAL_ROOM_ID)
result shouldBeEqualTo AN_EXISTING_ROOM_ID
aLocalRoomSummaryEntity.replacementRoomId shouldBeEqualTo AN_EXISTING_ROOM_ID
aLocalRoomSummaryEntity.creationState shouldBeEqualTo LocalRoomCreationState.CREATED
}
@Test
fun `given a local room id when execute then it is correctly executed`() = runTest {
// Given
val aCreateRoomParams = mockk<CreateRoomParams>()
val aLocalRoomSummaryEntity = mockk<LocalRoomSummaryEntity> {
every { roomSummaryEntity } returns mockk(relaxed = true)
every { createRoomParams } returns aCreateRoomParams
}
givenATombstoneEvent(null)
givenALocalRoomSummaryEntity(aLocalRoomSummaryEntity)
val aCreateRoomParams = mockk<CreateRoomParams>(relaxed = true)
givenALocalRoomSummary(aCreateRoomParams = aCreateRoomParams, aReplacementRoomId = null)
val aLocalRoomSummaryEntity = givenALocalRoomSummaryEntity(aCreateRoomParams = aCreateRoomParams, aReplacementRoomId = null)
coEvery { createRoomTask.execute(any()) } returns A_ROOM_ID
@ -127,32 +120,84 @@ internal class DefaultCreateRoomFromLocalRoomTaskTest {
val result = defaultCreateRoomFromLocalRoomTask.execute(params)
// Then
verifyTombstoneEvent(null)
fakeRoomSummaryDataSource.verifyGetLocalRoomSummary(A_LOCAL_ROOM_ID)
// CreateRoomTask has been called with the initial CreateRoomParams
coVerify { createRoomTask.execute(aCreateRoomParams) }
// The resulting roomId matches the roomId returned by the createRoomTask
result shouldBeEqualTo A_ROOM_ID
// A tombstone state event has been created
coVerify { CurrentStateEventEntity.getOrCreate(realm = any(), roomId = A_LOCAL_ROOM_ID, stateKey = any(), type = EventType.STATE_ROOM_TOMBSTONE) }
// The room creation state has correctly been updated
verifyOrder {
aLocalRoomSummaryEntity.creationState = LocalRoomCreationState.CREATING
aLocalRoomSummaryEntity.creationState = LocalRoomCreationState.CREATED
}
// The local room summary has been updated with the created room id
verify { aLocalRoomSummaryEntity.replacementRoomId = A_ROOM_ID }
aLocalRoomSummaryEntity.replacementRoomId shouldBeEqualTo A_ROOM_ID
aLocalRoomSummaryEntity.creationState shouldBeEqualTo LocalRoomCreationState.CREATED
}
private fun givenATombstoneEvent(event: Event?) {
fakeStateEventDataSource.givenGetStateEventReturns(event)
@Test
fun `given a local room id when execute with an exception then the creation state is correctly updated`() = runTest {
// Given
val aCreateRoomParams = mockk<CreateRoomParams>(relaxed = true)
givenALocalRoomSummary(aCreateRoomParams = aCreateRoomParams, aReplacementRoomId = null)
val aLocalRoomSummaryEntity = givenALocalRoomSummaryEntity(aCreateRoomParams = aCreateRoomParams, aReplacementRoomId = null)
coEvery { createRoomTask.execute(any()) }.throws(mockk())
// When
val params = CreateRoomFromLocalRoomTask.Params(A_LOCAL_ROOM_ID)
tryOrNull { defaultCreateRoomFromLocalRoomTask.execute(params) }
// Then
fakeRoomSummaryDataSource.verifyGetLocalRoomSummary(A_LOCAL_ROOM_ID)
// CreateRoomTask has been called with the initial CreateRoomParams
coVerify { createRoomTask.execute(aCreateRoomParams) }
// The room creation state has correctly been updated
verifyOrder {
aLocalRoomSummaryEntity.creationState = LocalRoomCreationState.CREATING
aLocalRoomSummaryEntity.creationState = LocalRoomCreationState.FAILURE
}
// The local room summary has been updated with the created room id
aLocalRoomSummaryEntity.replacementRoomId.shouldBeNull()
aLocalRoomSummaryEntity.creationState shouldBeEqualTo LocalRoomCreationState.FAILURE
}
private fun givenALocalRoomSummaryEntity(localRoomSummaryEntity: LocalRoomSummaryEntity) {
private fun givenALocalRoomSummary(
aCreateRoomParams: CreateRoomParams,
aCreationState: LocalRoomCreationState = LocalRoomCreationState.NOT_CREATED,
aReplacementRoomId: String? = null
): LocalRoomSummary {
val aLocalRoomSummary = LocalRoomSummary(
roomId = A_LOCAL_ROOM_ID,
roomSummary = mockk(relaxed = true),
createRoomParams = aCreateRoomParams,
creationState = aCreationState,
replacementRoomId = aReplacementRoomId,
)
fakeRoomSummaryDataSource.givenGetLocalRoomSummaryReturns(A_LOCAL_ROOM_ID, aLocalRoomSummary)
return aLocalRoomSummary
}
private fun givenALocalRoomSummaryEntity(
aCreateRoomParams: CreateRoomParams,
aCreationState: LocalRoomCreationState = LocalRoomCreationState.NOT_CREATED,
aReplacementRoomId: String? = null
): LocalRoomSummaryEntity {
val aLocalRoomSummaryEntity = spyk(LocalRoomSummaryEntity(
roomId = A_LOCAL_ROOM_ID,
roomSummaryEntity = mockk(relaxed = true),
replacementRoomId = aReplacementRoomId,
).apply {
createRoomParams = aCreateRoomParams
creationState = aCreationState
})
every {
fakeMonarchy.fakeRealm.instance
.where<LocalRoomSummaryEntity>()
.equalTo(LocalRoomSummaryEntityFields.ROOM_ID, A_LOCAL_ROOM_ID)
.findFirst()
} returns localRoomSummaryEntity
}
private fun verifyTombstoneEvent(expectedRoomId: String?) {
fakeStateEventDataSource.verifyGetStateEvent(A_LOCAL_ROOM_ID, EventType.STATE_ROOM_TOMBSTONE, QueryStringValue.IsEmpty)
fakeStateEventDataSource.instance.getStateEvent(A_LOCAL_ROOM_ID, EventType.STATE_ROOM_TOMBSTONE, QueryStringValue.IsEmpty)
?.content.toModel<RoomTombstoneContent>()
?.replacementRoomId shouldBeEqualTo expectedRoomId
} returns aLocalRoomSummaryEntity
return aLocalRoomSummaryEntity
}
}

View File

@ -47,6 +47,11 @@ internal class FakeMonarchy {
} coAnswers {
firstArg<Monarchy.RealmBlock>().doWithRealm(fakeRealm.instance)
}
coEvery {
instance.runTransactionSync(any())
} coAnswers {
firstArg<Realm.Transaction>().execute(fakeRealm.instance)
}
every { instance.realmConfiguration } returns mockk()
}

View File

@ -0,0 +1,36 @@
/*
* Copyright (c) 2022 The Matrix.org Foundation C.I.C.
*
* 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 org.matrix.android.sdk.test.fakes
import io.mockk.every
import io.mockk.mockk
import io.mockk.verify
import org.matrix.android.sdk.api.session.room.model.LocalRoomSummary
import org.matrix.android.sdk.internal.session.room.summary.RoomSummaryDataSource
internal class FakeRoomSummaryDataSource {
val instance: RoomSummaryDataSource = mockk()
fun givenGetLocalRoomSummaryReturns(roomId: String?, localRoomSummary: LocalRoomSummary?) {
every { instance.getLocalRoomSummary(roomId = roomId ?: any()) } returns localRoomSummary
}
fun verifyGetLocalRoomSummary(roomId: String) {
verify { instance.getLocalRoomSummary(roomId) }
}
}

View File

@ -372,7 +372,7 @@ dependencies {
gplayImplementation "com.google.android.gms:play-services-location:20.0.0"
// UnifiedPush gplay flavor only
gplayImplementation('com.github.UnifiedPush:android-embedded_fcm_distributor:2.1.2') {
gplayImplementation('com.google.firebase:firebase-messaging:23.0.8') {
exclude group: 'com.google.firebase', module: 'firebase-core'
exclude group: 'com.google.firebase', module: 'firebase-analytics'
exclude group: 'com.google.firebase', module: 'firebase-measurement-connector'

View File

@ -225,8 +225,8 @@ class VerifySessionInteractiveTest : VerificationTestBase() {
// Wait until local secrets are known (gossip)
withIdlingResource(allSecretsKnownIdling(uiSession)) {
onView(withId(R.id.groupToolbarAvatarImageView))
.perform(click())
onView(withId(R.id.roomListContainer))
.check(matches(isDisplayed()))
}
}

View File

@ -50,7 +50,7 @@ import im.vector.app.withIdlingResource
import timber.log.Timber
class ElementRobot(
private val labsPreferences: LabFeaturesPreferences = LabFeaturesPreferences(false)
private val labsPreferences: LabFeaturesPreferences = LabFeaturesPreferences(true)
) {
fun onboarding(block: OnboardingRobot.() -> Unit) {
block(OnboardingRobot())

View File

@ -38,7 +38,7 @@
<!-- Level 1: Labs -->
<bool name="settings_labs_thread_messages_default">false</bool>
<bool name="settings_labs_new_app_layout_default">false</bool>
<bool name="settings_labs_new_app_layout_default">true</bool>
<bool name="settings_timeline_show_live_sender_info_visible">true</bool>
<bool name="settings_timeline_show_live_sender_info_default">false</bool>
<!-- Level 1: Advanced settings -->

View File

@ -0,0 +1,47 @@
/*
* Copyright (c) 2022 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.app.core.utils
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
/**
* This observer detects when item was added or moved to the first position of the adapter, while recyclerView is scrolled to the top. This is necessary
* to force recycler to scroll to the top to make such item visible, because by default it will keep items on screen, while adding new item to the top,
* outside of the viewport
* @param layoutManager - [LinearLayoutManager] of the recycler view, which displays items
* @property onItemUpdated - callback to be called, when observer detects event
*/
class FirstItemUpdatedObserver(
layoutManager: LinearLayoutManager,
private val onItemUpdated: () -> Unit
) : RecyclerView.AdapterDataObserver() {
val layoutManager: LinearLayoutManager? by weak(layoutManager)
override fun onItemRangeMoved(fromPosition: Int, toPosition: Int, itemCount: Int) {
if ((toPosition == 0 || fromPosition == 0) && layoutManager?.findFirstCompletelyVisibleItemPosition() == 0) {
onItemUpdated.invoke()
}
}
override fun onItemRangeInserted(positionStart: Int, itemCount: Int) {
if (positionStart == 0 && layoutManager?.findFirstCompletelyVisibleItemPosition() == 0) {
onItemUpdated.invoke()
}
}
}

View File

@ -84,6 +84,7 @@ import im.vector.app.features.spaces.SpaceSettingsMenuBottomSheet
import im.vector.app.features.spaces.invite.SpaceInviteBottomSheet
import im.vector.app.features.spaces.share.ShareSpaceBottomSheet
import im.vector.app.features.themes.ThemeUtils
import im.vector.app.features.usercode.UserCodeActivity
import im.vector.app.features.workers.signout.ServerBackupStatusViewModel
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
@ -634,10 +635,18 @@ class HomeActivity :
launchInviteFriends()
true
}
R.id.menu_home_qr -> {
launchQrCode()
true
}
else -> false
}
}
private fun launchQrCode() {
startActivity(UserCodeActivity.newIntent(this, sharedActionViewModel.session.myUserId))
}
private fun launchInviteFriends() {
activeSessionHolder.getSafeActiveSession()?.permalinkService()?.createPermalink(sharedActionViewModel.session.myUserId)?.let { permalink ->
analyticsTracker.screen(MobileScreen(screenName = MobileScreen.ScreenName.InviteFriends))

View File

@ -119,17 +119,19 @@ class HomeActivityViewModel @AssistedInject constructor(
}
private fun observeReleaseNotes() = withState { state ->
// we don't want to show release notes for new users or after relogin
if (state.authenticationDescription == null && vectorPreferences.isNewAppLayoutEnabled()) {
releaseNotesPreferencesStore.appLayoutOnboardingShown.onEach { isAppLayoutOnboardingShown ->
if (!isAppLayoutOnboardingShown) {
_viewEvents.post(HomeActivityViewEvents.ShowReleaseNotes)
if (vectorPreferences.isNewAppLayoutEnabled()) {
// we don't want to show release notes for new users or after relogin
if (state.authenticationDescription == null) {
releaseNotesPreferencesStore.appLayoutOnboardingShown.onEach { isAppLayoutOnboardingShown ->
if (!isAppLayoutOnboardingShown) {
_viewEvents.post(HomeActivityViewEvents.ShowReleaseNotes)
}
}.launchIn(viewModelScope)
} else {
// we assume that users which came from auth flow either have seen updates already (relogin) or don't need them (new user)
viewModelScope.launch {
releaseNotesPreferencesStore.setAppLayoutOnboardingShown(true)
}
}.launchIn(viewModelScope)
} else {
// we assume that users which came from auth flow either have seen updates already (relogin) or don't need them (new user)
viewModelScope.launch {
releaseNotesPreferencesStore.setAppLayoutOnboardingShown(true)
}
}
}

View File

@ -51,7 +51,7 @@ sealed class RoomDetailViewEvents : VectorViewEvents {
object OpenRoomProfile : RoomDetailViewEvents()
data class ShowRoomAvatarFullScreen(val matrixItem: MatrixItem?, val view: View?) : RoomDetailViewEvents()
object ShowWaitingView : RoomDetailViewEvents()
data class ShowWaitingView(val text: String? = null) : RoomDetailViewEvents()
object HideWaitingView : RoomDetailViewEvents()
data class DownloadFileState(

View File

@ -493,7 +493,7 @@ class TimelineFragment :
is RoomDetailViewEvents.ShowInfoOkDialog -> showDialogWithMessage(it.message)
is RoomDetailViewEvents.JoinJitsiConference -> joinJitsiRoom(it.widget, it.withVideo)
RoomDetailViewEvents.LeaveJitsiConference -> leaveJitsiConference()
RoomDetailViewEvents.ShowWaitingView -> vectorBaseActivity.showWaitingView()
is RoomDetailViewEvents.ShowWaitingView -> vectorBaseActivity.showWaitingView(it.text)
RoomDetailViewEvents.HideWaitingView -> vectorBaseActivity.hideWaitingView()
is RoomDetailViewEvents.RequestNativeWidgetPermission -> requestNativeWidgetPermission(it)
is RoomDetailViewEvents.OpenRoom -> handleOpenRoom(it)

View File

@ -83,7 +83,6 @@ import org.matrix.android.sdk.api.query.QueryStringValue
import org.matrix.android.sdk.api.raw.RawService
import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.crypto.MXCryptoError
import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.api.session.events.model.LocalEcho
import org.matrix.android.sdk.api.session.events.model.RelationType
@ -100,9 +99,11 @@ import org.matrix.android.sdk.api.session.room.getTimelineEvent
import org.matrix.android.sdk.api.session.room.location.UpdateLiveLocationShareResult
import org.matrix.android.sdk.api.session.room.members.ChangeMembershipState
import org.matrix.android.sdk.api.session.room.members.roomMemberQueryParams
import org.matrix.android.sdk.api.session.room.model.LocalRoomCreationState
import org.matrix.android.sdk.api.session.room.model.Membership
import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary
import org.matrix.android.sdk.api.session.room.model.RoomSummary
import org.matrix.android.sdk.api.session.room.model.localecho.RoomLocalEcho
import org.matrix.android.sdk.api.session.room.model.message.getFileUrl
import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultContent
import org.matrix.android.sdk.api.session.room.model.tombstone.RoomTombstoneContent
@ -185,6 +186,7 @@ class TimelineViewModel @AssistedInject constructor(
init {
// This method will take care of a null room to update the state.
observeRoomSummary()
observeLocalRoomSummary()
if (room == null) {
timeline = null
} else {
@ -617,7 +619,7 @@ class TimelineViewModel @AssistedInject constructor(
}
private fun handleAddJitsiConference(action: RoomDetailAction.AddJitsiWidget) {
_viewEvents.post(RoomDetailViewEvents.ShowWaitingView)
_viewEvents.post(RoomDetailViewEvents.ShowWaitingView())
viewModelScope.launch(Dispatchers.IO) {
try {
val widget = jitsiService.createJitsiWidget(initialState.roomId, action.withVideo)
@ -637,7 +639,7 @@ class TimelineViewModel @AssistedInject constructor(
if (isJitsiWidget) {
setState { copy(jitsiState = jitsiState.copy(deleteWidgetInProgress = true)) }
} else {
_viewEvents.post(RoomDetailViewEvents.ShowWaitingView)
_viewEvents.post(RoomDetailViewEvents.ShowWaitingView())
}
session.widgetService().destroyRoomWidget(initialState.roomId, widgetId)
// local echo
@ -1231,6 +1233,28 @@ class TimelineViewModel @AssistedInject constructor(
}
}
private fun observeLocalRoomSummary() {
if (room != null && RoomLocalEcho.isLocalEchoId(room.roomId)) {
room.flow().liveLocalRoomSummary()
.unwrap()
.map { it.creationState }
.distinctUntilChanged()
.onEach { creationState ->
when (creationState) {
LocalRoomCreationState.NOT_CREATED -> Unit
LocalRoomCreationState.CREATING ->
_viewEvents.post(RoomDetailViewEvents.ShowWaitingView(stringProvider.getString(R.string.creating_direct_room)))
LocalRoomCreationState.FAILURE -> {
_viewEvents.post(RoomDetailViewEvents.HideWaitingView)
}
LocalRoomCreationState.CREATED ->
_viewEvents.post(RoomDetailViewEvents.OpenRoom(room.localRoomSummary()?.replacementRoomId!!, true))
}
}
.launchIn(viewModelScope)
}
}
private fun getUnreadState() {
if (room == null) return
combine(
@ -1322,26 +1346,11 @@ class TimelineViewModel @AssistedInject constructor(
}
}
room.getStateEvent(EventType.STATE_ROOM_TOMBSTONE, QueryStringValue.IsEmpty)?.also {
onRoomTombstoneUpdated(it)
setState { copy(tombstoneEvent = it) }
}
}
}
private var roomTombstoneHandled = false
private fun onRoomTombstoneUpdated(tombstoneEvent: Event) = withState { state ->
if (roomTombstoneHandled) return@withState
if (state.isLocalRoom()) {
// Local room has been replaced, so navigate to the new room
val roomId = tombstoneEvent.getClearContent()?.toModel<RoomTombstoneContent>()
?.replacementRoomId
?: return@withState
_viewEvents.post(RoomDetailViewEvents.OpenRoom(roomId, closeCurrentRoom = true))
roomTombstoneHandled = true
} else {
setState { copy(tombstoneEvent = tombstoneEvent) }
}
}
/**
* Navigates to the appropriate event (by paginating the thread timeline until the event is found
* in the snapshot. The main reason for this function is to support the /relations api

View File

@ -31,4 +31,5 @@ sealed class RoomListAction : VectorViewModelAction {
data class LeaveRoom(val roomId: String) : RoomListAction()
data class JoinSuggestedRoom(val roomId: String, val viaServers: List<String>?) : RoomListAction()
data class ShowRoomDetails(val roomId: String, val viaServers: List<String>?) : RoomListAction()
object DeleteAllLocalRoom : RoomListAction()
}

View File

@ -149,10 +149,13 @@ class RoomListFragment :
(it.contentEpoxyController as? RoomSummaryPagedController)?.roomChangeMembershipStates = ms
}
}
roomListViewModel.onEach(RoomListViewState::localRoomIds) {
// Local rooms should not exist anymore when the room list is shown
roomListViewModel.deleteLocalRooms(it)
}
}
override fun onStart() {
super.onStart()
// Local rooms should not exist anymore when the room list is shown
roomListViewModel.handle(RoomListAction.DeleteAllLocalRoom)
}
private fun refreshCollapseStates() {

View File

@ -97,7 +97,6 @@ class RoomListViewModel @AssistedInject constructor(
init {
observeMembershipChanges()
observeLocalRooms()
spaceStateHandler.getSelectedSpaceFlow()
.distinctUntilChanged()
@ -125,16 +124,6 @@ class RoomListViewModel @AssistedInject constructor(
}
}
private fun observeLocalRooms() {
session
.flow()
.liveRoomSummaries(roomSummaryQueryParams {
roomId = QueryStringValue.Contains(RoomLocalEcho.PREFIX)
})
.map { page -> page.map { it.roomId } }
.setOnEach { copy(localRoomIds = it) }
}
companion object : MavericksViewModelFactory<RoomListViewModel, RoomListViewState> by hiltMavericksViewModelFactory()
private val roomListSectionBuilder = RoomListSectionBuilder(
@ -166,6 +155,7 @@ class RoomListViewModel @AssistedInject constructor(
is RoomListAction.ToggleSection -> handleToggleSection(action.section)
is RoomListAction.JoinSuggestedRoom -> handleJoinSuggestedRoom(action)
is RoomListAction.ShowRoomDetails -> handleShowRoomDetails(action)
RoomListAction.DeleteAllLocalRoom -> handleDeleteLocalRooms()
}
}
@ -173,14 +163,6 @@ class RoomListViewModel @AssistedInject constructor(
return session.getRoom(roomId)?.stateService()?.isPublic().orFalse()
}
fun deleteLocalRooms(roomsIds: Iterable<String>) {
viewModelScope.launch {
roomsIds.forEach {
session.roomService().deleteLocalRoom(it)
}
}
}
// PRIVATE METHODS *****************************************************************************
private fun handleSelectRoom(action: RoomListAction.SelectRoom) = withState {
@ -338,4 +320,16 @@ class RoomListViewModel @AssistedInject constructor(
_viewEvents.post(value)
}
}
private fun handleDeleteLocalRooms() {
val localRoomIds = session.roomService()
.getRoomSummaries(roomSummaryQueryParams { roomId = QueryStringValue.Contains(RoomLocalEcho.PREFIX) })
.map { it.roomId }
viewModelScope.launch {
localRoomIds.forEach {
session.roomService().deleteLocalRoom(it)
}
}
}
}

View File

@ -31,7 +31,6 @@ data class RoomListViewState(
val asyncSuggestedRooms: Async<List<SpaceChildInfo>> = Uninitialized,
val currentUserName: String? = null,
val asyncSelectedSpace: Async<RoomSummary?> = Uninitialized,
val localRoomIds: List<String> = emptyList()
) : MavericksState {
constructor(args: RoomListParams) : this(displayMode = args.displayMode)

View File

@ -103,6 +103,9 @@ abstract class RoomSummaryItem : VectorEpoxyModel<RoomSummaryItem.Holder>(R.layo
@EpoxyAttribute
var showSelected: Boolean = false
@EpoxyAttribute
var useSingleLineForLastEvent: Boolean = false
override fun bind(holder: Holder) {
super.bind(holder)
@ -122,6 +125,10 @@ abstract class RoomSummaryItem : VectorEpoxyModel<RoomSummaryItem.Holder>(R.layo
holder.roomAvatarFailSendingImageView.isVisible = hasFailedSending
renderSelection(holder, showSelected)
holder.roomAvatarPresenceImageView.render(showPresence, userPresence)
if (useSingleLineForLastEvent) {
holder.subtitleView.setLines(1)
}
}
private fun renderDisplayMode(holder: Holder) = when (displayMode) {

View File

@ -51,7 +51,8 @@ class RoomSummaryItemFactory @Inject constructor(
roomChangeMembershipStates: Map<String, ChangeMembershipState>,
selectedRoomIds: Set<String>,
displayMode: RoomListDisplayMode,
listener: RoomListListener?
listener: RoomListListener?,
singleLineLastEvent: Boolean = false
): VectorEpoxyModel<*> {
return when (roomSummary.membership) {
Membership.INVITE -> {
@ -59,7 +60,7 @@ class RoomSummaryItemFactory @Inject constructor(
createInvitationItem(roomSummary, changeMembershipState, listener)
}
else -> createRoomItem(
roomSummary, selectedRoomIds, displayMode, listener?.let { it::onRoomClicked }, listener?.let { it::onRoomLongClicked }
roomSummary, selectedRoomIds, displayMode, singleLineLastEvent, listener?.let { it::onRoomClicked }, listener?.let { it::onRoomLongClicked }
)
}
}
@ -118,8 +119,9 @@ class RoomSummaryItemFactory @Inject constructor(
roomSummary: RoomSummary,
selectedRoomIds: Set<String>,
displayMode: RoomListDisplayMode,
singleLineLastEvent: Boolean,
onClick: ((RoomSummary) -> Unit)?,
onLongClick: ((RoomSummary) -> Boolean)?
onLongClick: ((RoomSummary) -> Boolean)?,
): VectorEpoxyModel<*> {
val subtitle = getSearchResultSubtitle(roomSummary)
val unreadCount = roomSummary.notificationCount
@ -140,7 +142,7 @@ class RoomSummaryItemFactory @Inject constructor(
} else {
createRoomSummaryItem(
roomSummary, displayMode, subtitle, latestEventTime, typingMessage,
latestFormattedEvent, showHighlighted, showSelected, unreadCount, onClick, onLongClick
latestFormattedEvent, showHighlighted, showSelected, unreadCount, singleLineLastEvent, onClick, onLongClick
)
}
}
@ -155,6 +157,7 @@ class RoomSummaryItemFactory @Inject constructor(
showHighlighted: Boolean,
showSelected: Boolean,
unreadCount: Int,
singleLineLastEvent: Boolean,
onClick: ((RoomSummary) -> Unit)?,
onLongClick: ((RoomSummary) -> Boolean)?
) = RoomSummaryItem_()
@ -177,6 +180,7 @@ class RoomSummaryItemFactory @Inject constructor(
.unreadNotificationCount(unreadCount)
.hasUnreadMessage(roomSummary.hasUnreadMessages)
.hasDraft(roomSummary.userDrafts.isNotEmpty())
.useSingleLineForLastEvent(singleLineLastEvent)
.itemLongClickListener { _ -> onLongClick?.invoke(roomSummary) ?: false }
.itemClickListener { onClick?.invoke(roomSummary) }

View File

@ -16,6 +16,8 @@
package im.vector.app.features.home.room.list
import android.widget.TextView
import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass
import im.vector.app.R
import im.vector.app.core.epoxy.VectorEpoxyHolder
@ -23,5 +25,18 @@ import im.vector.app.core.epoxy.VectorEpoxyModel
@EpoxyModelClass
abstract class RoomSummaryItemPlaceHolder : VectorEpoxyModel<RoomSummaryItemPlaceHolder.Holder>(R.layout.item_room_placeholder) {
class Holder : VectorEpoxyHolder()
@EpoxyAttribute
var useSingleLineForLastEvent: Boolean = false
override fun bind(holder: Holder) {
super.bind(holder)
if (useSingleLineForLastEvent) {
holder.subtitleView.setLines(1)
}
}
class Holder : VectorEpoxyHolder() {
val subtitleView by bind<TextView>(R.id.subtitleView)
}
}

View File

@ -17,18 +17,26 @@
package im.vector.app.features.home.room.list
import im.vector.app.features.home.RoomListDisplayMode
import im.vector.app.features.settings.FontScalePreferences
import org.matrix.android.sdk.api.session.room.model.RoomSummary
class RoomSummaryListController(
private val roomSummaryItemFactory: RoomSummaryItemFactory,
private val displayMode: RoomListDisplayMode
private val displayMode: RoomListDisplayMode,
fontScalePreferences: FontScalePreferences
) : CollapsableTypedEpoxyController<List<RoomSummary>>() {
var listener: RoomListListener? = null
private val shouldUseSingleLine: Boolean
init {
val fontScale = fontScalePreferences.getResolvedFontScaleValue()
shouldUseSingleLine = fontScale.scale > FontScalePreferences.SCALE_LARGE
}
override fun buildModels(data: List<RoomSummary>?) {
data?.forEach {
add(roomSummaryItemFactory.create(it, emptyMap(), emptySet(), displayMode, listener))
add(roomSummaryItemFactory.create(it, emptyMap(), emptySet(), displayMode, listener, shouldUseSingleLine))
}
}
}

View File

@ -20,18 +20,26 @@ import com.airbnb.epoxy.EpoxyModel
import com.airbnb.epoxy.paging.PagedListEpoxyController
import im.vector.app.core.utils.createUIHandler
import im.vector.app.features.home.RoomListDisplayMode
import im.vector.app.features.settings.FontScalePreferences
import org.matrix.android.sdk.api.session.room.members.ChangeMembershipState
import org.matrix.android.sdk.api.session.room.model.RoomSummary
class RoomSummaryPagedController(
private val roomSummaryItemFactory: RoomSummaryItemFactory,
private val displayMode: RoomListDisplayMode
private val displayMode: RoomListDisplayMode,
fontScalePreferences: FontScalePreferences
) : PagedListEpoxyController<RoomSummary>(
// Important it must match the PageList builder notify Looper
modelBuildingHandler = createUIHandler()
), CollapsableControllerExtension {
var listener: RoomListListener? = null
private val shouldUseSingleLine: Boolean
init {
val fontScale = fontScalePreferences.getResolvedFontScaleValue()
shouldUseSingleLine = fontScale.scale > FontScalePreferences.SCALE_LARGE
}
var roomChangeMembershipStates: Map<String, ChangeMembershipState>? = null
set(value) {
@ -57,8 +65,14 @@ class RoomSummaryPagedController(
}
override fun buildItemModel(currentPosition: Int, item: RoomSummary?): EpoxyModel<*> {
// for place holder if enabled
item ?: return RoomSummaryItemPlaceHolder_().apply { id(currentPosition) }
return roomSummaryItemFactory.create(item, roomChangeMembershipStates.orEmpty(), emptySet(), displayMode, listener)
return if (item == null) {
val host = this
RoomSummaryItemPlaceHolder_().apply {
id(currentPosition)
useSingleLineForLastEvent(host.shouldUseSingleLine)
}
} else {
roomSummaryItemFactory.create(item, roomChangeMembershipStates.orEmpty(), emptySet(), displayMode, listener, shouldUseSingleLine)
}
}
}

View File

@ -17,18 +17,20 @@
package im.vector.app.features.home.room.list
import im.vector.app.features.home.RoomListDisplayMode
import im.vector.app.features.settings.FontScalePreferences
import javax.inject.Inject
class RoomSummaryPagedControllerFactory @Inject constructor(
private val roomSummaryItemFactory: RoomSummaryItemFactory
private val roomSummaryItemFactory: RoomSummaryItemFactory,
private val fontScalePreferences: FontScalePreferences
) {
fun createRoomSummaryPagedController(displayMode: RoomListDisplayMode): RoomSummaryPagedController {
return RoomSummaryPagedController(roomSummaryItemFactory, displayMode)
return RoomSummaryPagedController(roomSummaryItemFactory, displayMode, fontScalePreferences)
}
fun createRoomSummaryListController(displayMode: RoomListDisplayMode): RoomSummaryListController {
return RoomSummaryListController(roomSummaryItemFactory, displayMode)
return RoomSummaryListController(roomSummaryItemFactory, displayMode, fontScalePreferences)
}
fun createSuggestedRoomListController(): SuggestedRoomListController {

View File

@ -24,12 +24,14 @@ import im.vector.app.features.home.RoomListDisplayMode
import im.vector.app.features.home.room.list.RoomListListener
import im.vector.app.features.home.room.list.RoomSummaryItemFactory
import im.vector.app.features.home.room.list.RoomSummaryItemPlaceHolder_
import im.vector.app.features.settings.FontScalePreferences
import org.matrix.android.sdk.api.session.room.members.ChangeMembershipState
import org.matrix.android.sdk.api.session.room.model.RoomSummary
import javax.inject.Inject
class HomeFilteredRoomsController @Inject constructor(
private val roomSummaryItemFactory: RoomSummaryItemFactory,
fontScalePreferences: FontScalePreferences
) : PagedListEpoxyController<RoomSummary>(
// Important it must match the PageList builder notify Looper
modelBuildingHandler = createUIHandler()
@ -47,6 +49,13 @@ class HomeFilteredRoomsController @Inject constructor(
private var emptyStateData: StateView.State.Empty? = null
private var currentState: StateView.State = StateView.State.Content
private val shouldUseSingleLine: Boolean
init {
val fontScale = fontScalePreferences.getResolvedFontScaleValue()
shouldUseSingleLine = fontScale.scale > FontScalePreferences.SCALE_LARGE
}
override fun addModels(models: List<EpoxyModel<*>>) {
if (models.isEmpty() && emptyStateData != null) {
emptyStateData?.let { emptyState ->
@ -67,7 +76,14 @@ class HomeFilteredRoomsController @Inject constructor(
}
override fun buildItemModel(currentPosition: Int, item: RoomSummary?): EpoxyModel<*> {
item ?: return RoomSummaryItemPlaceHolder_().apply { id(currentPosition) }
return roomSummaryItemFactory.create(item, roomChangeMembershipStates.orEmpty(), emptySet(), RoomListDisplayMode.ROOMS, listener)
return if (item == null) {
val host = this
RoomSummaryItemPlaceHolder_().apply {
id(currentPosition)
useSingleLineForLastEvent(host.shouldUseSingleLine)
}
} else {
roomSummaryItemFactory.create(item, roomChangeMembershipStates.orEmpty(), emptySet(), RoomListDisplayMode.ROOMS, listener, shouldUseSingleLine)
}
}
}

View File

@ -27,4 +27,5 @@ sealed class HomeRoomListAction : VectorViewModelAction {
data class ToggleTag(val roomId: String, val tag: String) : HomeRoomListAction()
data class LeaveRoom(val roomId: String) : HomeRoomListAction()
data class ChangeRoomFilter(val filter: HomeRoomFilter) : HomeRoomListAction()
object DeleteAllLocalRoom : HomeRoomListAction()
}

View File

@ -24,7 +24,6 @@ import android.view.ViewGroup
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.ConcatAdapter
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.airbnb.epoxy.OnModelBuildFinishedListener
import com.airbnb.mvrx.fragmentViewModel
import com.airbnb.mvrx.withState
@ -36,6 +35,7 @@ import im.vector.app.core.extensions.cleanup
import im.vector.app.core.platform.StateView
import im.vector.app.core.platform.VectorBaseFragment
import im.vector.app.core.resources.UserPreferencesProvider
import im.vector.app.core.utils.FirstItemUpdatedObserver
import im.vector.app.databinding.FragmentRoomListBinding
import im.vector.app.features.analytics.plan.ViewRoom
import im.vector.app.features.home.room.list.RoomListAnimator
@ -66,6 +66,7 @@ class HomeRoomListFragment :
private val roomListViewModel: HomeRoomListViewModel by fragmentViewModel()
private lateinit var sharedQuickActionsViewModel: RoomListQuickActionsSharedActionViewModel
private var concatAdapter = ConcatAdapter()
private lateinit var firstItemObserver: FirstItemUpdatedObserver
private var modelBuildListener: OnModelBuildFinishedListener? = null
private lateinit var stateRestorer: LayoutManagerStateRestorer
@ -82,6 +83,13 @@ class HomeRoomListFragment :
setupRecyclerView()
}
override fun onStart() {
super.onStart()
// Local rooms should not exist anymore when the room list is shown
roomListViewModel.handle(HomeRoomListAction.DeleteAllLocalRoom)
}
private fun setupObservers() {
sharedQuickActionsViewModel = activityViewModelProvider[RoomListQuickActionsSharedActionViewModel::class.java]
sharedQuickActionsViewModel
@ -130,6 +138,9 @@ class HomeRoomListFragment :
private fun setupRecyclerView() {
val layoutManager = LinearLayoutManager(context)
firstItemObserver = FirstItemUpdatedObserver(layoutManager) {
layoutManager.scrollToPosition(0)
}
stateRestorer = LayoutManagerStateRestorer(layoutManager).register()
views.roomListView.layoutManager = layoutManager
views.roomListView.itemAnimator = RoomListAnimator()
@ -158,14 +169,7 @@ class HomeRoomListFragment :
views.roomListView.adapter = concatAdapter
// we need to force scroll when recents/filter tabs are added to make them visible
concatAdapter.registerAdapterDataObserver(object : RecyclerView.AdapterDataObserver() {
override fun onItemRangeInserted(positionStart: Int, itemCount: Int) {
if (positionStart == 0) {
layoutManager.scrollToPosition(0)
}
}
})
concatAdapter.registerAdapterDataObserver(firstItemObserver)
}
override fun invalidate() = withState(roomListViewModel) { state ->
@ -233,6 +237,8 @@ class HomeRoomListFragment :
roomsController.listener = null
concatAdapter.unregisterAdapterDataObserver(firstItemObserver)
super.onDestroyView()
}

View File

@ -49,6 +49,7 @@ import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.extensions.orFalse
import org.matrix.android.sdk.api.query.QueryStringValue
import org.matrix.android.sdk.api.query.RoomCategoryFilter
import org.matrix.android.sdk.api.query.RoomTagQueryFilter
import org.matrix.android.sdk.api.query.toActiveSpaceOrNoFilter
@ -60,6 +61,7 @@ import org.matrix.android.sdk.api.session.room.RoomSummaryQueryParams
import org.matrix.android.sdk.api.session.room.UpdatableLivePageResult
import org.matrix.android.sdk.api.session.room.model.Membership
import org.matrix.android.sdk.api.session.room.model.RoomSummary
import org.matrix.android.sdk.api.session.room.model.localecho.RoomLocalEcho
import org.matrix.android.sdk.api.session.room.model.tag.RoomTag
import org.matrix.android.sdk.api.session.room.roomSummaryQueryParams
import org.matrix.android.sdk.api.session.room.state.isPublic
@ -329,6 +331,7 @@ class HomeRoomListViewModel @AssistedInject constructor(
is HomeRoomListAction.ChangeRoomNotificationState -> handleChangeNotificationMode(action)
is HomeRoomListAction.ToggleTag -> handleToggleTag(action)
is HomeRoomListAction.ChangeRoomFilter -> handleChangeRoomFilter(action.filter)
HomeRoomListAction.DeleteAllLocalRoom -> handleDeleteLocalRooms()
}
}
@ -399,6 +402,18 @@ class HomeRoomListViewModel @AssistedInject constructor(
}
}
private fun handleDeleteLocalRooms() = withState {
val localRoomIds = session.roomService()
.getRoomSummaries(roomSummaryQueryParams { roomId = QueryStringValue.Contains(RoomLocalEcho.PREFIX) })
.map { it.roomId }
viewModelScope.launch {
localRoomIds.forEach {
session.roomService().deleteLocalRoom(it)
}
}
}
private fun String.otherTag(): String? {
return when (this) {
RoomTag.ROOM_TAG_FAVOURITE -> RoomTag.ROOM_TAG_LOW_PRIORITY

View File

@ -26,5 +26,5 @@ import org.matrix.android.sdk.api.session.room.model.RoomSummary
data class HomeRoomListViewState(
val state: StateView.State = StateView.State.Content,
val headersData: RoomsHeadersData = RoomsHeadersData(),
val roomsLivePagedList: LiveData<PagedList<RoomSummary>>? = null
val roomsLivePagedList: LiveData<PagedList<RoomSummary>>? = null,
) : MavericksState

View File

@ -18,7 +18,7 @@ package im.vector.app.features.home.room.list.home.header
import android.content.res.Resources
import android.util.TypedValue
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.LinearLayoutManager
import com.airbnb.epoxy.Carousel
import com.airbnb.epoxy.CarouselModelBuilder
import com.airbnb.epoxy.EpoxyController
@ -27,6 +27,7 @@ import com.airbnb.epoxy.carousel
import com.google.android.material.color.MaterialColors
import im.vector.app.R
import im.vector.app.core.resources.StringProvider
import im.vector.app.core.utils.FirstItemUpdatedObserver
import im.vector.app.features.home.AvatarRenderer
import im.vector.app.features.home.room.list.RoomListListener
import org.matrix.android.sdk.api.session.room.model.RoomSummary
@ -47,22 +48,7 @@ class HomeRoomsHeadersController @Inject constructor(
private var carousel: Carousel? = null
private val carouselAdapterObserver = object : RecyclerView.AdapterDataObserver() {
override fun onItemRangeMoved(fromPosition: Int, toPosition: Int, itemCount: Int) {
if (toPosition == 0 || fromPosition == 0) {
carousel?.post {
carousel?.layoutManager?.scrollToPosition(0)
}
}
super.onItemRangeMoved(fromPosition, toPosition, itemCount)
}
override fun onItemRangeInserted(positionStart: Int, itemCount: Int) {
if (positionStart == 0) {
carousel?.layoutManager?.scrollToPosition(0)
}
}
}
private var carouselAdapterObserver: FirstItemUpdatedObserver? = null
private val recentsHPadding = TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP,
@ -113,25 +99,16 @@ class HomeRoomsHeadersController @Inject constructor(
)
onBind { _, view, _ ->
host.carousel = view
host.unsubscribeAdapterObserver()
host.subscribeAdapterObserver()
val colorSurface = MaterialColors.getColor(view, R.attr.vctr_toolbar_background)
view.setBackgroundColor(colorSurface)
try {
view.adapter?.registerAdapterDataObserver(host.carouselAdapterObserver)
} catch (e: IllegalStateException) {
// do nothing
}
}
onUnbind { _, view ->
onUnbind { _, _ ->
host.carousel = null
try {
view.adapter?.unregisterAdapterDataObserver(host.carouselAdapterObserver)
} catch (e: IllegalStateException) {
// do nothing
}
host.unsubscribeAdapterObserver()
}
withModelsFrom(recents) { roomSummary ->
@ -150,6 +127,33 @@ class HomeRoomsHeadersController @Inject constructor(
}
}
private fun unsubscribeAdapterObserver() {
carouselAdapterObserver?.let { observer ->
try {
carousel?.adapter?.unregisterAdapterDataObserver(observer)
carouselAdapterObserver = null
} catch (e: IllegalStateException) {
// do nothing
}
}
}
private fun subscribeAdapterObserver() {
(carousel?.layoutManager as? LinearLayoutManager)?.let { layoutManager ->
carouselAdapterObserver = FirstItemUpdatedObserver(layoutManager) {
carousel?.post {
layoutManager.scrollToPosition(0)
}
}.also { observer ->
try {
carousel?.adapter?.registerAdapterDataObserver(observer)
} catch (e: IllegalStateException) {
// do nothing
}
}
}
}
private fun addRoomFilterHeaderItem(
filterChangedListener: ((HomeRoomFilter) -> Unit)?,
filtersList: List<HomeRoomFilter>,

View File

@ -34,7 +34,7 @@ class ReleaseNotesPreferencesStore @Inject constructor(
private val context: Context
) {
private val isAppLayoutOnboardingShown = booleanPreferencesKey("SETTINGS_APP_LAYOUT_ONBOARDING_SHOWN")
private val isAppLayoutOnboardingShown = booleanPreferencesKey("SETTINGS_APP_LAYOUT_ONBOARDING_DISPLAYED")
val appLayoutOnboardingShown: Flow<Boolean> = context.dataStore.data
.map { preferences -> preferences[isAppLayoutOnboardingShown].orFalse() }

View File

@ -57,6 +57,16 @@ interface FontScalePreferences {
* @return list of values
*/
fun getAvailableScales(): List<FontScaleValue>
companion object {
const val SCALE_TINY = 0.70f
const val SCALE_SMALL = 0.85f
const val SCALE_NORMAL = 1.00f
const val SCALE_LARGE = 1.15f
const val SCALE_LARGER = 1.30f
const val SCALE_LARGEST = 1.45f
const val SCALE_HUGE = 1.60f
}
}
/**
@ -73,13 +83,13 @@ class FontScalePreferencesImpl @Inject constructor(
}
private val fontScaleValues = listOf(
FontScaleValue(0, "FONT_SCALE_TINY", 0.70f, R.string.tiny),
FontScaleValue(1, "FONT_SCALE_SMALL", 0.85f, R.string.small),
FontScaleValue(2, "FONT_SCALE_NORMAL", 1.00f, R.string.normal),
FontScaleValue(3, "FONT_SCALE_LARGE", 1.15f, R.string.large),
FontScaleValue(4, "FONT_SCALE_LARGER", 1.30f, R.string.larger),
FontScaleValue(5, "FONT_SCALE_LARGEST", 1.45f, R.string.largest),
FontScaleValue(6, "FONT_SCALE_HUGE", 1.60f, R.string.huge)
FontScaleValue(0, "FONT_SCALE_TINY", FontScalePreferences.SCALE_TINY, R.string.tiny),
FontScaleValue(1, "FONT_SCALE_SMALL", FontScalePreferences.SCALE_SMALL, R.string.small),
FontScaleValue(2, "FONT_SCALE_NORMAL", FontScalePreferences.SCALE_NORMAL, R.string.normal),
FontScaleValue(3, "FONT_SCALE_LARGE", FontScalePreferences.SCALE_LARGE, R.string.large),
FontScaleValue(4, "FONT_SCALE_LARGER", FontScalePreferences.SCALE_LARGER, R.string.larger),
FontScaleValue(5, "FONT_SCALE_LARGEST", FontScalePreferences.SCALE_LARGEST, R.string.largest),
FontScaleValue(6, "FONT_SCALE_HUGE", FontScalePreferences.SCALE_HUGE, R.string.huge)
)
private val normalFontScaleValue = fontScaleValues[2]

View File

@ -60,6 +60,7 @@ class IncomingShareController @Inject constructor(
roomSummary,
data.selectedRoomIds,
RoomListDisplayMode.FILTERED,
singleLineLastEvent = false,
callback?.let { it::onRoomClicked },
callback?.let { it::onRoomLongClicked }
)

View File

@ -2,10 +2,9 @@
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<size android:width="40dp" android:height="40dp"/>
<solid android:color="?vctr_reaction_background_off" />
<corners android:radius="8dp" />
</shape>
</shape>

View File

@ -5,7 +5,6 @@
android:id="@+id/recentRoot"
android:layout_width="84dp"
android:layout_height="wrap_content"
android:background="?vctr_toolbar_background"
android:clickable="true"
android:focusable="true"
android:foreground="?attr/selectableItemBackground"

View File

@ -190,7 +190,7 @@
android:layout_marginTop="3dp"
android:layout_marginEnd="8dp"
android:ellipsize="end"
android:maxLines="2"
android:lines="2"
android:textAlignment="viewStart"
android:textColor="?vctr_content_secondary"
app:layout_constraintEnd_toEndOf="parent"

View File

@ -16,7 +16,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
app:layout_constraintBottom_toBottomOf="parent"
android:layout_marginTop="12dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
@ -29,23 +29,20 @@
</FrameLayout>
<!-- Margin bottom does not work, so I use space -->
<Space
android:id="@+id/roomAvatarBottomSpace"
android:layout_width="0dp"
android:layout_height="12dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/roomAvatarContainer"
tools:layout_marginStart="20dp" />
<View
<TextView
android:id="@+id/roomNameView"
android:layout_width="wrap_content"
android:layout_height="15dp"
style="@style/Widget.Vector.TextView.Subtitle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/layout_horizontal_margin"
android:layout_marginTop="12dp"
android:layout_marginEnd="70dp"
android:background="@drawable/placeholder_shape_8"
android:duplicateParentState="true"
android:ellipsize="end"
android:maxLines="1"
android:textColor="?vctr_content_primary"
android:textStyle="bold"
app:layout_constrainedWidth="true"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.0"
@ -53,23 +50,38 @@
app:layout_constraintStart_toEndOf="@id/roomAvatarContainer"
app:layout_constraintTop_toTopOf="parent" />
<View
android:id="@+id/roomTypingView"
<TextView
android:id="@+id/subtitleView"
style="@style/Widget.Vector.TextView.Body"
android:layout_width="0dp"
android:layout_height="30dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="20dp"
android:layout_height="wrap_content"
android:layout_marginTop="3dp"
android:layout_marginEnd="8dp"
android:background="@drawable/placeholder_shape_8"
android:ellipsize="end"
android:lines="2"
android:textAlignment="viewStart"
android:textColor="?vctr_content_secondary"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="@id/roomNameView"
app:layout_constraintTop_toBottomOf="@id/roomNameView" />
<!-- Margin bottom does not work, so I use space -->
<Space
android:id="@+id/roomAvatarBottomSpace"
android:layout_width="0dp"
android:layout_height="7dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/subtitleView"
tools:layout_marginStart="120dp" />
<!-- We use vctr_list_separator_system here for a better rendering -->
<View
android:id="@+id/roomDividerView"
android:layout_width="0dp"
android:layout_height="1dp"
android:background="?vctr_list_separator_system"
app:layout_constraintTop_toBottomOf="@id/roomAvatarBottomSpace"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />

View File

@ -12,6 +12,11 @@
android:title="@string/invite_friends"
app:showAsAction="never" />
<item
android:id="@+id/menu_home_qr"
android:title="@string/add_by_qr_code"
app:showAsAction="never" />
<item
android:id="@+id/menu_home_suggestion"
android:icon="@drawable/ic_material_bug_report"
@ -42,5 +47,4 @@
android:title="@string/home_filter_placeholder_home"
app:iconTint="?vctr_content_secondary"
app:showAsAction="ifRoom" />
</menu>