Merge branch 'develop' into feature/fga/better_timeline_scroll

This commit is contained in:
ganfra 2023-07-13 11:24:00 +02:00
commit ca293d4f52
25 changed files with 124 additions and 71 deletions

View file

@ -26,11 +26,11 @@ jobs:
distribution: 'temurin' # See 'Supported distributions' for available options distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '17' java-version: '17'
- name: ⚙️ Run unit & screenshot tests, debug and release - name: ⚙️ Run unit tests, debug and release
run: ./gradlew test $CI_GRADLE_ARG_PROPERTIES -Pci-build=true run: ./gradlew test $CI_GRADLE_ARG_PROPERTIES
- name: ⚙️ Run unit & screenshot tests, generate kover report - name: 📈 Run screenshot tests, generate kover report and verify coverage
run: ./gradlew koverMergedReport $CI_GRADLE_ARG_PROPERTIES -Pci-build=true run: ./gradlew verifyPaparazziDebug koverMergedReport koverMergedVerify $CI_GRADLE_ARG_PROPERTIES -Pci-build=true
- name: ✅ Upload kover report - name: ✅ Upload kover report
if: always() if: always()

View file

@ -37,14 +37,11 @@ jobs:
with: with:
cache-read-only: ${{ github.ref != 'refs/heads/develop' }} cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
- name: ⚙️ Run unit & screenshot tests, debug and release - name: ⚙️ Run unit tests, debug and release
run: ./gradlew test $CI_GRADLE_ARG_PROPERTIES -Pci-build=true run: ./gradlew test $CI_GRADLE_ARG_PROPERTIES
- name: ⚙️ Run unit & screenshot tests, generate kover report - name: 📈 Run screenshot tests, generate kover report and verify coverage
run: ./gradlew koverMergedReport $CI_GRADLE_ARG_PROPERTIES -Pci-build=true run: ./gradlew verifyPaparazziDebug koverMergedReport koverMergedVerify $CI_GRADLE_ARG_PROPERTIES -Pci-build=true
- name: 📈 Verify coverage
run: ./gradlew koverMergedVerify $CI_GRADLE_ARG_PROPERTIES -Pci-build=true
- name: 🚫 Upload kover failed coverage reports - name: 🚫 Upload kover failed coverage reports
if: failure() if: failure()

View file

