Merge branch 'develop' into feature/fga/space_ui_tweaks

This commit is contained in:
ganfra 2026-02-10 09:31:50 +01:00
commit 0dec3a1cb6
174 changed files with 2905 additions and 1323 deletions

View file

@ -98,6 +98,18 @@ internal fun MatrixBadgeAtomNegativePreview() = ElementPreview {
)
}
@PreviewsDayNight
@Composable
internal fun MatrixBadgeAtomNeutralWrappingPreview() = ElementPreview {
MatrixBadgeAtom.View(
MatrixBadgeAtom.MatrixBadgeData(
text = "How much wood could a wood chuck chuck if a wood chuck could chuck wood",
icon = CompoundIcons.LockOff(),
type = MatrixBadgeAtom.Type.Info,
)
)
}
@PreviewsDayNight
@Composable
internal fun MatrixBadgeAtomInfoPreview() = ElementPreview {

View file

@ -9,9 +9,10 @@
package io.element.android.libraries.designsystem.atomic.molecules
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import io.element.android.libraries.designsystem.atomic.atoms.MatrixBadgeAtom
@ -22,10 +23,11 @@ fun MatrixBadgeRowMolecule(
data: ImmutableList<MatrixBadgeAtom.MatrixBadgeData>,
modifier: Modifier = Modifier,
) {
Row(
FlowRow(
modifier = modifier
.padding(start = 16.dp, end = 16.dp, top = 8.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp, Alignment.CenterHorizontally),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
for (badge in data) {
MatrixBadgeAtom.View(badge)

View file

@ -21,6 +21,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import io.element.android.compound.theme.ElementTheme
import io.element.android.compound.tokens.generated.CompoundIcons
@ -63,6 +64,8 @@ fun Badge(
text = text,
style = ElementTheme.typography.fontBodySmRegular,
color = textColor,
overflow = TextOverflow.Ellipsis,
softWrap = false,
)
}
}

View file

@ -84,6 +84,13 @@ enum class FeatureFlags(
defaultValue = { false },
isFinished = false,
),
RoomListSpaceFilters(
key = "feature.roomListSpaceFilters",
title = "Room list space filters",
description = "Allow filtering the room list by space.",
defaultValue = { false },
isFinished = false,
),
PrintLogsToLogcat(
key = "feature.print_logs_to_logcat",
title = "Print logs to logcat",

View file

@ -13,6 +13,7 @@ sealed class QrLoginException : Exception() {
data object ConnectionInsecure : QrLoginException()
data object Declined : QrLoginException()
data object Expired : QrLoginException()
data object NotFound : QrLoginException()
data object LinkingNotSupported : QrLoginException()
data object OidcMetadataInvalid : QrLoginException()
data object SlidingSyncNotAvailable : QrLoginException()
@ -20,5 +21,4 @@ sealed class QrLoginException : Exception() {
data object CheckCodeAlreadySent : QrLoginException()
data object CheckCodeCannotBeSent : QrLoginException()
data object Unknown : QrLoginException()
data object NotFound : QrLoginException()
}

View file

@ -8,6 +8,9 @@
package io.element.android.libraries.matrix.api.room.history
import androidx.compose.runtime.Immutable
@Immutable
sealed interface RoomHistoryVisibility {
/**
* Previous events are accessible to newly joined members from the point

View file

@ -28,4 +28,10 @@ sealed interface LatestEventValue {
val senderProfile: ProfileDetails,
val isSending: Boolean,
) : LatestEventValue
data class RoomInvite(
val timestamp: Long,
val inviterId: UserId?,
val invitedProfile: ProfileDetails,
) : LatestEventValue
}

View file

@ -8,6 +8,8 @@
package io.element.android.libraries.matrix.api.roomlist
import io.element.android.libraries.matrix.api.core.RoomId
sealed interface RoomListFilter {
companion object {
/**
@ -41,6 +43,10 @@ sealed interface RoomListFilter {
val filters: List<RoomListFilter>
) : RoomListFilter
data class Identifiers(
val values: List<RoomId>,
) : RoomListFilter
/**
* A filter that matches rooms that are unread.
*/

View file

@ -19,6 +19,7 @@ data class RoomSummary(
is LatestEventValue.None -> null
is LatestEventValue.Local -> latestEvent.timestamp
is LatestEventValue.Remote -> latestEvent.timestamp
is LatestEventValue.RoomInvite -> latestEvent.timestamp
}
val isOneToOne get() = info.activeMembersCount == 2L
}

View file

@ -12,9 +12,8 @@ import io.element.android.libraries.matrix.api.core.RoomId
import kotlinx.coroutines.flow.SharedFlow
interface SpaceService {
val spaceRoomsFlow: SharedFlow<List<SpaceRoom>>
suspend fun joinedSpaces(): Result<List<SpaceRoom>>
val topLevelSpacesFlow: SharedFlow<List<SpaceRoom>>
val spaceFiltersFlow: SharedFlow<List<SpaceServiceFilter>>
suspend fun joinedParents(spaceId: RoomId): Result<List<SpaceRoom>>
suspend fun getSpaceRoom(spaceId: RoomId): SpaceRoom?

View file

@ -0,0 +1,24 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.libraries.matrix.api.spaces
import io.element.android.libraries.matrix.api.core.RoomId
/**
* Represents a space filter for filtering rooms by space membership.
*
* @property spaceRoom The space room associated with this filter.
* @property level The nesting level of the space (0 = top level, 1 = first level child, etc.).
* @property descendants The list of room IDs that are descendants of this space.
*/
data class SpaceServiceFilter(
val spaceRoom: SpaceRoom,
val level: Int,
val descendants: List<RoomId>,
)

View file

@ -20,6 +20,7 @@ import org.matrix.rustcomponents.sdk.ClientDelegate
import org.matrix.rustcomponents.sdk.ClientSessionDelegate
import org.matrix.rustcomponents.sdk.Session
import timber.log.Timber
import uniffi.matrix_sdk_common.BackgroundTaskFailureReason
import java.lang.ref.WeakReference
import java.util.concurrent.atomic.AtomicBoolean
@ -120,6 +121,11 @@ class RustClientSessionDelegate(
}
}
override fun onBackgroundTaskErrorReport(taskName: String, error: BackgroundTaskFailureReason) {
// TODO actually implement the missing logic to report to sentry and crash the app
Timber.tag(loggerTag.value).e("onBackgroundTaskErrorReport(taskName=$taskName, error=$error)")
}
override fun retrieveSessionFromKeychain(userId: String): Session {
// This should never be called, as it's only used for multi-process setups
error("retrieveSessionFromKeychain should never be called for Android")

View file

@ -66,7 +66,7 @@ import io.element.android.libraries.matrix.impl.room.RoomContentForwarder
import io.element.android.libraries.matrix.impl.room.RoomInfoMapper
import io.element.android.libraries.matrix.impl.room.RoomSyncSubscriber
import io.element.android.libraries.matrix.impl.room.RustRoomFactory
import io.element.android.libraries.matrix.impl.room.TimelineEventTypeFilterFactory
import io.element.android.libraries.matrix.impl.room.TimelineEventFilterFactory
import io.element.android.libraries.matrix.impl.room.history.map
import io.element.android.libraries.matrix.impl.room.join.map
import io.element.android.libraries.matrix.impl.room.preview.RoomPreviewInfoMapper
@ -141,7 +141,7 @@ class RustMatrixClient(
dispatchers: CoroutineDispatchers,
baseCacheDirectory: File,
clock: SystemClock,
timelineEventTypeFilterFactory: TimelineEventTypeFilterFactory,
timelineEventFilterFactory: TimelineEventFilterFactory,
private val featureFlagService: FeatureFlagService,
private val analyticsService: AnalyticsService,
private val workManagerScheduler: WorkManagerScheduler,
@ -225,7 +225,7 @@ class RustMatrixClient(
systemClock = clock,
roomContentForwarder = RoomContentForwarder(innerRoomListService),
roomSyncSubscriber = roomSyncSubscriber,
timelineEventTypeFilterFactory = timelineEventTypeFilterFactory,
timelineEventFilterFactory = timelineEventFilterFactory,
roomMembershipObserver = roomMembershipObserver,
roomInfoMapper = roomInfoMapper,
featureFlagService = featureFlagService,

View file

@ -21,7 +21,7 @@ import io.element.android.libraries.matrix.impl.certificates.UserCertificatesPro
import io.element.android.libraries.matrix.impl.paths.SessionPaths
import io.element.android.libraries.matrix.impl.paths.getSessionPaths
import io.element.android.libraries.matrix.impl.proxy.ProxyProvider
import io.element.android.libraries.matrix.impl.room.TimelineEventTypeFilterFactory
import io.element.android.libraries.matrix.impl.room.TimelineEventFilterFactory
import io.element.android.libraries.matrix.impl.storage.SqliteStoreBuilderProvider
import io.element.android.libraries.matrix.impl.util.anonymizedTokens
import io.element.android.libraries.network.useragent.UserAgentProvider
@ -61,7 +61,7 @@ class RustMatrixClientFactory(
private val clock: SystemClock,
private val analyticsService: AnalyticsService,
private val featureFlagService: FeatureFlagService,
private val timelineEventTypeFilterFactory: TimelineEventTypeFilterFactory,
private val timelineEventFilterFactory: TimelineEventFilterFactory,
private val clientBuilderProvider: ClientBuilderProvider,
private val sqliteStoreBuilderProvider: SqliteStoreBuilderProvider,
private val workManagerScheduler: WorkManagerScheduler,
@ -115,7 +115,7 @@ class RustMatrixClientFactory(
dispatchers = coroutineDispatchers,
baseCacheDirectory = cacheDirectory,
clock = clock,
timelineEventTypeFilterFactory = timelineEventTypeFilterFactory,
timelineEventFilterFactory = timelineEventFilterFactory,
featureFlagService = featureFlagService,
analyticsService = analyticsService,
workManagerScheduler = workManagerScheduler,

View file

@ -38,6 +38,7 @@ object QrErrorMapper {
is RustHumanQrLoginException.ConnectionInsecure -> QrLoginException.ConnectionInsecure
is RustHumanQrLoginException.Declined -> QrLoginException.Declined
is RustHumanQrLoginException.Expired -> QrLoginException.Expired
is RustHumanQrLoginException.NotFound -> QrLoginException.NotFound
is RustHumanQrLoginException.OtherDeviceNotSignedIn -> QrLoginException.OtherDeviceNotSignedIn
is RustHumanQrLoginException.LinkingNotSupported -> QrLoginException.LinkingNotSupported
is RustHumanQrLoginException.Unknown -> QrLoginException.Unknown
@ -45,6 +46,5 @@ object QrErrorMapper {
is RustHumanQrLoginException.SlidingSyncNotAvailable -> QrLoginException.SlidingSyncNotAvailable
is RustHumanQrLoginException.CheckCodeAlreadySent -> QrLoginException.CheckCodeAlreadySent
is RustHumanQrLoginException.CheckCodeCannotBeSent -> QrLoginException.CheckCodeCannotBeSent
is RustHumanQrLoginException.NotFound -> QrLoginException.NotFound
}
}

View file

@ -80,6 +80,7 @@ import org.matrix.rustcomponents.sdk.getElementCallRequiredPermissions
import org.matrix.rustcomponents.sdk.use
import timber.log.Timber
import uniffi.matrix_sdk.RoomPowerLevelChanges
import uniffi.matrix_sdk_ui.TimelineEventFocusThreadMode
import uniffi.matrix_sdk_ui.TimelineReadReceiptTracking
import kotlin.coroutines.cancellation.CancellationException
import org.matrix.rustcomponents.sdk.IdentityStatusChange as RustIdentityStateChange
@ -177,21 +178,18 @@ class JoinedRustRoom(
): Result<Timeline> = withContext(roomDispatcher) {
val hideThreadedEvents = featureFlagService.isFeatureEnabled(FeatureFlags.Threads)
val focus = when (createTimelineParams) {
is CreateTimelineParams.PinnedOnly -> TimelineFocus.PinnedEvents(
maxEventsToLoad = 100u,
maxConcurrentRequests = 10u,
)
is CreateTimelineParams.PinnedOnly -> TimelineFocus.PinnedEvents
is CreateTimelineParams.MediaOnly -> TimelineFocus.Live(hideThreadedEvents = hideThreadedEvents)
is CreateTimelineParams.Focused -> TimelineFocus.Event(
eventId = createTimelineParams.focusedEventId.value,
numContextEvents = 50u,
hideThreadedEvents = hideThreadedEvents,
threadMode = TimelineEventFocusThreadMode.Automatic(hideThreadedEvents),
)
is CreateTimelineParams.MediaOnlyFocused -> TimelineFocus.Event(
eventId = createTimelineParams.focusedEventId.value,
numContextEvents = 50u,
// Never hide threaded events in media focused timeline
hideThreadedEvents = false,
threadMode = TimelineEventFocusThreadMode.Automatic(false),
)
is CreateTimelineParams.Threaded -> TimelineFocus.Thread(
rootEventId = createTimelineParams.threadRootEventId.value,

View file

@ -57,7 +57,7 @@ class RustRoomFactory(
private val roomListService: RoomListService,
private val innerRoomListService: InnerRoomListService,
private val roomSyncSubscriber: RoomSyncSubscriber,
private val timelineEventTypeFilterFactory: TimelineEventTypeFilterFactory,
private val timelineEventFilterFactory: TimelineEventFilterFactory,
private val featureFlagService: FeatureFlagService,
private val roomMembershipObserver: RoomMembershipObserver,
private val roomInfoMapper: RoomInfoMapper,
@ -70,7 +70,7 @@ class RustRoomFactory(
private val eventFilters = TimelineConfig.excludedEvents
.takeIf { it.isNotEmpty() }
?.let { listStateEventType ->
timelineEventTypeFilterFactory.create(listStateEventType)
timelineEventFilterFactory.create(listStateEventType)
}
suspend fun destroy() {
@ -133,7 +133,7 @@ class RustRoomFactory(
sdkRoom.timelineWithConfiguration(
TimelineConfiguration(
focus = TimelineFocus.Live(hideThreadedEvents = hideThreadedEvents),
filter = eventFilters?.let(TimelineFilter::EventTypeFilter) ?: TimelineFilter.All,
filter = eventFilters?.let(TimelineFilter::EventFilter) ?: TimelineFilter.All,
internalIdPrefix = "live",
dateDividerMode = DateDividerMode.DAILY,
trackReadReceipts = TimelineReadReceiptTracking.ALL_EVENTS,

View file

@ -12,16 +12,16 @@ import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.ContributesBinding
import io.element.android.libraries.matrix.api.room.StateEventType
import org.matrix.rustcomponents.sdk.FilterTimelineEventType
import org.matrix.rustcomponents.sdk.TimelineEventTypeFilter
import org.matrix.rustcomponents.sdk.TimelineEventFilter
interface TimelineEventTypeFilterFactory {
fun create(listStateEventType: List<StateEventType>): TimelineEventTypeFilter
interface TimelineEventFilterFactory {
fun create(listStateEventType: List<StateEventType>): TimelineEventFilter
}
@ContributesBinding(AppScope::class)
class RustTimelineEventTypeFilterFactory : TimelineEventTypeFilterFactory {
override fun create(listStateEventType: List<StateEventType>): TimelineEventTypeFilter {
return TimelineEventTypeFilter.exclude(
class RustTimelineEventFilterFactory : TimelineEventFilterFactory {
override fun create(listStateEventType: List<StateEventType>): TimelineEventFilter {
return TimelineEventFilter.excludeEventTypes(
listStateEventType.map { stateEventType ->
FilterTimelineEventType.State(stateEventType.map())
}

View file

@ -14,6 +14,7 @@ import org.matrix.rustcomponents.sdk.RoomListEntriesDynamicFilterKind.Any
import org.matrix.rustcomponents.sdk.RoomListEntriesDynamicFilterKind.Category
import org.matrix.rustcomponents.sdk.RoomListEntriesDynamicFilterKind.DeduplicateVersions
import org.matrix.rustcomponents.sdk.RoomListEntriesDynamicFilterKind.Favourite
import org.matrix.rustcomponents.sdk.RoomListEntriesDynamicFilterKind.Identifiers
import org.matrix.rustcomponents.sdk.RoomListEntriesDynamicFilterKind.Invite
import org.matrix.rustcomponents.sdk.RoomListEntriesDynamicFilterKind.NonLeft
import org.matrix.rustcomponents.sdk.RoomListEntriesDynamicFilterKind.NonSpace
@ -60,6 +61,7 @@ internal object RoomListFilterMapper {
return when (filter) {
is RoomListFilter.All -> All(filters = filter.filters.map { mapFilter(it) })
is RoomListFilter.Any -> Any(filters = filter.filters.map { mapFilter(it) })
is RoomListFilter.Identifiers -> Identifiers(identifiers = filter.values.map { it.value })
RoomListFilter.None -> None
RoomListFilter.Category.Group -> Category(RoomListFilterCategory.GROUP)
RoomListFilter.Category.People -> Category(RoomListFilterCategory.PEOPLE)

View file

@ -53,6 +53,11 @@ class RoomSummaryFactory(
senderProfile = event.profile.map(),
isOwn = event.isOwn,
)
is RustLatestEventValue.RemoteInvite -> LatestEventValue.RoomInvite(
timestamp = event.timestamp.toLong(),
inviterId = event.inviter?.let(::UserId),
invitedProfile = event.inviterProfile.map(),
)
}
}
return RoomSummary(

View file

@ -16,6 +16,7 @@ import io.element.android.libraries.matrix.api.spaces.LeaveSpaceHandle
import io.element.android.libraries.matrix.api.spaces.SpaceRoom
import io.element.android.libraries.matrix.api.spaces.SpaceRoomList
import io.element.android.libraries.matrix.api.spaces.SpaceService
import io.element.android.libraries.matrix.api.spaces.SpaceServiceFilter
import io.element.android.libraries.matrix.impl.util.cancelAndDestroy
import io.element.android.services.analytics.api.AnalyticsService
import kotlinx.coroutines.CoroutineDispatcher
@ -31,9 +32,11 @@ import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.withContext
import org.matrix.rustcomponents.sdk.SpaceFilterUpdate
import org.matrix.rustcomponents.sdk.SpaceListUpdate
import org.matrix.rustcomponents.sdk.SpaceServiceInterface
import org.matrix.rustcomponents.sdk.SpaceServiceJoinedSpacesListener
import org.matrix.rustcomponents.sdk.SpaceServiceSpaceFiltersListener
import timber.log.Timber
import org.matrix.rustcomponents.sdk.SpaceService as ClientSpaceService
@ -45,20 +48,20 @@ class RustSpaceService(
private val analyticsService: AnalyticsService,
) : SpaceService {
private val spaceRoomMapper = SpaceRoomMapper()
override val spaceRoomsFlow = MutableSharedFlow<List<SpaceRoom>>(replay = 1, extraBufferCapacity = 1)
private val spaceFilterMapper = SpaceServiceFilterMapper(spaceRoomMapper)
override val topLevelSpacesFlow = MutableSharedFlow<List<SpaceRoom>>(replay = 1, extraBufferCapacity = 1)
private val spaceListUpdateProcessor = SpaceListUpdateProcessor(
spaceRoomsFlow = spaceRoomsFlow,
spaceRoomsFlow = topLevelSpacesFlow,
mapper = spaceRoomMapper,
analyticsService = analyticsService,
)
override suspend fun joinedSpaces(): Result<List<SpaceRoom>> = withContext(sessionDispatcher) {
runCatchingExceptions {
innerSpaceService
.topLevelJoinedSpaces()
.map(spaceRoomMapper::map)
}
}
override val spaceFiltersFlow = MutableSharedFlow<List<SpaceServiceFilter>>(replay = 1, extraBufferCapacity = 1)
private val spaceFilterUpdateProcessor = SpaceServiceFilterUpdateProcessor(
spaceFiltersFlow = spaceFiltersFlow,
mapper = spaceFilterMapper,
)
override suspend fun joinedParents(spaceId: RoomId): Result<List<SpaceRoom>> = withContext(sessionDispatcher) {
runCatchingExceptions {
@ -123,6 +126,13 @@ class RustSpaceService(
spaceListUpdateProcessor.postUpdates(updates)
}
.launchIn(sessionCoroutineScope)
innerSpaceService
.spaceFilterListUpdate()
.onEach { updates ->
spaceFilterUpdateProcessor.postUpdates(updates)
}
.launchIn(sessionCoroutineScope)
}
}
@ -142,3 +152,20 @@ internal fun SpaceServiceInterface.spaceListUpdate(): Flow<List<SpaceListUpdate>
}.catch {
Timber.d(it, "spaceDiffFlow() failed")
}.buffer(Channel.UNLIMITED)
internal fun SpaceServiceInterface.spaceFilterListUpdate(): Flow<List<SpaceFilterUpdate>> =
callbackFlow {
val listener = object : SpaceServiceSpaceFiltersListener {
override fun onUpdate(filterUpdates: List<SpaceFilterUpdate>) {
trySendBlocking(filterUpdates)
}
}
Timber.d("Open spaceFilterDiffFlow for SpaceServiceInterface ${this@spaceFilterListUpdate}")
val taskHandle = subscribeToSpaceFilters(listener)
awaitClose {
Timber.d("Close spaceFilterDiffFlow for SpaceServiceInterface ${this@spaceFilterListUpdate}")
taskHandle.cancelAndDestroy()
}
}.catch {
Timber.d(it, "spaceFilterListUpdate() failed")
}.buffer(Channel.UNLIMITED)

View file

@ -0,0 +1,25 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.libraries.matrix.impl.spaces
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.spaces.SpaceServiceFilter
import org.matrix.rustcomponents.sdk.SpaceFilter as RustSpaceFilter
class SpaceServiceFilterMapper(
private val spaceRoomMapper: SpaceRoomMapper,
) {
fun map(spaceFilter: RustSpaceFilter): SpaceServiceFilter {
return SpaceServiceFilter(
spaceRoom = spaceRoomMapper.map(spaceFilter.spaceRoom),
level = spaceFilter.level.toInt(),
descendants = spaceFilter.descendants.map { RoomId(it) },
)
}
}

View file

@ -0,0 +1,85 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.libraries.matrix.impl.spaces
import io.element.android.libraries.matrix.api.spaces.SpaceServiceFilter
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import org.matrix.rustcomponents.sdk.SpaceFilterUpdate
import timber.log.Timber
internal class SpaceServiceFilterUpdateProcessor(
private val spaceFiltersFlow: MutableSharedFlow<List<SpaceServiceFilter>>,
private val mapper: SpaceServiceFilterMapper,
) {
private val mutex = Mutex()
suspend fun postUpdates(updates: List<SpaceFilterUpdate>) {
Timber.v("Update space filters from postUpdates (with ${updates.size} items) on ${Thread.currentThread()}")
updateSpaceFilters {
updates.forEach { update -> applyUpdate(update) }
}
}
private suspend fun updateSpaceFilters(block: MutableList<SpaceServiceFilter>.() -> Unit) =
mutex.withLock {
val spaceFilters = if (spaceFiltersFlow.replayCache.isNotEmpty()) {
spaceFiltersFlow.first().toMutableList()
} else {
mutableListOf()
}
block(spaceFilters)
spaceFiltersFlow.emit(spaceFilters)
}
private fun MutableList<SpaceServiceFilter>.applyUpdate(update: SpaceFilterUpdate) {
when (update) {
is SpaceFilterUpdate.Append -> {
val newFilters = update.values.map(mapper::map)
addAll(newFilters)
}
SpaceFilterUpdate.Clear -> clear()
is SpaceFilterUpdate.Insert -> {
val newFilter = mapper.map(update.value)
add(update.index.toInt(), newFilter)
}
SpaceFilterUpdate.PopBack -> {
removeAt(lastIndex)
}
SpaceFilterUpdate.PopFront -> {
removeAt(0)
}
is SpaceFilterUpdate.PushBack -> {
val newFilter = mapper.map(update.value)
add(newFilter)
}
is SpaceFilterUpdate.PushFront -> {
val newFilter = mapper.map(update.value)
add(0, newFilter)
}
is SpaceFilterUpdate.Remove -> {
removeAt(update.index.toInt())
}
is SpaceFilterUpdate.Reset -> {
clear()
val newFilters = update.values.map(mapper::map)
addAll(newFilters)
}
is SpaceFilterUpdate.Set -> {
val newFilter = mapper.map(update.value)
this[update.index.toInt()] = newFilter
}
is SpaceFilterUpdate.Truncate -> {
subList(update.length.toInt(), size).clear()
}
}
}
}

View file

@ -13,7 +13,7 @@ import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.matrix.impl.auth.FakeProxyProvider
import io.element.android.libraries.matrix.impl.auth.FakeUserCertificatesProvider
import io.element.android.libraries.matrix.impl.room.FakeTimelineEventTypeFilterFactory
import io.element.android.libraries.matrix.impl.room.FakeTimelineEventFilterFactory
import io.element.android.libraries.matrix.impl.storage.FakeSqliteStoreBuilderProvider
import io.element.android.libraries.network.useragent.SimpleUserAgentProvider
import io.element.android.libraries.sessionstorage.api.SessionStore
@ -63,7 +63,7 @@ fun TestScope.createRustMatrixClientFactory(
clock = FakeSystemClock(),
analyticsService = FakeAnalyticsService(),
featureFlagService = FakeFeatureFlagService(),
timelineEventTypeFilterFactory = FakeTimelineEventTypeFilterFactory(),
timelineEventFilterFactory = FakeTimelineEventFilterFactory(),
clientBuilderProvider = clientBuilderProvider,
sqliteStoreBuilderProvider = FakeSqliteStoreBuilderProvider(),
workManagerScheduler = workManagerScheduler,

View file

@ -15,7 +15,7 @@ import io.element.android.libraries.core.data.bytes
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiClient
import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiSyncService
import io.element.android.libraries.matrix.impl.room.FakeTimelineEventTypeFilterFactory
import io.element.android.libraries.matrix.impl.room.FakeTimelineEventFilterFactory
import io.element.android.libraries.matrix.test.AN_AVATAR_URL
import io.element.android.libraries.matrix.test.A_DEVICE_ID
import io.element.android.libraries.matrix.test.A_ROOM_ID
@ -150,7 +150,7 @@ class RustMatrixClientTest {
dispatchers = testCoroutineDispatchers(),
baseCacheDirectory = File(""),
clock = FakeSystemClock(),
timelineEventTypeFilterFactory = FakeTimelineEventTypeFilterFactory(),
timelineEventFilterFactory = FakeTimelineEventFilterFactory(),
featureFlagService = FakeFeatureFlagService(),
analyticsService = FakeAnalyticsService(),
workManagerScheduler = FakeWorkManagerScheduler(submitLambda = {}),

View file

@ -30,6 +30,7 @@ internal fun aRustNotificationItem(
hasMention: Boolean? = false,
threadId: ThreadId? = null,
actions: List<Action>? = null,
rawEvent: String = "",
) = NotificationItem(
event = event,
senderInfo = senderInfo,
@ -38,6 +39,7 @@ internal fun aRustNotificationItem(
hasMention = hasMention,
threadId = threadId?.value,
actions = actions,
rawEvent = rawEvent,
)
internal fun aRustBatchNotificationResultOk(

View file

@ -9,6 +9,6 @@
package io.element.android.libraries.matrix.impl.fixtures.fakes
import org.matrix.rustcomponents.sdk.NoHandle
import org.matrix.rustcomponents.sdk.TimelineEventTypeFilter
import org.matrix.rustcomponents.sdk.TimelineEventFilter
class FakeFfiTimelineEventTypeFilter : TimelineEventTypeFilter(NoHandle)
class FakeFfiTimelineEventFilter : TimelineEventFilter(NoHandle)

View file

@ -9,11 +9,11 @@
package io.element.android.libraries.matrix.impl.room
import io.element.android.libraries.matrix.api.room.StateEventType
import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiTimelineEventTypeFilter
import org.matrix.rustcomponents.sdk.TimelineEventTypeFilter
import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiTimelineEventFilter
import org.matrix.rustcomponents.sdk.TimelineEventFilter
class FakeTimelineEventTypeFilterFactory : TimelineEventTypeFilterFactory {
override fun create(listStateEventType: List<StateEventType>): TimelineEventTypeFilter {
return FakeFfiTimelineEventTypeFilter()
class FakeTimelineEventFilterFactory : TimelineEventFilterFactory {
override fun create(listStateEventType: List<StateEventType>): TimelineEventFilter {
return FakeFfiTimelineEventFilter()
}
}

View file

@ -13,6 +13,7 @@ import io.element.android.libraries.matrix.api.spaces.LeaveSpaceHandle
import io.element.android.libraries.matrix.api.spaces.SpaceRoom
import io.element.android.libraries.matrix.api.spaces.SpaceRoomList
import io.element.android.libraries.matrix.api.spaces.SpaceService
import io.element.android.libraries.matrix.api.spaces.SpaceServiceFilter
import io.element.android.tests.testutils.lambda.lambdaError
import io.element.android.tests.testutils.simulateLongTask
import kotlinx.coroutines.flow.MutableSharedFlow
@ -20,7 +21,6 @@ import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.asSharedFlow
class FakeSpaceService(
private val joinedSpacesResult: () -> Result<List<SpaceRoom>> = { lambdaError() },
private val spaceRoomListResult: (RoomId) -> SpaceRoomList = { lambdaError() },
private val leaveSpaceHandleResult: (RoomId) -> LeaveSpaceHandle = { lambdaError() },
private val removeChildFromSpaceResult: (RoomId, RoomId) -> Result<Unit> = { _, _ -> lambdaError() },
@ -29,16 +29,20 @@ class FakeSpaceService(
private val editableSpacesResult: () -> Result<List<SpaceRoom>> = { lambdaError() },
private val addChildToSpaceResult: (RoomId, RoomId) -> Result<Unit> = { _, _ -> lambdaError() },
) : SpaceService {
private val _spaceRoomsFlow = MutableSharedFlow<List<SpaceRoom>>()
override val spaceRoomsFlow: SharedFlow<List<SpaceRoom>>
get() = _spaceRoomsFlow.asSharedFlow()
private val _topLevelSpacesFlow = MutableSharedFlow<List<SpaceRoom>>()
override val topLevelSpacesFlow: SharedFlow<List<SpaceRoom>>
get() = _topLevelSpacesFlow.asSharedFlow()
suspend fun emitSpaceRoomList(value: List<SpaceRoom>) {
_spaceRoomsFlow.emit(value)
suspend fun emitTopLevelSpaces(value: List<SpaceRoom>) {
_topLevelSpacesFlow.emit(value)
}
override suspend fun joinedSpaces(): Result<List<SpaceRoom>> = simulateLongTask {
return joinedSpacesResult()
private val _spaceFiltersFlow = MutableSharedFlow<List<SpaceServiceFilter>>()
override val spaceFiltersFlow: SharedFlow<List<SpaceServiceFilter>>
get() = _spaceFiltersFlow.asSharedFlow()
suspend fun emitSpaceFilters(value: List<SpaceServiceFilter>) {
_spaceFiltersFlow.emit(value)
}
override suspend fun joinedParents(spaceId: RoomId): Result<List<SpaceRoom>> {

View file

@ -13,6 +13,7 @@
<item quantity="one">"%d varsel"</item>
<item quantity="other">"%d varsler"</item>
</plurals>
<string name="notification_error_unified_push_unregistered_android">"UnifiedPush-varslingsdistributøren kunne ikke registreres, så du vil ikke motta varsler lenger. Sjekk varslingsinnstillingene til appen og statusen til push-distributøren."</string>
<string name="notification_fallback_content">"Du har nye meldinger."</string>
<string name="notification_incoming_call">"📹 Innkommende anrop"</string>
<string name="notification_inline_reply_failed">"** Kunne ikke sende - vennligst åpne rommet"</string>
@ -37,6 +38,8 @@
<string name="notification_room_invite_body_with_sender">"%1$s inviterte deg til å bli med i rommet"</string>
<string name="notification_sender_me">"Meg"</string>
<string name="notification_sender_mention_reply">"%1$s nevnt eller besvart"</string>
<string name="notification_space_invite_body">"Inviterte deg til å bli med i området"</string>
<string name="notification_space_invite_body_with_sender">"%1$s inviterte deg til å bli med i området"</string>
<string name="notification_test_push_notification_content">"Du ser på varselet! Klikk på meg!"</string>
<string name="notification_thread_in_room">"Tråd i %1$s"</string>
<string name="notification_ticker_text_dm">"%1$s: %2$s"</string>

View file

@ -58,6 +58,7 @@
<string name="a11y_your_avatar">"Váš avatar"</string>
<string name="action_accept">"Přijmout"</string>
<string name="action_add_caption">"Přidat titulek"</string>
<string name="action_add_existing_rooms">"Přidat stávající místnosti"</string>
<string name="action_add_to_timeline">"Přidat na časovou osu"</string>
<string name="action_back">"Zpět"</string>
<string name="action_call">"Hovor"</string>
@ -77,6 +78,7 @@
<string name="action_copy_text">"Kopírovat text"</string>
<string name="action_create">"Vytvořit"</string>
<string name="action_create_room">"Vytvořit místnost"</string>
<string name="action_create_space">"Vytvořte prostor"</string>
<string name="action_deactivate">"Deaktivovat"</string>
<string name="action_deactivate_account">"Deaktivovat účet"</string>
<string name="action_decline">"Odmítnout"</string>
@ -93,6 +95,7 @@
<string name="action_enable">"Povolit"</string>
<string name="action_end_poll">"Ukončit hlasování"</string>
<string name="action_enter_pin">"Zadejte PIN"</string>
<string name="action_explore_public_spaces">"Prozkoumejte veřejné prostory"</string>
<string name="action_finish">"Dokončit"</string>
<string name="action_forgot_password">"Zapomněli jste heslo?"</string>
<string name="action_forward">"Přeposlat"</string>
@ -194,6 +197,7 @@
<string name="common_copied_to_clipboard">"Zkopírováno do schránky"</string>
<string name="common_copyright">"Autorská práva"</string>
<string name="common_creating_room">"Vytváření místnosti…"</string>
<string name="common_creating_space">"Vytváření prostoru…"</string>
<string name="common_current_user_canceled_knock">"Žádost zrušena"</string>
<string name="common_current_user_left_room">"Místnost opuštěna"</string>
<string name="common_current_user_left_space">"Opustit prostor"</string>
@ -295,6 +299,7 @@ Důvod: %1$s."</string>
<string name="common_reason">"Důvod"</string>
<string name="common_recovery_key">"Klíč pro obnovení"</string>
<string name="common_refreshing">"Obnovování…"</string>
<string name="common_removing">"Odstraňování…"</string>
<plurals name="common_replies">
<item quantity="other">"%1$d odpovědí"</item>
</plurals>
@ -303,6 +308,7 @@ Důvod: %1$s."</string>
<string name="common_report_a_problem">"Nahlásit problém"</string>
<string name="common_report_submitted">"Zpráva odeslána"</string>
<string name="common_rich_text_editor">"Editor formátovaného textu"</string>
<string name="common_role">"Role"</string>
<string name="common_room">"Místnost"</string>
<string name="common_room_name">"Název místnosti"</string>
<string name="common_room_name_placeholder">"např. název vašeho projektu"</string>
@ -319,6 +325,11 @@ Důvod: %1$s."</string>
<string name="common_security">"Zabezpečení"</string>
<string name="common_seen_by">"Viděno"</string>
<string name="common_select_account">"Vybrat účet"</string>
<plurals name="common_selected_count">
<item quantity="one">"%1$d vybraný"</item>
<item quantity="few">"%1$d vybrané"</item>
<item quantity="other">"%1$d vybraných"</item>
</plurals>
<string name="common_send_to">"Odeslat do"</string>
<string name="common_sending">"Odesílání…"</string>
<string name="common_sending_failed">"Odeslání se nezdařilo"</string>
@ -329,6 +340,7 @@ Důvod: %1$s."</string>
<string name="common_server_url">"URL serveru"</string>
<string name="common_settings">"Nastavení"</string>
<string name="common_share_space">"Sdílet prostor"</string>
<string name="common_shared_history">"Noví členové vidí historii"</string>
<string name="common_shared_location">"Sdílená poloha"</string>
<string name="common_shared_space">"Sdílený prostor"</string>
<string name="common_signing_out">"Odhlašování"</string>
@ -344,6 +356,7 @@ Důvod: %1$s."</string>
<string name="common_starting_chat">"Zahajování chatu…"</string>
<string name="common_sticker">"Nálepka"</string>
<string name="common_success">"Úspěch"</string>
<string name="common_suggested">"Doporučeno"</string>
<string name="common_suggestions">"Návrhy"</string>
<string name="common_syncing">"Synchronizace"</string>
<string name="common_system">"Systém"</string>
@ -380,7 +393,10 @@ Důvod: %1$s."</string>
<string name="common_voice_message">"Hlasová zpráva"</string>
<string name="common_waiting">"Čekání…"</string>
<string name="common_waiting_for_decryption_key">"Čekání na dešifrovací klíč"</string>
<string name="common_world_readable_history">"Kdokoli může vidět historii"</string>
<string name="common_you">"Vy"</string>
<string name="crypto_event_key_forwarded_known_profile_dialog_content">"%1$s (%2$s) sdílel(a) tuto zprávu v době, kdy jste nebyli v místnosti."</string>
<string name="crypto_event_key_forwarded_unknown_profile_dialog_content">"%1$s sdílel(a) tuto zprávu v době, kdy jste nebyli v místnosti."</string>
<string name="crypto_history_visible">"Tato místnost byla nastavena tak, aby noví členové mohli číst historii. %1$s"</string>
<string name="crypto_identity_change_pin_violation">"Identita uživatele %1$s se změnila. %2$s"</string>
<string name="crypto_identity_change_pin_violation_new">"Identita uživatele %1$s %2$s se změnila. %3$s"</string>
@ -474,6 +490,7 @@ Opravdu chcete pokračovat?"</string>
<string name="screen_share_this_location_action">"Sdílet tuto polohu"</string>
<string name="screen_space_list_description">"Prostory, které jste vytvořili nebo se k nim připojili."</string>
<string name="screen_space_list_details">"%1$s • %2$s"</string>
<string name="screen_space_list_empty_state_title">"Vytvořte prostory pro uspořádání místností"</string>
<string name="screen_space_list_parent_space">"%1$s prostor"</string>
<string name="screen_space_list_title">"Prostory"</string>
<string name="screen_timeline_item_menu_send_failure_changed_identity">"Zpráva nebyla odeslána, protože ověřená identita uživatele %1$s se změnila."</string>

View file

@ -389,6 +389,7 @@ Põhjus: %1$s."</string>
<string name="common_world_readable_history">"Kõik võivad ajalugu näha"</string>
<string name="common_you">"Sina"</string>
<string name="crypto_event_key_forwarded_known_profile_dialog_content">"Kuna sind polnud saatmise ajal jututoas, siis %1$s (%2$s) jagas seda sõnumit sinuga."</string>
<string name="crypto_event_key_forwarded_unknown_profile_dialog_content">"%1$s jagas seda sõnumit, kuna sind ei olnud selle algse saatmise ajal jututoas."</string>
<string name="crypto_history_visible">"See jututuba on seadistatud sedaviisi, et ka uued liikmed saavad lugeda varasemat ajalugu. %1$s"</string>
<string name="crypto_identity_change_pin_violation">"Kasutaja %1$s võrguidentiteet on lähtestatud. %2$s"</string>
<string name="crypto_identity_change_pin_violation_new">"Kasutaja %1$s %2$s võrguidentiteet on lähtestatud. %3$s"</string>

View file

@ -285,8 +285,10 @@ Raison : %1$s."</string>
</plurals>
<string name="common_preparing">"Préparation…"</string>
<string name="common_privacy_policy">"Politique de confidentialité"</string>
<string name="common_private">"Privé"</string>
<string name="common_private_room">"Salon privé"</string>
<string name="common_private_space">"Espace privé"</string>
<string name="common_public">"Public"</string>
<string name="common_public_room">"Salon public"</string>
<string name="common_public_space">"Espace public"</string>
<string name="common_reaction">"Réaction"</string>
@ -341,6 +343,7 @@ Raison : %1$s."</string>
<string name="common_something_went_wrong">"Une erreur sest produite"</string>
<string name="common_something_went_wrong_message">"Nous avons rencontré un problème. Veuillez réessayer."</string>
<string name="common_space">"Espace"</string>
<string name="common_space_members">"Membres de lespace"</string>
<string name="common_space_topic_placeholder">"Quel est le sujet de cet espace ?"</string>
<plurals name="common_spaces">
<item quantity="one">"%1$d Espace"</item>

View file

@ -56,6 +56,7 @@
<string name="a11y_your_avatar">"Din avatar"</string>
<string name="action_accept">"Godta"</string>
<string name="action_add_caption">"Legg til bildetekst"</string>
<string name="action_add_existing_rooms">"Legg til eksisterende rom"</string>
<string name="action_add_to_timeline">"Legg til i tidslinjen"</string>
<string name="action_back">"Tilbake"</string>
<string name="action_call">"Ring"</string>
@ -75,6 +76,7 @@
<string name="action_copy_text">"Kopier tekst"</string>
<string name="action_create">"Opprett"</string>
<string name="action_create_room">"Opprett rom"</string>
<string name="action_create_space">"Opprett område"</string>
<string name="action_deactivate">"Deaktiver"</string>
<string name="action_deactivate_account">"Deaktiver kontoen"</string>
<string name="action_decline">"Avslå"</string>
@ -162,6 +164,7 @@
<string name="action_static_map_load">"Trykk for å laste inn kart"</string>
<string name="action_take_photo">"Ta bilde"</string>
<string name="action_tap_for_options">"Trykk for alternativer"</string>
<string name="action_translate">"Oversett"</string>
<string name="action_try_again">"Prøv igjen"</string>
<string name="action_unpin">"Løsne"</string>
<string name="action_view">"Vis"</string>
@ -191,6 +194,7 @@
<string name="common_copied_to_clipboard">"Kopiert til utklippstavlen"</string>
<string name="common_copyright">"Opphavsrett"</string>
<string name="common_creating_room">"Oppretter rom …"</string>
<string name="common_creating_space">"Oppretter område…"</string>
<string name="common_current_user_canceled_knock">"Forespørsel kansellert"</string>
<string name="common_current_user_left_room">"Forlot rommet"</string>
<string name="common_current_user_left_space">"Forlot område"</string>
@ -236,6 +240,7 @@
<string name="common_light">"Lys"</string>
<string name="common_line_copied_to_clipboard">"Linje kopiert til utklippstavlen"</string>
<string name="common_link_copied_to_clipboard">"Lenke kopiert til utklippstavlen"</string>
<string name="common_link_new_device">"Koble til ny enhet"</string>
<string name="common_loading">"Laster inn…"</string>
<string name="common_loading_more">"Laster inn mer…"</string>
<plurals name="common_many_members">
@ -248,10 +253,12 @@
</plurals>
<string name="common_message">"Melding"</string>
<string name="common_message_actions">"Meldingshandlinger"</string>
<string name="common_message_failed_to_send">"Sending av beskjed feilet"</string>
<string name="common_message_layout">"Meldingsoppsett"</string>
<string name="common_message_removed">"Melding fjernet"</string>
<string name="common_modern">"Moderne"</string>
<string name="common_mute">"Demp"</string>
<string name="common_name">"Navn"</string>
<string name="common_name_and_id">"%1$s (%2$s)"</string>
<string name="common_no_results">"Ingen resultater"</string>
<string name="common_no_room_name">"Ingen romnavn"</string>
@ -324,6 +331,7 @@
<string name="common_something_went_wrong">"Noe gikk galt"</string>
<string name="common_something_went_wrong_message">"Vi har støtt på et problem. Vennligst prøv igjen."</string>
<string name="common_space">"Område"</string>
<string name="common_space_topic_placeholder">"Hva handler dette området om?"</string>
<plurals name="common_spaces">
<item quantity="one">"%1$d Område"</item>
<item quantity="other">"%1$d Områder"</item>
@ -331,6 +339,7 @@
<string name="common_starting_chat">"Starter chat…"</string>
<string name="common_sticker">"Klistremerke"</string>
<string name="common_success">"Suksess"</string>
<string name="common_suggested">"Foreslått"</string>
<string name="common_suggestions">"Forslag"</string>
<string name="common_syncing">"Synkroniserer"</string>
<string name="common_system">"System"</string>
@ -368,6 +377,7 @@
<string name="common_waiting">"Venter…"</string>
<string name="common_waiting_for_decryption_key">"Venter på denne meldingen"</string>
<string name="common_you">"Du"</string>
<string name="crypto_history_visible">"Dette rommet er konfigurert slik at nye medlemmer kan lese historikken.%1$s"</string>
<string name="crypto_identity_change_pin_violation">"%1$s\'s identitet ble tilbakestilt. %2$s"</string>
<string name="crypto_identity_change_pin_violation_new">"%1$ss %2$s-identitet ble tilbakestilt. %3$s"</string>
<string name="crypto_identity_change_pin_violation_new_user_id">"(%1$s)"</string>
@ -459,6 +469,7 @@ Er du sikker på at du vil fortsette?"</string>
<string name="screen_share_this_location_action">"Del denne lokasjonen"</string>
<string name="screen_space_list_description">"Områder du har opprettet eller blitt med i."</string>
<string name="screen_space_list_details">"%1$s • %2$s"</string>
<string name="screen_space_list_empty_state_title">"Opprett område for å organisere rom"</string>
<string name="screen_space_list_parent_space">"%1$s område"</string>
<string name="screen_space_list_title">"Områder"</string>
<string name="screen_timeline_item_menu_send_failure_changed_identity">"Meldingen ble ikke sendt fordi %1$ss verifiserte identitet er tilbakestilt."</string>