Merge branch 'develop' into feature/bca/rust_flavor

This commit is contained in:
valere 2022-12-16 09:33:37 +01:00
commit 3146f5209b
90 changed files with 856 additions and 292 deletions

View file

@ -1,3 +1,54 @@
Changes in Element v1.5.12 (2022-12-15)
=======================================
Features ✨
----------
- [Threads] - Threads Labs Flag is enabled by default and forced to be enabled for existing users, but sill can be disabled manually ([#5503](https://github.com/vector-im/element-android/issues/5503))
- [Session manager] Add action to signout all the other session ([#7693](https://github.com/vector-im/element-android/issues/7693))
- Remind unverified sessions with a banner once a week ([#7694](https://github.com/vector-im/element-android/issues/7694))
- [Session manager] Add actions to rename and signout current session ([#7697](https://github.com/vector-im/element-android/issues/7697))
- Voice Broadcast - Update last message in the room list ([#7719](https://github.com/vector-im/element-android/issues/7719))
- Delete unused client information from account data ([#7754](https://github.com/vector-im/element-android/issues/7754))
Bugfixes 🐛
----------
- Fix bad pills color background. For light and dark theme the color is now 61708B (iso EleWeb) ([#7274](https://github.com/vector-im/element-android/issues/7274))
- [Notifications] Fixed a bug when push notification was automatically dismissed while app is on background ([#7643](https://github.com/vector-im/element-android/issues/7643))
- ANR when asking to select the notification method ([#7653](https://github.com/vector-im/element-android/issues/7653))
- [Rich text editor] Fix design and spacing of rich text editor ([#7658](https://github.com/vector-im/element-android/issues/7658))
- [Rich text editor] Fix keyboard closing after collapsing editor ([#7659](https://github.com/vector-im/element-android/issues/7659))
- Rich Text Editor: fix several issues related to insets:
* Empty space displayed at the bottom when you don't have permissions to send messages into a room.
* Wrong insets being kept when you exit the room screen and the keyboard is displayed, then come back to it. ([#7680](https://github.com/vector-im/element-android/issues/7680))
- Fix crash in message composer when room is missing ([#7683](https://github.com/vector-im/element-android/issues/7683))
- Fix crash when invalid homeserver url is entered. ([#7684](https://github.com/vector-im/element-android/issues/7684))
- Rich Text Editor: improve performance when entering reply/edit/quote mode. ([#7691](https://github.com/vector-im/element-android/issues/7691))
- [Rich text editor] Add error tracking for rich text editor ([#7695](https://github.com/vector-im/element-android/issues/7695))
- Fix E2EE set up failure whilst signing in using QR code ([#7699](https://github.com/vector-im/element-android/issues/7699))
- Fix usage of unknown shield in room summary ([#7710](https://github.com/vector-im/element-android/issues/7710))
- Fix crash when the network is not available. ([#7725](https://github.com/vector-im/element-android/issues/7725))
- [Session manager] Sessions without encryption support should not prompt to verify ([#7733](https://github.com/vector-im/element-android/issues/7733))
- Fix issue of Scan QR code button sometimes not showing when it should be available ([#7737](https://github.com/vector-im/element-android/issues/7737))
- Verification request is not showing when verify session popup is displayed ([#7743](https://github.com/vector-im/element-android/issues/7743))
- Fix crash when inviting by email. ([#7744](https://github.com/vector-im/element-android/issues/7744))
- Revert usage of stable fields in live location sharing and polls ([#7751](https://github.com/vector-im/element-android/issues/7751))
- [Poll] Poll end event is not recognized ([#7753](https://github.com/vector-im/element-android/issues/7753))
- [Push Notifications] When push notification for threaded message is clicked, thread timeline will be opened instead of room's main timeline ([#7770](https://github.com/vector-im/element-android/issues/7770))
Other changes
-------------
- [Threads] - added API to fetch threads list from the server instead of building it locally from events ([#5819](https://github.com/vector-im/element-android/issues/5819))
- Add Z-Labs label for rich text editor and migrate to new label naming. ([#7477](https://github.com/vector-im/element-android/issues/7477))
- Crypto database migration tests ([#7645](https://github.com/vector-im/element-android/issues/7645))
- Add tracing Id for to device messages ([#7708](https://github.com/vector-im/element-android/issues/7708))
- Disable nightly popup and add an entry point in the advanced settings instead. ([#7723](https://github.com/vector-im/element-android/issues/7723))
- Save m.local_notification_settings.<device-id> event in account_data ([#7596](https://github.com/vector-im/element-android/issues/7596))
- Update notifications setting when m.local_notification_settings.<device-id> event changes for current device ([#7632](https://github.com/vector-im/element-android/issues/7632))
SDK API changes ⚠️
------------------
- Handle account data removal ([#7740](https://github.com/vector-im/element-android/issues/7740))
Changes in Element 1.5.11 (2022-12-07) Changes in Element 1.5.11 (2022-12-07)
====================================== ======================================

View file

@ -29,7 +29,7 @@ buildscript {
classpath 'org.sonarsource.scanner.gradle:sonarqube-gradle-plugin:3.5.0.2730' classpath 'org.sonarsource.scanner.gradle:sonarqube-gradle-plugin:3.5.0.2730'
classpath 'com.google.android.gms:oss-licenses-plugin:0.10.5' classpath 'com.google.android.gms:oss-licenses-plugin:0.10.5'
classpath "com.likethesalad.android:stem-plugin:2.2.3" classpath "com.likethesalad.android:stem-plugin:2.2.3"
classpath 'org.owasp:dependency-check-gradle:7.3.0' classpath 'org.owasp:dependency-check-gradle:7.4.1'
classpath "org.jetbrains.dokka:dokka-gradle-plugin:1.7.20" classpath "org.jetbrains.dokka:dokka-gradle-plugin:1.7.20"
classpath "org.jetbrains.kotlinx:kotlinx-knit:0.4.0" classpath "org.jetbrains.kotlinx:kotlinx-knit:0.4.0"
classpath 'com.jakewharton:butterknife-gradle-plugin:10.2.3' classpath 'com.jakewharton:butterknife-gradle-plugin:10.2.3'

View file

@ -1 +0,0 @@
Fix bad pills color background. For light and dark theme the color is now 61708B (iso EleWeb)

View file

@ -1 +0,0 @@
Add Z-Labs label for rich text editor and migrate to new label naming.

View file

@ -1 +0,0 @@
Save m.local_notification_settings.<device-id> event in account_data

View file

@ -1 +0,0 @@
Update notifications setting when m.local_notification_settings.<device-id> event changes for current device

View file

@ -1 +0,0 @@
[Notifications] Fixed a bug when push notification was automatically dismissed while app is on background

View file

@ -1 +0,0 @@
Crypto database migration tests

View file

@ -1 +0,0 @@
ANR when asking to select the notification method

View file

@ -1 +0,0 @@
[Rich text editor] Fix design and spacing of rich text editor

View file

@ -1 +0,0 @@
[Rich text editor] Fix keyboard closing after collapsing editor

View file

@ -1,3 +0,0 @@
Rich Text Editor: fix several issues related to insets:
* Empty space displayed at the bottom when you don't have permissions to send messages into a room.
* Wrong insets being kept when you exit the room screen and the keyboard is displayed, then come back to it.

View file

@ -1,2 +0,0 @@
Fix crash in message composer when room is missing

View file

@ -1 +0,0 @@
Fix crash when invalid homeserver url is entered.

View file

@ -1 +0,0 @@
Rich Text Editor: improve performance when entering reply/edit/quote mode.

View file

@ -1 +0,0 @@
[Session manager] Add action to signout all the other session

View file

@ -1 +0,0 @@
Remind unverified sessions with a banner once a week

View file

@ -1 +0,0 @@
[Rich text editor] Add error tracking for rich text editor

View file

@ -1 +0,0 @@
[Session manager] Add actions to rename and signout current session

View file

@ -1 +0,0 @@
Fix E2EE set up failure whilst signing in using QR code

View file

@ -1 +0,0 @@
Add tracing Id for to device messages

View file

@ -1 +0,0 @@
Fix usage of unknown shield in room summary

View file

@ -1 +0,0 @@
Disable nightly popup and add an entry point in the advanced settings instead.

View file

@ -1 +0,0 @@
Fix crash when the network is not available.

View file

@ -1 +0,0 @@
[Session manager] Sessions without encryption support should not prompt to verify

View file

@ -1 +0,0 @@
Fix issue of Scan QR code button sometimes not showing when it should be available

View file

@ -1 +0,0 @@
Handle account data removal

View file

@ -1 +0,0 @@
Verification request is not showing when verify session popup is displayed

View file

@ -1 +0,0 @@
Fix crash when inviting by email.

View file

@ -1 +0,0 @@
Revert usage of stable fields in live location sharing and polls

View file

@ -1 +0,0 @@
Delete unused client information from account data

View file

@ -1 +0,0 @@
[Push Notifications] When push notification for threaded message is clicked, thread timeline will be opened instead of room's main timeline

View file

@ -26,7 +26,7 @@ def jjwt = "0.11.5"
// Temporary version to unblock #6929. Once 0.16.0 is released we should use it, and revert // Temporary version to unblock #6929. Once 0.16.0 is released we should use it, and revert
// the whole commit which set version 0.16.0-SNAPSHOT // the whole commit which set version 0.16.0-SNAPSHOT
def vanniktechEmoji = "0.16.0-SNAPSHOT" def vanniktechEmoji = "0.16.0-SNAPSHOT"
def sentry = "6.9.0" def sentry = "6.9.2"
def fragment = "1.5.5" def fragment = "1.5.5"
// Testing // Testing
def mockk = "1.12.3" // We need to use 1.12.3 to have mocking in androidTest until a new version is released: https://github.com/mockk/mockk/issues/819 def mockk = "1.12.3" // We need to use 1.12.3 to have mocking in androidTest until a new version is released: https://github.com/mockk/mockk/issues/819

View file

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

View file

@ -134,6 +134,9 @@
<string name="notice_crypto_unable_to_decrypt">** Unable to decrypt: %s **</string> <string name="notice_crypto_unable_to_decrypt">** Unable to decrypt: %s **</string>
<string name="notice_crypto_error_unknown_inbound_session_id">The sender\'s device has not sent us the keys for this message.</string> <string name="notice_crypto_error_unknown_inbound_session_id">The sender\'s device has not sent us the keys for this message.</string>
<string name="notice_voice_broadcast_ended">%1$s ended a voice broadcast.</string>
<string name="notice_voice_broadcast_ended_by_you">You ended a voice broadcast.</string>
<!-- Messages --> <!-- Messages -->
<!-- Room Screen --> <!-- Room Screen -->
@ -3039,7 +3042,7 @@
<string name="labs_auto_report_uisi">Auto Report Decryption Errors.</string> <string name="labs_auto_report_uisi">Auto Report Decryption Errors.</string>
<string name="labs_auto_report_uisi_desc">Your system will automatically send logs when an unable to decrypt error occurs</string> <string name="labs_auto_report_uisi_desc">Your system will automatically send logs when an unable to decrypt error occurs</string>
<string name="labs_enable_thread_messages">Enable Thread Messages</string> <string name="labs_enable_thread_messages">Enable threaded messages</string>
<string name="labs_enable_thread_messages_desc">Note: app will be restarted</string> <string name="labs_enable_thread_messages_desc">Note: app will be restarted</string>
<string name="settings_show_latest_profile">Show latest user info</string> <string name="settings_show_latest_profile">Show latest user info</string>
<string name="settings_show_latest_profile_description">Show the latest profile info (avatar and display name) for all the messages.</string> <string name="settings_show_latest_profile_description">Show the latest profile info (avatar and display name) for all the messages.</string>
@ -3108,6 +3111,7 @@
<string name="audio_message_file_size">(%1$s)</string> <string name="audio_message_file_size">(%1$s)</string>
<string name="voice_broadcast_live">Live</string> <string name="voice_broadcast_live">Live</string>
<string name="voice_broadcast_live_broadcast">Live broadcast</string>
<!-- TODO Rename id to voice_broadcast_buffering --> <!-- TODO Rename id to voice_broadcast_buffering -->
<string name="a11y_voice_broadcast_buffering">Buffering…</string> <string name="a11y_voice_broadcast_buffering">Buffering…</string>
<string name="a11y_resume_voice_broadcast_record">Resume voice broadcast record</string> <string name="a11y_resume_voice_broadcast_record">Resume voice broadcast record</string>

View file

@ -31,7 +31,6 @@ import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary
import org.matrix.android.sdk.api.session.room.model.RoomSummary import org.matrix.android.sdk.api.session.room.model.RoomSummary
import org.matrix.android.sdk.api.session.room.notification.RoomNotificationState import org.matrix.android.sdk.api.session.room.notification.RoomNotificationState
import org.matrix.android.sdk.api.session.room.send.UserDraft import org.matrix.android.sdk.api.session.room.send.UserDraft
import org.matrix.android.sdk.api.session.room.threads.model.ThreadSummary
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.util.Optional import org.matrix.android.sdk.api.util.Optional
import org.matrix.android.sdk.api.util.toOptional import org.matrix.android.sdk.api.util.toOptional
@ -119,13 +118,6 @@ class FlowRoom(private val room: Room) {
return room.roomPushRuleService().getLiveRoomNotificationState().asFlow() return room.roomPushRuleService().getLiveRoomNotificationState().asFlow()
} }
fun liveThreadSummaries(): Flow<List<ThreadSummary>> {
return room.threadsService().getAllThreadSummariesLive().asFlow()
.startWith(room.coroutineDispatchers.io) {
room.threadsService().getAllThreadSummaries()
}
}
fun liveThreadList(): Flow<List<ThreadRootEvent>> { fun liveThreadList(): Flow<List<ThreadRootEvent>> {
return room.threadsLocalService().getAllThreadsLive().asFlow() return room.threadsLocalService().getAllThreadsLive().asFlow()
.startWith(room.coroutineDispatchers.io) { .startWith(room.coroutineDispatchers.io) {

View file

@ -63,7 +63,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.12\"" buildConfigField "String", "SDK_VERSION", "\"1.5.14\""
buildConfigField "String", "GIT_SDK_REVISION", "\"${gitRevision()}\"" buildConfigField "String", "GIT_SDK_REVISION", "\"${gitRevision()}\""
buildConfigField "String", "GIT_SDK_REVISION_UNIX_DATE", "\"${gitRevisionUnixDate()}\"" buildConfigField "String", "GIT_SDK_REVISION_UNIX_DATE", "\"${gitRevisionUnixDate()}\""

View file

@ -16,7 +16,7 @@
package org.matrix.android.sdk.common package org.matrix.android.sdk.common
import org.matrix.android.sdk.api.RoomDisplayNameFallbackProvider import org.matrix.android.sdk.api.provider.RoomDisplayNameFallbackProvider
class TestRoomDisplayNameFallbackProvider : RoomDisplayNameFallbackProvider { class TestRoomDisplayNameFallbackProvider : RoomDisplayNameFallbackProvider {

View file

@ -21,6 +21,9 @@ import okhttp3.Interceptor
import org.matrix.android.sdk.api.crypto.MXCryptoConfig import org.matrix.android.sdk.api.crypto.MXCryptoConfig
import org.matrix.android.sdk.api.metrics.CryptoMetricPlugin import org.matrix.android.sdk.api.metrics.CryptoMetricPlugin
import org.matrix.android.sdk.api.metrics.MetricPlugin import org.matrix.android.sdk.api.metrics.MetricPlugin
import org.matrix.android.sdk.api.provider.CustomEventTypesProvider
import org.matrix.android.sdk.api.provider.MatrixItemDisplayNameFallbackProvider
import org.matrix.android.sdk.api.provider.RoomDisplayNameFallbackProvider
import java.net.Proxy import java.net.Proxy
data class MatrixConfiguration( data class MatrixConfiguration(
@ -68,7 +71,7 @@ data class MatrixConfiguration(
/** /**
* Thread messages default enable/disabled value. * Thread messages default enable/disabled value.
*/ */
val threadMessagesEnabledDefault: Boolean = false, val threadMessagesEnabledDefault: Boolean = true,
/** /**
* List of network interceptors, they will be added when building an OkHttp client. * List of network interceptors, they will be added when building an OkHttp client.
*/ */
@ -77,11 +80,14 @@ data class MatrixConfiguration(
* Sync configuration. * Sync configuration.
*/ */
val syncConfig: SyncConfig = SyncConfig(), val syncConfig: SyncConfig = SyncConfig(),
/** /**
* Metrics plugin that can be used to capture metrics from matrix-sdk-android. * Metrics plugin that can be used to capture metrics from matrix-sdk-android.
*/ */
val metricPlugins: List<MetricPlugin> = emptyList(), val metricPlugins: List<MetricPlugin> = emptyList(),
val cryptoAnalyticsPlugin: CryptoMetricPlugin? = null val cryptoAnalyticsPlugin: CryptoMetricPlugin? = null,
/**
* CustomEventTypesProvider to provide custom event types to the sdk which should be processed with internal events.
*/
val customEventTypesProvider: CustomEventTypesProvider? = null,
) )

View file

@ -0,0 +1,30 @@
/*
* Copyright 2022 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.api.provider
import org.matrix.android.sdk.api.session.room.model.RoomSummary
/**
* Provide custom event types which should be processed with the internal event types.
*/
interface CustomEventTypesProvider {
/**
* Custom event types to include when computing [RoomSummary.latestPreviewableEvent].
*/
val customPreviewableEventTypes: List<String>
}

View file

@ -14,7 +14,7 @@
* limitations under the License. * limitations under the License.
*/ */
package org.matrix.android.sdk.api package org.matrix.android.sdk.api.provider
import org.matrix.android.sdk.api.util.MatrixItem import org.matrix.android.sdk.api.util.MatrixItem

View file

@ -14,7 +14,7 @@
* limitations under the License. * limitations under the License.
*/ */
package org.matrix.android.sdk.api package org.matrix.android.sdk.api.provider
/** /**
* This interface exists to let the implementation provide localized room display name fallback. * This interface exists to let the implementation provide localized room display name fallback.

View file

@ -0,0 +1,23 @@
/*
* Copyright 2022 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.api.session.room.threads
sealed class FetchThreadsResult {
data class ShouldFetchMore(val nextBatch: String) : FetchThreadsResult()
object ReachedEnd : FetchThreadsResult()
object Failed : FetchThreadsResult()
}

View file

@ -0,0 +1,26 @@
/*
* Copyright 2022 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.api.session.room.threads
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = false)
enum class ThreadFilter {
@Json(name = "all") ALL,
@Json(name = "participated") PARTICIPATED,
}

View file

@ -0,0 +1,27 @@
/*
* Copyright (c) 2021 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.api.session.room.threads
import androidx.lifecycle.LiveData
import androidx.paging.PagedList
import org.matrix.android.sdk.api.session.room.ResultBoundaries
import org.matrix.android.sdk.api.session.room.threads.model.ThreadSummary
data class ThreadLivePageResult(
val livePagedList: LiveData<PagedList<ThreadSummary>>,
val liveBoundaries: LiveData<ResultBoundaries>
)

View file

@ -16,7 +16,7 @@
package org.matrix.android.sdk.api.session.room.threads package org.matrix.android.sdk.api.session.room.threads
import androidx.lifecycle.LiveData import androidx.paging.PagedList
import org.matrix.android.sdk.api.session.room.threads.model.ThreadSummary import org.matrix.android.sdk.api.session.room.threads.model.ThreadSummary
/** /**
@ -27,15 +27,14 @@ import org.matrix.android.sdk.api.session.room.threads.model.ThreadSummary
*/ */
interface ThreadsService { interface ThreadsService {
/** suspend fun getPagedThreadsList(userParticipating: Boolean, pagedListConfig: PagedList.Config): ThreadLivePageResult
* Returns a [LiveData] list of all the [ThreadSummary] that exists at the room level.
*/ suspend fun fetchThreadList(nextBatchId: String?, limit: Int, filter: ThreadFilter = ThreadFilter.ALL): FetchThreadsResult
fun getAllThreadSummariesLive(): LiveData<List<ThreadSummary>>
/** /**
* Returns a list of all the [ThreadSummary] that exists at the room level. * Returns a list of all the [ThreadSummary] that exists at the room level.
*/ */
fun getAllThreadSummaries(): List<ThreadSummary> suspend fun getAllThreadSummaries(): List<ThreadSummary>
/** /**
* Enhance the provided ThreadSummary[List] by adding the latest * Enhance the provided ThreadSummary[List] by adding the latest
@ -51,9 +50,4 @@ interface ThreadsService {
* @param limit defines the number of max results the api will respond with * @param limit defines the number of max results the api will respond with
*/ */
suspend fun fetchThreadTimeline(rootThreadEventId: String, from: String, limit: Int) suspend fun fetchThreadTimeline(rootThreadEventId: String, from: String, limit: Int)
/**
* Fetch all thread summaries for the current room using the enhanced /messages api.
*/
suspend fun fetchThreadSummaries()
} }

View file

@ -62,6 +62,7 @@ import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo042
import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo043 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.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
@ -70,7 +71,7 @@ internal class RealmSessionStoreMigration @Inject constructor(
private val normalizer: Normalizer private val normalizer: Normalizer
) : MatrixRealmMigration( ) : MatrixRealmMigration(
dbName = "Session", dbName = "Session",
schemaVersion = 45L, schemaVersion = 46L,
) { ) {
/** /**
* Forces all RealmSessionStoreMigration instances to be equal. * Forces all RealmSessionStoreMigration instances to be equal.
@ -125,5 +126,6 @@ internal class RealmSessionStoreMigration @Inject constructor(
if (oldVersion < 43) MigrateSessionTo043(realm).perform() if (oldVersion < 43) MigrateSessionTo043(realm).perform()
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()
} }
} }

View file

@ -37,9 +37,11 @@ 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.model.EventInsertType
import org.matrix.android.sdk.internal.database.model.RoomEntity import org.matrix.android.sdk.internal.database.model.RoomEntity
import org.matrix.android.sdk.internal.database.model.TimelineEventEntity import org.matrix.android.sdk.internal.database.model.TimelineEventEntity
import org.matrix.android.sdk.internal.database.model.threads.ThreadListPageEntity
import org.matrix.android.sdk.internal.database.model.threads.ThreadSummaryEntity import org.matrix.android.sdk.internal.database.model.threads.ThreadSummaryEntity
import org.matrix.android.sdk.internal.database.model.threads.ThreadSummaryEntityFields import org.matrix.android.sdk.internal.database.model.threads.ThreadSummaryEntityFields
import org.matrix.android.sdk.internal.database.query.copyToRealmOrIgnore import org.matrix.android.sdk.internal.database.query.copyToRealmOrIgnore
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.database.query.getOrNull import org.matrix.android.sdk.internal.database.query.getOrNull
import org.matrix.android.sdk.internal.database.query.where import org.matrix.android.sdk.internal.database.query.where
@ -113,16 +115,16 @@ internal fun ThreadSummaryEntity.Companion.createOrUpdate(
userId: String, userId: String,
cryptoService: CryptoService? = null, cryptoService: CryptoService? = null,
currentTimeMillis: Long, currentTimeMillis: Long,
) { ): ThreadSummaryEntity? {
when (threadSummaryType) { when (threadSummaryType) {
ThreadSummaryUpdateType.REPLACE -> { ThreadSummaryUpdateType.REPLACE -> {
rootThreadEvent?.eventId ?: return rootThreadEvent?.eventId ?: return null
rootThreadEvent.senderId ?: return rootThreadEvent.senderId ?: return null
val numberOfThreads = rootThreadEvent.unsignedData?.relations?.latestThread?.count ?: return val numberOfThreads = rootThreadEvent.unsignedData?.relations?.latestThread?.count ?: return null
// Something is wrong with the server return // Something is wrong with the server return
if (numberOfThreads <= 0) return if (numberOfThreads <= 0) return null
val threadSummary = ThreadSummaryEntity.getOrCreate(realm, roomId, rootThreadEvent.eventId).also { val threadSummary = ThreadSummaryEntity.getOrCreate(realm, roomId, rootThreadEvent.eventId).also {
Timber.i("###THREADS ThreadSummaryHelper REPLACE eventId:${it.rootThreadEventId} ") Timber.i("###THREADS ThreadSummaryHelper REPLACE eventId:${it.rootThreadEventId} ")
@ -153,12 +155,13 @@ internal fun ThreadSummaryEntity.Companion.createOrUpdate(
) )
roomEntity.addIfNecessary(threadSummary) roomEntity.addIfNecessary(threadSummary)
return threadSummary
} }
ThreadSummaryUpdateType.ADD -> { ThreadSummaryUpdateType.ADD -> {
val rootThreadEventId = threadEventEntity?.rootThreadEventId ?: return val rootThreadEventId = threadEventEntity?.rootThreadEventId ?: return null
Timber.i("###THREADS ThreadSummaryHelper ADD for root eventId:$rootThreadEventId") Timber.i("###THREADS ThreadSummaryHelper ADD for root eventId:$rootThreadEventId")
val threadSummary = ThreadSummaryEntity.getOrNull(realm, roomId, rootThreadEventId) var threadSummary = ThreadSummaryEntity.getOrNull(realm, roomId, rootThreadEventId)
if (threadSummary != null) { if (threadSummary != null) {
// ThreadSummary exists so lets add the latest event // ThreadSummary exists so lets add the latest event
Timber.i("###THREADS ThreadSummaryHelper ADD root eventId:$rootThreadEventId exists, lets update latest thread event.") Timber.i("###THREADS ThreadSummaryHelper ADD root eventId:$rootThreadEventId exists, lets update latest thread event.")
@ -172,7 +175,7 @@ internal fun ThreadSummaryEntity.Companion.createOrUpdate(
Timber.i("###THREADS ThreadSummaryHelper ADD root eventId:$rootThreadEventId do not exists, lets try to create one") Timber.i("###THREADS ThreadSummaryHelper ADD root eventId:$rootThreadEventId do not exists, lets try to create one")
threadEventEntity.findRootThreadEvent()?.let { rootThreadEventEntity -> threadEventEntity.findRootThreadEvent()?.let { rootThreadEventEntity ->
// Root thread event entity exists so lets create a new record // Root thread event entity exists so lets create a new record
ThreadSummaryEntity.getOrCreate(realm, roomId, rootThreadEventEntity.eventId).let { threadSummary = ThreadSummaryEntity.getOrCreate(realm, roomId, rootThreadEventEntity.eventId).also {
it.updateThreadSummary( it.updateThreadSummary(
rootThreadEventEntity = rootThreadEventEntity, rootThreadEventEntity = rootThreadEventEntity,
numberOfThreads = 1, numberOfThreads = 1,
@ -183,7 +186,12 @@ internal fun ThreadSummaryEntity.Companion.createOrUpdate(
roomEntity.addIfNecessary(it) roomEntity.addIfNecessary(it)
} }
} }
threadSummary?.let {
ThreadListPageEntity.get(realm, roomId)?.threadSummaries?.add(it)
}
} }
return threadSummary
} }
} }
} }

View file

@ -0,0 +1,32 @@
/*
* 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.database.model.threads.ThreadListPageEntityFields
import org.matrix.android.sdk.internal.util.database.RealmMigrator
internal class MigrateSessionTo046(realm: DynamicRealm) : RealmMigrator(realm, 46) {
override fun doMigrate(realm: DynamicRealm) {
realm.schema.create("ThreadListPageEntity")
.addField(ThreadListPageEntityFields.ROOM_ID, String::class.java)
.addPrimaryKey(ThreadListPageEntityFields.ROOM_ID)
.setRequired(ThreadListPageEntityFields.ROOM_ID, true)
.addRealmListField(ThreadListPageEntityFields.THREAD_SUMMARIES.`$`, realm.schema.get("ThreadSummaryEntity")!!)
}
}

View file

@ -19,6 +19,7 @@ package org.matrix.android.sdk.internal.database.model
import io.realm.annotations.RealmModule import io.realm.annotations.RealmModule
import org.matrix.android.sdk.internal.database.model.livelocation.LiveLocationShareAggregatedSummaryEntity import org.matrix.android.sdk.internal.database.model.livelocation.LiveLocationShareAggregatedSummaryEntity
import org.matrix.android.sdk.internal.database.model.presence.UserPresenceEntity import org.matrix.android.sdk.internal.database.model.presence.UserPresenceEntity
import org.matrix.android.sdk.internal.database.model.threads.ThreadListPageEntity
import org.matrix.android.sdk.internal.database.model.threads.ThreadSummaryEntity import org.matrix.android.sdk.internal.database.model.threads.ThreadSummaryEntity
/** /**
@ -72,6 +73,7 @@ import org.matrix.android.sdk.internal.database.model.threads.ThreadSummaryEntit
UserPresenceEntity::class, UserPresenceEntity::class,
ThreadSummaryEntity::class, ThreadSummaryEntity::class,
SyncFilterParamsEntity::class, SyncFilterParamsEntity::class,
ThreadListPageEntity::class
] ]
) )
internal class SessionRealmModule internal class SessionRealmModule

View file

@ -0,0 +1,28 @@
/*
* 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.model.threads
import io.realm.RealmList
import io.realm.RealmObject
import io.realm.annotations.PrimaryKey
internal open class ThreadListPageEntity(
@PrimaryKey var roomId: String = "",
var threadSummaries: RealmList<ThreadSummaryEntity> = RealmList()
) : RealmObject() {
companion object
}

View file

@ -40,5 +40,8 @@ internal open class ThreadSummaryEntity(
@LinkingObjects("threadSummaries") @LinkingObjects("threadSummaries")
val room: RealmResults<RoomEntity>? = null val room: RealmResults<RoomEntity>? = null
@LinkingObjects("threadSummaries")
val page: RealmResults<ThreadListPageEntity>? = null
companion object companion object
} }

View file

@ -0,0 +1,31 @@
/*
* 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.query
import io.realm.Realm
import io.realm.kotlin.createObject
import io.realm.kotlin.where
import org.matrix.android.sdk.internal.database.model.threads.ThreadListPageEntity
import org.matrix.android.sdk.internal.database.model.threads.ThreadListPageEntityFields
internal fun ThreadListPageEntity.Companion.get(realm: Realm, roomId: String): ThreadListPageEntity? {
return realm.where<ThreadListPageEntity>().equalTo(ThreadListPageEntityFields.ROOM_ID, roomId).findFirst()
}
internal fun ThreadListPageEntity.Companion.getOrCreate(realm: Realm, roomId: String): ThreadListPageEntity {
return get(realm, roomId) ?: realm.createObject(roomId)
}

View file

@ -22,6 +22,7 @@ import io.realm.RealmQuery
import io.realm.RealmResults import io.realm.RealmResults
import io.realm.Sort import io.realm.Sort
import io.realm.kotlin.where import io.realm.kotlin.where
import org.matrix.android.sdk.api.session.events.model.EventType
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.api.session.room.timeline.TimelineEventFilters import org.matrix.android.sdk.api.session.room.timeline.TimelineEventFilters
import org.matrix.android.sdk.internal.database.model.ChunkEntity import org.matrix.android.sdk.internal.database.model.ChunkEntity
@ -94,14 +95,27 @@ internal fun RealmQuery<TimelineEventEntity>.filterEvents(filters: TimelineEvent
if (filters.filterTypes && filters.allowedTypes.isNotEmpty()) { if (filters.filterTypes && filters.allowedTypes.isNotEmpty()) {
beginGroup() beginGroup()
filters.allowedTypes.forEachIndexed { index, filter -> filters.allowedTypes.forEachIndexed { index, filter ->
if (filter.stateKey == null) { if (filter.eventType == EventType.ENCRYPTED) {
equalTo(TimelineEventEntityFields.ROOT.TYPE, filter.eventType) val otherTypes = filters.allowedTypes.minus(filter).map { it.eventType }
if (filter.stateKey == null) {
filterEncryptedTypes(otherTypes)
} else {
beginGroup()
filterEncryptedTypes(otherTypes)
and()
equalTo(TimelineEventEntityFields.ROOT.STATE_KEY, filter.stateKey)
endGroup()
}
} else { } else {
beginGroup() if (filter.stateKey == null) {
equalTo(TimelineEventEntityFields.ROOT.TYPE, filter.eventType) equalTo(TimelineEventEntityFields.ROOT.TYPE, filter.eventType)
and() } else {
equalTo(TimelineEventEntityFields.ROOT.STATE_KEY, filter.stateKey) beginGroup()
endGroup() equalTo(TimelineEventEntityFields.ROOT.TYPE, filter.eventType)
and()
equalTo(TimelineEventEntityFields.ROOT.STATE_KEY, filter.stateKey)
endGroup()
}
} }
if (index != filters.allowedTypes.size - 1) { if (index != filters.allowedTypes.size - 1) {
or() or()
@ -115,7 +129,6 @@ internal fun RealmQuery<TimelineEventEntity>.filterEvents(filters: TimelineEvent
if (filters.filterEdits) { if (filters.filterEdits) {
not().like(TimelineEventEntityFields.ROOT.CONTENT, TimelineEventFilter.Content.EDIT) not().like(TimelineEventEntityFields.ROOT.CONTENT, TimelineEventFilter.Content.EDIT)
not().like(TimelineEventEntityFields.ROOT.CONTENT, TimelineEventFilter.Content.RESPONSE) not().like(TimelineEventEntityFields.ROOT.CONTENT, TimelineEventFilter.Content.RESPONSE)
not().like(TimelineEventEntityFields.ROOT.CONTENT, TimelineEventFilter.Content.REFERENCE)
} }
if (filters.filterRedacted) { if (filters.filterRedacted) {
not().like(TimelineEventEntityFields.ROOT.UNSIGNED_DATA, TimelineEventFilter.Unsigned.REDACTED) not().like(TimelineEventEntityFields.ROOT.UNSIGNED_DATA, TimelineEventFilter.Unsigned.REDACTED)
@ -124,6 +137,21 @@ internal fun RealmQuery<TimelineEventEntity>.filterEvents(filters: TimelineEvent
return this return this
} }
internal fun RealmQuery<TimelineEventEntity>.filterEncryptedTypes(allowedTypes: List<String>): RealmQuery<TimelineEventEntity> {
beginGroup()
equalTo(TimelineEventEntityFields.ROOT.TYPE, EventType.ENCRYPTED)
and()
beginGroup()
isNull(TimelineEventEntityFields.ROOT.DECRYPTION_RESULT_JSON)
allowedTypes.forEach { eventType ->
or()
like(TimelineEventEntityFields.ROOT.DECRYPTION_RESULT_JSON, TimelineEventFilter.DecryptedContent.type(eventType))
}
endGroup()
endGroup()
return this
}
internal fun RealmQuery<TimelineEventEntity>.filterTypes(filterTypes: List<String>): RealmQuery<TimelineEventEntity> { internal fun RealmQuery<TimelineEventEntity>.filterTypes(filterTypes: List<String>): RealmQuery<TimelineEventEntity> {
return if (filterTypes.isEmpty()) { return if (filterTypes.isEmpty()) {
this this

View file

@ -34,6 +34,7 @@ internal object TimelineEventFilter {
*/ */
internal object DecryptedContent { internal object DecryptedContent {
internal const val URL = """{*"file":*"url":*}""" internal const val URL = """{*"file":*"url":*}"""
fun type(type: String) = """{*"type":*"$type"*}"""
} }
/** /**

View file

@ -35,6 +35,7 @@ import org.matrix.android.sdk.internal.session.room.membership.joining.InviteBod
import org.matrix.android.sdk.internal.session.room.membership.threepid.ThreePidInviteBody import org.matrix.android.sdk.internal.session.room.membership.threepid.ThreePidInviteBody
import org.matrix.android.sdk.internal.session.room.read.ReadBody import org.matrix.android.sdk.internal.session.room.read.ReadBody
import org.matrix.android.sdk.internal.session.room.relation.RelationsResponse import org.matrix.android.sdk.internal.session.room.relation.RelationsResponse
import org.matrix.android.sdk.internal.session.room.relation.threads.ThreadSummariesResponse
import org.matrix.android.sdk.internal.session.room.reporting.ReportContentBody import org.matrix.android.sdk.internal.session.room.reporting.ReportContentBody
import org.matrix.android.sdk.internal.session.room.send.SendResponse import org.matrix.android.sdk.internal.session.room.send.SendResponse
import org.matrix.android.sdk.internal.session.room.tags.TagBody import org.matrix.android.sdk.internal.session.room.tags.TagBody
@ -464,4 +465,12 @@ internal interface RoomAPI {
@Path("roomIdOrAlias") roomidOrAlias: String, @Path("roomIdOrAlias") roomidOrAlias: String,
@Query("via") viaServers: List<String>? @Query("via") viaServers: List<String>?
): RoomStrippedState ): RoomStrippedState
@GET(NetworkConstants.URI_API_PREFIX_PATH_V1 + "rooms/{roomId}/threads")
suspend fun getThreadsList(
@Path("roomId") roomId: String,
@Query("include") include: String? = "all",
@Query("from") from: String? = null,
@Query("limit") limit: Int? = null
): ThreadSummariesResponse
} }

View file

@ -29,7 +29,6 @@ import org.matrix.android.sdk.api.session.room.getTimelineEvent
import org.matrix.android.sdk.api.session.room.model.PollSummaryContent import org.matrix.android.sdk.api.session.room.model.PollSummaryContent
import org.matrix.android.sdk.api.session.room.model.VoteInfo import org.matrix.android.sdk.api.session.room.model.VoteInfo
import org.matrix.android.sdk.api.session.room.model.VoteSummary import org.matrix.android.sdk.api.session.room.model.VoteSummary
import org.matrix.android.sdk.api.session.room.model.message.MessageEndPollContent
import org.matrix.android.sdk.api.session.room.model.message.MessagePollContent import org.matrix.android.sdk.api.session.room.model.message.MessagePollContent
import org.matrix.android.sdk.api.session.room.model.message.MessagePollResponseContent import org.matrix.android.sdk.api.session.room.model.message.MessagePollResponseContent
import org.matrix.android.sdk.api.session.room.powerlevels.PowerLevelsHelper import org.matrix.android.sdk.api.session.room.powerlevels.PowerLevelsHelper
@ -78,7 +77,7 @@ class DefaultPollAggregationProcessor @Inject constructor() : PollAggregationPro
val content = event.getClearContent()?.toModel<MessagePollResponseContent>() ?: return false val content = event.getClearContent()?.toModel<MessagePollResponseContent>() ?: return false
val roomId = event.roomId ?: return false val roomId = event.roomId ?: return false
val senderId = event.senderId ?: return false val senderId = event.senderId ?: return false
val targetEventId = (event.getRelationContent() ?: content.relatesTo)?.eventId ?: return false val targetEventId = event.getRelationContent()?.eventId ?: return false
val targetPollContent = getPollContent(session, roomId, targetEventId) ?: return false val targetPollContent = getPollContent(session, roomId, targetEventId) ?: return false
val annotationsSummaryEntity = getAnnotationsSummaryEntity(realm, roomId, targetEventId) val annotationsSummaryEntity = getAnnotationsSummaryEntity(realm, roomId, targetEventId)
@ -154,9 +153,8 @@ class DefaultPollAggregationProcessor @Inject constructor() : PollAggregationPro
} }
override fun handlePollEndEvent(session: Session, powerLevelsHelper: PowerLevelsHelper, realm: Realm, event: Event): Boolean { override fun handlePollEndEvent(session: Session, powerLevelsHelper: PowerLevelsHelper, realm: Realm, event: Event): Boolean {
val content = event.getClearContent()?.toModel<MessageEndPollContent>() ?: return false
val roomId = event.roomId ?: return false val roomId = event.roomId ?: return false
val pollEventId = content.relatesTo?.eventId ?: return false val pollEventId = event.getRelationContent()?.eventId ?: return false
val pollOwnerId = getPollEvent(session, roomId, pollEventId)?.root?.senderId val pollOwnerId = getPollEvent(session, roomId, pollEventId)?.root?.senderId
val isPollOwner = pollOwnerId == event.senderId val isPollOwner = pollOwnerId == event.senderId

View file

@ -16,37 +16,38 @@
package org.matrix.android.sdk.internal.session.room.relation.threads package org.matrix.android.sdk.internal.session.room.relation.threads
import com.zhuinden.monarchy.Monarchy import com.zhuinden.monarchy.Monarchy
import io.realm.RealmList
import org.matrix.android.sdk.api.session.crypto.CryptoService import org.matrix.android.sdk.api.session.crypto.CryptoService
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.threads.FetchThreadsResult
import org.matrix.android.sdk.api.session.room.threads.ThreadFilter
import org.matrix.android.sdk.api.session.room.threads.model.ThreadSummaryUpdateType import org.matrix.android.sdk.api.session.room.threads.model.ThreadSummaryUpdateType
import org.matrix.android.sdk.internal.database.helper.createOrUpdate import org.matrix.android.sdk.internal.database.helper.createOrUpdate
import org.matrix.android.sdk.internal.database.model.RoomEntity import org.matrix.android.sdk.internal.database.model.RoomEntity
import org.matrix.android.sdk.internal.database.model.threads.ThreadListPageEntity
import org.matrix.android.sdk.internal.database.model.threads.ThreadSummaryEntity import org.matrix.android.sdk.internal.database.model.threads.ThreadSummaryEntity
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.di.SessionDatabase import org.matrix.android.sdk.internal.di.SessionDatabase
import org.matrix.android.sdk.internal.di.UserId import org.matrix.android.sdk.internal.di.UserId
import org.matrix.android.sdk.internal.network.GlobalErrorReceiver import org.matrix.android.sdk.internal.network.GlobalErrorReceiver
import org.matrix.android.sdk.internal.network.executeRequest import org.matrix.android.sdk.internal.network.executeRequest
import org.matrix.android.sdk.internal.session.filter.FilterFactory
import org.matrix.android.sdk.internal.session.room.RoomAPI import org.matrix.android.sdk.internal.session.room.RoomAPI
import org.matrix.android.sdk.internal.session.room.timeline.PaginationDirection
import org.matrix.android.sdk.internal.session.room.timeline.PaginationResponse
import org.matrix.android.sdk.internal.task.Task import org.matrix.android.sdk.internal.task.Task
import org.matrix.android.sdk.internal.util.awaitTransaction import org.matrix.android.sdk.internal.util.awaitTransaction
import org.matrix.android.sdk.internal.util.time.Clock import org.matrix.android.sdk.internal.util.time.Clock
import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
/*** /***
* This class is responsible to Fetch all the thread in the current room, * This class is responsible to Fetch all the thread in the current room,
* To fetch all threads in a room, the /messages API is used with newly added filtering options. * To fetch all threads in a room, the /messages API is used with newly added filtering options.
*/ */
internal interface FetchThreadSummariesTask : Task<FetchThreadSummariesTask.Params, DefaultFetchThreadSummariesTask.Result> { internal interface FetchThreadSummariesTask : Task<FetchThreadSummariesTask.Params, FetchThreadsResult> {
data class Params( data class Params(
val roomId: String, val roomId: String,
val from: String = "", val from: String? = null,
val limit: Int = 500, val limit: Int = 5,
val isUserParticipating: Boolean = true val filter: ThreadFilter? = null,
) )
} }
@ -59,39 +60,43 @@ internal class DefaultFetchThreadSummariesTask @Inject constructor(
private val clock: Clock, private val clock: Clock,
) : FetchThreadSummariesTask { ) : FetchThreadSummariesTask {
override suspend fun execute(params: FetchThreadSummariesTask.Params): Result { override suspend fun execute(params: FetchThreadSummariesTask.Params): FetchThreadsResult {
val filter = FilterFactory.createThreadsFilter( val response = executeRequest(globalErrorReceiver) {
numberOfEvents = params.limit, roomAPI.getThreadsList(
userId = if (params.isUserParticipating) userId else null roomId = params.roomId,
).toJSONString() include = params.filter?.toString()?.lowercase(),
from = params.from,
val response = executeRequest( limit = params.limit
globalErrorReceiver, )
canRetry = true
) {
roomAPI.getRoomMessagesFrom(params.roomId, params.from, PaginationDirection.BACKWARDS.value, params.limit, filter)
} }
Timber.i("###THREADS DefaultFetchThreadSummariesTask Fetched size:${response.events.size} nextBatch:${response.end} ") handleResponse(response, params)
return handleResponse(response, params) return when {
response.nextBatch != null -> FetchThreadsResult.ShouldFetchMore(response.nextBatch)
else -> FetchThreadsResult.ReachedEnd
}
} }
private suspend fun handleResponse( private suspend fun handleResponse(
response: PaginationResponse, response: ThreadSummariesResponse,
params: FetchThreadSummariesTask.Params params: FetchThreadSummariesTask.Params
): Result { ) {
val rootThreadList = response.events val rootThreadList = response.chunk
val threadSummaries = RealmList<ThreadSummaryEntity>()
monarchy.awaitTransaction { realm -> monarchy.awaitTransaction { realm ->
val roomEntity = RoomEntity.where(realm, roomId = params.roomId).findFirst() ?: return@awaitTransaction val roomEntity = RoomEntity.where(realm, roomId = params.roomId).findFirst() ?: return@awaitTransaction
val roomMemberContentsByUser = HashMap<String, RoomMemberContent?>() val roomMemberContentsByUser = HashMap<String, RoomMemberContent?>()
for (rootThreadEvent in rootThreadList) { for (rootThreadEvent in rootThreadList) {
if (rootThreadEvent.eventId == null || rootThreadEvent.senderId == null || rootThreadEvent.type == null) { if (rootThreadEvent.eventId == null || rootThreadEvent.senderId == null || rootThreadEvent.type == null) {
continue continue
} }
ThreadSummaryEntity.createOrUpdate( val threadSummary = ThreadSummaryEntity.createOrUpdate(
threadSummaryType = ThreadSummaryUpdateType.REPLACE, threadSummaryType = ThreadSummaryUpdateType.REPLACE,
realm = realm, realm = realm,
roomId = params.roomId, roomId = params.roomId,
@ -102,14 +107,16 @@ internal class DefaultFetchThreadSummariesTask @Inject constructor(
cryptoService = cryptoService, cryptoService = cryptoService,
currentTimeMillis = clock.epochMillis(), currentTimeMillis = clock.epochMillis(),
) )
threadSummaries.add(threadSummary)
}
val page = ThreadListPageEntity.getOrCreate(realm, params.roomId)
threadSummaries.forEach {
if (!page.threadSummaries.contains(it)) {
page.threadSummaries.add(it)
}
} }
} }
return Result.SUCCESS
}
enum class Result {
SHOULD_FETCH_MORE,
REACHED_END,
SUCCESS
} }
} }

View file

@ -0,0 +1,27 @@
/*
* 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.session.room.relation.threads
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import org.matrix.android.sdk.api.session.events.model.Event
@JsonClass(generateAdapter = true)
internal data class ThreadSummariesResponse(
@Json(name = "chunk") val chunk: List<Event>,
@Json(name = "next_batch") val nextBatch: String?,
@Json(name = "prev_batch") val prevBatch: String?
)

View file

@ -17,17 +17,23 @@
package org.matrix.android.sdk.internal.session.room.summary package org.matrix.android.sdk.internal.session.room.summary
import io.realm.Realm import io.realm.Realm
import org.matrix.android.sdk.api.MatrixConfiguration
import org.matrix.android.sdk.api.session.room.summary.RoomSummaryConstants import org.matrix.android.sdk.api.session.room.summary.RoomSummaryConstants
import org.matrix.android.sdk.api.session.room.timeline.EventTypeFilter import org.matrix.android.sdk.api.session.room.timeline.EventTypeFilter
import org.matrix.android.sdk.api.session.room.timeline.TimelineEventFilters import org.matrix.android.sdk.api.session.room.timeline.TimelineEventFilters
import org.matrix.android.sdk.internal.database.model.TimelineEventEntity import org.matrix.android.sdk.internal.database.model.TimelineEventEntity
import org.matrix.android.sdk.internal.database.query.latestEvent import org.matrix.android.sdk.internal.database.query.latestEvent
import javax.inject.Inject
internal object RoomSummaryEventsHelper { internal class RoomSummaryEventsHelper @Inject constructor(
matrixConfiguration: MatrixConfiguration,
) {
private val previewFilters = TimelineEventFilters( private val previewFilters = TimelineEventFilters(
filterTypes = true, filterTypes = true,
allowedTypes = RoomSummaryConstants.PREVIEWABLE_TYPES.map { EventTypeFilter(eventType = it, stateKey = null) }, allowedTypes = RoomSummaryConstants.PREVIEWABLE_TYPES
.plus(matrixConfiguration.customEventTypesProvider?.customPreviewableEventTypes.orEmpty())
.map { EventTypeFilter(eventType = it, stateKey = null) },
filterUseless = true, filterUseless = true,
filterRedacted = false, filterRedacted = false,
filterEdits = true filterEdits = true

View file

@ -73,13 +73,14 @@ internal class RoomSummaryUpdater @Inject constructor(
private val roomAvatarResolver: RoomAvatarResolver, private val roomAvatarResolver: RoomAvatarResolver,
private val roomAccountDataDataSource: RoomAccountDataDataSource, private val roomAccountDataDataSource: RoomAccountDataDataSource,
private val homeServerCapabilitiesService: HomeServerCapabilitiesService, private val homeServerCapabilitiesService: HomeServerCapabilitiesService,
private val roomSummaryEventDecryptor: RoomSummaryEventDecryptor private val roomSummaryEventDecryptor: RoomSummaryEventDecryptor,
private val roomSummaryEventsHelper: RoomSummaryEventsHelper,
) { ) {
fun refreshLatestPreviewContent(realm: Realm, roomId: String) { fun refreshLatestPreviewContent(realm: Realm, roomId: String) {
val roomSummaryEntity = RoomSummaryEntity.getOrNull(realm, roomId) val roomSummaryEntity = RoomSummaryEntity.getOrNull(realm, roomId)
if (roomSummaryEntity != null) { if (roomSummaryEntity != null) {
val latestPreviewableEvent = RoomSummaryEventsHelper.getLatestPreviewableEvent(realm, roomId) val latestPreviewableEvent = roomSummaryEventsHelper.getLatestPreviewableEvent(realm, roomId)
latestPreviewableEvent?.attemptToDecrypt() latestPreviewableEvent?.attemptToDecrypt()
} }
} }
@ -141,7 +142,7 @@ internal class RoomSummaryUpdater @Inject constructor(
val encryptionEvent = CurrentStateEventEntity.getOrNull(realm, roomId, type = EventType.STATE_ROOM_ENCRYPTION, stateKey = "")?.root val encryptionEvent = CurrentStateEventEntity.getOrNull(realm, roomId, type = EventType.STATE_ROOM_ENCRYPTION, stateKey = "")?.root
Timber.d("## CRYPTO: currentEncryptionEvent is $encryptionEvent") Timber.d("## CRYPTO: currentEncryptionEvent is $encryptionEvent")
val latestPreviewableEvent = RoomSummaryEventsHelper.getLatestPreviewableEvent(realm, roomId) val latestPreviewableEvent = roomSummaryEventsHelper.getLatestPreviewableEvent(realm, roomId)
val lastActivityFromEvent = latestPreviewableEvent?.root?.originServerTs val lastActivityFromEvent = latestPreviewableEvent?.root?.originServerTs
if (lastActivityFromEvent != null) { if (lastActivityFromEvent != null) {
@ -224,7 +225,7 @@ internal class RoomSummaryUpdater @Inject constructor(
fun updateSendingInformation(realm: Realm, roomId: String) { fun updateSendingInformation(realm: Realm, roomId: String) {
val roomSummaryEntity = RoomSummaryEntity.getOrCreate(realm, roomId) val roomSummaryEntity = RoomSummaryEntity.getOrCreate(realm, roomId)
roomSummaryEntity.updateHasFailedSending() roomSummaryEntity.updateHasFailedSending()
roomSummaryEntity.latestPreviewableEvent = RoomSummaryEventsHelper.getLatestPreviewableEvent(realm, roomId) roomSummaryEntity.latestPreviewableEvent = roomSummaryEventsHelper.getLatestPreviewableEvent(realm, roomId)
} }
/** /**

View file

@ -16,30 +16,39 @@
package org.matrix.android.sdk.internal.session.room.threads package org.matrix.android.sdk.internal.session.room.threads
import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData
import androidx.paging.LivePagedListBuilder
import androidx.paging.PagedList
import com.zhuinden.monarchy.Monarchy import com.zhuinden.monarchy.Monarchy
import dagger.assisted.Assisted import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject import dagger.assisted.AssistedInject
import io.realm.Realm import io.realm.Realm
import io.realm.Sort
import io.realm.kotlin.where
import org.matrix.android.sdk.api.session.room.ResultBoundaries
import org.matrix.android.sdk.api.session.room.threads.FetchThreadsResult
import org.matrix.android.sdk.api.session.room.threads.ThreadFilter
import org.matrix.android.sdk.api.session.room.threads.ThreadLivePageResult
import org.matrix.android.sdk.api.session.room.threads.ThreadsService import org.matrix.android.sdk.api.session.room.threads.ThreadsService
import org.matrix.android.sdk.api.session.room.threads.model.ThreadSummary import org.matrix.android.sdk.api.session.room.threads.model.ThreadSummary
import org.matrix.android.sdk.internal.database.helper.enhanceWithEditions import org.matrix.android.sdk.internal.database.helper.enhanceWithEditions
import org.matrix.android.sdk.internal.database.helper.findAllThreadsForRoomId import org.matrix.android.sdk.internal.database.helper.findAllThreadsForRoomId
import org.matrix.android.sdk.internal.database.mapper.ThreadSummaryMapper import org.matrix.android.sdk.internal.database.mapper.ThreadSummaryMapper
import org.matrix.android.sdk.internal.database.model.threads.ThreadListPageEntity
import org.matrix.android.sdk.internal.database.model.threads.ThreadSummaryEntity import org.matrix.android.sdk.internal.database.model.threads.ThreadSummaryEntity
import org.matrix.android.sdk.internal.database.model.threads.ThreadSummaryEntityFields
import org.matrix.android.sdk.internal.di.SessionDatabase import org.matrix.android.sdk.internal.di.SessionDatabase
import org.matrix.android.sdk.internal.di.UserId
import org.matrix.android.sdk.internal.session.room.relation.threads.FetchThreadSummariesTask import org.matrix.android.sdk.internal.session.room.relation.threads.FetchThreadSummariesTask
import org.matrix.android.sdk.internal.session.room.relation.threads.FetchThreadTimelineTask import org.matrix.android.sdk.internal.session.room.relation.threads.FetchThreadTimelineTask
import org.matrix.android.sdk.internal.util.awaitTransaction
internal class DefaultThreadsService @AssistedInject constructor( internal class DefaultThreadsService @AssistedInject constructor(
@Assisted private val roomId: String, @Assisted private val roomId: String,
@UserId private val userId: String,
private val fetchThreadTimelineTask: FetchThreadTimelineTask, private val fetchThreadTimelineTask: FetchThreadTimelineTask,
private val fetchThreadSummariesTask: FetchThreadSummariesTask,
@SessionDatabase private val monarchy: Monarchy, @SessionDatabase private val monarchy: Monarchy,
private val threadSummaryMapper: ThreadSummaryMapper private val threadSummaryMapper: ThreadSummaryMapper,
private val fetchThreadSummariesTask: FetchThreadSummariesTask,
) : ThreadsService { ) : ThreadsService {
@AssistedFactory @AssistedFactory
@ -47,16 +56,58 @@ internal class DefaultThreadsService @AssistedInject constructor(
fun create(roomId: String): DefaultThreadsService fun create(roomId: String): DefaultThreadsService
} }
override fun getAllThreadSummariesLive(): LiveData<List<ThreadSummary>> { override suspend fun getPagedThreadsList(userParticipating: Boolean, pagedListConfig: PagedList.Config): ThreadLivePageResult {
return monarchy.findAllMappedWithChanges( monarchy.awaitTransaction { realm ->
{ ThreadSummaryEntity.findAllThreadsForRoomId(it, roomId = roomId) }, realm.where<ThreadListPageEntity>().findAll().deleteAllFromRealm()
{ }
threadSummaryMapper.map(it)
val realmDataSourceFactory = monarchy.createDataSourceFactory { realm ->
realm
.where<ThreadSummaryEntity>().equalTo(ThreadSummaryEntityFields.PAGE.ROOM_ID, roomId)
.sort(ThreadSummaryEntityFields.LATEST_THREAD_EVENT_ENTITY.ORIGIN_SERVER_TS, Sort.DESCENDING)
}
val dataSourceFactory = realmDataSourceFactory.map {
threadSummaryMapper.map(it)
}
val boundaries = MutableLiveData(ResultBoundaries())
val builder = LivePagedListBuilder(dataSourceFactory, pagedListConfig).also {
it.setBoundaryCallback(object : PagedList.BoundaryCallback<ThreadSummary>() {
override fun onItemAtEndLoaded(itemAtEnd: ThreadSummary) {
boundaries.postValue(boundaries.value?.copy(endLoaded = true))
} }
override fun onItemAtFrontLoaded(itemAtFront: ThreadSummary) {
boundaries.postValue(boundaries.value?.copy(frontLoaded = true))
}
override fun onZeroItemsLoaded() {
boundaries.postValue(boundaries.value?.copy(zeroItemLoaded = true))
}
})
}
val livePagedList = monarchy.findAllPagedWithChanges(
realmDataSourceFactory,
builder
)
return ThreadLivePageResult(livePagedList, boundaries)
}
override suspend fun fetchThreadList(nextBatchId: String?, limit: Int, filter: ThreadFilter): FetchThreadsResult {
return fetchThreadSummariesTask.execute(
FetchThreadSummariesTask.Params(
roomId = roomId,
from = nextBatchId,
limit = limit,
filter = filter
)
) )
} }
override fun getAllThreadSummaries(): List<ThreadSummary> { override suspend fun getAllThreadSummaries(): List<ThreadSummary> {
return monarchy.fetchAllMappedSync( return monarchy.fetchAllMappedSync(
{ ThreadSummaryEntity.findAllThreadsForRoomId(it, roomId = roomId) }, { ThreadSummaryEntity.findAllThreadsForRoomId(it, roomId = roomId) },
{ threadSummaryMapper.map(it) } { threadSummaryMapper.map(it) }
@ -79,12 +130,4 @@ internal class DefaultThreadsService @AssistedInject constructor(
) )
) )
} }
override suspend fun fetchThreadSummaries() {
fetchThreadSummariesTask.execute(
FetchThreadSummariesTask.Params(
roomId = roomId
)
)
}
} }

View file

@ -47,7 +47,7 @@ import org.matrix.android.sdk.test.fakes.FakeRealm
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
class PollAggregationProcessorTest { class DefaultPollAggregationProcessorTest {
private val pollAggregationProcessor: PollAggregationProcessor = DefaultPollAggregationProcessor() private val pollAggregationProcessor: PollAggregationProcessor = DefaultPollAggregationProcessor()
private val realm = FakeRealm() private val realm = FakeRealm()

View file

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

View file

@ -49,6 +49,7 @@ import im.vector.app.features.analytics.VectorAnalytics
import im.vector.app.features.analytics.errors.ErrorTracker import im.vector.app.features.analytics.errors.ErrorTracker
import im.vector.app.features.analytics.impl.DefaultVectorAnalytics import im.vector.app.features.analytics.impl.DefaultVectorAnalytics
import im.vector.app.features.analytics.metrics.VectorPlugins import im.vector.app.features.analytics.metrics.VectorPlugins
import im.vector.app.features.configuration.VectorCustomEventTypesProvider
import im.vector.app.features.invite.AutoAcceptInvites import im.vector.app.features.invite.AutoAcceptInvites
import im.vector.app.features.invite.CompileTimeAutoAcceptInvites import im.vector.app.features.invite.CompileTimeAutoAcceptInvites
import im.vector.app.features.navigation.DefaultNavigator import im.vector.app.features.navigation.DefaultNavigator
@ -145,6 +146,7 @@ import javax.inject.Singleton
vectorRoomDisplayNameFallbackProvider: VectorRoomDisplayNameFallbackProvider, vectorRoomDisplayNameFallbackProvider: VectorRoomDisplayNameFallbackProvider,
flipperProxy: FlipperProxy, flipperProxy: FlipperProxy,
vectorPlugins: VectorPlugins, vectorPlugins: VectorPlugins,
vectorCustomEventTypesProvider: VectorCustomEventTypesProvider,
): MatrixConfiguration { ): MatrixConfiguration {
return MatrixConfiguration( return MatrixConfiguration(
applicationFlavor = BuildConfig.FLAVOR_DESCRIPTION, applicationFlavor = BuildConfig.FLAVOR_DESCRIPTION,
@ -156,6 +158,7 @@ import javax.inject.Singleton
), ),
metricPlugins = vectorPlugins.plugins(), metricPlugins = vectorPlugins.plugins(),
cryptoAnalyticsPlugin = vectorPlugins.cryptoMetricPlugin, cryptoAnalyticsPlugin = vectorPlugins.cryptoMetricPlugin,
customEventTypesProvider = vectorCustomEventTypesProvider,
) )
} }

View file

@ -39,7 +39,7 @@
<!-- Level 1: Labs --> <!-- Level 1: Labs -->
<bool name="settings_labs_deferred_dm_visible">true</bool> <bool name="settings_labs_deferred_dm_visible">true</bool>
<bool name="settings_labs_deferred_dm_default">true</bool> <bool name="settings_labs_deferred_dm_default">true</bool>
<bool name="settings_labs_thread_messages_default">false</bool> <bool name="settings_labs_thread_messages_default">true</bool>
<bool name="settings_labs_new_app_layout_default">true</bool> <bool name="settings_labs_new_app_layout_default">true</bool>
<bool name="settings_labs_new_session_manager_default">false</bool> <bool name="settings_labs_new_session_manager_default">false</bool>
<bool name="settings_labs_new_session_manager_visible">true</bool> <bool name="settings_labs_new_session_manager_visible">true</bool>

View file

@ -27,7 +27,7 @@ import im.vector.app.features.voicebroadcast.listening.VoiceBroadcastPlayer
import im.vector.app.features.voicebroadcast.listening.VoiceBroadcastPlayerImpl import im.vector.app.features.voicebroadcast.listening.VoiceBroadcastPlayerImpl
import im.vector.app.features.voicebroadcast.recording.VoiceBroadcastRecorder import im.vector.app.features.voicebroadcast.recording.VoiceBroadcastRecorder
import im.vector.app.features.voicebroadcast.recording.VoiceBroadcastRecorderQ import im.vector.app.features.voicebroadcast.recording.VoiceBroadcastRecorderQ
import im.vector.app.features.voicebroadcast.usecase.GetMostRecentVoiceBroadcastStateEventUseCase import im.vector.app.features.voicebroadcast.usecase.GetVoiceBroadcastStateEventLiveUseCase
import javax.inject.Singleton import javax.inject.Singleton
@InstallIn(SingletonComponent::class) @InstallIn(SingletonComponent::class)
@ -40,13 +40,13 @@ abstract class VoiceModule {
fun providesVoiceBroadcastRecorder( fun providesVoiceBroadcastRecorder(
context: Context, context: Context,
sessionHolder: ActiveSessionHolder, sessionHolder: ActiveSessionHolder,
getMostRecentVoiceBroadcastStateEventUseCase: GetMostRecentVoiceBroadcastStateEventUseCase, getVoiceBroadcastStateEventLiveUseCase: GetVoiceBroadcastStateEventLiveUseCase,
): VoiceBroadcastRecorder? { ): VoiceBroadcastRecorder? {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
VoiceBroadcastRecorderQ( VoiceBroadcastRecorderQ(
context = context, context = context,
sessionHolder = sessionHolder, sessionHolder = sessionHolder,
getVoiceBroadcastEventUseCase = getMostRecentVoiceBroadcastStateEventUseCase getVoiceBroadcastEventUseCase = getVoiceBroadcastStateEventLiveUseCase
) )
} else { } else {
null null

View file

@ -0,0 +1,28 @@
/*
* 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.configuration
import im.vector.app.features.voicebroadcast.VoiceBroadcastConstants
import org.matrix.android.sdk.api.provider.CustomEventTypesProvider
import javax.inject.Inject
class VectorCustomEventTypesProvider @Inject constructor() : CustomEventTypesProvider {
override val customPreviewableEventTypes = listOf(
VoiceBroadcastConstants.STATE_ROOM_VOICE_BROADCAST_INFO
)
}

View file

@ -16,7 +16,7 @@
package im.vector.app.features.displayname package im.vector.app.features.displayname
import org.matrix.android.sdk.api.MatrixItemDisplayNameFallbackProvider import org.matrix.android.sdk.api.provider.MatrixItemDisplayNameFallbackProvider
import org.matrix.android.sdk.api.util.MatrixItem import org.matrix.android.sdk.api.util.MatrixItem
// Used to provide the fallback to the MatrixSDK, in the MatrixConfiguration // Used to provide the fallback to the MatrixSDK, in the MatrixConfiguration

View file

@ -251,6 +251,12 @@ class HomeActivityViewModel @AssistedInject constructor(
// } // }
when { when {
!vectorPreferences.areThreadMessagesEnabled() && !vectorPreferences.wasThreadFlagChangedManually() -> {
vectorPreferences.setThreadMessagesEnabled()
lightweightSettingsStorage.setThreadMessagesEnabled(vectorPreferences.areThreadMessagesEnabled())
// Clear Cache
_viewEvents.post(HomeActivityViewEvents.MigrateThreads(checkSession = false))
}
// Notify users // Notify users
vectorPreferences.shouldNotifyUserAboutThreads() && vectorPreferences.areThreadMessagesEnabled() -> { vectorPreferences.shouldNotifyUserAboutThreads() && vectorPreferences.areThreadMessagesEnabled() -> {
Timber.i("----> Notify users about threads") Timber.i("----> Notify users about threads")

View file

@ -20,9 +20,15 @@ import dagger.Lazy
import im.vector.app.EmojiSpanify import im.vector.app.EmojiSpanify
import im.vector.app.R import im.vector.app.R
import im.vector.app.core.extensions.getVectorLastMessageContent import im.vector.app.core.extensions.getVectorLastMessageContent
import im.vector.app.core.extensions.orEmpty
import im.vector.app.core.resources.ColorProvider import im.vector.app.core.resources.ColorProvider
import im.vector.app.core.resources.DrawableProvider
import im.vector.app.core.resources.StringProvider import im.vector.app.core.resources.StringProvider
import im.vector.app.features.html.EventHtmlRenderer import im.vector.app.features.html.EventHtmlRenderer
import im.vector.app.features.voicebroadcast.VoiceBroadcastConstants
import im.vector.app.features.voicebroadcast.isLive
import im.vector.app.features.voicebroadcast.model.asVoiceBroadcastEvent
import me.gujun.android.span.image
import me.gujun.android.span.span import me.gujun.android.span.span
import org.commonmark.node.Document import org.commonmark.node.Document
import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.events.model.Event
@ -41,6 +47,7 @@ import javax.inject.Inject
class DisplayableEventFormatter @Inject constructor( class DisplayableEventFormatter @Inject constructor(
private val stringProvider: StringProvider, private val stringProvider: StringProvider,
private val colorProvider: ColorProvider, private val colorProvider: ColorProvider,
private val drawableProvider: DrawableProvider,
private val emojiSpanify: EmojiSpanify, private val emojiSpanify: EmojiSpanify,
private val noticeEventFormatter: NoticeEventFormatter, private val noticeEventFormatter: NoticeEventFormatter,
private val htmlRenderer: Lazy<EventHtmlRenderer> private val htmlRenderer: Lazy<EventHtmlRenderer>
@ -135,6 +142,9 @@ class DisplayableEventFormatter @Inject constructor(
in EventType.STATE_ROOM_BEACON_INFO.values -> { in EventType.STATE_ROOM_BEACON_INFO.values -> {
simpleFormat(senderName, stringProvider.getString(R.string.sent_live_location), appendAuthor) simpleFormat(senderName, stringProvider.getString(R.string.sent_live_location), appendAuthor)
} }
VoiceBroadcastConstants.STATE_ROOM_VOICE_BROADCAST_INFO -> {
formatVoiceBroadcastEvent(timelineEvent.root, isDm, senderName)
}
else -> { else -> {
span { span {
text = noticeEventFormatter.format(timelineEvent, isDm) ?: "" text = noticeEventFormatter.format(timelineEvent, isDm) ?: ""
@ -252,4 +262,20 @@ class DisplayableEventFormatter @Inject constructor(
body body
} }
} }
private fun formatVoiceBroadcastEvent(event: Event, isDm: Boolean, senderName: String): CharSequence {
return if (event.asVoiceBroadcastEvent()?.isLive == true) {
span {
drawableProvider.getDrawable(R.drawable.ic_voice_broadcast, colorProvider.getColor(R.color.palette_vermilion))?.let {
image(it)
+" "
}
span(stringProvider.getString(R.string.voice_broadcast_live_broadcast)) {
textColor = colorProvider.getColor(R.color.palette_vermilion)
}
}
} else {
noticeEventFormatter.format(event, senderName, isDm).orEmpty()
}
}
} }

View file

@ -22,6 +22,8 @@ import im.vector.app.core.resources.StringProvider
import im.vector.app.features.roomprofile.permissions.RoleFormatter import im.vector.app.features.roomprofile.permissions.RoleFormatter
import im.vector.app.features.settings.VectorPreferences import im.vector.app.features.settings.VectorPreferences
import im.vector.app.features.voicebroadcast.VoiceBroadcastConstants import im.vector.app.features.voicebroadcast.VoiceBroadcastConstants
import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState
import im.vector.app.features.voicebroadcast.model.asVoiceBroadcastEvent
import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_MEGOLM import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_MEGOLM
import org.matrix.android.sdk.api.extensions.appendNl import org.matrix.android.sdk.api.extensions.appendNl
import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.extensions.orFalse
@ -67,30 +69,32 @@ class NoticeEventFormatter @Inject constructor(
private fun Event.isSentByCurrentUser() = senderId != null && senderId == currentUserId private fun Event.isSentByCurrentUser() = senderId != null && senderId == currentUserId
fun format(timelineEvent: TimelineEvent, isDm: Boolean): CharSequence? { fun format(timelineEvent: TimelineEvent, isDm: Boolean): CharSequence? {
return when (val type = timelineEvent.root.getClearType()) { val event = timelineEvent.root
EventType.STATE_ROOM_JOIN_RULES -> formatJoinRulesEvent(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName, isDm) val senderName = timelineEvent.senderInfo.disambiguatedDisplayName
EventType.STATE_ROOM_CREATE -> formatRoomCreateEvent(timelineEvent.root, isDm) return when (val type = event.getClearType()) {
EventType.STATE_ROOM_NAME -> formatRoomNameEvent(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName) EventType.STATE_ROOM_JOIN_RULES -> formatJoinRulesEvent(event, senderName, isDm)
EventType.STATE_ROOM_TOPIC -> formatRoomTopicEvent(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName) EventType.STATE_ROOM_CREATE -> formatRoomCreateEvent(event, isDm)
EventType.STATE_ROOM_AVATAR -> formatRoomAvatarEvent(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName) EventType.STATE_ROOM_NAME -> formatRoomNameEvent(event, senderName)
EventType.STATE_ROOM_MEMBER -> formatRoomMemberEvent(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName, isDm) EventType.STATE_ROOM_TOPIC -> formatRoomTopicEvent(event, senderName)
EventType.STATE_ROOM_THIRD_PARTY_INVITE -> formatRoomThirdPartyInvite(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName, isDm) EventType.STATE_ROOM_AVATAR -> formatRoomAvatarEvent(event, senderName)
EventType.STATE_ROOM_ALIASES -> formatRoomAliasesEvent(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName) EventType.STATE_ROOM_MEMBER -> formatRoomMemberEvent(event, senderName, isDm)
EventType.STATE_ROOM_CANONICAL_ALIAS -> formatRoomCanonicalAliasEvent(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName) EventType.STATE_ROOM_THIRD_PARTY_INVITE -> formatRoomThirdPartyInvite(event, senderName, isDm)
EventType.STATE_ROOM_HISTORY_VISIBILITY -> EventType.STATE_ROOM_ALIASES -> formatRoomAliasesEvent(event, senderName)
formatRoomHistoryVisibilityEvent(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName, isDm) EventType.STATE_ROOM_CANONICAL_ALIAS -> formatRoomCanonicalAliasEvent(event, senderName)
EventType.STATE_ROOM_SERVER_ACL -> formatRoomServerAclEvent(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName) EventType.STATE_ROOM_HISTORY_VISIBILITY -> formatRoomHistoryVisibilityEvent(event, senderName, isDm)
EventType.STATE_ROOM_GUEST_ACCESS -> formatRoomGuestAccessEvent(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName, isDm) EventType.STATE_ROOM_SERVER_ACL -> formatRoomServerAclEvent(event, senderName)
EventType.STATE_ROOM_ENCRYPTION -> formatRoomEncryptionEvent(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName) EventType.STATE_ROOM_GUEST_ACCESS -> formatRoomGuestAccessEvent(event, senderName, isDm)
EventType.STATE_ROOM_ENCRYPTION -> formatRoomEncryptionEvent(event, senderName)
EventType.STATE_ROOM_WIDGET, EventType.STATE_ROOM_WIDGET,
EventType.STATE_ROOM_WIDGET_LEGACY -> formatWidgetEvent(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName) EventType.STATE_ROOM_WIDGET_LEGACY -> formatWidgetEvent(event, senderName)
EventType.STATE_ROOM_TOMBSTONE -> formatRoomTombstoneEvent(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName, isDm) EventType.STATE_ROOM_TOMBSTONE -> formatRoomTombstoneEvent(event, senderName, isDm)
EventType.STATE_ROOM_POWER_LEVELS -> formatRoomPowerLevels(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName) EventType.STATE_ROOM_POWER_LEVELS -> formatRoomPowerLevels(event, senderName)
EventType.CALL_INVITE, EventType.CALL_INVITE,
EventType.CALL_CANDIDATES, EventType.CALL_CANDIDATES,
EventType.CALL_HANGUP, EventType.CALL_HANGUP,
EventType.CALL_REJECT, EventType.CALL_REJECT,
EventType.CALL_ANSWER -> formatCallEvent(type, timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName) EventType.CALL_ANSWER -> formatCallEvent(type, event, senderName)
VoiceBroadcastConstants.STATE_ROOM_VOICE_BROADCAST_INFO -> formatVoiceBroadcastEvent(event, senderName)
EventType.CALL_NEGOTIATE, EventType.CALL_NEGOTIATE,
EventType.CALL_SELECT_ANSWER, EventType.CALL_SELECT_ANSWER,
EventType.CALL_REPLACES, EventType.CALL_REPLACES,
@ -109,8 +113,7 @@ class NoticeEventFormatter @Inject constructor(
EventType.STICKER, EventType.STICKER,
in EventType.POLL_RESPONSE.values, in EventType.POLL_RESPONSE.values,
in EventType.POLL_END.values, in EventType.POLL_END.values,
in EventType.BEACON_LOCATION_DATA.values, in EventType.BEACON_LOCATION_DATA.values -> formatDebug(event)
VoiceBroadcastConstants.STATE_ROOM_VOICE_BROADCAST_INFO -> formatDebug(timelineEvent.root)
else -> { else -> {
Timber.v("Type $type not handled by this formatter") Timber.v("Type $type not handled by this formatter")
null null
@ -191,6 +194,7 @@ class NoticeEventFormatter @Inject constructor(
EventType.CALL_REJECT, EventType.CALL_REJECT,
EventType.CALL_ANSWER -> formatCallEvent(type, event, senderName) EventType.CALL_ANSWER -> formatCallEvent(type, event, senderName)
EventType.STATE_ROOM_TOMBSTONE -> formatRoomTombstoneEvent(event, senderName, isDm) EventType.STATE_ROOM_TOMBSTONE -> formatRoomTombstoneEvent(event, senderName, isDm)
VoiceBroadcastConstants.STATE_ROOM_VOICE_BROADCAST_INFO -> formatVoiceBroadcastEvent(event, senderName)
else -> { else -> {
Timber.v("Type $type not handled by this formatter") Timber.v("Type $type not handled by this formatter")
null null
@ -894,4 +898,16 @@ class NoticeEventFormatter @Inject constructor(
} }
} }
} }
private fun formatVoiceBroadcastEvent(event: Event, senderName: String?): CharSequence {
return if (event.asVoiceBroadcastEvent()?.content?.voiceBroadcastState == VoiceBroadcastState.STOPPED) {
if (event.isSentByCurrentUser()) {
sp.getString(R.string.notice_voice_broadcast_ended_by_you)
} else {
sp.getString(R.string.notice_voice_broadcast_ended, senderName)
}
} else {
formatDebug(event)
}
}
} }

View file

@ -252,7 +252,7 @@ class TimelineEventVisibilityHelper @Inject constructor(
} }
if (root.getClearType() == VoiceBroadcastConstants.STATE_ROOM_VOICE_BROADCAST_INFO && if (root.getClearType() == VoiceBroadcastConstants.STATE_ROOM_VOICE_BROADCAST_INFO &&
root.asVoiceBroadcastEvent()?.content?.voiceBroadcastState != VoiceBroadcastState.STARTED) { root.asVoiceBroadcastEvent()?.content?.voiceBroadcastState !in arrayOf(VoiceBroadcastState.STARTED, VoiceBroadcastState.STOPPED)) {
return true return true
} }

View file

@ -22,6 +22,7 @@ import com.airbnb.mvrx.Loading
import im.vector.app.R import im.vector.app.R
import im.vector.app.core.date.DateFormatKind import im.vector.app.core.date.DateFormatKind
import im.vector.app.core.date.VectorDateFormatter import im.vector.app.core.date.VectorDateFormatter
import im.vector.app.core.di.ActiveSessionHolder
import im.vector.app.core.epoxy.VectorEpoxyModel import im.vector.app.core.epoxy.VectorEpoxyModel
import im.vector.app.core.error.ErrorFormatter import im.vector.app.core.error.ErrorFormatter
import im.vector.app.core.resources.StringProvider import im.vector.app.core.resources.StringProvider
@ -29,21 +30,33 @@ import im.vector.app.features.home.AvatarRenderer
import im.vector.app.features.home.RoomListDisplayMode import im.vector.app.features.home.RoomListDisplayMode
import im.vector.app.features.home.room.detail.timeline.format.DisplayableEventFormatter import im.vector.app.features.home.room.detail.timeline.format.DisplayableEventFormatter
import im.vector.app.features.home.room.typing.TypingHelper import im.vector.app.features.home.room.typing.TypingHelper
import im.vector.app.features.voicebroadcast.isLive
import im.vector.app.features.voicebroadcast.isVoiceBroadcast
import im.vector.app.features.voicebroadcast.model.asVoiceBroadcastEvent
import im.vector.app.features.voicebroadcast.usecase.GetRoomLiveVoiceBroadcastsUseCase
import im.vector.lib.core.utils.epoxy.charsequence.toEpoxyCharSequence import im.vector.lib.core.utils.epoxy.charsequence.toEpoxyCharSequence
import org.matrix.android.sdk.api.extensions.orFalse
import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.api.session.getRoom
import org.matrix.android.sdk.api.session.room.getTimelineEvent
import org.matrix.android.sdk.api.session.room.members.ChangeMembershipState import org.matrix.android.sdk.api.session.room.members.ChangeMembershipState
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.RoomSummary import org.matrix.android.sdk.api.session.room.model.RoomSummary
import org.matrix.android.sdk.api.session.room.model.SpaceChildInfo import org.matrix.android.sdk.api.session.room.model.SpaceChildInfo
import org.matrix.android.sdk.api.session.room.model.message.asMessageAudioEvent
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
import org.matrix.android.sdk.api.util.toMatrixItem import org.matrix.android.sdk.api.util.toMatrixItem
import javax.inject.Inject import javax.inject.Inject
class RoomSummaryItemFactory @Inject constructor( class RoomSummaryItemFactory @Inject constructor(
private val sessionHolder: ActiveSessionHolder,
private val displayableEventFormatter: DisplayableEventFormatter, private val displayableEventFormatter: DisplayableEventFormatter,
private val dateFormatter: VectorDateFormatter, private val dateFormatter: VectorDateFormatter,
private val stringProvider: StringProvider, private val stringProvider: StringProvider,
private val typingHelper: TypingHelper, private val typingHelper: TypingHelper,
private val avatarRenderer: AvatarRenderer, private val avatarRenderer: AvatarRenderer,
private val errorFormatter: ErrorFormatter private val errorFormatter: ErrorFormatter,
private val getRoomLiveVoiceBroadcastsUseCase: GetRoomLiveVoiceBroadcastsUseCase,
) { ) {
fun create( fun create(
@ -129,13 +142,15 @@ class RoomSummaryItemFactory @Inject constructor(
val showSelected = selectedRoomIds.contains(roomSummary.roomId) val showSelected = selectedRoomIds.contains(roomSummary.roomId)
var latestFormattedEvent: CharSequence = "" var latestFormattedEvent: CharSequence = ""
var latestEventTime = "" var latestEventTime = ""
val latestEvent = roomSummary.latestPreviewableEvent val latestEvent = roomSummary.getVectorLatestPreviewableEvent()
if (latestEvent != null) { if (latestEvent != null) {
latestFormattedEvent = displayableEventFormatter.format(latestEvent, roomSummary.isDirect, roomSummary.isDirect.not()) latestFormattedEvent = displayableEventFormatter.format(latestEvent, roomSummary.isDirect, roomSummary.isDirect.not())
latestEventTime = dateFormatter.format(latestEvent.root.originServerTs, DateFormatKind.ROOM_LIST) latestEventTime = dateFormatter.format(latestEvent.root.originServerTs, DateFormatKind.ROOM_LIST)
} }
val typingMessage = typingHelper.getTypingMessage(roomSummary.typingUsers) val typingMessage = typingHelper.getTypingMessage(roomSummary.typingUsers)
// Skip typing while there is a live voice broadcast
.takeUnless { latestEvent?.root?.asVoiceBroadcastEvent()?.isLive.orFalse() }.orEmpty()
return if (subtitle.isBlank() && displayMode == RoomListDisplayMode.FILTERED) { return if (subtitle.isBlank() && displayMode == RoomListDisplayMode.FILTERED) {
createCenteredRoomSummaryItem(roomSummary, displayMode, showSelected, unreadCount, onClick, onLongClick) createCenteredRoomSummaryItem(roomSummary, displayMode, showSelected, unreadCount, onClick, onLongClick)
@ -225,4 +240,14 @@ class RoomSummaryItemFactory @Inject constructor(
else -> stringProvider.getQuantityString(R.plurals.search_space_multiple_parents, size - 1, directParentNames[0], size - 1) else -> stringProvider.getQuantityString(R.plurals.search_space_multiple_parents, size - 1, directParentNames[0], size - 1)
} }
} }
private fun RoomSummary.getVectorLatestPreviewableEvent(): TimelineEvent? {
val room = sessionHolder.getSafeActiveSession()?.getRoom(roomId) ?: return latestPreviewableEvent
val liveVoiceBroadcastTimelineEvent = getRoomLiveVoiceBroadcastsUseCase.execute(roomId).lastOrNull()
?.root?.eventId?.let { room.getTimelineEvent(it) }
return latestPreviewableEvent?.takeIf { it.root.getClearType() == EventType.CALL_INVITE }
?: liveVoiceBroadcastTimelineEvent
?: latestPreviewableEvent
?.takeUnless { it.root.asMessageAudioEvent()?.isVoiceBroadcast().orFalse() } // Skip voice messages related to voice broadcast
}
} }

View file

@ -19,24 +19,18 @@ package im.vector.app.features.home.room.threads.list.viewmodel
import com.airbnb.epoxy.EpoxyController import com.airbnb.epoxy.EpoxyController
import im.vector.app.core.date.DateFormatKind import im.vector.app.core.date.DateFormatKind
import im.vector.app.core.date.VectorDateFormatter import im.vector.app.core.date.VectorDateFormatter
import im.vector.app.core.resources.StringProvider
import im.vector.app.features.home.AvatarRenderer import im.vector.app.features.home.AvatarRenderer
import im.vector.app.features.home.room.detail.timeline.format.DisplayableEventFormatter import im.vector.app.features.home.room.detail.timeline.format.DisplayableEventFormatter
import im.vector.app.features.home.room.threads.list.model.threadListItem import im.vector.app.features.home.room.threads.list.model.threadListItem
import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.room.threads.model.ThreadSummary
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.threads.ThreadNotificationState import org.matrix.android.sdk.api.session.threads.ThreadNotificationState
import org.matrix.android.sdk.api.util.toMatrixItem import org.matrix.android.sdk.api.util.toMatrixItem
import org.matrix.android.sdk.api.util.toMatrixItemOrNull
import javax.inject.Inject import javax.inject.Inject
class ThreadListController @Inject constructor( class ThreadListController @Inject constructor(
private val avatarRenderer: AvatarRenderer, private val avatarRenderer: AvatarRenderer,
private val stringProvider: StringProvider,
private val dateFormatter: VectorDateFormatter, private val dateFormatter: VectorDateFormatter,
private val displayableEventFormatter: DisplayableEventFormatter, private val displayableEventFormatter: DisplayableEventFormatter,
private val session: Session
) : EpoxyController() { ) : EpoxyController() {
var listener: Listener? = null var listener: Listener? = null
@ -48,64 +42,7 @@ class ThreadListController @Inject constructor(
requestModelBuild() requestModelBuild()
} }
override fun buildModels() = override fun buildModels() {
when (session.homeServerCapabilitiesService().getHomeServerCapabilities().canUseThreading) {
true -> buildThreadSummaries()
false -> buildThreadList()
}
/**
* Building thread summaries when homeserver supports threading.
*/
private fun buildThreadSummaries() {
val safeViewState = viewState ?: return
val host = this
safeViewState.threadSummaryList.invoke()
?.filter {
if (safeViewState.shouldFilterThreads) {
it.isUserParticipating
} else {
true
}
}
?.forEach { threadSummary ->
val date = dateFormatter.format(threadSummary.latestEvent?.originServerTs, DateFormatKind.ROOM_LIST)
val lastMessageFormatted = threadSummary.let {
displayableEventFormatter.formatThreadSummary(
event = it.latestEvent,
latestEdition = it.threadEditions.latestThreadEdition
).toString()
}
val rootMessageFormatted = threadSummary.let {
displayableEventFormatter.formatThreadSummary(
event = it.rootEvent,
latestEdition = it.threadEditions.rootThreadEdition
).toString()
}
threadListItem {
id(threadSummary.rootEvent?.eventId)
avatarRenderer(host.avatarRenderer)
matrixItem(threadSummary.rootThreadSenderInfo.toMatrixItem())
title(threadSummary.rootThreadSenderInfo.displayName.orEmpty())
date(date)
rootMessageDeleted(threadSummary.rootEvent?.isRedacted() ?: false)
// TODO refactor notifications that with the new thread summary
threadNotificationState(threadSummary.rootEvent?.threadDetails?.threadNotificationState ?: ThreadNotificationState.NO_NEW_MESSAGE)
rootMessage(rootMessageFormatted)
lastMessage(lastMessageFormatted)
lastMessageCounter(threadSummary.numberOfThreads.toString())
lastMessageMatrixItem(threadSummary.latestThreadSenderInfo.toMatrixItemOrNull())
itemClickListener {
host.listener?.onThreadSummaryClicked(threadSummary)
}
}
}
}
/**
* Building local thread list when homeserver do not support threading.
*/
private fun buildThreadList() {
val safeViewState = viewState ?: return val safeViewState = viewState ?: return
val host = this val host = this
safeViewState.rootThreadEventList.invoke() safeViewState.rootThreadEventList.invoke()
@ -152,7 +89,6 @@ class ThreadListController @Inject constructor(
} }
interface Listener { interface Listener {
fun onThreadSummaryClicked(threadSummary: ThreadSummary)
fun onThreadListClicked(timelineEvent: TimelineEvent) fun onThreadListClicked(timelineEvent: TimelineEvent)
} }
} }

View file

@ -0,0 +1,84 @@
/*
* Copyright 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.threads.list.viewmodel
import com.airbnb.epoxy.EpoxyModel
import com.airbnb.epoxy.paging.PagedListEpoxyController
import im.vector.app.core.date.DateFormatKind
import im.vector.app.core.date.VectorDateFormatter
import im.vector.app.core.utils.createUIHandler
import im.vector.app.features.home.AvatarRenderer
import im.vector.app.features.home.room.detail.timeline.format.DisplayableEventFormatter
import im.vector.app.features.home.room.threads.list.model.ThreadListItem_
import org.matrix.android.sdk.api.session.room.threads.model.ThreadSummary
import org.matrix.android.sdk.api.session.threads.ThreadNotificationState
import org.matrix.android.sdk.api.util.toMatrixItem
import org.matrix.android.sdk.api.util.toMatrixItemOrNull
import javax.inject.Inject
class ThreadListPagedController @Inject constructor(
private val avatarRenderer: AvatarRenderer,
private val dateFormatter: VectorDateFormatter,
private val displayableEventFormatter: DisplayableEventFormatter,
) : PagedListEpoxyController<ThreadSummary>(
// Important it must match the PageList builder notify Looper
modelBuildingHandler = createUIHandler()
) {
var listener: Listener? = null
override fun buildItemModel(currentPosition: Int, item: ThreadSummary?): EpoxyModel<*> {
if (item == null) {
throw java.lang.NullPointerException()
}
val host = this
val date = dateFormatter.format(item.latestEvent?.originServerTs, DateFormatKind.ROOM_LIST)
val lastMessageFormatted = item.let {
displayableEventFormatter.formatThreadSummary(
event = it.latestEvent,
latestEdition = it.threadEditions.latestThreadEdition
).toString()
}
val rootMessageFormatted = item.let {
displayableEventFormatter.formatThreadSummary(
event = it.rootEvent,
latestEdition = it.threadEditions.rootThreadEdition
).toString()
}
return ThreadListItem_()
.id(item.rootEvent?.eventId)
.avatarRenderer(host.avatarRenderer)
.matrixItem(item.rootThreadSenderInfo.toMatrixItem())
.title(item.rootThreadSenderInfo.displayName.orEmpty())
.date(date)
.rootMessageDeleted(item.rootEvent?.isRedacted() ?: false)
// TODO refactor notifications that with the new thread summary
.threadNotificationState(item.rootEvent?.threadDetails?.threadNotificationState ?: ThreadNotificationState.NO_NEW_MESSAGE)
.rootMessage(rootMessageFormatted)
.lastMessage(lastMessageFormatted)
.lastMessageCounter(item.numberOfThreads.toString())
.lastMessageMatrixItem(item.latestThreadSenderInfo.toMatrixItemOrNull())
.itemClickListener {
host.listener?.onThreadSummaryClicked(item)
}
}
interface Listener {
fun onThreadSummaryClicked(threadSummary: ThreadSummary)
}
}

View file

@ -16,6 +16,11 @@
package im.vector.app.features.home.room.threads.list.viewmodel package im.vector.app.features.home.room.threads.list.viewmodel
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.Observer
import androidx.lifecycle.asFlow
import androidx.paging.PagedList
import com.airbnb.mvrx.FragmentViewModelContext import com.airbnb.mvrx.FragmentViewModelContext
import com.airbnb.mvrx.MavericksViewModelFactory import com.airbnb.mvrx.MavericksViewModelFactory
import com.airbnb.mvrx.ViewModelContext import com.airbnb.mvrx.ViewModelContext
@ -29,23 +34,47 @@ import im.vector.app.features.analytics.AnalyticsTracker
import im.vector.app.features.analytics.extensions.toAnalyticsInteraction import im.vector.app.features.analytics.extensions.toAnalyticsInteraction
import im.vector.app.features.analytics.plan.Interaction import im.vector.app.features.analytics.plan.Interaction
import im.vector.app.features.home.room.threads.list.views.ThreadListFragment import im.vector.app.features.home.room.threads.list.views.ThreadListFragment
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
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 org.matrix.android.sdk.api.session.getRoom import org.matrix.android.sdk.api.session.getRoom
import org.matrix.android.sdk.api.session.room.threads.FetchThreadsResult
import org.matrix.android.sdk.api.session.room.threads.ThreadFilter
import org.matrix.android.sdk.api.session.room.threads.model.ThreadSummary
import org.matrix.android.sdk.api.session.threads.ThreadTimelineEvent import org.matrix.android.sdk.api.session.threads.ThreadTimelineEvent
import org.matrix.android.sdk.flow.flow import org.matrix.android.sdk.flow.flow
class ThreadListViewModel @AssistedInject constructor( class ThreadListViewModel @AssistedInject constructor(
@Assisted val initialState: ThreadListViewState, @Assisted val initialState: ThreadListViewState,
private val analyticsTracker: AnalyticsTracker, private val analyticsTracker: AnalyticsTracker,
private val session: Session private val session: Session,
) : ) : VectorViewModel<ThreadListViewState, EmptyAction, EmptyViewEvents>(initialState) {
VectorViewModel<ThreadListViewState, EmptyAction, EmptyViewEvents>(initialState) {
private val room = session.getRoom(initialState.roomId) private val room = session.getRoom(initialState.roomId)
private val defaultPagedListConfig = PagedList.Config.Builder()
.setPageSize(20)
.setInitialLoadSizeHint(40)
.setEnablePlaceholders(false)
.setPrefetchDistance(10)
.build()
private var nextBatchId: String? = null
private var hasReachedEnd: Boolean = false
private var boundariesJob: Job? = null
private var livePagedList: LiveData<PagedList<ThreadSummary>>? = null
private val _threadsLivePagedList = MutableLiveData<PagedList<ThreadSummary>>()
val threadsLivePagedList: LiveData<PagedList<ThreadSummary>> = _threadsLivePagedList
private val internalPagedListObserver = Observer<PagedList<ThreadSummary>> {
_threadsLivePagedList.postValue(it)
setLoading(false)
}
@AssistedFactory @AssistedFactory
interface Factory { interface Factory {
fun create(initialState: ThreadListViewState): ThreadListViewModel fun create(initialState: ThreadListViewState): ThreadListViewModel
@ -54,7 +83,7 @@ class ThreadListViewModel @AssistedInject constructor(
companion object : MavericksViewModelFactory<ThreadListViewModel, ThreadListViewState> { companion object : MavericksViewModelFactory<ThreadListViewModel, ThreadListViewState> {
@JvmStatic @JvmStatic
override fun create(viewModelContext: ViewModelContext, state: ThreadListViewState): ThreadListViewModel? { override fun create(viewModelContext: ViewModelContext, state: ThreadListViewState): ThreadListViewModel {
val fragment: ThreadListFragment = (viewModelContext as FragmentViewModelContext).fragment() val fragment: ThreadListFragment = (viewModelContext as FragmentViewModelContext).fragment()
return fragment.threadListViewModelFactory.create(state) return fragment.threadListViewModelFactory.create(state)
} }
@ -72,7 +101,7 @@ class ThreadListViewModel @AssistedInject constructor(
private fun fetchAndObserveThreads() { private fun fetchAndObserveThreads() {
when (session.homeServerCapabilitiesService().getHomeServerCapabilities().canUseThreading) { when (session.homeServerCapabilitiesService().getHomeServerCapabilities().canUseThreading) {
true -> { true -> {
fetchThreadList() setLoading(true)
observeThreadSummaries() observeThreadSummaries()
} }
false -> observeThreadsList() false -> observeThreadsList()
@ -82,14 +111,33 @@ class ThreadListViewModel @AssistedInject constructor(
/** /**
* Observing thread summaries when homeserver support threading. * Observing thread summaries when homeserver support threading.
*/ */
private fun observeThreadSummaries() { private fun observeThreadSummaries() = withState { state ->
room?.flow() viewModelScope.launch {
?.liveThreadSummaries() nextBatchId = null
?.map { room.threadsService().enhanceThreadWithEditions(it) } hasReachedEnd = false
?.flowOn(room.coroutineDispatchers.io)
?.execute { asyncThreads -> livePagedList?.removeObserver(internalPagedListObserver)
copy(threadSummaryList = asyncThreads)
} room?.threadsService()
?.getPagedThreadsList(state.shouldFilterThreads, defaultPagedListConfig)?.let { result ->
livePagedList = result.livePagedList
livePagedList?.observeForever(internalPagedListObserver)
boundariesJob = result.liveBoundaries.asFlow()
.onEach {
if (it.endLoaded) {
if (!hasReachedEnd) {
fetchNextPage()
}
}
}
.launchIn(viewModelScope)
}
setLoading(true)
fetchNextPage()
}
} }
/** /**
@ -111,14 +159,6 @@ class ThreadListViewModel @AssistedInject constructor(
} }
} }
private fun fetchThreadList() {
viewModelScope.launch {
setLoading(true)
room?.threadsService()?.fetchThreadSummaries()
setLoading(false)
}
}
private fun setLoading(isLoading: Boolean) { private fun setLoading(isLoading: Boolean) {
setState { setState {
copy(isLoading = isLoading) copy(isLoading = isLoading)
@ -132,5 +172,30 @@ class ThreadListViewModel @AssistedInject constructor(
setState { setState {
copy(shouldFilterThreads = shouldFilterThreads) copy(shouldFilterThreads = shouldFilterThreads)
} }
fetchAndObserveThreads()
}
private suspend fun fetchNextPage() {
val filter = when (awaitState().shouldFilterThreads) {
true -> ThreadFilter.PARTICIPATED
false -> ThreadFilter.ALL
}
room?.threadsService()?.fetchThreadList(
nextBatchId = nextBatchId,
limit = defaultPagedListConfig.pageSize,
filter = filter,
).let { result ->
when (result) {
is FetchThreadsResult.ReachedEnd -> {
hasReachedEnd = true
}
is FetchThreadsResult.ShouldFetchMore -> {
nextBatchId = result.nextBatch
}
else -> {
}
}
}
} }
} }

View file

@ -20,11 +20,9 @@ import com.airbnb.mvrx.Async
import com.airbnb.mvrx.MavericksState import com.airbnb.mvrx.MavericksState
import com.airbnb.mvrx.Uninitialized import com.airbnb.mvrx.Uninitialized
import im.vector.app.features.home.room.threads.arguments.ThreadListArgs import im.vector.app.features.home.room.threads.arguments.ThreadListArgs
import org.matrix.android.sdk.api.session.room.threads.model.ThreadSummary
import org.matrix.android.sdk.api.session.threads.ThreadTimelineEvent import org.matrix.android.sdk.api.session.threads.ThreadTimelineEvent
data class ThreadListViewState( data class ThreadListViewState(
val threadSummaryList: Async<List<ThreadSummary>> = Uninitialized,
val rootThreadEventList: Async<List<ThreadTimelineEvent>> = Uninitialized, val rootThreadEventList: Async<List<ThreadTimelineEvent>> = Uninitialized,
val shouldFilterThreads: Boolean = false, val shouldFilterThreads: Boolean = false,
val isLoading: Boolean = false, val isLoading: Boolean = false,

View file

@ -40,6 +40,7 @@ import im.vector.app.features.home.room.threads.ThreadsActivity
import im.vector.app.features.home.room.threads.arguments.ThreadListArgs import im.vector.app.features.home.room.threads.arguments.ThreadListArgs
import im.vector.app.features.home.room.threads.arguments.ThreadTimelineArgs import im.vector.app.features.home.room.threads.arguments.ThreadTimelineArgs
import im.vector.app.features.home.room.threads.list.viewmodel.ThreadListController import im.vector.app.features.home.room.threads.list.viewmodel.ThreadListController
import im.vector.app.features.home.room.threads.list.viewmodel.ThreadListPagedController
import im.vector.app.features.home.room.threads.list.viewmodel.ThreadListViewModel import im.vector.app.features.home.room.threads.list.viewmodel.ThreadListViewModel
import im.vector.app.features.home.room.threads.list.viewmodel.ThreadListViewState import im.vector.app.features.home.room.threads.list.viewmodel.ThreadListViewState
import im.vector.app.features.rageshake.BugReporter import im.vector.app.features.rageshake.BugReporter
@ -52,12 +53,14 @@ import javax.inject.Inject
@AndroidEntryPoint @AndroidEntryPoint
class ThreadListFragment : class ThreadListFragment :
VectorBaseFragment<FragmentThreadListBinding>(), VectorBaseFragment<FragmentThreadListBinding>(),
ThreadListPagedController.Listener,
ThreadListController.Listener, ThreadListController.Listener,
VectorMenuProvider { VectorMenuProvider {
@Inject lateinit var avatarRenderer: AvatarRenderer @Inject lateinit var avatarRenderer: AvatarRenderer
@Inject lateinit var bugReporter: BugReporter @Inject lateinit var bugReporter: BugReporter
@Inject lateinit var threadListController: ThreadListController @Inject lateinit var threadListController: ThreadListPagedController
@Inject lateinit var legacyThreadListController: ThreadListController
@Inject lateinit var threadListViewModelFactory: ThreadListViewModel.Factory @Inject lateinit var threadListViewModelFactory: ThreadListViewModel.Factory
private val threadListViewModel: ThreadListViewModel by fragmentViewModel() private val threadListViewModel: ThreadListViewModel by fragmentViewModel()
@ -100,7 +103,7 @@ class ThreadListFragment :
val filterBadge = filterIcon.findViewById<View>(R.id.threadListFilterBadge) val filterBadge = filterIcon.findViewById<View>(R.id.threadListFilterBadge)
filterBadge.isVisible = state.shouldFilterThreads filterBadge.isVisible = state.shouldFilterThreads
when (threadListViewModel.canHomeserverUseThreading()) { when (threadListViewModel.canHomeserverUseThreading()) {
true -> menu.findItem(R.id.menu_thread_list_filter).isVisible = !state.threadSummaryList.invoke().isNullOrEmpty() true -> menu.findItem(R.id.menu_thread_list_filter).isVisible = true
false -> menu.findItem(R.id.menu_thread_list_filter).isVisible = !state.rootThreadEventList.invoke().isNullOrEmpty() false -> menu.findItem(R.id.menu_thread_list_filter).isVisible = !state.rootThreadEventList.invoke().isNullOrEmpty()
} }
} }
@ -111,8 +114,18 @@ class ThreadListFragment :
initToolbar() initToolbar()
initTextConstants() initTextConstants()
initBetaFeedback() initBetaFeedback()
views.threadListRecyclerView.configureWith(threadListController, TimelineItemAnimator(), hasFixedSize = false)
threadListController.listener = this if (threadListViewModel.canHomeserverUseThreading()) {
views.threadListRecyclerView.configureWith(threadListController, TimelineItemAnimator(), hasFixedSize = false)
threadListController.listener = this
threadListViewModel.threadsLivePagedList.observe(viewLifecycleOwner) { threadsList ->
threadListController.submitList(threadsList)
}
} else {
views.threadListRecyclerView.configureWith(legacyThreadListController, TimelineItemAnimator(), hasFixedSize = false)
legacyThreadListController.listener = this
}
} }
override fun onDestroyView() { override fun onDestroyView() {
@ -144,7 +157,9 @@ class ThreadListFragment :
override fun invalidate() = withState(threadListViewModel) { state -> override fun invalidate() = withState(threadListViewModel) { state ->
invalidateOptionsMenu() invalidateOptionsMenu()
renderEmptyStateIfNeeded(state) renderEmptyStateIfNeeded(state)
threadListController.update(state) if (!threadListViewModel.canHomeserverUseThreading()) {
legacyThreadListController.update(state)
}
renderLoaderIfNeeded(state) renderLoaderIfNeeded(state)
} }
@ -185,7 +200,7 @@ class ThreadListFragment :
private fun renderEmptyStateIfNeeded(state: ThreadListViewState) { private fun renderEmptyStateIfNeeded(state: ThreadListViewState) {
when (threadListViewModel.canHomeserverUseThreading()) { when (threadListViewModel.canHomeserverUseThreading()) {
true -> views.threadListEmptyConstraintLayout.isVisible = state.threadSummaryList.invoke().isNullOrEmpty() true -> views.threadListEmptyConstraintLayout.isVisible = false
false -> views.threadListEmptyConstraintLayout.isVisible = state.rootThreadEventList.invoke().isNullOrEmpty() false -> views.threadListEmptyConstraintLayout.isVisible = state.rootThreadEventList.invoke().isNullOrEmpty()
} }
} }

View file

@ -18,7 +18,7 @@ package im.vector.app.features.room
import android.content.Context import android.content.Context
import im.vector.app.R import im.vector.app.R
import org.matrix.android.sdk.api.RoomDisplayNameFallbackProvider import org.matrix.android.sdk.api.provider.RoomDisplayNameFallbackProvider
import javax.inject.Inject import javax.inject.Inject
class VectorRoomDisplayNameFallbackProvider @Inject constructor( class VectorRoomDisplayNameFallbackProvider @Inject constructor(

View file

@ -239,6 +239,7 @@ class VectorPreferences @Inject constructor(
// This key will be used to identify clients with the new thread support enabled m.thread // This key will be used to identify clients with the new thread support enabled m.thread
const val SETTINGS_LABS_ENABLE_THREAD_MESSAGES = "SETTINGS_LABS_ENABLE_THREAD_MESSAGES_FINAL" const val SETTINGS_LABS_ENABLE_THREAD_MESSAGES = "SETTINGS_LABS_ENABLE_THREAD_MESSAGES_FINAL"
const val SETTINGS_LABS_THREAD_MESSAGES_CHANGED_BY_USER = "SETTINGS_LABS_THREAD_MESSAGES_CHANGED_BY_USER"
const val SETTINGS_THREAD_MESSAGES_SYNCED = "SETTINGS_THREAD_MESSAGES_SYNCED" const val SETTINGS_THREAD_MESSAGES_SYNCED = "SETTINGS_THREAD_MESSAGES_SYNCED"
// This key will be used to enable user for displaying live user info or not. // This key will be used to enable user for displaying live user info or not.
@ -1135,6 +1136,24 @@ class VectorPreferences @Inject constructor(
.apply() .apply()
} }
/**
* Indicates whether or not user changed threads flag manually. We need this to not force flag to be enabled on app start.
* Should be removed when Threads flag will be removed
*/
fun wasThreadFlagChangedManually(): Boolean {
return defaultPrefs.getBoolean(SETTINGS_LABS_THREAD_MESSAGES_CHANGED_BY_USER, false)
}
/**
* Sets the flag to indicate that user changed threads flag (e.g. disabled them).
*/
fun setThreadFlagChangedManually() {
defaultPrefs
.edit()
.putBoolean(SETTINGS_LABS_THREAD_MESSAGES_CHANGED_BY_USER, true)
.apply()
}
/** /**
* Indicates whether or not the user will be notified about the new thread support. * Indicates whether or not the user will be notified about the new thread support.
* We should notify the user only if he had old thread support enabled. * We should notify the user only if he had old thread support enabled.

View file

@ -141,6 +141,7 @@ class VectorSettingsLabsFragment :
*/ */
private fun onThreadsPreferenceClicked() { private fun onThreadsPreferenceClicked() {
// We should migrate threads only if threads are disabled // We should migrate threads only if threads are disabled
vectorPreferences.setThreadFlagChangedManually()
vectorPreferences.setShouldMigrateThreads(!vectorPreferences.areThreadMessagesEnabled()) vectorPreferences.setShouldMigrateThreads(!vectorPreferences.areThreadMessagesEnabled())
lightweightSettingsStorage.setThreadMessagesEnabled(vectorPreferences.areThreadMessagesEnabled()) lightweightSettingsStorage.setThreadMessagesEnabled(vectorPreferences.areThreadMessagesEnabled())
displayLoadingView() displayLoadingView()

View file

@ -31,7 +31,7 @@ import im.vector.app.features.voicebroadcast.listening.VoiceBroadcastPlayer.Stat
import im.vector.app.features.voicebroadcast.listening.usecase.GetLiveVoiceBroadcastChunksUseCase import im.vector.app.features.voicebroadcast.listening.usecase.GetLiveVoiceBroadcastChunksUseCase
import im.vector.app.features.voicebroadcast.model.VoiceBroadcast import im.vector.app.features.voicebroadcast.model.VoiceBroadcast
import im.vector.app.features.voicebroadcast.model.VoiceBroadcastEvent import im.vector.app.features.voicebroadcast.model.VoiceBroadcastEvent
import im.vector.app.features.voicebroadcast.usecase.GetMostRecentVoiceBroadcastStateEventUseCase import im.vector.app.features.voicebroadcast.usecase.GetVoiceBroadcastStateEventLiveUseCase
import im.vector.lib.core.utils.timer.CountUpTimer import im.vector.lib.core.utils.timer.CountUpTimer
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
@ -48,7 +48,7 @@ import javax.inject.Singleton
class VoiceBroadcastPlayerImpl @Inject constructor( class VoiceBroadcastPlayerImpl @Inject constructor(
private val sessionHolder: ActiveSessionHolder, private val sessionHolder: ActiveSessionHolder,
private val playbackTracker: AudioMessagePlaybackTracker, private val playbackTracker: AudioMessagePlaybackTracker,
private val getVoiceBroadcastEventUseCase: GetMostRecentVoiceBroadcastStateEventUseCase, private val getVoiceBroadcastEventUseCase: GetVoiceBroadcastStateEventLiveUseCase,
private val getLiveVoiceBroadcastChunksUseCase: GetLiveVoiceBroadcastChunksUseCase private val getLiveVoiceBroadcastChunksUseCase: GetLiveVoiceBroadcastChunksUseCase
) : VoiceBroadcastPlayer { ) : VoiceBroadcastPlayer {

View file

@ -24,7 +24,7 @@ import im.vector.app.features.voicebroadcast.model.VoiceBroadcastEvent
import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState
import im.vector.app.features.voicebroadcast.model.asVoiceBroadcastEvent import im.vector.app.features.voicebroadcast.model.asVoiceBroadcastEvent
import im.vector.app.features.voicebroadcast.sequence import im.vector.app.features.voicebroadcast.sequence
import im.vector.app.features.voicebroadcast.usecase.GetMostRecentVoiceBroadcastStateEventUseCase import im.vector.app.features.voicebroadcast.usecase.GetVoiceBroadcastStateEventLiveUseCase
import im.vector.app.features.voicebroadcast.voiceBroadcastId import im.vector.app.features.voicebroadcast.voiceBroadcastId
import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
@ -48,7 +48,7 @@ import javax.inject.Inject
*/ */
class GetLiveVoiceBroadcastChunksUseCase @Inject constructor( class GetLiveVoiceBroadcastChunksUseCase @Inject constructor(
private val activeSessionHolder: ActiveSessionHolder, private val activeSessionHolder: ActiveSessionHolder,
private val getVoiceBroadcastEventUseCase: GetMostRecentVoiceBroadcastStateEventUseCase, private val getVoiceBroadcastEventUseCase: GetVoiceBroadcastStateEventLiveUseCase,
) { ) {
fun execute(voiceBroadcast: VoiceBroadcast): Flow<List<MessageAudioEvent>> { fun execute(voiceBroadcast: VoiceBroadcast): Flow<List<MessageAudioEvent>> {

View file

@ -26,7 +26,7 @@ import im.vector.app.features.voice.AbstractVoiceRecorderQ
import im.vector.app.features.voicebroadcast.model.VoiceBroadcast import im.vector.app.features.voicebroadcast.model.VoiceBroadcast
import im.vector.app.features.voicebroadcast.model.VoiceBroadcastEvent import im.vector.app.features.voicebroadcast.model.VoiceBroadcastEvent
import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState
import im.vector.app.features.voicebroadcast.usecase.GetMostRecentVoiceBroadcastStateEventUseCase import im.vector.app.features.voicebroadcast.usecase.GetVoiceBroadcastStateEventLiveUseCase
import im.vector.lib.core.utils.timer.CountUpTimer import im.vector.lib.core.utils.timer.CountUpTimer
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
@ -40,7 +40,7 @@ import java.util.concurrent.TimeUnit
class VoiceBroadcastRecorderQ( class VoiceBroadcastRecorderQ(
context: Context, context: Context,
private val sessionHolder: ActiveSessionHolder, private val sessionHolder: ActiveSessionHolder,
private val getVoiceBroadcastEventUseCase: GetMostRecentVoiceBroadcastStateEventUseCase private val getVoiceBroadcastEventUseCase: GetVoiceBroadcastStateEventLiveUseCase
) : AbstractVoiceRecorderQ(context), VoiceBroadcastRecorder { ) : AbstractVoiceRecorderQ(context), VoiceBroadcastRecorder {
private val session get() = sessionHolder.getActiveSession() private val session get() = sessionHolder.getActiveSession()

View file

@ -28,7 +28,7 @@ import im.vector.app.features.voicebroadcast.model.VoiceBroadcast
import im.vector.app.features.voicebroadcast.model.VoiceBroadcastChunk import im.vector.app.features.voicebroadcast.model.VoiceBroadcastChunk
import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState
import im.vector.app.features.voicebroadcast.recording.VoiceBroadcastRecorder import im.vector.app.features.voicebroadcast.recording.VoiceBroadcastRecorder
import im.vector.app.features.voicebroadcast.usecase.GetOngoingVoiceBroadcastsUseCase import im.vector.app.features.voicebroadcast.usecase.GetRoomLiveVoiceBroadcastsUseCase
import im.vector.lib.multipicker.utils.toMultiPickerAudioType import im.vector.lib.multipicker.utils.toMultiPickerAudioType
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -56,7 +56,7 @@ class StartVoiceBroadcastUseCase @Inject constructor(
private val voiceBroadcastRecorder: VoiceBroadcastRecorder?, private val voiceBroadcastRecorder: VoiceBroadcastRecorder?,
private val context: Context, private val context: Context,
private val buildMeta: BuildMeta, private val buildMeta: BuildMeta,
private val getOngoingVoiceBroadcastsUseCase: GetOngoingVoiceBroadcastsUseCase, private val getRoomLiveVoiceBroadcastsUseCase: GetRoomLiveVoiceBroadcastsUseCase,
private val stopVoiceBroadcastUseCase: StopVoiceBroadcastUseCase, private val stopVoiceBroadcastUseCase: StopVoiceBroadcastUseCase,
) { ) {
@ -152,7 +152,7 @@ class StartVoiceBroadcastUseCase @Inject constructor(
Timber.d("## StartVoiceBroadcastUseCase: Cannot start voice broadcast: another voice broadcast") Timber.d("## StartVoiceBroadcastUseCase: Cannot start voice broadcast: another voice broadcast")
throw VoiceBroadcastFailure.RecordingError.UserAlreadyBroadcasting throw VoiceBroadcastFailure.RecordingError.UserAlreadyBroadcasting
} }
getOngoingVoiceBroadcastsUseCase.execute(room.roomId).isNotEmpty() -> { getRoomLiveVoiceBroadcastsUseCase.execute(room.roomId).isNotEmpty() -> {
Timber.d("## StartVoiceBroadcastUseCase: Cannot start voice broadcast: user already broadcasting") Timber.d("## StartVoiceBroadcastUseCase: Cannot start voice broadcast: user already broadcasting")
throw VoiceBroadcastFailure.RecordingError.BlockedBySomeoneElse throw VoiceBroadcastFailure.RecordingError.BlockedBySomeoneElse
} }

View file

@ -19,7 +19,7 @@ package im.vector.app.features.voicebroadcast.recording.usecase
import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.core.di.ActiveSessionHolder
import im.vector.app.features.voicebroadcast.VoiceBroadcastHelper import im.vector.app.features.voicebroadcast.VoiceBroadcastHelper
import im.vector.app.features.voicebroadcast.model.asVoiceBroadcastEvent import im.vector.app.features.voicebroadcast.model.asVoiceBroadcastEvent
import im.vector.app.features.voicebroadcast.usecase.GetOngoingVoiceBroadcastsUseCase import im.vector.app.features.voicebroadcast.usecase.GetRoomLiveVoiceBroadcastsUseCase
import org.matrix.android.sdk.api.query.QueryStringValue import org.matrix.android.sdk.api.query.QueryStringValue
import org.matrix.android.sdk.api.session.getRoom import org.matrix.android.sdk.api.session.getRoom
import org.matrix.android.sdk.api.session.room.model.Membership import org.matrix.android.sdk.api.session.room.model.Membership
@ -32,7 +32,7 @@ import javax.inject.Inject
*/ */
class StopOngoingVoiceBroadcastUseCase @Inject constructor( class StopOngoingVoiceBroadcastUseCase @Inject constructor(
private val activeSessionHolder: ActiveSessionHolder, private val activeSessionHolder: ActiveSessionHolder,
private val getOngoingVoiceBroadcastsUseCase: GetOngoingVoiceBroadcastsUseCase, private val getRoomLiveVoiceBroadcastsUseCase: GetRoomLiveVoiceBroadcastsUseCase,
private val voiceBroadcastHelper: VoiceBroadcastHelper, private val voiceBroadcastHelper: VoiceBroadcastHelper,
) { ) {
@ -53,7 +53,7 @@ class StopOngoingVoiceBroadcastUseCase @Inject constructor(
recentRooms recentRooms
.forEach { room -> .forEach { room ->
val ongoingVoiceBroadcasts = getOngoingVoiceBroadcastsUseCase.execute(room.roomId) val ongoingVoiceBroadcasts = getRoomLiveVoiceBroadcastsUseCase.execute(room.roomId)
val myOngoingVoiceBroadcastId = ongoingVoiceBroadcasts.find { it.root.stateKey == session.myUserId }?.reference?.eventId val myOngoingVoiceBroadcastId = ongoingVoiceBroadcasts.find { it.root.stateKey == session.myUserId }?.reference?.eventId
val initialEvent = myOngoingVoiceBroadcastId?.let { room.timelineService().getTimelineEvent(it)?.root?.asVoiceBroadcastEvent() } val initialEvent = myOngoingVoiceBroadcastId?.let { room.timelineService().getTimelineEvent(it)?.root?.asVoiceBroadcastEvent() }
if (myOngoingVoiceBroadcastId != null && initialEvent?.content?.deviceId == session.sessionParams.deviceId) { if (myOngoingVoiceBroadcastId != null && initialEvent?.content?.deviceId == session.sessionParams.deviceId) {

View file

@ -18,32 +18,26 @@ package im.vector.app.features.voicebroadcast.usecase
import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.core.di.ActiveSessionHolder
import im.vector.app.features.voicebroadcast.VoiceBroadcastConstants import im.vector.app.features.voicebroadcast.VoiceBroadcastConstants
import im.vector.app.features.voicebroadcast.isLive
import im.vector.app.features.voicebroadcast.model.VoiceBroadcastEvent import im.vector.app.features.voicebroadcast.model.VoiceBroadcastEvent
import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState
import im.vector.app.features.voicebroadcast.model.asVoiceBroadcastEvent import im.vector.app.features.voicebroadcast.model.asVoiceBroadcastEvent
import org.matrix.android.sdk.api.query.QueryStringValue import org.matrix.android.sdk.api.query.QueryStringValue
import org.matrix.android.sdk.api.session.getRoom import org.matrix.android.sdk.api.session.getRoom
import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
class GetOngoingVoiceBroadcastsUseCase @Inject constructor( class GetRoomLiveVoiceBroadcastsUseCase @Inject constructor(
private val activeSessionHolder: ActiveSessionHolder, private val activeSessionHolder: ActiveSessionHolder,
) { ) {
fun execute(roomId: String): List<VoiceBroadcastEvent> { fun execute(roomId: String): List<VoiceBroadcastEvent> {
val session = activeSessionHolder.getSafeActiveSession() ?: run { val session = activeSessionHolder.getSafeActiveSession() ?: return emptyList()
Timber.d("## GetOngoingVoiceBroadcastsUseCase: no active session")
return emptyList()
}
val room = session.getRoom(roomId) ?: error("Unknown roomId: $roomId") val room = session.getRoom(roomId) ?: error("Unknown roomId: $roomId")
Timber.d("## GetLastVoiceBroadcastUseCase: get last voice broadcast in $roomId")
return room.stateService().getStateEvents( return room.stateService().getStateEvents(
setOf(VoiceBroadcastConstants.STATE_ROOM_VOICE_BROADCAST_INFO), setOf(VoiceBroadcastConstants.STATE_ROOM_VOICE_BROADCAST_INFO),
QueryStringValue.IsNotEmpty QueryStringValue.IsNotEmpty
) )
.mapNotNull { it.asVoiceBroadcastEvent() } .mapNotNull { it.asVoiceBroadcastEvent() }
.filter { it.content?.voiceBroadcastState != null && it.content?.voiceBroadcastState != VoiceBroadcastState.STOPPED } .filter { it.isLive }
} }
} }

View file

@ -42,7 +42,7 @@ import org.matrix.android.sdk.flow.mapOptional
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
class GetMostRecentVoiceBroadcastStateEventUseCase @Inject constructor( class GetVoiceBroadcastStateEventLiveUseCase @Inject constructor(
private val session: Session, private val session: Session,
) { ) {

View file

@ -52,14 +52,14 @@ class StartVoiceBroadcastUseCaseTest {
private val fakeRoom = FakeRoom() private val fakeRoom = FakeRoom()
private val fakeSession = FakeSession(fakeRoomService = FakeRoomService(fakeRoom)) private val fakeSession = FakeSession(fakeRoomService = FakeRoomService(fakeRoom))
private val fakeVoiceBroadcastRecorder = mockk<VoiceBroadcastRecorder>(relaxed = true) private val fakeVoiceBroadcastRecorder = mockk<VoiceBroadcastRecorder>(relaxed = true)
private val fakeGetOngoingVoiceBroadcastsUseCase = mockk<GetOngoingVoiceBroadcastsUseCase>() private val fakeGetRoomLiveVoiceBroadcastsUseCase = mockk<GetRoomLiveVoiceBroadcastsUseCase>()
private val startVoiceBroadcastUseCase = spyk( private val startVoiceBroadcastUseCase = spyk(
StartVoiceBroadcastUseCase( StartVoiceBroadcastUseCase(
session = fakeSession, session = fakeSession,
voiceBroadcastRecorder = fakeVoiceBroadcastRecorder, voiceBroadcastRecorder = fakeVoiceBroadcastRecorder,
context = FakeContext().instance, context = FakeContext().instance,
buildMeta = mockk(), buildMeta = mockk(),
getOngoingVoiceBroadcastsUseCase = fakeGetOngoingVoiceBroadcastsUseCase, getRoomLiveVoiceBroadcastsUseCase = fakeGetRoomLiveVoiceBroadcastsUseCase,
stopVoiceBroadcastUseCase = mockk() stopVoiceBroadcastUseCase = mockk()
) )
) )
@ -140,7 +140,7 @@ class StartVoiceBroadcastUseCaseTest {
} }
.mapNotNull { it.asVoiceBroadcastEvent() } .mapNotNull { it.asVoiceBroadcastEvent() }
.filter { it.content?.voiceBroadcastState != VoiceBroadcastState.STOPPED } .filter { it.content?.voiceBroadcastState != VoiceBroadcastState.STOPPED }
every { fakeGetOngoingVoiceBroadcastsUseCase.execute(any()) } returns events every { fakeGetRoomLiveVoiceBroadcastsUseCase.execute(any()) } returns events
} }
private data class VoiceBroadcast(val userId: String, val state: VoiceBroadcastState) private data class VoiceBroadcast(val userId: String, val state: VoiceBroadcastState)