@ -135,6 +135,15 @@ allprojects {
allprojects { allprojects {
tasks.withType<Test> { tasks.withType<Test> {
maxParallelForks = (Runtime.getRuntime().availableProcessors() / 2).coerceAtLeast(1) maxParallelForks = (Runtime.getRuntime().availableProcessors() / 2).coerceAtLeast(1)
val isScreenshotTest = project.gradle.startParameter.taskNames.any { it.contains("paparazzi", ignoreCase = true) }
if (isScreenshotTest) {
// Increase heap size for screenshot tests
maxHeapSize = "1g"
} else {
// Disable screenshot tests by default
exclude("**/ScreenshotTest*")
}
} }
} }
@ -245,9 +254,11 @@ koverMerged {
excludes += "io.element.android.libraries.push.impl.notifications.NotificationState*" excludes += "io.element.android.libraries.push.impl.notifications.NotificationState*"
excludes += "io.element.android.features.messages.impl.media.local.pdf.PdfViewerState" excludes += "io.element.android.features.messages.impl.media.local.pdf.PdfViewerState"
excludes += "io.element.android.features.messages.impl.media.local.LocalMediaViewState" excludes += "io.element.android.features.messages.impl.media.local.LocalMediaViewState"
excludes += "io.element.android.features.location.impl.map.MapState" excludes += "io.element.android.features.location.impl.map.MapState*"
excludes += "io.element.android.libraries.matrix.api.timeline.item.event.LocalEventSendState*" excludes += "io.element.android.libraries.matrix.api.timeline.item.event.LocalEventSendState*"
excludes += "io.element.android.libraries.designsystem.swipe.SwipeableActionsState*" excludes += "io.element.android.libraries.designsystem.swipe.SwipeableActionsState*"
excludes += "io.element.android.features.messages.impl.timeline.components.ExpandableState*"
excludes += "io.element.android.features.messages.impl.timeline.model.bubble.BubbleState*"
} }
bound { bound {
minValue = 90 minValue = 90

View file

@ -36,6 +36,7 @@ import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableList
import kotlinx.collections.immutable.toPersistentList import kotlinx.collections.immutable.toPersistentList
import java.util.UUID
import kotlin.random.Random import kotlin.random.Random
fun aTimelineState(timelineItems: ImmutableList<TimelineItem> = persistentListOf()) = TimelineState( fun aTimelineState(timelineItems: ImmutableList<TimelineItem> = persistentListOf()) = TimelineState(
@ -96,7 +97,7 @@ internal fun aTimelineItemList(content: TimelineItemEventContent): ImmutableList
} }
fun aTimelineItemDaySeparator(): TimelineItem.Virtual { fun aTimelineItemDaySeparator(): TimelineItem.Virtual {
return TimelineItem.Virtual("virtual_day", aTimelineItemDaySeparatorModel("Today")) return TimelineItem.Virtual(UUID.randomUUID().toString(), aTimelineItemDaySeparatorModel("Today"))
} }
internal fun aTimelineItemEvent( internal fun aTimelineItemEvent(
@ -111,7 +112,7 @@ internal fun aTimelineItemEvent(
timelineItemReactions: TimelineItemReactions = aTimelineItemReactions(), timelineItemReactions: TimelineItemReactions = aTimelineItemReactions(),
): TimelineItem.Event { ): TimelineItem.Event {
return TimelineItem.Event( return TimelineItem.Event(
id = eventId.value, id = UUID.randomUUID().toString(),
eventId = eventId, eventId = eventId,
transactionId = transactionId, transactionId = transactionId,
senderId = UserId("@senderId:domain"), senderId = UserId("@senderId:domain"),

View file

@ -106,7 +106,7 @@ class TimelineItemsFactory @Inject constructor(
val timelineItemState = val timelineItemState =
when (val currentTimelineItem = timelineItems[index]) { when (val currentTimelineItem = timelineItems[index]) {
is MatrixTimelineItem.Event -> eventItemFactory.create(currentTimelineItem, index, timelineItems) is MatrixTimelineItem.Event -> eventItemFactory.create(currentTimelineItem, index, timelineItems)
is MatrixTimelineItem.Virtual -> virtualItemFactory.create(currentTimelineItem, index) is MatrixTimelineItem.Virtual -> virtualItemFactory.create(currentTimelineItem)
MatrixTimelineItem.Other -> null MatrixTimelineItem.Other -> null
} }
timelineItemsCache[index] = timelineItemState timelineItemsCache[index] = timelineItemState

View file

@ -71,7 +71,7 @@ class TimelineItemEventFactory @Inject constructor(
size = AvatarSize.TimelineSender size = AvatarSize.TimelineSender
) )
return TimelineItem.Event( return TimelineItem.Event(
id = currentTimelineItem.uniqueId, id = currentTimelineItem.uniqueId.toString(),
eventId = currentTimelineItem.eventId, eventId = currentTimelineItem.eventId,
transactionId = currentTimelineItem.transactionId, transactionId = currentTimelineItem.transactionId,
senderId = currentSender, senderId = currentSender,

View file

@ -29,10 +29,9 @@ class TimelineItemVirtualFactory @Inject constructor(
fun create( fun create(
virtualTimelineItem: MatrixTimelineItem.Virtual, virtualTimelineItem: MatrixTimelineItem.Virtual,
index: Int,
): TimelineItem.Virtual { ): TimelineItem.Virtual {
return TimelineItem.Virtual( return TimelineItem.Virtual(
id = "virtual_item_$index", id = virtualTimelineItem.uniqueId.toString(),
model = virtualTimelineItem.computeModel() model = virtualTimelineItem.computeModel()
) )
} }

View file

@ -24,8 +24,8 @@ import io.element.android.libraries.designsystem.components.avatar.AvatarData
import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.timeline.item.TimelineItemDebugInfo import io.element.android.libraries.matrix.api.timeline.item.TimelineItemDebugInfo
import io.element.android.libraries.matrix.api.timeline.item.event.LocalEventSendState
import io.element.android.libraries.matrix.api.timeline.item.event.InReplyTo import io.element.android.libraries.matrix.api.timeline.item.event.InReplyTo
import io.element.android.libraries.matrix.api.timeline.item.event.LocalEventSendState
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
@Immutable @Immutable
@ -83,6 +83,6 @@ sealed interface TimelineItem {
val events: ImmutableList<Event>, val events: ImmutableList<Event>,
) : TimelineItem { ) : TimelineItem {
// use last id with a suffix. Last will not change in cas of new event from backpagination. // use last id with a suffix. Last will not change in cas of new event from backpagination.
val id = events.last().id + "_group" val id = "${events.last().id}_group"
} }
} }

View file

@ -96,7 +96,7 @@ class TimelinePresenterTest {
fun `present - on scroll finished send read receipt if an event is before the index`() = runTest { fun `present - on scroll finished send read receipt if an event is before the index`() = runTest {
val timeline = FakeMatrixTimeline() val timeline = FakeMatrixTimeline()
val timelineItemsFactory = aTimelineItemsFactory().apply { val timelineItemsFactory = aTimelineItemsFactory().apply {
replaceWith(listOf(MatrixTimelineItem.Event(anEventTimelineItem()))) replaceWith(listOf(MatrixTimelineItem.Event(0, anEventTimelineItem())))
} }
val room = FakeMatrixRoom(matrixTimeline = timeline) val room = FakeMatrixRoom(matrixTimeline = timeline)
val presenter = TimelinePresenter( val presenter = TimelinePresenter(
@ -119,7 +119,7 @@ class TimelinePresenterTest {
fun `present - on scroll finished will not send read receipt no event is before the index`() = runTest { fun `present - on scroll finished will not send read receipt no event is before the index`() = runTest {
val timeline = FakeMatrixTimeline() val timeline = FakeMatrixTimeline()
val timelineItemsFactory = aTimelineItemsFactory().apply { val timelineItemsFactory = aTimelineItemsFactory().apply {
replaceWith(listOf(MatrixTimelineItem.Event(anEventTimelineItem()))) replaceWith(listOf(MatrixTimelineItem.Event(0, anEventTimelineItem())))
} }
val room = FakeMatrixRoom(matrixTimeline = timeline) val room = FakeMatrixRoom(matrixTimeline = timeline)
val presenter = TimelinePresenter( val presenter = TimelinePresenter(
@ -142,7 +142,7 @@ class TimelinePresenterTest {
fun `present - on scroll finished will not send read receipt only virtual events exist before the index`() = runTest { fun `present - on scroll finished will not send read receipt only virtual events exist before the index`() = runTest {
val timeline = FakeMatrixTimeline() val timeline = FakeMatrixTimeline()
val timelineItemsFactory = aTimelineItemsFactory().apply { val timelineItemsFactory = aTimelineItemsFactory().apply {
replaceWith(listOf(MatrixTimelineItem.Virtual(VirtualTimelineItem.ReadMarker))) replaceWith(listOf(MatrixTimelineItem.Virtual(0, VirtualTimelineItem.ReadMarker)))
} }
val room = FakeMatrixRoom(matrixTimeline = timeline) val room = FakeMatrixRoom(matrixTimeline = timeline)
val presenter = TimelinePresenter( val presenter = TimelinePresenter(

View file

@ -145,7 +145,7 @@ jsoup = { module = "org.jsoup:jsoup", version.ref = "jsoup" }
appyx_core = { module = "com.bumble.appyx:core", version.ref = "appyx" } appyx_core = { module = "com.bumble.appyx:core", version.ref = "appyx" }
molecule-runtime = { module = "app.cash.molecule:molecule-runtime", version.ref = "molecule" } molecule-runtime = { module = "app.cash.molecule:molecule-runtime", version.ref = "molecule" }
timber = "com.jakewharton.timber:timber:5.0.1" timber = "com.jakewharton.timber:timber:5.0.1"
matrix_sdk = "org.matrix.rustcomponents:sdk-android:0.1.29" matrix_sdk = "org.matrix.rustcomponents:sdk-android:0.1.31"
sqldelight-driver-android = { module = "com.squareup.sqldelight:android-driver", version.ref = "sqldelight" } sqldelight-driver-android = { module = "com.squareup.sqldelight:android-driver", version.ref = "sqldelight" }
sqldelight-driver-jvm = { module = "com.squareup.sqldelight:sqlite-driver", version.ref = "sqldelight" } sqldelight-driver-jvm = { module = "com.squareup.sqldelight:sqlite-driver", version.ref = "sqldelight" }
sqldelight-coroutines = { module = "com.squareup.sqldelight:coroutines-extensions", version.ref = "sqldelight" } sqldelight-coroutines = { module = "com.squareup.sqldelight:coroutines-extensions", version.ref = "sqldelight" }

View file

@ -21,13 +21,12 @@ import io.element.android.libraries.matrix.api.timeline.item.event.EventTimeline
import io.element.android.libraries.matrix.api.timeline.item.virtual.VirtualTimelineItem import io.element.android.libraries.matrix.api.timeline.item.virtual.VirtualTimelineItem
sealed interface MatrixTimelineItem { sealed interface MatrixTimelineItem {
data class Event(val event: EventTimelineItem) : MatrixTimelineItem { data class Event(val uniqueId: Long, val event: EventTimelineItem) : MatrixTimelineItem {
val uniqueId: String = event.uniqueIdentifier
val eventId: EventId? = event.eventId val eventId: EventId? = event.eventId
val transactionId: String? = event.transactionId val transactionId: String? = event.transactionId
} }
data class Virtual(val virtual: VirtualTimelineItem) : MatrixTimelineItem data class Virtual(val uniqueId: Long, val virtual: VirtualTimelineItem) : MatrixTimelineItem
object Other : MatrixTimelineItem object Other : MatrixTimelineItem
} }

View file

@ -21,7 +21,6 @@ import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.timeline.item.TimelineItemDebugInfo import io.element.android.libraries.matrix.api.timeline.item.TimelineItemDebugInfo
data class EventTimelineItem( data class EventTimelineItem(
val uniqueIdentifier: String,
val eventId: EventId?, val eventId: EventId?,
val transactionId: String?, val transactionId: String?,
val isEditable: Boolean, val isEditable: Boolean,

View file

@ -47,6 +47,7 @@ import io.element.android.libraries.matrix.impl.room.RoomContentForwarder
import io.element.android.libraries.matrix.impl.room.RustMatrixRoom import io.element.android.libraries.matrix.impl.room.RustMatrixRoom
import io.element.android.libraries.matrix.impl.room.RustRoomSummaryDataSource import io.element.android.libraries.matrix.impl.room.RustRoomSummaryDataSource
import io.element.android.libraries.matrix.impl.room.roomOrNull import io.element.android.libraries.matrix.impl.room.roomOrNull
import io.element.android.libraries.matrix.impl.room.stateFlow
import io.element.android.libraries.matrix.impl.sync.RustSyncService import io.element.android.libraries.matrix.impl.sync.RustSyncService
import io.element.android.libraries.matrix.impl.usersearch.UserProfileMapper import io.element.android.libraries.matrix.impl.usersearch.UserProfileMapper
import io.element.android.libraries.matrix.impl.usersearch.UserSearchResultMapper import io.element.android.libraries.matrix.impl.usersearch.UserSearchResultMapper
@ -85,17 +86,23 @@ class RustMatrixClient constructor(
) : MatrixClient { ) : MatrixClient {
override val sessionId: UserId = UserId(client.userId()) override val sessionId: UserId = UserId(client.userId())
private val roomListService = client.roomListServiceWithEncryption() private val app = client.app().use { builder ->
builder.finish()
}
private val roomListService = app.roomListService()
private val sessionDispatcher = dispatchers.io.limitedParallelism(64) private val sessionDispatcher = dispatchers.io.limitedParallelism(64)
private val sessionCoroutineScope = appCoroutineScope.childScope(dispatchers.main, "Session-${sessionId}") private val sessionCoroutineScope = appCoroutineScope.childScope(dispatchers.main, "Session-${sessionId}")
private val verificationService = RustSessionVerificationService() private val verificationService = RustSessionVerificationService()
private val syncService = RustSyncService(roomListService, sessionCoroutineScope) private val syncService = RustSyncService(app, roomListService.stateFlow(), sessionCoroutineScope)
private val pushersService = RustPushersService( private val pushersService = RustPushersService(
client = client, client = client,
dispatchers = dispatchers, dispatchers = dispatchers,
) )
private val notificationClient = client.notificationClient().use { builder ->
private val notificationService = RustNotificationService(client) builder.finish()
}
private val notificationService = RustNotificationService(notificationClient)
private val clientDelegate = object : ClientDelegate { private val clientDelegate = object : ClientDelegate {
override fun didReceiveAuthError(isSoftLogout: Boolean) { override fun didReceiveAuthError(isSoftLogout: Boolean) {
@ -249,7 +256,9 @@ class RustMatrixClient constructor(
sessionCoroutineScope.cancel() sessionCoroutineScope.cancel()
client.setDelegate(null) client.setDelegate(null)
verificationService.destroy() verificationService.destroy()
app.destroy()
roomListService.destroy() roomListService.destroy()
notificationClient.destroy()
client.destroy() client.destroy()
} }

View file

@ -27,12 +27,12 @@ import org.matrix.rustcomponents.sdk.use
class NotificationMapper { class NotificationMapper {
private val timelineEventMapper = TimelineEventMapper() private val timelineEventMapper = TimelineEventMapper()
fun map(notificationItem: NotificationItem): NotificationData { fun map(roomId: RoomId, notificationItem: NotificationItem): NotificationData {
return notificationItem.use { item -> return notificationItem.use { item ->
NotificationData( NotificationData(
senderId = UserId(item.event.senderId()), senderId = UserId(item.event.senderId()),
eventId = EventId(item.event.eventId()), eventId = EventId(item.event.eventId()),
roomId = RoomId(item.roomInfo.id), roomId = roomId,
senderAvatarUrl = item.senderInfo.avatarUrl, senderAvatarUrl = item.senderInfo.avatarUrl,
senderDisplayName = item.senderInfo.displayName, senderDisplayName = item.senderInfo.displayName,
roomAvatarUrl = item.roomInfo.avatarUrl ?: item.senderInfo.avatarUrl.takeIf { item.roomInfo.isDirect }, roomAvatarUrl = item.roomInfo.avatarUrl ?: item.senderInfo.avatarUrl.takeIf { item.roomInfo.isDirect },

View file

@ -21,11 +21,11 @@ import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.matrix.api.notification.NotificationData import io.element.android.libraries.matrix.api.notification.NotificationData
import io.element.android.libraries.matrix.api.notification.NotificationService import io.element.android.libraries.matrix.api.notification.NotificationService
import org.matrix.rustcomponents.sdk.Client import org.matrix.rustcomponents.sdk.NotificationClient
import org.matrix.rustcomponents.sdk.use import org.matrix.rustcomponents.sdk.use
class RustNotificationService( class RustNotificationService(
private val client: Client, private val notificationClient: NotificationClient,
) : NotificationService { ) : NotificationService {
private val notificationMapper: NotificationMapper = NotificationMapper() private val notificationMapper: NotificationMapper = NotificationMapper()
@ -36,8 +36,10 @@ class RustNotificationService(
filterByPushRules: Boolean, filterByPushRules: Boolean,
): Result<NotificationData?> { ): Result<NotificationData?> {
return runCatching { return runCatching {
val item = client.getNotificationItem(roomId.value, eventId.value, filterByPushRules) val item = notificationClient.getNotification(roomId.value, eventId.value)
item?.use(notificationMapper::map) item?.use {
notificationMapper.map(roomId, it)
}
} }
} }
} }

View file

@ -24,7 +24,7 @@ import org.matrix.rustcomponents.sdk.RoomListItem
class RoomSummaryDetailsFactory(private val roomMessageFactory: RoomMessageFactory = RoomMessageFactory()) { class RoomSummaryDetailsFactory(private val roomMessageFactory: RoomMessageFactory = RoomMessageFactory()) {
fun create(roomListItem: RoomListItem, room: Room?): RoomSummaryDetails { suspend fun create(roomListItem: RoomListItem, room: Room?): RoomSummaryDetails {
val latestRoomMessage = roomListItem.latestEvent()?.use { val latestRoomMessage = roomListItem.latestEvent()?.use {
roomMessageFactory.create(it) roomMessageFactory.create(it)
} }

View file

@ -16,6 +16,7 @@
package io.element.android.libraries.matrix.impl.room package io.element.android.libraries.matrix.impl.room
import io.element.android.libraries.core.coroutine.parallelMap
import io.element.android.libraries.matrix.api.room.RoomSummary import io.element.android.libraries.matrix.api.room.RoomSummary
import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
@ -43,7 +44,8 @@ class RoomSummaryListProcessor(
suspend fun postEntries(entries: List<RoomListEntry>) { suspend fun postEntries(entries: List<RoomListEntry>) {
updateRoomSummaries { updateRoomSummaries {
Timber.v("Update rooms from postEntries (with ${entries.size} items) on ${Thread.currentThread()}") Timber.v("Update rooms from postEntries (with ${entries.size} items) on ${Thread.currentThread()}")
addAll(entries.map(::buildSummaryForRoomListEntry)) val roomSummaries = entries.parallelMap(::buildSummaryForRoomListEntry)
addAll(roomSummaries)
} }
initLatch.complete(Unit) initLatch.complete(Unit)
} }
@ -57,7 +59,7 @@ class RoomSummaryListProcessor(
} }
} }
private fun MutableList<RoomSummary>.applyUpdate(update: RoomListEntriesUpdate) { private suspend fun MutableList<RoomSummary>.applyUpdate(update: RoomListEntriesUpdate) {
when (update) { when (update) {
is RoomListEntriesUpdate.Append -> { is RoomListEntriesUpdate.Append -> {
val roomSummaries = update.values.map { val roomSummaries = update.values.map {
@ -100,7 +102,7 @@ class RoomSummaryListProcessor(
} }
} }
private fun buildSummaryForRoomListEntry(entry: RoomListEntry): RoomSummary { private suspend fun buildSummaryForRoomListEntry(entry: RoomListEntry): RoomSummary {
return when (entry) { return when (entry) {
RoomListEntry.Empty -> buildEmptyRoomSummary() RoomListEntry.Empty -> buildEmptyRoomSummary()
is RoomListEntry.Filled -> buildAndCacheRoomSummaryForIdentifier(entry.roomId) is RoomListEntry.Filled -> buildAndCacheRoomSummaryForIdentifier(entry.roomId)
@ -114,7 +116,7 @@ class RoomSummaryListProcessor(
return RoomSummary.Empty(UUID.randomUUID().toString()) return RoomSummary.Empty(UUID.randomUUID().toString())
} }
private fun buildAndCacheRoomSummaryForIdentifier(identifier: String): RoomSummary { private suspend fun buildAndCacheRoomSummaryForIdentifier(identifier: String): RoomSummary {
val builtRoomSummary = roomListService.roomOrNull(identifier)?.use { roomListItem -> val builtRoomSummary = roomListService.roomOrNull(identifier)?.use { roomListItem ->
roomListItem.fullRoomOrNull().use { fullRoom -> roomListItem.fullRoomOrNull().use { fullRoom ->
RoomSummary.Filled( RoomSummary.Filled(
@ -134,7 +136,7 @@ class RoomSummaryListProcessor(
} }
} }
private suspend fun updateRoomSummaries(block: MutableList<RoomSummary>.() -> Unit) = private suspend fun updateRoomSummaries(block: suspend MutableList<RoomSummary>.() -> Unit) =
mutex.withLock { mutex.withLock {
val mutableRoomSummaries = roomSummaries.value.toMutableList() val mutableRoomSummaries = roomSummaries.value.toMutableList()
block(mutableRoomSummaries) block(mutableRoomSummaries)

View file

@ -0,0 +1,36 @@
/*
* Copyright (c) 2023 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 io.element.android.libraries.matrix.impl.sync
import io.element.android.libraries.matrix.impl.util.mxCallbackFlow
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.channels.trySendBlocking
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.buffer
import org.matrix.rustcomponents.sdk.App
import org.matrix.rustcomponents.sdk.AppState
import org.matrix.rustcomponents.sdk.AppStateObserver
fun App.stateFlow(): Flow<AppState> =
mxCallbackFlow {
val listener = object : AppStateObserver {
override fun onUpdate(state: AppState) {
trySendBlocking(state)
}
}
state(listener)
}.buffer(Channel.UNLIMITED)

View file

@ -17,6 +17,7 @@
package io.element.android.libraries.matrix.impl.sync package io.element.android.libraries.matrix.impl.sync
import io.element.android.libraries.matrix.api.sync.SyncState import io.element.android.libraries.matrix.api.sync.SyncState
import org.matrix.rustcomponents.sdk.AppState
import org.matrix.rustcomponents.sdk.RoomListServiceState import org.matrix.rustcomponents.sdk.RoomListServiceState
internal fun RoomListServiceState.toSyncState(): SyncState { internal fun RoomListServiceState.toSyncState(): SyncState {
@ -28,3 +29,11 @@ internal fun RoomListServiceState.toSyncState(): SyncState {
RoomListServiceState.TERMINATED -> SyncState.Terminated RoomListServiceState.TERMINATED -> SyncState.Terminated
} }
} }
internal fun AppState.toSyncState(): SyncState {
return when (this) {
AppState.RUNNING -> SyncState.Syncing
AppState.TERMINATED -> SyncState.Terminated
AppState.ERROR -> SyncState.InError
}
}

View file

@ -18,47 +18,39 @@ package io.element.android.libraries.matrix.impl.sync
import io.element.android.libraries.matrix.api.sync.SyncService import io.element.android.libraries.matrix.api.sync.SyncService
import io.element.android.libraries.matrix.api.sync.SyncState import io.element.android.libraries.matrix.api.sync.SyncState
import io.element.android.libraries.matrix.impl.room.stateFlow
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.stateIn
import org.matrix.rustcomponents.sdk.RoomListService import org.matrix.rustcomponents.sdk.App
import org.matrix.rustcomponents.sdk.RoomListServiceState import org.matrix.rustcomponents.sdk.RoomListServiceState
import timber.log.Timber import timber.log.Timber
import java.util.concurrent.atomic.AtomicBoolean
class RustSyncService( class RustSyncService(
private val roomListService: RoomListService, private val app: App,
roomListStateFlow: Flow<RoomListServiceState>,
sessionCoroutineScope: CoroutineScope sessionCoroutineScope: CoroutineScope
) : SyncService { ) : SyncService {
private val isSyncing = AtomicBoolean(false)
override fun startSync() = runCatching { override fun startSync() = runCatching {
if (isSyncing.compareAndSet(false, true)) { Timber.v("Start sync")
Timber.v("Start sync") app.start()
roomListService.sync()
}
} }
override fun stopSync() = runCatching { override fun stopSync() = runCatching {
if (isSyncing.compareAndSet(true, false)) { Timber.v("Stop sync")
Timber.v("Stop sync") app.pause()
roomListService.stopSync()
}
} }
override val syncState: StateFlow<SyncState> = override val syncState: StateFlow<SyncState> =
roomListService roomListStateFlow
.stateFlow()
.map(RoomListServiceState::toSyncState) .map(RoomListServiceState::toSyncState)
.onEach { state -> .onEach { state ->
Timber.v("Sync state=$state") Timber.v("Sync state=$state")
isSyncing.set(state == SyncState.Syncing)
} }
.distinctUntilChanged() .distinctUntilChanged()
.stateIn(sessionCoroutineScope, SharingStarted.Eagerly, SyncState.Idle) .stateIn(sessionCoroutineScope, SharingStarted.Eagerly, SyncState.Idle)

View file

@ -32,21 +32,20 @@ class MatrixTimelineItemMapper(
) { ) {
fun map(timelineItem: TimelineItem): MatrixTimelineItem = timelineItem.use { fun map(timelineItem: TimelineItem): MatrixTimelineItem = timelineItem.use {
val uniqueId = timelineItem.uniqueId().toLong()
val asEvent = it.asEvent() val asEvent = it.asEvent()
if (asEvent != null) { if (asEvent != null) {
val eventTimelineItem = eventTimelineItemMapper.map(asEvent) val eventTimelineItem = eventTimelineItemMapper.map(asEvent)
if (eventTimelineItem.hasNotLoadedInReplyTo() && eventTimelineItem.eventId != null) { if (eventTimelineItem.hasNotLoadedInReplyTo() && eventTimelineItem.eventId != null) {
fetchEventDetails(eventTimelineItem.eventId!!) fetchEventDetails(eventTimelineItem.eventId!!)
} }
return MatrixTimelineItem.Event(eventTimelineItem) return MatrixTimelineItem.Event(uniqueId, eventTimelineItem)
} }
val asVirtual = it.asVirtual() val asVirtual = it.asVirtual()
if (asVirtual != null) { if (asVirtual != null) {
val virtualTimelineItem = virtualTimelineItemMapper.map(asVirtual) val virtualTimelineItem = virtualTimelineItemMapper.map(asVirtual)
return MatrixTimelineItem.Virtual(virtualTimelineItem) return MatrixTimelineItem.Virtual(uniqueId, virtualTimelineItem)
} }
return MatrixTimelineItem.Other return MatrixTimelineItem.Other
} }

View file

@ -20,8 +20,8 @@ import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.timeline.item.TimelineItemDebugInfo import io.element.android.libraries.matrix.api.timeline.item.TimelineItemDebugInfo
import io.element.android.libraries.matrix.api.timeline.item.event.EventReaction import io.element.android.libraries.matrix.api.timeline.item.event.EventReaction
import io.element.android.libraries.matrix.api.timeline.item.event.LocalEventSendState
import io.element.android.libraries.matrix.api.timeline.item.event.EventTimelineItem import io.element.android.libraries.matrix.api.timeline.item.event.EventTimelineItem
import io.element.android.libraries.matrix.api.timeline.item.event.LocalEventSendState
import io.element.android.libraries.matrix.api.timeline.item.event.ProfileTimelineDetails import io.element.android.libraries.matrix.api.timeline.item.event.ProfileTimelineDetails
import org.matrix.rustcomponents.sdk.Reaction import org.matrix.rustcomponents.sdk.Reaction
import org.matrix.rustcomponents.sdk.EventSendState as RustEventSendState import org.matrix.rustcomponents.sdk.EventSendState as RustEventSendState
@ -33,7 +33,6 @@ class EventTimelineItemMapper(private val contentMapper: TimelineEventContentMap
fun map(eventTimelineItem: RustEventTimelineItem): EventTimelineItem = eventTimelineItem.use { fun map(eventTimelineItem: RustEventTimelineItem): EventTimelineItem = eventTimelineItem.use {
EventTimelineItem( EventTimelineItem(
uniqueIdentifier = it.uniqueIdentifier(),
eventId = it.eventId()?.let(::EventId), eventId = it.eventId()?.let(::EventId),
transactionId = it.transactionId(), transactionId = it.transactionId(),
isEditable = it.isEditable(), isEditable = it.isEditable(),
@ -79,7 +78,7 @@ private fun List<Reaction>?.map(): List<EventReaction> {
EventReaction( EventReaction(
key = it.key, key = it.key,
count = it.count.toLong(), count = it.count.toLong(),
senderIds = it.senders.map { sender -> UserId(sender) } senderIds = it.senders.map { sender -> UserId(sender.senderId) }
) )
} ?: emptyList() } ?: emptyList()
} }

View file

@ -23,6 +23,7 @@ import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.matrix.api.core.SpaceId import io.element.android.libraries.matrix.api.core.SpaceId
import io.element.android.libraries.matrix.api.core.ThreadId import io.element.android.libraries.matrix.api.core.ThreadId
import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.core.UserId
import java.util.UUID
const val A_USER_NAME = "alice" const val A_USER_NAME = "alice"
const val A_PASSWORD = "password" const val A_PASSWORD = "password"
@ -57,4 +58,3 @@ const val A_FAILURE_REASON = "There has been a failure"
val A_THROWABLE = Throwable(A_FAILURE_REASON) val A_THROWABLE = Throwable(A_FAILURE_REASON)
val AN_EXCEPTION = Exception(A_FAILURE_REASON) val AN_EXCEPTION = Exception(A_FAILURE_REASON)

View file

@ -87,7 +87,6 @@ fun aRoomMessage(
) )
fun anEventTimelineItem( fun anEventTimelineItem(
uniqueIdentifier: String = A_UNIQUE_ID,
eventId: EventId = AN_EVENT_ID, eventId: EventId = AN_EVENT_ID,
transactionId: String? = null, transactionId: String? = null,
isEditable: Boolean = false, isEditable: Boolean = false,
@ -102,7 +101,6 @@ fun anEventTimelineItem(
content: EventContent = aProfileChangeMessageContent(), content: EventContent = aProfileChangeMessageContent(),
debugInfo: TimelineItemDebugInfo = aTimelineItemDebugInfo(), debugInfo: TimelineItemDebugInfo = aTimelineItemDebugInfo(),
) = EventTimelineItem( ) = EventTimelineItem(
uniqueIdentifier = uniqueIdentifier,
eventId = eventId, eventId = eventId,
transactionId = transactionId, transactionId = transactionId,
isEditable = isEditable, isEditable = isEditable,

View file

@ -232,6 +232,7 @@ class NotificationFactory @Inject constructor(
.setSmallIcon(smallIcon) .setSmallIcon(smallIcon)
.setColor(accentColor) .setColor(accentColor)
.setAutoCancel(true) .setAutoCancel(true)
.setWhen(fallbackNotifiableEvent.timestamp)
// Ideally we'd use `createOpenRoomPendingIntent` here, but the broken notification might apply to an invite // Ideally we'd use `createOpenRoomPendingIntent` here, but the broken notification might apply to an invite
// and the user won't have access to the room yet, resulting in an error screen. // and the user won't have access to the room yet, resulting in an error screen.
.setContentIntent(pendingIntentFactory.createOpenSessionPendingIntent(fallbackNotifiableEvent.sessionId)) .setContentIntent(pendingIntentFactory.createOpenSessionPendingIntent(fallbackNotifiableEvent.sessionId))