port profile and page to reducer pattern
This commit is contained in:
parent
72fa795d38
commit
70bc1be72d
|
@ -10,6 +10,11 @@ class JobBag {
|
|||
jobs[key] = job
|
||||
}
|
||||
|
||||
fun replace(key: String, job: Job) {
|
||||
jobs[key]?.cancel()
|
||||
jobs[key] = job
|
||||
}
|
||||
|
||||
fun cancel(key: String) {
|
||||
jobs.remove(key)?.cancel()
|
||||
}
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
package fake
|
||||
|
||||
import app.dapk.st.core.JobBag
|
||||
import io.mockk.mockk
|
||||
|
||||
class FakeJobBag {
|
||||
val instance = mockk<JobBag>()
|
||||
}
|
||||
|
|
@ -64,6 +64,48 @@ sealed interface ActionHandler<S> {
|
|||
class Delegate<S>(override val key: KClass<Action>, val handler: ReducerScope<S>.(Action) -> ActionHandler<S>) : ActionHandler<S>
|
||||
}
|
||||
|
||||
data class Combined2<S1, S2>(val state1: S1, val state2: S2)
|
||||
|
||||
fun interface SharedStateScope<C> {
|
||||
fun getSharedState(): C
|
||||
}
|
||||
|
||||
fun <S> shareState(block: SharedStateScope<S>.() -> ReducerFactory<S>): ReducerFactory<S> {
|
||||
var internalScope: ReducerScope<S>? = null
|
||||
val scope = SharedStateScope { internalScope!!.getState() }
|
||||
val combinedFactory = block(scope)
|
||||
return object : ReducerFactory<S> {
|
||||
override fun create(scope: ReducerScope<S>) = combinedFactory.create(scope).also { internalScope = scope }
|
||||
override fun initialState() = combinedFactory.initialState()
|
||||
}
|
||||
}
|
||||
|
||||
fun <S1, S2> combineReducers(r1: ReducerFactory<S1>, r2: ReducerFactory<S2>): ReducerFactory<Combined2<S1, S2>> {
|
||||
return object : ReducerFactory<Combined2<S1, S2>> {
|
||||
override fun create(scope: ReducerScope<Combined2<S1, S2>>): Reducer<Combined2<S1, S2>> {
|
||||
val r1Scope = object : ReducerScope<S1> {
|
||||
override val coroutineScope: CoroutineScope = scope.coroutineScope
|
||||
override suspend fun dispatch(action: Action) = scope.dispatch(action)
|
||||
override fun getState() = scope.getState().state1
|
||||
}
|
||||
|
||||
val r2Scope = object : ReducerScope<S2> {
|
||||
override val coroutineScope: CoroutineScope = scope.coroutineScope
|
||||
override suspend fun dispatch(action: Action) = scope.dispatch(action)
|
||||
override fun getState() = scope.getState().state2
|
||||
}
|
||||
|
||||
val r1Reducer = r1.create(r1Scope)
|
||||
val r2Reducer = r2.create(r2Scope)
|
||||
return Reducer {
|
||||
Combined2(r1Reducer.reduce(it), r2Reducer.reduce(it))
|
||||
}
|
||||
}
|
||||
|
||||
override fun initialState(): Combined2<S1, S2> = Combined2(r1.initialState(), r2.initialState())
|
||||
}
|
||||
}
|
||||
|
||||
fun <S> createReducer(
|
||||
initialState: S,
|
||||
vararg reducers: (ReducerScope<S>) -> ActionHandler<S>,
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
package app.dapk.st.directory
|
||||
|
||||
import app.dapk.st.core.JobBag
|
||||
import app.dapk.st.directory.state.*
|
||||
import app.dapk.st.engine.DirectoryItem
|
||||
import app.dapk.st.engine.UnreadCount
|
||||
import fake.FakeChatEngine
|
||||
import fake.FakeJobBag
|
||||
import fixture.aRoomOverview
|
||||
import io.mockk.mockk
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
|
@ -91,8 +91,3 @@ class DirectoryReducerTest {
|
|||
internal class FakeShortcutHandler {
|
||||
val instance = mockk<ShortcutHandler>()
|
||||
}
|
||||
|
||||
class FakeJobBag {
|
||||
val instance = mockk<JobBag>()
|
||||
}
|
||||
|
||||
|
|
|
@ -23,9 +23,9 @@ import kotlinx.parcelize.Parcelize
|
|||
class ImageGalleryActivity : DapkActivity() {
|
||||
|
||||
private val module by unsafeLazy { module<ImageGalleryModule>() }
|
||||
private val viewModel by viewModel {
|
||||
private val imageGalleryState by state {
|
||||
val payload = intent.getParcelableExtra("key") as? ImageGalleryActivityPayload
|
||||
module.imageGalleryViewModel(payload!!.roomName)
|
||||
module.imageGalleryState(payload!!.roomName)
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
|
@ -42,7 +42,7 @@ class ImageGalleryActivity : DapkActivity() {
|
|||
setContent {
|
||||
Surface {
|
||||
PermissionGuard(permissionState) {
|
||||
ImageGalleryScreen(viewModel, onTopLevelBack = { finish() }) { media ->
|
||||
ImageGalleryScreen(imageGalleryState, onTopLevelBack = { finish() }) { media ->
|
||||
setResult(RESULT_OK, Intent().setData(media.uri))
|
||||
finish()
|
||||
}
|
||||
|
|
|
@ -1,18 +1,22 @@
|
|||
package app.dapk.st.messenger.gallery
|
||||
|
||||
import android.content.ContentResolver
|
||||
import app.dapk.st.core.CoroutineDispatchers
|
||||
import app.dapk.st.core.ProvidableModule
|
||||
import app.dapk.st.core.*
|
||||
import app.dapk.st.messenger.gallery.state.ImageGalleryState
|
||||
import app.dapk.st.messenger.gallery.state.imageGalleryReducer
|
||||
|
||||
class ImageGalleryModule(
|
||||
private val contentResolver: ContentResolver,
|
||||
private val dispatchers: CoroutineDispatchers,
|
||||
) : ProvidableModule {
|
||||
|
||||
fun imageGalleryViewModel(roomName: String) = ImageGalleryViewModel(
|
||||
FetchMediaFoldersUseCase(contentResolver, dispatchers),
|
||||
FetchMediaUseCase(contentResolver, dispatchers),
|
||||
roomName = roomName,
|
||||
)
|
||||
fun imageGalleryState(roomName: String): ImageGalleryState = createStateViewModel {
|
||||
imageGalleryReducer(
|
||||
roomName = roomName,
|
||||
FetchMediaFoldersUseCase(contentResolver, dispatchers),
|
||||
FetchMediaUseCase(contentResolver, dispatchers),
|
||||
JobBag(),
|
||||
)
|
||||
}
|
||||
|
||||
}
|
|
@ -24,35 +24,41 @@ import app.dapk.st.core.components.CenteredLoading
|
|||
import app.dapk.st.design.components.GenericError
|
||||
import app.dapk.st.design.components.Spider
|
||||
import app.dapk.st.design.components.SpiderPage
|
||||
import app.dapk.st.messenger.gallery.state.ImageGalleryActions
|
||||
import app.dapk.st.messenger.gallery.state.ImageGalleryPage
|
||||
import app.dapk.st.messenger.gallery.state.ImageGalleryState
|
||||
import coil.compose.rememberAsyncImagePainter
|
||||
import coil.request.ImageRequest
|
||||
|
||||
@Composable
|
||||
fun ImageGalleryScreen(viewModel: ImageGalleryViewModel, onTopLevelBack: () -> Unit, onImageSelected: (Media) -> Unit) {
|
||||
fun ImageGalleryScreen(state: ImageGalleryState, onTopLevelBack: () -> Unit, onImageSelected: (Media) -> Unit) {
|
||||
LifecycleEffect(onStart = {
|
||||
viewModel.start()
|
||||
state.dispatch(ImageGalleryActions.Visible)
|
||||
})
|
||||
|
||||
val onNavigate: (SpiderPage<out ImageGalleryPage>?) -> Unit = {
|
||||
when (it) {
|
||||
null -> onTopLevelBack()
|
||||
else -> viewModel.goTo(it)
|
||||
else -> state.dispatch(PageAction.GoTo(it))
|
||||
}
|
||||
}
|
||||
|
||||
Spider(currentPage = viewModel.state.page, onNavigate = onNavigate) {
|
||||
Spider(currentPage = state.current.state1.page, onNavigate = onNavigate) {
|
||||
item(ImageGalleryPage.Routes.folders) {
|
||||
ImageGalleryFolders(it, onClick = { viewModel.selectFolder(it) }, onRetry = { viewModel.start() })
|
||||
ImageGalleryFolders(
|
||||
it,
|
||||
onClick = { state.dispatch(ImageGalleryActions.SelectFolder(it)) },
|
||||
onRetry = { state.dispatch(ImageGalleryActions.Visible) }
|
||||
)
|
||||
}
|
||||
item(ImageGalleryPage.Routes.files) {
|
||||
ImageGalleryMedia(it, onImageSelected, onRetry = { viewModel.selectFolder(it.folder) })
|
||||
ImageGalleryMedia(it, onImageSelected, onRetry = { state.dispatch(ImageGalleryActions.SelectFolder(it.folder)) })
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ImageGalleryFolders(state: ImageGalleryPage.Folders, onClick: (Folder) -> Unit, onRetry: () -> Unit) {
|
||||
private fun ImageGalleryFolders(state: ImageGalleryPage.Folders, onClick: (Folder) -> Unit, onRetry: () -> Unit) {
|
||||
val screenWidth = LocalConfiguration.current.screenWidthDp
|
||||
|
||||
val gradient = Brush.verticalGradient(
|
||||
|
@ -108,7 +114,7 @@ fun ImageGalleryFolders(state: ImageGalleryPage.Folders, onClick: (Folder) -> Un
|
|||
}
|
||||
|
||||
@Composable
|
||||
fun ImageGalleryMedia(state: ImageGalleryPage.Files, onFileSelected: (Media) -> Unit, onRetry: () -> Unit) {
|
||||
private fun ImageGalleryMedia(state: ImageGalleryPage.Files, onFileSelected: (Media) -> Unit, onRetry: () -> Unit) {
|
||||
val screenWidth = LocalConfiguration.current.screenWidthDp
|
||||
|
||||
Column {
|
||||
|
|
|
@ -1,89 +0,0 @@
|
|||
package app.dapk.st.messenger.gallery
|
||||
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import app.dapk.st.core.Lce
|
||||
import app.dapk.st.design.components.Route
|
||||
import app.dapk.st.design.components.SpiderPage
|
||||
import app.dapk.st.viewmodel.DapkViewModel
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class ImageGalleryViewModel(
|
||||
private val foldersUseCase: FetchMediaFoldersUseCase,
|
||||
private val fetchMediaUseCase: FetchMediaUseCase,
|
||||
roomName: String,
|
||||
) : DapkViewModel<ImageGalleryState, ImageGalleryEvent>(
|
||||
initialState = ImageGalleryState(
|
||||
page = SpiderPage(
|
||||
route = ImageGalleryPage.Routes.folders,
|
||||
label = "Send to $roomName",
|
||||
parent = null,
|
||||
state = ImageGalleryPage.Folders(Lce.Loading())
|
||||
)
|
||||
)
|
||||
) {
|
||||
|
||||
private var currentPageJob: Job? = null
|
||||
|
||||
fun start() {
|
||||
currentPageJob?.cancel()
|
||||
currentPageJob = viewModelScope.launch {
|
||||
val folders = foldersUseCase.fetchFolders()
|
||||
updatePageState<ImageGalleryPage.Folders> { copy(content = Lce.Content(folders)) }
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
fun goTo(page: SpiderPage<out ImageGalleryPage>) {
|
||||
currentPageJob?.cancel()
|
||||
updateState { copy(page = page) }
|
||||
}
|
||||
|
||||
fun selectFolder(folder: Folder) {
|
||||
currentPageJob?.cancel()
|
||||
|
||||
updateState {
|
||||
copy(
|
||||
page = SpiderPage(
|
||||
route = ImageGalleryPage.Routes.files,
|
||||
label = page.label,
|
||||
parent = ImageGalleryPage.Routes.folders,
|
||||
state = ImageGalleryPage.Files(Lce.Loading(), folder)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
currentPageJob = viewModelScope.launch {
|
||||
val media = fetchMediaUseCase.getMediaInBucket(folder.bucketId)
|
||||
updatePageState<ImageGalleryPage.Files> {
|
||||
copy(content = Lce.Content(media))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
private inline fun <reified S : ImageGalleryPage> updatePageState(crossinline block: S.() -> S) {
|
||||
val page = state.page
|
||||
val currentState = page.state
|
||||
require(currentState is S)
|
||||
updateState { copy(page = (page as SpiderPage<S>).copy(state = block(page.state))) }
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
data class ImageGalleryState(
|
||||
val page: SpiderPage<out ImageGalleryPage>,
|
||||
)
|
||||
|
||||
|
||||
sealed interface ImageGalleryPage {
|
||||
data class Folders(val content: Lce<List<Folder>>) : ImageGalleryPage
|
||||
data class Files(val content: Lce<List<Media>>, val folder: Folder) : ImageGalleryPage
|
||||
|
||||
object Routes {
|
||||
val folders = Route<Folders>("Folders")
|
||||
val files = Route<Files>("Files")
|
||||
}
|
||||
}
|
||||
|
||||
sealed interface ImageGalleryEvent
|
|
@ -0,0 +1,40 @@
|
|||
package app.dapk.st.messenger.gallery
|
||||
|
||||
import app.dapk.st.design.components.SpiderPage
|
||||
import app.dapk.state.Action
|
||||
import app.dapk.state.ReducerFactory
|
||||
import app.dapk.state.change
|
||||
import app.dapk.state.createReducer
|
||||
|
||||
fun <P : Any> createPageReducer(
|
||||
initialPage: SpiderPage<out P>
|
||||
): ReducerFactory<PageContainer<P>> {
|
||||
return createReducer(
|
||||
initialState = PageContainer(
|
||||
page = initialPage
|
||||
),
|
||||
|
||||
change(PageAction.GoTo::class) { action, state ->
|
||||
state.copy(page = action.page as SpiderPage<P>)
|
||||
},
|
||||
|
||||
change(PageAction.UpdatePage::class) { action, state ->
|
||||
val isSamePage = state.page.state::class == action.pageContent::class
|
||||
if (isSamePage) {
|
||||
val updatedPageContent = (state.page as SpiderPage<Any>).copy(state = action.pageContent)
|
||||
state.copy(page = updatedPageContent as SpiderPage<out P>)
|
||||
} else {
|
||||
state
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
sealed interface PageAction : Action {
|
||||
data class GoTo<P : Any>(val page: SpiderPage<P>) : PageAction
|
||||
data class UpdatePage<P : Any>(val pageContent: P) : PageAction
|
||||
}
|
||||
|
||||
data class PageContainer<P>(
|
||||
val page: SpiderPage<out P>
|
||||
)
|
|
@ -0,0 +1,9 @@
|
|||
package app.dapk.st.messenger.gallery.state
|
||||
|
||||
import app.dapk.st.messenger.gallery.Folder
|
||||
import app.dapk.state.Action
|
||||
|
||||
sealed interface ImageGalleryActions : Action {
|
||||
object Visible : ImageGalleryActions
|
||||
data class SelectFolder(val folder: Folder) : ImageGalleryActions
|
||||
}
|
|
@ -0,0 +1,59 @@
|
|||
package app.dapk.st.messenger.gallery.state
|
||||
|
||||
import app.dapk.st.core.JobBag
|
||||
import app.dapk.st.core.Lce
|
||||
import app.dapk.st.design.components.SpiderPage
|
||||
import app.dapk.st.messenger.gallery.*
|
||||
import app.dapk.state.*
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
fun imageGalleryReducer(
|
||||
roomName: String,
|
||||
foldersUseCase: FetchMediaFoldersUseCase,
|
||||
fetchMediaUseCase: FetchMediaUseCase,
|
||||
jobBag: JobBag,
|
||||
) = shareState {
|
||||
combineReducers(
|
||||
createPageReducer(roomName),
|
||||
createImageGalleryPageReducer(jobBag, foldersUseCase, fetchMediaUseCase),
|
||||
)
|
||||
}
|
||||
|
||||
private fun createPageReducer(roomName: String): ReducerFactory<PageContainer<ImageGalleryPage>> = createPageReducer(
|
||||
initialPage = SpiderPage(
|
||||
route = ImageGalleryPage.Routes.folders,
|
||||
label = "Send to $roomName",
|
||||
parent = null,
|
||||
state = ImageGalleryPage.Folders(Lce.Loading())
|
||||
)
|
||||
)
|
||||
|
||||
private fun SharedStateScope<Combined2<PageContainer<ImageGalleryPage>, Unit>>.createImageGalleryPageReducer(
|
||||
jobBag: JobBag,
|
||||
foldersUseCase: FetchMediaFoldersUseCase,
|
||||
fetchMediaUseCase: FetchMediaUseCase
|
||||
) = createReducer(
|
||||
initialState = Unit,
|
||||
|
||||
async(ImageGalleryActions.Visible::class) {
|
||||
jobBag.replace("page", coroutineScope.launch {
|
||||
val folders = foldersUseCase.fetchFolders()
|
||||
dispatch(PageAction.UpdatePage(ImageGalleryPage.Folders(Lce.Content(folders))))
|
||||
})
|
||||
},
|
||||
|
||||
async(ImageGalleryActions.SelectFolder::class) { action ->
|
||||
val page = SpiderPage(
|
||||
route = ImageGalleryPage.Routes.files,
|
||||
label = getSharedState().state1.page.label,
|
||||
parent = ImageGalleryPage.Routes.folders,
|
||||
state = ImageGalleryPage.Files(Lce.Loading(), action.folder)
|
||||
)
|
||||
dispatch(PageAction.GoTo(page))
|
||||
|
||||
jobBag.replace("page", coroutineScope.launch {
|
||||
val media = fetchMediaUseCase.getMediaInBucket(action.folder.bucketId)
|
||||
dispatch(PageAction.UpdatePage(page.state.copy(content = Lce.Content(media))))
|
||||
})
|
||||
},
|
||||
)
|
|
@ -0,0 +1,21 @@
|
|||
package app.dapk.st.messenger.gallery.state
|
||||
|
||||
import app.dapk.st.core.Lce
|
||||
import app.dapk.st.core.State
|
||||
import app.dapk.st.design.components.Route
|
||||
import app.dapk.st.messenger.gallery.Folder
|
||||
import app.dapk.st.messenger.gallery.Media
|
||||
import app.dapk.st.messenger.gallery.PageContainer
|
||||
import app.dapk.state.Combined2
|
||||
|
||||
typealias ImageGalleryState = State<Combined2<PageContainer<ImageGalleryPage>, Unit>, Unit>
|
||||
|
||||
sealed interface ImageGalleryPage {
|
||||
data class Folders(val content: Lce<List<Folder>>) : ImageGalleryPage
|
||||
data class Files(val content: Lce<List<Media>>, val folder: Folder) : ImageGalleryPage
|
||||
|
||||
object Routes {
|
||||
val folders = Route<Folders>("Folders")
|
||||
val files = Route<Files>("Files")
|
||||
}
|
||||
}
|
|
@ -12,6 +12,7 @@ import app.dapk.st.matrix.common.asString
|
|||
import app.dapk.st.messenger.state.*
|
||||
import app.dapk.st.navigator.MessageAttachment
|
||||
import fake.FakeChatEngine
|
||||
import fake.FakeJobBag
|
||||
import fake.FakeMessageOptionsStore
|
||||
import fixture.*
|
||||
import io.mockk.every
|
||||
|
@ -351,10 +352,6 @@ class FakeCopyToClipboard {
|
|||
val instance = mockk<CopyToClipboard>()
|
||||
}
|
||||
|
||||
class FakeJobBag {
|
||||
val instance = mockk<JobBag>()
|
||||
}
|
||||
|
||||
class FakeDeviceMeta {
|
||||
val instance = mockk<DeviceMeta>()
|
||||
|
||||
|
|
Loading…
Reference in New Issue