Merge branch 'develop' into feature/fga/better_timeline_scroll
This commit is contained in:
commit
ca293d4f52
25 changed files with 124 additions and 71 deletions
8
.github/workflows/nightlyReports.yml
vendored
8
.github/workflows/nightlyReports.yml
vendored
|
|
@ -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()
|
||||||
|
|
|
||||||
11
.github/workflows/tests.yml
vendored
11
.github/workflows/tests.yml
vendored
|
|
@ -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()
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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"),
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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" }
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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 ->
|
||||||
|
builder.finish()
|
||||||
|
}
|
||||||
|
|
||||||
private val notificationService = RustNotificationService(client)
|
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()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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 },
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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))
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue