mirror of
https://github.com/SchildiChat/SchildiChat-android.git
synced 2024-11-28 22:18:46 +03:00
Merge branch 'release/1.5.16' into main
This commit is contained in:
commit
133fff1a8d
103 changed files with 1886 additions and 562 deletions
31
CHANGES.md
31
CHANGES.md
|
@ -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)
|
||||||
=======================================
|
=======================================
|
||||||
|
|
||||||
|
|
|
@ -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 : [
|
||||||
|
|
2
fastlane/metadata/android/en-US/changelogs/40105160.txt
Normal file
2
fastlane/metadata/android/en-US/changelogs/40105160.txt
Normal 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
|
|
@ -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. You’ll 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. You’ll 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. You’ll 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. You’ll 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 don’t use anymore.</item>
|
<item quantity="one">Consider signing out from old sessions (%1$d day or more) that you don’t use anymore.</item>
|
||||||
<item quantity="other">Consider signing out from old sessions (%1$d days or more) that you don’t use anymore.</item>
|
<item quantity="other">Consider signing out from old sessions (%1$d days or more) that you don’t 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>
|
||||||
|
|
|
@ -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()}\""
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()
|
||||||
)
|
)
|
||||||
|
|
|
@ -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.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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,
|
|
@ -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.
|
||||||
*
|
*
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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) }
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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")
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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)
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
|
@ -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,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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)
|
|
@ -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())
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)
|
||||||
|
|
|
@ -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) }
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)
|
|
||||||
}
|
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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": {
|
||||||
|
|
|
@ -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'
|
||||||
|
|
|
@ -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()
|
||||||
|
|
||||||
|
|
|
@ -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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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))
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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<*, *>
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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() {
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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?)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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) }
|
|
||||||
}
|
|
||||||
}
|
}
|
|
@ -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()
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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()
|
||||||
|
}
|
|
@ -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))
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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)
|
||||||
|
|
|
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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(),
|
||||||
|
|
|
@ -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
|
||||||
)
|
)
|
||||||
|
|
|
@ -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)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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,
|
||||||
)
|
)
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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() {
|
||||||
|
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()
|
||||||
|
|
12
vector/src/main/res/drawable/ic_composer_link.xml
Normal file
12
vector/src/main/res/drawable/ic_composer_link.xml
Normal 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>
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
|
||||||
|
|
|
@ -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"
|
||||||
|
|
117
vector/src/main/res/layout/fragment_set_link.xml
Normal file
117
vector/src/main/res/layout/fragment_set_link.xml
Normal 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>
|
|
@ -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" />
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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" />
|
||||||
|
|
|
@ -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">
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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
|
@ -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
|
||||||
|
|
|
@ -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() {
|
||||||
|
|
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
Loading…
Reference in a new issue