Merge pull request #67 from vector-im/feature/fga/fix_restoration

Feature/fga/fix restoration
This commit is contained in:
ganfra 2023-02-02 11:04:06 +01:00 committed by GitHub
commit e80c6a3b19
42 changed files with 343 additions and 237 deletions

View file

@ -171,7 +171,7 @@ dependencies {
// https://developer.android.com/studio/write/java8-support#library-desugaring-versions // https://developer.android.com/studio/write/java8-support#library-desugaring-versions
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:1.2.2") coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:1.2.2")
implementation(libs.appyx.core) implementation(libs.appyx.core)
implementation(libs.androidx.splash)
implementation(libs.androidx.corektx) implementation(libs.androidx.corektx)
implementation(libs.androidx.lifecycle.runtime) implementation(libs.androidx.lifecycle.runtime)
implementation(libs.androidx.activity.compose) implementation(libs.androidx.activity.compose)

View file

@ -31,7 +31,8 @@
tools:targetApi="33"> tools:targetApi="33">
<activity <activity
android:name=".MainActivity" android:name=".MainActivity"
android:configChanges="orientation|screenSize|screenLayout|keyboardHidden" android:theme="@style/Theme.ElementX.Splash"
android:configChanges="orientation|screenSize|screenLayout|keyboardHidden|uiMode"
android:exported="true" android:exported="true"
android:windowSoftInputMode="adjustResize" android:windowSoftInputMode="adjustResize"
tools:ignore="LockedOrientationActivity"> tools:ignore="LockedOrientationActivity">

View file

@ -22,6 +22,7 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface import androidx.compose.material3.Surface
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.core.view.WindowCompat import androidx.core.view.WindowCompat
import com.bumble.appyx.core.integration.NodeHost import com.bumble.appyx.core.integration.NodeHost
import com.bumble.appyx.core.integrationpoint.NodeComponentActivity import com.bumble.appyx.core.integrationpoint.NodeComponentActivity
@ -34,8 +35,10 @@ import io.element.android.x.node.RootFlowNode
class MainActivity : NodeComponentActivity() { class MainActivity : NodeComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
installSplashScreen()
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
val appBindings = bindings<AppBindings>() val appBindings = bindings<AppBindings>()
appBindings.matrixClientsHolder().restore(savedInstanceState)
WindowCompat.setDecorFitsSystemWindows(window, false) WindowCompat.setDecorFitsSystemWindows(window, false)
setContent { setContent {
ElementXTheme { ElementXTheme {
@ -48,11 +51,17 @@ class MainActivity : NodeComponentActivity() {
buildContext = it, buildContext = it,
appComponentOwner = applicationContext as DaggerComponentOwner, appComponentOwner = applicationContext as DaggerComponentOwner,
authenticationService = appBindings.authenticationService(), authenticationService = appBindings.authenticationService(),
presenter = appBindings.rootPresenter() presenter = appBindings.rootPresenter(),
matrixClientsHolder = appBindings.matrixClientsHolder()
) )
} }
} }
} }
} }
} }
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
bindings<AppBindings>().matrixClientsHolder().onSaveInstanceState(outState)
}
} }

View file

@ -24,7 +24,7 @@ import kotlinx.coroutines.CoroutineScope
@ContributesTo(AppScope::class) @ContributesTo(AppScope::class)
interface AppBindings { interface AppBindings {
fun coroutineScope(): CoroutineScope
fun rootPresenter(): RootPresenter fun rootPresenter(): RootPresenter
fun authenticationService(): MatrixAuthenticationService fun authenticationService(): MatrixAuthenticationService
fun matrixClientsHolder(): MatrixClientsHolder
} }

View file

@ -0,0 +1,79 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.x.di
import android.os.Bundle
import io.element.android.libraries.di.AppScope
import io.element.android.libraries.di.SingleIn
import io.element.android.libraries.matrix.MatrixClient
import io.element.android.libraries.matrix.auth.MatrixAuthenticationService
import io.element.android.libraries.matrix.core.SessionId
import kotlinx.coroutines.runBlocking
import timber.log.Timber
import java.util.concurrent.ConcurrentHashMap
import javax.inject.Inject
private const val SAVE_INSTANCE_KEY = "io.element.android.x.di.MatrixClientsHolder.SaveInstanceKey"
@SingleIn(AppScope::class)
class MatrixClientsHolder @Inject constructor(private val authenticationService: MatrixAuthenticationService) {
private val sessionIdsToMatrixClient = ConcurrentHashMap<SessionId, MatrixClient>()
fun add(matrixClient: MatrixClient) {
sessionIdsToMatrixClient[matrixClient.sessionId] = matrixClient
}
fun removeAll() {
sessionIdsToMatrixClient.clear()
}
fun remove(sessionId: SessionId) {
sessionIdsToMatrixClient.remove(sessionId)
}
fun isEmpty(): Boolean = sessionIdsToMatrixClient.isEmpty()
fun knowSession(sessionId: SessionId): Boolean = sessionIdsToMatrixClient.containsKey(sessionId)
fun getOrNull(sessionId: SessionId): MatrixClient? {
return sessionIdsToMatrixClient[sessionId]
}
@Suppress("DEPRECATION")
fun restore(savedInstanceState: Bundle?) {
if (savedInstanceState == null || sessionIdsToMatrixClient.isNotEmpty()) return
val sessionIds = savedInstanceState.getSerializable(SAVE_INSTANCE_KEY) as? Array<SessionId>
if (sessionIds.isNullOrEmpty()) return
// Not ideal but should only happens in case of process recreation. This ensure we restore all the active sessions before restoring the node graphs.
runBlocking {
sessionIds.forEach { sessionId ->
Timber.v("Restore matrix session: $sessionId")
val matrixClient = authenticationService.restoreSession(sessionId)
if (matrixClient != null) {
add(matrixClient)
}
}
}
}
fun onSaveInstanceState(outState: Bundle) {
val sessionKeys = sessionIdsToMatrixClient.keys.toTypedArray()
Timber.v("Save matrix session keys = $sessionKeys")
outState.putSerializable(SAVE_INSTANCE_KEY, sessionKeys)
}
}

View file

@ -0,0 +1,31 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.x.node
import com.bumble.appyx.navmodel.backstack.BackStack
import com.bumble.appyx.navmodel.backstack.operation.NewRoot
/**
* Don't process NewRoot if the nav target already exists in the stack.
*/
fun <T : Any> BackStack<T>.safeRoot(element: T) {
val containsRoot = elements.value.any {
it.key.navTarget == element
}
if (containsRoot) return
accept(NewRoot(element))
}

View file

