Merge branch 'develop' into feature/ons/device_manager_security_sessions
This commit is contained in:
commit
45cf7dcd63
2
.github/workflows/post-pr.yml
vendored
2
.github/workflows/post-pr.yml
vendored
@ -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:
|
||||
|
75
.github/workflows/tests.yml
vendored
75
.github/workflows/tests.yml
vendored
@ -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
1
changelog.d/6970.wip
Normal 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
1
changelog.d/7079.bugfix
Normal 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
1
changelog.d/7108.misc
Normal 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
1
changelog.d/7153.wip
Normal 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
1
changelog.d/7166.misc
Normal file
@ -0,0 +1 @@
|
||||
New App Layout is now enabled by default! Go to the Settings > Labs to toggle this
|
@ -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) {
|
||||
|
@ -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.
|
||||
*/
|
||||
|
@ -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]
|
||||
|
@ -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
|
||||
}
|
@ -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,
|
||||
)
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
@ -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)
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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"
|
||||
|
@ -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)
|
||||
|
@ -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>) {
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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()
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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()
|
||||
}
|
||||
|
||||
|
@ -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) }
|
||||
}
|
||||
}
|
@ -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'
|
||||
|
@ -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()))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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())
|
||||
|
@ -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 -->
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
@ -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))
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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(
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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()
|
||||
}
|
||||
|
@ -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() {
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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) {
|
||||
|
@ -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) }
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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()
|
||||
}
|
||||
|
@ -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()
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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>,
|
||||
|
@ -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() }
|
||||
|
@ -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]
|
||||
|
@ -60,6 +60,7 @@ class IncomingShareController @Inject constructor(
|
||||
roomSummary,
|
||||
data.selectedRoomIds,
|
||||
RoomListDisplayMode.FILTERED,
|
||||
singleLineLastEvent = false,
|
||||
callback?.let { it::onRoomClicked },
|
||||
callback?.let { it::onRoomLongClicked }
|
||||
)
|
||||
|
@ -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>
|
||||
|
@ -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"
|
||||
|
@ -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"
|
||||
|
@ -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" />
|
||||
|
@ -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>
|
||||
|
Loading…
x
Reference in New Issue
Block a user