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) 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 // 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 // the whole commit which set version 0.16.0-SNAPSHOT
def vanniktechEmoji = "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" def fragment = "1.5.5"
// Testing // 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 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", 'appdistributionApi' : "com.google.firebase:firebase-appdistribution-api-ktx:$appDistribution",
'appdistribution' : "com.google.firebase:firebase-appdistribution:$appDistribution", 'appdistribution' : "com.google.firebase:firebase-appdistribution:$appDistribution",
// Phone number https://github.com/google/libphonenumber // 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 : [
'dagger' : "com.google.dagger:dagger:$dagger", 'dagger' : "com.google.dagger:dagger:$dagger",
@ -98,7 +98,7 @@ ext.libs = [
], ],
element : [ element : [
'opusencoder' : "io.element.android:opusencoder:1.1.0", '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 : [ squareup : [
'moshi' : "com.squareup.moshi:moshi:$moshi", 'moshi' : "com.squareup.moshi:moshi:$moshi",
@ -129,7 +129,7 @@ ext.libs = [
'mavericksTesting' : "com.airbnb.android:mavericks-testing:$mavericks" 'mavericksTesting' : "com.airbnb.android:mavericks-testing:$mavericks"
], ],
maplibre : [ 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" 'pluginAnnotation' : "org.maplibre.gl:android-plugin-annotation-v9:1.0.0"
], ],
mockk : [ 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_got_it">Got it</string>
<string name="action_select_all">Select all</string> <string name="action_select_all">Select all</string>
<string name="action_deselect_all">Deselect 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> <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> <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 --> <!-- 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="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">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> <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="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> <item quantity="other">Consider signing out from old sessions (%1$d days or more) that you dont use anymore.</item>
</plurals> </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_session_title">Session</string>
<string name="device_manager_device_title">Device</string> <string name="device_manager_device_title">Device</string>
<!-- Examples: Last activity Yesterday at 6PM, Last activity Aug 31 at 5:47PM --> <!-- 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">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> <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_bold">Apply bold format</string>
<string name="rich_text_editor_format_italic">Apply italic 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_strikethrough">Apply strikethrough format</string>
<string name="rich_text_editor_format_underline">Apply underline 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="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 --> <!-- ReplyTo events -->
<string name="message_reply_to_prefix">In reply to</string> <string name="message_reply_to_prefix">In reply to</string>
<string name="message_reply_to_sender_sent_file">sent a file.</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. // that the app's state is completely cleared between tests.
testInstrumentationRunnerArguments clearPackageData: 'true' 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", "\"${gitRevision()}\""
buildConfigField "String", "GIT_SDK_REVISION_UNIX_DATE", "\"${gitRevisionUnixDate()}\"" 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.Timeline
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent 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.room.timeline.TimelineSettings
import org.matrix.android.sdk.api.session.sync.filter.SyncFilterBuilder
import timber.log.Timber import timber.log.Timber
import java.util.UUID import java.util.UUID
import java.util.concurrent.CountDownLatch import java.util.concurrent.CountDownLatch
@ -347,10 +346,6 @@ class CommonTestHelper internal constructor(context: Context, val cryptoConfig:
assertTrue(registrationResult is RegistrationResult.Success) assertTrue(registrationResult is RegistrationResult.Success)
val session = (registrationResult as RegistrationResult.Success).session val session = (registrationResult as RegistrationResult.Success).session
session.open() session.open()
session.filterService().setSyncFilter(
SyncFilterBuilder()
.lazyLoadMembersForStateEvents(true)
)
if (sessionTestParams.withInitialSync) { if (sessionTestParams.withInitialSync) {
syncSession(session, 120_000) syncSession(session, 120_000)
} }

View file

@ -16,9 +16,13 @@
package org.matrix.android.sdk.api package org.matrix.android.sdk.api
import org.matrix.android.sdk.api.session.sync.filter.SyncFilterParams
data class SyncConfig( data class SyncConfig(
/** /**
* Time to keep sync connection alive for before making another request in milliseconds. * Time to keep sync connection alive for before making another request in milliseconds.
*/ */
val longPollTimeout: Long = 30_000L, 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.signout.SignOutService
import org.matrix.android.sdk.api.session.space.SpaceService 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.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.sync.SyncService
import org.matrix.android.sdk.api.session.terms.TermsService import org.matrix.android.sdk.api.session.terms.TermsService
import org.matrix.android.sdk.api.session.thirdparty.ThirdPartyService import org.matrix.android.sdk.api.session.thirdparty.ThirdPartyService
@ -163,11 +162,6 @@ interface Session {
*/ */
fun signOutService(): SignOutService fun signOutService(): SignOutService
/**
* Returns the FilterService associated with the session.
*/
fun filterService(): FilterService
/** /**
* Returns the PushRuleService associated with the session. * 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 fun Event.isSticker(): Boolean = getClearType() == EventType.STICKER

View file

@ -14,9 +14,9 @@
* limitations under the License. * 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 lazyLoadMembersForStateEvents: Boolean? = null,
val lazyLoadMembersForMessageEvents: Boolean? = null, val lazyLoadMembersForMessageEvents: Boolean? = null,
val useThreadNotifications: 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.MatrixCallback
import org.matrix.android.sdk.api.MatrixCoroutineDispatchers import org.matrix.android.sdk.api.MatrixCoroutineDispatchers
import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_OLM 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.logger.LoggerTag
import org.matrix.android.sdk.api.session.crypto.MXCryptoError 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.MXEventDecryptionResult
import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap 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.Event
import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.api.session.events.model.content.OlmEventContent import org.matrix.android.sdk.api.session.events.model.content.OlmEventContent
@ -85,6 +87,27 @@ internal class EventDecryptor @Inject constructor(
return internalDecryptEvent(event, timeline) 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. * 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.MigrateSessionTo044
import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo045 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.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.Normalizer
import org.matrix.android.sdk.internal.util.database.MatrixRealmMigration import org.matrix.android.sdk.internal.util.database.MatrixRealmMigration
import javax.inject.Inject import javax.inject.Inject
@ -71,7 +72,7 @@ internal class RealmSessionStoreMigration @Inject constructor(
private val normalizer: Normalizer private val normalizer: Normalizer
) : MatrixRealmMigration( ) : MatrixRealmMigration(
dbName = "Session", dbName = "Session",
schemaVersion = 46L, schemaVersion = 47L,
) { ) {
/** /**
* Forces all RealmSessionStoreMigration instances to be equal. * Forces all RealmSessionStoreMigration instances to be equal.
@ -127,5 +128,6 @@ internal class RealmSessionStoreMigration @Inject constructor(
if (oldVersion < 44) MigrateSessionTo044(realm).perform() if (oldVersion < 44) MigrateSessionTo044(realm).perform()
if (oldVersion < 45) MigrateSessionTo045(realm).perform() if (oldVersion < 45) MigrateSessionTo045(realm).perform()
if (oldVersion < 46) MigrateSessionTo046(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, SpaceParentSummaryEntity::class,
UserPresenceEntity::class, UserPresenceEntity::class,
ThreadSummaryEntity::class, ThreadSummaryEntity::class,
SyncFilterParamsEntity::class,
ThreadListPageEntity::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.securestorage.SharedSecretStorageService
import org.matrix.android.sdk.api.session.signout.SignOutService 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.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.sync.SyncService
import org.matrix.android.sdk.api.session.terms.TermsService import org.matrix.android.sdk.api.session.terms.TermsService
import org.matrix.android.sdk.api.session.thirdparty.ThirdPartyService 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 roomService: Lazy<RoomService>,
private val roomDirectoryService: Lazy<RoomDirectoryService>, private val roomDirectoryService: Lazy<RoomDirectoryService>,
private val userService: Lazy<UserService>, private val userService: Lazy<UserService>,
private val filterService: Lazy<FilterService>,
private val federationService: Lazy<FederationService>, private val federationService: Lazy<FederationService>,
private val cacheService: Lazy<CacheService>, private val cacheService: Lazy<CacheService>,
private val signOutService: Lazy<SignOutService>, private val signOutService: Lazy<SignOutService>,
@ -209,7 +207,6 @@ internal class DefaultSession @Inject constructor(
override fun roomDirectoryService(): RoomDirectoryService = roomDirectoryService.get() override fun roomDirectoryService(): RoomDirectoryService = roomDirectoryService.get()
override fun userService(): UserService = userService.get() override fun userService(): UserService = userService.get()
override fun signOutService(): SignOutService = signOutService.get() override fun signOutService(): SignOutService = signOutService.get()
override fun filterService(): FilterService = filterService.get()
override fun pushRuleService(): PushRuleService = pushRuleService.get() override fun pushRuleService(): PushRuleService = pushRuleService.get()
override fun pushersService(): PushersService = pushersService.get() override fun pushersService(): PushersService = pushersService.get()
override fun eventService(): EventService = eventService.get() override fun eventService(): EventService = eventService.get()

View file

@ -17,20 +17,15 @@
package org.matrix.android.sdk.internal.session.filter package org.matrix.android.sdk.internal.session.filter
import com.zhuinden.monarchy.Monarchy 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.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.get
import org.matrix.android.sdk.internal.database.query.getOrCreate import org.matrix.android.sdk.internal.database.query.getOrCreate
import org.matrix.android.sdk.internal.di.SessionDatabase 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 org.matrix.android.sdk.internal.util.awaitTransaction
import javax.inject.Inject import javax.inject.Inject
internal class DefaultFilterRepository @Inject constructor( internal class DefaultFilterRepository @Inject constructor(
@SessionDatabase private val monarchy: Monarchy, @SessionDatabase private val monarchy: Monarchy,
private val filterParamsMapper: FilterParamsMapper
) : FilterRepository { ) : FilterRepository {
override suspend fun storeSyncFilter(filter: Filter, filterId: String, roomEventFilter: RoomEventFilter) { override suspend fun storeSyncFilter(filter: Filter, filterId: String, roomEventFilter: RoomEventFilter) {
@ -69,19 +64,4 @@ internal class DefaultFilterRepository @Inject constructor(
FilterEntity.getOrCreate(it).roomEventFilterJson 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.Binds
import dagger.Module import dagger.Module
import dagger.Provides import dagger.Provides
import org.matrix.android.sdk.api.session.sync.FilterService
import org.matrix.android.sdk.internal.session.SessionScope import org.matrix.android.sdk.internal.session.SessionScope
import retrofit2.Retrofit import retrofit2.Retrofit
@ -39,9 +38,6 @@ internal abstract class FilterModule {
@Binds @Binds
abstract fun bindFilterRepository(repository: DefaultFilterRepository): FilterRepository abstract fun bindFilterRepository(repository: DefaultFilterRepository): FilterRepository
@Binds
abstract fun bindFilterService(service: DefaultFilterService): FilterService
@Binds @Binds
abstract fun bindSaveFilterTask(task: DefaultSaveFilterTask): SaveFilterTask abstract fun bindSaveFilterTask(task: DefaultSaveFilterTask): SaveFilterTask

View file

@ -16,8 +16,6 @@
package org.matrix.android.sdk.internal.session.filter package org.matrix.android.sdk.internal.session.filter
import org.matrix.android.sdk.internal.sync.filter.SyncFilterParams
/** /**
* Repository for request filters. * Repository for request filters.
*/ */
@ -44,14 +42,4 @@ internal interface FilterRepository {
* Return the room filter. * Return the room filter.
*/ */
suspend fun getRoomFilterBody(): String 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 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.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.session.homeserver.HomeServerCapabilitiesDataSource
import org.matrix.android.sdk.internal.sync.filter.SyncFilterBuilder
import org.matrix.android.sdk.internal.task.Task import org.matrix.android.sdk.internal.task.Task
import javax.inject.Inject import javax.inject.Inject
@ -27,7 +28,8 @@ internal interface GetCurrentFilterTask : Task<Unit, String>
internal class DefaultGetCurrentFilterTask @Inject constructor( internal class DefaultGetCurrentFilterTask @Inject constructor(
private val filterRepository: FilterRepository, private val filterRepository: FilterRepository,
private val homeServerCapabilitiesDataSource: HomeServerCapabilitiesDataSource, private val homeServerCapabilitiesDataSource: HomeServerCapabilitiesDataSource,
private val saveFilterTask: SaveFilterTask private val saveFilterTask: SaveFilterTask,
private val matrixConfiguration: MatrixConfiguration
) : GetCurrentFilterTask { ) : GetCurrentFilterTask {
override suspend fun execute(params: Unit): String { override suspend fun execute(params: Unit): String {
@ -35,7 +37,7 @@ internal class DefaultGetCurrentFilterTask @Inject constructor(
val storedFilterBody = filterRepository.getStoredSyncFilterBody() val storedFilterBody = filterRepository.getStoredSyncFilterBody()
val homeServerCapabilities = homeServerCapabilitiesDataSource.getHomeServerCapabilities() ?: HomeServerCapabilities() val homeServerCapabilities = homeServerCapabilitiesDataSource.getHomeServerCapabilities() ?: HomeServerCapabilities()
val currentFilter = SyncFilterBuilder() val currentFilter = SyncFilterBuilder()
.with(filterRepository.getStoredFilterParams()) .with(matrixConfiguration.syncConfig.syncFilterParams)
.build(homeServerCapabilities) .build(homeServerCapabilities)
val currentFilterBody = currentFilter.toJSONString() 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.Content
import org.matrix.android.sdk.api.session.events.model.Event 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.Membership
import org.matrix.android.sdk.api.session.room.model.RoomStrippedState import org.matrix.android.sdk.api.session.room.model.RoomStrippedState
import org.matrix.android.sdk.api.session.room.model.roomdirectory.PublicRoomsParams 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 * @param limit max number of Event to retrieve
*/ */
@GET(NetworkConstants.URI_API_PREFIX_PATH_UNSTABLE + "rooms/{roomId}/relations/{eventId}/{relationType}/{eventType}") @GET(NetworkConstants.URI_API_PREFIX_PATH_UNSTABLE + "rooms/{roomId}/relations/{eventId}/{relationType}/{eventType}")
suspend fun getRelations( suspend fun getRelationsWithEventType(
@Path("roomId") roomId: String, @Path("roomId") roomId: String,
@Path("eventId") eventId: String, @Path("eventId") eventId: String,
@Path("relationType") relationType: String, @Path("relationType") relationType: String,
@ -262,7 +261,7 @@ internal interface RoomAPI {
): RelationsResponse ): 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 roomId the room Id
* @param eventId the event Id * @param eventId the event Id
@ -272,10 +271,10 @@ internal interface RoomAPI {
* @param limit max number of Event to retrieve * @param limit max number of Event to retrieve
*/ */
@GET(NetworkConstants.URI_API_PREFIX_PATH_UNSTABLE + "rooms/{roomId}/relations/{eventId}/{relationType}") @GET(NetworkConstants.URI_API_PREFIX_PATH_UNSTABLE + "rooms/{roomId}/relations/{eventId}/{relationType}")
suspend fun getThreadsRelations( suspend fun getRelations(
@Path("roomId") roomId: String, @Path("roomId") roomId: String,
@Path("eventId") eventId: String, @Path("eventId") eventId: String,
@Path("relationType") relationType: String = RelationType.THREAD, @Path("relationType") relationType: String,
@Query("from") from: String? = null, @Query("from") from: String? = null,
@Query("to") to: String? = null, @Query("to") to: String? = null,
@Query("limit") limit: Int? = 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.FetchEditHistoryTask
import org.matrix.android.sdk.internal.session.room.relation.FindReactionEventForUndoTask 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.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.DefaultFetchThreadSummariesTask
import org.matrix.android.sdk.internal.session.room.relation.threads.DefaultFetchThreadTimelineTask import org.matrix.android.sdk.internal.session.room.relation.threads.DefaultFetchThreadTimelineTask
import org.matrix.android.sdk.internal.session.room.relation.threads.FetchThreadSummariesTask import org.matrix.android.sdk.internal.session.room.relation.threads.FetchThreadSummariesTask
@ -354,4 +356,7 @@ internal abstract class RoomModule {
@Binds @Binds
abstract fun bindRedactLiveLocationShareTask(task: DefaultRedactLiveLocationShareTask): RedactLiveLocationShareTask 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 package org.matrix.android.sdk.internal.session.room.aggregation.poll
import io.realm.Realm import io.realm.Realm
import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.extensions.orFalse
import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.events.model.Event 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.create
import org.matrix.android.sdk.internal.database.query.getOrCreate import org.matrix.android.sdk.internal.database.query.getOrCreate
import org.matrix.android.sdk.internal.database.query.where import org.matrix.android.sdk.internal.database.query.where
import org.matrix.android.sdk.internal.session.room.relation.poll.FetchPollResponseEventsTask
import org.matrix.android.sdk.internal.task.TaskExecutor
import javax.inject.Inject 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 { override fun handlePollStartEvent(realm: Realm, event: Event): Boolean {
val content = event.getClearContent()?.toModel<MessagePollContent>() val content = event.getClearContent()?.toModel<MessagePollContent>()
@ -174,6 +180,10 @@ class DefaultPollAggregationProcessor @Inject constructor() : PollAggregationPro
aggregatedPollSummaryEntity.sourceEvents.add(event.eventId) aggregatedPollSummaryEntity.sourceEvents.add(event.eventId)
} }
if (!isLocalEcho) {
ensurePollIsFullyAggregated(roomId, pollEventId)
}
return true return true
} }
@ -200,4 +210,20 @@ class DefaultPollAggregationProcessor @Inject constructor() : PollAggregationPro
eventAnnotationsSummaryEntity.pollResponseSummary = it 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> { override suspend fun execute(params: FetchEditHistoryTask.Params): List<Event> {
val isRoomEncrypted = cryptoSessionInfoProvider.isRoomEncrypted(params.roomId) val isRoomEncrypted = cryptoSessionInfoProvider.isRoomEncrypted(params.roomId)
val response = executeRequest(globalErrorReceiver) { val response = executeRequest(globalErrorReceiver) {
roomAPI.getRelations( roomAPI.getRelationsWithEventType(
roomId = params.roomId, roomId = params.roomId,
eventId = params.eventId, eventId = params.eventId,
relationType = RelationType.REPLACE, 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.crypto.model.OlmDecryptionResult
import org.matrix.android.sdk.api.session.events.model.Event 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.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.model.RoomMemberContent
import org.matrix.android.sdk.api.session.room.send.SendState import org.matrix.android.sdk.api.session.room.send.SendState
import org.matrix.android.sdk.internal.crypto.DefaultCryptoService 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 { override suspend fun execute(params: FetchThreadTimelineTask.Params): Result {
val response = executeRequest(globalErrorReceiver) { val response = executeRequest(globalErrorReceiver) {
roomAPI.getThreadsRelations( roomAPI.getRelations(
roomId = params.roomId, roomId = params.roomId,
eventId = params.rootThreadEventId, eventId = params.rootThreadEventId,
relationType = RelationType.THREAD,
from = params.from, 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.clear()
roomSummaryEntity.otherMemberIds.addAll(otherRoomMembers) 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 (roomSummaryEntity.isEncrypted && otherRoomMembers.isNotEmpty()) {
if (aggregator == null) { if (aggregator == null) {
// Do it now // Do it now

View file

@ -16,8 +16,6 @@
package org.matrix.android.sdk.internal.session.room.timeline 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.api.session.events.model.Event
import org.matrix.android.sdk.internal.crypto.EventDecryptor import org.matrix.android.sdk.internal.crypto.EventDecryptor
import org.matrix.android.sdk.internal.network.GlobalErrorReceiver import org.matrix.android.sdk.internal.network.GlobalErrorReceiver
@ -48,18 +46,7 @@ internal class DefaultGetEventTask @Inject constructor(
// Try to decrypt the Event // Try to decrypt the Event
if (event.isEncrypted()) { if (event.isEncrypted()) {
tryOrNull(message = "Unable to decrypt the event") { eventDecryptor.decryptEventAndSaveResult(event, timeline = "")
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
)
}
} }
event.ageLocalTs = clock.epochMillis() - (event.unsignedData?.age ?: 0) 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 io.realm.Realm
import org.matrix.android.sdk.api.session.room.sender.SenderInfo 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.di.UserId
import org.matrix.android.sdk.internal.session.room.membership.RoomMemberHelper import org.matrix.android.sdk.internal.session.room.membership.RoomMemberHelper
import org.matrix.android.sdk.internal.session.typing.DefaultTypingUsersTracker 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? // TODO This could be handled outside of the Realm transaction. Use the new aggregator?
fun handle(realm: Realm, roomId: String, ephemeralResult: RoomSyncHandler.EphemeralResult?) { 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 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 senderInfo = typingIds.map { userId ->
val roomMemberSummaryEntity = roomMemberHelper.getLastRoomMember(userId) val roomMemberSummaryEntity = roomMemberHelper.getLastRoomMember(userId)
SenderInfo( SenderInfo(

View file

@ -14,15 +14,15 @@
* limitations under the License. * 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.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.Filter
import org.matrix.android.sdk.internal.session.filter.RoomEventFilter import org.matrix.android.sdk.internal.session.filter.RoomEventFilter
import org.matrix.android.sdk.internal.session.filter.RoomFilter 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 lazyLoadMembersForStateEvents: Boolean? = null
private var lazyLoadMembersForMessageEvents: Boolean? = null private var lazyLoadMembersForMessageEvents: Boolean? = null
private var useThreadNotifications: 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 { internal fun build(homeServerCapabilities: HomeServerCapabilities): Filter {
return Filter( return Filter(
room = buildRoomFilter(homeServerCapabilities) room = buildRoomFilter(homeServerCapabilities)

View file

@ -16,9 +16,13 @@
package org.matrix.android.sdk.internal.session.room.aggregation.poll package org.matrix.android.sdk.internal.session.room.aggregation.poll
import io.mockk.coVerify
import io.mockk.every import io.mockk.every
import io.mockk.mockk import io.mockk.mockk
import io.realm.RealmList 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.shouldBeFalse
import org.amshove.kluent.shouldBeTrue import org.amshove.kluent.shouldBeTrue
import org.junit.Before 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_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.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_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_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_REFERENCE_EVENT
import org.matrix.android.sdk.internal.session.room.aggregation.poll.PollEventsTestData.A_POLL_REPLACE_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_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_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.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.FakeRealm
import org.matrix.android.sdk.test.fakes.FakeTaskExecutor
import org.matrix.android.sdk.test.fakes.givenEqualTo import org.matrix.android.sdk.test.fakes.givenEqualTo
import org.matrix.android.sdk.test.fakes.givenFindFirst import org.matrix.android.sdk.test.fakes.givenFindFirst
@OptIn(ExperimentalCoroutinesApi::class)
class DefaultPollAggregationProcessorTest { 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 realm = FakeRealm()
private val session = mockk<Session>() private val session = mockk<Session>()
@ -114,16 +128,28 @@ class DefaultPollAggregationProcessorTest {
} }
@Test @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 { realm.instance.createObject(PollResponseAggregatedSummaryEntity::class.java) } returns PollResponseAggregatedSummaryEntity()
every { fakeTaskExecutor.instance.executorScope } returns this
// When
val powerLevelsHelper = mockRedactionPowerLevels(A_USER_ID_1, true) val powerLevelsHelper = mockRedactionPowerLevels(A_USER_ID_1, true)
// Then
pollAggregationProcessor.handlePollEndEvent(session, powerLevelsHelper, realm.instance, A_POLL_END_EVENT).shouldBeTrue() pollAggregationProcessor.handlePollEndEvent(session, powerLevelsHelper, realm.instance, A_POLL_END_EVENT).shouldBeTrue()
} }
@Test @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 { realm.instance.createObject(PollResponseAggregatedSummaryEntity::class.java) } returns PollResponseAggregatedSummaryEntity()
every { fakeTaskExecutor.instance.executorScope } returns this
// When
val powerLevelsHelper = mockRedactionPowerLevels(A_USER_ID_1, false) val powerLevelsHelper = mockRedactionPowerLevels(A_USER_ID_1, false)
// Then
pollAggregationProcessor.handlePollEndEvent(session, powerLevelsHelper, realm.instance, A_POLL_END_EVENT).shouldBeTrue() 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() 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() { private fun mockEventAnnotationsSummaryEntity() {
realm.givenWhere<EventAnnotationsSummaryEntity>() realm.givenWhere<EventAnnotationsSummaryEntity>()
.givenFindFirst(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 package org.matrix.android.sdk.internal.sync
import io.mockk.mockk
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import org.amshove.kluent.shouldBeEqualTo import org.amshove.kluent.shouldBeEqualTo
import org.junit.Test 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.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.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.FakeFilterRepository
import org.matrix.android.sdk.test.fakes.FakeHomeServerCapabilitiesDataSource import org.matrix.android.sdk.test.fakes.FakeHomeServerCapabilitiesDataSource
import org.matrix.android.sdk.test.fakes.FakeSaveFilterTask 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 const val A_FILTER_ID = "filter-id"
private val A_HOMESERVER_CAPABILITIES = HomeServerCapabilities() private val A_HOMESERVER_CAPABILITIES = HomeServerCapabilities()
private val A_SYNC_FILTER_PARAMS = SyncFilterParams( private val A_SYNC_FILTER_PARAMS = SyncFilterParams(
lazyLoadMembersForMessageEvents = true,
lazyLoadMembersForStateEvents = true, lazyLoadMembersForStateEvents = true,
useThreadNotifications = true useThreadNotifications = true
) )
@ -46,13 +48,16 @@ class DefaultGetCurrentFilterTaskTest {
private val getCurrentFilterTask = DefaultGetCurrentFilterTask( private val getCurrentFilterTask = DefaultGetCurrentFilterTask(
filterRepository = filterRepository, filterRepository = filterRepository,
homeServerCapabilitiesDataSource = homeServerCapabilitiesDataSource.instance, homeServerCapabilitiesDataSource = homeServerCapabilitiesDataSource.instance,
saveFilterTask = saveFilterTask saveFilterTask = saveFilterTask,
matrixConfiguration = MatrixConfiguration(
applicationFlavor = "TestFlavor",
roomDisplayNameFallbackProvider = mockk(),
syncConfig = SyncConfig(syncFilterParams = SyncFilterParams(lazyLoadMembersForStateEvents = true, useThreadNotifications = true)),
)
) )
@Test @Test
fun `given no filter is stored, when execute, then executes task to save new filter`() = runTest { 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) homeServerCapabilitiesDataSource.givenHomeServerCapabilities(A_HOMESERVER_CAPABILITIES)
filterRepository.givenFilterStored(null, null) filterRepository.givenFilterStored(null, null)
@ -68,8 +73,6 @@ class DefaultGetCurrentFilterTaskTest {
@Test @Test
fun `given filter is stored and didn't change, when execute, then returns stored filter id`() = runTest { 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) homeServerCapabilitiesDataSource.givenHomeServerCapabilities(A_HOMESERVER_CAPABILITIES)
val filter = SyncFilterBuilder().with(A_SYNC_FILTER_PARAMS).build(A_HOMESERVER_CAPABILITIES) val filter = SyncFilterBuilder().with(A_SYNC_FILTER_PARAMS).build(A_HOMESERVER_CAPABILITIES)
@ -82,8 +85,6 @@ class DefaultGetCurrentFilterTaskTest {
@Test @Test
fun `given filter is set and home server capabilities has changed, when execute, then executes task to save new filter`() = runTest { 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) homeServerCapabilitiesDataSource.givenHomeServerCapabilities(A_HOMESERVER_CAPABILITIES)
val filter = SyncFilterBuilder().with(A_SYNC_FILTER_PARAMS).build(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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -14,14 +14,9 @@
* limitations under the License. * 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 { class FakeFetchPollResponseEventsTask : FetchPollResponseEventsTask by mockk(relaxed = true)
/**
* Configure the filter for the sync.
*/
suspend fun setSyncFilter(filterBuilder: SyncFilterBuilder)
}

View file

@ -19,7 +19,6 @@ package org.matrix.android.sdk.test.fakes
import io.mockk.coEvery import io.mockk.coEvery
import io.mockk.mockk import io.mockk.mockk
import org.matrix.android.sdk.internal.session.filter.FilterRepository import org.matrix.android.sdk.internal.session.filter.FilterRepository
import org.matrix.android.sdk.internal.sync.filter.SyncFilterParams
internal class FakeFilterRepository : FilterRepository by mockk() { internal class FakeFilterRepository : FilterRepository by mockk() {
@ -27,8 +26,4 @@ internal class FakeFilterRepository : FilterRepository by mockk() {
coEvery { getStoredSyncFilterId() } returns filterId coEvery { getStoredSyncFilterId() } returns filterId
coEvery { getStoredSyncFilterBody() } returns filterBody 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 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. * 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", "begging",
"mercy", "mercy",
"puppy eyes", "puppy eyes",
"face" "face",
"cry",
"tears",
"sad",
"grievance"
] ]
}, },
"face-holding-back-tears": { "face-holding-back-tears": {
@ -3060,9 +3064,7 @@
"fearful", "fearful",
"scared", "scared",
"terrified", "terrified",
"nervous", "nervous"
"oops",
"huh"
] ]
}, },
"anxious-face-with-sweat": { "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. // 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 // When creating a hotfix, you should decrease the value, since the current value
// is the value for the next regular release. // is the value for the next regular release.
ext.versionPatch = 14 ext.versionPatch = 16
static def getGitTimestamp() { static def getGitTimestamp() {
def cmd = 'git show -s --format=%ct' 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) 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 { return object : ViewAction {
private val clock = DefaultClock() 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.features.MainActivity
import im.vector.app.getString import im.vector.app.getString
import im.vector.app.ui.robot.ElementRobot 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.settings.labs.LabFeaturesPreferences
import im.vector.app.ui.robot.withDeveloperMode import im.vector.app.ui.robot.withDeveloperMode
import org.junit.Rule 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.space { selectSpace(spaceName) }
elementRobot.layoutPreferences { elementRobot.layoutPreferences {
@ -175,7 +178,6 @@ class UiAllScreensSanityTest {
* Testing multiple threads screens * Testing multiple threads screens
*/ */
private fun testThreadScreens() { private fun testThreadScreens() {
elementRobot.toggleLabFeature(LabFeature.THREAD_MESSAGES)
elementRobot.newRoom { elementRobot.newRoom {
createNewRoom { createNewRoom {
crawl() 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 com.adevinta.android.barista.interaction.BaristaClickInteractions.clickOn
import im.vector.app.R import im.vector.app.R
import im.vector.app.espresso.tools.waitUntilActivityVisible 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.espresso.tools.waitUntilViewVisible
import im.vector.app.features.home.HomeActivity import im.vector.app.features.home.HomeActivity
import im.vector.app.features.home.room.detail.RoomDetailActivity import im.vector.app.features.home.room.detail.RoomDetailActivity
@ -86,14 +85,17 @@ class SpaceCreateRobot {
clickOn(R.id.nextButton) clickOn(R.id.nextButton)
waitUntilViewVisible(withId(R.id.recyclerView)) waitUntilViewVisible(withId(R.id.recyclerView))
clickOn(R.id.nextButton) clickOn(R.id.nextButton)
// waitUntilActivityVisible<RoomDetailActivity> {
// waitUntilDialogVisible(withId(R.id.inviteByMxidButton))
// }
// // close invite dialog
// pressBack()
waitUntilActivityVisible<RoomDetailActivity> { waitUntilActivityVisible<RoomDetailActivity> {
waitUntilDialogVisible(withId(R.id.inviteByMxidButton)) pressBack()
} }
// close invite dialog // waitUntilViewVisible(withId(R.id.timelineRecyclerView))
pressBack()
waitUntilViewVisible(withId(R.id.timelineRecyclerView))
// close room // close room
pressBack() // pressBack()
waitUntilViewVisible(withId(R.id.roomListContainer)) waitUntilViewVisible(withId(R.id.roomListContainer))
} }
} }

View file

@ -89,9 +89,8 @@ class SpaceMenuRobot {
clickOnSheet(R.id.leaveSpace) clickOnSheet(R.id.leaveSpace)
waitUntilActivityVisible<SpaceLeaveAdvancedActivity> { waitUntilActivityVisible<SpaceLeaveAdvancedActivity> {
waitUntilViewVisible(ViewMatchers.withId(R.id.roomList)) waitUntilViewVisible(ViewMatchers.withId(R.id.roomList))
}
clickOn(R.id.spaceLeaveSelectAll) clickOn(R.id.spaceLeaveSelectAll)
clickOn(R.id.spaceLeaveButton) 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 kotlinx.coroutines.SupervisorJob
import org.matrix.android.sdk.api.Matrix import org.matrix.android.sdk.api.Matrix
import org.matrix.android.sdk.api.MatrixConfiguration 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.AuthenticationService
import org.matrix.android.sdk.api.auth.HomeServerHistoryService import org.matrix.android.sdk.api.auth.HomeServerHistoryService
import org.matrix.android.sdk.api.legacy.LegacySessionImporter import org.matrix.android.sdk.api.legacy.LegacySessionImporter
import org.matrix.android.sdk.api.raw.RawService import org.matrix.android.sdk.api.raw.RawService
import org.matrix.android.sdk.api.session.Session 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 org.matrix.android.sdk.api.settings.LightweightSettingsStorage
import javax.inject.Singleton import javax.inject.Singleton
@ -157,6 +159,9 @@ import javax.inject.Singleton
), ),
metricPlugins = vectorPlugins.plugins(), metricPlugins = vectorPlugins.plugins(),
customEventTypesProvider = vectorCustomEventTypesProvider, 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.breadcrumbs.BreadcrumbsViewModel
import im.vector.app.features.home.room.detail.TimelineViewModel 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.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.search.SearchViewModel
import im.vector.app.features.home.room.detail.timeline.action.MessageActionsViewModel import im.vector.app.features.home.room.detail.timeline.action.MessageActionsViewModel
import im.vector.app.features.home.room.detail.timeline.edithistory.ViewEditHistoryViewModel import im.vector.app.features.home.room.detail.timeline.edithistory.ViewEditHistoryViewModel
@ -691,4 +692,9 @@ interface MavericksViewModelModule {
fun vectorSettingsNotificationPreferenceViewModelFactory( fun vectorSettingsNotificationPreferenceViewModelFactory(
factory: VectorSettingsNotificationPreferenceViewModel.Factory factory: VectorSettingsNotificationPreferenceViewModel.Factory
): MavericksAssistedViewModelFactory<*, *> ): 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.session.coroutineScope
import im.vector.app.features.settings.VectorPreferences import im.vector.app.features.settings.VectorPreferences
import im.vector.app.features.settings.devices.v2.notification.UpdateNotificationSettingsAccountDataUseCase import im.vector.app.features.settings.devices.v2.notification.UpdateNotificationSettingsAccountDataUseCase
import im.vector.app.features.sync.SyncUtils
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.Session
import timber.log.Timber import timber.log.Timber
@ -43,9 +42,6 @@ class ConfigureAndStartSessionUseCase @Inject constructor(
fun execute(session: Session, startSyncing: Boolean = true) { fun execute(session: Session, startSyncing: Boolean = true) {
Timber.i("Configure and start session for ${session.myUserId}. startSyncing: $startSyncing") Timber.i("Configure and start session for ${session.myUserId}. startSyncing: $startSyncing")
session.open() session.open()
session.coroutineScope.launch {
session.filterService().setSyncFilter(SyncUtils.getSyncFilterBuilder())
}
if (startSyncing) { if (startSyncing) {
session.startSyncing(context) session.startSyncing(context)
} }

View file

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

View file

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

View file

@ -413,6 +413,7 @@ class TimelineFragment :
is RoomDetailViewEvents.DisplayAndAcceptCall -> acceptIncomingCall(it) is RoomDetailViewEvents.DisplayAndAcceptCall -> acceptIncomingCall(it)
RoomDetailViewEvents.RoomReplacementStarted -> handleRoomReplacement() RoomDetailViewEvents.RoomReplacementStarted -> handleRoomReplacement()
RoomDetailViewEvents.OpenElementCallWidget -> handleOpenElementCallWidget() 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() { override fun onTapToReturnToCall() {
callManager.getCurrentCall()?.let { call -> callManager.getCurrentCall()?.let { call ->
VectorCallActivity.newIntent( VectorCallActivity.newIntent(

View file

@ -633,7 +633,8 @@ class TimelineViewModel @AssistedInject constructor(
} }
VoiceBroadcastAction.Recording.Pause -> voiceBroadcastHelper.pauseVoiceBroadcast(room.roomId) VoiceBroadcastAction.Recording.Pause -> voiceBroadcastHelper.pauseVoiceBroadcast(room.roomId)
VoiceBroadcastAction.Recording.Resume -> voiceBroadcastHelper.resumeVoiceBroadcast(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) is VoiceBroadcastAction.Listening.PlayOrResume -> voiceBroadcastHelper.playOrResumePlayback(action.voiceBroadcast)
VoiceBroadcastAction.Listening.Pause -> voiceBroadcastHelper.pausePlayback() VoiceBroadcastAction.Listening.Pause -> voiceBroadcastHelper.pausePlayback()
VoiceBroadcastAction.Listening.Stop -> voiceBroadcastHelper.stopPlayback() 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
import im.vector.app.features.home.room.detail.RoomDetailAction.VoiceBroadcastAction 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.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.composer.voice.VoiceMessageRecorderView
import im.vector.app.features.home.room.detail.timeline.action.MessageSharedActionViewModel import im.vector.app.features.home.room.detail.timeline.action.MessageSharedActionViewModel
import im.vector.app.features.home.room.detail.upgrade.MigrateRoomBottomSheet 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 lateinit var sharedActionViewModel: MessageSharedActionViewModel
private val attachmentViewModel: AttachmentTypeSelectorViewModel by fragmentViewModel() private val attachmentViewModel: AttachmentTypeSelectorViewModel by fragmentViewModel()
private val attachmentActionsViewModel: AttachmentTypeSelectorSharedActionViewModel by viewModels() private val attachmentActionsViewModel: AttachmentTypeSelectorSharedActionViewModel by viewModels()
private val setLinkActionsViewModel: SetLinkSharedActionViewModel by viewModels()
private val composer: MessageComposerView get() { private val composer: MessageComposerView get() {
return if (vectorPreferences.isRichTextEditorEnabled()) { return if (vectorPreferences.isRichTextEditorEnabled()) {
@ -212,6 +216,14 @@ class MessageComposerFragment : VectorBaseFragment<FragmentComposerBinding>(), A
.onEach { onTypeSelected(it.attachmentType) } .onEach { onTypeSelected(it.attachmentType) }
.launchIn(lifecycleScope) .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 } messageComposerViewModel.stateFlow.map { it.isFullScreen }
.distinctUntilChanged() .distinctUntilChanged()
.onEach { isFullScreen -> .onEach { isFullScreen ->
@ -385,6 +397,10 @@ class MessageComposerFragment : VectorBaseFragment<FragmentComposerBinding>(), A
override fun onFullScreenModeChanged() = withState(messageComposerViewModel) { state -> override fun onFullScreenModeChanged() = withState(messageComposerViewModel) { state ->
messageComposerViewModel.handle(MessageComposerAction.SetFullScreen(!state.isFullScreen)) 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 onAddAttachment()
fun onExpandOrCompactChange() fun onExpandOrCompactChange()
fun onFullScreenModeChanged() 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 im.vector.app.databinding.ViewRichTextMenuButtonBinding
import io.element.android.wysiwyg.EditorEditText import io.element.android.wysiwyg.EditorEditText
import io.element.android.wysiwyg.inputhandlers.models.InlineFormat import io.element.android.wysiwyg.inputhandlers.models.InlineFormat
import io.element.android.wysiwyg.inputhandlers.models.LinkAction
import io.element.android.wysiwyg.utils.RustErrorCollector import io.element.android.wysiwyg.utils.RustErrorCollector
import uniffi.wysiwyg_composer.ActionState import uniffi.wysiwyg_composer.ActionState
import uniffi.wysiwyg_composer.ComposerAction import uniffi.wysiwyg_composer.ComposerAction
@ -231,7 +232,24 @@ internal class RichTextComposerLayout @JvmOverloads constructor(
addRichTextMenuItem(R.drawable.ic_composer_strikethrough, R.string.rich_text_editor_format_strikethrough, ComposerAction.STRIKE_THROUGH) { addRichTextMenuItem(R.drawable.ic_composer_strikethrough, R.string.rich_text_editor_format_strikethrough, ComposerAction.STRIKE_THROUGH) {
views.richTextComposerEditText.toggleInlineFormat(InlineFormat.StrikeThrough) 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") @SuppressLint("ClickableViewAccessibility")
private fun disallowParentInterceptTouchEvent(view: View) { private fun disallowParentInterceptTouchEvent(view: View) {

View file

@ -14,23 +14,17 @@
* limitations under the License. * 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 im.vector.app.core.platform.VectorViewModelAction
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
class FakeFilterService : FilterService by mockk() { sealed class SetLinkAction : VectorViewModelAction {
data class LinkChanged(
val newLink: String
) : SetLinkAction()
fun givenSetFilterSucceeds() { data class Save(
coEvery { setSyncFilter(any()) } just runs val link: String,
} val text: String,
) : SetLinkAction()
fun verifySetSyncFilter(filterBuilder: SyncFilterBuilder) {
coVerify { setSyncFilter(filterBuilder) }
}
} }

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

View file

@ -118,6 +118,7 @@ class NotificationBroadcastReceiver : BroadcastReceiver() {
private fun handleSmartReply(intent: Intent, context: Context) { private fun handleSmartReply(intent: Intent, context: Context) {
val message = getReplyMessage(intent) val message = getReplyMessage(intent)
val roomId = intent.getStringExtra(KEY_ROOM_ID) val roomId = intent.getStringExtra(KEY_ROOM_ID)
val threadId = intent.getStringExtra(KEY_THREAD_ID)
if (message.isNullOrBlank() || roomId.isNullOrBlank()) { if (message.isNullOrBlank() || roomId.isNullOrBlank()) {
// ignore this event // ignore this event
@ -126,13 +127,20 @@ class NotificationBroadcastReceiver : BroadcastReceiver() {
} }
activeSessionHolder.getActiveSession().let { session -> activeSessionHolder.getActiveSession().let { session ->
session.getRoom(roomId)?.let { room -> 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?) { 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) room.sendService().sendTextMessage(message)
}
// Create a new event to be displayed in the notification drawer, right now // Create a new event to be displayed in the notification drawer, right now
@ -148,7 +156,7 @@ class NotificationBroadcastReceiver : BroadcastReceiver() {
body = message, body = message,
imageUriString = null, imageUriString = null,
roomId = room.roomId, 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, roomName = room.roomSummary()?.displayName ?: room.roomId,
roomIsDirect = room.roomSummary()?.isDirect == true, roomIsDirect = room.roomSummary()?.isDirect == true,
outGoingMessage = true, outGoingMessage = true,
@ -223,6 +231,7 @@ class NotificationBroadcastReceiver : BroadcastReceiver() {
companion object { companion object {
const val KEY_ROOM_ID = "roomID" const val KEY_ROOM_ID = "roomID"
const val KEY_THREAD_ID = "threadID"
const val KEY_TEXT_REPLY = "key_text_reply" const val KEY_TEXT_REPLY = "key_text_reply"
} }
} }

View file

@ -657,7 +657,7 @@ class NotificationUtils @Inject constructor(
// Quick reply // Quick reply
if (!roomInfo.hasSmartReplyError) { if (!roomInfo.hasSmartReplyError) {
buildQuickReplyIntent(roomInfo.roomId, senderDisplayNameForReplyCompat)?.let { replyPendingIntent -> buildQuickReplyIntent(roomInfo.roomId, threadId, senderDisplayNameForReplyCompat)?.let { replyPendingIntent ->
val remoteInput = RemoteInput.Builder(NotificationBroadcastReceiver.KEY_TEXT_REPLY) val remoteInput = RemoteInput.Builder(NotificationBroadcastReceiver.KEY_TEXT_REPLY)
.setLabel(stringProvider.getString(R.string.action_quick_reply)) .setLabel(stringProvider.getString(R.string.action_quick_reply))
.build() .build()
@ -892,13 +892,17 @@ class NotificationUtils @Inject constructor(
However, for Android devices running Marshmallow and below (API level 23 and below), 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. 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 val intent: Intent
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
intent = Intent(context, NotificationBroadcastReceiver::class.java) intent = Intent(context, NotificationBroadcastReceiver::class.java)
intent.action = actionIds.smartReply intent.action = actionIds.smartReply
intent.data = createIgnoredUri(roomId) intent.data = createIgnoredUri(roomId)
intent.putExtra(NotificationBroadcastReceiver.KEY_ROOM_ID, roomId) intent.putExtra(NotificationBroadcastReceiver.KEY_ROOM_ID, roomId)
threadId?.let {
intent.putExtra(NotificationBroadcastReceiver.KEY_THREAD_ID, it)
}
return PendingIntent.getBroadcast( return PendingIntent.getBroadcast(
context, context,
clock.epochMillis().toInt(), clock.epochMillis().toInt(),

View file

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

View file

@ -31,12 +31,11 @@ class VectorSettingsDevicesViewNavigator @Inject constructor() {
fun navigateToOtherSessions( fun navigateToOtherSessions(
context: Context, context: Context,
titleResourceId: Int,
defaultFilter: DeviceManagerFilterType, defaultFilter: DeviceManagerFilterType,
excludeCurrentDevice: Boolean, excludeCurrentDevice: Boolean,
) { ) {
context.startActivity( 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.epoxy.VectorEpoxyModel
import im.vector.app.core.utils.DimensionConverter import im.vector.app.core.utils.DimensionConverter
private const val EXTRA_TOP_MARGIN_DP = 48 private const val EXTRA_TOP_MARGIN_DP = 32
@EpoxyModelClass @EpoxyModelClass
abstract class SessionDetailsHeaderItem : VectorEpoxyModel<SessionDetailsHeaderItem.Holder>(R.layout.item_session_details_header) { abstract class SessionDetailsHeaderItem : VectorEpoxyModel<SessionDetailsHeaderItem.Holder>(R.layout.item_session_details_header) {

View file

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

View file

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

View file

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

View file

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

View file

@ -20,7 +20,6 @@ import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.view.View import android.view.View
import androidx.annotation.StringRes
import com.airbnb.mvrx.Mavericks import com.airbnb.mvrx.Mavericks
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import im.vector.app.core.extensions.addFragment import im.vector.app.core.extensions.addFragment
@ -48,13 +47,11 @@ class OtherSessionsActivity : SimpleFragmentActivity() {
companion object { companion object {
fun newIntent( fun newIntent(
context: Context, context: Context,
@StringRes
titleResourceId: Int,
defaultFilter: DeviceManagerFilterType, defaultFilter: DeviceManagerFilterType,
excludeCurrentDevice: Boolean, excludeCurrentDevice: Boolean,
): Intent { ): Intent {
return Intent(context, OtherSessionsActivity::class.java).apply { 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 package im.vector.app.features.settings.devices.v2.othersessions
import android.os.Parcelable import android.os.Parcelable
import androidx.annotation.StringRes
import im.vector.app.features.settings.devices.v2.filter.DeviceManagerFilterType import im.vector.app.features.settings.devices.v2.filter.DeviceManagerFilterType
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
@Parcelize @Parcelize
data class OtherSessionsArgs( data class OtherSessionsArgs(
@StringRes
val titleResourceId: Int,
val defaultFilter: DeviceManagerFilterType, val defaultFilter: DeviceManagerFilterType,
val excludeCurrentDevice: Boolean, val excludeCurrentDevice: Boolean,
) : Parcelable ) : Parcelable

View file

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

View file

@ -224,7 +224,7 @@ class SessionOverviewFragment :
isVerifyButtonVisible = isCurrentSession || viewState.isCurrentSessionTrusted, isVerifyButtonVisible = isCurrentSession || viewState.isCurrentSessionTrusted,
isDetailsButtonVisible = false, isDetailsButtonVisible = false,
isLearnMoreLinkVisible = deviceInfo.roomEncryptionTrustLevel != RoomEncryptionTrustLevel.Default, isLearnMoreLinkVisible = deviceInfo.roomEncryptionTrustLevel != RoomEncryptionTrustLevel.Default,
isLastSeenDetailsVisible = !isCurrentSession, isLastActivityVisible = !isCurrentSession,
isShowingIpAddress = viewState.isShowingIpAddress, isShowingIpAddress = viewState.isShowingIpAddress,
) )
views.sessionOverviewInfo.render(infoViewState, dateFormatter, drawableProvider, colorProvider, stringProvider) 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.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.view.ViewTreeObserver
import androidx.core.widget.doOnTextChanged import androidx.core.widget.doOnTextChanged
import com.airbnb.mvrx.fragmentViewModel import com.airbnb.mvrx.fragmentViewModel
import com.airbnb.mvrx.withState import com.airbnb.mvrx.withState
@ -62,12 +63,24 @@ class RenameSessionFragment :
} }
private fun initEditText() { private fun initEditText() {
views.renameSessionEditText.showKeyboard(andRequestFocus = true) showKeyboard()
views.renameSessionEditText.doOnTextChanged { text, _, _, _ -> views.renameSessionEditText.doOnTextChanged { text, _, _, _ ->
viewModel.handle(RenameSessionAction.EditLocally(text.toString())) 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() { private fun initSaveButton() {
views.renameSessionSave.debouncedClicks { views.renameSessionSave.debouncedClicks {
viewModel.handle(RenameSessionAction.SaveModifications) viewModel.handle(RenameSessionAction.SaveModifications)
@ -89,7 +102,9 @@ class RenameSessionFragment :
title = getString(R.string.device_manager_learn_more_session_rename_title), title = getString(R.string.device_manager_learn_more_session_rename_title),
description = getString(R.string.device_manager_learn_more_session_rename), description = getString(R.string.device_manager_learn_more_session_rename),
) )
SessionLearnMoreBottomSheet.show(childFragmentManager, args) SessionLearnMoreBottomSheet
.show(childFragmentManager, args)
.onDismiss = { showKeyboard() }
} }
private fun observeViewEvents() { 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 android.util.TypedValue
import androidx.annotation.AttrRes import androidx.annotation.AttrRes
import androidx.annotation.ColorInt import androidx.annotation.ColorInt
import androidx.annotation.StyleRes
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.content.edit import androidx.core.content.edit
import androidx.core.graphics.drawable.DrawableCompat import androidx.core.graphics.drawable.DrawableCompat
@ -113,19 +114,16 @@ object ThemeUtils {
*/ */
fun setApplicationTheme(context: Context, aTheme: String) { fun setApplicationTheme(context: Context, aTheme: String) {
currentTheme.set(aTheme) currentTheme.set(aTheme)
context.setTheme( context.setTheme(themeToRes(context, aTheme))
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
}
)
// Clear the cache // Clear the cache
mColorByAttr.clear() 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 * Set the activity theme according to the selected one. Default is Light, so if this is the current
* theme, the theme is not changed. * theme, the theme is not changed.
@ -200,4 +198,13 @@ object ThemeUtils {
DrawableCompat.setTint(tinted, color) DrawableCompat.setTint(tinted, color)
return tinted 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) } listeners[voiceBroadcast.voiceBroadcastId] = CopyOnWriteArrayList<Listener>().apply { add(listener) }
} }
listener.onPlayingStateChanged(if (voiceBroadcast == currentVoiceBroadcast) playingState else State.IDLE) 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) { override fun removeListener(voiceBroadcast: VoiceBroadcast, listener: Listener) {
@ -373,11 +373,6 @@ class VoiceBroadcastPlayerImpl @Inject constructor(
} }
private fun onLiveListeningChanged(isLiveListening: Boolean) { 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 // Live has ended and last chunk has been reached, we can stop the playback
if (!isLiveListening && playingState == State.BUFFERING && playlist.currentSequence == mostRecentVoiceBroadcastEvent?.content?.lastChunkSequence) { if (!isLiveListening && playingState == State.BUFFERING && playlist.currentSequence == mostRecentVoiceBroadcastEvent?.content?.lastChunkSequence) {
stop() 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" <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="vertical" android:orientation="vertical">
android:paddingHorizontal="24dp"
android:paddingBottom="32dp">
<View <View
android:layout_width="36dp" android:layout_width="36dp"
@ -18,14 +16,22 @@
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="12dp" android:layout_marginTop="12dp"
android:paddingHorizontal="24dp"
android:text="@string/device_manager_filter_bottom_sheet_title" /> android:text="@string/device_manager_filter_bottom_sheet_title" />
<ScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
android:paddingHorizontal="24dp"
android:paddingBottom="32dp"
android:scrollbarStyle="outsideOverlay">
<RadioGroup <RadioGroup
android:id="@+id/filterOptionsRadioGroup" android:id="@+id/filterOptionsRadioGroup"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="24dp" android:paddingTop="24dp"
android:layoutDirection="rtl"
android:showDividers="none"> android:showDividers="none">
<RadioButton <RadioButton
@ -33,25 +39,34 @@
style="@style/TextAppearance.Vector.Subtitle.Medium.DevicesManagement" style="@style/TextAppearance.Vector.Subtitle.Medium.DevicesManagement"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="start"
android:background="?android:selectableItemBackground"
android:button="@null"
android:checked="true" android:checked="true"
android:drawableEnd="?android:attr/listChoiceIndicatorSingle"
android:minHeight="0dp" android:minHeight="0dp"
android:text="@string/device_manager_filter_option_all_sessions" /> android:text="@string/device_manager_filter_option_all_sessions"
android:textAlignment="textStart" />
<RadioButton <RadioButton
android:id="@+id/filterOptionVerifiedRadioButton" android:id="@+id/filterOptionVerifiedRadioButton"
style="@style/TextAppearance.Vector.Subtitle.Medium.DevicesManagement" style="@style/TextAppearance.Vector.Subtitle.Medium.DevicesManagement"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="start"
android:layout_marginTop="24dp" android:layout_marginTop="24dp"
android:background="?android:selectableItemBackground"
android:button="@null"
android:drawableEnd="?android:attr/listChoiceIndicatorSingle"
android:minHeight="0dp" android:minHeight="0dp"
android:text="@string/device_manager_filter_option_verified" /> android:text="@string/device_manager_filter_option_verified"
android:textAlignment="textStart" />
<TextView <TextView
android:id="@+id/filterOptionVerifiedTextView" android:id="@+id/filterOptionVerifiedTextView"
style="@style/TextAppearance.Vector.Body.DevicesManagement" style="@style/TextAppearance.Vector.Body.DevicesManagement"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="end"
android:text="@string/device_manager_filter_option_verified_description" /> android:text="@string/device_manager_filter_option_verified_description" />
<RadioButton <RadioButton
@ -59,16 +74,20 @@
style="@style/TextAppearance.Vector.Subtitle.Medium.DevicesManagement" style="@style/TextAppearance.Vector.Subtitle.Medium.DevicesManagement"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="start"
android:layout_marginTop="16dp" android:layout_marginTop="16dp"
android:background="?android:selectableItemBackground"
android:button="@null"
android:drawableEnd="?android:attr/listChoiceIndicatorSingle"
android:minHeight="0dp" android:minHeight="0dp"
android:text="@string/device_manager_filter_option_unverified" /> android:text="@string/device_manager_filter_option_unverified"
android:textAlignment="textStart" />
<TextView <TextView
android:id="@+id/filterOptionUnverifiedTextView" android:id="@+id/filterOptionUnverifiedTextView"
style="@style/TextAppearance.Vector.Body.DevicesManagement" style="@style/TextAppearance.Vector.Body.DevicesManagement"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="end"
android:text="@string/device_manager_filter_option_unverified_description" /> android:text="@string/device_manager_filter_option_unverified_description" />
<RadioButton <RadioButton
@ -76,17 +95,23 @@
style="@style/TextAppearance.Vector.Subtitle.Medium.DevicesManagement" style="@style/TextAppearance.Vector.Subtitle.Medium.DevicesManagement"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="start"
android:layout_marginTop="16dp" android:layout_marginTop="16dp"
android:background="?android:selectableItemBackground"
android:button="@null"
android:drawableEnd="?android:attr/listChoiceIndicatorSingle"
android:minHeight="0dp" android:minHeight="0dp"
android:text="@string/device_manager_filter_option_inactive" /> android:text="@string/device_manager_filter_option_inactive"
android:textAlignment="textStart" />
<TextView <TextView
android:id="@+id/filterOptionInactiveTextView" android:id="@+id/filterOptionInactiveTextView"
style="@style/TextAppearance.Vector.Body.DevicesManagement" style="@style/TextAppearance.Vector.Body.DevicesManagement"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content" />
android:layout_gravity="end" />
</RadioGroup> </RadioGroup>
</ScrollView>
</LinearLayout> </LinearLayout>

View file

@ -1,22 +1,101 @@
<?xml version="1.0" encoding="utf-8"?> <?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:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent">
<com.google.android.material.appbar.AppBarLayout <androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/appBarLayout"
android:layout_width="match_parent" 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_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_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="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">
<com.google.android.material.appbar.CollapsingToolbarLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_scrollFlags="scroll|exitUntilCollapsed"
app:titleEnabled="false"
app:toolbarId="@id/otherSessionsToolbar">
<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_marginTop="?attr/actionBarSize"
android:layout_marginBottom="16dp"
app:layout_collapseMode="parallax"
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="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 <com.google.android.material.appbar.MaterialToolbar
android:id="@+id/otherSessionsToolbar" android:id="@+id/otherSessionsToolbar"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="?attr/actionBarSize"
app:layout_collapseMode="pin"
app:navigationIcon="@drawable/ic_back_24dp" app:navigationIcon="@drawable/ic_back_24dp"
app:title="@string/device_manager_sessions_other_title"> app:title="@string/device_manager_sessions_other_title">
@ -46,75 +125,8 @@
</com.google.android.material.appbar.MaterialToolbar> </com.google.android.material.appbar.MaterialToolbar>
</com.google.android.material.appbar.CollapsingToolbarLayout>
</com.google.android.material.appbar.AppBarLayout> </com.google.android.material.appbar.AppBarLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
<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>

View file

@ -47,6 +47,7 @@
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginHorizontal="8dp" android:layout_marginHorizontal="8dp"
android:layout_marginTop="4dp"
android:text="@string/device_manager_session_overview_signout" android:text="@string/device_manager_session_overview_signout"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0" 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_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginHorizontal="16dp" android:layout_marginHorizontal="16dp"
android:layout_marginVertical="16dp" android:layout_marginVertical="4dp"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/deviceListHeaderCurrentSession" /> app:layout_constraintTop_toBottomOf="@id/deviceListHeaderCurrentSession" />

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -24,7 +24,7 @@
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginHorizontal="@dimen/layout_horizontal_margin" 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_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/sessions_list_header_title" 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.core.session.clientinfo.UpdateMatrixClientInfoUseCase
import im.vector.app.features.session.coroutineScope import im.vector.app.features.session.coroutineScope
import im.vector.app.features.settings.devices.v2.notification.UpdateNotificationSettingsAccountDataUseCase 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.FakeContext
import im.vector.app.test.fakes.FakeNotificationsSettingUpdater import im.vector.app.test.fakes.FakeNotificationsSettingUpdater
import im.vector.app.test.fakes.FakeSession import im.vector.app.test.fakes.FakeSession
@ -87,7 +86,6 @@ class ConfigureAndStartSessionUseCaseTest {
// Then // Then
verify { aSession.startSyncing(fakeContext.instance) } verify { aSession.startSyncing(fakeContext.instance) }
aSession.fakeFilterService.verifySetSyncFilter(SyncUtils.getSyncFilterBuilder())
aSession.fakePushersService.verifyRefreshPushers() aSession.fakePushersService.verifyRefreshPushers()
fakeWebRtcCallManager.verifyCheckForProtocolsSupportIfNeeded() fakeWebRtcCallManager.verifyCheckForProtocolsSupportIfNeeded()
coVerify { coVerify {
@ -112,7 +110,6 @@ class ConfigureAndStartSessionUseCaseTest {
// Then // Then
verify { aSession.startSyncing(fakeContext.instance) } verify { aSession.startSyncing(fakeContext.instance) }
aSession.fakeFilterService.verifySetSyncFilter(SyncUtils.getSyncFilterBuilder())
aSession.fakePushersService.verifyRefreshPushers() aSession.fakePushersService.verifyRefreshPushers()
fakeWebRtcCallManager.verifyCheckForProtocolsSupportIfNeeded() fakeWebRtcCallManager.verifyCheckForProtocolsSupportIfNeeded()
coVerify(inverse = true) { coVerify(inverse = true) {
@ -140,7 +137,6 @@ class ConfigureAndStartSessionUseCaseTest {
// Then // Then
verify(inverse = true) { aSession.startSyncing(fakeContext.instance) } verify(inverse = true) { aSession.startSyncing(fakeContext.instance) }
aSession.fakeFilterService.verifySetSyncFilter(SyncUtils.getSyncFilterBuilder())
aSession.fakePushersService.verifyRefreshPushers() aSession.fakePushersService.verifyRefreshPushers()
fakeWebRtcCallManager.verifyCheckForProtocolsSupportIfNeeded() fakeWebRtcCallManager.verifyCheckForProtocolsSupportIfNeeded()
coVerify { coVerify {
@ -152,7 +148,6 @@ class ConfigureAndStartSessionUseCaseTest {
private fun givenASession(): FakeSession { private fun givenASession(): FakeSession {
val fakeSession = FakeSession() val fakeSession = FakeSession()
every { fakeSession.open() } just runs every { fakeSession.open() } just runs
fakeSession.fakeFilterService.givenSetFilterSucceeds()
every { fakeSession.startSyncing(any()) } just runs every { fakeSession.startSyncing(any()) } just runs
fakeSession.fakePushersService.givenRefreshPushersSucceeds() fakeSession.fakePushersService.givenRefreshPushersSucceeds()
return fakeSession return fakeSession

View file

@ -63,6 +63,11 @@ class DeleteMatrixClientInfoUseCaseTest {
// Given // Given
val error = Exception() val error = Exception()
givenSetMatrixClientInfoFails(error) givenSetMatrixClientInfoFails(error)
val expectedClientInfoToBeSet = MatrixClientInfoContent(
name = "",
version = "",
url = "",
)
// When // When
val result = deleteMatrixClientInfoUseCase.execute() val result = deleteMatrixClientInfoUseCase.execute()
@ -70,6 +75,12 @@ class DeleteMatrixClientInfoUseCaseTest {
// Then // Then
result.isFailure shouldBe true result.isFailure shouldBe true
result.exceptionOrNull() shouldBeEqualTo error result.exceptionOrNull() shouldBeEqualTo error
coVerify {
fakeSetMatrixClientInfoUseCase.execute(
fakeActiveSessionHolder.fakeSession,
expectedClientInfoToBeSet
)
}
} }
private fun givenSetMatrixClientInfoSucceeds() { 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.CryptoDeviceInfo
import org.matrix.android.sdk.api.session.crypto.model.DeviceInfo 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.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 import org.matrix.android.sdk.api.session.uia.DefaultBaseAuth
private const val A_CURRENT_DEVICE_ID = "current-device-id" private const val A_CURRENT_DEVICE_ID = "current-device-id"
@ -76,6 +78,10 @@ class DevicesViewModelTest {
private val fakeVectorPreferences = FakeVectorPreferences() private val fakeVectorPreferences = FakeVectorPreferences()
private val toggleIpAddressVisibilityUseCase = mockk<ToggleIpAddressVisibilityUseCase>() private val toggleIpAddressVisibilityUseCase = mockk<ToggleIpAddressVisibilityUseCase>()
private val verifiedTransaction = mockk<VerificationTransaction>().apply {
every { state } returns VerificationTxState.Verified
}
private fun createViewModel(): DevicesViewModel { private fun createViewModel(): DevicesViewModel {
return DevicesViewModel( return DevicesViewModel(
initialState = DevicesViewState(), initialState = DevicesViewState(),
@ -375,6 +381,18 @@ class DevicesViewModelTest {
viewModelTest.finish() 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 { private fun givenCurrentSessionCrossSigningInfo(): CurrentSessionCrossSigningInfo {
val currentSessionCrossSigningInfo = mockk<CurrentSessionCrossSigningInfo>() val currentSessionCrossSigningInfo = mockk<CurrentSessionCrossSigningInfo>()
every { currentSessionCrossSigningInfo.deviceId } returns A_CURRENT_DEVICE_ID 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 import org.junit.Test
private const val A_SESSION_ID = "session_id" private const val A_SESSION_ID = "session_id"
private const val A_TITLE_RESOURCE_ID = 1234
private val A_DEFAULT_FILTER = DeviceManagerFilterType.INACTIVE private val A_DEFAULT_FILTER = DeviceManagerFilterType.INACTIVE
class VectorSettingsDevicesViewNavigatorTest { class VectorSettingsDevicesViewNavigatorTest {
@ -67,11 +66,11 @@ class VectorSettingsDevicesViewNavigatorTest {
@Test @Test
fun `given an intent when navigating to other sessions list then it starts the correct activity`() { fun `given an intent when navigating to other sessions list then it starts the correct activity`() {
// Given // Given
val intent = givenIntentForOtherSessions(A_TITLE_RESOURCE_ID, A_DEFAULT_FILTER, true) val intent = givenIntentForOtherSessions(A_DEFAULT_FILTER, true)
context.givenStartActivity(intent) context.givenStartActivity(intent)
// When // When
vectorSettingsDevicesViewNavigator.navigateToOtherSessions(context.instance, A_TITLE_RESOURCE_ID, A_DEFAULT_FILTER, true) vectorSettingsDevicesViewNavigator.navigateToOtherSessions(context.instance, A_DEFAULT_FILTER, true)
// Then // Then
context.verifyStartActivity(intent) context.verifyStartActivity(intent)
@ -96,9 +95,9 @@ class VectorSettingsDevicesViewNavigatorTest {
return intent return intent
} }
private fun givenIntentForOtherSessions(titleResourceId: Int, defaultFilter: DeviceManagerFilterType, excludeCurrentDevice: Boolean): Intent { private fun givenIntentForOtherSessions(defaultFilter: DeviceManagerFilterType, excludeCurrentDevice: Boolean): Intent {
val intent = mockk<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 return intent
} }

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