Merge branch 'develop' into feature/fga/space_list_join_action
This commit is contained in:
commit
bb5a4f4954
70 changed files with 824 additions and 385 deletions
|
|
@ -9,15 +9,15 @@ package io.element.android.x.di
|
|||
|
||||
import dev.zacsweers.metro.ContributesBinding
|
||||
import dev.zacsweers.metro.Inject
|
||||
import io.element.android.appnav.di.RoomComponentFactory
|
||||
import io.element.android.appnav.di.RoomGraphFactory
|
||||
import io.element.android.libraries.di.SessionScope
|
||||
import io.element.android.libraries.matrix.api.room.JoinedRoom
|
||||
|
||||
@ContributesBinding(SessionScope::class)
|
||||
@Inject
|
||||
class DefaultRoomComponentFactory(
|
||||
class DefaultRoomGraphFactory(
|
||||
private val sessionGraph: SessionGraph,
|
||||
) : RoomComponentFactory {
|
||||
) : RoomGraphFactory {
|
||||
override fun create(room: JoinedRoom): Any {
|
||||
return sessionGraph.roomGraphFactory
|
||||
.create(room, room)
|
||||
|
|
@ -7,18 +7,15 @@
|
|||
|
||||
package io.element.android.x.di
|
||||
|
||||
import dev.zacsweers.metro.ContributesTo
|
||||
import dev.zacsweers.metro.GraphExtension
|
||||
import dev.zacsweers.metro.Provides
|
||||
import io.element.android.libraries.architecture.NodeFactoriesBindings
|
||||
import io.element.android.libraries.di.RoomScope
|
||||
import io.element.android.libraries.di.SessionScope
|
||||
import io.element.android.libraries.matrix.api.room.BaseRoom
|
||||
import io.element.android.libraries.matrix.api.room.JoinedRoom
|
||||
|
||||
@GraphExtension(RoomScope::class)
|
||||
interface RoomGraph : NodeFactoriesBindings {
|
||||
@ContributesTo(SessionScope::class)
|
||||
@GraphExtension.Factory
|
||||
interface Factory {
|
||||
fun create(
|
||||
|
|
|
|||
|
|
@ -7,8 +7,6 @@
|
|||
|
||||
package io.element.android.x.di
|
||||
|
||||
import dev.zacsweers.metro.AppScope
|
||||
import dev.zacsweers.metro.ContributesTo
|
||||
import dev.zacsweers.metro.GraphExtension
|
||||
import dev.zacsweers.metro.Provides
|
||||
import io.element.android.libraries.architecture.NodeFactoriesBindings
|
||||
|
|
@ -19,7 +17,6 @@ import io.element.android.libraries.matrix.api.MatrixClient
|
|||
interface SessionGraph : NodeFactoriesBindings {
|
||||
val roomGraphFactory: RoomGraph.Factory
|
||||
|
||||
@ContributesTo(AppScope::class)
|
||||
@GraphExtension.Factory
|
||||
interface Factory {
|
||||
fun create(@Provides matrixClient: MatrixClient): SessionGraph
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@ dependencies {
|
|||
implementation(projects.libraries.pushproviders.api)
|
||||
implementation(projects.libraries.designsystem)
|
||||
implementation(projects.libraries.matrixui)
|
||||
implementation(projects.libraries.uiCommon)
|
||||
implementation(projects.libraries.uiStrings)
|
||||
implementation(projects.features.login.api)
|
||||
|
||||
|
|
|
|||
|
|
@ -36,10 +36,8 @@ import com.bumble.appyx.navmodel.backstack.operation.pop
|
|||
import com.bumble.appyx.navmodel.backstack.operation.push
|
||||
import com.bumble.appyx.navmodel.backstack.operation.replace
|
||||
import com.bumble.appyx.navmodel.backstack.operation.singleTop
|
||||
import dev.zacsweers.metro.AppScope
|
||||
import dev.zacsweers.metro.Assisted
|
||||
import dev.zacsweers.metro.AssistedInject
|
||||
import dev.zacsweers.metro.Inject
|
||||
import im.vector.app.features.analytics.plan.JoinedRoom
|
||||
import io.element.android.annotations.ContributesNode
|
||||
import io.element.android.appnav.loggedin.LoggedInNode
|
||||
|
|
@ -82,6 +80,7 @@ import io.element.android.libraries.matrix.api.permalink.PermalinkData
|
|||
import io.element.android.libraries.matrix.api.verification.SessionVerificationServiceListener
|
||||
import io.element.android.libraries.matrix.api.verification.VerificationRequest
|
||||
import io.element.android.libraries.push.api.notifications.conversations.NotificationConversationService
|
||||
import io.element.android.libraries.ui.common.nodes.emptyNode
|
||||
import io.element.android.services.appnavstate.api.AppNavigationStateService
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.first
|
||||
|
|
@ -282,7 +281,7 @@ class LoggedInFlowNode(
|
|||
|
||||
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
|
||||
return when (navTarget) {
|
||||
NavTarget.Placeholder -> createNode<PlaceholderNode>(buildContext)
|
||||
NavTarget.Placeholder -> emptyNode(buildContext)
|
||||
NavTarget.LoggedInPermanent -> {
|
||||
val callback = object : LoggedInNode.Callback {
|
||||
override fun navigateToNotificationTroubleshoot() {
|
||||
|
|
@ -549,13 +548,6 @@ class LoggedInFlowNode(
|
|||
}
|
||||
}
|
||||
|
||||
@ContributesNode(AppScope::class)
|
||||
@Inject
|
||||
class PlaceholderNode(
|
||||
@Assisted buildContext: BuildContext,
|
||||
@Assisted plugins: List<Plugin>,
|
||||
) : Node(buildContext, plugins = plugins)
|
||||
|
||||
@Parcelize
|
||||
private class AttachRoomOperation(
|
||||
val roomTarget: LoggedInFlowNode.NavTarget.Room,
|
||||
|
|
|
|||
|
|
@ -11,15 +11,11 @@ import android.content.Intent
|
|||
import android.os.Parcelable
|
||||
import androidx.compose.animation.core.Spring
|
||||
import androidx.compose.animation.core.spring
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.bumble.appyx.core.modality.BuildContext
|
||||
import com.bumble.appyx.core.node.Node
|
||||
import com.bumble.appyx.core.node.node
|
||||
import com.bumble.appyx.core.plugin.Plugin
|
||||
import com.bumble.appyx.core.state.MutableSavedStateMap
|
||||
import com.bumble.appyx.navmodel.backstack.BackStack
|
||||
|
|
@ -51,7 +47,6 @@ import io.element.android.libraries.architecture.createNode
|
|||
import io.element.android.libraries.architecture.waitForChildAttached
|
||||
import io.element.android.libraries.core.uri.ensureProtocol
|
||||
import io.element.android.libraries.deeplink.api.DeeplinkData
|
||||
import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator
|
||||
import io.element.android.libraries.featureflag.api.FeatureFlagService
|
||||
import io.element.android.libraries.featureflag.api.FeatureFlags
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
|
|
@ -61,6 +56,7 @@ import io.element.android.libraries.oidc.api.OidcAction
|
|||
import io.element.android.libraries.oidc.api.OidcActionFlow
|
||||
import io.element.android.libraries.sessionstorage.api.LoggedInState
|
||||
import io.element.android.libraries.sessionstorage.api.SessionStore
|
||||
import io.element.android.libraries.ui.common.nodes.emptyNode
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
|
|
@ -219,9 +215,10 @@ import timber.log.Timber
|
|||
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
|
||||
return when (navTarget) {
|
||||
is NavTarget.LoggedInFlow -> {
|
||||
val matrixClient = matrixSessionCache.getOrNull(navTarget.sessionId) ?: return splashNode(buildContext).also {
|
||||
Timber.w("Couldn't find any session, go through SplashScreen")
|
||||
}
|
||||
val matrixClient = matrixSessionCache.getOrNull(navTarget.sessionId)
|
||||
?: return emptyNode(buildContext).also {
|
||||
Timber.w("Couldn't find any session, go through SplashScreen")
|
||||
}
|
||||
val inputs = LoggedInAppScopeFlowNode.Inputs(matrixClient)
|
||||
val callback = object : LoggedInAppScopeFlowNode.Callback {
|
||||
override fun onOpenBugReport() {
|
||||
|
|
@ -252,7 +249,7 @@ import timber.log.Timber
|
|||
)
|
||||
).build()
|
||||
}
|
||||
NavTarget.SplashScreen -> splashNode(buildContext)
|
||||
NavTarget.SplashScreen -> emptyNode(buildContext)
|
||||
NavTarget.BugReport -> {
|
||||
val callback = object : BugReportEntryPoint.Callback {
|
||||
override fun onDone() {
|
||||
|
|
@ -289,12 +286,6 @@ import timber.log.Timber
|
|||
}
|
||||
}
|
||||
|
||||
private fun splashNode(buildContext: BuildContext) = node(buildContext) {
|
||||
Box(modifier = it.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||
CircularProgressIndicator()
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun handleIntent(intent: Intent) {
|
||||
val resolvedIntent = intentResolver.resolve(intent) ?: return
|
||||
when (resolvedIntent) {
|
||||
|
|
|
|||
|
|
@ -9,6 +9,6 @@ package io.element.android.appnav.di
|
|||
|
||||
import io.element.android.libraries.matrix.api.room.JoinedRoom
|
||||
|
||||
fun interface RoomComponentFactory {
|
||||
fun interface RoomGraphFactory {
|
||||
fun create(room: JoinedRoom): Any
|
||||
}
|
||||
|
|
@ -20,7 +20,7 @@ import com.bumble.appyx.navmodel.backstack.operation.push
|
|||
import dev.zacsweers.metro.Assisted
|
||||
import dev.zacsweers.metro.AssistedInject
|
||||
import io.element.android.annotations.ContributesNode
|
||||
import io.element.android.appnav.di.RoomComponentFactory
|
||||
import io.element.android.appnav.di.RoomGraphFactory
|
||||
import io.element.android.appnav.room.RoomNavigationTarget
|
||||
import io.element.android.features.messages.api.MessagesEntryPoint
|
||||
import io.element.android.features.roomdetails.api.RoomDetailsEntryPoint
|
||||
|
|
@ -56,7 +56,7 @@ class JoinedRoomLoadedFlowNode(
|
|||
private val sessionCoroutineScope: CoroutineScope,
|
||||
private val matrixClient: MatrixClient,
|
||||
private val activeRoomsHolder: ActiveRoomsHolder,
|
||||
roomComponentFactory: RoomComponentFactory,
|
||||
roomGraphFactory: RoomGraphFactory,
|
||||
) : BaseFlowNode<JoinedRoomLoadedFlowNode.NavTarget>(
|
||||
backstack = BackStack(
|
||||
initialElement = when (val input = plugins.filterIsInstance<Inputs>().first().initialElement) {
|
||||
|
|
@ -83,7 +83,7 @@ class JoinedRoomLoadedFlowNode(
|
|||
|
||||
private val inputs: Inputs = inputs()
|
||||
private val callbacks = plugins.filterIsInstance<Callback>()
|
||||
override val graph = roomComponentFactory.create(inputs.room)
|
||||
override val graph = roomGraphFactory.create(inputs.room)
|
||||
|
||||
init {
|
||||
lifecycle.subscribe(
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ import com.bumble.appyx.navmodel.backstack.activeElement
|
|||
import com.bumble.appyx.testing.junit4.util.MainDispatcherRule
|
||||
import com.bumble.appyx.testing.unit.common.helper.parentNodeTestHelper
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.appnav.di.RoomComponentFactory
|
||||
import io.element.android.appnav.di.RoomGraphFactory
|
||||
import io.element.android.appnav.room.RoomNavigationTarget
|
||||
import io.element.android.appnav.room.joined.JoinedRoomLoadedFlowNode
|
||||
import io.element.android.features.messages.api.MessagesEntryPoint
|
||||
|
|
@ -70,7 +70,7 @@ class JoinedRoomLoadedFlowNodeTest {
|
|||
}
|
||||
}
|
||||
|
||||
private class FakeRoomComponentFactory : RoomComponentFactory {
|
||||
private class FakeRoomGraphFactory : RoomGraphFactory {
|
||||
override fun create(room: JoinedRoom): Any {
|
||||
return Unit
|
||||
}
|
||||
|
|
@ -110,7 +110,7 @@ class JoinedRoomLoadedFlowNodeTest {
|
|||
roomDetailsEntryPoint = roomDetailsEntryPoint,
|
||||
appNavigationStateService = FakeAppNavigationStateService(),
|
||||
sessionCoroutineScope = this,
|
||||
roomComponentFactory = FakeRoomComponentFactory(),
|
||||
roomGraphFactory = FakeRoomGraphFactory(),
|
||||
matrixClient = FakeMatrixClient(),
|
||||
activeRoomsHolder = activeRoomsHolder,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ import com.bumble.appyx.core.plugin.Plugin
|
|||
import dev.zacsweers.metro.Assisted
|
||||
import dev.zacsweers.metro.AssistedInject
|
||||
import io.element.android.annotations.ContributesNode
|
||||
import io.element.android.appnav.di.RoomComponentFactory
|
||||
import io.element.android.appnav.di.RoomGraphFactory
|
||||
import io.element.android.features.changeroommemberroes.api.ChangeRoomMemberRolesEntryPoint
|
||||
import io.element.android.features.changeroommemberroes.api.ChangeRoomMemberRolesListType
|
||||
import io.element.android.libraries.architecture.NodeInputs
|
||||
|
|
@ -36,7 +36,7 @@ import kotlinx.parcelize.Parcelize
|
|||
class ChangeRoomMemberRolesRootNode(
|
||||
@Assisted buildContext: BuildContext,
|
||||
@Assisted plugins: List<Plugin>,
|
||||
roomComponentFactory: RoomComponentFactory,
|
||||
roomGraphFactory: RoomGraphFactory,
|
||||
) : ParentNode<ChangeRoomMemberRolesRootNode.NavTarget>(
|
||||
navModel = PermanentNavModel(
|
||||
navTargets = setOf(NavTarget),
|
||||
|
|
@ -54,7 +54,7 @@ class ChangeRoomMemberRolesRootNode(
|
|||
|
||||
private val inputs = inputs<Inputs>()
|
||||
|
||||
override val graph = roomComponentFactory.create(inputs.joinedRoom)
|
||||
override val graph = roomGraphFactory.create(inputs.joinedRoom)
|
||||
|
||||
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
|
||||
return createNode<ChangeRolesNode>(
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ class DefaultChangeRoomMemberRolesEntyPointTest {
|
|||
ChangeRoomMemberRolesRootNode(
|
||||
buildContext = buildContext,
|
||||
plugins = plugins,
|
||||
roomComponentFactory = { },
|
||||
roomGraphFactory = { },
|
||||
)
|
||||
}
|
||||
val room = FakeJoinedRoom()
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ dependencies {
|
|||
implementation(projects.libraries.matrixui)
|
||||
implementation(projects.libraries.designsystem)
|
||||
implementation(projects.libraries.preferences.api)
|
||||
implementation(projects.libraries.uiCommon)
|
||||
implementation(projects.libraries.uiStrings)
|
||||
implementation(projects.libraries.testtags)
|
||||
implementation(projects.features.analytics.api)
|
||||
|
|
|
|||
|
|
@ -8,10 +8,7 @@
|
|||
package io.element.android.features.ftue.impl
|
||||
|
||||
import android.os.Parcelable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.bumble.appyx.core.modality.BuildContext
|
||||
|
|
@ -20,10 +17,8 @@ import com.bumble.appyx.core.plugin.Plugin
|
|||
import com.bumble.appyx.navmodel.backstack.BackStack
|
||||
import com.bumble.appyx.navmodel.backstack.operation.newRoot
|
||||
import com.bumble.appyx.navmodel.backstack.operation.replace
|
||||
import dev.zacsweers.metro.AppScope
|
||||
import dev.zacsweers.metro.Assisted
|
||||
import dev.zacsweers.metro.AssistedInject
|
||||
import dev.zacsweers.metro.Inject
|
||||
import io.element.android.annotations.ContributesNode
|
||||
import io.element.android.features.analytics.api.AnalyticsEntryPoint
|
||||
import io.element.android.features.ftue.impl.notifications.NotificationsOptInNode
|
||||
|
|
@ -35,8 +30,8 @@ import io.element.android.features.lockscreen.api.LockScreenEntryPoint
|
|||
import io.element.android.libraries.architecture.BackstackView
|
||||
import io.element.android.libraries.architecture.BaseFlowNode
|
||||
import io.element.android.libraries.architecture.createNode
|
||||
import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator
|
||||
import io.element.android.libraries.di.SessionScope
|
||||
import io.element.android.libraries.ui.common.nodes.emptyNode
|
||||
import kotlinx.coroutines.flow.filterIsInstance
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
|
|
@ -88,7 +83,7 @@ class FtueFlowNode(
|
|||
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
|
||||
return when (navTarget) {
|
||||
NavTarget.Placeholder -> {
|
||||
createNode<PlaceholderNode>(buildContext)
|
||||
emptyNode(buildContext)
|
||||
}
|
||||
is NavTarget.SessionVerification -> {
|
||||
val callback = object : FtueSessionVerificationFlowNode.Callback {
|
||||
|
|
@ -147,17 +142,3 @@ class FtueFlowNode(
|
|||
BackstackView()
|
||||
}
|
||||
}
|
||||
|
||||
@ContributesNode(AppScope::class)
|
||||
@Inject
|
||||
class PlaceholderNode(
|
||||
@Assisted buildContext: BuildContext,
|
||||
@Assisted plugins: List<Plugin>,
|
||||
) : Node(buildContext, plugins = plugins) {
|
||||
@Composable
|
||||
override fun View(modifier: Modifier) {
|
||||
Box(modifier = modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||
CircularProgressIndicator()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@ dependencies {
|
|||
implementation(projects.libraries.testtags)
|
||||
implementation(projects.libraries.uiUtils)
|
||||
implementation(projects.features.logout.api)
|
||||
implementation(projects.libraries.uiCommon)
|
||||
implementation(projects.libraries.uiStrings)
|
||||
implementation(projects.libraries.sessionStorage.api)
|
||||
implementation(projects.services.appnavstate.api)
|
||||
|
|
|
|||
|
|
@ -14,7 +14,6 @@ import androidx.lifecycle.lifecycleScope
|
|||
import com.bumble.appyx.core.lifecycle.subscribe
|
||||
import com.bumble.appyx.core.modality.BuildContext
|
||||
import com.bumble.appyx.core.node.Node
|
||||
import com.bumble.appyx.core.node.node
|
||||
import com.bumble.appyx.core.plugin.Plugin
|
||||
import com.bumble.appyx.navmodel.backstack.BackStack
|
||||
import com.bumble.appyx.navmodel.backstack.operation.newRoot
|
||||
|
|
@ -30,6 +29,7 @@ import io.element.android.libraries.architecture.BackstackView
|
|||
import io.element.android.libraries.architecture.BaseFlowNode
|
||||
import io.element.android.libraries.architecture.createNode
|
||||
import io.element.android.libraries.di.SessionScope
|
||||
import io.element.android.libraries.ui.common.nodes.emptyNode
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
|
@ -42,7 +42,7 @@ class LockScreenSettingsFlowNode(
|
|||
private val pinCodeManager: PinCodeManager,
|
||||
) : BaseFlowNode<LockScreenSettingsFlowNode.NavTarget>(
|
||||
backstack = BackStack(
|
||||
initialElement = NavTarget.Unknown,
|
||||
initialElement = NavTarget.Loading,
|
||||
savedStateMap = buildContext.savedStateMap,
|
||||
),
|
||||
buildContext = buildContext,
|
||||
|
|
@ -50,7 +50,7 @@ class LockScreenSettingsFlowNode(
|
|||
) {
|
||||
sealed interface NavTarget : Parcelable {
|
||||
@Parcelize
|
||||
data object Unknown : NavTarget
|
||||
data object Loading : NavTarget
|
||||
|
||||
@Parcelize
|
||||
data object Unlock : NavTarget
|
||||
|
|
@ -94,6 +94,9 @@ class LockScreenSettingsFlowNode(
|
|||
|
||||
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
|
||||
return when (navTarget) {
|
||||
NavTarget.Loading -> {
|
||||
emptyNode(buildContext)
|
||||
}
|
||||
NavTarget.Unlock -> {
|
||||
val callback = object : PinUnlockNode.Callback {
|
||||
override fun onUnlock() {
|
||||
|
|
@ -113,7 +116,6 @@ class LockScreenSettingsFlowNode(
|
|||
}
|
||||
createNode<LockScreenSettingsNode>(buildContext, plugins = listOf(callback))
|
||||
}
|
||||
NavTarget.Unknown -> node(buildContext) { }
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -459,6 +459,10 @@ class MessagesFlowNode(
|
|||
analyticsService.captureInteraction(Interaction.Name.MobileRoomCallButton)
|
||||
elementCallEntryPoint.startCall(callType)
|
||||
}
|
||||
|
||||
override fun onOpenThread(threadRootId: ThreadId, focusedEventId: EventId?) {
|
||||
backstack.push(NavTarget.OpenThread(threadRootId, focusedEventId))
|
||||
}
|
||||
}
|
||||
createNode<ThreadedMessagesNode>(buildContext, listOf(inputs, callback))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,6 +21,6 @@ interface MessagesNavigator {
|
|||
fun onReportContentClick(eventId: EventId, senderId: UserId)
|
||||
fun onEditPollClick(eventId: EventId)
|
||||
fun onPreviewAttachment(attachments: ImmutableList<Attachment>, inReplyToEventId: EventId?)
|
||||
fun onNavigateToRoom(roomId: RoomId, serverNames: List<String>)
|
||||
fun onNavigateToRoom(roomId: RoomId, eventId: EventId?, serverNames: List<String>)
|
||||
fun onOpenThread(threadRootId: ThreadId, focusedEventId: EventId?)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -92,6 +92,14 @@ class MessagesNode(
|
|||
private val knockRequestsBannerRenderer: KnockRequestsBannerRenderer,
|
||||
private val roomMemberModerationRenderer: RoomMemberModerationRenderer,
|
||||
) : Node(buildContext, plugins = plugins), MessagesNavigator {
|
||||
private val callbacks = plugins<Callback>()
|
||||
|
||||
data class Inputs(
|
||||
val focusedEventId: EventId?,
|
||||
) : NodeInputs
|
||||
|
||||
private val inputs = inputs<Inputs>()
|
||||
|
||||
private val timelineController = TimelineController(room, room.liveTimeline)
|
||||
private val presenter = presenterFactory.create(
|
||||
navigator = this,
|
||||
|
|
@ -99,18 +107,12 @@ class MessagesNode(
|
|||
timelinePresenter = timelinePresenterFactory.create(timelineController = timelineController, this),
|
||||
actionListPresenter = actionListPresenterFactory.create(
|
||||
postProcessor = TimelineItemActionPostProcessor.Default,
|
||||
timelineMode = timelineController.mainTimelineMode()
|
||||
timelineMode = timelineController.mainTimelineMode(),
|
||||
),
|
||||
timelineController = timelineController,
|
||||
)
|
||||
private val callbacks = plugins<Callback>()
|
||||
|
||||
data class Inputs(val focusedEventId: EventId?) : NodeInputs
|
||||
|
||||
private val inputs = inputs<Inputs>()
|
||||
|
||||
interface Callback : Plugin {
|
||||
fun onRoomDetailsClick()
|
||||
fun onEventClick(timelineMode: Timeline.Mode, event: TimelineItem.Event): Boolean
|
||||
fun onPreviewAttachments(attachments: ImmutableList<Attachment>, inReplyToEventId: EventId?)
|
||||
fun onUserDataClick(userId: UserId)
|
||||
|
|
@ -122,9 +124,10 @@ class MessagesNode(
|
|||
fun onCreatePollClick()
|
||||
fun onEditPollClick(eventId: EventId)
|
||||
fun onJoinCallClick(roomId: RoomId)
|
||||
fun onOpenThread(threadRootId: ThreadId, focusedEventId: EventId?)
|
||||
fun onRoomDetailsClick()
|
||||
fun onViewAllPinnedEvents()
|
||||
fun onViewKnockRequests()
|
||||
fun onOpenThread(threadRootId: ThreadId, focusedEventId: EventId?)
|
||||
}
|
||||
|
||||
override fun onBuilt() {
|
||||
|
|
@ -143,6 +146,14 @@ class MessagesNode(
|
|||
callbacks.forEach { it.onRoomDetailsClick() }
|
||||
}
|
||||
|
||||
private fun onViewAllPinnedMessagesClick() {
|
||||
callbacks.forEach { it.onViewAllPinnedEvents() }
|
||||
}
|
||||
|
||||
private fun onViewKnockRequestsClick() {
|
||||
callbacks.forEach { it.onViewKnockRequests() }
|
||||
}
|
||||
|
||||
private fun onEventClick(timelineMode: Timeline.Mode, event: TimelineItem.Event): Boolean {
|
||||
// Note: cannot use `callbacks.all { it.onEventClick(event) }` because:
|
||||
// - if callbacks is empty, it will return true and we want to return false.
|
||||
|
|
@ -223,11 +234,11 @@ class MessagesNode(
|
|||
callbacks.forEach { it.onPreviewAttachments(attachments, inReplyToEventId) }
|
||||
}
|
||||
|
||||
override fun onNavigateToRoom(roomId: RoomId, serverNames: List<String>) {
|
||||
override fun onNavigateToRoom(roomId: RoomId, eventId: EventId?, serverNames: List<String>) {
|
||||
if (roomId == room.roomId) {
|
||||
displaySameRoomToast()
|
||||
} else {
|
||||
val permalinkData = PermalinkData.RoomLink(roomId.toRoomIdOrAlias(), viaParameters = serverNames.toImmutableList())
|
||||
val permalinkData = PermalinkData.RoomLink(roomId.toRoomIdOrAlias(), eventId, viaParameters = serverNames.toImmutableList())
|
||||
callbacks.forEach { it.onPermalinkClick(permalinkData) }
|
||||
}
|
||||
}
|
||||
|
|
@ -236,10 +247,6 @@ class MessagesNode(
|
|||
callbacks.forEach { it.onOpenThread(threadRootId, focusedEventId) }
|
||||
}
|
||||
|
||||
private fun onViewAllPinnedMessagesClick() {
|
||||
callbacks.forEach { it.onViewAllPinnedEvents() }
|
||||
}
|
||||
|
||||
private fun onSendLocationClick() {
|
||||
callbacks.forEach { it.onSendLocationClick() }
|
||||
}
|
||||
|
|
@ -252,10 +259,6 @@ class MessagesNode(
|
|||
callbacks.forEach { it.onJoinCallClick(room.roomId) }
|
||||
}
|
||||
|
||||
private fun onViewKnockRequestsClick() {
|
||||
callbacks.forEach { it.onViewKnockRequests() }
|
||||
}
|
||||
|
||||
private fun displaySameRoomToast() {
|
||||
context.toast(CommonStrings.screen_room_permalink_same_room_android)
|
||||
}
|
||||
|
|
@ -291,7 +294,15 @@ class MessagesNode(
|
|||
}
|
||||
},
|
||||
onUserDataClick = this::onUserDataClick,
|
||||
onLinkClick = { url, customTab -> onLinkClick(activity, isDark, url, state.timelineState.eventSink, customTab) },
|
||||
onLinkClick = { url, customTab ->
|
||||
onLinkClick(
|
||||
activity = activity,
|
||||
darkTheme = isDark,
|
||||
url = url,
|
||||
eventSink = state.timelineState.eventSink,
|
||||
customTab = customTab,
|
||||
)
|
||||
},
|
||||
onSendLocationClick = this::onSendLocationClick,
|
||||
onCreatePollClick = this::onCreatePollClick,
|
||||
onJoinCallClick = this::onJoinCallClick,
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@
|
|||
package io.element.android.features.messages.impl.threads
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import androidx.activity.compose.LocalActivity
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
|
|
@ -44,19 +43,18 @@ import io.element.android.features.messages.impl.timeline.di.TimelineItemPresent
|
|||
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
||||
import io.element.android.libraries.androidutils.browser.openUrlInChromeCustomTab
|
||||
import io.element.android.libraries.androidutils.system.openUrlInExternalApp
|
||||
import io.element.android.libraries.androidutils.system.toast
|
||||
import io.element.android.libraries.architecture.NodeInputs
|
||||
import io.element.android.libraries.architecture.inputs
|
||||
import io.element.android.libraries.core.bool.orFalse
|
||||
import io.element.android.libraries.designsystem.utils.OnLifecycleEvent
|
||||
import io.element.android.libraries.di.RoomScope
|
||||
import io.element.android.libraries.di.annotations.ApplicationContext
|
||||
import io.element.android.libraries.di.annotations.SessionCoroutineScope
|
||||
import io.element.android.libraries.matrix.api.analytics.toAnalyticsViewRoom
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
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.toRoomIdOrAlias
|
||||
import io.element.android.libraries.matrix.api.permalink.PermalinkData
|
||||
import io.element.android.libraries.matrix.api.permalink.PermalinkParser
|
||||
import io.element.android.libraries.matrix.api.room.CreateTimelineParams
|
||||
|
|
@ -65,9 +63,9 @@ import io.element.android.libraries.matrix.api.room.alias.matches
|
|||
import io.element.android.libraries.matrix.api.timeline.Timeline
|
||||
import io.element.android.libraries.matrix.api.timeline.item.TimelineItemDebugInfo
|
||||
import io.element.android.libraries.mediaplayer.api.MediaPlayer
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
import io.element.android.services.analytics.api.AnalyticsService
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
|
|
@ -77,7 +75,6 @@ import kotlinx.coroutines.runBlocking
|
|||
class ThreadedMessagesNode(
|
||||
@Assisted buildContext: BuildContext,
|
||||
@Assisted plugins: List<Plugin>,
|
||||
@ApplicationContext private val context: Context,
|
||||
@SessionCoroutineScope private val sessionCoroutineScope: CoroutineScope,
|
||||
private val room: JoinedRoom,
|
||||
private val analyticsService: AnalyticsService,
|
||||
|
|
@ -125,6 +122,7 @@ class ThreadedMessagesNode(
|
|||
fun onCreatePollClick()
|
||||
fun onEditPollClick(eventId: EventId)
|
||||
fun onJoinCallClick(roomId: RoomId)
|
||||
fun onOpenThread(threadRootId: ThreadId, focusedEventId: EventId?)
|
||||
}
|
||||
|
||||
override fun onBuilt() {
|
||||
|
|
@ -191,8 +189,11 @@ class ThreadedMessagesNode(
|
|||
if (eventId != null) {
|
||||
eventSink(TimelineEvents.FocusOnEvent(eventId))
|
||||
} else {
|
||||
// Click on the same room, ignore
|
||||
displaySameRoomToast()
|
||||
// Click on the same room, navigate up
|
||||
// Note that it can not be enough to go back to the room if the thread has been opened
|
||||
// following a permalink from another thread. In this case navigating up will go back
|
||||
// to the previous thread. But this should not happen often.
|
||||
navigateUp()
|
||||
}
|
||||
} else {
|
||||
callbacks.forEach { it.onPermalinkClick(roomLink) }
|
||||
|
|
@ -219,7 +220,14 @@ class ThreadedMessagesNode(
|
|||
callbacks.forEach { it.onPreviewAttachments(attachments, inReplyToEventId) }
|
||||
}
|
||||
|
||||
override fun onNavigateToRoom(roomId: RoomId, serverNames: List<String>) = Unit
|
||||
override fun onNavigateToRoom(roomId: RoomId, eventId: EventId?, serverNames: List<String>) {
|
||||
val permalinkData = PermalinkData.RoomLink(roomId.toRoomIdOrAlias(), eventId, viaParameters = serverNames.toImmutableList())
|
||||
callbacks.forEach { it.onPermalinkClick(permalinkData) }
|
||||
}
|
||||
|
||||
override fun onOpenThread(threadRootId: ThreadId, focusedEventId: EventId?) {
|
||||
callbacks.forEach { it.onOpenThread(threadRootId, focusedEventId) }
|
||||
}
|
||||
|
||||
private fun onSendLocationClick() {
|
||||
callbacks.forEach { it.onSendLocationClick() }
|
||||
|
|
@ -233,13 +241,6 @@ class ThreadedMessagesNode(
|
|||
callbacks.forEach { it.onJoinCallClick(room.roomId) }
|
||||
}
|
||||
|
||||
private fun displaySameRoomToast() {
|
||||
context.toast(CommonStrings.screen_room_permalink_same_room_android)
|
||||
}
|
||||
|
||||
override fun onOpenThread(threadRootId: ThreadId, focusedEventId: EventId?) {
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun View(modifier: Modifier) {
|
||||
val activity = requireNotNull(LocalActivity.current)
|
||||
|
|
@ -273,11 +274,11 @@ class ThreadedMessagesNode(
|
|||
onUserDataClick = this::onUserDataClick,
|
||||
onLinkClick = { url, customTab ->
|
||||
onLinkClick(
|
||||
activity,
|
||||
isDark,
|
||||
url,
|
||||
state.timelineState.eventSink,
|
||||
customTab
|
||||
activity = activity,
|
||||
darkTheme = isDark,
|
||||
url = url,
|
||||
eventSink = state.timelineState.eventSink,
|
||||
customTab = customTab,
|
||||
)
|
||||
},
|
||||
onSendLocationClick = this::onSendLocationClick,
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import dev.zacsweers.metro.binding
|
|||
import io.element.android.features.messages.impl.timeline.di.LiveTimeline
|
||||
import io.element.android.libraries.di.RoomScope
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
import io.element.android.libraries.matrix.api.core.ThreadId
|
||||
import io.element.android.libraries.matrix.api.room.CreateTimelineParams
|
||||
import io.element.android.libraries.matrix.api.room.JoinedRoom
|
||||
import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
|
||||
|
|
@ -74,21 +75,26 @@ class TimelineController(
|
|||
}
|
||||
}
|
||||
|
||||
suspend fun focusOnEvent(eventId: EventId): Result<Unit> {
|
||||
return room.createTimeline(CreateTimelineParams.Focused(eventId))
|
||||
.onFailure {
|
||||
if (it is CancellationException) {
|
||||
throw it
|
||||
}
|
||||
}
|
||||
.map { newDetachedTimeline ->
|
||||
detachedTimelineFlow.getAndUpdate { current ->
|
||||
if (current.isPresent) {
|
||||
current.get().close()
|
||||
suspend fun focusOnEvent(eventId: EventId, threadRootId: ThreadId?): Result<EventFocusResult> {
|
||||
return if (threadRootId != null) {
|
||||
Result.success(EventFocusResult.IsInThread(threadRootId))
|
||||
} else {
|
||||
room.createTimeline(CreateTimelineParams.Focused(eventId))
|
||||
.onFailure {
|
||||
if (it is CancellationException) {
|
||||
throw it
|
||||
}
|
||||
Optional.of(newDetachedTimeline)
|
||||
}
|
||||
}
|
||||
.map { newDetachedTimeline ->
|
||||
detachedTimelineFlow.getAndUpdate { current ->
|
||||
if (current.isPresent) {
|
||||
current.get().close()
|
||||
}
|
||||
Optional.of(newDetachedTimeline)
|
||||
}
|
||||
EventFocusResult.FocusedOnLive
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -136,3 +142,8 @@ class TimelineController(
|
|||
return currentTimelineFlow
|
||||
}
|
||||
}
|
||||
|
||||
sealed interface EventFocusResult {
|
||||
data object FocusedOnLive : EventFocusResult
|
||||
data class IsInThread(val threadId: ThreadId) : EventFocusResult
|
||||
}
|
||||
|
|
|
|||
|
|
@ -44,6 +44,7 @@ import io.element.android.libraries.featureflag.api.FeatureFlagService
|
|||
import io.element.android.libraries.featureflag.api.FeatureFlags
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
import io.element.android.libraries.matrix.api.core.UniqueId
|
||||
import io.element.android.libraries.matrix.api.core.asEventId
|
||||
import io.element.android.libraries.matrix.api.room.JoinedRoom
|
||||
import io.element.android.libraries.matrix.api.room.MessageEventType
|
||||
import io.element.android.libraries.matrix.api.room.isDm
|
||||
|
|
@ -207,7 +208,7 @@ class TimelinePresenter(
|
|||
is TimelineEvents.NavigateToPredecessorOrSuccessorRoom -> {
|
||||
// Navigate to the predecessor or successor room
|
||||
val serverNames = calculateServerNamesForRoom(room)
|
||||
navigator.onNavigateToRoom(event.roomId, serverNames)
|
||||
navigator.onNavigateToRoom(event.roomId, null, serverNames)
|
||||
}
|
||||
is TimelineEvents.OpenThread -> {
|
||||
navigator.onOpenThread(
|
||||
|
|
@ -257,13 +258,39 @@ class TimelinePresenter(
|
|||
}
|
||||
is FocusRequestState.Loading -> {
|
||||
val eventId = currentFocusRequestState.eventId
|
||||
timelineController.focusOnEvent(eventId)
|
||||
.onSuccess {
|
||||
focusRequestState = FocusRequestState.Success(eventId = eventId)
|
||||
}
|
||||
.onFailure {
|
||||
focusRequestState = FocusRequestState.Failure(it)
|
||||
}
|
||||
val threadId = room.threadRootIdForEvent(eventId).getOrElse {
|
||||
focusRequestState = FocusRequestState.Failure(it)
|
||||
return@LaunchedEffect
|
||||
}
|
||||
|
||||
if (timelineController.mainTimelineMode() is Timeline.Mode.Thread && threadId == null) {
|
||||
// We are in a thread timeline, and the event isn't part of a thread, we need to navigate back to the room
|
||||
focusRequestState = FocusRequestState.None
|
||||
navigator.onNavigateToRoom(room.roomId, eventId, calculateServerNamesForRoom(room))
|
||||
} else {
|
||||
timelineController.focusOnEvent(eventId, threadId)
|
||||
.onSuccess { result ->
|
||||
when (result) {
|
||||
is EventFocusResult.FocusedOnLive -> {
|
||||
focusRequestState = FocusRequestState.Success(eventId = eventId)
|
||||
}
|
||||
is EventFocusResult.IsInThread -> {
|
||||
val currentThreadId = (timelineController.mainTimelineMode() as? Timeline.Mode.Thread)?.threadRootId
|
||||
if (currentThreadId == result.threadId) {
|
||||
// It's the same thread, we just focus on the event
|
||||
focusRequestState = FocusRequestState.Success(eventId = eventId)
|
||||
} else {
|
||||
focusRequestState = FocusRequestState.Success(eventId = result.threadId.asEventId())
|
||||
// It's part of a thread we're not in, let's open it in another timeline
|
||||
navigator.onOpenThread(result.threadId, eventId)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.onFailure {
|
||||
focusRequestState = FocusRequestState.Failure(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
else -> Unit
|
||||
}
|
||||
|
|
@ -341,7 +368,7 @@ class TimelinePresenter(
|
|||
newMostRecentItemId != prevMostRecentItemIdValue
|
||||
|
||||
if (hasNewEvent) {
|
||||
val newMostRecentEvent = newMostRecentItem as? TimelineItem.Event
|
||||
val newMostRecentEvent = newMostRecentItem
|
||||
// Scroll to bottom if the new event is from me, even if sent from another device
|
||||
val fromMe = newMostRecentEvent?.isMine == true
|
||||
newEventState.value = if (fromMe) {
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ class FakeMessagesNavigator(
|
|||
private val onReportContentClickLambda: (eventId: EventId, senderId: UserId) -> Unit = { _, _ -> lambdaError() },
|
||||
private val onEditPollClickLambda: (eventId: EventId) -> Unit = { _ -> lambdaError() },
|
||||
private val onPreviewAttachmentLambda: (attachments: ImmutableList<Attachment>, inReplyToEventId: EventId?) -> Unit = { _, _ -> lambdaError() },
|
||||
private val onNavigateToRoomLambda: (roomId: RoomId, serverNames: List<String>) -> Unit = { _, _ -> lambdaError() },
|
||||
private val onNavigateToRoomLambda: (roomId: RoomId, threadId: EventId?, serverNames: List<String>) -> Unit = { _, _, _ -> lambdaError() },
|
||||
private val onOpenThreadLambda: (threadRootId: ThreadId, focusedEventId: EventId?) -> Unit = { _, _ -> lambdaError() },
|
||||
) : MessagesNavigator {
|
||||
override fun onShowEventDebugInfoClick(eventId: EventId?, debugInfo: TimelineItemDebugInfo) {
|
||||
|
|
@ -45,8 +45,8 @@ class FakeMessagesNavigator(
|
|||
onPreviewAttachmentLambda(attachments, inReplyToEventId)
|
||||
}
|
||||
|
||||
override fun onNavigateToRoom(roomId: RoomId, serverNames: List<String>) {
|
||||
onNavigateToRoomLambda(roomId, serverNames)
|
||||
override fun onNavigateToRoom(roomId: RoomId, eventId: EventId?, serverNames: List<String>) {
|
||||
onNavigateToRoomLambda(roomId, eventId, serverNames)
|
||||
}
|
||||
|
||||
override fun onOpenThread(threadRootId: ThreadId, focusedEventId: EventId?) {
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ class TimelineControllerTest {
|
|||
assertThat(state).isEqualTo(liveTimeline)
|
||||
}
|
||||
assertThat(sut.isLive().first()).isTrue()
|
||||
sut.focusOnEvent(AN_EVENT_ID)
|
||||
sut.focusOnEvent(AN_EVENT_ID, null)
|
||||
awaitItem().also { state ->
|
||||
assertThat(state).isEqualTo(detachedTimeline)
|
||||
}
|
||||
|
|
@ -78,14 +78,14 @@ class TimelineControllerTest {
|
|||
awaitItem().also { state ->
|
||||
assertThat(state).isEqualTo(liveTimeline)
|
||||
}
|
||||
sut.focusOnEvent(AN_EVENT_ID)
|
||||
sut.focusOnEvent(AN_EVENT_ID, null)
|
||||
awaitItem().also { state ->
|
||||
assertThat(state).isEqualTo(detachedTimeline1)
|
||||
}
|
||||
assertThat(detachedTimeline1.closeCounter).isEqualTo(0)
|
||||
assertThat(detachedTimeline2.closeCounter).isEqualTo(0)
|
||||
// Focus on another event should close the previous detached timeline
|
||||
sut.focusOnEvent(AN_EVENT_ID)
|
||||
sut.focusOnEvent(AN_EVENT_ID, null)
|
||||
awaitItem().also { state ->
|
||||
assertThat(state).isEqualTo(detachedTimeline2)
|
||||
}
|
||||
|
|
@ -124,7 +124,7 @@ class TimelineControllerTest {
|
|||
awaitItem().also { state ->
|
||||
assertThat(state).isEqualTo(liveTimeline)
|
||||
}
|
||||
sut.focusOnEvent(AN_EVENT_ID)
|
||||
sut.focusOnEvent(AN_EVENT_ID, null)
|
||||
awaitItem().also { state ->
|
||||
assertThat(state).isEqualTo(detachedTimeline)
|
||||
}
|
||||
|
|
@ -171,11 +171,11 @@ class TimelineControllerTest {
|
|||
)
|
||||
val sut = TimelineController(room = joinedRoom, liveTimeline = liveTimeline)
|
||||
sut.activeTimelineFlow().test {
|
||||
sut.focusOnEvent(AN_EVENT_ID)
|
||||
sut.focusOnEvent(AN_EVENT_ID, null)
|
||||
awaitItem().also { state ->
|
||||
assertThat(state).isEqualTo(liveTimeline)
|
||||
}
|
||||
sut.focusOnEvent(AN_EVENT_ID)
|
||||
sut.focusOnEvent(AN_EVENT_ID, null)
|
||||
awaitItem().also { state ->
|
||||
assertThat(state).isEqualTo(detachedTimeline)
|
||||
}
|
||||
|
|
@ -200,7 +200,7 @@ class TimelineControllerTest {
|
|||
awaitItem().also { state ->
|
||||
assertThat(state).isEqualTo(liveTimeline)
|
||||
}
|
||||
sut.focusOnEvent(AN_EVENT_ID)
|
||||
sut.focusOnEvent(AN_EVENT_ID, null)
|
||||
awaitItem().also { state ->
|
||||
assertThat(state).isEqualTo(detachedTimeline)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,7 +31,9 @@ import io.element.android.features.roomcall.api.aStandByCallState
|
|||
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.core.ThreadId
|
||||
import io.element.android.libraries.matrix.api.core.UniqueId
|
||||
import io.element.android.libraries.matrix.api.core.asEventId
|
||||
import io.element.android.libraries.matrix.api.room.RoomMembersState
|
||||
import io.element.android.libraries.matrix.api.room.tombstone.PredecessorRoom
|
||||
import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
|
||||
|
|
@ -44,6 +46,8 @@ import io.element.android.libraries.matrix.api.timeline.item.virtual.VirtualTime
|
|||
import io.element.android.libraries.matrix.test.AN_EVENT_ID
|
||||
import io.element.android.libraries.matrix.test.AN_EVENT_ID_2
|
||||
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
||||
import io.element.android.libraries.matrix.test.A_THREAD_ID
|
||||
import io.element.android.libraries.matrix.test.A_THREAD_ID_2
|
||||
import io.element.android.libraries.matrix.test.A_UNIQUE_ID
|
||||
import io.element.android.libraries.matrix.test.A_UNIQUE_ID_2
|
||||
import io.element.android.libraries.matrix.test.A_USER_ID
|
||||
|
|
@ -535,7 +539,10 @@ class TimelinePresenterTest {
|
|||
val room = FakeJoinedRoom(
|
||||
liveTimeline = liveTimeline,
|
||||
createTimelineResult = { Result.success(detachedTimeline) },
|
||||
baseRoom = FakeBaseRoom(canUserSendMessageResult = { _, _ -> Result.success(true) }),
|
||||
baseRoom = FakeBaseRoom(
|
||||
canUserSendMessageResult = { _, _ -> Result.success(true) },
|
||||
threadRootIdForEventResult = { _ -> Result.success(null) },
|
||||
),
|
||||
)
|
||||
val presenter = createTimelinePresenter(
|
||||
room = room,
|
||||
|
|
@ -613,7 +620,10 @@ class TimelinePresenterTest {
|
|||
timelineItems = flowOf(emptyList()),
|
||||
),
|
||||
createTimelineResult = { Result.failure(RuntimeException("An error")) },
|
||||
baseRoom = FakeBaseRoom(canUserSendMessageResult = { _, _ -> Result.success(true) }),
|
||||
baseRoom = FakeBaseRoom(
|
||||
canUserSendMessageResult = { _, _ -> Result.success(true) },
|
||||
threadRootIdForEventResult = { _ -> Result.success(null) },
|
||||
),
|
||||
)
|
||||
)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
|
|
@ -639,6 +649,246 @@ class TimelinePresenterTest {
|
|||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - focus on event in a thread opens the thread`() = runTest {
|
||||
val threadId = A_THREAD_ID
|
||||
val detachedTimeline = FakeTimeline(
|
||||
mode = Timeline.Mode.FocusedOnEvent(AN_EVENT_ID_2),
|
||||
timelineItems = flowOf(
|
||||
listOf(
|
||||
MatrixTimelineItem.Event(
|
||||
uniqueId = A_UNIQUE_ID,
|
||||
event = anEventTimelineItem(),
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
val liveTimeline = FakeTimeline(
|
||||
timelineItems = flowOf(emptyList())
|
||||
)
|
||||
val room = FakeJoinedRoom(
|
||||
liveTimeline = liveTimeline,
|
||||
createTimelineResult = { Result.success(detachedTimeline) },
|
||||
baseRoom = FakeBaseRoom(
|
||||
canUserSendMessageResult = { _, _ -> Result.success(true) },
|
||||
threadRootIdForEventResult = { _ -> Result.success(threadId) },
|
||||
),
|
||||
)
|
||||
val openThreadLambda = lambdaRecorder { _: ThreadId, _: EventId? -> }
|
||||
val navigator = FakeMessagesNavigator(onOpenThreadLambda = openThreadLambda)
|
||||
val presenter = createTimelinePresenter(
|
||||
room = room,
|
||||
timeline = liveTimeline,
|
||||
messagesNavigator = navigator,
|
||||
)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val initialState = awaitFirstItem()
|
||||
initialState.eventSink.invoke(TimelineEvents.FocusOnEvent(AN_EVENT_ID))
|
||||
|
||||
awaitItem().also { state ->
|
||||
assertThat(state.focusedEventId).isEqualTo(AN_EVENT_ID)
|
||||
assertThat(state.focusRequestState).isEqualTo(FocusRequestState.Requested(AN_EVENT_ID, Duration.ZERO))
|
||||
}
|
||||
|
||||
advanceUntilIdle()
|
||||
|
||||
assertThat(awaitItem().focusRequestState).isEqualTo(FocusRequestState.Loading(AN_EVENT_ID))
|
||||
|
||||
// The live timeline focuses in the thread root
|
||||
assertThat(awaitItem().focusRequestState).isEqualTo(FocusRequestState.Success(A_THREAD_ID.asEventId()))
|
||||
|
||||
// The thread is opened
|
||||
openThreadLambda.assertions()
|
||||
.isCalledOnce()
|
||||
.with(
|
||||
value(threadId),
|
||||
value(AN_EVENT_ID),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - focus on event in a thread when in the same thread just moves the focus`() = runTest {
|
||||
val threadId = A_THREAD_ID
|
||||
val detachedTimeline = FakeTimeline(
|
||||
mode = Timeline.Mode.FocusedOnEvent(AN_EVENT_ID_2),
|
||||
timelineItems = flowOf(
|
||||
listOf(
|
||||
MatrixTimelineItem.Event(
|
||||
uniqueId = A_UNIQUE_ID,
|
||||
event = anEventTimelineItem(),
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
val liveTimeline = FakeTimeline(
|
||||
mode = Timeline.Mode.Thread(threadId),
|
||||
timelineItems = flowOf(emptyList())
|
||||
)
|
||||
val room = FakeJoinedRoom(
|
||||
liveTimeline = liveTimeline,
|
||||
createTimelineResult = { Result.success(detachedTimeline) },
|
||||
baseRoom = FakeBaseRoom(
|
||||
canUserSendMessageResult = { _, _ -> Result.success(true) },
|
||||
threadRootIdForEventResult = { _ -> Result.success(threadId) },
|
||||
),
|
||||
)
|
||||
val openThreadLambda = lambdaRecorder { _: ThreadId, _: EventId? -> }
|
||||
val navigator = FakeMessagesNavigator(onOpenThreadLambda = openThreadLambda)
|
||||
val presenter = createTimelinePresenter(
|
||||
room = room,
|
||||
timeline = liveTimeline,
|
||||
messagesNavigator = navigator,
|
||||
)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val initialState = awaitFirstItem()
|
||||
initialState.eventSink.invoke(TimelineEvents.FocusOnEvent(AN_EVENT_ID))
|
||||
|
||||
awaitItem().also { state ->
|
||||
assertThat(state.focusedEventId).isEqualTo(AN_EVENT_ID)
|
||||
assertThat(state.focusRequestState).isEqualTo(FocusRequestState.Requested(AN_EVENT_ID, Duration.ZERO))
|
||||
}
|
||||
|
||||
advanceUntilIdle()
|
||||
|
||||
assertThat(awaitItem().focusRequestState).isEqualTo(FocusRequestState.Loading(AN_EVENT_ID))
|
||||
|
||||
// The live timeline focuses in the event directly since we are already in the thread
|
||||
assertThat(awaitItem().focusRequestState).isEqualTo(FocusRequestState.Success(AN_EVENT_ID))
|
||||
|
||||
// The thread is not opened again
|
||||
openThreadLambda.assertions().isNeverCalled()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - focus on event in a thread when in a different thread opens the new thread`() = runTest {
|
||||
val currentThreadId = A_THREAD_ID
|
||||
val detachedTimeline = FakeTimeline(
|
||||
mode = Timeline.Mode.FocusedOnEvent(AN_EVENT_ID_2),
|
||||
timelineItems = flowOf(
|
||||
listOf(
|
||||
MatrixTimelineItem.Event(
|
||||
uniqueId = A_UNIQUE_ID,
|
||||
event = anEventTimelineItem(),
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
val liveTimeline = FakeTimeline(
|
||||
mode = Timeline.Mode.Thread(currentThreadId),
|
||||
timelineItems = flowOf(emptyList())
|
||||
)
|
||||
val room = FakeJoinedRoom(
|
||||
liveTimeline = liveTimeline,
|
||||
createTimelineResult = { Result.success(detachedTimeline) },
|
||||
baseRoom = FakeBaseRoom(
|
||||
canUserSendMessageResult = { _, _ -> Result.success(true) },
|
||||
// Use a different thread id
|
||||
threadRootIdForEventResult = { _ -> Result.success(A_THREAD_ID_2) },
|
||||
),
|
||||
)
|
||||
val openThreadLambda = lambdaRecorder { _: ThreadId, _: EventId? -> }
|
||||
val navigator = FakeMessagesNavigator(onOpenThreadLambda = openThreadLambda)
|
||||
val presenter = createTimelinePresenter(
|
||||
room = room,
|
||||
timeline = liveTimeline,
|
||||
messagesNavigator = navigator,
|
||||
)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val initialState = awaitFirstItem()
|
||||
initialState.eventSink.invoke(TimelineEvents.FocusOnEvent(AN_EVENT_ID))
|
||||
|
||||
awaitItem().also { state ->
|
||||
assertThat(state.focusedEventId).isEqualTo(AN_EVENT_ID)
|
||||
assertThat(state.focusRequestState).isEqualTo(FocusRequestState.Requested(AN_EVENT_ID, Duration.ZERO))
|
||||
}
|
||||
|
||||
advanceUntilIdle()
|
||||
|
||||
assertThat(awaitItem().focusRequestState).isEqualTo(FocusRequestState.Loading(AN_EVENT_ID))
|
||||
|
||||
// The live timeline focuses in the event directly since we are already in the thread
|
||||
assertThat(awaitItem().focusRequestState).isEqualTo(FocusRequestState.Success(A_THREAD_ID_2.asEventId()))
|
||||
|
||||
// The other thread is opened
|
||||
openThreadLambda.assertions()
|
||||
.isCalledOnce()
|
||||
.with(
|
||||
value(A_THREAD_ID_2),
|
||||
value(AN_EVENT_ID),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - focus on event in a the room while in a thread of that room opens the room`() = runTest {
|
||||
val detachedTimeline = FakeTimeline(
|
||||
mode = Timeline.Mode.FocusedOnEvent(AN_EVENT_ID_2),
|
||||
timelineItems = flowOf(
|
||||
listOf(
|
||||
MatrixTimelineItem.Event(
|
||||
uniqueId = A_UNIQUE_ID,
|
||||
event = anEventTimelineItem(),
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
val liveTimeline = FakeTimeline(
|
||||
mode = Timeline.Mode.Thread(A_THREAD_ID),
|
||||
timelineItems = flowOf(emptyList())
|
||||
)
|
||||
val room = FakeJoinedRoom(
|
||||
liveTimeline = liveTimeline,
|
||||
createTimelineResult = { Result.success(detachedTimeline) },
|
||||
baseRoom = FakeBaseRoom(
|
||||
canUserSendMessageResult = { _, _ -> Result.success(true) },
|
||||
// The event is in the main timeline, not in a thread
|
||||
threadRootIdForEventResult = { _ -> Result.success(null) },
|
||||
),
|
||||
)
|
||||
val openRoomLambda = lambdaRecorder { _: RoomId, _: EventId?, _: List<String> -> }
|
||||
val navigator = FakeMessagesNavigator(onNavigateToRoomLambda = openRoomLambda)
|
||||
val presenter = createTimelinePresenter(
|
||||
room = room,
|
||||
timeline = liveTimeline,
|
||||
messagesNavigator = navigator,
|
||||
)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val initialState = awaitFirstItem()
|
||||
initialState.eventSink.invoke(TimelineEvents.FocusOnEvent(AN_EVENT_ID))
|
||||
|
||||
awaitItem().also { state ->
|
||||
assertThat(state.focusedEventId).isEqualTo(AN_EVENT_ID)
|
||||
assertThat(state.focusRequestState).isEqualTo(FocusRequestState.Requested(AN_EVENT_ID, Duration.ZERO))
|
||||
}
|
||||
|
||||
advanceUntilIdle()
|
||||
|
||||
assertThat(awaitItem().focusRequestState).isEqualTo(FocusRequestState.Loading(AN_EVENT_ID))
|
||||
|
||||
// The focus state will reset
|
||||
assertThat(awaitItem().focusRequestState).isEqualTo(FocusRequestState.None)
|
||||
|
||||
// The room is opened again
|
||||
openRoomLambda.assertions()
|
||||
.isCalledOnce()
|
||||
.with(
|
||||
value(room.roomId),
|
||||
value(AN_EVENT_ID),
|
||||
value(emptyList<String>())
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - show shield hide shield`() = runTest {
|
||||
val presenter = createTimelinePresenter()
|
||||
|
|
@ -754,7 +1004,7 @@ class TimelinePresenterTest {
|
|||
canUserSendMessageResult = { _, _ -> Result.success(true) },
|
||||
),
|
||||
)
|
||||
val onNavigateToRoomLambda = lambdaRecorder<RoomId, List<String>, Unit> { _, _ -> }
|
||||
val onNavigateToRoomLambda = lambdaRecorder<RoomId, EventId?, List<String>, Unit> { _, _, _ -> }
|
||||
val navigator = FakeMessagesNavigator(
|
||||
onNavigateToRoomLambda = onNavigateToRoomLambda
|
||||
)
|
||||
|
|
@ -766,6 +1016,8 @@ class TimelinePresenterTest {
|
|||
.isCalledOnce()
|
||||
.with(
|
||||
value(A_ROOM_ID),
|
||||
// No event id when navigating to a successor/predecessor room
|
||||
value(null),
|
||||
value(emptyList<String>())
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,30 +13,36 @@ import android.os.Parcelable
|
|||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import com.bumble.appyx.core.lifecycle.subscribe
|
||||
import com.bumble.appyx.core.modality.BuildContext
|
||||
import com.bumble.appyx.core.node.Node
|
||||
import com.bumble.appyx.core.plugin.Plugin
|
||||
import com.bumble.appyx.navmodel.backstack.BackStack
|
||||
import com.bumble.appyx.navmodel.backstack.operation.push
|
||||
import dev.zacsweers.metro.Assisted
|
||||
import dev.zacsweers.metro.Inject
|
||||
import dev.zacsweers.metro.AssistedInject
|
||||
import io.element.android.annotations.ContributesNode
|
||||
import io.element.android.features.space.api.SpaceEntryPoint
|
||||
import io.element.android.features.space.impl.di.SpaceFlowGraph
|
||||
import io.element.android.features.space.impl.leave.LeaveSpaceNode
|
||||
import io.element.android.features.space.impl.root.SpaceNode
|
||||
import io.element.android.libraries.architecture.BackstackView
|
||||
import io.element.android.libraries.architecture.BaseFlowNode
|
||||
import io.element.android.libraries.architecture.createNode
|
||||
import io.element.android.libraries.architecture.inputs
|
||||
import io.element.android.libraries.di.DependencyInjectionGraphOwner
|
||||
import io.element.android.libraries.di.SessionScope
|
||||
import io.element.android.libraries.matrix.api.MatrixClient
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@ContributesNode(SessionScope::class)
|
||||
@Inject
|
||||
@AssistedInject
|
||||
class SpaceFlowNode(
|
||||
@Assisted val buildContext: BuildContext,
|
||||
@Assisted plugins: List<Plugin>,
|
||||
matrixClient: MatrixClient,
|
||||
graphFactory: SpaceFlowGraph.Factory,
|
||||
) : BaseFlowNode<SpaceFlowNode.NavTarget>(
|
||||
backstack = BackStack(
|
||||
initialElement = NavTarget.Root,
|
||||
|
|
@ -44,9 +50,11 @@ class SpaceFlowNode(
|
|||
),
|
||||
buildContext = buildContext,
|
||||
plugins = plugins,
|
||||
) {
|
||||
), DependencyInjectionGraphOwner {
|
||||
private val inputs: SpaceEntryPoint.Inputs = inputs()
|
||||
private val callback = plugins.filterIsInstance<SpaceEntryPoint.Callback>().single()
|
||||
private val spaceRoomList = matrixClient.spaceService.spaceRoomList(inputs.roomId)
|
||||
override val graph = graphFactory.create(spaceRoomList)
|
||||
|
||||
sealed interface NavTarget : Parcelable {
|
||||
@Parcelize
|
||||
|
|
@ -56,6 +64,15 @@ class SpaceFlowNode(
|
|||
data object Leave : NavTarget
|
||||
}
|
||||
|
||||
override fun onBuilt() {
|
||||
super.onBuilt()
|
||||
lifecycle.subscribe(
|
||||
onDestroy = {
|
||||
spaceRoomList.destroy()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
|
||||
return when (navTarget) {
|
||||
NavTarget.Leave -> {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,24 @@
|
|||
/*
|
||||
* Copyright 2024 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.features.space.impl.di
|
||||
|
||||
import dev.zacsweers.metro.ContributesTo
|
||||
import dev.zacsweers.metro.GraphExtension
|
||||
import dev.zacsweers.metro.Provides
|
||||
import io.element.android.libraries.architecture.NodeFactoriesBindings
|
||||
import io.element.android.libraries.di.SessionScope
|
||||
import io.element.android.libraries.matrix.api.spaces.SpaceRoomList
|
||||
|
||||
@GraphExtension(SpaceFlowScope::class)
|
||||
interface SpaceFlowGraph : NodeFactoriesBindings {
|
||||
@ContributesTo(SessionScope::class)
|
||||
@GraphExtension.Factory
|
||||
interface Factory {
|
||||
fun create(@Provides spaceRoomList: SpaceRoomList): SpaceFlowGraph
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
/*
|
||||
* Copyright 2024 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.features.space.impl.di
|
||||
|
||||
abstract class SpaceFlowScope private constructor()
|
||||
|
|
@ -15,20 +15,15 @@ import com.bumble.appyx.core.plugin.Plugin
|
|||
import dev.zacsweers.metro.Assisted
|
||||
import dev.zacsweers.metro.AssistedInject
|
||||
import io.element.android.annotations.ContributesNode
|
||||
import io.element.android.features.space.api.SpaceEntryPoint
|
||||
import io.element.android.libraries.architecture.inputs
|
||||
import io.element.android.libraries.di.SessionScope
|
||||
import io.element.android.features.space.impl.di.SpaceFlowScope
|
||||
|
||||
@ContributesNode(SessionScope::class)
|
||||
@ContributesNode(SpaceFlowScope::class)
|
||||
@AssistedInject
|
||||
class LeaveSpaceNode(
|
||||
@Assisted buildContext: BuildContext,
|
||||
@Assisted plugins: List<Plugin>,
|
||||
presenterFactory: LeaveSpacePresenter.Factory,
|
||||
private val presenter: LeaveSpacePresenter,
|
||||
) : Node(buildContext, plugins = plugins) {
|
||||
private val inputs: SpaceEntryPoint.Inputs = inputs()
|
||||
private val presenter = presenterFactory.create(inputs)
|
||||
|
||||
@Composable
|
||||
override fun View(modifier: Modifier) {
|
||||
val state = presenter.present()
|
||||
|
|
|
|||
|
|
@ -15,17 +15,14 @@ import androidx.compose.runtime.mutableStateOf
|
|||
import androidx.compose.runtime.produceState
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import dev.zacsweers.metro.Assisted
|
||||
import dev.zacsweers.metro.AssistedFactory
|
||||
import dev.zacsweers.metro.Inject
|
||||
import io.element.android.features.space.api.SpaceEntryPoint
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
import io.element.android.libraries.architecture.runUpdatingState
|
||||
import io.element.android.libraries.matrix.api.MatrixClient
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.spaces.SpaceRoom
|
||||
import io.element.android.libraries.matrix.api.spaces.SpaceRoomList
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.ImmutableSet
|
||||
import kotlinx.collections.immutable.persistentSetOf
|
||||
|
|
@ -37,16 +34,8 @@ import kotlin.jvm.optionals.getOrNull
|
|||
|
||||
@Inject
|
||||
class LeaveSpacePresenter(
|
||||
@Assisted private val inputs: SpaceEntryPoint.Inputs,
|
||||
matrixClient: MatrixClient,
|
||||
private val spaceRoomList: SpaceRoomList,
|
||||
) : Presenter<LeaveSpaceState> {
|
||||
@AssistedFactory
|
||||
fun interface Factory {
|
||||
fun create(inputs: SpaceEntryPoint.Inputs): LeaveSpacePresenter
|
||||
}
|
||||
|
||||
private val spaceRoomList = matrixClient.spaceService.spaceRoomList(inputs.roomId)
|
||||
|
||||
@Composable
|
||||
override fun present(): LeaveSpaceState {
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
|
|
|
|||
|
|
@ -19,24 +19,24 @@ import dev.zacsweers.metro.Assisted
|
|||
import dev.zacsweers.metro.AssistedInject
|
||||
import io.element.android.annotations.ContributesNode
|
||||
import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteView
|
||||
import io.element.android.features.space.api.SpaceEntryPoint
|
||||
import io.element.android.features.space.impl.di.SpaceFlowScope
|
||||
import io.element.android.libraries.androidutils.R
|
||||
import io.element.android.libraries.androidutils.system.startSharePlainTextIntent
|
||||
import io.element.android.libraries.architecture.inputs
|
||||
import io.element.android.libraries.di.SessionScope
|
||||
import io.element.android.libraries.matrix.api.MatrixClient
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.spaces.SpaceRoomList
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
|
||||
@ContributesNode(SessionScope::class)
|
||||
@ContributesNode(SpaceFlowScope::class)
|
||||
@AssistedInject
|
||||
class SpaceNode(
|
||||
@Assisted buildContext: BuildContext,
|
||||
@Assisted plugins: List<Plugin>,
|
||||
presenterFactory: SpacePresenter.Factory,
|
||||
private val presenter: SpacePresenter,
|
||||
private val matrixClient: MatrixClient,
|
||||
private val spaceRoomList: SpaceRoomList,
|
||||
private val acceptDeclineInviteView: AcceptDeclineInviteView,
|
||||
) : Node(buildContext, plugins = plugins) {
|
||||
interface Callback : Plugin {
|
||||
|
|
@ -44,12 +44,10 @@ class SpaceNode(
|
|||
fun onLeaveSpace()
|
||||
}
|
||||
|
||||
private val inputs: SpaceEntryPoint.Inputs = inputs()
|
||||
private val callback = plugins.filterIsInstance<Callback>().single()
|
||||
private val presenter = presenterFactory.create(inputs)
|
||||
|
||||
private fun onShareRoom(context: Context) = lifecycleScope.launch {
|
||||
matrixClient.getRoom(inputs.roomId)?.use { room ->
|
||||
matrixClient.getRoom(spaceRoomList.roomId)?.use { room ->
|
||||
room.getPermalink()
|
||||
.onSuccess { permalink ->
|
||||
context.startSharePlainTextIntent(
|
||||
|
|
|
|||
|
|
@ -14,15 +14,12 @@ import androidx.compose.runtime.getValue
|
|||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import dev.zacsweers.metro.Assisted
|
||||
import dev.zacsweers.metro.AssistedFactory
|
||||
import dev.zacsweers.metro.AssistedInject
|
||||
import dev.zacsweers.metro.Inject
|
||||
import im.vector.app.features.analytics.plan.JoinedRoom
|
||||
import io.element.android.features.invite.api.SeenInvitesStore
|
||||
import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteEvents
|
||||
import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteState
|
||||
import io.element.android.features.invite.api.toInviteData
|
||||
import io.element.android.features.space.api.SpaceEntryPoint
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
import io.element.android.libraries.core.coroutine.mapState
|
||||
|
|
@ -44,20 +41,15 @@ import kotlinx.coroutines.flow.map
|
|||
import kotlinx.coroutines.launch
|
||||
import kotlin.jvm.optionals.getOrNull
|
||||
|
||||
@AssistedInject class SpacePresenter(
|
||||
@Assisted private val inputs: SpaceEntryPoint.Inputs,
|
||||
@Inject
|
||||
class SpacePresenter(
|
||||
private val spaceRoomList: SpaceRoomList,
|
||||
private val client: MatrixClient,
|
||||
private val seenInvitesStore: SeenInvitesStore,
|
||||
private val joinRoom: JoinRoom,
|
||||
private val acceptDeclineInvitePresenter: Presenter<AcceptDeclineInviteState>,
|
||||
@SessionCoroutineScope private val sessionCoroutineScope: CoroutineScope,
|
||||
) : Presenter<SpaceState> {
|
||||
@AssistedFactory fun interface Factory {
|
||||
fun create(inputs: SpaceEntryPoint.Inputs): SpacePresenter
|
||||
}
|
||||
|
||||
private val spaceRoomList = client.spaceService.spaceRoomList(inputs.roomId)
|
||||
|
||||
@Composable
|
||||
override fun present(): SpaceState {
|
||||
LaunchedEffect(Unit) {
|
||||
|
|
|
|||
|
|
@ -12,8 +12,12 @@ import com.bumble.appyx.core.modality.BuildContext
|
|||
import com.bumble.appyx.testing.junit4.util.MainDispatcherRule
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.features.space.api.SpaceEntryPoint
|
||||
import io.element.android.features.space.impl.di.FakeSpaceFlowGraph
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
||||
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
||||
import io.element.android.libraries.matrix.test.spaces.FakeSpaceRoomList
|
||||
import io.element.android.libraries.matrix.test.spaces.FakeSpaceService
|
||||
import io.element.android.tests.testutils.lambda.lambdaError
|
||||
import io.element.android.tests.testutils.node.TestParentNode
|
||||
import org.junit.Rule
|
||||
|
|
@ -34,6 +38,12 @@ class DefaultSpaceEntryPointTest {
|
|||
SpaceFlowNode(
|
||||
buildContext = buildContext,
|
||||
plugins = plugins,
|
||||
matrixClient = FakeMatrixClient(
|
||||
spaceService = FakeSpaceService(
|
||||
spaceRoomListResult = { _: RoomId -> FakeSpaceRoomList(A_ROOM_ID) }
|
||||
)
|
||||
),
|
||||
graphFactory = FakeSpaceFlowGraph.Factory
|
||||
)
|
||||
}
|
||||
val callback = object : SpaceEntryPoint.Callback {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,25 @@
|
|||
/*
|
||||
* 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.features.space.impl.di
|
||||
|
||||
import com.bumble.appyx.core.node.Node
|
||||
import io.element.android.libraries.architecture.AssistedNodeFactory
|
||||
import io.element.android.libraries.matrix.api.spaces.SpaceRoomList
|
||||
import kotlin.reflect.KClass
|
||||
|
||||
class FakeSpaceFlowGraph : SpaceFlowGraph {
|
||||
object Factory : SpaceFlowGraph.Factory {
|
||||
override fun create(spaceRoomList: SpaceRoomList): SpaceFlowGraph {
|
||||
return FakeSpaceFlowGraph()
|
||||
}
|
||||
}
|
||||
|
||||
override fun nodeFactories(): Map<KClass<out Node>, AssistedNodeFactory<*>> {
|
||||
return emptyMap()
|
||||
}
|
||||
}
|
||||
|
|
@ -10,15 +10,11 @@
|
|||
package io.element.android.features.space.impl.leave
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.features.space.api.SpaceEntryPoint
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.libraries.matrix.api.MatrixClient
|
||||
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
||||
import io.element.android.libraries.matrix.api.spaces.SpaceRoomList
|
||||
import io.element.android.libraries.matrix.test.A_SPACE_NAME
|
||||
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
||||
import io.element.android.libraries.matrix.test.spaces.FakeSpaceRoomList
|
||||
import io.element.android.libraries.matrix.test.spaces.FakeSpaceService
|
||||
import io.element.android.libraries.previewutils.room.aSpaceRoom
|
||||
import io.element.android.tests.testutils.test
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
|
|
@ -29,15 +25,7 @@ import org.junit.Test
|
|||
class LeaveSpacePresenterTest {
|
||||
@Test
|
||||
fun `present - initial state`() = runTest {
|
||||
val presenter = createLeaveSpacePresenter(
|
||||
matrixClient = FakeMatrixClient(
|
||||
spaceService = FakeSpaceService(
|
||||
spaceRoomListResult = {
|
||||
FakeSpaceRoomList()
|
||||
},
|
||||
),
|
||||
),
|
||||
)
|
||||
val presenter = createLeaveSpacePresenter()
|
||||
presenter.test {
|
||||
val state = awaitItem()
|
||||
assertThat(state.spaceName).isNull()
|
||||
|
|
@ -51,11 +39,7 @@ class LeaveSpacePresenterTest {
|
|||
fun `present - current space name`() = runTest {
|
||||
val fakeSpaceRoomList = FakeSpaceRoomList()
|
||||
val presenter = createLeaveSpacePresenter(
|
||||
matrixClient = FakeMatrixClient(
|
||||
spaceService = FakeSpaceService(
|
||||
spaceRoomListResult = { fakeSpaceRoomList },
|
||||
),
|
||||
),
|
||||
spaceRoomList = fakeSpaceRoomList,
|
||||
)
|
||||
presenter.test {
|
||||
val state = awaitItem()
|
||||
|
|
@ -71,12 +55,10 @@ class LeaveSpacePresenterTest {
|
|||
}
|
||||
|
||||
private fun createLeaveSpacePresenter(
|
||||
inputs: SpaceEntryPoint.Inputs = SpaceEntryPoint.Inputs(A_ROOM_ID),
|
||||
matrixClient: MatrixClient = FakeMatrixClient(),
|
||||
spaceRoomList: SpaceRoomList = FakeSpaceRoomList(),
|
||||
): LeaveSpacePresenter {
|
||||
return LeaveSpacePresenter(
|
||||
inputs = inputs,
|
||||
matrixClient = matrixClient,
|
||||
spaceRoomList = spaceRoomList,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,7 +16,6 @@ import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteS
|
|||
import io.element.android.features.invite.api.acceptdecline.anAcceptDeclineInviteState
|
||||
import io.element.android.features.invite.api.toInviteData
|
||||
import io.element.android.features.invite.test.InMemorySeenInvitesStore
|
||||
import io.element.android.features.space.api.SpaceEntryPoint
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
import io.element.android.libraries.matrix.api.MatrixClient
|
||||
|
|
@ -31,7 +30,6 @@ import io.element.android.libraries.matrix.test.A_ROOM_ID_2
|
|||
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
||||
import io.element.android.libraries.matrix.test.room.join.FakeJoinRoom
|
||||
import io.element.android.libraries.matrix.test.spaces.FakeSpaceRoomList
|
||||
import io.element.android.libraries.matrix.test.spaces.FakeSpaceService
|
||||
import io.element.android.libraries.previewutils.room.aSpaceRoom
|
||||
import io.element.android.tests.testutils.EventsRecorder
|
||||
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
||||
|
|
@ -51,17 +49,8 @@ class SpacePresenterTest {
|
|||
val paginateResult = lambdaRecorder<Result<Unit>> {
|
||||
Result.success(Unit)
|
||||
}
|
||||
val presenter = createSpacePresenter(
|
||||
client = FakeMatrixClient(
|
||||
spaceService = FakeSpaceService(
|
||||
spaceRoomListResult = {
|
||||
FakeSpaceRoomList(
|
||||
paginateResult = paginateResult,
|
||||
)
|
||||
},
|
||||
),
|
||||
),
|
||||
)
|
||||
val spaceRoomList = FakeSpaceRoomList(paginateResult = paginateResult)
|
||||
val presenter = createSpacePresenter(spaceRoomList = spaceRoomList)
|
||||
presenter.test {
|
||||
val state = awaitItem()
|
||||
assertThat(state.currentSpace).isNull()
|
||||
|
|
@ -81,17 +70,8 @@ class SpacePresenterTest {
|
|||
val paginateResult = lambdaRecorder<Result<Unit>> {
|
||||
Result.success(Unit)
|
||||
}
|
||||
val presenter = createSpacePresenter(
|
||||
client = FakeMatrixClient(
|
||||
spaceService = FakeSpaceService(
|
||||
spaceRoomListResult = {
|
||||
FakeSpaceRoomList(
|
||||
paginateResult = paginateResult,
|
||||
)
|
||||
},
|
||||
),
|
||||
),
|
||||
)
|
||||
val spaceRoomList = FakeSpaceRoomList(paginateResult = paginateResult)
|
||||
val presenter = createSpacePresenter(spaceRoomList = spaceRoomList)
|
||||
presenter.test {
|
||||
val state = awaitItem()
|
||||
advanceUntilIdle()
|
||||
|
|
@ -104,25 +84,20 @@ class SpacePresenterTest {
|
|||
|
||||
@Test
|
||||
fun `present - has more to load value`() = runTest {
|
||||
val fakeSpaceRoomList = FakeSpaceRoomList(
|
||||
paginateResult = { Result.success(Unit) },
|
||||
)
|
||||
val presenter = createSpacePresenter(
|
||||
client = FakeMatrixClient(
|
||||
spaceService = FakeSpaceService(
|
||||
spaceRoomListResult = { fakeSpaceRoomList },
|
||||
),
|
||||
),
|
||||
)
|
||||
val paginateResult = lambdaRecorder<Result<Unit>> {
|
||||
Result.success(Unit)
|
||||
}
|
||||
val spaceRoomList = FakeSpaceRoomList(paginateResult = paginateResult)
|
||||
val presenter = createSpacePresenter(spaceRoomList = spaceRoomList)
|
||||
presenter.test {
|
||||
val state = awaitItem()
|
||||
advanceUntilIdle()
|
||||
assertThat(state.hasMoreToLoad).isTrue()
|
||||
fakeSpaceRoomList.emitPaginationStatus(
|
||||
spaceRoomList.emitPaginationStatus(
|
||||
SpaceRoomList.PaginationStatus.Idle(hasMoreToLoad = false)
|
||||
)
|
||||
assertThat(awaitItem().hasMoreToLoad).isFalse()
|
||||
fakeSpaceRoomList.emitPaginationStatus(
|
||||
spaceRoomList.emitPaginationStatus(
|
||||
SpaceRoomList.PaginationStatus.Idle(hasMoreToLoad = true)
|
||||
)
|
||||
assertThat(awaitItem().hasMoreToLoad).isTrue()
|
||||
|
|
@ -131,44 +106,34 @@ class SpacePresenterTest {
|
|||
|
||||
@Test
|
||||
fun `present - current space value`() = runTest {
|
||||
val fakeSpaceRoomList = FakeSpaceRoomList(
|
||||
paginateResult = { Result.success(Unit) },
|
||||
)
|
||||
val presenter = createSpacePresenter(
|
||||
client = FakeMatrixClient(
|
||||
spaceService = FakeSpaceService(
|
||||
spaceRoomListResult = { fakeSpaceRoomList },
|
||||
),
|
||||
),
|
||||
)
|
||||
val paginateResult = lambdaRecorder<Result<Unit>> {
|
||||
Result.success(Unit)
|
||||
}
|
||||
val spaceRoomList = FakeSpaceRoomList(paginateResult = paginateResult)
|
||||
val presenter = createSpacePresenter(spaceRoomList = spaceRoomList)
|
||||
presenter.test {
|
||||
val state = awaitItem()
|
||||
advanceUntilIdle()
|
||||
assertThat(state.currentSpace).isNull()
|
||||
val aSpace = aSpaceRoom()
|
||||
fakeSpaceRoomList.emitCurrentSpace(aSpace)
|
||||
spaceRoomList.emitCurrentSpace(aSpace)
|
||||
assertThat(awaitItem().currentSpace).isEqualTo(aSpace)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - children value`() = runTest {
|
||||
val fakeSpaceRoomList = FakeSpaceRoomList(
|
||||
paginateResult = { Result.success(Unit) },
|
||||
)
|
||||
val presenter = createSpacePresenter(
|
||||
client = FakeMatrixClient(
|
||||
spaceService = FakeSpaceService(
|
||||
spaceRoomListResult = { fakeSpaceRoomList },
|
||||
),
|
||||
),
|
||||
)
|
||||
val paginateResult = lambdaRecorder<Result<Unit>> {
|
||||
Result.success(Unit)
|
||||
}
|
||||
val spaceRoomList = FakeSpaceRoomList(paginateResult = paginateResult)
|
||||
val presenter = createSpacePresenter(spaceRoomList = spaceRoomList)
|
||||
presenter.test {
|
||||
val state = awaitItem()
|
||||
advanceUntilIdle()
|
||||
assertThat(state.children).isEmpty()
|
||||
val aSpace = aSpaceRoom()
|
||||
fakeSpaceRoomList.emitSpaceRooms(listOf(aSpace))
|
||||
spaceRoomList.emitSpaceRooms(listOf(aSpace))
|
||||
assertThat(awaitItem().children).containsExactly(aSpace)
|
||||
}
|
||||
}
|
||||
|
|
@ -195,11 +160,7 @@ class SpacePresenterTest {
|
|||
paginateResult = { Result.success(Unit) },
|
||||
)
|
||||
val presenter = createSpacePresenter(
|
||||
client = FakeMatrixClient(
|
||||
spaceService = FakeSpaceService(
|
||||
spaceRoomListResult = { fakeSpaceRoomList },
|
||||
),
|
||||
),
|
||||
spaceRoomList = fakeSpaceRoomList,
|
||||
joinRoom = FakeJoinRoom(
|
||||
lambda = joinRoom,
|
||||
),
|
||||
|
|
@ -253,11 +214,7 @@ class SpacePresenterTest {
|
|||
paginateResult = { Result.success(Unit) },
|
||||
)
|
||||
val presenter = createSpacePresenter(
|
||||
client = FakeMatrixClient(
|
||||
spaceService = FakeSpaceService(
|
||||
spaceRoomListResult = { fakeSpaceRoomList },
|
||||
),
|
||||
),
|
||||
spaceRoomList = fakeSpaceRoomList,
|
||||
joinRoom = FakeJoinRoom(
|
||||
lambda = { _, _, _ -> Result.failure(AN_EXCEPTION) },
|
||||
),
|
||||
|
|
@ -312,11 +269,7 @@ class SpacePresenterTest {
|
|||
paginateResult = { Result.success(Unit) },
|
||||
)
|
||||
val presenter = createSpacePresenter(
|
||||
client = FakeMatrixClient(
|
||||
spaceService = FakeSpaceService(
|
||||
spaceRoomListResult = { fakeSpaceRoomList },
|
||||
),
|
||||
),
|
||||
spaceRoomList = fakeSpaceRoomList,
|
||||
acceptDeclineInvitePresenter = {
|
||||
anAcceptDeclineInviteState(
|
||||
eventSink = eventRecorder,
|
||||
|
|
@ -348,8 +301,8 @@ class SpacePresenterTest {
|
|||
}
|
||||
|
||||
private fun TestScope.createSpacePresenter(
|
||||
inputs: SpaceEntryPoint.Inputs = SpaceEntryPoint.Inputs(A_ROOM_ID),
|
||||
client: MatrixClient = FakeMatrixClient(),
|
||||
spaceRoomList: SpaceRoomList = FakeSpaceRoomList(),
|
||||
seenInvitesStore: SeenInvitesStore = InMemorySeenInvitesStore(),
|
||||
joinRoom: JoinRoom = FakeJoinRoom(
|
||||
lambda = { _, _, _ -> Result.success(Unit) },
|
||||
|
|
@ -357,8 +310,8 @@ class SpacePresenterTest {
|
|||
acceptDeclineInvitePresenter: Presenter<AcceptDeclineInviteState> = Presenter { anAcceptDeclineInviteState() },
|
||||
): SpacePresenter {
|
||||
return SpacePresenter(
|
||||
inputs = inputs,
|
||||
client = client,
|
||||
spaceRoomList = spaceRoomList,
|
||||
seenInvitesStore = seenInvitesStore,
|
||||
joinRoom = joinRoom,
|
||||
acceptDeclineInvitePresenter = acceptDeclineInvitePresenter,
|
||||
|
|
|
|||
|
|
@ -155,7 +155,7 @@ class IncomingVerificationPresenter(
|
|||
StateMachineState.RejectingIncomingVerification,
|
||||
null -> {
|
||||
Step.Initial(
|
||||
deviceDisplayName = sessionVerificationRequestDetails.senderProfile.displayName ?: sessionVerificationRequestDetails.deviceId.value,
|
||||
deviceDisplayName = sessionVerificationRequestDetails.deviceDisplayName,
|
||||
deviceId = sessionVerificationRequestDetails.deviceId,
|
||||
formattedSignInTime = formattedSignInTime,
|
||||
isWaiting = machineState == StateMachineState.AcceptingIncomingVerification ||
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ data class IncomingVerificationState(
|
|||
@Stable
|
||||
sealed interface Step {
|
||||
data class Initial(
|
||||
val deviceDisplayName: String,
|
||||
val deviceDisplayName: String?,
|
||||
val deviceId: DeviceId,
|
||||
val formattedSignInTime: String,
|
||||
val isWaiting: Boolean,
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import io.element.android.features.verifysession.impl.ui.aEmojisSessionVerificat
|
|||
import io.element.android.libraries.matrix.api.core.DeviceId
|
||||
import io.element.android.libraries.matrix.api.core.FlowId
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||
import io.element.android.libraries.matrix.api.verification.SessionVerificationRequestDetails
|
||||
import io.element.android.libraries.matrix.api.verification.VerificationRequest
|
||||
|
||||
|
|
@ -55,26 +56,28 @@ internal fun aStepInitial(
|
|||
|
||||
internal fun anIncomingSessionVerificationRequest() = VerificationRequest.Incoming.OtherSession(
|
||||
details = SessionVerificationRequestDetails(
|
||||
senderProfile = SessionVerificationRequestDetails.SenderProfile(
|
||||
senderProfile = MatrixUser(
|
||||
userId = UserId("@alice:example.com"),
|
||||
displayName = "Alice",
|
||||
avatarUrl = null,
|
||||
),
|
||||
flowId = FlowId("1234"),
|
||||
deviceId = DeviceId("ILAKNDNASDLK"),
|
||||
deviceDisplayName = "a device name",
|
||||
firstSeenTimestamp = 0,
|
||||
)
|
||||
)
|
||||
|
||||
internal fun anIncomingUserVerificationRequest() = VerificationRequest.Incoming.User(
|
||||
details = SessionVerificationRequestDetails(
|
||||
senderProfile = SessionVerificationRequestDetails.SenderProfile(
|
||||
senderProfile = MatrixUser(
|
||||
userId = UserId("@alice:example.com"),
|
||||
displayName = "Alice",
|
||||
avatarUrl = null,
|
||||
),
|
||||
flowId = FlowId("1234"),
|
||||
deviceId = DeviceId("ILAKNDNASDLK"),
|
||||
deviceDisplayName = "a device name",
|
||||
firstSeenTimestamp = 0,
|
||||
)
|
||||
)
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ import androidx.compose.ui.semantics.focused
|
|||
import androidx.compose.ui.semantics.progressBarRangeInfo
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.compound.theme.ElementTheme
|
||||
|
|
@ -214,9 +215,7 @@ private fun ContentInitial(
|
|||
.padding(top = 24.dp),
|
||||
) {
|
||||
VerificationUserProfileContent(
|
||||
userId = request.details.senderProfile.userId,
|
||||
displayName = request.details.senderProfile.displayName,
|
||||
avatarUrl = request.details.senderProfile.avatarUrl,
|
||||
user = request.details.senderProfile,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -292,3 +291,11 @@ internal fun IncomingVerificationViewPreview(@PreviewParameter(IncomingVerificat
|
|||
state = state,
|
||||
)
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
internal fun IncomingVerificationViewA11yPreview() = ElementPreview {
|
||||
IncomingVerificationView(
|
||||
state = anIncomingVerificationState(),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ import io.element.android.libraries.ui.strings.CommonStrings
|
|||
|
||||
@Composable
|
||||
fun SessionDetailsView(
|
||||
deviceName: String,
|
||||
deviceName: String?,
|
||||
deviceId: DeviceId,
|
||||
signInFormattedTimestamp: String,
|
||||
modifier: Modifier = Modifier,
|
||||
|
|
@ -61,7 +61,7 @@ fun SessionDetailsView(
|
|||
resourceId = CompoundDrawables.ic_compound_devices
|
||||
)
|
||||
Text(
|
||||
text = deviceName,
|
||||
text = deviceName ?: deviceId.value,
|
||||
style = ElementTheme.typography.fontBodyMdMedium,
|
||||
color = ElementTheme.colors.textPrimary,
|
||||
)
|
||||
|
|
@ -87,9 +87,16 @@ fun SessionDetailsView(
|
|||
@PreviewsDayNight
|
||||
@Composable
|
||||
internal fun SessionDetailsViewPreview() = ElementPreview {
|
||||
SessionDetailsView(
|
||||
deviceName = "Element X Android",
|
||||
deviceId = DeviceId("ILAKNDNASDLK"),
|
||||
signInFormattedTimestamp = "12:34",
|
||||
)
|
||||
Column {
|
||||
SessionDetailsView(
|
||||
deviceName = "Element X Android",
|
||||
deviceId = DeviceId("ILAKNDNASDLK"),
|
||||
signInFormattedTimestamp = "12:34",
|
||||
)
|
||||
SessionDetailsView(
|
||||
deviceName = null,
|
||||
deviceId = DeviceId("ILAKNDNASDLK"),
|
||||
signInFormattedTimestamp = "12:34",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import androidx.compose.foundation.layout.Row
|
|||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
|
|
@ -23,7 +24,6 @@ import androidx.compose.ui.draw.clip
|
|||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.compound.theme.ElementTheme
|
||||
import io.element.android.libraries.designsystem.components.avatar.Avatar
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarType
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||
|
|
@ -31,18 +31,21 @@ import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
|||
import io.element.android.libraries.designsystem.theme.components.Text
|
||||
import io.element.android.libraries.designsystem.utils.CommonDrawables
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||
import io.element.android.libraries.matrix.ui.model.getAvatarData
|
||||
import io.element.android.libraries.matrix.ui.model.getBestName
|
||||
|
||||
/**
|
||||
* Ref: https://www.figma.com/design/lMrKOhS8BEb75GXVq7FnNI/ER-96--User-Verification-by-Emoji?node-id=116-52049
|
||||
*/
|
||||
@Composable
|
||||
fun VerificationUserProfileContent(
|
||||
userId: UserId,
|
||||
displayName: String?,
|
||||
avatarUrl: String?,
|
||||
user: MatrixUser,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val avatarData = remember(userId, displayName, avatarUrl) {
|
||||
AvatarData(id = userId.value, name = displayName, url = avatarUrl, size = AvatarSize.UserVerification)
|
||||
val avatarData = remember(user) {
|
||||
user.getAvatarData(AvatarSize.UserVerification)
|
||||
}
|
||||
|
||||
Row(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
|
|
@ -55,12 +58,20 @@ fun VerificationUserProfileContent(
|
|||
avatarData = avatarData,
|
||||
avatarType = AvatarType.User,
|
||||
)
|
||||
Spacer(modifier = Modifier.padding(12.dp))
|
||||
Spacer(modifier = Modifier.width(12.dp))
|
||||
Column(verticalArrangement = Arrangement.spacedBy(2.dp)) {
|
||||
Text(text = displayName ?: userId.value, style = ElementTheme.typography.fontBodyLgMedium, color = ElementTheme.colors.textPrimary)
|
||||
Text(
|
||||
text = user.getBestName(),
|
||||
style = ElementTheme.typography.fontBodyLgMedium,
|
||||
color = ElementTheme.colors.textPrimary,
|
||||
)
|
||||
|
||||
if (displayName != null) {
|
||||
Text(text = userId.value, style = ElementTheme.typography.fontBodyMdRegular, color = ElementTheme.colors.textSecondary)
|
||||
if (user.displayName.isNullOrEmpty().not()) {
|
||||
Text(
|
||||
text = user.userId.value,
|
||||
style = ElementTheme.typography.fontBodyMdRegular,
|
||||
color = ElementTheme.colors.textSecondary,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -72,8 +83,10 @@ internal fun VerificationUserProfileContentPreview() = ElementPreview(
|
|||
drawableFallbackForImages = CommonDrawables.sample_avatar
|
||||
) {
|
||||
VerificationUserProfileContent(
|
||||
userId = UserId("@alice:example.com"),
|
||||
displayName = "Alice",
|
||||
avatarUrl = "https://example.com/avatar.png",
|
||||
user = MatrixUser(
|
||||
userId = UserId("@alice:example.com"),
|
||||
displayName = "Alice",
|
||||
avatarUrl = "https://example.com/avatar.png",
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import io.element.android.features.verifysession.impl.ui.aEmojisSessionVerificat
|
|||
import io.element.android.libraries.dateformatter.api.DateFormatter
|
||||
import io.element.android.libraries.dateformatter.test.FakeDateFormatter
|
||||
import io.element.android.libraries.matrix.api.core.FlowId
|
||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||
import io.element.android.libraries.matrix.api.verification.SessionVerificationRequestDetails
|
||||
import io.element.android.libraries.matrix.api.verification.SessionVerificationService
|
||||
import io.element.android.libraries.matrix.api.verification.VerificationFlowState
|
||||
|
|
@ -293,13 +294,14 @@ class IncomingVerificationPresenterTest {
|
|||
|
||||
private val anIncomingSessionVerificationRequest = VerificationRequest.Incoming.OtherSession(
|
||||
details = SessionVerificationRequestDetails(
|
||||
senderProfile = SessionVerificationRequestDetails.SenderProfile(
|
||||
senderProfile = MatrixUser(
|
||||
userId = A_USER_ID,
|
||||
displayName = "a device name",
|
||||
displayName = "a user name",
|
||||
avatarUrl = null,
|
||||
),
|
||||
flowId = FlowId("flowId"),
|
||||
deviceId = A_DEVICE_ID,
|
||||
deviceDisplayName = "a device name",
|
||||
firstSeenTimestamp = A_TIMESTAMP,
|
||||
)
|
||||
)
|
||||
|
|
|
|||
|
|
@ -244,7 +244,9 @@ interface BaseRoom : Closeable {
|
|||
|
||||
suspend fun subscribeToCallDecline(notificationEventId: EventId): Flow<UserId>
|
||||
|
||||
/**
|
||||
suspend fun threadRootIdForEvent(eventId: EventId): Result<ThreadId?>
|
||||
|
||||
/**
|
||||
* Destroy the room and release all resources associated to it.
|
||||
*/
|
||||
fun destroy()
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@
|
|||
|
||||
package io.element.android.libraries.matrix.api.spaces
|
||||
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import java.util.Optional
|
||||
|
|
@ -17,9 +18,13 @@ interface SpaceRoomList {
|
|||
data class Idle(val hasMoreToLoad: Boolean) : PaginationStatus
|
||||
}
|
||||
|
||||
val roomId: RoomId
|
||||
|
||||
val currentSpaceFlow: StateFlow<Optional<SpaceRoom>>
|
||||
|
||||
val spaceRoomsFlow: Flow<List<SpaceRoom>>
|
||||
val paginationStatusFlow: StateFlow<PaginationStatus>
|
||||
suspend fun paginate(): Result<Unit>
|
||||
|
||||
fun destroy()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,20 +10,14 @@ package io.element.android.libraries.matrix.api.verification
|
|||
import android.os.Parcelable
|
||||
import io.element.android.libraries.matrix.api.core.DeviceId
|
||||
import io.element.android.libraries.matrix.api.core.FlowId
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@Parcelize
|
||||
data class SessionVerificationRequestDetails(
|
||||
val senderProfile: SenderProfile,
|
||||
val senderProfile: MatrixUser,
|
||||
val flowId: FlowId,
|
||||
val deviceId: DeviceId,
|
||||
val deviceDisplayName: String?,
|
||||
val firstSeenTimestamp: Long,
|
||||
) : Parcelable {
|
||||
@Parcelize
|
||||
data class SenderProfile(
|
||||
val userId: UserId,
|
||||
val displayName: String?,
|
||||
val avatarUrl: String?,
|
||||
) : Parcelable
|
||||
}
|
||||
) : Parcelable
|
||||
|
|
|
|||
|
|
@ -51,6 +51,7 @@ import io.element.android.libraries.matrix.api.user.MatrixUser
|
|||
import io.element.android.libraries.matrix.api.verification.SessionVerificationService
|
||||
import io.element.android.libraries.matrix.impl.encryption.RustEncryptionService
|
||||
import io.element.android.libraries.matrix.impl.exception.mapClientException
|
||||
import io.element.android.libraries.matrix.impl.mapper.map
|
||||
import io.element.android.libraries.matrix.impl.media.RustMediaLoader
|
||||
import io.element.android.libraries.matrix.impl.media.RustMediaPreviewService
|
||||
import io.element.android.libraries.matrix.impl.notification.RustNotificationService
|
||||
|
|
@ -75,7 +76,6 @@ import io.element.android.libraries.matrix.impl.roomlist.roomOrNull
|
|||
import io.element.android.libraries.matrix.impl.spaces.RustSpaceService
|
||||
import io.element.android.libraries.matrix.impl.sync.RustSyncService
|
||||
import io.element.android.libraries.matrix.impl.sync.map
|
||||
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.util.SessionPathsProvider
|
||||
import io.element.android.libraries.matrix.impl.util.cancelAndDestroy
|
||||
|
|
@ -403,7 +403,7 @@ class RustMatrixClient(
|
|||
|
||||
override suspend fun getProfile(userId: UserId): Result<MatrixUser> = withContext(sessionDispatcher) {
|
||||
runCatchingExceptions {
|
||||
innerClient.getProfile(userId.value).let(UserProfileMapper::map)
|
||||
innerClient.getProfile(userId.value).map()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -5,17 +5,14 @@
|
|||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.matrix.impl.usersearch
|
||||
package io.element.android.libraries.matrix.impl.mapper
|
||||
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||
import org.matrix.rustcomponents.sdk.UserProfile
|
||||
|
||||
object UserProfileMapper {
|
||||
fun map(userProfile: UserProfile): MatrixUser =
|
||||
MatrixUser(
|
||||
userId = UserId(userProfile.userId),
|
||||
displayName = userProfile.displayName,
|
||||
avatarUrl = userProfile.avatarUrl,
|
||||
)
|
||||
}
|
||||
fun UserProfile.map() = MatrixUser(
|
||||
userId = UserId(userId),
|
||||
displayName = displayName,
|
||||
avatarUrl = avatarUrl,
|
||||
)
|
||||
|
|
@ -322,4 +322,12 @@ class RustBaseRoom(
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun threadRootIdForEvent(eventId: EventId): Result<ThreadId?> = withContext(roomDispatcher) {
|
||||
runCatchingExceptions {
|
||||
innerRoom.loadOrFetchEvent(eventId.value).use {
|
||||
it.threadRootEventId()?.let(::ThreadId)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,26 +8,31 @@
|
|||
package io.element.android.libraries.matrix.impl.spaces
|
||||
|
||||
import io.element.android.libraries.core.extensions.runCatchingExceptions
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.spaces.SpaceRoom
|
||||
import io.element.android.libraries.matrix.api.spaces.SpaceRoomList
|
||||
import kotlinx.coroutines.CompletableDeferred
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import uniffi.matrix_sdk_ui.SpaceRoomListPaginationState
|
||||
import java.util.Optional
|
||||
import org.matrix.rustcomponents.sdk.SpaceRoomList as InnerSpaceRoomList
|
||||
|
||||
class RustSpaceRoomList(
|
||||
override val roomId: RoomId,
|
||||
private val innerProvider: suspend () -> InnerSpaceRoomList,
|
||||
sessionCoroutineScope: CoroutineScope,
|
||||
private val coroutineScope: CoroutineScope,
|
||||
spaceRoomMapper: SpaceRoomMapper,
|
||||
) : SpaceRoomList {
|
||||
private val inner = CompletableDeferred<InnerSpaceRoomList>()
|
||||
private val innerCompletable = CompletableDeferred<InnerSpaceRoomList>()
|
||||
|
||||
override val currentSpaceFlow = MutableStateFlow<Optional<SpaceRoom>>(Optional.empty())
|
||||
|
||||
|
|
@ -41,37 +46,45 @@ class RustSpaceRoomList(
|
|||
)
|
||||
|
||||
init {
|
||||
sessionCoroutineScope.launch {
|
||||
inner.complete(innerProvider())
|
||||
}
|
||||
sessionCoroutineScope.launch {
|
||||
inner.await().paginationStateFlow()
|
||||
coroutineScope.launch {
|
||||
val inner = innerProvider()
|
||||
innerCompletable.complete(inner)
|
||||
|
||||
inner.paginationStateFlow()
|
||||
.onEach { paginationStatus ->
|
||||
paginationStatusFlow.emit(paginationStatus.into())
|
||||
}
|
||||
.collect()
|
||||
}
|
||||
.launchIn(this)
|
||||
|
||||
sessionCoroutineScope.launch {
|
||||
inner.await().spaceListUpdateFlow()
|
||||
inner.spaceListUpdateFlow()
|
||||
.onEach { updates ->
|
||||
spaceListUpdateProcessor.postUpdates(updates)
|
||||
}
|
||||
.collect()
|
||||
}
|
||||
sessionCoroutineScope.launch {
|
||||
inner.await().spaceUpdateFlow()
|
||||
.launchIn(this)
|
||||
|
||||
inner.spaceUpdateFlow()
|
||||
.map { space -> space.map(spaceRoomMapper::map) }
|
||||
.onEach { space ->
|
||||
currentSpaceFlow.emit(space)
|
||||
}
|
||||
.collect()
|
||||
.launchIn(this)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun paginate(): Result<Unit> {
|
||||
return runCatchingExceptions {
|
||||
inner.await().paginate()
|
||||
innerCompletable.await().paginate()
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
override fun destroy() {
|
||||
Timber.d("Destroying SpaceRoomList $roomId")
|
||||
coroutineScope.cancel()
|
||||
try {
|
||||
innerCompletable.getCompleted().destroy()
|
||||
} catch (_: Exception) {
|
||||
// Ignore, we just want to make sure it's completed
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@
|
|||
|
||||
package io.element.android.libraries.matrix.impl.spaces
|
||||
|
||||
import io.element.android.libraries.core.coroutine.childScope
|
||||
import io.element.android.libraries.core.extensions.runCatchingExceptions
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.spaces.SpaceRoom
|
||||
|
|
@ -54,9 +55,11 @@ class RustSpaceService(
|
|||
}
|
||||
|
||||
override fun spaceRoomList(id: RoomId): SpaceRoomList {
|
||||
val childCoroutineScope = sessionCoroutineScope.childScope(sessionDispatcher, "SpaceRoomListScope-$this")
|
||||
return RustSpaceRoomList(
|
||||
roomId = id,
|
||||
innerProvider = { innerSpaceService.spaceRoomList(id.value) },
|
||||
sessionCoroutineScope = sessionCoroutineScope,
|
||||
coroutineScope = childCoroutineScope,
|
||||
spaceRoomMapper = spaceRoomMapper,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,13 +8,16 @@
|
|||
package io.element.android.libraries.matrix.impl.usersearch
|
||||
|
||||
import io.element.android.libraries.matrix.api.user.MatrixSearchUserResults
|
||||
import io.element.android.libraries.matrix.impl.mapper.map
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import org.matrix.rustcomponents.sdk.SearchUsersResults
|
||||
|
||||
object UserSearchResultMapper {
|
||||
fun map(result: SearchUsersResults): MatrixSearchUserResults {
|
||||
return MatrixSearchUserResults(
|
||||
results = result.results.map(UserProfileMapper::map).toImmutableList(),
|
||||
results = result.results
|
||||
.map { userProfile -> userProfile.map() }
|
||||
.toImmutableList(),
|
||||
limited = result.limited,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,22 +12,17 @@ import io.element.android.libraries.matrix.api.core.FlowId
|
|||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.matrix.api.verification.SessionVerificationRequestDetails
|
||||
import io.element.android.libraries.matrix.api.verification.VerificationRequest
|
||||
import io.element.android.libraries.matrix.impl.mapper.map
|
||||
import org.matrix.rustcomponents.sdk.SessionVerificationRequestDetails as RustSessionVerificationRequestDetails
|
||||
import org.matrix.rustcomponents.sdk.UserProfile as RustUserProfile
|
||||
|
||||
fun RustSessionVerificationRequestDetails.map() = SessionVerificationRequestDetails(
|
||||
senderProfile = senderProfile.map(),
|
||||
flowId = FlowId(flowId),
|
||||
deviceId = DeviceId(deviceId),
|
||||
deviceDisplayName = deviceDisplayName,
|
||||
firstSeenTimestamp = firstSeenTimestamp.toLong(),
|
||||
)
|
||||
|
||||
fun RustUserProfile.map() = SessionVerificationRequestDetails.SenderProfile(
|
||||
userId = UserId(userId),
|
||||
displayName = displayName,
|
||||
avatarUrl = avatarUrl,
|
||||
)
|
||||
|
||||
fun RustSessionVerificationRequestDetails.toVerificationRequest(currentUserId: UserId): VerificationRequest.Incoming {
|
||||
val details = map()
|
||||
return if (currentUserId == details.senderProfile.userId) {
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.matrix.impl.usersearch
|
||||
package io.element.android.libraries.matrix.impl.mapper
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||
|
|
@ -16,7 +16,7 @@ import org.junit.Test
|
|||
class UserProfileMapperTest {
|
||||
@Test
|
||||
fun map() {
|
||||
assertThat(UserProfileMapper.map(aRustUserProfile(A_USER_ID.value, "displayName", "avatarUrl")))
|
||||
assertThat(aRustUserProfile(A_USER_ID.value, "displayName", "avatarUrl").map())
|
||||
.isEqualTo(MatrixUser(A_USER_ID, "displayName", "avatarUrl"))
|
||||
}
|
||||
}
|
||||
|
|
@ -11,9 +11,11 @@ package io.element.android.libraries.matrix.impl.spaces
|
|||
|
||||
import app.cash.turbine.test
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.spaces.SpaceRoomList
|
||||
import io.element.android.libraries.matrix.impl.fixtures.factories.aRustSpaceRoom
|
||||
import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiSpaceRoomList
|
||||
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
||||
import io.element.android.libraries.matrix.test.A_ROOM_ID_2
|
||||
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
|
|
@ -84,13 +86,15 @@ class RustSpaceRoomListTest {
|
|||
}
|
||||
|
||||
private fun TestScope.createRustSpaceRoomList(
|
||||
roomId: RoomId = A_ROOM_ID,
|
||||
innerSpaceRoomList: InnerSpaceRoomList = FakeFfiSpaceRoomList(),
|
||||
innerProvider: suspend () -> InnerSpaceRoomList = { innerSpaceRoomList },
|
||||
spaceRoomMapper: SpaceRoomMapper = SpaceRoomMapper(),
|
||||
): RustSpaceRoomList {
|
||||
return RustSpaceRoomList(
|
||||
roomId = roomId,
|
||||
innerProvider = innerProvider,
|
||||
sessionCoroutineScope = backgroundScope,
|
||||
coroutineScope = backgroundScope,
|
||||
spaceRoomMapper = spaceRoomMapper,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -71,6 +71,7 @@ class FakeBaseRoom(
|
|||
private val forgetResult: () -> Result<Unit> = { lambdaError() },
|
||||
private val reportRoomResult: (String?) -> Result<Unit> = { lambdaError() },
|
||||
private val predecessorRoomResult: () -> PredecessorRoom? = { null },
|
||||
private val threadRootIdForEventResult: (EventId) -> Result<ThreadId?> = { lambdaError() },
|
||||
) : BaseRoom {
|
||||
private val _roomInfoFlow: MutableStateFlow<RoomInfo> = MutableStateFlow(initialRoomInfo)
|
||||
override val roomInfoFlow: StateFlow<RoomInfo> = _roomInfoFlow
|
||||
|
|
@ -244,6 +245,10 @@ class FakeBaseRoom(
|
|||
fun givenUpdateMembersResult(result: () -> Unit) {
|
||||
updateMembersResult = result
|
||||
}
|
||||
|
||||
override suspend fun threadRootIdForEvent(eventId: EventId): Result<ThreadId?> {
|
||||
return threadRootIdForEventResult(eventId)
|
||||
}
|
||||
}
|
||||
|
||||
fun defaultRoomPowerLevelValues() = RoomPowerLevelsValues(
|
||||
|
|
|
|||
|
|
@ -7,8 +7,10 @@
|
|||
|
||||
package io.element.android.libraries.matrix.test.spaces
|
||||
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.spaces.SpaceRoom
|
||||
import io.element.android.libraries.matrix.api.spaces.SpaceRoomList
|
||||
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
||||
import io.element.android.tests.testutils.lambda.lambdaError
|
||||
import io.element.android.tests.testutils.simulateLongTask
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
|
@ -18,6 +20,7 @@ import kotlinx.coroutines.flow.asStateFlow
|
|||
import java.util.Optional
|
||||
|
||||
class FakeSpaceRoomList(
|
||||
override val roomId: RoomId = A_ROOM_ID,
|
||||
initialSpaceFlowValue: SpaceRoom? = null,
|
||||
initialSpaceRoomsValue: List<SpaceRoom> = emptyList(),
|
||||
initialSpaceRoomList: SpaceRoomList.PaginationStatus = SpaceRoomList.PaginationStatus.Loading,
|
||||
|
|
@ -47,4 +50,8 @@ class FakeSpaceRoomList(
|
|||
override suspend fun paginate(): Result<Unit> = simulateLongTask {
|
||||
paginateResult()
|
||||
}
|
||||
|
||||
override fun destroy() {
|
||||
// No op
|
||||
}
|
||||
}
|
||||
|
|
|
|||
19
libraries/ui-common/build.gradle.kts
Normal file
19
libraries/ui-common/build.gradle.kts
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
plugins {
|
||||
id("io.element.android-compose-library")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "io.element.android.libraries.ui.common"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(libs.appyx.core)
|
||||
implementation(projects.libraries.designsystem)
|
||||
}
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
/*
|
||||
* 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.ui.common.nodes
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import com.bumble.appyx.core.modality.BuildContext
|
||||
import com.bumble.appyx.core.node.Node
|
||||
import com.bumble.appyx.core.node.node
|
||||
import io.element.android.compound.theme.ElementTheme
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||
|
||||
/**
|
||||
* Ref: https://www.figma.com/design/0MMNu7cTOzLOlWb7ctTkv3/Element-X?node-id=1518-85323
|
||||
*/
|
||||
fun emptyNode(
|
||||
buildContext: BuildContext,
|
||||
): Node = node(buildContext) { modifier ->
|
||||
EmptyView(modifier)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun EmptyView(
|
||||
modifier: Modifier = Modifier,
|
||||
) = Box(
|
||||
modifier = modifier
|
||||
.fillMaxSize()
|
||||
.background(ElementTheme.colors.bgCanvasDefault),
|
||||
)
|
||||
|
||||
@PreviewsDayNight
|
||||
@Composable
|
||||
internal fun EmptyViewPreview() = ElementPreview {
|
||||
EmptyView(Modifier)
|
||||
}
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
/*
|
||||
* 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.tests.konsist
|
||||
|
||||
import com.lemonappdev.konsist.api.Konsist
|
||||
import com.lemonappdev.konsist.api.ext.list.withAnnotationOf
|
||||
import com.lemonappdev.konsist.api.ext.list.withParameter
|
||||
import com.lemonappdev.konsist.api.verify.assertTrue
|
||||
import dev.zacsweers.metro.Assisted
|
||||
import dev.zacsweers.metro.Inject
|
||||
import org.junit.Test
|
||||
|
||||
class KonsistDiTest {
|
||||
@Test
|
||||
fun `class annotated with @Inject should not have constructors with @Assisted parameter`() {
|
||||
Konsist
|
||||
.scopeFromProject()
|
||||
.classes()
|
||||
.withAnnotationOf(Inject::class)
|
||||
.assertTrue(
|
||||
additionalMessage = "Class with @Assisted parameter in constructor should be annotated with @AssistedInject and not @Inject"
|
||||
) { classDeclaration ->
|
||||
classDeclaration.constructors
|
||||
.withParameter { parameterDeclaration ->
|
||||
parameterDeclaration.hasAnnotationOf(Assisted::class)
|
||||
}
|
||||
.isEmpty()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:d345035d1ee105f7fa1702a3f953df99b0bfce2b0cf48cc92e6248d80690e4b5
|
||||
size 14582
|
||||
oid sha256:537781aec261b622c47345b48e8fbfd68e3cf0817bb36bf0e34e83769d6d6d21
|
||||
size 24337
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:15b1f5afddbd729f1ec01326280b11e00f2b7c84e816bd980eac96dc61db92f2
|
||||
size 13970
|
||||
oid sha256:9e5bf90db9ac805e4054c8d9c759181768a61f13bd30092bdf98fe0a962144f8
|
||||
size 23678
|
||||
|
|
|
|||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:2eecd4b52a31208444d14b5017e42fc31df0391bb0ead311c01c9359a7e42f03
|
||||
size 74367
|
||||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:17d9ed21a98412af097ec5f9a209ecfeb33de2c94a3aec2afc1443469106a4f9
|
||||
size 38451
|
||||
oid sha256:f0d95c8515dfe38a3de02fb68a035e41bedac03abadae44e371a85e6f32c9174
|
||||
size 38460
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:78507911cefb2a6c7ab9b0ddf1a7a55bc4c13adadbd3a60075ca7eb89969a8b7
|
||||
size 32351
|
||||
oid sha256:74ecd0909c13895cd8d2e3827fb05fba98bd61f315735b281fd5502a9ae02bd2
|
||||
size 32354
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:541458c40f7b0e7c1dd18ba9211564fb053c264e965a7aaaede4e39c11a6420c
|
||||
size 37505
|
||||
oid sha256:18e41a828de1e73f0909449c096bc7d9660e5b677075845657267db731b913ab
|
||||
size 37597
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:f268e84b9d477429d817fc4b80ff99fbaff45434e236bae0ce05d656b4337675
|
||||
size 31620
|
||||
oid sha256:6242746ae0ba35d6e2e4a9c1d4c2a5e9433015eb7711767342afe4386edecfa1
|
||||
size 31701
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:84870e3b5554f97ff8093286bbf246b74b3bfa787a8318304740f55492809721
|
||||
size 12397
|
||||
oid sha256:43836f421a5f9ab68e4a3f6b3430a6d3daeec9f737bc1b20940a3fc9597057c3
|
||||
size 12451
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:72e7183662d01486545e6702faf22dd867cc47ed72a50b8cd2c24a307cef58c9
|
||||
size 12408
|
||||
oid sha256:053fe3e5699e98280bd40008a26f92a64e34c0f3bda1a52434137745e1ed9cf2
|
||||
size 12360
|
||||
|
|
|
|||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:96a867cb12498cbdc97957bee07855dfaa13602baddaf933aff2b666ef4c7650
|
||||
size 3642
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:5bb36ccd718f3fec5b04f1bc812dc7718b5ea7fa4619c8b031466297a8d016fd
|
||||
size 3659
|
||||
Loading…
Add table
Add a link
Reference in a new issue