@ -21,13 +21,10 @@ import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import com.bumble.appyx.core.composable.Children import com.bumble.appyx.core.composable.Children
import com.bumble.appyx.core.lifecycle.subscribe
import com.bumble.appyx.core.modality.BuildContext 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.node.ParentNode import com.bumble.appyx.core.node.ParentNode
@ -39,11 +36,10 @@ import com.bumble.appyx.navmodel.backstack.operation.push
import io.element.android.features.rageshake.bugreport.BugReportNode import io.element.android.features.rageshake.bugreport.BugReportNode
import io.element.android.libraries.architecture.animation.rememberDefaultTransitionHandler import io.element.android.libraries.architecture.animation.rememberDefaultTransitionHandler
import io.element.android.libraries.architecture.createNode import io.element.android.libraries.architecture.createNode
import io.element.android.libraries.architecture.presenterConnector
import io.element.android.libraries.di.DaggerComponentOwner import io.element.android.libraries.di.DaggerComponentOwner
import io.element.android.libraries.matrix.MatrixClient
import io.element.android.libraries.matrix.auth.MatrixAuthenticationService import io.element.android.libraries.matrix.auth.MatrixAuthenticationService
import io.element.android.libraries.matrix.core.SessionId import io.element.android.libraries.matrix.core.SessionId
import io.element.android.x.di.MatrixClientsHolder
import io.element.android.x.root.RootPresenter import io.element.android.x.root.RootPresenter
import io.element.android.x.root.RootView import io.element.android.x.root.RootView
import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.distinctUntilChanged
@ -51,61 +47,86 @@ import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
import timber.log.Timber import timber.log.Timber
import java.util.concurrent.ConcurrentHashMap
class RootFlowNode( class RootFlowNode(
buildContext: BuildContext, private val buildContext: BuildContext,
private val backstack: BackStack<NavTarget> = BackStack( private val backstack: BackStack<NavTarget> = BackStack(
initialElement = NavTarget.SplashScreen, initialElement = NavTarget.SplashScreen,
savedStateMap = buildContext.savedStateMap, savedStateMap = buildContext.savedStateMap,
), ),
private val appComponentOwner: DaggerComponentOwner, private val appComponentOwner: DaggerComponentOwner,
private val authenticationService: MatrixAuthenticationService, private val authenticationService: MatrixAuthenticationService,
presenter: RootPresenter private val matrixClientsHolder: MatrixClientsHolder,
private val presenter: RootPresenter
) : ) :
ParentNode<RootFlowNode.NavTarget>( ParentNode<RootFlowNode.NavTarget>(
navModel = backstack, navModel = backstack,
buildContext = buildContext, buildContext = buildContext
), ),
DaggerComponentOwner by appComponentOwner { DaggerComponentOwner by appComponentOwner {
private val matrixClientsHolder = ConcurrentHashMap<SessionId, MatrixClient>()
private val presenterConnector = presenterConnector(presenter)
override fun onBuilt() { override fun onBuilt() {
super.onBuilt() super.onBuilt()
whenChildAttached(LoggedInFlowNode::class) { _, child -> observeLoggedInState()
child.lifecycle.subscribe(
onDestroy = { matrixClientsHolder.remove(child.sessionId) }
)
} }
private fun observeLoggedInState() {
authenticationService.isLoggedIn() authenticationService.isLoggedIn()
.distinctUntilChanged() .distinctUntilChanged()
.onEach { isLoggedIn -> .onEach { isLoggedIn ->
Timber.v("isLoggedIn=$isLoggedIn") Timber.v("isLoggedIn=$isLoggedIn")
if (isLoggedIn) { if (isLoggedIn) {
val matrixClient = authenticationService.restoreSession() tryToRestoreLatestSession(
if (matrixClient == null) { onSuccess = { switchToLoggedInFlow(it) },
backstack.newRoot(NavTarget.NotLoggedInFlow) onFailure = { switchToLogoutFlow() }
)
} else { } else {
matrixClientsHolder[matrixClient.sessionId] = matrixClient switchToLogoutFlow()
backstack.newRoot(NavTarget.LoggedInFlow(matrixClient.sessionId))
}
} else {
backstack.newRoot(NavTarget.NotLoggedInFlow)
} }
} }
.launchIn(lifecycleScope) .launchIn(lifecycleScope)
} }
private fun switchToLoggedInFlow(sessionId: SessionId) {
backstack.safeRoot(NavTarget.LoggedInFlow(sessionId = sessionId))
}
private fun switchToLogoutFlow() {
matrixClientsHolder.removeAll()
backstack.safeRoot(NavTarget.NotLoggedInFlow)
}
private suspend fun tryToRestoreLatestSession(
onSuccess: (SessionId) -> Unit = {},
onFailure: () -> Unit = {}
) {
val latestKnownSessionId = authenticationService.getLatestSessionId()
if (latestKnownSessionId == null) {
onFailure()
return
}
if (matrixClientsHolder.knowSession(latestKnownSessionId)) {
onSuccess(latestKnownSessionId)
return
}
val matrixClient = authenticationService.restoreSession(latestKnownSessionId)
if (matrixClient == null) {
Timber.v("Failed to restore session...")
onFailure()
} else {
matrixClientsHolder.add(matrixClient)
onSuccess(matrixClient.sessionId)
}
}
private fun onOpenBugReport() { private fun onOpenBugReport() {
backstack.push(NavTarget.BugReport) backstack.push(NavTarget.BugReport)
} }
@Composable @Composable
override fun View(modifier: Modifier) { override fun View(modifier: Modifier) {
val state by presenterConnector.stateFlow.collectAsState() val state = presenter.present()
RootView( RootView(
state = state, state = state,
modifier = modifier, modifier = modifier,
@ -142,8 +163,10 @@ class RootFlowNode(
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node { override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
return when (navTarget) { return when (navTarget) {
is NavTarget.LoggedInFlow -> { is NavTarget.LoggedInFlow -> {
val matrixClient = val matrixClient = matrixClientsHolder.getOrNull(navTarget.sessionId) ?: return splashNode(buildContext).also {
matrixClientsHolder[navTarget.sessionId] ?: throw IllegalStateException("Makes sure to give a matrixClient with the given sessionId") Timber.w("Couldn't find any session, go through SplashScreen")
backstack.newRoot(NavTarget.SplashScreen)
}
LoggedInFlowNode( LoggedInFlowNode(
buildContext = buildContext, buildContext = buildContext,
sessionId = navTarget.sessionId, sessionId = navTarget.sessionId,
@ -152,12 +175,14 @@ class RootFlowNode(
) )
} }
NavTarget.NotLoggedInFlow -> NotLoggedInFlowNode(buildContext) NavTarget.NotLoggedInFlow -> NotLoggedInFlowNode(buildContext)
NavTarget.SplashScreen -> node(buildContext) { NavTarget.SplashScreen -> splashNode(buildContext)
NavTarget.BugReport -> createNode<BugReportNode>(buildContext, plugins = listOf(bugReportNodeCallback))
}
}
private fun splashNode(buildContext: BuildContext) = node(buildContext) {
Box(modifier = it.fillMaxSize(), contentAlignment = Alignment.Center) { Box(modifier = it.fillMaxSize(), contentAlignment = Alignment.Center) {
CircularProgressIndicator() CircularProgressIndicator()
} }
} }
NavTarget.BugReport -> createNode<BugReportNode>(buildContext, plugins = listOf(bugReportNodeCallback))
}
}
} }

View file

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<inset
xmlns:android="http://schemas.android.com/apk/res/android"
android:drawable="@mipmap/ic_launcher_round"
android:insetTop="80dp"
android:insetRight="80dp"
android:insetBottom="80dp"
android:insetLeft="80dp" />

View file

@ -0,0 +1,26 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ Copyright (c) 2023 New Vector Ltd
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<resources>
<style name="Theme.ElementX.Splash" parent="Theme.SplashScreen">
<item name="windowSplashScreenBackground">@color/black</item>
<item name="windowSplashScreenAnimatedIcon">@drawable/splash_icon</item>
<item name="postSplashScreenTheme">@style/Theme.ElementX</item>
</style>
<style name="Theme.ElementX" parent="Theme.ElementAndroid" />
</resources>

View file

@ -16,6 +16,10 @@
--> -->
<resources> <resources>
<style name="Theme.ElementX.Splash" parent="Theme.SplashScreen.IconBackground">
<item name="windowSplashScreenBackground">@color/white</item>
<item name="windowSplashScreenAnimatedIcon">@drawable/splash_icon</item>
<item name="postSplashScreenTheme">@style/Theme.ElementX</item>
</style>
<style name="Theme.ElementX" parent="Theme.ElementAndroid" /> <style name="Theme.ElementX" parent="Theme.ElementAndroid" />
</resources> </resources>

View file

@ -17,8 +17,6 @@
package io.element.android.features.login.changeserver package io.element.android.features.login.changeserver
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import com.bumble.appyx.core.modality.BuildContext import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node import com.bumble.appyx.core.node.Node
@ -26,25 +24,22 @@ import com.bumble.appyx.core.plugin.Plugin
import dagger.assisted.Assisted import dagger.assisted.Assisted
import dagger.assisted.AssistedInject import dagger.assisted.AssistedInject
import io.element.android.anvilannotations.ContributesNode import io.element.android.anvilannotations.ContributesNode
import io.element.android.libraries.architecture.presenterConnector
import io.element.android.libraries.di.AppScope import io.element.android.libraries.di.AppScope
@ContributesNode(AppScope::class) @ContributesNode(AppScope::class)
class ChangeServerNode @AssistedInject constructor( class ChangeServerNode @AssistedInject constructor(
@Assisted buildContext: BuildContext, @Assisted buildContext: BuildContext,
@Assisted plugins: List<Plugin>, @Assisted plugins: List<Plugin>,
presenter: ChangeServerPresenter, private val presenter: ChangeServerPresenter,
) : Node(buildContext, plugins = plugins) { ) : Node(buildContext, plugins = plugins) {
private val presenterConnector = presenterConnector(presenter)
private fun onSuccess() { private fun onSuccess() {
navigateUp() navigateUp()
} }
@Composable @Composable
override fun View(modifier: Modifier) { override fun View(modifier: Modifier) {
val state by presenterConnector.stateFlow.collectAsState() val state = presenter.present()
ChangeServerView( ChangeServerView(
state = state, state = state,
modifier = modifier, modifier = modifier,

View file

@ -17,8 +17,6 @@
package io.element.android.features.login.root package io.element.android.features.login.root
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle
import com.bumble.appyx.core.modality.BuildContext import com.bumble.appyx.core.modality.BuildContext
@ -28,7 +26,6 @@ import com.bumble.appyx.core.plugin.plugins
import dagger.assisted.Assisted import dagger.assisted.Assisted
import dagger.assisted.AssistedInject import dagger.assisted.AssistedInject
import io.element.android.anvilannotations.ContributesNode import io.element.android.anvilannotations.ContributesNode
import io.element.android.libraries.architecture.presenterConnector
import io.element.android.libraries.designsystem.utils.OnLifecycleEvent import io.element.android.libraries.designsystem.utils.OnLifecycleEvent
import io.element.android.libraries.di.AppScope import io.element.android.libraries.di.AppScope
@ -36,11 +33,9 @@ import io.element.android.libraries.di.AppScope
class LoginRootNode @AssistedInject constructor( class LoginRootNode @AssistedInject constructor(
@Assisted buildContext: BuildContext, @Assisted buildContext: BuildContext,
@Assisted plugins: List<Plugin>, @Assisted plugins: List<Plugin>,
presenter: LoginRootPresenter, private val presenter: LoginRootPresenter,
) : Node(buildContext, plugins = plugins) { ) : Node(buildContext, plugins = plugins) {
private val presenterConnector = presenterConnector(presenter)
interface Callback : Plugin { interface Callback : Plugin {
fun onChangeHomeServer() fun onChangeHomeServer()
} }
@ -51,7 +46,7 @@ class LoginRootNode @AssistedInject constructor(
@Composable @Composable
override fun View(modifier: Modifier) { override fun View(modifier: Modifier) {
val state by presenterConnector.stateFlow.collectAsState() val state = presenter.present()
OnLifecycleEvent { _, event -> OnLifecycleEvent { _, event ->
when (event) { when (event) {
Lifecycle.Event.ON_RESUME -> state.eventSink(LoginRootEvents.RefreshHomeServer) Lifecycle.Event.ON_RESUME -> state.eventSink(LoginRootEvents.RefreshHomeServer)

View file

@ -17,8 +17,6 @@
package io.element.android.features.messages package io.element.android.features.messages
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import com.bumble.appyx.core.modality.BuildContext import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node import com.bumble.appyx.core.node.Node
@ -26,21 +24,18 @@ import com.bumble.appyx.core.plugin.Plugin
import dagger.assisted.Assisted import dagger.assisted.Assisted
import dagger.assisted.AssistedInject import dagger.assisted.AssistedInject
import io.element.android.anvilannotations.ContributesNode import io.element.android.anvilannotations.ContributesNode
import io.element.android.libraries.architecture.presenterConnector
import io.element.android.libraries.di.RoomScope import io.element.android.libraries.di.RoomScope
@ContributesNode(RoomScope::class) @ContributesNode(RoomScope::class)
class MessagesNode @AssistedInject constructor( class MessagesNode @AssistedInject constructor(
@Assisted buildContext: BuildContext, @Assisted buildContext: BuildContext,
@Assisted plugins: List<Plugin>, @Assisted plugins: List<Plugin>,
presenter: MessagesPresenter, private val presenter: MessagesPresenter,
) : Node(buildContext, plugins = plugins) { ) : Node(buildContext, plugins = plugins) {
private val connector = presenterConnector(presenter)
@Composable @Composable
override fun View(modifier: Modifier) { override fun View(modifier: Modifier) {
val state by connector.stateFlow.collectAsState() val state = presenter.present()
MessagesView( MessagesView(
state = state, state = state,
onBackPressed = this::navigateUp, onBackPressed = this::navigateUp,

View file

@ -36,9 +36,7 @@ import io.element.android.features.messages.timeline.model.content.TimelineItemT
import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.designsystem.components.avatar.AvatarData 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.AvatarSize
import io.element.android.libraries.matrix.MatrixClient
import io.element.android.libraries.matrix.room.MatrixRoom import io.element.android.libraries.matrix.room.MatrixRoom
import io.element.android.libraries.matrix.ui.MatrixItemHelper
import io.element.android.libraries.textcomposer.MessageComposerMode import io.element.android.libraries.textcomposer.MessageComposerMode
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -46,15 +44,12 @@ import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
class MessagesPresenter @Inject constructor( class MessagesPresenter @Inject constructor(
private val matrixClient: MatrixClient,
private val room: MatrixRoom, private val room: MatrixRoom,
private val composerPresenter: MessageComposerPresenter, private val composerPresenter: MessageComposerPresenter,
private val timelinePresenter: TimelinePresenter, private val timelinePresenter: TimelinePresenter,
private val actionListPresenter: ActionListPresenter, private val actionListPresenter: ActionListPresenter,
) : Presenter<MessagesState> { ) : Presenter<MessagesState> {
private val matrixItemHelper = MatrixItemHelper(matrixClient)
@Composable @Composable
override fun present(): MessagesState { override fun present(): MessagesState {
val localCoroutineScope = rememberCoroutineScope() val localCoroutineScope = rememberCoroutineScope()
@ -71,8 +66,9 @@ class MessagesPresenter @Inject constructor(
} }
LaunchedEffect(syncUpdateFlow) { LaunchedEffect(syncUpdateFlow) {
roomAvatar.value = roomAvatar.value =
matrixItemHelper.loadAvatarData( AvatarData(
room = room, name = room.bestName,
url = room.avatarUrl,
size = AvatarSize.SMALL size = AvatarSize.SMALL
) )
roomName.value = room.name roomName.value = room.name

View file

@ -20,6 +20,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.core.data.StableCharSequence import io.element.android.libraries.core.data.StableCharSequence
@ -40,7 +41,7 @@ class MessageComposerPresenter @Inject constructor(
val isFullScreen = rememberSaveable { val isFullScreen = rememberSaveable {
mutableStateOf(false) mutableStateOf(false)
} }
val text: MutableState<StableCharSequence> = rememberSaveable { val text: MutableState<StableCharSequence> = remember {
mutableStateOf(StableCharSequence("")) mutableStateOf(StableCharSequence(""))
} }
val composerMode: MutableState<MessageComposerMode> = rememberSaveable { val composerMode: MutableState<MessageComposerMode> = rememberSaveable {

View file

@ -32,6 +32,7 @@ import io.element.android.features.messages.timeline.model.content.TimelineItemR
import io.element.android.features.messages.timeline.model.content.TimelineItemTextContent import io.element.android.features.messages.timeline.model.content.TimelineItemTextContent
import io.element.android.features.messages.timeline.model.content.TimelineItemUnknownContent import io.element.android.features.messages.timeline.model.content.TimelineItemUnknownContent
import io.element.android.features.messages.timeline.util.invalidateLast import io.element.android.features.messages.timeline.util.invalidateLast
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.AvatarSize
import io.element.android.libraries.matrix.core.EventId import io.element.android.libraries.matrix.core.EventId
import io.element.android.libraries.matrix.media.MediaResolver import io.element.android.libraries.matrix.media.MediaResolver
@ -154,8 +155,7 @@ class TimelineItemsFactory @Inject constructor(
computeGroupPosition(currentTimelineItem, timelineItems, index) computeGroupPosition(currentTimelineItem, timelineItems, index)
val senderDisplayName = room.userDisplayName(currentSender).getOrNull() val senderDisplayName = room.userDisplayName(currentSender).getOrNull()
val senderAvatarUrl = room.userAvatarUrl(currentSender).getOrNull() val senderAvatarUrl = room.userAvatarUrl(currentSender).getOrNull()
val senderAvatarData = val senderAvatarData = AvatarData(
matrixItemHelper.loadAvatarData(
name = senderDisplayName ?: currentSender, name = senderDisplayName ?: currentSender,
url = senderAvatarUrl, url = senderAvatarUrl,
size = AvatarSize.SMALL size = AvatarSize.SMALL

View file

@ -17,8 +17,6 @@
package io.element.android.features.preferences.root package io.element.android.features.preferences.root
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import com.bumble.appyx.core.modality.BuildContext import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node import com.bumble.appyx.core.node.Node
@ -27,29 +25,26 @@ import com.bumble.appyx.core.plugin.plugins
import dagger.assisted.Assisted import dagger.assisted.Assisted
import dagger.assisted.AssistedInject import dagger.assisted.AssistedInject
import io.element.android.anvilannotations.ContributesNode import io.element.android.anvilannotations.ContributesNode
import io.element.android.libraries.architecture.presenterConnector
import io.element.android.libraries.di.SessionScope import io.element.android.libraries.di.SessionScope
@ContributesNode(SessionScope::class) @ContributesNode(SessionScope::class)
class PreferencesRootNode @AssistedInject constructor( class PreferencesRootNode @AssistedInject constructor(
@Assisted buildContext: BuildContext, @Assisted buildContext: BuildContext,
@Assisted plugins: List<Plugin>, @Assisted plugins: List<Plugin>,
presenter: PreferencesRootPresenter, private val presenter: PreferencesRootPresenter,
) : Node(buildContext, plugins = plugins) { ) : Node(buildContext, plugins = plugins) {
interface Callback : Plugin { interface Callback : Plugin {
fun onOpenBugReport() fun onOpenBugReport()
} }
private val presenterConnector = presenterConnector(presenter)
private fun onOpenBugReport() { private fun onOpenBugReport() {
plugins<Callback>().forEach { it.onOpenBugReport() } plugins<Callback>().forEach { it.onOpenBugReport() }
} }
@Composable @Composable
override fun View(modifier: Modifier) { override fun View(modifier: Modifier) {
val state by presenterConnector.stateFlow.collectAsState() val state = presenter.present()
PreferencesRootView( PreferencesRootView(
state = state, state = state,
modifier = modifier, modifier = modifier,

View file

@ -17,8 +17,6 @@
package io.element.android.features.rageshake.bugreport package io.element.android.features.rageshake.bugreport
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import com.bumble.appyx.core.modality.BuildContext import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node import com.bumble.appyx.core.node.Node
@ -27,25 +25,22 @@ import com.bumble.appyx.core.plugin.plugins
import dagger.assisted.Assisted import dagger.assisted.Assisted
import dagger.assisted.AssistedInject import dagger.assisted.AssistedInject
import io.element.android.anvilannotations.ContributesNode import io.element.android.anvilannotations.ContributesNode
import io.element.android.libraries.architecture.presenterConnector
import io.element.android.libraries.di.AppScope import io.element.android.libraries.di.AppScope
@ContributesNode(AppScope::class) @ContributesNode(AppScope::class)
class BugReportNode @AssistedInject constructor( class BugReportNode @AssistedInject constructor(
@Assisted buildContext: BuildContext, @Assisted buildContext: BuildContext,
@Assisted plugins: List<Plugin>, @Assisted plugins: List<Plugin>,
presenter: BugReportPresenter, private val presenter: BugReportPresenter,
) : Node(buildContext, plugins = plugins) { ) : Node(buildContext, plugins = plugins) {
private val presenterConnector = presenterConnector(presenter)
interface Callback : Plugin { interface Callback : Plugin {
fun onBugReportSent() fun onBugReportSent()
} }
@Composable @Composable
override fun View(modifier: Modifier) { override fun View(modifier: Modifier) {
val state by presenterConnector.stateFlow.collectAsState() val state = presenter.present()
BugReportView( BugReportView(
state = state, state = state,
modifier = modifier, modifier = modifier,

View file

@ -17,8 +17,6 @@
package io.element.android.features.roomlist package io.element.android.features.roomlist
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import com.bumble.appyx.core.modality.BuildContext import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node import com.bumble.appyx.core.node.Node
@ -27,7 +25,6 @@ import com.bumble.appyx.core.plugin.plugins
import dagger.assisted.Assisted import dagger.assisted.Assisted
import dagger.assisted.AssistedInject import dagger.assisted.AssistedInject
import io.element.android.anvilannotations.ContributesNode import io.element.android.anvilannotations.ContributesNode
import io.element.android.libraries.architecture.presenterConnector
import io.element.android.libraries.di.SessionScope import io.element.android.libraries.di.SessionScope
import io.element.android.libraries.matrix.core.RoomId import io.element.android.libraries.matrix.core.RoomId
@ -35,7 +32,7 @@ import io.element.android.libraries.matrix.core.RoomId
class RoomListNode @AssistedInject constructor( class RoomListNode @AssistedInject constructor(
@Assisted buildContext: BuildContext, @Assisted buildContext: BuildContext,
@Assisted plugins: List<Plugin>, @Assisted plugins: List<Plugin>,
presenter: RoomListPresenter, private val presenter: RoomListPresenter,
) : Node(buildContext, plugins = plugins) { ) : Node(buildContext, plugins = plugins) {
interface Callback : Plugin { interface Callback : Plugin {
@ -43,8 +40,6 @@ class RoomListNode @AssistedInject constructor(
fun onSettingsClicked() fun onSettingsClicked()
} }
private val connector = presenterConnector(presenter)
private fun onRoomClicked(roomId: RoomId) { private fun onRoomClicked(roomId: RoomId) {
plugins<Callback>().forEach { it.onRoomClicked(roomId) } plugins<Callback>().forEach { it.onRoomClicked(roomId) }
} }
@ -55,7 +50,7 @@ class RoomListNode @AssistedInject constructor(
@Composable @Composable
override fun View(modifier: Modifier) { override fun View(modifier: Modifier) {
val state by connector.stateFlow.collectAsState() val state = presenter.present()
RoomListView( RoomListView(
state = state, state = state,
modifier = modifier, modifier = modifier,

View file

@ -34,7 +34,6 @@ import io.element.android.libraries.core.coroutine.parallelMap
import io.element.android.libraries.designsystem.components.avatar.AvatarData 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.AvatarSize
import io.element.android.libraries.matrix.MatrixClient import io.element.android.libraries.matrix.MatrixClient
import io.element.android.libraries.matrix.media.MediaResolver
import io.element.android.libraries.matrix.room.RoomSummary import io.element.android.libraries.matrix.room.RoomSummary
import io.element.android.libraries.matrix.ui.model.MatrixUser import io.element.android.libraries.matrix.ui.model.MatrixUser
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
@ -42,6 +41,7 @@ import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
private const val extendedRangeSize = 40 private const val extendedRangeSize = 40
@ -61,7 +61,9 @@ class RoomListPresenter @Inject constructor(
val roomSummaries by client val roomSummaries by client
.roomSummaryDataSource() .roomSummaryDataSource()
.roomSummaries() .roomSummaries()
.collectAsState(initial = null) .collectAsState()
Timber.v("RoomSummaries size = ${roomSummaries.size}")
val filteredRoomSummaries: MutableState<ImmutableList<RoomListRoomSummary>> = remember { val filteredRoomSummaries: MutableState<ImmutableList<RoomListRoomSummary>> = remember {
mutableStateOf(persistentListOf()) mutableStateOf(persistentListOf())
@ -105,15 +107,14 @@ class RoomListPresenter @Inject constructor(
val userAvatarUrl = client.loadUserAvatarURLString().getOrNull() val userAvatarUrl = client.loadUserAvatarURLString().getOrNull()
val userDisplayName = client.loadUserDisplayName().getOrNull() val userDisplayName = client.loadUserDisplayName().getOrNull()
val avatarData = val avatarData =
loadAvatarData( AvatarData(
userDisplayName ?: client.userId().value, name = userDisplayName ?: client.userId().value,
userAvatarUrl, url = userAvatarUrl,
AvatarSize.SMALL size = AvatarSize.SMALL
) )
matrixUser.value = MatrixUser( matrixUser.value = MatrixUser(
id = client.userId(), id = client.userId(),
username = userDisplayName ?: client.userId().value, username = userDisplayName ?: client.userId().value,
avatarUrl = userAvatarUrl,
avatarData = avatarData, avatarData = avatarData,
) )
} }
@ -135,9 +136,9 @@ class RoomListPresenter @Inject constructor(
when (roomSummary) { when (roomSummary) {
is RoomSummary.Empty -> RoomListRoomSummaryPlaceholders.create(roomSummary.identifier) is RoomSummary.Empty -> RoomListRoomSummaryPlaceholders.create(roomSummary.identifier)
is RoomSummary.Filled -> { is RoomSummary.Filled -> {
val avatarData = loadAvatarData( val avatarData = AvatarData(
roomSummary.details.name, name = roomSummary.details.name,
roomSummary.details.avatarURLString url = roomSummary.details.avatarURLString
) )
RoomListRoomSummary( RoomListRoomSummary(
id = roomSummary.identifier(), id = roomSummary.identifier(),
@ -151,14 +152,4 @@ class RoomListPresenter @Inject constructor(
} }
} }
} }
private suspend fun loadAvatarData(
name: String,
url: String?,
size: AvatarSize = AvatarSize.MEDIUM
): AvatarData {
val model = client.mediaResolver()
.resolve(url, kind = MediaResolver.Kind.Thumbnail(size.value))
return AvatarData(name, model, size)
}
} }

View file

@ -17,8 +17,6 @@
package io.element.android.features.template package io.element.android.features.template
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import com.bumble.appyx.core.modality.BuildContext import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node import com.bumble.appyx.core.node.Node
@ -26,7 +24,6 @@ import com.bumble.appyx.core.plugin.Plugin
import dagger.assisted.Assisted import dagger.assisted.Assisted
import dagger.assisted.AssistedInject import dagger.assisted.AssistedInject
import io.element.android.anvilannotations.ContributesNode import io.element.android.anvilannotations.ContributesNode
import io.element.android.libraries.architecture.presenterConnector
import io.element.android.libraries.di.AppScope import io.element.android.libraries.di.AppScope
// TODO Change to use the right Scope for your feature. For now it can be AppScope, SessionScope or RoomScope // TODO Change to use the right Scope for your feature. For now it can be AppScope, SessionScope or RoomScope
@ -34,14 +31,12 @@ import io.element.android.libraries.di.AppScope
class TemplateNode @AssistedInject constructor( class TemplateNode @AssistedInject constructor(
@Assisted buildContext: BuildContext, @Assisted buildContext: BuildContext,
@Assisted plugins: List<Plugin>, @Assisted plugins: List<Plugin>,
presenter: TemplatePresenter, private val presenter: TemplatePresenter,
) : Node(buildContext, plugins = plugins) { ) : Node(buildContext, plugins = plugins) {
private val presenterConnector = presenterConnector(presenter)
@Composable @Composable
override fun View(modifier: Modifier) { override fun View(modifier: Modifier) {
val state by presenterConnector.stateFlow.collectAsState() val state = presenter.present()
TemplateView( TemplateView(
state = state, state = state,
modifier = modifier modifier = modifier

View file

@ -37,7 +37,7 @@ datetime = "0.4.0"
serialization_json = "1.4.1" serialization_json = "1.4.1"
showkase = "1.0.0-beta14" showkase = "1.0.0-beta14"
jsoup = "1.15.3" jsoup = "1.15.3"
appyx = "1.0.1" appyx = "1.0.3"
dependencycheck = "7.4.4" dependencycheck = "7.4.4"
stem = "2.2.3" stem = "2.2.3"
@ -66,6 +66,7 @@ androidx_recyclerview = { module = "androidx.recyclerview:recyclerview", version
androidx_lifecycle_runtime = { module = "androidx.lifecycle:lifecycle-runtime-ktx", version.ref = "lifecycle" } androidx_lifecycle_runtime = { module = "androidx.lifecycle:lifecycle-runtime-ktx", version.ref = "lifecycle" }
androidx_lifecycle_compose = { module = "androidx.lifecycle:compose", version.ref = "lifecycle" } androidx_lifecycle_compose = { module = "androidx.lifecycle:compose", version.ref = "lifecycle" }
androidx_lifecycle_process = { module = "androidx.lifecycle:lifecycle-process", version.ref = "lifecycle" } androidx_lifecycle_process = { module = "androidx.lifecycle:lifecycle-process", version.ref = "lifecycle" }
androidx_splash = "androidx.core:core-splashscreen:1.0.0"
androidx_activity_compose = { module = "androidx.activity:activity-compose", version.ref = "activity_compose" } androidx_activity_compose = { module = "androidx.activity:activity-compose", version.ref = "activity_compose" }
androidx_startup = { module = "androidx.startup:startup-runtime", version.ref = "startup" } androidx_startup = { module = "androidx.startup:startup-runtime", version.ref = "startup" }

View file

@ -28,6 +28,5 @@ dependencies {
api(projects.libraries.di) api(projects.libraries.di)
api(libs.dagger) api(libs.dagger)
api(libs.appyx.core) api(libs.appyx.core)
api(libs.molecule.runtime)
api(libs.androidx.lifecycle.runtime) api(libs.androidx.lifecycle.runtime)
} }

View file

@ -1,37 +0,0 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.libraries.architecture
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.lifecycleScope
import app.cash.molecule.AndroidUiDispatcher
import app.cash.molecule.RecompositionClock
import app.cash.molecule.launchMolecule
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.StateFlow
inline fun <reified State> LifecycleOwner.presenterConnector(presenter: Presenter<State>): LifecyclePresenterConnector<State> =
LifecyclePresenterConnector(lifecycleOwner = this, presenter = presenter)
class LifecyclePresenterConnector<State>(lifecycleOwner: LifecycleOwner, presenter: Presenter<State>) {
private val moleculeScope = CoroutineScope(lifecycleOwner.lifecycleScope.coroutineContext + AndroidUiDispatcher.Main)
val stateFlow: StateFlow<State> = moleculeScope.launchMolecule(RecompositionClock.Immediate) {
presenter.present()
}
}

View file

@ -41,7 +41,7 @@ fun Avatar(avatarData: AvatarData, modifier: Modifier = Modifier) {
val commonModifier = modifier val commonModifier = modifier
.size(avatarData.size.dp) .size(avatarData.size.dp)
.clip(CircleShape) .clip(CircleShape)
if (avatarData.model == null) { if (avatarData.url == null) {
InitialsAvatar( InitialsAvatar(
avatarData = avatarData, avatarData = avatarData,
modifier = commonModifier, modifier = commonModifier,
@ -60,7 +60,7 @@ private fun ImageAvatar(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
AsyncImage( AsyncImage(
model = avatarData.model, model = avatarData,
onError = { onError = {
Timber.e("TAG", "Error $it\n${it.result}", it.result.throwable) Timber.e("TAG", "Error $it\n${it.result}", it.result.throwable)
}, },

View file

@ -21,29 +21,6 @@ import androidx.compose.runtime.Immutable
@Immutable @Immutable
data class AvatarData( data class AvatarData(
val name: String = "", val name: String = "",
val model: ByteArray? = null, val url: String? = null,
val size: AvatarSize = AvatarSize.MEDIUM val size: AvatarSize = AvatarSize.MEDIUM
) { )
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as AvatarData
if (name != other.name) return false
if (model != null) {
if (other.model == null) return false
if (!model.contentEquals(other.model)) return false
} else if (other.model != null) return false
if (size != other.size) return false
return true
}
override fun hashCode(): Int {
var result = name.hashCode()
result = 31 * result + (model?.contentHashCode() ?: 0)
result = 31 * result + size.value
return result
}
}

View file

@ -23,7 +23,7 @@ import kotlinx.coroutines.flow.Flow
interface MatrixAuthenticationService { interface MatrixAuthenticationService {
fun isLoggedIn(): Flow<Boolean> fun isLoggedIn(): Flow<Boolean>
suspend fun getLatestSessionId(): SessionId? suspend fun getLatestSessionId(): SessionId?
suspend fun restoreSession(): MatrixClient? suspend fun restoreSession(sessionId: SessionId): MatrixClient?
fun getHomeserver(): String? fun getHomeserver(): String?
fun getHomeserverOrDefault(): String fun getHomeserverOrDefault(): String
suspend fun setHomeserver(homeserver: String) suspend fun setHomeserver(homeserver: String)

View file

@ -52,8 +52,8 @@ class RustMatrixAuthenticationService @Inject constructor(
sessionStore.getLatestSession()?.sessionId() sessionStore.getLatestSession()?.sessionId()
} }
override suspend fun restoreSession() = withContext(coroutineDispatchers.io) { override suspend fun restoreSession(sessionId: SessionId) = withContext(coroutineDispatchers.io) {
sessionStore.getLatestSession() sessionStore.getSession(sessionId)
?.let { session -> ?.let { session ->
try { try {
ClientBuilder() ClientBuilder()

View file

@ -29,7 +29,7 @@ interface MediaResolver {
} }
data class Meta( data class Meta(
val source: MediaSource, val source: MediaSource?,
val kind: Kind val kind: Kind
) )

View file

@ -28,6 +28,7 @@ internal class RustMediaResolver(private val client: MatrixClient) : MediaResolv
} }
override suspend fun resolve(meta: MediaResolver.Meta): ByteArray? { override suspend fun resolve(meta: MediaResolver.Meta): ByteArray? {
if (meta.source == null) return null
return when (meta.kind) { return when (meta.kind) {
is MediaResolver.Kind.Content -> client.loadMediaContentForSource(meta.source) is MediaResolver.Kind.Content -> client.loadMediaContentForSource(meta.source)
is MediaResolver.Kind.Thumbnail -> client.loadMediaThumbnailForSource( is MediaResolver.Kind.Thumbnail -> client.loadMediaThumbnailForSource(

View file

@ -26,6 +26,7 @@ import kotlinx.coroutines.cancel
import kotlinx.coroutines.cancelChildren import kotlinx.coroutines.cancelChildren
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.sample import kotlinx.coroutines.flow.sample
@ -43,7 +44,7 @@ import java.util.Collections
import java.util.UUID import java.util.UUID
interface RoomSummaryDataSource { interface RoomSummaryDataSource {
fun roomSummaries(): Flow<List<RoomSummary>> fun roomSummaries(): StateFlow<List<RoomSummary>>
fun setSlidingSyncRange(range: IntRange) fun setSlidingSyncRange(range: IntRange)
} }
@ -98,9 +99,9 @@ internal class RustRoomSummaryDataSource(
coroutineScope.cancel() coroutineScope.cancel()
} }
@OptIn(FlowPreview::class) //@OptIn(FlowPreview::class)
override fun roomSummaries(): Flow<List<RoomSummary>> { override fun roomSummaries(): StateFlow<List<RoomSummary>> {
return roomSummaries.sample(50) return roomSummaries
} }
override fun setSlidingSyncRange(range: IntRange) { override fun setSlidingSyncRange(range: IntRange) {

View file

@ -26,6 +26,7 @@ import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.libraries.di.AppScope import io.element.android.libraries.di.AppScope
import io.element.android.libraries.di.ApplicationContext import io.element.android.libraries.di.ApplicationContext
import io.element.android.libraries.di.SingleIn import io.element.android.libraries.di.SingleIn
import io.element.android.libraries.matrix.core.SessionId
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
@ -94,6 +95,11 @@ class PreferencesSessionStore @Inject constructor(
} }
} }
override suspend fun getSession(sessionId: SessionId): Session? {
//TODO we should have a proper session management
return getLatestSession()
}
override suspend fun reset() { override suspend fun reset() {
store.edit { it.clear() } store.edit { it.clear() }
} }

View file

@ -16,12 +16,14 @@
package io.element.android.libraries.matrix.session package io.element.android.libraries.matrix.session
import io.element.android.libraries.matrix.core.SessionId
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import org.matrix.rustcomponents.sdk.Session import org.matrix.rustcomponents.sdk.Session
interface SessionStore { interface SessionStore {
fun isLoggedIn(): Flow<Boolean> fun isLoggedIn(): Flow<Boolean>
suspend fun storeData(session: Session) suspend fun storeData(session: Session)
suspend fun getSession(sessionId: SessionId): Session?
suspend fun getLatestSession(): Session? suspend fun getLatestSession(): Session?
suspend fun reset() suspend fun reset()
} }

View file

@ -18,13 +18,13 @@ package io.element.android.libraries.matrixtest.room
import io.element.android.libraries.matrix.room.RoomSummary import io.element.android.libraries.matrix.room.RoomSummary
import io.element.android.libraries.matrix.room.RoomSummaryDataSource import io.element.android.libraries.matrix.room.RoomSummaryDataSource
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.flow.StateFlow
class InMemoryRoomSummaryDataSource : RoomSummaryDataSource { class InMemoryRoomSummaryDataSource : RoomSummaryDataSource {
override fun roomSummaries(): Flow<List<RoomSummary>> { override fun roomSummaries(): StateFlow<List<RoomSummary>> {
return emptyFlow() return MutableStateFlow(emptyList())
} }
override fun setSlidingSyncRange(range: IntRange) = Unit override fun setSlidingSyncRange(range: IntRange) = Unit

View file

@ -19,9 +19,6 @@ package io.element.android.libraries.matrix.ui
import io.element.android.libraries.designsystem.components.avatar.AvatarData 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.AvatarSize
import io.element.android.libraries.matrix.MatrixClient import io.element.android.libraries.matrix.MatrixClient
import io.element.android.libraries.matrix.media.MediaResolver
import io.element.android.libraries.matrix.room.MatrixRoom
import io.element.android.libraries.matrix.room.RoomSummary
import io.element.android.libraries.matrix.ui.model.MatrixUser import io.element.android.libraries.matrix.ui.model.MatrixUser
import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
@ -40,7 +37,7 @@ class MatrixItemHelper @Inject constructor(
val userAvatarUrl = client.loadUserAvatarURLString().getOrNull() val userAvatarUrl = client.loadUserAvatarURLString().getOrNull()
val userDisplayName = client.loadUserDisplayName().getOrNull() val userDisplayName = client.loadUserDisplayName().getOrNull()
val avatarData = val avatarData =
loadAvatarData( AvatarData(
userDisplayName ?: client.userId().value, userDisplayName ?: client.userId().value,
userAvatarUrl, userAvatarUrl,
avatarSize avatarSize
@ -48,35 +45,8 @@ class MatrixItemHelper @Inject constructor(
MatrixUser( MatrixUser(
id = client.userId(), id = client.userId(),
username = userDisplayName, username = userDisplayName,
avatarUrl = userAvatarUrl,
avatarData = avatarData, avatarData = avatarData,
) )
}.asFlow() }.asFlow()
} }
suspend fun loadAvatarData(room: MatrixRoom, size: AvatarSize): AvatarData {
return loadAvatarData(
name = room.bestName,
url = room.avatarUrl,
size = size
)
}
suspend fun loadAvatarData(roomSummary: RoomSummary.Filled, size: AvatarSize): AvatarData {
return loadAvatarData(
name = roomSummary.details.name,
url = roomSummary.details.avatarURLString,
size = size
)
}
suspend fun loadAvatarData(
name: String,
url: String?,
size: AvatarSize
): AvatarData {
val model = client.mediaResolver()
.resolve(url, kind = MediaResolver.Kind.Thumbnail(size.value))
return AvatarData(name, model, size)
}
} }

View file

@ -87,7 +87,6 @@ fun MatrixUserHeaderPreview() {
MatrixUser( MatrixUser(
id = UserId("@alice:server.org"), id = UserId("@alice:server.org"),
username = "Alice", username = "Alice",
avatarUrl = null,
avatarData = AvatarData("Alice") avatarData = AvatarData("Alice")
) )
) )
@ -100,7 +99,6 @@ fun MatrixUserHeaderNoUsernamePreview() {
MatrixUser( MatrixUser(
id = UserId("@alice:server.org"), id = UserId("@alice:server.org"),
username = null, username = null,
avatarUrl = null,
avatarData = AvatarData("Alice") avatarData = AvatarData("Alice")
) )
) )

View file

@ -91,7 +91,6 @@ fun MatrixUserRowPreview() {
MatrixUser( MatrixUser(
id = UserId("@alice:server.org"), id = UserId("@alice:server.org"),
username = "Alice", username = "Alice",
avatarUrl = null,
avatarData = AvatarData("Alice") avatarData = AvatarData("Alice")
) )
) )

View file

@ -0,0 +1,26 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.libraries.matrix.ui.media
import io.element.android.libraries.designsystem.components.avatar.AvatarData
import io.element.android.libraries.matrix.media.MediaResolver
import org.matrix.rustcomponents.sdk.mediaSourceFromUrl
fun AvatarData.toMetadata(): MediaResolver.Meta {
val mediaSource = url?.let { mediaSourceFromUrl(it) }
return MediaResolver.Meta(source = mediaSource, kind = MediaResolver.Kind.Thumbnail(size.value))
}

View file

@ -31,8 +31,10 @@ class LoggedInImageLoaderFactory @Inject constructor(
return ImageLoader return ImageLoader
.Builder(context) .Builder(context)
.components { .components {
add(AvatarKeyer())
add(MediaKeyer()) add(MediaKeyer())
add(MediaFetcher.Factory(matrixClient)) add(MediaFetcher.AvatarFactory(matrixClient))
add(MediaFetcher.MetaFactory(matrixClient))
} }
.build() .build()
} }

View file

@ -20,6 +20,7 @@ import coil.ImageLoader
import coil.fetch.FetchResult import coil.fetch.FetchResult
import coil.fetch.Fetcher import coil.fetch.Fetcher
import coil.request.Options import coil.request.Options
import io.element.android.libraries.designsystem.components.avatar.AvatarData
import io.element.android.libraries.matrix.MatrixClient import io.element.android.libraries.matrix.MatrixClient
import io.element.android.libraries.matrix.media.MediaResolver import io.element.android.libraries.matrix.media.MediaResolver
import java.nio.ByteBuffer import java.nio.ByteBuffer
@ -37,7 +38,7 @@ internal class MediaFetcher(
return imageLoader.components.newFetcher(byteBuffer, options, imageLoader)?.first?.fetch() return imageLoader.components.newFetcher(byteBuffer, options, imageLoader)?.first?.fetch()
} }
class Factory(private val client: MatrixClient) : class MetaFactory(private val client: MatrixClient) :
Fetcher.Factory<MediaResolver.Meta> { Fetcher.Factory<MediaResolver.Meta> {
override fun create( override fun create(
data: MediaResolver.Meta, data: MediaResolver.Meta,
@ -52,4 +53,20 @@ internal class MediaFetcher(
) )
} }
} }
class AvatarFactory(private val client: MatrixClient) :
Fetcher.Factory<AvatarData> {
override fun create(
data: AvatarData,
options: Options,
imageLoader: ImageLoader
): Fetcher {
return MediaFetcher(
mediaResolver = client.mediaResolver(),
meta = data.toMetadata(),
options = options,
imageLoader = imageLoader
)
}
}
} }

View file

@ -18,10 +18,19 @@ package io.element.android.libraries.matrix.ui.media
import coil.key.Keyer import coil.key.Keyer
import coil.request.Options import coil.request.Options
import io.element.android.libraries.designsystem.components.avatar.AvatarData
import io.element.android.libraries.matrix.media.MediaResolver import io.element.android.libraries.matrix.media.MediaResolver
internal class AvatarKeyer : Keyer<AvatarData> {
override fun key(data: AvatarData, options: Options): String? {
return data.toMetadata().toKey()
}
}
internal class MediaKeyer : Keyer<MediaResolver.Meta> { internal class MediaKeyer : Keyer<MediaResolver.Meta> {
override fun key(data: MediaResolver.Meta, options: Options): String? { override fun key(data: MediaResolver.Meta, options: Options): String? {
return "${data.source.url()}_${data.kind}" return data.toKey()
} }
} }
private fun MediaResolver.Meta.toKey() = "${source?.url()}_${kind}"

View file

@ -24,7 +24,6 @@ import io.element.android.libraries.matrix.core.UserId
data class MatrixUser( data class MatrixUser(
val id: UserId, val id: UserId,
val username: String? = null, val username: String? = null,
val avatarUrl: String? = null,
val avatarData: AvatarData = AvatarData(), val avatarData: AvatarData = AvatarData(),
) )