Merge branch 'release/1.5.16' into main

This commit is contained in:
Valere 2022-12-29 18:19:37 +01:00
commit 133fff1a8d
103 changed files with 1886 additions and 562 deletions

View file

@ -1,3 +1,34 @@
Changes in Element v.5.16 (2022-12-29)
======================================
Features ✨
----------
- [Rich text editor] Add support for links ([#7746](https://github.com/vector-im/element-android/issues/7746))
- [Poll] When a poll is ended, use /relations API to ensure poll results are correct ([#7767](https://github.com/vector-im/element-android/issues/7767))
- [Session manager] Security recommendations cards: whole view should be tappable ([#7795](https://github.com/vector-im/element-android/issues/7795))
- [Session manager] Other sessions list: header should not be sticky ([#7797](https://github.com/vector-im/element-android/issues/7797))
Bugfixes 🐛
----------
- Do not show typing notification of ignored users. ([#2965](https://github.com/vector-im/element-android/issues/2965))
- [Push Notifications, Threads] - quick reply to threaded notification now sent to thread except main timeline ([#7475](https://github.com/vector-im/element-android/issues/7475))
- [Session manager] Other sessions list: filter option is displayed when selection mode is enabled ([#7784](https://github.com/vector-im/element-android/issues/7784))
- [Session manager] Other sessions: Filter bottom sheet cut in landscape mode ([#7786](https://github.com/vector-im/element-android/issues/7786))
- Automatically show keyboard after learn more bottom sheet is dismissed ([#7790](https://github.com/vector-im/element-android/issues/7790))
- [Session Manager] Other sessions list: cannot select/deselect session by a long press when in select mode ([#7792](https://github.com/vector-im/element-android/issues/7792))
- Fix current session ip address visibility ([#7794](https://github.com/vector-im/element-android/issues/7794))
- Device Manager UI review fixes ([#7798](https://github.com/vector-im/element-android/issues/7798))
SDK API changes ⚠️
------------------
- [Sync] Sync Filter params are moved to MatrixConfiguration and will not be stored in session realm to avoid bug when session cache is cleared ([#7843](https://github.com/vector-im/element-android/issues/7843))
Other changes
-------------
- [Voice Broadcast] Replace the player timeline ([#7821](https://github.com/vector-im/element-android/issues/7821))
- Increase session manager test coverage ([#7836](https://github.com/vector-im/element-android/issues/7836))
Changes in Element v1.5.14 (2022-12-20)
=======================================

View file

@ -26,7 +26,7 @@ def jjwt = "0.11.5"
// Temporary version to unblock #6929. Once 0.16.0 is released we should use it, and revert
// the whole commit which set version 0.16.0-SNAPSHOT
def vanniktechEmoji = "0.16.0-SNAPSHOT"
def sentry = "6.9.0"
def sentry = "6.9.2"
def fragment = "1.5.5"
// Testing
def mockk = "1.12.3" // We need to use 1.12.3 to have mocking in androidTest until a new version is released: https://github.com/mockk/mockk/issues/819
@ -83,7 +83,7 @@ ext.libs = [
'appdistributionApi' : "com.google.firebase:firebase-appdistribution-api-ktx:$appDistribution",
'appdistribution' : "com.google.firebase:firebase-appdistribution:$appDistribution",
// Phone number https://github.com/google/libphonenumber
'phonenumber' : "com.googlecode.libphonenumber:libphonenumber:8.13.1"
'phonenumber' : "com.googlecode.libphonenumber:libphonenumber:8.13.3"
],
dagger : [
'dagger' : "com.google.dagger:dagger:$dagger",
@ -98,7 +98,7 @@ ext.libs = [
],
element : [
'opusencoder' : "io.element.android:opusencoder:1.1.0",
'wysiwyg' : "io.element.android:wysiwyg:0.9.0"
'wysiwyg' : "io.element.android:wysiwyg:0.10.0"
],
squareup : [
'moshi' : "com.squareup.moshi:moshi:$moshi",
@ -129,7 +129,7 @@ ext.libs = [
'mavericksTesting' : "com.airbnb.android:mavericks-testing:$mavericks"
],
maplibre : [
'androidSdk' : "org.maplibre.gl:android-sdk:9.5.2",
'androidSdk' : "org.maplibre.gl:android-sdk:9.6.0",
'pluginAnnotation' : "org.maplibre.gl:android-plugin-annotation-v9:1.0.0"
],
mockk : [

View file

@ -0,0 +1,2 @@
Main changes in this version: Thread are now enabled by default.
Full changelog: https://github.com/vector-im/element-android/releases

View file

@ -419,6 +419,7 @@
<string name="action_got_it">Got it</string>
<string name="action_select_all">Select all</string>
<string name="action_deselect_all">Deselect all</string>
<string name="action_stop">Yes, Stop</string>
<string name="copied_to_clipboard">Copied to clipboard</string>
@ -3120,6 +3121,8 @@
<string name="error_voice_broadcast_already_in_progress_message">You are already recording a voice broadcast. Please end your current voice broadcast to start a new one.</string>
<!-- Examples of usage: 6h 15min 30sec left / 15min 30sec left / 30sec left -->
<string name="voice_broadcast_recording_time_left">%1$s left</string>
<string name="stop_voice_broadcast_dialog_title">Stop live broadcasting?</string>
<string name="stop_voice_broadcast_content">Are you sure you want to stop your live broadcast? This will end the broadcast and the full recording will be available in the room.</string>
<string name="upgrade_room_for_restricted">Anyone in %s will be able to find and join this room - no need to manually invite everyone. Youll be able to change this in room settings anytime.</string>
<string name="upgrade_room_for_restricted_no_param">Anyone in a parent space will be able to find and join this room - no need to manually invite everyone. Youll be able to change this in room settings anytime.</string>
@ -3335,7 +3338,7 @@
<item quantity="one">Consider signing out from old sessions (%1$d day or more) that you dont use anymore.</item>
<item quantity="other">Consider signing out from old sessions (%1$d days or more) that you dont use anymore.</item>
</plurals>
<string name="device_manager_current_session_title">Current Session</string>
<string name="device_manager_current_session_title">Current session</string>
<string name="device_manager_session_title">Session</string>
<string name="device_manager_device_title">Device</string>
<!-- Examples: Last activity Yesterday at 6PM, Last activity Aug 31 at 5:47PM -->
@ -3476,13 +3479,19 @@
<string name="qr_code_login_confirm_security_code">Confirm</string>
<string name="qr_code_login_confirm_security_code_description">Please ensure that you know the origin of this code. By linking devices, you will provide someone with full access to your account.</string>
<!-- WYSIWYG Composer -->
<!-- Rich text editor -->
<string name="rich_text_editor_format_bold">Apply bold format</string>
<string name="rich_text_editor_format_italic">Apply italic format</string>
<string name="rich_text_editor_format_strikethrough">Apply strikethrough format</string>
<string name="rich_text_editor_format_underline">Apply underline format</string>
<string name="rich_text_editor_link">Set link</string>
<string name="rich_text_editor_full_screen_toggle">Toggle full screen mode</string>
<string name="set_link_text">Text</string>
<string name="set_link_link">Link</string>
<string name="set_link_create">Create a link</string>
<string name="set_link_edit">Edit link</string>
<!-- ReplyTo events -->
<string name="message_reply_to_prefix">In reply to</string>
<string name="message_reply_to_sender_sent_file">sent a file.</string>

View file

@ -62,7 +62,7 @@ android {
// that the app's state is completely cleared between tests.
testInstrumentationRunnerArguments clearPackageData: 'true'
buildConfigField "String", "SDK_VERSION", "\"1.5.14\""
buildConfigField "String", "SDK_VERSION", "\"1.5.16\""
buildConfigField "String", "GIT_SDK_REVISION", "\"${gitRevision()}\""
buildConfigField "String", "GIT_SDK_REVISION_UNIX_DATE", "\"${gitRevisionUnixDate()}\""

View file

@ -50,7 +50,6 @@ import org.matrix.android.sdk.api.session.room.send.SendState
import org.matrix.android.sdk.api.session.room.timeline.Timeline
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
import org.matrix.android.sdk.api.session.room.timeline.TimelineSettings
import org.matrix.android.sdk.api.session.sync.filter.SyncFilterBuilder
import timber.log.Timber
import java.util.UUID
import java.util.concurrent.CountDownLatch
@ -347,10 +346,6 @@ class CommonTestHelper internal constructor(context: Context, val cryptoConfig:
assertTrue(registrationResult is RegistrationResult.Success)
val session = (registrationResult as RegistrationResult.Success).session
session.open()
session.filterService().setSyncFilter(
SyncFilterBuilder()
.lazyLoadMembersForStateEvents(true)
)
if (sessionTestParams.withInitialSync) {
syncSession(session, 120_000)
}

View file

@ -16,9 +16,13 @@
package org.matrix.android.sdk.api
import org.matrix.android.sdk.api.session.sync.filter.SyncFilterParams
data class SyncConfig(
/**
* Time to keep sync connection alive for before making another request in milliseconds.
*/
val longPollTimeout: Long = 30_000L,
val syncFilterParams: SyncFilterParams = SyncFilterParams()
)

View file

@ -50,7 +50,6 @@ import org.matrix.android.sdk.api.session.securestorage.SharedSecretStorageServi
import org.matrix.android.sdk.api.session.signout.SignOutService
import org.matrix.android.sdk.api.session.space.SpaceService
import org.matrix.android.sdk.api.session.statistics.StatisticsListener
import org.matrix.android.sdk.api.session.sync.FilterService
import org.matrix.android.sdk.api.session.sync.SyncService
import org.matrix.android.sdk.api.session.terms.TermsService
import org.matrix.android.sdk.api.session.thirdparty.ThirdPartyService
@ -163,11 +162,6 @@ interface Session {
*/
fun signOutService(): SignOutService
/**
* Returns the FilterService associated with the session.
*/
fun filterService(): FilterService
/**
* Returns the PushRuleService associated with the session.
*/

View file

@ -388,7 +388,13 @@ fun Event.isLocationMessage(): Boolean {
}
}
fun Event.isPoll(): Boolean = getClearType() in EventType.POLL_START.values || getClearType() in EventType.POLL_END.values
fun Event.isPoll(): Boolean = isPollStart() || isPollEnd()
fun Event.isPollStart(): Boolean = getClearType() in EventType.POLL_START.values
fun Event.isPollResponse(): Boolean = getClearType() in EventType.POLL_RESPONSE.values
fun Event.isPollEnd(): Boolean = getClearType() in EventType.POLL_END.values
fun Event.isSticker(): Boolean = getClearType() == EventType.STICKER

View file

@ -14,9 +14,9 @@
* limitations under the License.
*/
package org.matrix.android.sdk.internal.sync.filter
package org.matrix.android.sdk.api.session.sync.filter
internal data class SyncFilterParams(
data class SyncFilterParams(
val lazyLoadMembersForStateEvents: Boolean? = null,
val lazyLoadMembersForMessageEvents: Boolean? = null,
val useThreadNotifications: Boolean? = null,

View file

@ -24,10 +24,12 @@ import kotlinx.coroutines.withContext
import org.matrix.android.sdk.api.MatrixCallback
import org.matrix.android.sdk.api.MatrixCoroutineDispatchers
import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_OLM
import org.matrix.android.sdk.api.extensions.tryOrNull
import org.matrix.android.sdk.api.logger.LoggerTag
import org.matrix.android.sdk.api.session.crypto.MXCryptoError
import org.matrix.android.sdk.api.session.crypto.model.MXEventDecryptionResult
import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap
import org.matrix.android.sdk.api.session.crypto.model.OlmDecryptionResult
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.content.OlmEventContent
@ -85,6 +87,27 @@ internal class EventDecryptor @Inject constructor(
return internalDecryptEvent(event, timeline)
}
/**
* Decrypt an event and save the result in the given event.
*
* @param event the raw event.
* @param timeline the id of the timeline where the event is decrypted. It is used to prevent replay attack.
*/
suspend fun decryptEventAndSaveResult(event: Event, timeline: String) {
tryOrNull(message = "Unable to decrypt the event") {
decryptEvent(event, timeline)
}
?.let { result ->
event.mxDecryptionResult = OlmDecryptionResult(
payload = result.clearEvent,
senderKey = result.senderCurve25519Key,
keysClaimed = result.claimedEd25519Key?.let { mapOf("ed25519" to it) },
forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain,
isSafe = result.isSafe
)
}
}
/**
* Decrypt an event asynchronously.
*

View file

@ -63,6 +63,7 @@ import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo043
import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo044
import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo045
import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo046
import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo047
import org.matrix.android.sdk.internal.util.Normalizer
import org.matrix.android.sdk.internal.util.database.MatrixRealmMigration
import javax.inject.Inject
@ -71,7 +72,7 @@ internal class RealmSessionStoreMigration @Inject constructor(
private val normalizer: Normalizer
) : MatrixRealmMigration(
dbName = "Session",
schemaVersion = 46L,
schemaVersion = 47L,
) {
/**
* Forces all RealmSessionStoreMigration instances to be equal.
@ -127,5 +128,6 @@ internal class RealmSessionStoreMigration @Inject constructor(
if (oldVersion < 44) MigrateSessionTo044(realm).perform()
if (oldVersion < 45) MigrateSessionTo045(realm).perform()
if (oldVersion < 46) MigrateSessionTo046(realm).perform()
if (oldVersion < 47) MigrateSessionTo047(realm).perform()
}
}

View file

@ -1,61 +0,0 @@
/*
* 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 io.realm.RealmList
import org.matrix.android.sdk.internal.database.model.SyncFilterParamsEntity
import org.matrix.android.sdk.internal.sync.filter.SyncFilterParams
import javax.inject.Inject
internal class FilterParamsMapper @Inject constructor() {
fun map(entity: SyncFilterParamsEntity): SyncFilterParams {
val eventTypes = if (entity.listOfSupportedEventTypesHasBeenSet) {
entity.listOfSupportedEventTypes?.toList()
} else {
null
}
val stateEventTypes = if (entity.listOfSupportedStateEventTypesHasBeenSet) {
entity.listOfSupportedStateEventTypes?.toList()
} else {
null
}
return SyncFilterParams(
useThreadNotifications = entity.useThreadNotifications,
lazyLoadMembersForMessageEvents = entity.lazyLoadMembersForMessageEvents,
lazyLoadMembersForStateEvents = entity.lazyLoadMembersForStateEvents,
listOfSupportedEventTypes = eventTypes,
listOfSupportedStateEventTypes = stateEventTypes,
)
}
fun map(params: SyncFilterParams): SyncFilterParamsEntity {
return SyncFilterParamsEntity(
useThreadNotifications = params.useThreadNotifications,
lazyLoadMembersForMessageEvents = params.lazyLoadMembersForMessageEvents,
lazyLoadMembersForStateEvents = params.lazyLoadMembersForStateEvents,
listOfSupportedEventTypes = params.listOfSupportedEventTypes.toRealmList(),
listOfSupportedEventTypesHasBeenSet = params.listOfSupportedEventTypes != null,
listOfSupportedStateEventTypes = params.listOfSupportedStateEventTypes.toRealmList(),
listOfSupportedStateEventTypesHasBeenSet = params.listOfSupportedStateEventTypes != null,
)
}
private fun List<String>?.toRealmList(): RealmList<String>? {
return this?.toTypedArray()?.let { RealmList(*it) }
}
}

View file

@ -0,0 +1,27 @@
/*
* 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.internal.util.database.RealmMigrator
internal class MigrateSessionTo047(realm: DynamicRealm) : RealmMigrator(realm, 47) {
override fun doMigrate(realm: DynamicRealm) {
realm.schema.remove("SyncFilterParamsEntity")
}
}

View file

@ -72,7 +72,6 @@ import org.matrix.android.sdk.internal.database.model.threads.ThreadSummaryEntit
SpaceParentSummaryEntity::class,
UserPresenceEntity::class,
ThreadSummaryEntity::class,
SyncFilterParamsEntity::class,
ThreadListPageEntity::class
]
)

View file

@ -57,7 +57,6 @@ import org.matrix.android.sdk.api.session.search.SearchService
import org.matrix.android.sdk.api.session.securestorage.SharedSecretStorageService
import org.matrix.android.sdk.api.session.signout.SignOutService
import org.matrix.android.sdk.api.session.space.SpaceService
import org.matrix.android.sdk.api.session.sync.FilterService
import org.matrix.android.sdk.api.session.sync.SyncService
import org.matrix.android.sdk.api.session.terms.TermsService
import org.matrix.android.sdk.api.session.thirdparty.ThirdPartyService
@ -97,7 +96,6 @@ internal class DefaultSession @Inject constructor(
private val roomService: Lazy<RoomService>,
private val roomDirectoryService: Lazy<RoomDirectoryService>,
private val userService: Lazy<UserService>,
private val filterService: Lazy<FilterService>,
private val federationService: Lazy<FederationService>,
private val cacheService: Lazy<CacheService>,
private val signOutService: Lazy<SignOutService>,
@ -209,7 +207,6 @@ internal class DefaultSession @Inject constructor(
override fun roomDirectoryService(): RoomDirectoryService = roomDirectoryService.get()
override fun userService(): UserService = userService.get()
override fun signOutService(): SignOutService = signOutService.get()
override fun filterService(): FilterService = filterService.get()
override fun pushRuleService(): PushRuleService = pushRuleService.get()
override fun pushersService(): PushersService = pushersService.get()
override fun eventService(): EventService = eventService.get()

View file

@ -17,20 +17,15 @@
package org.matrix.android.sdk.internal.session.filter
import com.zhuinden.monarchy.Monarchy
import io.realm.kotlin.where
import org.matrix.android.sdk.internal.database.mapper.FilterParamsMapper
import org.matrix.android.sdk.internal.database.model.FilterEntity
import org.matrix.android.sdk.internal.database.model.SyncFilterParamsEntity
import org.matrix.android.sdk.internal.database.query.get
import org.matrix.android.sdk.internal.database.query.getOrCreate
import org.matrix.android.sdk.internal.di.SessionDatabase
import org.matrix.android.sdk.internal.sync.filter.SyncFilterParams
import org.matrix.android.sdk.internal.util.awaitTransaction
import javax.inject.Inject
internal class DefaultFilterRepository @Inject constructor(
@SessionDatabase private val monarchy: Monarchy,
private val filterParamsMapper: FilterParamsMapper
) : FilterRepository {
override suspend fun storeSyncFilter(filter: Filter, filterId: String, roomEventFilter: RoomEventFilter) {
@ -69,19 +64,4 @@ internal class DefaultFilterRepository @Inject constructor(
FilterEntity.getOrCreate(it).roomEventFilterJson
}
}
override suspend fun getStoredFilterParams(): SyncFilterParams? {
return monarchy.awaitTransaction { realm ->
realm.where<SyncFilterParamsEntity>().findFirst()?.let {
filterParamsMapper.map(it)
}
}
}
override suspend fun storeFilterParams(params: SyncFilterParams) {
return monarchy.awaitTransaction { realm ->
val entity = filterParamsMapper.map(params)
realm.insertOrUpdate(entity)
}
}
}

View file

@ -1,43 +0,0 @@
/*
* Copyright 2020 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.session.filter
import org.matrix.android.sdk.api.session.sync.FilterService
import org.matrix.android.sdk.api.session.sync.filter.SyncFilterBuilder
import org.matrix.android.sdk.internal.session.homeserver.HomeServerCapabilitiesDataSource
import javax.inject.Inject
internal class DefaultFilterService @Inject constructor(
private val saveFilterTask: SaveFilterTask,
private val filterRepository: FilterRepository,
private val homeServerCapabilitiesDataSource: HomeServerCapabilitiesDataSource,
) : FilterService {
// TODO Pass a list of support events instead
override suspend fun setSyncFilter(filterBuilder: SyncFilterBuilder) {
filterRepository.storeFilterParams(filterBuilder.extractParams())
// don't upload/store filter until homeserver capabilities are fetched
homeServerCapabilitiesDataSource.getHomeServerCapabilities()?.let { homeServerCapabilities ->
saveFilterTask.execute(
SaveFilterTask.Params(
filter = filterBuilder.build(homeServerCapabilities)
)
)
}
}
}

View file

@ -19,7 +19,6 @@ package org.matrix.android.sdk.internal.session.filter
import dagger.Binds
import dagger.Module
import dagger.Provides
import org.matrix.android.sdk.api.session.sync.FilterService
import org.matrix.android.sdk.internal.session.SessionScope
import retrofit2.Retrofit
@ -39,9 +38,6 @@ internal abstract class FilterModule {
@Binds
abstract fun bindFilterRepository(repository: DefaultFilterRepository): FilterRepository
@Binds
abstract fun bindFilterService(service: DefaultFilterService): FilterService
@Binds
abstract fun bindSaveFilterTask(task: DefaultSaveFilterTask): SaveFilterTask

View file

@ -16,8 +16,6 @@
package org.matrix.android.sdk.internal.session.filter
import org.matrix.android.sdk.internal.sync.filter.SyncFilterParams
/**
* Repository for request filters.
*/
@ -44,14 +42,4 @@ internal interface FilterRepository {
* Return the room filter.
*/
suspend fun getRoomFilterBody(): String
/**
* Returns filter params stored in local storage if it exists.
*/
suspend fun getStoredFilterParams(): SyncFilterParams?
/**
* Stores filter params to local storage.
*/
suspend fun storeFilterParams(params: SyncFilterParams)
}

View file

@ -16,9 +16,10 @@
package org.matrix.android.sdk.internal.session.filter
import org.matrix.android.sdk.api.MatrixConfiguration
import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilities
import org.matrix.android.sdk.api.session.sync.filter.SyncFilterBuilder
import org.matrix.android.sdk.internal.session.homeserver.HomeServerCapabilitiesDataSource
import org.matrix.android.sdk.internal.sync.filter.SyncFilterBuilder
import org.matrix.android.sdk.internal.task.Task
import javax.inject.Inject
@ -27,7 +28,8 @@ internal interface GetCurrentFilterTask : Task<Unit, String>
internal class DefaultGetCurrentFilterTask @Inject constructor(
private val filterRepository: FilterRepository,
private val homeServerCapabilitiesDataSource: HomeServerCapabilitiesDataSource,
private val saveFilterTask: SaveFilterTask
private val saveFilterTask: SaveFilterTask,
private val matrixConfiguration: MatrixConfiguration
) : GetCurrentFilterTask {
override suspend fun execute(params: Unit): String {
@ -35,7 +37,7 @@ internal class DefaultGetCurrentFilterTask @Inject constructor(
val storedFilterBody = filterRepository.getStoredSyncFilterBody()
val homeServerCapabilities = homeServerCapabilitiesDataSource.getHomeServerCapabilities() ?: HomeServerCapabilities()
val currentFilter = SyncFilterBuilder()
.with(filterRepository.getStoredFilterParams())
.with(matrixConfiguration.syncConfig.syncFilterParams)
.build(homeServerCapabilities)
val currentFilterBody = currentFilter.toJSONString()

View file

@ -18,7 +18,6 @@ package org.matrix.android.sdk.internal.session.room
import org.matrix.android.sdk.api.session.events.model.Content
import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.events.model.RelationType
import org.matrix.android.sdk.api.session.room.model.Membership
import org.matrix.android.sdk.api.session.room.model.RoomStrippedState
import org.matrix.android.sdk.api.session.room.model.roomdirectory.PublicRoomsParams
@ -251,7 +250,7 @@ internal interface RoomAPI {
* @param limit max number of Event to retrieve
*/
@GET(NetworkConstants.URI_API_PREFIX_PATH_UNSTABLE + "rooms/{roomId}/relations/{eventId}/{relationType}/{eventType}")
suspend fun getRelations(
suspend fun getRelationsWithEventType(
@Path("roomId") roomId: String,
@Path("eventId") eventId: String,
@Path("relationType") relationType: String,
@ -262,7 +261,7 @@ internal interface RoomAPI {
): RelationsResponse
/**
* Paginate relations for thread events based in normal topological order.
* Paginate relations for events based in normal topological order.
*
* @param roomId the room Id
* @param eventId the event Id
@ -272,10 +271,10 @@ internal interface RoomAPI {
* @param limit max number of Event to retrieve
*/
@GET(NetworkConstants.URI_API_PREFIX_PATH_UNSTABLE + "rooms/{roomId}/relations/{eventId}/{relationType}")
suspend fun getThreadsRelations(
suspend fun getRelations(
@Path("roomId") roomId: String,
@Path("eventId") eventId: String,
@Path("relationType") relationType: String = RelationType.THREAD,
@Path("relationType") relationType: String,
@Query("from") from: String? = null,
@Query("to") to: String? = null,
@Query("limit") limit: Int? = null

View file

@ -99,6 +99,8 @@ import org.matrix.android.sdk.internal.session.room.relation.DefaultUpdateQuickR
import org.matrix.android.sdk.internal.session.room.relation.FetchEditHistoryTask
import org.matrix.android.sdk.internal.session.room.relation.FindReactionEventForUndoTask
import org.matrix.android.sdk.internal.session.room.relation.UpdateQuickReactionTask
import org.matrix.android.sdk.internal.session.room.relation.poll.DefaultFetchPollResponseEventsTask
import org.matrix.android.sdk.internal.session.room.relation.poll.FetchPollResponseEventsTask
import org.matrix.android.sdk.internal.session.room.relation.threads.DefaultFetchThreadSummariesTask
import org.matrix.android.sdk.internal.session.room.relation.threads.DefaultFetchThreadTimelineTask
import org.matrix.android.sdk.internal.session.room.relation.threads.FetchThreadSummariesTask
@ -354,4 +356,7 @@ internal abstract class RoomModule {
@Binds
abstract fun bindRedactLiveLocationShareTask(task: DefaultRedactLiveLocationShareTask): RedactLiveLocationShareTask
@Binds
abstract fun bindFetchPollResponseEventsTask(task: DefaultFetchPollResponseEventsTask): FetchPollResponseEventsTask
}

View file

@ -17,6 +17,7 @@
package org.matrix.android.sdk.internal.session.room.aggregation.poll
import io.realm.Realm
import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.extensions.orFalse
import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.events.model.Event
@ -40,9 +41,14 @@ import org.matrix.android.sdk.internal.database.model.PollResponseAggregatedSumm
import org.matrix.android.sdk.internal.database.query.create
import org.matrix.android.sdk.internal.database.query.getOrCreate
import org.matrix.android.sdk.internal.database.query.where
import org.matrix.android.sdk.internal.session.room.relation.poll.FetchPollResponseEventsTask
import org.matrix.android.sdk.internal.task.TaskExecutor
import javax.inject.Inject
class DefaultPollAggregationProcessor @Inject constructor() : PollAggregationProcessor {
internal class DefaultPollAggregationProcessor @Inject constructor(
private val taskExecutor: TaskExecutor,
private val fetchPollResponseEventsTask: FetchPollResponseEventsTask,
) : PollAggregationProcessor {
override fun handlePollStartEvent(realm: Realm, event: Event): Boolean {
val content = event.getClearContent()?.toModel<MessagePollContent>()
@ -174,6 +180,10 @@ class DefaultPollAggregationProcessor @Inject constructor() : PollAggregationPro
aggregatedPollSummaryEntity.sourceEvents.add(event.eventId)
}
if (!isLocalEcho) {
ensurePollIsFullyAggregated(roomId, pollEventId)
}
return true
}
@ -200,4 +210,20 @@ class DefaultPollAggregationProcessor @Inject constructor() : PollAggregationPro
eventAnnotationsSummaryEntity.pollResponseSummary = it
}
}
/**
* Check that all related votes to a given poll are all retrieved and aggregated.
*/
private fun ensurePollIsFullyAggregated(
roomId: String,
pollEventId: String
) {
taskExecutor.executorScope.launch {
val params = FetchPollResponseEventsTask.Params(
roomId = roomId,
startPollEventId = pollEventId,
)
fetchPollResponseEventsTask.execute(params)
}
}
}

View file

@ -43,7 +43,7 @@ internal class DefaultFetchEditHistoryTask @Inject constructor(
override suspend fun execute(params: FetchEditHistoryTask.Params): List<Event> {
val isRoomEncrypted = cryptoSessionInfoProvider.isRoomEncrypted(params.roomId)
val response = executeRequest(globalErrorReceiver) {
roomAPI.getRelations(
roomAPI.getRelationsWithEventType(
roomId = params.roomId,
eventId = params.eventId,
relationType = RelationType.REPLACE,

View file

@ -0,0 +1,118 @@
/*
* 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.session.room.relation.poll
import androidx.annotation.VisibleForTesting
import com.zhuinden.monarchy.Monarchy
import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.events.model.RelationType
import org.matrix.android.sdk.api.session.events.model.isPollResponse
import org.matrix.android.sdk.api.session.room.send.SendState
import org.matrix.android.sdk.internal.crypto.EventDecryptor
import org.matrix.android.sdk.internal.database.mapper.toEntity
import org.matrix.android.sdk.internal.database.model.EventEntity
import org.matrix.android.sdk.internal.database.model.EventInsertType
import org.matrix.android.sdk.internal.database.query.copyToRealmOrIgnore
import org.matrix.android.sdk.internal.database.query.where
import org.matrix.android.sdk.internal.di.SessionDatabase
import org.matrix.android.sdk.internal.network.GlobalErrorReceiver
import org.matrix.android.sdk.internal.network.executeRequest
import org.matrix.android.sdk.internal.session.room.RoomAPI
import org.matrix.android.sdk.internal.session.room.relation.RelationsResponse
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 javax.inject.Inject
@VisibleForTesting
const val FETCH_RELATED_EVENTS_LIMIT = 50
/**
* Task to fetch all the vote events to ensure full aggregation for a given poll.
*/
internal interface FetchPollResponseEventsTask : Task<FetchPollResponseEventsTask.Params, Result<Unit>> {
data class Params(
val roomId: String,
val startPollEventId: String,
)
}
internal class DefaultFetchPollResponseEventsTask @Inject constructor(
private val roomAPI: RoomAPI,
private val globalErrorReceiver: GlobalErrorReceiver,
@SessionDatabase private val monarchy: Monarchy,
private val clock: Clock,
private val eventDecryptor: EventDecryptor,
) : FetchPollResponseEventsTask {
override suspend fun execute(params: FetchPollResponseEventsTask.Params): Result<Unit> = runCatching {
var nextBatch: String? = fetchAndProcessRelatedEventsFrom(params)
while (nextBatch?.isNotEmpty() == true) {
nextBatch = fetchAndProcessRelatedEventsFrom(params, from = nextBatch)
}
}
private suspend fun fetchAndProcessRelatedEventsFrom(params: FetchPollResponseEventsTask.Params, from: String? = null): String? {
val response = getRelatedEvents(params, from)
val filteredEvents = response.chunks
.map { decryptEventIfNeeded(it) }
.filter { it.isPollResponse() }
addMissingEventsInDB(params.roomId, filteredEvents)
return response.nextBatch
}
private suspend fun getRelatedEvents(params: FetchPollResponseEventsTask.Params, from: String? = null): RelationsResponse {
return executeRequest(globalErrorReceiver, canRetry = true) {
roomAPI.getRelations(
roomId = params.roomId,
eventId = params.startPollEventId,
relationType = RelationType.REFERENCE,
from = from,
limit = FETCH_RELATED_EVENTS_LIMIT,
)
}
}
private suspend fun addMissingEventsInDB(roomId: String, events: List<Event>) {
monarchy.awaitTransaction { realm ->
val eventIdsToCheck = events.mapNotNull { it.eventId }.filter { it.isNotEmpty() }
if (eventIdsToCheck.isNotEmpty()) {
val existingIds = EventEntity.where(realm, eventIdsToCheck).findAll().toList().map { it.eventId }
events.filterNot { it.eventId in existingIds }
.map { it.toEntity(roomId = roomId, sendState = SendState.SYNCED, ageLocalTs = computeLocalTs(it)) }
.forEach { it.copyToRealmOrIgnore(realm, EventInsertType.PAGINATION) }
}
}
}
private suspend fun decryptEventIfNeeded(event: Event): Event {
if (event.isEncrypted()) {
eventDecryptor.decryptEventAndSaveResult(event, timeline = "")
}
event.ageLocalTs = computeLocalTs(event)
return event
}
private fun computeLocalTs(event: Event) = clock.epochMillis() - (event.unsignedData?.age ?: 0)
}

View file

@ -22,6 +22,7 @@ import org.matrix.android.sdk.api.session.crypto.MXCryptoError
import org.matrix.android.sdk.api.session.crypto.model.OlmDecryptionResult
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.RelationType
import org.matrix.android.sdk.api.session.room.model.RoomMemberContent
import org.matrix.android.sdk.api.session.room.send.SendState
import org.matrix.android.sdk.internal.crypto.DefaultCryptoService
@ -102,11 +103,12 @@ internal class DefaultFetchThreadTimelineTask @Inject constructor(
override suspend fun execute(params: FetchThreadTimelineTask.Params): Result {
val response = executeRequest(globalErrorReceiver) {
roomAPI.getThreadsRelations(
roomAPI.getRelations(
roomId = params.roomId,
eventId = params.rootThreadEventId,
relationType = RelationType.THREAD,
from = params.from,
limit = params.limit
limit = params.limit,
)
}

View file

@ -196,6 +196,11 @@ internal class RoomSummaryUpdater @Inject constructor(
roomSummaryEntity.otherMemberIds.clear()
roomSummaryEntity.otherMemberIds.addAll(otherRoomMembers)
if (roomSummary?.joinedMembersCount == null) {
// in case m.joined_member_count from sync summary was null?
// better to use what we know
roomSummaryEntity.joinedMembersCount = otherRoomMembers.size + 1
}
if (roomSummaryEntity.isEncrypted && otherRoomMembers.isNotEmpty()) {
if (aggregator == null) {
// Do it now

View file

@ -16,8 +16,6 @@
package org.matrix.android.sdk.internal.session.room.timeline
import org.matrix.android.sdk.api.extensions.tryOrNull
import org.matrix.android.sdk.api.session.crypto.model.OlmDecryptionResult
import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.internal.crypto.EventDecryptor
import org.matrix.android.sdk.internal.network.GlobalErrorReceiver
@ -48,18 +46,7 @@ internal class DefaultGetEventTask @Inject constructor(
// Try to decrypt the Event
if (event.isEncrypted()) {
tryOrNull(message = "Unable to decrypt the event") {
eventDecryptor.decryptEvent(event, "")
}
?.let { result ->
event.mxDecryptionResult = OlmDecryptionResult(
payload = result.clearEvent,
senderKey = result.senderCurve25519Key,
keysClaimed = result.claimedEd25519Key?.let { mapOf("ed25519" to it) },
forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain,
isSafe = result.isSafe
)
}
eventDecryptor.decryptEventAndSaveResult(event, timeline = "")
}
event.ageLocalTs = clock.epochMillis() - (event.unsignedData?.age ?: 0)

View file

@ -18,6 +18,7 @@ package org.matrix.android.sdk.internal.session.sync.handler.room
import io.realm.Realm
import org.matrix.android.sdk.api.session.room.sender.SenderInfo
import org.matrix.android.sdk.internal.database.model.IgnoredUserEntity
import org.matrix.android.sdk.internal.di.UserId
import org.matrix.android.sdk.internal.session.room.membership.RoomMemberHelper
import org.matrix.android.sdk.internal.session.typing.DefaultTypingUsersTracker
@ -30,8 +31,15 @@ internal class RoomTypingUsersHandler @Inject constructor(
// TODO This could be handled outside of the Realm transaction. Use the new aggregator?
fun handle(realm: Realm, roomId: String, ephemeralResult: RoomSyncHandler.EphemeralResult?) {
val typingUserIds = ephemeralResult?.typingUserIds
if (typingUserIds.isNullOrEmpty()) {
typingUsersTracker.setTypingUsersFromRoom(roomId, emptyList())
return
}
// Filter ignored users and current user
val filteredUserIds = realm.where(IgnoredUserEntity::class.java).findAll().map { it.userId } + userId
val roomMemberHelper = RoomMemberHelper(realm, roomId)
val typingIds = ephemeralResult?.typingUserIds?.filter { it != userId }.orEmpty()
val typingIds = typingUserIds.filter { it !in filteredUserIds }
val senderInfo = typingIds.map { userId ->
val roomMemberSummaryEntity = roomMemberHelper.getLastRoomMember(userId)
SenderInfo(

View file

@ -14,15 +14,15 @@
* limitations under the License.
*/
package org.matrix.android.sdk.api.session.sync.filter
package org.matrix.android.sdk.internal.sync.filter
import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilities
import org.matrix.android.sdk.api.session.sync.filter.SyncFilterParams
import org.matrix.android.sdk.internal.session.filter.Filter
import org.matrix.android.sdk.internal.session.filter.RoomEventFilter
import org.matrix.android.sdk.internal.session.filter.RoomFilter
import org.matrix.android.sdk.internal.sync.filter.SyncFilterParams
class SyncFilterBuilder {
internal class SyncFilterBuilder {
private var lazyLoadMembersForStateEvents: Boolean? = null
private var lazyLoadMembersForMessageEvents: Boolean? = null
private var useThreadNotifications: Boolean? = null
@ -54,16 +54,6 @@ class SyncFilterBuilder {
}
}
internal fun extractParams(): SyncFilterParams {
return SyncFilterParams(
useThreadNotifications = useThreadNotifications,
lazyLoadMembersForMessageEvents = lazyLoadMembersForMessageEvents,
lazyLoadMembersForStateEvents = lazyLoadMembersForStateEvents,
listOfSupportedEventTypes = listOfSupportedEventTypes,
listOfSupportedStateEventTypes = listOfSupportedStateEventTypes,
)
}
internal fun build(homeServerCapabilities: HomeServerCapabilities): Filter {
return Filter(
room = buildRoomFilter(homeServerCapabilities)

View file

@ -16,9 +16,13 @@
package org.matrix.android.sdk.internal.session.room.aggregation.poll
import io.mockk.coVerify
import io.mockk.every
import io.mockk.mockk
import io.realm.RealmList
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.advanceUntilIdle
import kotlinx.coroutines.test.runTest
import org.amshove.kluent.shouldBeFalse
import org.amshove.kluent.shouldBeTrue
import org.junit.Before
@ -34,6 +38,7 @@ import org.matrix.android.sdk.internal.database.model.PollResponseAggregatedSumm
import org.matrix.android.sdk.internal.session.room.aggregation.poll.PollEventsTestData.AN_EVENT_ID
import org.matrix.android.sdk.internal.session.room.aggregation.poll.PollEventsTestData.AN_INVALID_POLL_RESPONSE_EVENT
import org.matrix.android.sdk.internal.session.room.aggregation.poll.PollEventsTestData.A_BROKEN_POLL_REPLACE_EVENT
import org.matrix.android.sdk.internal.session.room.aggregation.poll.PollEventsTestData.A_POLL_END_CONTENT
import org.matrix.android.sdk.internal.session.room.aggregation.poll.PollEventsTestData.A_POLL_END_EVENT
import org.matrix.android.sdk.internal.session.room.aggregation.poll.PollEventsTestData.A_POLL_REFERENCE_EVENT
import org.matrix.android.sdk.internal.session.room.aggregation.poll.PollEventsTestData.A_POLL_REPLACE_EVENT
@ -43,13 +48,22 @@ import org.matrix.android.sdk.internal.session.room.aggregation.poll.PollEventsT
import org.matrix.android.sdk.internal.session.room.aggregation.poll.PollEventsTestData.A_ROOM_ID
import org.matrix.android.sdk.internal.session.room.aggregation.poll.PollEventsTestData.A_TIMELINE_EVENT
import org.matrix.android.sdk.internal.session.room.aggregation.poll.PollEventsTestData.A_USER_ID_1
import org.matrix.android.sdk.internal.session.room.relation.poll.FetchPollResponseEventsTask
import org.matrix.android.sdk.test.fakes.FakeFetchPollResponseEventsTask
import org.matrix.android.sdk.test.fakes.FakeRealm
import org.matrix.android.sdk.test.fakes.FakeTaskExecutor
import org.matrix.android.sdk.test.fakes.givenEqualTo
import org.matrix.android.sdk.test.fakes.givenFindFirst
@OptIn(ExperimentalCoroutinesApi::class)
class DefaultPollAggregationProcessorTest {
private val pollAggregationProcessor: PollAggregationProcessor = DefaultPollAggregationProcessor()
private val fakeTaskExecutor = FakeTaskExecutor()
private val fakeFetchPollResponseEventsTask = FakeFetchPollResponseEventsTask()
private val pollAggregationProcessor: PollAggregationProcessor = DefaultPollAggregationProcessor(
taskExecutor = fakeTaskExecutor.instance,
fetchPollResponseEventsTask = fakeFetchPollResponseEventsTask
)
private val realm = FakeRealm()
private val session = mockk<Session>()
@ -114,16 +128,28 @@ class DefaultPollAggregationProcessorTest {
}
@Test
fun `given a poll end event, when processing, then is processed and return true`() {
fun `given a poll end event, when processing, then is processed and return true`() = runTest {
// Given
every { realm.instance.createObject(PollResponseAggregatedSummaryEntity::class.java) } returns PollResponseAggregatedSummaryEntity()
every { fakeTaskExecutor.instance.executorScope } returns this
// When
val powerLevelsHelper = mockRedactionPowerLevels(A_USER_ID_1, true)
// Then
pollAggregationProcessor.handlePollEndEvent(session, powerLevelsHelper, realm.instance, A_POLL_END_EVENT).shouldBeTrue()
}
@Test
fun `given a poll end event for my own poll without enough redaction power level, when processing, then is processed and returns true`() {
fun `given a poll end event for my own poll without enough redaction power level, when processing, then is processed and returns true`() = runTest {
// Given
every { realm.instance.createObject(PollResponseAggregatedSummaryEntity::class.java) } returns PollResponseAggregatedSummaryEntity()
every { fakeTaskExecutor.instance.executorScope } returns this
// When
val powerLevelsHelper = mockRedactionPowerLevels(A_USER_ID_1, false)
// Then
pollAggregationProcessor.handlePollEndEvent(session, powerLevelsHelper, realm.instance, A_POLL_END_EVENT).shouldBeTrue()
}
@ -135,6 +161,28 @@ class DefaultPollAggregationProcessorTest {
pollAggregationProcessor.handlePollEndEvent(session, powerLevelsHelper, realm.instance, event).shouldBeFalse()
}
@Test
fun `given a non local echo poll end event, when is processed, then ensure to aggregate all poll responses`() = runTest {
// Given
every { realm.instance.createObject(PollResponseAggregatedSummaryEntity::class.java) } returns PollResponseAggregatedSummaryEntity()
val powerLevelsHelper = mockRedactionPowerLevels("another-sender-id", true)
val event = A_POLL_END_EVENT.copy(senderId = "another-sender-id")
every { fakeTaskExecutor.instance.executorScope } returns this
val expectedParams = FetchPollResponseEventsTask.Params(
roomId = A_POLL_END_EVENT.roomId.orEmpty(),
startPollEventId = A_POLL_END_CONTENT.relatesTo?.eventId.orEmpty(),
)
// When
pollAggregationProcessor.handlePollEndEvent(session, powerLevelsHelper, realm.instance, event)
advanceUntilIdle()
// Then
coVerify {
fakeFetchPollResponseEventsTask.execute(expectedParams)
}
}
private fun mockEventAnnotationsSummaryEntity() {
realm.givenWhere<EventAnnotationsSummaryEntity>()
.givenFindFirst(EventAnnotationsSummaryEntity())

View file

@ -0,0 +1,163 @@
/*
* 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.session.room.relation.poll
import io.mockk.every
import io.mockk.mockk
import io.mockk.mockkStatic
import io.mockk.unmockkAll
import io.mockk.verify
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest
import org.junit.After
import org.junit.Before
import org.junit.Test
import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.events.model.RelationType
import org.matrix.android.sdk.api.session.events.model.isPollResponse
import org.matrix.android.sdk.api.session.room.send.SendState
import org.matrix.android.sdk.internal.database.mapper.toEntity
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.query.copyToRealmOrIgnore
import org.matrix.android.sdk.internal.session.room.relation.RelationsResponse
import org.matrix.android.sdk.test.fakes.FakeClock
import org.matrix.android.sdk.test.fakes.FakeEventDecryptor
import org.matrix.android.sdk.test.fakes.FakeGlobalErrorReceiver
import org.matrix.android.sdk.test.fakes.FakeMonarchy
import org.matrix.android.sdk.test.fakes.FakeRoomApi
import org.matrix.android.sdk.test.fakes.givenFindAll
import org.matrix.android.sdk.test.fakes.givenIn
@OptIn(ExperimentalCoroutinesApi::class)
internal class DefaultFetchPollResponseEventsTaskTest {
private val fakeRoomAPI = FakeRoomApi()
private val fakeGlobalErrorReceiver = FakeGlobalErrorReceiver()
private val fakeMonarchy = FakeMonarchy()
private val fakeClock = FakeClock()
private val fakeEventDecryptor = FakeEventDecryptor()
private val defaultFetchPollResponseEventsTask = DefaultFetchPollResponseEventsTask(
roomAPI = fakeRoomAPI.instance,
globalErrorReceiver = fakeGlobalErrorReceiver,
monarchy = fakeMonarchy.instance,
clock = fakeClock,
eventDecryptor = fakeEventDecryptor.instance,
)
@Before
fun setup() {
mockkStatic("org.matrix.android.sdk.api.session.events.model.EventKt")
mockkStatic("org.matrix.android.sdk.internal.database.mapper.EventMapperKt")
mockkStatic("org.matrix.android.sdk.internal.database.query.EventEntityQueriesKt")
}
@After
fun tearDown() {
unmockkAll()
}
@Test
fun `given a room and a poll when execute then fetch related events and store them in local if needed`() = runTest {
// Given
val aRoomId = "roomId"
val aPollEventId = "eventId"
val params = givenTaskParams(roomId = aRoomId, eventId = aPollEventId)
val aNextBatchToken = "nextBatch"
val anEventId1 = "eventId1"
val anEventId2 = "eventId2"
val anEventId3 = "eventId3"
val anEventId4 = "eventId4"
val event1 = givenAnEvent(eventId = anEventId1, isPollResponse = true, isEncrypted = true)
val event2 = givenAnEvent(eventId = anEventId2, isPollResponse = true, isEncrypted = true)
val event3 = givenAnEvent(eventId = anEventId3, isPollResponse = false, isEncrypted = false)
val event4 = givenAnEvent(eventId = anEventId4, isPollResponse = false, isEncrypted = false)
val firstEvents = listOf(event1, event2)
val secondEvents = listOf(event3, event4)
val firstResponse = givenARelationsResponse(events = firstEvents, nextBatch = aNextBatchToken)
fakeRoomAPI.givenGetRelationsReturns(from = null, relationsResponse = firstResponse)
val secondResponse = givenARelationsResponse(events = secondEvents, nextBatch = null)
fakeRoomAPI.givenGetRelationsReturns(from = aNextBatchToken, relationsResponse = secondResponse)
fakeEventDecryptor.givenDecryptEventAndSaveResultSuccess(event1)
fakeEventDecryptor.givenDecryptEventAndSaveResultSuccess(event2)
fakeClock.givenEpoch(123)
givenExistingEventEntities(eventIdsToCheck = listOf(anEventId1, anEventId2), existingIds = listOf(anEventId1))
val eventEntityToSave = EventEntity(eventId = anEventId2)
every { event2.toEntity(any(), any(), any()) } returns eventEntityToSave
every { eventEntityToSave.copyToRealmOrIgnore(any(), any()) } returns eventEntityToSave
// When
defaultFetchPollResponseEventsTask.execute(params)
// Then
fakeRoomAPI.verifyGetRelations(
roomId = params.roomId,
eventId = params.startPollEventId,
relationType = RelationType.REFERENCE,
from = null,
limit = FETCH_RELATED_EVENTS_LIMIT
)
fakeRoomAPI.verifyGetRelations(
roomId = params.roomId,
eventId = params.startPollEventId,
relationType = RelationType.REFERENCE,
from = aNextBatchToken,
limit = FETCH_RELATED_EVENTS_LIMIT
)
fakeEventDecryptor.verifyDecryptEventAndSaveResult(event1, timeline = "")
fakeEventDecryptor.verifyDecryptEventAndSaveResult(event2, timeline = "")
// Check we save in DB the event2 which is a non stored poll response
verify {
event2.toEntity(aRoomId, SendState.SYNCED, any())
eventEntityToSave.copyToRealmOrIgnore(fakeMonarchy.fakeRealm.instance, EventInsertType.PAGINATION)
}
}
private fun givenTaskParams(roomId: String, eventId: String) = FetchPollResponseEventsTask.Params(
roomId = roomId,
startPollEventId = eventId,
)
private fun givenARelationsResponse(events: List<Event>, nextBatch: String?): RelationsResponse {
return RelationsResponse(
chunks = events,
nextBatch = nextBatch,
prevBatch = null,
)
}
private fun givenAnEvent(
eventId: String,
isPollResponse: Boolean,
isEncrypted: Boolean,
): Event {
val event = mockk<Event>(relaxed = true)
every { event.eventId } returns eventId
every { event.isPollResponse() } returns isPollResponse
every { event.isEncrypted() } returns isEncrypted
return event
}
private fun givenExistingEventEntities(eventIdsToCheck: List<String>, existingIds: List<String>) {
val eventEntities = existingIds.map { EventEntity(eventId = it) }
fakeMonarchy.givenWhere<EventEntity>()
.givenIn(EventEntityFields.EVENT_ID, eventIdsToCheck)
.givenFindAll(eventEntities)
}
}

View file

@ -16,14 +16,17 @@
package org.matrix.android.sdk.internal.sync
import io.mockk.mockk
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest
import org.amshove.kluent.shouldBeEqualTo
import org.junit.Test
import org.matrix.android.sdk.api.MatrixConfiguration
import org.matrix.android.sdk.api.SyncConfig
import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilities
import org.matrix.android.sdk.api.session.sync.filter.SyncFilterBuilder
import org.matrix.android.sdk.api.session.sync.filter.SyncFilterParams
import org.matrix.android.sdk.internal.session.filter.DefaultGetCurrentFilterTask
import org.matrix.android.sdk.internal.sync.filter.SyncFilterParams
import org.matrix.android.sdk.internal.sync.filter.SyncFilterBuilder
import org.matrix.android.sdk.test.fakes.FakeFilterRepository
import org.matrix.android.sdk.test.fakes.FakeHomeServerCapabilitiesDataSource
import org.matrix.android.sdk.test.fakes.FakeSaveFilterTask
@ -31,7 +34,6 @@ import org.matrix.android.sdk.test.fakes.FakeSaveFilterTask
private const val A_FILTER_ID = "filter-id"
private val A_HOMESERVER_CAPABILITIES = HomeServerCapabilities()
private val A_SYNC_FILTER_PARAMS = SyncFilterParams(
lazyLoadMembersForMessageEvents = true,
lazyLoadMembersForStateEvents = true,
useThreadNotifications = true
)
@ -46,13 +48,16 @@ class DefaultGetCurrentFilterTaskTest {
private val getCurrentFilterTask = DefaultGetCurrentFilterTask(
filterRepository = filterRepository,
homeServerCapabilitiesDataSource = homeServerCapabilitiesDataSource.instance,
saveFilterTask = saveFilterTask
saveFilterTask = saveFilterTask,
matrixConfiguration = MatrixConfiguration(
applicationFlavor = "TestFlavor",
roomDisplayNameFallbackProvider = mockk(),
syncConfig = SyncConfig(syncFilterParams = SyncFilterParams(lazyLoadMembersForStateEvents = true, useThreadNotifications = true)),
)
)
@Test
fun `given no filter is stored, when execute, then executes task to save new filter`() = runTest {
filterRepository.givenFilterParamsAreStored(A_SYNC_FILTER_PARAMS)
homeServerCapabilitiesDataSource.givenHomeServerCapabilities(A_HOMESERVER_CAPABILITIES)
filterRepository.givenFilterStored(null, null)
@ -68,8 +73,6 @@ class DefaultGetCurrentFilterTaskTest {
@Test
fun `given filter is stored and didn't change, when execute, then returns stored filter id`() = runTest {
filterRepository.givenFilterParamsAreStored(A_SYNC_FILTER_PARAMS)
homeServerCapabilitiesDataSource.givenHomeServerCapabilities(A_HOMESERVER_CAPABILITIES)
val filter = SyncFilterBuilder().with(A_SYNC_FILTER_PARAMS).build(A_HOMESERVER_CAPABILITIES)
@ -82,8 +85,6 @@ class DefaultGetCurrentFilterTaskTest {
@Test
fun `given filter is set and home server capabilities has changed, when execute, then executes task to save new filter`() = runTest {
filterRepository.givenFilterParamsAreStored(A_SYNC_FILTER_PARAMS)
homeServerCapabilitiesDataSource.givenHomeServerCapabilities(A_HOMESERVER_CAPABILITIES)
val filter = SyncFilterBuilder().with(A_SYNC_FILTER_PARAMS).build(A_HOMESERVER_CAPABILITIES)

View file

@ -0,0 +1,35 @@
/*
* 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.coJustRun
import io.mockk.coVerify
import io.mockk.mockk
import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.internal.crypto.EventDecryptor
internal class FakeEventDecryptor {
val instance: EventDecryptor = mockk()
fun givenDecryptEventAndSaveResultSuccess(event: Event) {
coJustRun { instance.decryptEventAndSaveResult(event, any()) }
}
fun verifyDecryptEventAndSaveResult(event: Event, timeline: String) {
coVerify { instance.decryptEventAndSaveResult(event, timeline) }
}
}

View file

@ -1,5 +1,5 @@
/*
* Copyright 2020 The Matrix.org Foundation C.I.C.
* 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.
@ -14,14 +14,9 @@
* limitations under the License.
*/
package org.matrix.android.sdk.api.session.sync
package org.matrix.android.sdk.test.fakes
import org.matrix.android.sdk.api.session.sync.filter.SyncFilterBuilder
import io.mockk.mockk
import org.matrix.android.sdk.internal.session.room.relation.poll.FetchPollResponseEventsTask
interface FilterService {
/**
* Configure the filter for the sync.
*/
suspend fun setSyncFilter(filterBuilder: SyncFilterBuilder)
}
class FakeFetchPollResponseEventsTask : FetchPollResponseEventsTask by mockk(relaxed = true)

View file

@ -19,7 +19,6 @@ package org.matrix.android.sdk.test.fakes
import io.mockk.coEvery
import io.mockk.mockk
import org.matrix.android.sdk.internal.session.filter.FilterRepository
import org.matrix.android.sdk.internal.sync.filter.SyncFilterParams
internal class FakeFilterRepository : FilterRepository by mockk() {
@ -27,8 +26,4 @@ internal class FakeFilterRepository : FilterRepository by mockk() {
coEvery { getStoredSyncFilterId() } returns filterId
coEvery { getStoredSyncFilterBody() } returns filterBody
}
fun givenFilterParamsAreStored(syncFilterParams: SyncFilterParams?) {
coEvery { getStoredFilterParams() } returns syncFilterParams
}
}

View file

@ -109,6 +109,14 @@ inline fun <reified T : RealmModel> RealmQuery<T>.givenLessThan(
return this
}
inline fun <reified T : RealmModel> RealmQuery<T>.givenIn(
fieldName: String,
values: List<String>,
): RealmQuery<T> {
every { `in`(fieldName, values.toTypedArray()) } returns this
return this
}
/**
* Should be called on a mocked RealmObject and not on a real RealmObject so that the underlying final method is mocked.
*/

View file

@ -0,0 +1,61 @@
/*
* 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.coEvery
import io.mockk.coVerify
import io.mockk.mockk
import org.matrix.android.sdk.internal.session.room.RoomAPI
import org.matrix.android.sdk.internal.session.room.relation.RelationsResponse
internal class FakeRoomApi {
val instance: RoomAPI = mockk()
fun givenGetRelationsReturns(
from: String?,
relationsResponse: RelationsResponse,
) {
coEvery {
instance.getRelations(
roomId = any(),
eventId = any(),
relationType = any(),
from = from,
limit = any()
)
} returns relationsResponse
}
fun verifyGetRelations(
roomId: String,
eventId: String,
relationType: String,
from: String?,
limit: Int,
) {
coVerify {
instance.getRelations(
roomId = roomId,
eventId = eventId,
relationType = relationType,
from = from,
limit = limit
)
}
}
}

View file

@ -3013,7 +3013,11 @@
"begging",
"mercy",
"puppy eyes",
"face"
"face",
"cry",
"tears",
"sad",
"grievance"
]
},
"face-holding-back-tears": {
@ -3060,9 +3064,7 @@
"fearful",
"scared",
"terrified",
"nervous",
"oops",
"huh"
"nervous"
]
},
"anxious-face-with-sweat": {

View file

@ -37,7 +37,7 @@ ext.versionMinor = 5
// Note: even values are reserved for regular release, odd values for hotfix release.
// When creating a hotfix, you should decrease the value, since the current value
// is the value for the next regular release.
ext.versionPatch = 14
ext.versionPatch = 16
static def getGitTimestamp() {
def cmd = 'git show -s --format=%ct'

View file

@ -89,7 +89,7 @@ fun getString(@StringRes id: Int): String {
return EspressoHelper.getCurrentActivity()!!.resources.getString(id)
}
fun waitForView(viewMatcher: Matcher<View>, timeout: Long = 10_000, waitForDisplayed: Boolean = true): ViewAction {
fun waitForView(viewMatcher: Matcher<View>, timeout: Long = 20_000, waitForDisplayed: Boolean = true): ViewAction {
return object : ViewAction {
private val clock = DefaultClock()

View file

@ -28,7 +28,6 @@ import im.vector.app.espresso.tools.ScreenshotFailureRule
import im.vector.app.features.MainActivity
import im.vector.app.getString
import im.vector.app.ui.robot.ElementRobot
import im.vector.app.ui.robot.settings.labs.LabFeature
import im.vector.app.ui.robot.settings.labs.LabFeaturesPreferences
import im.vector.app.ui.robot.withDeveloperMode
import org.junit.Rule
@ -133,6 +132,10 @@ class UiAllScreensSanityTest {
}
}
// Some instability with the bottomsheet
// not sure what's the source, maybe the expanded state?
Thread.sleep(10_000)
elementRobot.space { selectSpace(spaceName) }
elementRobot.layoutPreferences {
@ -175,7 +178,6 @@ class UiAllScreensSanityTest {
* Testing multiple threads screens
*/
private fun testThreadScreens() {
elementRobot.toggleLabFeature(LabFeature.THREAD_MESSAGES)
elementRobot.newRoom {
createNewRoom {
crawl()
@ -189,6 +191,5 @@ class UiAllScreensSanityTest {
}
}
}
elementRobot.toggleLabFeature(LabFeature.THREAD_MESSAGES)
}
}

View file

@ -28,7 +28,6 @@ import androidx.test.espresso.matcher.ViewMatchers.withText
import com.adevinta.android.barista.interaction.BaristaClickInteractions.clickOn
import im.vector.app.R
import im.vector.app.espresso.tools.waitUntilActivityVisible
import im.vector.app.espresso.tools.waitUntilDialogVisible
import im.vector.app.espresso.tools.waitUntilViewVisible
import im.vector.app.features.home.HomeActivity
import im.vector.app.features.home.room.detail.RoomDetailActivity
@ -86,14 +85,17 @@ class SpaceCreateRobot {
clickOn(R.id.nextButton)
waitUntilViewVisible(withId(R.id.recyclerView))
clickOn(R.id.nextButton)
// waitUntilActivityVisible<RoomDetailActivity> {
// waitUntilDialogVisible(withId(R.id.inviteByMxidButton))
// }
// // close invite dialog
// pressBack()
waitUntilActivityVisible<RoomDetailActivity> {
waitUntilDialogVisible(withId(R.id.inviteByMxidButton))
pressBack()
}
// close invite dialog
pressBack()
waitUntilViewVisible(withId(R.id.timelineRecyclerView))
// waitUntilViewVisible(withId(R.id.timelineRecyclerView))
// close room
pressBack()
// pressBack()
waitUntilViewVisible(withId(R.id.roomListContainer))
}
}

View file

@ -89,9 +89,8 @@ class SpaceMenuRobot {
clickOnSheet(R.id.leaveSpace)
waitUntilActivityVisible<SpaceLeaveAdvancedActivity> {
waitUntilViewVisible(ViewMatchers.withId(R.id.roomList))
clickOn(R.id.spaceLeaveSelectAll)
clickOn(R.id.spaceLeaveButton)
}
clickOn(R.id.spaceLeaveSelectAll)
clickOn(R.id.spaceLeaveButton)
waitUntilViewVisible(ViewMatchers.withId(R.id.groupListView))
}
}

View file

@ -70,11 +70,13 @@ import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.SupervisorJob
import org.matrix.android.sdk.api.Matrix
import org.matrix.android.sdk.api.MatrixConfiguration
import org.matrix.android.sdk.api.SyncConfig
import org.matrix.android.sdk.api.auth.AuthenticationService
import org.matrix.android.sdk.api.auth.HomeServerHistoryService
import org.matrix.android.sdk.api.legacy.LegacySessionImporter
import org.matrix.android.sdk.api.raw.RawService
import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.sync.filter.SyncFilterParams
import org.matrix.android.sdk.api.settings.LightweightSettingsStorage
import javax.inject.Singleton
@ -157,6 +159,9 @@ import javax.inject.Singleton
),
metricPlugins = vectorPlugins.plugins(),
customEventTypesProvider = vectorCustomEventTypesProvider,
syncConfig = SyncConfig(
syncFilterParams = SyncFilterParams(lazyLoadMembersForStateEvents = true, useThreadNotifications = true)
)
)
}

View file

@ -46,6 +46,7 @@ import im.vector.app.features.home.UserColorAccountDataViewModel
import im.vector.app.features.home.room.breadcrumbs.BreadcrumbsViewModel
import im.vector.app.features.home.room.detail.TimelineViewModel
import im.vector.app.features.home.room.detail.composer.MessageComposerViewModel
import im.vector.app.features.home.room.detail.composer.link.SetLinkViewModel
import im.vector.app.features.home.room.detail.search.SearchViewModel
import im.vector.app.features.home.room.detail.timeline.action.MessageActionsViewModel
import im.vector.app.features.home.room.detail.timeline.edithistory.ViewEditHistoryViewModel
@ -691,4 +692,9 @@ interface MavericksViewModelModule {
fun vectorSettingsNotificationPreferenceViewModelFactory(
factory: VectorSettingsNotificationPreferenceViewModel.Factory
): MavericksAssistedViewModelFactory<*, *>
@Binds
@IntoMap
@MavericksViewModelKey(SetLinkViewModel::class)
fun setLinkViewModelFactory(factory: SetLinkViewModel.Factory): MavericksAssistedViewModelFactory<*, *>
}

View file

@ -0,0 +1,155 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.core.platform
import android.content.Context
import android.os.Bundle
import android.os.Parcelable
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.annotation.CallSuper
import androidx.fragment.app.DialogFragment
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
import androidx.viewbinding.ViewBinding
import com.airbnb.mvrx.MavericksView
import dagger.hilt.android.EntryPointAccessors
import im.vector.app.R
import im.vector.app.core.di.ActivityEntryPoint
import im.vector.app.core.extensions.singletonEntryPoint
import im.vector.app.core.extensions.toMvRxBundle
import im.vector.app.features.analytics.AnalyticsTracker
import im.vector.app.features.analytics.plan.MobileScreen
import im.vector.app.features.themes.ThemeUtils
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import reactivecircus.flowbinding.android.view.clicks
import timber.log.Timber
/**
* Add Mavericks capabilities, handle DI and bindings.
*/
abstract class VectorBaseDialogFragment<VB : ViewBinding> : DialogFragment(), MavericksView {
/* ==========================================================================================
* Analytics
* ========================================================================================== */
protected var analyticsScreenName: MobileScreen.ScreenName? = null
protected lateinit var analyticsTracker: AnalyticsTracker
/* ==========================================================================================
* View
* ========================================================================================== */
private var _binding: VB? = null
// This property is only valid between onCreateView and onDestroyView.
protected val views: VB
get() = _binding!!
abstract fun getBinding(inflater: LayoutInflater, container: ViewGroup?): VB
/* ==========================================================================================
* View model
* ========================================================================================== */
private lateinit var viewModelFactory: ViewModelProvider.Factory
protected val activityViewModelProvider
get() = ViewModelProvider(requireActivity(), viewModelFactory)
protected val fragmentViewModelProvider
get() = ViewModelProvider(this, viewModelFactory)
val vectorBaseActivity: VectorBaseActivity<*> by lazy {
activity as VectorBaseActivity<*>
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setStyle(STYLE_NORMAL, ThemeUtils.getApplicationThemeRes(requireContext()))
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
_binding = getBinding(inflater, container)
return views.root
}
@CallSuper
override fun onDestroyView() {
_binding = null
super.onDestroyView()
}
@CallSuper
override fun onDestroy() {
super.onDestroy()
}
override fun onAttach(context: Context) {
val activityEntryPoint = EntryPointAccessors.fromActivity(vectorBaseActivity, ActivityEntryPoint::class.java)
viewModelFactory = activityEntryPoint.viewModelFactory()
val singletonEntryPoint = context.singletonEntryPoint()
analyticsTracker = singletonEntryPoint.analyticsTracker()
super.onAttach(context)
}
override fun onResume() {
super.onResume()
Timber.i("onResume BottomSheet ${javaClass.simpleName}")
analyticsScreenName?.let {
analyticsTracker.screen(MobileScreen(screenName = it))
}
}
override fun onStart() {
super.onStart()
// This ensures that invalidate() is called for static screens that don't
// subscribe to a ViewModel.
postInvalidate()
requireDialog().window?.setWindowAnimations(R.style.Animation_AppCompat_Dialog)
}
protected fun setArguments(args: Parcelable? = null) {
arguments = args.toMvRxBundle()
}
/* ==========================================================================================
* Views
* ========================================================================================== */
protected fun View.debouncedClicks(onClicked: () -> Unit) {
clicks()
.onEach { onClicked() }
.launchIn(viewLifecycleOwner.lifecycleScope)
}
/* ==========================================================================================
* ViewEvents
* ========================================================================================== */
protected fun <T : VectorViewEvents> VectorViewModel<*, *, T>.observeViewEvents(observer: (T) -> Unit) {
viewEvents
.stream()
.onEach {
observer(it)
}
.launchIn(viewLifecycleOwner.lifecycleScope)
}
}

View file

@ -25,7 +25,6 @@ import im.vector.app.features.call.webrtc.WebRtcCallManager
import im.vector.app.features.session.coroutineScope
import im.vector.app.features.settings.VectorPreferences
import im.vector.app.features.settings.devices.v2.notification.UpdateNotificationSettingsAccountDataUseCase
import im.vector.app.features.sync.SyncUtils
import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.session.Session
import timber.log.Timber
@ -43,9 +42,6 @@ class ConfigureAndStartSessionUseCase @Inject constructor(
fun execute(session: Session, startSyncing: Boolean = true) {
Timber.i("Configure and start session for ${session.myUserId}. startSyncing: $startSyncing")
session.open()
session.coroutineScope.launch {
session.filterService().setSyncFilter(SyncUtils.getSyncFilterBuilder())
}
if (startSyncing) {
session.startSyncing(context)
}

View file

@ -127,6 +127,7 @@ sealed class RoomDetailAction : VectorViewModelAction {
object Pause : Recording()
object Resume : Recording()
object Stop : Recording()
object StopConfirmed : Recording()
}
sealed class Listening : VoiceBroadcastAction() {

View file

@ -71,6 +71,8 @@ sealed class RoomDetailViewEvents : VectorViewEvents {
object DisplayEnableIntegrationsWarning : RoomDetailViewEvents()
object DisplayPromptToStopVoiceBroadcast : RoomDetailViewEvents()
data class OpenStickerPicker(val widget: Widget) : RoomDetailViewEvents()
object OpenIntegrationManager : RoomDetailViewEvents()

View file

@ -413,6 +413,7 @@ class TimelineFragment :
is RoomDetailViewEvents.DisplayAndAcceptCall -> acceptIncomingCall(it)
RoomDetailViewEvents.RoomReplacementStarted -> handleRoomReplacement()
RoomDetailViewEvents.OpenElementCallWidget -> handleOpenElementCallWidget()
RoomDetailViewEvents.DisplayPromptToStopVoiceBroadcast -> displayPromptToStopVoiceBroadcast()
}
}
@ -2006,6 +2007,20 @@ class TimelineFragment :
}
}
private fun displayPromptToStopVoiceBroadcast() {
ConfirmationDialogBuilder
.show(
activity = requireActivity(),
askForReason = false,
confirmationRes = R.string.stop_voice_broadcast_content,
positiveRes = R.string.action_stop,
reasonHintRes = 0,
titleRes = R.string.stop_voice_broadcast_dialog_title
) {
timelineViewModel.handle(RoomDetailAction.VoiceBroadcastAction.Recording.StopConfirmed)
}
}
override fun onTapToReturnToCall() {
callManager.getCurrentCall()?.let { call ->
VectorCallActivity.newIntent(

View file

@ -633,7 +633,8 @@ class TimelineViewModel @AssistedInject constructor(
}
VoiceBroadcastAction.Recording.Pause -> voiceBroadcastHelper.pauseVoiceBroadcast(room.roomId)
VoiceBroadcastAction.Recording.Resume -> voiceBroadcastHelper.resumeVoiceBroadcast(room.roomId)
VoiceBroadcastAction.Recording.Stop -> voiceBroadcastHelper.stopVoiceBroadcast(room.roomId)
VoiceBroadcastAction.Recording.Stop -> _viewEvents.post(RoomDetailViewEvents.DisplayPromptToStopVoiceBroadcast)
VoiceBroadcastAction.Recording.StopConfirmed -> voiceBroadcastHelper.stopVoiceBroadcast(room.roomId)
is VoiceBroadcastAction.Listening.PlayOrResume -> voiceBroadcastHelper.playOrResumePlayback(action.voiceBroadcast)
VoiceBroadcastAction.Listening.Pause -> voiceBroadcastHelper.pausePlayback()
VoiceBroadcastAction.Listening.Stop -> voiceBroadcastHelper.stopPlayback()

View file

@ -80,6 +80,9 @@ import im.vector.app.features.home.room.detail.AutoCompleter
import im.vector.app.features.home.room.detail.RoomDetailAction
import im.vector.app.features.home.room.detail.RoomDetailAction.VoiceBroadcastAction
import im.vector.app.features.home.room.detail.TimelineViewModel
import im.vector.app.features.home.room.detail.composer.link.SetLinkFragment
import im.vector.app.features.home.room.detail.composer.link.SetLinkSharedAction
import im.vector.app.features.home.room.detail.composer.link.SetLinkSharedActionViewModel
import im.vector.app.features.home.room.detail.composer.voice.VoiceMessageRecorderView
import im.vector.app.features.home.room.detail.timeline.action.MessageSharedActionViewModel
import im.vector.app.features.home.room.detail.upgrade.MigrateRoomBottomSheet
@ -147,6 +150,7 @@ class MessageComposerFragment : VectorBaseFragment<FragmentComposerBinding>(), A
private lateinit var sharedActionViewModel: MessageSharedActionViewModel
private val attachmentViewModel: AttachmentTypeSelectorViewModel by fragmentViewModel()
private val attachmentActionsViewModel: AttachmentTypeSelectorSharedActionViewModel by viewModels()
private val setLinkActionsViewModel: SetLinkSharedActionViewModel by viewModels()
private val composer: MessageComposerView get() {
return if (vectorPreferences.isRichTextEditorEnabled()) {
@ -212,6 +216,14 @@ class MessageComposerFragment : VectorBaseFragment<FragmentComposerBinding>(), A
.onEach { onTypeSelected(it.attachmentType) }
.launchIn(lifecycleScope)
setLinkActionsViewModel.stream()
.onEach { when (it) {
is SetLinkSharedAction.Insert -> views.richTextComposerLayout.insertLink(it.link, it.text)
is SetLinkSharedAction.Set -> views.richTextComposerLayout.setLink(it.link)
SetLinkSharedAction.Remove -> views.richTextComposerLayout.removeLink()
} }
.launchIn(lifecycleScope)
messageComposerViewModel.stateFlow.map { it.isFullScreen }
.distinctUntilChanged()
.onEach { isFullScreen ->
@ -385,6 +397,10 @@ class MessageComposerFragment : VectorBaseFragment<FragmentComposerBinding>(), A
override fun onFullScreenModeChanged() = withState(messageComposerViewModel) { state ->
messageComposerViewModel.handle(MessageComposerAction.SetFullScreen(!state.isFullScreen))
}
override fun onSetLink(isTextSupported: Boolean, initialLink: String?) {
SetLinkFragment.show(isTextSupported, initialLink, childFragmentManager)
}
}
}

View file

@ -45,4 +45,5 @@ interface Callback : ComposerEditText.Callback {
fun onAddAttachment()
fun onExpandOrCompactChange()
fun onFullScreenModeChanged()
fun onSetLink(isTextSupported: Boolean, initialLink: String?)
}

View file

@ -49,6 +49,7 @@ import im.vector.app.databinding.ComposerRichTextLayoutBinding
import im.vector.app.databinding.ViewRichTextMenuButtonBinding
import io.element.android.wysiwyg.EditorEditText
import io.element.android.wysiwyg.inputhandlers.models.InlineFormat
import io.element.android.wysiwyg.inputhandlers.models.LinkAction
import io.element.android.wysiwyg.utils.RustErrorCollector
import uniffi.wysiwyg_composer.ActionState
import uniffi.wysiwyg_composer.ComposerAction
@ -231,8 +232,25 @@ internal class RichTextComposerLayout @JvmOverloads constructor(
addRichTextMenuItem(R.drawable.ic_composer_strikethrough, R.string.rich_text_editor_format_strikethrough, ComposerAction.STRIKE_THROUGH) {
views.richTextComposerEditText.toggleInlineFormat(InlineFormat.StrikeThrough)
}
addRichTextMenuItem(R.drawable.ic_composer_link, R.string.rich_text_editor_link, ComposerAction.LINK) {
views.richTextComposerEditText.getLinkAction()?.let {
when (it) {
LinkAction.InsertLink -> callback?.onSetLink(isTextSupported = true, initialLink = null)
is LinkAction.SetLink -> callback?.onSetLink(isTextSupported = false, initialLink = it.currentLink)
}
}
}
}
fun setLink(link: String?) =
views.richTextComposerEditText.setLink(link)
fun insertLink(link: String, text: String) =
views.richTextComposerEditText.insertLink(link, text)
fun removeLink() =
views.richTextComposerEditText.removeLink()
@SuppressLint("ClickableViewAccessibility")
private fun disallowParentInterceptTouchEvent(view: View) {
view.setOnTouchListener { v, event ->

View file

@ -14,23 +14,17 @@
* limitations under the License.
*/
package im.vector.app.test.fakes
package im.vector.app.features.home.room.detail.composer.link
import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.just
import io.mockk.mockk
import io.mockk.runs
import org.matrix.android.sdk.api.session.sync.FilterService
import org.matrix.android.sdk.api.session.sync.filter.SyncFilterBuilder
import im.vector.app.core.platform.VectorViewModelAction
class FakeFilterService : FilterService by mockk() {
sealed class SetLinkAction : VectorViewModelAction {
data class LinkChanged(
val newLink: String
) : SetLinkAction()
fun givenSetFilterSucceeds() {
coEvery { setSyncFilter(any()) } just runs
}
fun verifySetSyncFilter(filterBuilder: SyncFilterBuilder) {
coVerify { setSyncFilter(filterBuilder) }
}
data class Save(
val link: String,
val text: String,
) : SetLinkAction()
}

View file

@ -0,0 +1,131 @@
/*
* Copyright (c) 2021 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.features.home.room.detail.composer.link
import android.os.Bundle
import android.os.Parcelable
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.view.isGone
import androidx.fragment.app.FragmentManager
import androidx.fragment.app.viewModels
import androidx.lifecycle.lifecycleScope
import com.airbnb.mvrx.args
import com.airbnb.mvrx.fragmentViewModel
import com.airbnb.mvrx.withState
import dagger.hilt.android.AndroidEntryPoint
import im.vector.app.R
import im.vector.app.core.platform.VectorBaseDialogFragment
import im.vector.app.databinding.FragmentSetLinkBinding
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.parcelize.Parcelize
import reactivecircus.flowbinding.android.widget.textChanges
@AndroidEntryPoint
class SetLinkFragment :
VectorBaseDialogFragment<FragmentSetLinkBinding>() {
@Parcelize
data class Args(
val isTextSupported: Boolean,
val initialLink: String?,
) : Parcelable
private val viewModel: SetLinkViewModel by fragmentViewModel()
private val sharedActionViewModel: SetLinkSharedActionViewModel by viewModels(
ownerProducer = { requireParentFragment() }
)
private val args: Args by args()
override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentSetLinkBinding {
return FragmentSetLinkBinding.inflate(inflater, container, false)
}
companion object {
fun show(isTextSupported: Boolean, initialLink: String?, fragmentManager: FragmentManager) =
SetLinkFragment().apply {
setArguments(Args(isTextSupported, initialLink))
}.show(fragmentManager, "SetLinkBottomSheet")
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
views.link.setText(args.initialLink)
views.link.textChanges()
.onEach {
viewModel.handle(SetLinkAction.LinkChanged(it.toString()))
}
.launchIn(viewLifecycleOwner.lifecycleScope)
views.save.debouncedClicks {
viewModel.handle(
SetLinkAction.Save(
link = views.link.text.toString(),
text = views.text.text.toString(),
)
)
}
views.cancel.debouncedClicks(::onCancel)
views.remove.debouncedClicks(::onRemove)
viewModel.observeViewEvents {
when (it) {
is SetLinkViewEvents.SavedLinkAndText -> handleInsert(link = it.link, text = it.text)
is SetLinkViewEvents.SavedLink -> handleSet(link = it.link)
}
}
views.toolbar.setNavigationOnClickListener {
dismiss()
}
}
override fun invalidate() = withState(viewModel) { viewState ->
views.toolbar.title = getString(
if (viewState.initialLink != null) {
R.string.set_link_edit
} else {
R.string.set_link_create
}
)
views.remove.isGone = !viewState.removeVisible
views.save.isEnabled = viewState.saveEnabled
views.textLayout.isGone = !viewState.isTextSupported
}
private fun handleInsert(link: String, text: String) {
sharedActionViewModel.post(SetLinkSharedAction.Insert(text, link))
dismiss()
}
private fun handleSet(link: String) {
sharedActionViewModel.post(SetLinkSharedAction.Set(link))
dismiss()
}
private fun onRemove() {
sharedActionViewModel.post(SetLinkSharedAction.Remove)
dismiss()
}
private fun onCancel() = dismiss()
}

View file

@ -0,0 +1,37 @@
/*
* 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.features.home.room.detail.composer.link
import im.vector.app.core.platform.VectorSharedAction
import im.vector.app.core.platform.VectorSharedActionViewModel
import javax.inject.Inject
class SetLinkSharedActionViewModel @Inject constructor() :
VectorSharedActionViewModel<SetLinkSharedAction>()
sealed interface SetLinkSharedAction : VectorSharedAction {
data class Set(
val link: String,
) : SetLinkSharedAction
data class Insert(
val text: String,
val link: String,
) : SetLinkSharedAction
object Remove : SetLinkSharedAction
}

View file

@ -0,0 +1,31 @@
/*
* Copyright (c) 2021 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.features.home.room.detail.composer.link
import im.vector.app.core.platform.VectorViewEvents
sealed class SetLinkViewEvents : VectorViewEvents {
data class SavedLink(
val link: String,
) : SetLinkViewEvents()
data class SavedLinkAndText(
val link: String,
val text: String,
) : SetLinkViewEvents()
}

View file

@ -0,0 +1,55 @@
/*
* Copyright (c) 2021 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.features.home.room.detail.composer.link
import com.airbnb.mvrx.MavericksViewModelFactory
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import im.vector.app.core.di.MavericksAssistedViewModelFactory
import im.vector.app.core.di.hiltMavericksViewModelFactory
import im.vector.app.core.platform.VectorViewModel
class SetLinkViewModel @AssistedInject constructor(
@Assisted private val initialState: SetLinkViewState,
) : VectorViewModel<SetLinkViewState, SetLinkAction, SetLinkViewEvents>(initialState) {
@AssistedFactory
interface Factory : MavericksAssistedViewModelFactory<SetLinkViewModel, SetLinkViewState> {
override fun create(initialState: SetLinkViewState): SetLinkViewModel
}
companion object : MavericksViewModelFactory<SetLinkViewModel, SetLinkViewState> by hiltMavericksViewModelFactory()
override fun handle(action: SetLinkAction) = when (action) {
is SetLinkAction.LinkChanged -> handleLinkChanged(action.newLink)
is SetLinkAction.Save -> handleSave(action.link, action.text)
}
private fun handleLinkChanged(newLink: String) = setState {
copy(saveEnabled = newLink != initialLink.orEmpty())
}
private fun handleSave(
link: String,
text: String
) = if (initialState.isTextSupported) {
_viewEvents.post(SetLinkViewEvents.SavedLinkAndText(link, text))
} else {
_viewEvents.post(SetLinkViewEvents.SavedLink(link))
}
}

View file

@ -0,0 +1,34 @@
/*
* Copyright (c) 2021 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.features.home.room.detail.composer.link
import com.airbnb.mvrx.MavericksState
data class SetLinkViewState(
val isTextSupported: Boolean,
val initialLink: String?,
val saveEnabled: Boolean,
) : MavericksState {
constructor(args: SetLinkFragment.Args) : this(
isTextSupported = args.isTextSupported,
initialLink = args.initialLink,
saveEnabled = false,
)
val removeVisible = initialLink != null
}

View file

@ -93,7 +93,7 @@ abstract class MessageVoiceBroadcastListeningItem : AbsMessageVoiceBroadcastItem
override fun renderLiveIndicator(holder: Holder) {
when {
voiceBroadcastState == null || voiceBroadcastState == VoiceBroadcastState.STOPPED -> renderNoLiveIndicator(holder)
voiceBroadcastState == VoiceBroadcastState.PAUSED || !player.isLiveListening -> renderPausedLiveIndicator(holder)
voiceBroadcastState == VoiceBroadcastState.PAUSED -> renderPausedLiveIndicator(holder)
else -> renderPlayingLiveIndicator(holder)
}
}
@ -122,10 +122,14 @@ abstract class MessageVoiceBroadcastListeningItem : AbsMessageVoiceBroadcastItem
private fun bindSeekBar(holder: Holder) {
with(holder) {
durationView.text = formatPlaybackTime(duration)
remainingTimeView.text = formatRemainingTime(duration)
elapsedTimeView.text = formatPlaybackTime(0)
seekBar.max = duration
seekBar.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener {
override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) = Unit
override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) {
remainingTimeView.text = formatRemainingTime(duration - progress)
elapsedTimeView.text = formatPlaybackTime(progress)
}
override fun onStartTrackingTouch(seekBar: SeekBar) {
isUserSeeking = true
@ -156,6 +160,7 @@ abstract class MessageVoiceBroadcastListeningItem : AbsMessageVoiceBroadcastItem
}
private fun formatPlaybackTime(time: Int) = DateUtils.formatElapsedTime((time / 1000).toLong())
private fun formatRemainingTime(time: Int) = if (time < 1000) formatPlaybackTime(time) else String.format("-%s", formatPlaybackTime(time))
override fun unbind(holder: Holder) {
super.unbind(holder)
@ -177,7 +182,8 @@ abstract class MessageVoiceBroadcastListeningItem : AbsMessageVoiceBroadcastItem
val fastBackwardButton by bind<ImageButton>(R.id.fastBackwardButton)
val fastForwardButton by bind<ImageButton>(R.id.fastForwardButton)
val seekBar by bind<SeekBar>(R.id.seekBar)
val durationView by bind<TextView>(R.id.playbackDuration)
val remainingTimeView by bind<TextView>(R.id.remainingTime)
val elapsedTimeView by bind<TextView>(R.id.elapsedTime)
val broadcasterNameMetadata by bind<VoiceBroadcastMetadataView>(R.id.broadcasterNameMetadata)
val voiceBroadcastMetadata by bind<VoiceBroadcastMetadataView>(R.id.voiceBroadcastMetadata)
val listenersCountMetadata by bind<VoiceBroadcastMetadataView>(R.id.listenersCountMetadata)

View file

@ -118,6 +118,7 @@ class NotificationBroadcastReceiver : BroadcastReceiver() {
private fun handleSmartReply(intent: Intent, context: Context) {
val message = getReplyMessage(intent)
val roomId = intent.getStringExtra(KEY_ROOM_ID)
val threadId = intent.getStringExtra(KEY_THREAD_ID)
if (message.isNullOrBlank() || roomId.isNullOrBlank()) {
// ignore this event
@ -126,13 +127,20 @@ class NotificationBroadcastReceiver : BroadcastReceiver() {
}
activeSessionHolder.getActiveSession().let { session ->
session.getRoom(roomId)?.let { room ->
sendMatrixEvent(message, session, room, context)
sendMatrixEvent(message, threadId, session, room, context)
}
}
}
private fun sendMatrixEvent(message: String, session: Session, room: Room, context: Context?) {
room.sendService().sendTextMessage(message)
private fun sendMatrixEvent(message: String, threadId: String?, session: Session, room: Room, context: Context?) {
if (threadId != null) {
room.relationService().replyInThread(
rootThreadEventId = threadId,
replyInThreadText = message,
)
} else {
room.sendService().sendTextMessage(message)
}
// Create a new event to be displayed in the notification drawer, right now
@ -148,7 +156,7 @@ class NotificationBroadcastReceiver : BroadcastReceiver() {
body = message,
imageUriString = null,
roomId = room.roomId,
threadId = null, // needs to be changed: https://github.com/vector-im/element-android/issues/7475
threadId = threadId,
roomName = room.roomSummary()?.displayName ?: room.roomId,
roomIsDirect = room.roomSummary()?.isDirect == true,
outGoingMessage = true,
@ -223,6 +231,7 @@ class NotificationBroadcastReceiver : BroadcastReceiver() {
companion object {
const val KEY_ROOM_ID = "roomID"
const val KEY_THREAD_ID = "threadID"
const val KEY_TEXT_REPLY = "key_text_reply"
}
}

View file

@ -657,7 +657,7 @@ class NotificationUtils @Inject constructor(
// Quick reply
if (!roomInfo.hasSmartReplyError) {
buildQuickReplyIntent(roomInfo.roomId, senderDisplayNameForReplyCompat)?.let { replyPendingIntent ->
buildQuickReplyIntent(roomInfo.roomId, threadId, senderDisplayNameForReplyCompat)?.let { replyPendingIntent ->
val remoteInput = RemoteInput.Builder(NotificationBroadcastReceiver.KEY_TEXT_REPLY)
.setLabel(stringProvider.getString(R.string.action_quick_reply))
.build()
@ -892,13 +892,17 @@ class NotificationUtils @Inject constructor(
However, for Android devices running Marshmallow and below (API level 23 and below),
it will be more appropriate to use an activity. Since you have to provide your own UI.
*/
private fun buildQuickReplyIntent(roomId: String, senderName: String?): PendingIntent? {
private fun buildQuickReplyIntent(roomId: String, threadId: String?, senderName: String?): PendingIntent? {
val intent: Intent
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
intent = Intent(context, NotificationBroadcastReceiver::class.java)
intent.action = actionIds.smartReply
intent.data = createIgnoredUri(roomId)
intent.putExtra(NotificationBroadcastReceiver.KEY_ROOM_ID, roomId)
threadId?.let {
intent.putExtra(NotificationBroadcastReceiver.KEY_THREAD_ID, it)
}
return PendingIntent.getBroadcast(
context,
clock.epochMillis().toInt(),

View file

@ -223,7 +223,6 @@ class VectorSettingsDevicesFragment :
override fun onViewAllClicked() {
viewNavigator.navigateToOtherSessions(
requireActivity(),
R.string.device_manager_header_section_security_recommendations_title,
DeviceManagerFilterType.UNVERIFIED,
excludeCurrentDevice = true
)
@ -233,7 +232,6 @@ class VectorSettingsDevicesFragment :
override fun onViewAllClicked() {
viewNavigator.navigateToOtherSessions(
requireActivity(),
R.string.device_manager_header_section_security_recommendations_title,
DeviceManagerFilterType.INACTIVE,
excludeCurrentDevice = true
)
@ -447,7 +445,6 @@ class VectorSettingsDevicesFragment :
override fun onViewAllOtherSessionsClicked() {
viewNavigator.navigateToOtherSessions(
context = requireActivity(),
titleResourceId = R.string.device_manager_sessions_other_title,
defaultFilter = DeviceManagerFilterType.ALL_SESSIONS,
excludeCurrentDevice = true
)

View file

@ -31,12 +31,11 @@ class VectorSettingsDevicesViewNavigator @Inject constructor() {
fun navigateToOtherSessions(
context: Context,
titleResourceId: Int,
defaultFilter: DeviceManagerFilterType,
excludeCurrentDevice: Boolean,
) {
context.startActivity(
OtherSessionsActivity.newIntent(context, titleResourceId, defaultFilter, excludeCurrentDevice)
OtherSessionsActivity.newIntent(context, defaultFilter, excludeCurrentDevice)
)
}

View file

@ -27,7 +27,7 @@ import im.vector.app.core.epoxy.VectorEpoxyHolder
import im.vector.app.core.epoxy.VectorEpoxyModel
import im.vector.app.core.utils.DimensionConverter
private const val EXTRA_TOP_MARGIN_DP = 48
private const val EXTRA_TOP_MARGIN_DP = 32
@EpoxyModelClass
abstract class SessionDetailsHeaderItem : VectorEpoxyModel<SessionDetailsHeaderItem.Holder>(R.layout.item_session_details_header) {

View file

@ -53,6 +53,9 @@ class SecurityRecommendationView @JvmOverloads constructor(
setImage(it)
}
setOnClickListener {
callback?.onViewAllClicked()
}
views.recommendationViewAllButton.setOnClickListener {
callback?.onViewAllClicked()
}

View file

@ -75,7 +75,7 @@ class SessionInfoView @JvmOverloads constructor(
renderDeviceLastSeenDetails(
sessionInfoViewState.deviceFullInfo.isInactive,
sessionInfoViewState.deviceFullInfo.deviceInfo,
sessionInfoViewState.isLastSeenDetailsVisible,
sessionInfoViewState.isLastActivityVisible,
sessionInfoViewState.isShowingIpAddress,
dateFormatter,
drawableProvider,
@ -197,7 +197,7 @@ class SessionInfoView @JvmOverloads constructor(
} else {
views.sessionInfoLastActivityTextView.isGone = true
}
views.sessionInfoLastIPAddressTextView.setTextOrHide(deviceInfo.lastSeenIp?.takeIf { isLastSeenDetailsVisible && isShowingIpAddress })
views.sessionInfoLastIPAddressTextView.setTextOrHide(deviceInfo.lastSeenIp?.takeIf { isShowingIpAddress })
}
private fun renderDetailsButton(isDetailsButtonVisible: Boolean) {

View file

@ -24,6 +24,6 @@ data class SessionInfoViewState(
val isVerifyButtonVisible: Boolean = true,
val isDetailsButtonVisible: Boolean = true,
val isLearnMoreLinkVisible: Boolean = false,
val isLastSeenDetailsVisible: Boolean = false,
val isLastActivityVisible: Boolean = false,
val isShowingIpAddress: Boolean = false,
)

View file

@ -16,6 +16,7 @@
package im.vector.app.features.settings.devices.v2.more
import android.content.DialogInterface
import android.os.Bundle
import android.os.Parcelable
import android.view.LayoutInflater
@ -42,6 +43,8 @@ class SessionLearnMoreBottomSheet : VectorBaseBottomSheetDialogFragment<BottomSh
override val showExpanded = true
var onDismiss: (() -> Unit)? = null
override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): BottomSheetSessionLearnMoreBinding {
return BottomSheetSessionLearnMoreBinding.inflate(inflater, container, false)
}
@ -57,6 +60,11 @@ class SessionLearnMoreBottomSheet : VectorBaseBottomSheetDialogFragment<BottomSh
}
}
override fun onDismiss(dialog: DialogInterface) {
super.onDismiss(dialog)
onDismiss?.invoke()
}
override fun invalidate() = withState(viewModel) { viewState ->
super.invalidate()
views.bottomSheetSessionLearnMoreTitle.text = viewState.title
@ -65,11 +73,12 @@ class SessionLearnMoreBottomSheet : VectorBaseBottomSheetDialogFragment<BottomSh
companion object {
fun show(fragmentManager: FragmentManager, args: Args) {
fun show(fragmentManager: FragmentManager, args: Args): SessionLearnMoreBottomSheet {
val bottomSheet = SessionLearnMoreBottomSheet()
bottomSheet.isCancelable = true
bottomSheet.setArguments(args)
bottomSheet.show(fragmentManager, "SessionLearnMoreBottomSheet")
return bottomSheet
}
}
}

View file

@ -20,7 +20,6 @@ import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.view.View
import androidx.annotation.StringRes
import com.airbnb.mvrx.Mavericks
import dagger.hilt.android.AndroidEntryPoint
import im.vector.app.core.extensions.addFragment
@ -48,13 +47,11 @@ class OtherSessionsActivity : SimpleFragmentActivity() {
companion object {
fun newIntent(
context: Context,
@StringRes
titleResourceId: Int,
defaultFilter: DeviceManagerFilterType,
excludeCurrentDevice: Boolean,
): Intent {
return Intent(context, OtherSessionsActivity::class.java).apply {
putExtra(Mavericks.KEY_ARG, OtherSessionsArgs(titleResourceId, defaultFilter, excludeCurrentDevice))
putExtra(Mavericks.KEY_ARG, OtherSessionsArgs(defaultFilter, excludeCurrentDevice))
}
}
}

View file

@ -17,14 +17,11 @@
package im.vector.app.features.settings.devices.v2.othersessions
import android.os.Parcelable
import androidx.annotation.StringRes
import im.vector.app.features.settings.devices.v2.filter.DeviceManagerFilterType
import kotlinx.parcelize.Parcelize
@Parcelize
data class OtherSessionsArgs(
@StringRes
val titleResourceId: Int,
val defaultFilter: DeviceManagerFilterType,
val excludeCurrentDevice: Boolean,
) : Parcelable

View file

@ -182,7 +182,9 @@ class OtherSessionsFragment :
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setupToolbar(views.otherSessionsToolbar).setTitle(args.titleResourceId).allowBack()
setupToolbar(views.otherSessionsToolbar)
.setTitle(R.string.device_manager_sessions_other_title)
.allowBack()
observeViewEvents()
initFilterView()
}
@ -225,6 +227,7 @@ class OtherSessionsFragment :
override fun invalidate() = withState(viewModel) { state ->
updateLoading(state.isLoading)
updateFilterView(state.isSelectModeEnabled)
if (state.devices is Success) {
val devices = state.devices.invoke()
renderDevices(devices, state.currentFilter, state.isShowingIpAddress)
@ -240,13 +243,17 @@ class OtherSessionsFragment :
}
}
private fun updateFilterView(isSelectModeEnabled: Boolean) {
views.otherSessionsFilterFrameLayout.isVisible = isSelectModeEnabled.not()
}
private fun updateToolbar(devices: List<DeviceFullInfo>, isSelectModeEnabled: Boolean) {
invalidateOptionsMenu()
val title = if (isSelectModeEnabled) {
val selection = devices.count { it.isSelected }
stringProvider.getQuantityString(R.plurals.x_selected, selection, selection)
} else {
getString(args.titleResourceId)
getString(R.string.device_manager_sessions_other_title)
}
toolbar?.title = title
}
@ -341,6 +348,8 @@ class OtherSessionsFragment :
override fun onOtherSessionLongClicked(deviceId: String) = withState(viewModel) { state ->
if (!state.isSelectModeEnabled) {
enableSelectMode(true, deviceId)
} else {
viewModel.handle(OtherSessionsAction.ToggleSelectionForDevice(deviceId))
}
}

View file

@ -224,7 +224,7 @@ class SessionOverviewFragment :
isVerifyButtonVisible = isCurrentSession || viewState.isCurrentSessionTrusted,
isDetailsButtonVisible = false,
isLearnMoreLinkVisible = deviceInfo.roomEncryptionTrustLevel != RoomEncryptionTrustLevel.Default,
isLastSeenDetailsVisible = !isCurrentSession,
isLastActivityVisible = !isCurrentSession,
isShowingIpAddress = viewState.isShowingIpAddress,
)
views.sessionOverviewInfo.render(infoViewState, dateFormatter, drawableProvider, colorProvider, stringProvider)

View file

@ -20,6 +20,7 @@ import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.ViewTreeObserver
import androidx.core.widget.doOnTextChanged
import com.airbnb.mvrx.fragmentViewModel
import com.airbnb.mvrx.withState
@ -62,12 +63,24 @@ class RenameSessionFragment :
}
private fun initEditText() {
views.renameSessionEditText.showKeyboard(andRequestFocus = true)
showKeyboard()
views.renameSessionEditText.doOnTextChanged { text, _, _, _ ->
viewModel.handle(RenameSessionAction.EditLocally(text.toString()))
}
}
private fun showKeyboard() {
val focusChangeListener = object : ViewTreeObserver.OnWindowFocusChangeListener {
override fun onWindowFocusChanged(hasFocus: Boolean) {
if (hasFocus) {
views.renameSessionEditText.showKeyboard(andRequestFocus = true)
}
views.renameSessionEditText.viewTreeObserver.removeOnWindowFocusChangeListener(this)
}
}
views.renameSessionEditText.viewTreeObserver.addOnWindowFocusChangeListener(focusChangeListener)
}
private fun initSaveButton() {
views.renameSessionSave.debouncedClicks {
viewModel.handle(RenameSessionAction.SaveModifications)
@ -89,7 +102,9 @@ class RenameSessionFragment :
title = getString(R.string.device_manager_learn_more_session_rename_title),
description = getString(R.string.device_manager_learn_more_session_rename),
)
SessionLearnMoreBottomSheet.show(childFragmentManager, args)
SessionLearnMoreBottomSheet
.show(childFragmentManager, args)
.onDismiss = { showKeyboard() }
}
private fun observeViewEvents() {

View file

@ -1,48 +0,0 @@
/*
* 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.features.sync
import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.api.session.sync.filter.SyncFilterBuilder
object SyncUtils {
// Get only managed types by Element
private val listOfSupportedTimelineEventTypes = listOf(
// TODO Complete the list
EventType.MESSAGE
)
// Get only managed types by Element
private val listOfSupportedStateEventTypes = listOf(
// TODO Complete the list
EventType.STATE_ROOM_MEMBER
)
fun getSyncFilterBuilder(): SyncFilterBuilder {
return SyncFilterBuilder()
.useThreadNotifications(true)
.lazyLoadMembersForStateEvents(true)
/**
* Currently we don't set [lazy_load_members = true] for Filter.room.timeline even though we set it for RoomFilter which is used later to
* fetch messages in a room. It's not clear if it's done so by mistake or intentionally, so changing it could case side effects and need
* careful testing
* */
// .lazyLoadMembersForMessageEvents(true)
// .listOfSupportedStateEventTypes(listOfSupportedStateEventTypes)
// .listOfSupportedTimelineEventTypes(listOfSupportedTimelineEventTypes)
}
}

View file

@ -24,6 +24,7 @@ import android.graphics.drawable.Drawable
import android.util.TypedValue
import androidx.annotation.AttrRes
import androidx.annotation.ColorInt
import androidx.annotation.StyleRes
import androidx.core.content.ContextCompat
import androidx.core.content.edit
import androidx.core.graphics.drawable.DrawableCompat
@ -113,19 +114,16 @@ object ThemeUtils {
*/
fun setApplicationTheme(context: Context, aTheme: String) {
currentTheme.set(aTheme)
context.setTheme(
when (aTheme) {
SYSTEM_THEME_VALUE -> if (isSystemDarkTheme(context.resources)) R.style.Theme_Vector_Dark else R.style.Theme_Vector_Light
THEME_DARK_VALUE -> R.style.Theme_Vector_Dark
THEME_BLACK_VALUE -> R.style.Theme_Vector_Black
else -> R.style.Theme_Vector_Light
}
)
context.setTheme(themeToRes(context, aTheme))
// Clear the cache
mColorByAttr.clear()
}
@StyleRes
fun getApplicationThemeRes(context: Context) =
themeToRes(context, currentTheme.get())
/**
* Set the activity theme according to the selected one. Default is Light, so if this is the current
* theme, the theme is not changed.
@ -200,4 +198,13 @@ object ThemeUtils {
DrawableCompat.setTint(tinted, color)
return tinted
}
@StyleRes
private fun themeToRes(context: Context, theme: String): Int =
when (theme) {
SYSTEM_THEME_VALUE -> if (isSystemDarkTheme(context.resources)) R.style.Theme_Vector_Dark else R.style.Theme_Vector_Light
THEME_DARK_VALUE -> R.style.Theme_Vector_Dark
THEME_BLACK_VALUE -> R.style.Theme_Vector_Black
else -> R.style.Theme_Vector_Light
}
}

View file

@ -130,7 +130,7 @@ class VoiceBroadcastPlayerImpl @Inject constructor(
listeners[voiceBroadcast.voiceBroadcastId] = CopyOnWriteArrayList<Listener>().apply { add(listener) }
}
listener.onPlayingStateChanged(if (voiceBroadcast == currentVoiceBroadcast) playingState else State.IDLE)
listener.onLiveModeChanged(voiceBroadcast == currentVoiceBroadcast && isLiveListening)
listener.onLiveModeChanged(voiceBroadcast == currentVoiceBroadcast)
}
override fun removeListener(voiceBroadcast: VoiceBroadcast, listener: Listener) {
@ -373,11 +373,6 @@ class VoiceBroadcastPlayerImpl @Inject constructor(
}
private fun onLiveListeningChanged(isLiveListening: Boolean) {
currentVoiceBroadcast?.voiceBroadcastId?.let { voiceBroadcastId ->
// Notify live mode change to all the listeners attached to the current voice broadcast id
listeners[voiceBroadcastId]?.forEach { listener -> listener.onLiveModeChanged(isLiveListening) }
}
// Live has ended and last chunk has been reached, we can stop the playback
if (!isLiveListening && playingState == State.BUFFERING && playlist.currentSequence == mostRecentVoiceBroadcastEvent?.content?.lastChunkSequence) {
stop()

View file

@ -0,0 +1,12 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="44dp"
android:height="44dp"
android:viewportWidth="44"
android:viewportHeight="44">
<path
android:pathData="M22.566,16.151L23.101,15.616C24.577,14.14 26.956,14.126 28.415,15.585C29.874,17.044 29.86,19.423 28.383,20.899L25.844,23.438C24.368,24.915 21.989,24.929 20.53,23.47M21.434,27.849L20.899,28.383C19.423,29.86 17.044,29.874 15.585,28.415C14.126,26.956 14.14,24.577 15.616,23.101L18.156,20.562C19.632,19.086 22.011,19.071 23.47,20.53"
android:strokeWidth="1.5"
android:fillColor="#00000000"
android:strokeColor="#8D97A5"
android:strokeLineCap="round"/>
</vector>

View file

@ -2,9 +2,7 @@
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingHorizontal="24dp"
android:paddingBottom="32dp">
android:orientation="vertical">
<View
android:layout_width="36dp"
@ -18,75 +16,102 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:paddingHorizontal="24dp"
android:text="@string/device_manager_filter_bottom_sheet_title" />
<RadioGroup
android:id="@+id/filterOptionsRadioGroup"
<ScrollView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:layoutDirection="rtl"
android:showDividers="none">
android:layout_height="match_parent"
android:clipToPadding="false"
android:paddingHorizontal="24dp"
android:paddingBottom="32dp"
android:scrollbarStyle="outsideOverlay">
<RadioButton
android:id="@+id/filterOptionAllSessionsRadioButton"
style="@style/TextAppearance.Vector.Subtitle.Medium.DevicesManagement"
<RadioGroup
android:id="@+id/filterOptionsRadioGroup"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:checked="true"
android:minHeight="0dp"
android:text="@string/device_manager_filter_option_all_sessions" />
android:paddingTop="24dp"
android:showDividers="none">
<RadioButton
android:id="@+id/filterOptionVerifiedRadioButton"
style="@style/TextAppearance.Vector.Subtitle.Medium.DevicesManagement"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:minHeight="0dp"
android:text="@string/device_manager_filter_option_verified" />
<RadioButton
android:id="@+id/filterOptionAllSessionsRadioButton"
style="@style/TextAppearance.Vector.Subtitle.Medium.DevicesManagement"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="start"
android:background="?android:selectableItemBackground"
android:button="@null"
android:checked="true"
android:drawableEnd="?android:attr/listChoiceIndicatorSingle"
android:minHeight="0dp"
android:text="@string/device_manager_filter_option_all_sessions"
android:textAlignment="textStart" />
<TextView
android:id="@+id/filterOptionVerifiedTextView"
style="@style/TextAppearance.Vector.Body.DevicesManagement"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:text="@string/device_manager_filter_option_verified_description" />
<RadioButton
android:id="@+id/filterOptionVerifiedRadioButton"
style="@style/TextAppearance.Vector.Subtitle.Medium.DevicesManagement"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="start"
android:layout_marginTop="24dp"
android:background="?android:selectableItemBackground"
android:button="@null"
android:drawableEnd="?android:attr/listChoiceIndicatorSingle"
android:minHeight="0dp"
android:text="@string/device_manager_filter_option_verified"
android:textAlignment="textStart" />
<RadioButton
android:id="@+id/filterOptionUnverifiedRadioButton"
style="@style/TextAppearance.Vector.Subtitle.Medium.DevicesManagement"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:minHeight="0dp"
android:text="@string/device_manager_filter_option_unverified" />
<TextView
android:id="@+id/filterOptionVerifiedTextView"
style="@style/TextAppearance.Vector.Body.DevicesManagement"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/device_manager_filter_option_verified_description" />
<TextView
android:id="@+id/filterOptionUnverifiedTextView"
style="@style/TextAppearance.Vector.Body.DevicesManagement"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:text="@string/device_manager_filter_option_unverified_description" />
<RadioButton
android:id="@+id/filterOptionUnverifiedRadioButton"
style="@style/TextAppearance.Vector.Subtitle.Medium.DevicesManagement"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="start"
android:layout_marginTop="16dp"
android:background="?android:selectableItemBackground"
android:button="@null"
android:drawableEnd="?android:attr/listChoiceIndicatorSingle"
android:minHeight="0dp"
android:text="@string/device_manager_filter_option_unverified"
android:textAlignment="textStart" />
<RadioButton
android:id="@+id/filterOptionInactiveRadioButton"
style="@style/TextAppearance.Vector.Subtitle.Medium.DevicesManagement"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:minHeight="0dp"
android:text="@string/device_manager_filter_option_inactive" />
<TextView
android:id="@+id/filterOptionUnverifiedTextView"
style="@style/TextAppearance.Vector.Body.DevicesManagement"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/device_manager_filter_option_unverified_description" />
<TextView
android:id="@+id/filterOptionInactiveTextView"
style="@style/TextAppearance.Vector.Body.DevicesManagement"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end" />
<RadioButton
android:id="@+id/filterOptionInactiveRadioButton"
style="@style/TextAppearance.Vector.Subtitle.Medium.DevicesManagement"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="start"
android:layout_marginTop="16dp"
android:background="?android:selectableItemBackground"
android:button="@null"
android:drawableEnd="?android:attr/listChoiceIndicatorSingle"
android:minHeight="0dp"
android:text="@string/device_manager_filter_option_inactive"
android:textAlignment="textStart" />
</RadioGroup>
<TextView
android:id="@+id/filterOptionInactiveTextView"
style="@style/TextAppearance.Vector.Body.DevicesManagement"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</RadioGroup>
</ScrollView>
</LinearLayout>

View file

@ -1,120 +1,132 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior">
<LinearLayout
android:id="@+id/otherSessionsNotFoundLayout"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="72dp"
android:layout_marginTop="32dp"
android:layout_marginEnd="16dp"
android:orientation="vertical"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<TextView
android:id="@+id/otherSessionsNotFoundTextView"
style="@style/TextAppearance.Vector.Body"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
tools:text="@string/device_manager_other_sessions_no_verified_sessions_found" />
<Button
android:id="@+id/otherSessionsClearFilterButton"
style="@style/Widget.Vector.Button.Text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:gravity="start"
android:padding="0dp"
android:text="@string/device_manager_other_sessions_clear_filter" />
</LinearLayout>
<im.vector.app.features.settings.devices.v2.list.OtherSessionsView
android:id="@+id/deviceListOtherSessions"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/appBarLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
android:layout_height="wrap_content">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/otherSessionsToolbar"
<com.google.android.material.appbar.CollapsingToolbarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:navigationIcon="@drawable/ic_back_24dp"
app:title="@string/device_manager_sessions_other_title">
android:layout_height="match_parent"
app:layout_scrollFlags="scroll|exitUntilCollapsed"
app:titleEnabled="false"
app:toolbarId="@id/otherSessionsToolbar">
<FrameLayout
android:id="@+id/otherSessionsFilterFrameLayout"
android:layout_width="wrap_content"
<im.vector.app.features.settings.devices.v2.list.SessionsListHeaderView
android:id="@+id/deviceListHeaderOtherSessions"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:layout_marginEnd="8dp"
android:padding="8dp">
android:layout_marginTop="?attr/actionBarSize"
android:layout_marginBottom="16dp"
app:layout_collapseMode="parallax"
app:sessionsListHeaderDescription="@string/device_manager_sessions_other_description"
app:sessionsListHeaderHasLearnMoreLink="false"
app:sessionsListHeaderTitle="" />
<ImageView
<im.vector.app.features.settings.devices.v2.othersessions.OtherSessionsSecurityRecommendationView
android:id="@+id/otherSessionsSecurityRecommendationView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="?attr/actionBarSize"
android:layout_marginBottom="16dp"
android:paddingTop="20dp"
android:visibility="gone"
app:layout_collapseMode="parallax"
app:otherSessionsRecommendationDescription="@string/device_manager_other_sessions_recommendation_description_unverified"
app:otherSessionsRecommendationImageBackgroundTint="@color/shield_color_warning_background"
app:otherSessionsRecommendationImageResource="@drawable/ic_shield_warning_no_border"
app:otherSessionsRecommendationTitle="@string/device_manager_other_sessions_recommendation_title_unverified"
tools:visibility="visible" />
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/otherSessionsToolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:layout_collapseMode="pin"
app:navigationIcon="@drawable/ic_back_24dp"
app:title="@string/device_manager_sessions_other_title">
<FrameLayout
android:id="@+id/otherSessionsFilterFrameLayout"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription="@string/a11y_device_manager_filter"
android:src="@drawable/ic_filter" />
android:layout_gravity="end"
android:layout_marginEnd="8dp"
android:padding="8dp">
<ImageView
android:id="@+id/otherSessionsFilterBadgeImageView"
android:layout_width="12dp"
android:layout_height="12dp"
android:layout_marginStart="12dp"
android:importantForAccessibility="no"
android:src="@drawable/circle_with_transparent_border" />
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription="@string/a11y_device_manager_filter"
android:src="@drawable/ic_filter" />
</FrameLayout>
<ImageView
android:id="@+id/otherSessionsFilterBadgeImageView"
android:layout_width="12dp"
android:layout_height="12dp"
android:layout_marginStart="12dp"
android:importantForAccessibility="no"
android:src="@drawable/circle_with_transparent_border" />
</com.google.android.material.appbar.MaterialToolbar>
</FrameLayout>
</com.google.android.material.appbar.MaterialToolbar>
</com.google.android.material.appbar.CollapsingToolbarLayout>
</com.google.android.material.appbar.AppBarLayout>
<im.vector.app.features.settings.devices.v2.list.SessionsListHeaderView
android:id="@+id/deviceListHeaderOtherSessions"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/appBarLayout"
app:sessionsListHeaderDescription="@string/device_manager_sessions_other_description"
app:sessionsListHeaderHasLearnMoreLink="false"
app:sessionsListHeaderTitle="" />
<im.vector.app.features.settings.devices.v2.othersessions.OtherSessionsSecurityRecommendationView
android:id="@+id/otherSessionsSecurityRecommendationView"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="20dp"
android:visibility="gone"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/deviceListHeaderOtherSessions"
app:otherSessionsRecommendationDescription="@string/device_manager_other_sessions_recommendation_description_unverified"
app:otherSessionsRecommendationImageBackgroundTint="@color/shield_color_warning_background"
app:otherSessionsRecommendationImageResource="@drawable/ic_shield_warning_no_border"
app:otherSessionsRecommendationTitle="@string/device_manager_other_sessions_recommendation_title_unverified"
tools:visibility="visible" />
<LinearLayout
android:id="@+id/otherSessionsNotFoundLayout"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="72dp"
android:layout_marginTop="32dp"
android:layout_marginEnd="16dp"
android:orientation="vertical"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/otherSessionsSecurityRecommendationView">
<TextView
android:id="@+id/otherSessionsNotFoundTextView"
style="@style/TextAppearance.Vector.Body"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
tools:text="@string/device_manager_other_sessions_no_verified_sessions_found" />
<Button
android:id="@+id/otherSessionsClearFilterButton"
style="@style/Widget.Vector.Button.Text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:gravity="start"
android:padding="0dp"
android:text="@string/device_manager_other_sessions_clear_filter" />
</LinearLayout>
<im.vector.app.features.settings.devices.v2.list.OtherSessionsView
android:id="@+id/deviceListOtherSessions"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginTop="32dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/otherSessionsSecurityRecommendationView" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View file

@ -47,6 +47,7 @@
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginHorizontal="8dp"
android:layout_marginTop="4dp"
android:text="@string/device_manager_session_overview_signout"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0"

View file

@ -0,0 +1,117 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/appBarLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?actionBarSize"
app:navigationIcon="@drawable/ic_x_18dp"
app:title="@string/set_link_create" />
</com.google.android.material.appbar.AppBarLayout>
<ScrollView
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintBottom_toTopOf="@id/save"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHeight_min="100dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/appBarLayout">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/textLayout"
style="@style/Widget.Vector.TextInputLayout.Form"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
android:hint="@string/set_link_text">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/text"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/linkLayout"
style="@style/Widget.Vector.TextInputLayout.Form"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
android:hint="@string/set_link_link">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/link"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="textUri" />
</com.google.android.material.textfield.TextInputLayout>
</LinearLayout>
</ScrollView>
<Button
android:id="@+id/save"
style="@style/Widget.Vector.Button.CallToAction"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="16dp"
android:height="56dp"
android:text="@string/action_save"
android:textAllCaps="false"
app:iconGravity="textStart"
app:layout_constraintBottom_toTopOf="@id/remove"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
<Button
android:id="@+id/remove"
style="@style/Widget.Vector.Button.Destructive"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="16dp"
android:height="56dp"
android:text="@string/action_remove"
android:textAllCaps="false"
android:visibility="gone"
app:iconGravity="textStart"
app:layout_constraintBottom_toTopOf="@id/cancel"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
tools:visibility="visible" />
<Button
android:id="@+id/cancel"
style="@style/Widget.Vector.Button.Outlined"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="16dp"
android:layout_marginBottom="8dp"
android:height="56dp"
android:text="@string/action_cancel"
android:textAllCaps="false"
app:iconGravity="textStart"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -75,7 +75,7 @@
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginHorizontal="16dp"
android:layout_marginVertical="16dp"
android:layout_marginVertical="4dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/deviceListHeaderCurrentSession" />

View file

@ -5,9 +5,15 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:foreground="?selectableItemBackground"
android:paddingHorizontal="8dp"
android:paddingTop="8dp">
<androidx.constraintlayout.widget.Guideline
android:id="@+id/startGuideline"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintGuide_begin="8dp" />
<View
android:id="@+id/otherSessionItemBackground"
android:layout_width="0dp"
@ -29,7 +35,7 @@
android:contentDescription="@string/a11y_device_manager_device_type_mobile"
android:padding="8dp"
app:layout_constraintBottom_toBottomOf="@id/otherSessionItemBackground"
app:layout_constraintStart_toStartOf="@id/otherSessionItemBackground"
app:layout_constraintStart_toStartOf="@id/startGuideline"
app:layout_constraintTop_toTopOf="@id/otherSessionItemBackground"
tools:src="@drawable/ic_device_type_mobile" />
@ -52,8 +58,8 @@
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginEnd="8dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="8dp"
android:ellipsize="end"
android:lines="1"
app:layout_constraintEnd_toEndOf="parent"
@ -89,7 +95,7 @@
android:id="@+id/otherSessionSeparator"
android:layout_width="0dp"
android:layout_height="1dp"
android:layout_marginTop="8dp"
android:layout_marginTop="16dp"
android:background="?vctr_content_quinary"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="@id/otherSessionNameTextView"

View file

@ -9,7 +9,7 @@
android:id="@+id/sessionDetailsContentTitle"
style="@style/TextAppearance.Vector.Body.DevicesManagement"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_height="0dp"
android:layout_marginStart="@dimen/layout_horizontal_margin"
app:layout_constraintBottom_toTopOf="@id/sessionDetailsContentDivider"
app:layout_constraintEnd_toStartOf="@id/sessionDetailsContentDescription"
@ -22,14 +22,14 @@
style="@style/TextAppearance.Vector.Body"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginStart="12dp"
android:layout_marginEnd="@dimen/layout_horizontal_margin"
android:gravity="end"
app:layout_constraintBottom_toTopOf="@id/sessionDetailsContentDivider"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/sessionDetailsContentTitle"
app:layout_constraintTop_toTopOf="@id/sessionDetailsContentTop"
tools:text="Element Web: Firefox" />
tools:text="app.element.io: Firefox on macOS" />
<View
android:id="@+id/sessionDetailsContentDivider"

View file

@ -140,27 +140,40 @@
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:layout_marginEnd="6dp"
android:paddingStart="0dp"
android:paddingEnd="0dp"
android:progressDrawable="@drawable/bg_seek_bar"
android:thumbTint="?vctr_content_secondary"
android:thumbOffset="3dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/playbackDuration"
android:thumbTint="?vctr_content_secondary"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/controllerButtonsFlow"
tools:progress="0" />
tools:progress="50" />
<TextView
android:id="@+id/playbackDuration"
android:id="@+id/elapsedTime"
style="@style/Widget.Vector.TextView.Caption"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
android:layout_marginTop="-3dp"
android:textColor="?vctr_content_tertiary"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/seekBar"
tools:ignore="NegativeMargin"
tools:text="0:11" />
<TextView
android:id="@+id/remainingTime"
style="@style/Widget.Vector.TextView.Caption"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="-3dp"
android:layout_marginEnd="4dp"
android:textColor="?vctr_content_tertiary"
app:layout_constraintBottom_toBottomOf="@id/seekBar"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@id/seekBar"
tools:text="0:23" />
app:layout_constraintTop_toBottomOf="@id/seekBar"
tools:ignore="NegativeMargin"
tools:text="-0:12" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -3,7 +3,8 @@
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
android:layout_height="match_parent"
android:paddingBottom="8dp">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/otherSessionsRecyclerView"
@ -19,8 +20,9 @@
style="@style/Widget.Vector.Button.Text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="72dp"
android:layout_marginTop="4dp"
android:padding="0dp"
android:layout_marginStart="16dp"
app:layout_constraintStart_toStartOf="@id/otherSessionsRecyclerView"
app:layout_constraintTop_toBottomOf="@id/otherSessionsRecyclerView"
tools:text="@string/device_manager_other_sessions_view_all" />

View file

@ -5,6 +5,7 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/bg_current_session"
android:foreground="?attr/selectableItemBackground"
android:paddingHorizontal="16dp"
android:paddingTop="16dp"
android:paddingBottom="8dp">

View file

@ -6,7 +6,7 @@
android:layout_height="wrap_content"
android:background="@drawable/bg_current_session"
android:paddingHorizontal="24dp"
android:paddingBottom="16dp">
android:paddingBottom="8dp">
<ImageView
android:id="@+id/sessionInfoDeviceTypeImageView"

View file

@ -24,7 +24,7 @@
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginHorizontal="@dimen/layout_horizontal_margin"
android:layout_marginTop="18.5dp"
android:layout_marginTop="@dimen/layout_vertical_margin"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/sessions_list_header_title"

File diff suppressed because one or more lines are too long

View file

@ -20,7 +20,6 @@ import im.vector.app.core.extensions.startSyncing
import im.vector.app.core.session.clientinfo.UpdateMatrixClientInfoUseCase
import im.vector.app.features.session.coroutineScope
import im.vector.app.features.settings.devices.v2.notification.UpdateNotificationSettingsAccountDataUseCase
import im.vector.app.features.sync.SyncUtils
import im.vector.app.test.fakes.FakeContext
import im.vector.app.test.fakes.FakeNotificationsSettingUpdater
import im.vector.app.test.fakes.FakeSession
@ -87,7 +86,6 @@ class ConfigureAndStartSessionUseCaseTest {
// Then
verify { aSession.startSyncing(fakeContext.instance) }
aSession.fakeFilterService.verifySetSyncFilter(SyncUtils.getSyncFilterBuilder())
aSession.fakePushersService.verifyRefreshPushers()
fakeWebRtcCallManager.verifyCheckForProtocolsSupportIfNeeded()
coVerify {
@ -112,7 +110,6 @@ class ConfigureAndStartSessionUseCaseTest {
// Then
verify { aSession.startSyncing(fakeContext.instance) }
aSession.fakeFilterService.verifySetSyncFilter(SyncUtils.getSyncFilterBuilder())
aSession.fakePushersService.verifyRefreshPushers()
fakeWebRtcCallManager.verifyCheckForProtocolsSupportIfNeeded()
coVerify(inverse = true) {
@ -140,7 +137,6 @@ class ConfigureAndStartSessionUseCaseTest {
// Then
verify(inverse = true) { aSession.startSyncing(fakeContext.instance) }
aSession.fakeFilterService.verifySetSyncFilter(SyncUtils.getSyncFilterBuilder())
aSession.fakePushersService.verifyRefreshPushers()
fakeWebRtcCallManager.verifyCheckForProtocolsSupportIfNeeded()
coVerify {
@ -152,7 +148,6 @@ class ConfigureAndStartSessionUseCaseTest {
private fun givenASession(): FakeSession {
val fakeSession = FakeSession()
every { fakeSession.open() } just runs
fakeSession.fakeFilterService.givenSetFilterSucceeds()
every { fakeSession.startSyncing(any()) } just runs
fakeSession.fakePushersService.givenRefreshPushersSucceeds()
return fakeSession

View file

@ -63,6 +63,11 @@ class DeleteMatrixClientInfoUseCaseTest {
// Given
val error = Exception()
givenSetMatrixClientInfoFails(error)
val expectedClientInfoToBeSet = MatrixClientInfoContent(
name = "",
version = "",
url = "",
)
// When
val result = deleteMatrixClientInfoUseCase.execute()
@ -70,6 +75,12 @@ class DeleteMatrixClientInfoUseCaseTest {
// Then
result.isFailure shouldBe true
result.exceptionOrNull() shouldBeEqualTo error
coVerify {
fakeSetMatrixClientInfoUseCase.execute(
fakeActiveSessionHolder.fakeSession,
expectedClientInfoToBeSet
)
}
}
private fun givenSetMatrixClientInfoSucceeds() {

View file

@ -0,0 +1,157 @@
/*
* 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.features.home.room.detail.composer.link
import com.airbnb.mvrx.test.MavericksTestRule
import im.vector.app.test.test
import im.vector.app.test.testDispatcher
import org.junit.Rule
import org.junit.Test
class SetLinkViewModelTest {
@get:Rule
val mavericksTestRule = MavericksTestRule(testDispatcher = testDispatcher)
companion object {
const val link = "https://matrix.org"
const val newLink = "https://matrix.org/new"
const val text = "Matrix"
}
private val fragmentArgs = SetLinkFragment.Args(
isTextSupported = true,
initialLink = link
)
private fun createViewModel(
args: SetLinkFragment.Args
) = SetLinkViewModel(
initialState = SetLinkViewState(args),
)
@Test
fun `given no initial link, then remove button is hidden`() {
val viewModel = createViewModel(
fragmentArgs
.copy(initialLink = null)
)
val viewModelTest = viewModel.test()
viewModelTest
.assertLatestState { !it.removeVisible }
.finish()
}
@Test
fun `given no initial link, when link changed, then remove button is still hidden`() {
val viewModel = createViewModel(
fragmentArgs.copy(initialLink = null)
)
val viewModelTest = viewModel.test()
viewModel.handle(SetLinkAction.LinkChanged(newLink))
viewModelTest
.assertLatestState { !it.removeVisible }
.finish()
}
@Test
fun `when link is unchanged, it disables the save button`() {
val viewModel = createViewModel(
fragmentArgs
.copy(initialLink = link)
)
val viewModelTest = viewModel.test()
viewModelTest
.assertLatestState { !it.saveEnabled }
.finish()
}
@Test
fun `when link is changed, it enables the save button`() {
val viewModel = createViewModel(
fragmentArgs.copy(initialLink = link)
)
val viewModelTest = viewModel.test()
viewModel.handle(SetLinkAction.LinkChanged(newLink))
viewModelTest
.assertLatestState { it.saveEnabled }
.finish()
}
@Test
fun `given no initial link, when link is changed to empty, it disables the save button`() {
val viewModel = createViewModel(
fragmentArgs.copy(initialLink = null)
)
val viewModelTest = viewModel.test()
viewModel.handle(SetLinkAction.LinkChanged(""))
viewModelTest
.assertLatestState {
!it.saveEnabled
}
.finish()
}
@Test
fun `given text is supported, when saved, it emits the right event`() {
val viewModel = createViewModel(
fragmentArgs.copy(isTextSupported = true)
)
val viewModelTest = viewModel.test()
viewModel.handle(
SetLinkAction.Save(link = newLink, text = text)
)
viewModelTest
.assertEvent {
it == SetLinkViewEvents.SavedLinkAndText(
link = newLink,
text = text,
)
}
.finish()
}
@Test
fun `given text is not supported, when saved, it emits the right event`() {
val viewModel = createViewModel(
fragmentArgs.copy(isTextSupported = false)
)
val viewModelTest = viewModel.test()
viewModel.handle(
SetLinkAction.Save(link = newLink, text = text)
)
viewModelTest
.assertEvent {
it == SetLinkViewEvents.SavedLink(link = newLink)
}
.finish()
}
}

View file

@ -53,6 +53,8 @@ import org.matrix.android.sdk.api.session.crypto.crosssigning.DeviceTrustLevel
import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo
import org.matrix.android.sdk.api.session.crypto.model.DeviceInfo
import org.matrix.android.sdk.api.session.crypto.model.RoomEncryptionTrustLevel
import org.matrix.android.sdk.api.session.crypto.verification.VerificationTransaction
import org.matrix.android.sdk.api.session.crypto.verification.VerificationTxState
import org.matrix.android.sdk.api.session.uia.DefaultBaseAuth
private const val A_CURRENT_DEVICE_ID = "current-device-id"
@ -76,6 +78,10 @@ class DevicesViewModelTest {
private val fakeVectorPreferences = FakeVectorPreferences()
private val toggleIpAddressVisibilityUseCase = mockk<ToggleIpAddressVisibilityUseCase>()
private val verifiedTransaction = mockk<VerificationTransaction>().apply {
every { state } returns VerificationTxState.Verified
}
private fun createViewModel(): DevicesViewModel {
return DevicesViewModel(
initialState = DevicesViewState(),
@ -375,6 +381,18 @@ class DevicesViewModelTest {
viewModelTest.finish()
}
@Test
fun `given the view model when a verified transaction is updated then device list is refreshed`() {
// Given
val viewModel = createViewModel()
// When
viewModel.transactionUpdated(verifiedTransaction)
// Then
verify { viewModel.refreshDeviceList() }
}
private fun givenCurrentSessionCrossSigningInfo(): CurrentSessionCrossSigningInfo {
val currentSessionCrossSigningInfo = mockk<CurrentSessionCrossSigningInfo>()
every { currentSessionCrossSigningInfo.deviceId } returns A_CURRENT_DEVICE_ID

View file

@ -0,0 +1,53 @@
/*
* 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.features.settings.devices.v2
import im.vector.app.test.fakes.FakeVectorPreferences
import org.junit.Test
class ToggleIpAddressVisibilityUseCaseTest {
private val fakeVectorPreferences = FakeVectorPreferences()
private val toggleIpAddressVisibilityUseCase = ToggleIpAddressVisibilityUseCase(
vectorPreferences = fakeVectorPreferences.instance,
)
@Test
fun `given ip addresses are currently visible then then visibility is set as false`() {
// Given
fakeVectorPreferences.givenShowIpAddressInSessionManagerScreens(true)
// When
toggleIpAddressVisibilityUseCase.execute()
// Then
fakeVectorPreferences.verifySetIpAddressVisibilityInDeviceManagerScreens(false)
}
@Test
fun `given ip addresses are currently not visible then then visibility is set as true`() {
// Given
fakeVectorPreferences.givenShowIpAddressInSessionManagerScreens(false)
// When
toggleIpAddressVisibilityUseCase.execute()
// Then
fakeVectorPreferences.verifySetIpAddressVisibilityInDeviceManagerScreens(true)
}
}

View file

@ -31,7 +31,6 @@ import org.junit.Before
import org.junit.Test
private const val A_SESSION_ID = "session_id"
private const val A_TITLE_RESOURCE_ID = 1234
private val A_DEFAULT_FILTER = DeviceManagerFilterType.INACTIVE
class VectorSettingsDevicesViewNavigatorTest {
@ -67,11 +66,11 @@ class VectorSettingsDevicesViewNavigatorTest {
@Test
fun `given an intent when navigating to other sessions list then it starts the correct activity`() {
// Given
val intent = givenIntentForOtherSessions(A_TITLE_RESOURCE_ID, A_DEFAULT_FILTER, true)
val intent = givenIntentForOtherSessions(A_DEFAULT_FILTER, true)
context.givenStartActivity(intent)
// When
vectorSettingsDevicesViewNavigator.navigateToOtherSessions(context.instance, A_TITLE_RESOURCE_ID, A_DEFAULT_FILTER, true)
vectorSettingsDevicesViewNavigator.navigateToOtherSessions(context.instance, A_DEFAULT_FILTER, true)
// Then
context.verifyStartActivity(intent)
@ -96,9 +95,9 @@ class VectorSettingsDevicesViewNavigatorTest {
return intent
}
private fun givenIntentForOtherSessions(titleResourceId: Int, defaultFilter: DeviceManagerFilterType, excludeCurrentDevice: Boolean): Intent {
private fun givenIntentForOtherSessions(defaultFilter: DeviceManagerFilterType, excludeCurrentDevice: Boolean): Intent {
val intent = mockk<Intent>()
every { OtherSessionsActivity.newIntent(context.instance, titleResourceId, defaultFilter, excludeCurrentDevice) } returns intent
every { OtherSessionsActivity.newIntent(context.instance, defaultFilter, excludeCurrentDevice) } returns intent
return intent
}

Some files were not shown because too many files have changed in this diff Show more