From c31f60e200008e16921a2cf494a0de55ad9f2e78 Mon Sep 17 00:00:00 2001 From: Tlaster Date: Wed, 11 Feb 2026 18:02:31 +0900 Subject: [PATCH 01/14] update UiList model --- .../flare/ui/screen/list/EditListScreen.kt | 22 +- .../flare/ui/component/UiListItemComponent.kt | 414 ++++++++++++++++-- .../dev/dimension/flare/ui/model/UiListExt.kt | 55 +-- .../bluesky/BlueskyFeedsWithTabsPresenter.kt | 10 +- .../screen/list/AllListWithTabsPresenter.kt | 10 +- .../MisskeyAntennasListWithTabsPresenter.kt | 10 +- iosApp/flare/UI/Component/UiListView.swift | 97 +++- iosApp/flare/UI/Screen/AllListScreen.swift | 2 +- .../datasource/bluesky/BlueskyDataSource.kt | 52 +-- .../datasource/mastodon/MastodonDataSource.kt | 25 +- .../datasource/microblog/ListDataSource.kt | 6 +- .../misskey/AntennasListPagingSource.kt | 6 +- .../misskey/FeaturedChannelPagingSource.kt | 36 ++ .../datasource/misskey/MisskeyDataSource.kt | 27 +- .../data/datasource/xqt/XQTDataSource.kt | 27 +- .../data/network/misskey/MisskeyService.kt | 5 +- .../data/network/misskey/api/ChannelsApi.kt | 7 +- .../data/network/misskey/api/model/Channel.kt | 6 +- .../api/model/ChannelsFeaturedRequest.kt | 10 + .../dev/dimension/flare/ui/model/UiList.kt | 61 ++- .../flare/ui/model/mapper/Bluesky.kt | 7 +- .../flare/ui/model/mapper/Misskey.kt | 25 +- .../dimension/flare/ui/model/mapper/VVO.kt | 5 +- .../dimension/flare/ui/model/mapper/XQT.kt | 7 +- .../home/bluesky/BlueskyFeedPresenter.kt | 18 +- .../home/bluesky/BlueskyFeedsPresenter.kt | 12 +- .../ui/presenter/list/AllListPresenter.kt | 2 +- .../presenter/list/AntennasListPresenter.kt | 4 +- .../list/EditAccountListPresenter.kt | 4 +- .../ui/presenter/list/ListInfoPresenter.kt | 2 +- .../list/PinnableTimelineTabPresenter.kt | 8 +- 31 files changed, 752 insertions(+), 230 deletions(-) create mode 100644 shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/misskey/FeaturedChannelPagingSource.kt create mode 100644 shared/src/commonMain/kotlin/dev/dimension/flare/data/network/misskey/api/model/ChannelsFeaturedRequest.kt diff --git a/app/src/main/java/dev/dimension/flare/ui/screen/list/EditListScreen.kt b/app/src/main/java/dev/dimension/flare/ui/screen/list/EditListScreen.kt index f6c166d3b..61d5f5dfc 100644 --- a/app/src/main/java/dev/dimension/flare/ui/screen/list/EditListScreen.kt +++ b/app/src/main/java/dev/dimension/flare/ui/screen/list/EditListScreen.kt @@ -170,9 +170,16 @@ internal fun EditListScreen( } else { state.listInfo .onSuccess { - if (it.avatar != null) { + val avatar = + when (it) { + is dev.dimension.flare.ui.model.UiList.List -> it.avatar + is dev.dimension.flare.ui.model.UiList.Feed -> it.avatar + is dev.dimension.flare.ui.model.UiList.Channel -> it.banner + is dev.dimension.flare.ui.model.UiList.Antenna -> null + } + if (avatar != null) { NetworkImage( - model = it.avatar, + model = avatar, contentDescription = null, modifier = Modifier @@ -380,7 +387,16 @@ private fun presenter( append(it.title) } description.edit { - append(it.description) + val desc = + when (it) { + is dev.dimension.flare.ui.model.UiList.List -> it.description + is dev.dimension.flare.ui.model.UiList.Feed -> it.description + is dev.dimension.flare.ui.model.UiList.Channel -> it.description + is dev.dimension.flare.ui.model.UiList.Antenna -> null + } + if (desc != null) { + append(desc) + } } } } diff --git a/compose-ui/src/commonMain/kotlin/dev/dimension/flare/ui/component/UiListItemComponent.kt b/compose-ui/src/commonMain/kotlin/dev/dimension/flare/ui/component/UiListItemComponent.kt index c8382847c..d4bfee21b 100644 --- a/compose-ui/src/commonMain/kotlin/dev/dimension/flare/ui/component/UiListItemComponent.kt +++ b/compose-ui/src/commonMain/kotlin/dev/dimension/flare/ui/component/UiListItemComponent.kt @@ -27,7 +27,6 @@ import dev.dimension.flare.compose.ui.feeds_discover_feeds_created_by import dev.dimension.flare.compose.ui.list_empty import dev.dimension.flare.compose.ui.list_error import dev.dimension.flare.ui.common.itemsIndexed -import dev.dimension.flare.ui.component.placeholder import dev.dimension.flare.ui.component.platform.PlatformListItem import dev.dimension.flare.ui.component.platform.PlatformSegmentedListItem import dev.dimension.flare.ui.component.platform.PlatformText @@ -36,10 +35,10 @@ import dev.dimension.flare.ui.theme.PlatformTheme import dev.dimension.flare.ui.theme.screenHorizontalPadding import org.jetbrains.compose.resources.stringResource -public fun LazyListScope.uiListItemComponent( - items: PagingState, - onClicked: ((UiList) -> Unit)? = null, - trailingContent: @Composable RowScope.(UiList) -> Unit = {}, +public fun LazyListScope.uiListItemComponent( + items: PagingState, + onClicked: ((T) -> Unit)? = null, + trailingContent: @Composable RowScope.(T) -> Unit = {}, ) { itemsIndexed( items, @@ -94,7 +93,12 @@ public fun LazyListScope.uiListItemComponent( }, ) { index, itemCount, item -> UiListItem( - onClicked = onClicked, + onClicked = + onClicked?.let { + { + it.invoke(item) + } + }, item = item, trailingContent = trailingContent, index = index, @@ -104,15 +108,73 @@ public fun LazyListScope.uiListItemComponent( } @Composable -public fun UiListItem( - onClicked: ((UiList) -> Unit)?, - item: UiList, - trailingContent: @Composable (RowScope.(UiList) -> Unit), +public fun UiListItem( + onClicked: (() -> Unit)?, + item: T, + trailingContent: @Composable (RowScope.(T) -> Unit), + index: Int, + totalCount: Int, + modifier: Modifier = Modifier, +) { + when (item) { + is UiList.List -> + UiListCard( + item = item, + onClicked = onClicked, + trailingContent = { + trailingContent.invoke(this, item) + }, + index = index, + totalCount = totalCount, + modifier = modifier, + ) + is UiList.Feed -> + UiFeedCard( + item = item, + onClicked = onClicked, + trailingContent = { + trailingContent.invoke(this, item) + }, + index = index, + totalCount = totalCount, + modifier = modifier, + ) + is UiList.Antenna -> + UiAntennaCard( + item = item, + onClicked = onClicked, + trailingContent = { + trailingContent.invoke(this, item) + }, + index = index, + totalCount = totalCount, + modifier = modifier, + ) + is UiList.Channel -> + UiChannelCard( + item = item, + onClicked = onClicked, + trailingContent = { + trailingContent.invoke(this, item) + }, + index = index, + totalCount = totalCount, + modifier = modifier, + ) + } +} + +@Composable +private fun UiListCard( + item: UiList.List, + onClicked: (() -> Unit)?, + trailingContent: @Composable (RowScope.() -> Unit), index: Int, totalCount: Int, modifier: Modifier = Modifier, ) { - if (item.description?.takeIf { it.isNotEmpty() } != null) { + val description = item.description + if (!description.isNullOrEmpty()) { Column( modifier = modifier @@ -124,17 +186,12 @@ public fun UiListItem( if (onClicked == null) { it } else { - it - .clickable { - onClicked(item) - } + it.clickable { onClicked() } } }, ) { PlatformListItem( - headlineContent = { - PlatformText(text = item.title) - }, + headlineContent = { PlatformText(text = item.title) }, leadingContent = { if (item.avatar != null) { NetworkImage( @@ -147,7 +204,7 @@ public fun UiListItem( ) } else { FAIcon( - imageVector = FontAwesomeIcons.Solid.Rss, + imageVector = FontAwesomeIcons.Solid.List, contentDescription = null, modifier = Modifier @@ -174,36 +231,164 @@ public fun UiListItem( } }, trailingContent = { - Row( - verticalAlignment = Alignment.CenterVertically, - ) { - trailingContent.invoke(this, item) + Row(verticalAlignment = Alignment.CenterVertically) { + trailingContent.invoke(this) } }, ) - item.description?.takeIf { it.isNotEmpty() }?.let { - PlatformText( - text = it, - modifier = - Modifier - .background(PlatformTheme.colorScheme.card) - .fillMaxWidth() - .padding(bottom = 8.dp) - .padding(horizontal = screenHorizontalPadding), - ) - } + PlatformText( + text = description, + modifier = + Modifier + .background(PlatformTheme.colorScheme.card) + .fillMaxWidth() + .padding(bottom = 8.dp) + .padding(horizontal = screenHorizontalPadding), + ) } } else { PlatformSegmentedListItem( modifier = modifier, index = index, totalCount = totalCount, - onClick = { - onClicked?.invoke(item) + onClick = { onClicked?.invoke() }, + headlineContent = { PlatformText(text = item.title) }, + leadingContent = { + if (item.avatar != null) { + NetworkImage( + model = item.avatar, + contentDescription = item.title, + modifier = + Modifier + .size(AvatarComponentDefaults.size) + .clip(PlatformTheme.shapes.medium), + ) + } else { + FAIcon( + imageVector = FontAwesomeIcons.Solid.List, + contentDescription = null, + modifier = + Modifier + .size(AvatarComponentDefaults.size) + .background( + color = PlatformTheme.colorScheme.primaryContainer, + shape = PlatformTheme.shapes.medium, + ).padding(8.dp), + tint = PlatformTheme.colorScheme.onPrimaryContainer, + ) + } + }, + supportingContent = { + if (item.creator != null) { + PlatformText( + text = + stringResource( + Res.string.feeds_discover_feeds_created_by, + item.creator?.handle ?: "Unknown", + ), + style = PlatformTheme.typography.caption, + color = PlatformTheme.colorScheme.caption, + ) + } }, - headlineContent = { - PlatformText(text = item.title) + trailingContent = { + Row(verticalAlignment = Alignment.CenterVertically) { + trailingContent.invoke(this) + } }, + ) + } +} + +@Composable +private fun UiFeedCard( + item: UiList.Feed, + onClicked: (() -> Unit)?, + trailingContent: @Composable (RowScope.() -> Unit), + index: Int, + totalCount: Int, + modifier: Modifier = Modifier, +) { + val description = item.description + if (!description.isNullOrEmpty()) { + Column( + modifier = + modifier + .listCard( + index = index, + totalCount = totalCount, + ).background(PlatformTheme.colorScheme.card) + .let { + if (onClicked == null) { + it + } else { + it.clickable { onClicked() } + } + }, + ) { + PlatformListItem( + headlineContent = { PlatformText(text = item.title) }, + leadingContent = { + if (item.avatar != null) { + NetworkImage( + model = item.avatar, + contentDescription = item.title, + modifier = + Modifier + .size(AvatarComponentDefaults.size) + .clip(PlatformTheme.shapes.medium), + ) + } else { + FAIcon( + imageVector = FontAwesomeIcons.Solid.Rss, + contentDescription = null, + modifier = + Modifier + .size(AvatarComponentDefaults.size) + .background( + color = PlatformTheme.colorScheme.primaryContainer, + shape = PlatformTheme.shapes.medium, + ).padding(8.dp), + tint = PlatformTheme.colorScheme.onPrimaryContainer, + ) + } + }, + supportingContent = { + if (item.creator != null) { + PlatformText( + text = + stringResource( + Res.string.feeds_discover_feeds_created_by, + item.creator?.handle ?: "Unknown", + ), + style = PlatformTheme.typography.caption, + color = PlatformTheme.colorScheme.caption, + ) + } + }, + trailingContent = { + Row(verticalAlignment = Alignment.CenterVertically) { + trailingContent.invoke(this) + } + }, + ) + PlatformText( + text = description, + modifier = + Modifier + .background(PlatformTheme.colorScheme.card) + .fillMaxWidth() + .padding(bottom = 8.dp) + .padding(horizontal = screenHorizontalPadding), + ) + } + } else { + PlatformSegmentedListItem( + modifier = modifier, + index = index, + totalCount = totalCount, + onClick = { onClicked?.invoke() }, + headlineContent = { PlatformText(text = item.title) }, leadingContent = { if (item.avatar != null) { NetworkImage( @@ -243,10 +428,155 @@ public fun UiListItem( } }, trailingContent = { - Row( - verticalAlignment = Alignment.CenterVertically, - ) { - trailingContent.invoke(this, item) + Row(verticalAlignment = Alignment.CenterVertically) { + trailingContent.invoke(this) + } + }, + ) + } +} + +@Composable +private fun UiAntennaCard( + item: UiList.Antenna, + onClicked: (() -> Unit)?, + trailingContent: @Composable (RowScope.() -> Unit), + index: Int, + totalCount: Int, + modifier: Modifier = Modifier, +) { + PlatformSegmentedListItem( + modifier = modifier, + index = index, + totalCount = totalCount, + onClick = { onClicked?.invoke() }, + headlineContent = { PlatformText(text = item.title) }, + leadingContent = { + FAIcon( + imageVector = FontAwesomeIcons.Solid.Rss, + contentDescription = null, + modifier = + Modifier + .size(AvatarComponentDefaults.size) + .background( + color = PlatformTheme.colorScheme.primaryContainer, + shape = PlatformTheme.shapes.medium, + ).padding(8.dp), + tint = PlatformTheme.colorScheme.onPrimaryContainer, + ) + }, + trailingContent = { + Row(verticalAlignment = Alignment.CenterVertically) { + trailingContent.invoke(this) + } + }, + ) +} + +@Composable +private fun UiChannelCard( + item: UiList.Channel, + onClicked: (() -> Unit)?, + trailingContent: @Composable (RowScope.() -> Unit), + index: Int, + totalCount: Int, + modifier: Modifier = Modifier, +) { + val description = item.description + if (!description.isNullOrEmpty()) { + Column( + modifier = + modifier + .listCard( + index = index, + totalCount = totalCount, + ).background(PlatformTheme.colorScheme.card) + .let { + if (onClicked == null) { + it + } else { + it.clickable { onClicked() } + } + }, + ) { + PlatformListItem( + headlineContent = { PlatformText(text = item.title) }, + leadingContent = { + if (item.banner != null) { + NetworkImage( + model = item.banner, + contentDescription = item.title, + modifier = + Modifier + .size(AvatarComponentDefaults.size) + .clip(PlatformTheme.shapes.medium), + ) + } else { + FAIcon( + imageVector = FontAwesomeIcons.Solid.List, + contentDescription = null, + modifier = + Modifier + .size(AvatarComponentDefaults.size) + .background( + color = PlatformTheme.colorScheme.primaryContainer, + shape = PlatformTheme.shapes.medium, + ).padding(8.dp), + tint = PlatformTheme.colorScheme.onPrimaryContainer, + ) + } + }, + trailingContent = { + Row(verticalAlignment = Alignment.CenterVertically) { + trailingContent.invoke(this) + } + }, + ) + PlatformText( + text = description, + modifier = + Modifier + .background(PlatformTheme.colorScheme.card) + .fillMaxWidth() + .padding(bottom = 8.dp) + .padding(horizontal = screenHorizontalPadding), + ) + } + } else { + PlatformSegmentedListItem( + modifier = modifier, + index = index, + totalCount = totalCount, + onClick = { onClicked?.invoke() }, + headlineContent = { PlatformText(text = item.title) }, + leadingContent = { + if (item.banner != null) { + NetworkImage( + model = item.banner, + contentDescription = item.title, + modifier = + Modifier + .size(AvatarComponentDefaults.size) + .clip(PlatformTheme.shapes.medium), + ) + } else { + FAIcon( + imageVector = FontAwesomeIcons.Solid.List, + contentDescription = null, + modifier = + Modifier + .size(AvatarComponentDefaults.size) + .background( + color = PlatformTheme.colorScheme.primaryContainer, + shape = PlatformTheme.shapes.medium, + ).padding(8.dp), + tint = PlatformTheme.colorScheme.onPrimaryContainer, + ) + } + }, + trailingContent = { + Row(verticalAlignment = Alignment.CenterVertically) { + trailingContent.invoke(this) } }, ) diff --git a/compose-ui/src/commonMain/kotlin/dev/dimension/flare/ui/model/UiListExt.kt b/compose-ui/src/commonMain/kotlin/dev/dimension/flare/ui/model/UiListExt.kt index 142c94555..3046dab31 100644 --- a/compose-ui/src/commonMain/kotlin/dev/dimension/flare/ui/model/UiListExt.kt +++ b/compose-ui/src/commonMain/kotlin/dev/dimension/flare/ui/model/UiListExt.kt @@ -1,62 +1,65 @@ package dev.dimension.flare.ui.model -import dev.dimension.flare.data.model.Bluesky.FeedTabItem +import dev.dimension.flare.data.model.Bluesky import dev.dimension.flare.data.model.IconType -import dev.dimension.flare.data.model.IconType.Mixed import dev.dimension.flare.data.model.ListTimelineTabItem import dev.dimension.flare.data.model.Misskey import dev.dimension.flare.data.model.TabItem import dev.dimension.flare.data.model.TabMetaData import dev.dimension.flare.data.model.TitleType import dev.dimension.flare.model.AccountType -import dev.dimension.flare.model.AccountType.Specific import dev.dimension.flare.model.MicroBlogKey public fun UiList.toTabItem(accountKey: MicroBlogKey): TabItem = - when (type) { - UiList.Type.Feed -> { - FeedTabItem( - account = Specific(accountKey), - uri = id, + when (this) { + is UiList.List -> + ListTimelineTabItem( + account = AccountType.Specific(accountKey), + listId = id, metaData = TabMetaData( title = TitleType.Text(title), icon = - Mixed( - icon = IconType.Material.MaterialIcon.List, - userKey = accountKey, - ), + avatar?.let { + IconType.Url(it) + } ?: IconType.Material(IconType.Material.MaterialIcon.List), ), ) - } - UiList.Type.List -> - ListTimelineTabItem( + is UiList.Feed -> + Bluesky.FeedTabItem( account = AccountType.Specific(accountKey), - listId = id, + uri = id, metaData = TabMetaData( title = TitleType.Text(title), icon = - IconType.Mixed( - icon = IconType.Material.MaterialIcon.List, - userKey = accountKey, - ), + avatar?.let { + IconType.Url(it) + } ?: IconType.Material(IconType.Material.MaterialIcon.Feeds), ), ) - UiList.Type.Antenna -> + is UiList.Antenna -> Misskey.AntennasTimelineTabItem( account = AccountType.Specific(accountKey), antennasId = id, metaData = TabMetaData( title = TitleType.Text(title), - icon = - IconType.Mixed( - icon = IconType.Material.MaterialIcon.Rss, - userKey = accountKey, - ), + icon = IconType.Material(IconType.Material.MaterialIcon.List), ), ) + + is UiList.Channel -> + TODO() +// Misskey.ChannelTimelineTabItem( +// account = AccountType.Specific(accountKey), +// channelId = id, +// metaData = +// TabMetaData( +// title = TitleType.Text(title), +// icon = IconType.Material(IconType.Material.MaterialIcon.List), +// ), +// ) } diff --git a/compose-ui/src/commonMain/kotlin/dev/dimension/flare/ui/screen/bluesky/BlueskyFeedsWithTabsPresenter.kt b/compose-ui/src/commonMain/kotlin/dev/dimension/flare/ui/screen/bluesky/BlueskyFeedsWithTabsPresenter.kt index 59f53a1b1..9017b36cb 100644 --- a/compose-ui/src/commonMain/kotlin/dev/dimension/flare/ui/screen/bluesky/BlueskyFeedsWithTabsPresenter.kt +++ b/compose-ui/src/commonMain/kotlin/dev/dimension/flare/ui/screen/bluesky/BlueskyFeedsWithTabsPresenter.kt @@ -24,10 +24,10 @@ public class BlueskyFeedsWithTabsPresenter( private val accountType: AccountType, ) : PresenterBase() { private val pinTabsPresenter by lazy { - object : PinTabsPresenter() { + object : PinTabsPresenter() { override fun List.filterPinned(): List = filterIsInstance().map { it.uri } - override fun getTimelineTabItem(item: UiList): TimelineTabItem = + override fun getTimelineTabItem(item: UiList.Feed): TimelineTabItem = Bluesky.FeedTabItem( account = accountType, uri = item.id, @@ -41,7 +41,7 @@ public class BlueskyFeedsWithTabsPresenter( ), ) - override fun List.filter(item: UiList): List = + override fun List.filter(item: UiList.Feed): List = filter { if (it is Bluesky.FeedTabItem) { it.uri != item.id @@ -61,7 +61,7 @@ public class BlueskyFeedsWithTabsPresenter( BlueskyFeedsPresenter(accountType = accountType) }.invoke() val tabState = pinTabsPresenter.invoke() - return object : State, BlueskyFeedsState by state, PinTabsPresenter.State by tabState { + return object : State, BlueskyFeedsState by state, PinTabsPresenter.State by tabState { override val isRefreshing: Boolean get() = isRefreshing @@ -77,7 +77,7 @@ public class BlueskyFeedsWithTabsPresenter( public interface State : BlueskyFeedsState, - PinTabsPresenter.State { + PinTabsPresenter.State { public val isRefreshing: Boolean public fun refresh() diff --git a/compose-ui/src/commonMain/kotlin/dev/dimension/flare/ui/screen/list/AllListWithTabsPresenter.kt b/compose-ui/src/commonMain/kotlin/dev/dimension/flare/ui/screen/list/AllListWithTabsPresenter.kt index e9b2006d6..ee4fefa05 100644 --- a/compose-ui/src/commonMain/kotlin/dev/dimension/flare/ui/screen/list/AllListWithTabsPresenter.kt +++ b/compose-ui/src/commonMain/kotlin/dev/dimension/flare/ui/screen/list/AllListWithTabsPresenter.kt @@ -19,12 +19,12 @@ public class AllListWithTabsPresenter( private val accountType: AccountType, ) : PresenterBase() { private val pinTabsPresenter by lazy { - object : PinTabsPresenter() { + object : PinTabsPresenter() { override fun List.filterPinned(): List = filterIsInstance() .map { it.listId } - override fun getTimelineTabItem(item: UiList): TimelineTabItem = + override fun getTimelineTabItem(item: UiList.List): TimelineTabItem = ListTimelineTabItem( account = accountType, listId = item.id, @@ -38,7 +38,7 @@ public class AllListWithTabsPresenter( ), ) - override fun List.filter(item: UiList): List = + override fun List.filter(item: UiList.List): List = filter { if (it is ListTimelineTabItem) { it.listId != item.id @@ -61,11 +61,11 @@ public class AllListWithTabsPresenter( return object : State, AllListState by state, - PinTabsPresenter.State by pinState { + PinTabsPresenter.State by pinState { } } public interface State : AllListState, - PinTabsPresenter.State + PinTabsPresenter.State } diff --git a/compose-ui/src/commonMain/kotlin/dev/dimension/flare/ui/screen/misskey/MisskeyAntennasListWithTabsPresenter.kt b/compose-ui/src/commonMain/kotlin/dev/dimension/flare/ui/screen/misskey/MisskeyAntennasListWithTabsPresenter.kt index 76cf94c7b..2ad3af500 100644 --- a/compose-ui/src/commonMain/kotlin/dev/dimension/flare/ui/screen/misskey/MisskeyAntennasListWithTabsPresenter.kt +++ b/compose-ui/src/commonMain/kotlin/dev/dimension/flare/ui/screen/misskey/MisskeyAntennasListWithTabsPresenter.kt @@ -17,12 +17,12 @@ public class MisskeyAntennasListWithTabsPresenter( private val accountType: AccountType, ) : PresenterBase() { private val pinTabsPresenter by lazy { - object : PinTabsPresenter() { + object : PinTabsPresenter() { override fun List.filterPinned(): List = filterIsInstance() .map { it.antennasId } - override fun getTimelineTabItem(item: UiList): TimelineTabItem = + override fun getTimelineTabItem(item: UiList.Antenna): TimelineTabItem = Misskey.AntennasTimelineTabItem( account = accountType, antennasId = item.id, @@ -33,7 +33,7 @@ public class MisskeyAntennasListWithTabsPresenter( ), ) - override fun List.filter(item: UiList): List = + override fun List.filter(item: UiList.Antenna): List = filter { if (it is Misskey.AntennasTimelineTabItem) { it.antennasId != item.id @@ -55,10 +55,10 @@ public class MisskeyAntennasListWithTabsPresenter( return object : State, AntennasListPresenter.State by state, - PinTabsPresenter.State by pinTabsState {} + PinTabsPresenter.State by pinTabsState {} } public interface State : - PinTabsPresenter.State, + PinTabsPresenter.State, AntennasListPresenter.State } diff --git a/iosApp/flare/UI/Component/UiListView.swift b/iosApp/flare/UI/Component/UiListView.swift index fc5ff5c47..80c52ded1 100644 --- a/iosApp/flare/UI/Component/UiListView.swift +++ b/iosApp/flare/UI/Component/UiListView.swift @@ -3,6 +3,24 @@ import KotlinSharedUI struct UiListView: View { let data: UiList + var body: some View { + switch data { + case let list as UiList.List: + UiListRow(data: list) + case let feed as UiList.Feed: + UiFeedRow(data: feed) + case let antenna as UiList.Antenna: + UiAntennaRow(data: antenna) + case let channel as UiList.Channel: + UiChannelRow(data: channel) + default: + EmptyView() + } + } +} + +private struct UiListRow: View { + let data: UiList.List var body: some View { VStack( alignment: .leading, @@ -12,10 +30,85 @@ struct UiListView: View { Text(data.title) } icon: { if let image = data.avatar { - AvatarView(data: image) + NetworkImage(data: image) .frame(width: 24, height: 24) + .clipShape(RoundedRectangle(cornerRadius: 4)) } else { - Image("fa-list") + Image(systemName: "list.bullet") + .frame(width: 24, height: 24) + } + } + if let desc = data.description_, !desc.isEmpty { + Text(desc) + .font(.caption) + .foregroundStyle(.secondary) + } + } + } +} + +private struct UiFeedRow: View { + let data: UiList.Feed + var body: some View { + VStack( + alignment: .leading, + spacing: 8 + ) { + Label { + Text(data.title) + } icon: { + if let image = data.avatar { + NetworkImage(data: image) + .frame(width: 24, height: 24) + .clipShape(RoundedRectangle(cornerRadius: 4)) + } else { + Image(systemName: "rss") + .frame(width: 24, height: 24) + } + } + if let desc = data.description_, !desc.isEmpty { + Text(desc) + .font(.caption) + .foregroundStyle(.secondary) + } + } + } +} + +private struct UiAntennaRow: View { + let data: UiList.Antenna + var body: some View { + VStack( + alignment: .leading, + spacing: 8 + ) { + Label { + Text(data.title) + } icon: { + Image(systemName: "antenna.radiowaves.left.and.right") + .frame(width: 24, height: 24) + } + } + } +} + +private struct UiChannelRow: View { + let data: UiList.Channel + var body: some View { + VStack( + alignment: .leading, + spacing: 8 + ) { + Label { + Text(data.title) + } icon: { + if let image = data.banner { + NetworkImage(data: image) + .frame(width: 24, height: 24) + .clipShape(RoundedRectangle(cornerRadius: 4)) + } else { + Image(systemName: "tv") + .frame(width: 24, height: 24) } } if let desc = data.description_, !desc.isEmpty { diff --git a/iosApp/flare/UI/Screen/AllListScreen.swift b/iosApp/flare/UI/Screen/AllListScreen.swift index 4fb08ca7d..988f341bf 100644 --- a/iosApp/flare/UI/Screen/AllListScreen.swift +++ b/iosApp/flare/UI/Screen/AllListScreen.swift @@ -30,7 +30,7 @@ struct AllListScreen: View { ) ) { UiListView(data: item) - .if(!item.readonly) { view in + .if(item.readonly) { view in view.swipeActions(edge: .leading) { Button { editListId = item.id diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/bluesky/BlueskyDataSource.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/bluesky/BlueskyDataSource.kt index 381e93468..12536c656 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/bluesky/BlueskyDataSource.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/bluesky/BlueskyDataSource.kt @@ -1095,7 +1095,7 @@ internal class BlueskyDataSource( private val myFeedsKey = "my_feeds_$accountKey" - val myFeeds: CacheData> by lazy { + val myFeeds: CacheData> by lazy { MemCacheable( key = myFeedsKey, ) { @@ -1134,14 +1134,14 @@ internal class BlueskyDataSource( fun popularFeeds( query: String?, scope: CoroutineScope, - ): Flow>> = + ): Flow>> = Pager( config = pagingConfig, ) { - object : BasePagingSource() { - override fun getRefreshKey(state: PagingState): String? = null + object : BasePagingSource() { + override fun getRefreshKey(state: PagingState): String? = null - override suspend fun doLoad(params: LoadParams): LoadResult { + override suspend fun doLoad(params: LoadParams): LoadResult { val result = service .getPopularFeedGeneratorsUnspecced( @@ -1168,7 +1168,7 @@ internal class BlueskyDataSource( .let { feeds -> combine( feeds, - MemCacheable.subscribe>(myFeedsKey), + MemCacheable.subscribe>(myFeedsKey), ) { popular, my -> popular.map { item -> item to my.any { it.id == item.id } @@ -1178,7 +1178,7 @@ internal class BlueskyDataSource( private fun feedInfoKey(uri: String) = "feed_info_$uri" - fun feedInfo(uri: String): MemCacheable = + fun feedInfo(uri: String): MemCacheable = MemCacheable( key = feedInfoKey(uri), ) { @@ -1214,8 +1214,8 @@ internal class BlueskyDataSource( uri = uri, ) - suspend fun subscribeFeed(data: UiList) { - MemCacheable.updateWith>( + suspend fun subscribeFeed(data: UiList.Feed) { + MemCacheable.updateWith>( key = myFeedsKey, ) { (it + data).toImmutableList() @@ -1265,7 +1265,7 @@ internal class BlueskyDataSource( ) myFeeds.refresh() }.onFailure { - MemCacheable.updateWith>( + MemCacheable.updateWith>( key = myFeedsKey, ) { it.filterNot { item -> item.id == data.id }.toImmutableList() @@ -1273,8 +1273,8 @@ internal class BlueskyDataSource( } } - suspend fun unsubscribeFeed(data: UiList) { - MemCacheable.updateWith>( + suspend fun unsubscribeFeed(data: UiList.Feed) { + MemCacheable.updateWith>( key = myFeedsKey, ) { it.filterNot { item -> item.id == data.id }.toImmutableList() @@ -1323,7 +1323,7 @@ internal class BlueskyDataSource( ) myFeeds.refresh() }.onFailure { - MemCacheable.updateWith>( + MemCacheable.updateWith>( key = myFeedsKey, ) { (it + data).toImmutableList() @@ -1331,7 +1331,7 @@ internal class BlueskyDataSource( } } - suspend fun favouriteFeed(data: UiList) { + suspend fun favouriteFeed(data: UiList.Feed) { MemCacheable.update( key = feedInfoKey(data.id), value = @@ -1367,18 +1367,18 @@ internal class BlueskyDataSource( private val myListKey = "my_list_$accountKey" - override fun myList(scope: CoroutineScope): Flow> = + override fun myList(scope: CoroutineScope): Flow> = memoryPager( pageSize = 20, pagingKey = myListKey, scope = scope, mediator = - object : BaseRemoteMediator() { + object : BaseRemoteMediator() { var cursor: String? = null override suspend fun doLoad( loadType: LoadType, - state: PagingState, + state: PagingState, ): MediatorResult { val result = service @@ -1396,7 +1396,7 @@ internal class BlueskyDataSource( it.render(accountKey) }.toImmutableList() cursor = result.cursor - MemoryPagingSource.update( + MemoryPagingSource.update( key = myListKey, value = items, ) @@ -1409,7 +1409,7 @@ internal class BlueskyDataSource( private fun listInfoKey(uri: String) = "list_info_$uri" - override fun listInfo(listId: String): MemCacheable = + override fun listInfo(listId: String): CacheData = MemCacheable( key = listInfoKey(listId), ) { @@ -1471,7 +1471,7 @@ internal class BlueskyDataSource( .list .render(accountKey) .let { list -> - MemoryPagingSource.updateWith( + MemoryPagingSource.updateWith( key = myListKey, ) { (listOf(list) + it).toImmutableList() @@ -1500,7 +1500,7 @@ internal class BlueskyDataSource( ), ) }.onSuccess { - MemoryPagingSource.updateWith( + MemoryPagingSource.updateWith( key = myListKey, ) { it.filterNot { item -> item.id == listId }.toImmutableList() @@ -1556,7 +1556,7 @@ internal class BlueskyDataSource( ), ) }.onSuccess { - MemoryPagingSource.updateWith( + MemoryPagingSource.updateWith( key = myListKey, ) { it @@ -1665,7 +1665,7 @@ internal class BlueskyDataSource( ).requireResponse() .list .render(accountKey) - MemCacheable.updateWith>(userListsKey(userKey)) { + MemCacheable.updateWith>(userListsKey(userKey)) { (it + list).toImmutableList() } service.createRecord( @@ -1696,7 +1696,7 @@ internal class BlueskyDataSource( .filter { user -> user.key.id != userKey.id } .toImmutableList() } - MemCacheable.updateWith>(userListsKey(userKey)) { + MemCacheable.updateWith>(userListsKey(userKey)) { it .filter { list -> list.id != listId } .toImmutableList() @@ -1771,12 +1771,12 @@ internal class BlueskyDataSource( private fun userListsKey(userKey: MicroBlogKey) = "userLists_${userKey.id}" - override fun userLists(userKey: MicroBlogKey): MemCacheable> = + override fun userLists(userKey: MicroBlogKey): MemCacheable> = MemCacheable( key = userListsKey(userKey), ) { var cursor: String? = null - val lists = mutableListOf() + val lists = mutableListOf() val allLists = service .getLists( diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/mastodon/MastodonDataSource.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/mastodon/MastodonDataSource.kt index 13f695bf7..ee887780e 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/mastodon/MastodonDataSource.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/mastodon/MastodonDataSource.kt @@ -918,16 +918,16 @@ internal open class MastodonDataSource( private val listKey: String get() = "allLists_$accountKey" - override fun myList(scope: CoroutineScope): Flow> = + override fun myList(scope: CoroutineScope): Flow> = memoryPager( pageSize = 20, pagingKey = listKey, scope = scope, mediator = - object : BaseRemoteMediator() { + object : BaseRemoteMediator() { override suspend fun doLoad( loadType: LoadType, - state: PagingState, + state: PagingState, ): MediatorResult { if (loadType == LoadType.PREPEND) { return MediatorResult.Success(endOfPaginationReached = true) @@ -941,7 +941,7 @@ internal open class MastodonDataSource( } }.toImmutableList() - MemoryPagingSource.update( + MemoryPagingSource.update( key = listKey, value = result, ) @@ -958,15 +958,14 @@ internal open class MastodonDataSource( service.createList(PostList(title = title)) }.onSuccess { response -> if (response.id != null) { - MemoryPagingSource.updateWith( + MemoryPagingSource.updateWith( key = listKey, ) { it .plus( - UiList( + UiList.List( id = response.id, title = title, - platformType = PlatformType.Mastodon, ), ).toImmutableList() } @@ -978,7 +977,7 @@ internal open class MastodonDataSource( tryRun { service.deleteList(listId) }.onSuccess { - MemoryPagingSource.updateWith( + MemoryPagingSource.updateWith( key = listKey, ) { it @@ -995,7 +994,7 @@ internal open class MastodonDataSource( tryRun { service.updateList(listId, PostList(title = title)) }.onSuccess { - MemoryPagingSource.updateWith( + MemoryPagingSource.updateWith( key = listKey, ) { it @@ -1010,7 +1009,7 @@ internal open class MastodonDataSource( } } - override fun listInfo(listId: String): CacheData = + override fun listInfo(listId: String): CacheData = MemCacheable( key = "listInfo_$listId", fetchSource = { @@ -1098,7 +1097,7 @@ internal open class MastodonDataSource( } val list = service.getList(listId) if (list.id != null) { - MemCacheable.updateWith>( + MemCacheable.updateWith>( key = userListsKey(userKey), ) { it @@ -1125,7 +1124,7 @@ internal open class MastodonDataSource( .filter { user -> user.key.id != userKey.id } .toImmutableList() } - MemCacheable.updateWith>( + MemCacheable.updateWith>( key = userListsKey(userKey), ) { it @@ -1140,7 +1139,7 @@ internal open class MastodonDataSource( private fun userListsKey(userKey: MicroBlogKey) = "userLists_${userKey.id}" - override fun userLists(userKey: MicroBlogKey): MemCacheable> = + override fun userLists(userKey: MicroBlogKey): MemCacheable> = MemCacheable( key = userListsKey(userKey), ) { diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/microblog/ListDataSource.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/microblog/ListDataSource.kt index def8645d3..53b6e99a8 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/microblog/ListDataSource.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/microblog/ListDataSource.kt @@ -13,9 +13,9 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow internal interface ListDataSource { - fun myList(scope: CoroutineScope): Flow> + fun myList(scope: CoroutineScope): Flow> - fun listInfo(listId: String): CacheData + fun listInfo(listId: String): CacheData fun listMembers( listId: String, @@ -48,7 +48,7 @@ internal interface ListDataSource { fun listMemberCache(listId: String): Flow> - fun userLists(userKey: MicroBlogKey): MemCacheable> + fun userLists(userKey: MicroBlogKey): MemCacheable> } public data class ListMetaData( diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/misskey/AntennasListPagingSource.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/misskey/AntennasListPagingSource.kt index e403b1c9e..830d8d9ae 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/misskey/AntennasListPagingSource.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/misskey/AntennasListPagingSource.kt @@ -9,8 +9,8 @@ import dev.dimension.flare.ui.model.mapper.render internal class AntennasListPagingSource( private val service: MisskeyService, -) : BasePagingSource() { - override suspend fun doLoad(params: LoadParams): LoadResult = +) : BasePagingSource() { + override suspend fun doLoad(params: LoadParams): LoadResult = tryRun { service.antennasList().map { it.render() @@ -28,5 +28,5 @@ internal class AntennasListPagingSource( }, ) - override fun getRefreshKey(state: PagingState): Int? = null + override fun getRefreshKey(state: PagingState): Int? = null } diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/misskey/FeaturedChannelPagingSource.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/misskey/FeaturedChannelPagingSource.kt new file mode 100644 index 000000000..919489f0f --- /dev/null +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/misskey/FeaturedChannelPagingSource.kt @@ -0,0 +1,36 @@ +package dev.dimension.flare.data.datasource.misskey + +import androidx.paging.PagingState +import dev.dimension.flare.common.BasePagingSource +import dev.dimension.flare.data.network.misskey.MisskeyService +import dev.dimension.flare.data.network.misskey.api.model.ChannelsFeaturedRequest +import dev.dimension.flare.data.repository.tryRun +import dev.dimension.flare.ui.model.UiList +import dev.dimension.flare.ui.model.mapper.render + +internal class FeaturedChannelPagingSource( + private val service: MisskeyService, +) : BasePagingSource() { + override suspend fun doLoad(params: LoadParams): LoadResult = + tryRun { + service + .channelsFeatured( + request = ChannelsFeaturedRequest(), + ).map { + it.render() + } + }.fold( + onSuccess = { antennas -> + LoadResult.Page( + data = antennas, + prevKey = null, + nextKey = null, + ) + }, + onFailure = { error -> + LoadResult.Error(error) + }, + ) + + override fun getRefreshKey(state: PagingState): Int? = null +} diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/misskey/MisskeyDataSource.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/misskey/MisskeyDataSource.kt index e8633cd22..9dcabc5c3 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/misskey/MisskeyDataSource.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/misskey/MisskeyDataSource.kt @@ -1019,16 +1019,16 @@ internal class MisskeyDataSource( private val listKey: String get() = "allLists_$accountKey" - override fun myList(scope: CoroutineScope): Flow> = + override fun myList(scope: CoroutineScope): Flow> = memoryPager( pageSize = 20, pagingKey = listKey, scope = scope, mediator = - object : BaseRemoteMediator() { + object : BaseRemoteMediator() { override suspend fun doLoad( loadType: LoadType, - state: PagingState, + state: PagingState, ): MediatorResult { if (loadType == LoadType.PREPEND) { return MediatorResult.Success(endOfPaginationReached = true) @@ -1042,7 +1042,7 @@ internal class MisskeyDataSource( it.render() }.toImmutableList() - MemoryPagingSource.update( + MemoryPagingSource.update( key = listKey, value = result.toImmutableList(), ) @@ -1063,15 +1063,14 @@ internal class MisskeyDataSource( ), ) }.onSuccess { response -> - MemoryPagingSource.updateWith( + MemoryPagingSource.updateWith( key = listKey, ) { it .plus( - UiList( + UiList.List( id = response.id, title = metaData.title, - platformType = PlatformType.Mastodon, ), ).toImmutableList() } @@ -1084,7 +1083,7 @@ internal class MisskeyDataSource( UsersListsDeleteRequest(listId = listId), ) }.onSuccess { - MemoryPagingSource.updateWith( + MemoryPagingSource.updateWith( key = listKey, ) { it @@ -1106,7 +1105,7 @@ internal class MisskeyDataSource( ), ) }.onSuccess { - MemoryPagingSource.updateWith( + MemoryPagingSource.updateWith( key = listKey, ) { it @@ -1121,7 +1120,7 @@ internal class MisskeyDataSource( } } - override fun listInfo(listId: String): CacheData = + override fun listInfo(listId: String): CacheData = MemCacheable( key = "listInfo_$listId", fetchSource = { @@ -1232,7 +1231,7 @@ internal class MisskeyDataSource( listId = listId, ), ) - MemCacheable.updateWith>( + MemCacheable.updateWith>( key = userListsKey(userKey), ) { it @@ -1260,7 +1259,7 @@ internal class MisskeyDataSource( .filter { user -> user.key.id != userKey.id } .toImmutableList() } - MemCacheable.updateWith>( + MemCacheable.updateWith>( key = userListsKey(userKey), ) { it @@ -1281,7 +1280,7 @@ internal class MisskeyDataSource( override fun listMemberCache(listId: String): Flow> = MemoryPagingSource.getFlow(listMemberKey(listId)) - override fun userLists(userKey: MicroBlogKey): MemCacheable> = + override fun userLists(userKey: MicroBlogKey): MemCacheable> = MemCacheable( key = userListsKey(userKey), ) { @@ -1378,7 +1377,7 @@ internal class MisskeyDataSource( fun antennasList( scope: CoroutineScope, pageSize: Int = 20, - ): Flow> = + ): Flow> = Pager( config = pagingConfig, ) { diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/xqt/XQTDataSource.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/xqt/XQTDataSource.kt index 23a62c937..df5e788b7 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/xqt/XQTDataSource.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/xqt/XQTDataSource.kt @@ -1204,18 +1204,18 @@ internal class XQTDataSource( private val listKey: String get() = "allLists_$accountKey" - override fun myList(scope: CoroutineScope): Flow> = + override fun myList(scope: CoroutineScope): Flow> = memoryPager( pageSize = 20, pagingKey = listKey, scope = scope, mediator = - object : BaseRemoteMediator() { + object : BaseRemoteMediator() { var cursor: String? = null override suspend fun doLoad( loadType: LoadType, - state: PagingState, + state: PagingState, ): MediatorResult { if (loadType == LoadType.PREPEND) { return MediatorResult.Success(endOfPaginationReached = true) @@ -1250,12 +1250,12 @@ internal class XQTDataSource( .toImmutableList() if (loadType == LoadType.REFRESH) { - MemoryPagingSource.update( + MemoryPagingSource.update( key = listKey, value = result, ) } else if (loadType == LoadType.APPEND) { - MemoryPagingSource.append( + MemoryPagingSource.append( key = listKey, value = result, ) @@ -1284,16 +1284,15 @@ internal class XQTDataSource( }.onSuccess { response -> val data = response.body()?.data?.list if (data?.idStr != null) { - MemoryPagingSource.updateWith( + MemoryPagingSource.updateWith( key = listKey, ) { it .plus( - UiList( + UiList.List( id = data.idStr, title = metaData.title, description = metaData.description, - platformType = PlatformType.Mastodon, ), ).toImmutableList() } @@ -1313,7 +1312,7 @@ internal class XQTDataSource( ), ) }.onSuccess { - MemoryPagingSource.updateWith( + MemoryPagingSource.updateWith( key = listKey, ) { it @@ -1341,7 +1340,7 @@ internal class XQTDataSource( ), ) }.onSuccess { - MemoryPagingSource.updateWith( + MemoryPagingSource.updateWith( key = listKey, ) { it @@ -1359,7 +1358,7 @@ internal class XQTDataSource( } } - override fun listInfo(listId: String): CacheData = + override fun listInfo(listId: String): CacheData = MemCacheable( key = "listInfo_$listId", fetchSource = { @@ -1487,7 +1486,7 @@ internal class XQTDataSource( } val list = getListInfo(listId) if (list?.id != null) { - MemCacheable.updateWith>( + MemCacheable.updateWith>( key = userListsKey(userKey), ) { it @@ -1520,7 +1519,7 @@ internal class XQTDataSource( .filter { user -> user.key.id != userKey.id } .toImmutableList() } - MemCacheable.updateWith>( + MemCacheable.updateWith>( key = userListsKey(userKey), ) { it @@ -1533,7 +1532,7 @@ internal class XQTDataSource( override fun listMemberCache(listId: String): Flow> = MemoryPagingSource.getFlow(listMemberKey(listId)) - override fun userLists(userKey: MicroBlogKey): MemCacheable> = + override fun userLists(userKey: MicroBlogKey): MemCacheable> = MemCacheable( key = userListsKey(userKey), ) { diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/network/misskey/MisskeyService.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/network/misskey/MisskeyService.kt index bdd79c0ae..a8d290600 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/data/network/misskey/MisskeyService.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/data/network/misskey/MisskeyService.kt @@ -3,6 +3,7 @@ package dev.dimension.flare.data.network.misskey import dev.dimension.flare.data.network.ktorfit import dev.dimension.flare.data.network.misskey.api.AccountApi import dev.dimension.flare.data.network.misskey.api.AntennasApi +import dev.dimension.flare.data.network.misskey.api.ChannelsApi import dev.dimension.flare.data.network.misskey.api.DriveApi import dev.dimension.flare.data.network.misskey.api.FollowingApi import dev.dimension.flare.data.network.misskey.api.HashtagsApi @@ -13,6 +14,7 @@ import dev.dimension.flare.data.network.misskey.api.ReactionsApi import dev.dimension.flare.data.network.misskey.api.UsersApi import dev.dimension.flare.data.network.misskey.api.createAccountApi import dev.dimension.flare.data.network.misskey.api.createAntennasApi +import dev.dimension.flare.data.network.misskey.api.createChannelsApi import dev.dimension.flare.data.network.misskey.api.createDriveApi import dev.dimension.flare.data.network.misskey.api.createFollowingApi import dev.dimension.flare.data.network.misskey.api.createHashtagsApi @@ -64,7 +66,8 @@ internal class MisskeyService( FollowingApi by config(baseUrl, accessTokenFlow).createFollowingApi(), HashtagsApi by config(baseUrl, accessTokenFlow).createHashtagsApi(), ListsApi by config(baseUrl, accessTokenFlow).createListsApi(), - AntennasApi by config(baseUrl, accessTokenFlow).createAntennasApi() { + AntennasApi by config(baseUrl, accessTokenFlow).createAntennasApi(), + ChannelsApi by config(baseUrl, accessTokenFlow).createChannelsApi() { suspend fun upload( data: ByteArray, name: String, diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/network/misskey/api/ChannelsApi.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/network/misskey/api/ChannelsApi.kt index 55673254e..111257bbe 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/data/network/misskey/api/ChannelsApi.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/data/network/misskey/api/ChannelsApi.kt @@ -1,9 +1,11 @@ package dev.dimension.flare.data.network.misskey.api import de.jensklingenberg.ktorfit.http.Body +import de.jensklingenberg.ktorfit.http.Header import de.jensklingenberg.ktorfit.http.POST import dev.dimension.flare.data.network.misskey.api.model.Channel import dev.dimension.flare.data.network.misskey.api.model.ChannelsCreateRequest +import dev.dimension.flare.data.network.misskey.api.model.ChannelsFeaturedRequest import dev.dimension.flare.data.network.misskey.api.model.ChannelsFollowRequest import dev.dimension.flare.data.network.misskey.api.model.ChannelsFollowedRequest import dev.dimension.flare.data.network.misskey.api.model.ChannelsSearchRequest @@ -58,11 +60,12 @@ internal interface ChannelsApi { * - 418: I'm Ai * - 500: Internal server error * - * @param body * @return [kotlin.collections.List] + * @param request * @return [kotlin.collections.List] */ @POST("channels/featured") suspend fun channelsFeatured( - @Body body: kotlin.Any, + @Header("Content-Type") contentType: kotlin.String = "application/json", + @Body request: ChannelsFeaturedRequest, ): kotlin.collections.List /** diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/network/misskey/api/model/Channel.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/network/misskey/api/model/Channel.kt index b65a5b0a0..e1f145bc6 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/data/network/misskey/api/model/Channel.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/data/network/misskey/api/model/Channel.kt @@ -29,9 +29,9 @@ internal data class Channel( @SerialName(value = "name") val name: kotlin.String, @SerialName(value = "description") val description: kotlin.String? = null, @SerialName(value = "bannerUrl") val bannerUrl: kotlin.String? = null, - @SerialName(value = "isArchived") val isArchived: kotlin.Boolean, - @SerialName(value = "notesCount") val notesCount: kotlin.Double, - @SerialName(value = "usersCount") val usersCount: kotlin.Double, + @SerialName(value = "isArchived") val isArchived: kotlin.Boolean? = null, + @SerialName(value = "notesCount") val notesCount: kotlin.Double? = null, + @SerialName(value = "usersCount") val usersCount: kotlin.Double? = null, @SerialName(value = "userId") val userId: kotlin.String? = null, @SerialName(value = "pinnedNoteIds") val pinnedNoteIds: kotlin.collections.List, @SerialName(value = "color") val color: kotlin.String, diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/network/misskey/api/model/ChannelsFeaturedRequest.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/network/misskey/api/model/ChannelsFeaturedRequest.kt new file mode 100644 index 000000000..7d7a05f7d --- /dev/null +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/data/network/misskey/api/model/ChannelsFeaturedRequest.kt @@ -0,0 +1,10 @@ +package dev.dimension.flare.data.network.misskey.api.model + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +internal data class ChannelsFeaturedRequest( + @SerialName(value = "limit") val limit: Int? = 10, + @SerialName(value = "allowPartial") val allowPartial: Boolean? = true, +) \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/model/UiList.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/model/UiList.kt index 3eecb5a4d..f16b83691 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/model/UiList.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/model/UiList.kt @@ -1,24 +1,49 @@ package dev.dimension.flare.ui.model import androidx.compose.runtime.Immutable -import dev.dimension.flare.model.PlatformType @Immutable -public data class UiList internal constructor( - val id: String, - val title: String, - val description: String? = null, - val avatar: String? = null, - val creator: UiUserV2? = null, - val likedCount: UiNumber = UiNumber(0), - val liked: Boolean = false, - val platformType: PlatformType, - val type: Type = Type.List, - val readonly: Boolean = false, -) { - public enum class Type { - Feed, - List, - Antenna, - } +public sealed class UiList { + public abstract val id: String + public abstract val title: String + + @Immutable + public data class List( + override val id: String, + override val title: String, + val description: String? = null, + val avatar: String? = null, + val creator: UiUserV2? = null, + val readonly: Boolean = false, + ) : UiList() + + @Immutable + public data class Feed( + override val id: String, + override val title: String, + val description: String? = null, + val avatar: String? = null, + val creator: UiUserV2? = null, + val likedCount: UiNumber = UiNumber(0), + val liked: Boolean = false, + ) : UiList() + + @Immutable + public data class Antenna( + override val id: String, + override val title: String, + ) : UiList() + + @Immutable + public data class Channel( + override val id: String, + override val title: String, + val isArchived: Boolean, + val notesCount: Double, + val usersCount: Double, + val description: String? = null, + val banner: String? = null, + val isFollowing: Boolean? = null, + val isFavorited: Boolean? = null, + ) : UiList() } diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/model/mapper/Bluesky.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/model/mapper/Bluesky.kt index 4da2a2c24..9ca5ab1da 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/model/mapper/Bluesky.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/model/mapper/Bluesky.kt @@ -1371,7 +1371,7 @@ private fun render( } internal fun GeneratorView.render(accountKey: MicroBlogKey) = - UiList( + UiList.Feed( id = uri.atUri, title = displayName, description = description, @@ -1379,18 +1379,15 @@ internal fun GeneratorView.render(accountKey: MicroBlogKey) = creator = creator.render(accountKey), likedCount = UiNumber(likeCount ?: 0), liked = viewer?.like?.atUri != null, - platformType = PlatformType.Bluesky, - type = UiList.Type.Feed, ) internal fun ListView.render(accountKey: MicroBlogKey) = - UiList( + UiList.List( id = uri.atUri, title = name, description = description, avatar = avatar?.uri, creator = creator.render(accountKey), - platformType = PlatformType.Bluesky, ) internal fun MessageContent.Bluesky.render(accountKey: MicroBlogKey) = diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/model/mapper/Misskey.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/model/mapper/Misskey.kt index c323d4700..faf16216a 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/model/mapper/Misskey.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/model/mapper/Misskey.kt @@ -7,6 +7,7 @@ import dev.dimension.flare.data.datasource.microblog.ActionMenu import dev.dimension.flare.data.datasource.microblog.StatusEvent import dev.dimension.flare.data.datasource.microblog.userActionsMenu import dev.dimension.flare.data.network.misskey.api.model.Antenna +import dev.dimension.flare.data.network.misskey.api.model.Channel import dev.dimension.flare.data.network.misskey.api.model.DriveFile import dev.dimension.flare.data.network.misskey.api.model.EmojiSimple import dev.dimension.flare.data.network.misskey.api.model.Meta200Response @@ -1043,11 +1044,10 @@ private fun moe.tlaster.mfm.parser.tree.Node.toHtml( } } -internal fun UserList.render(): UiList = - UiList( +internal fun UserList.render(): UiList.List = + UiList.List( id = id, title = name, - platformType = PlatformType.Misskey, ) internal fun Meta200Response.render(): UiInstanceMetadata { @@ -1100,10 +1100,21 @@ internal fun Meta200Response.render(): UiInstanceMetadata { ) } -internal fun Antenna.render(): UiList = - UiList( +internal fun Antenna.render(): UiList.Antenna = + UiList.Antenna( id = id, title = name, - platformType = PlatformType.Misskey, - type = UiList.Type.Antenna, + ) + +internal fun Channel.render(): UiList.Channel = + UiList.Channel( + id = id, + title = name, + description = description, + isArchived = isArchived ?: false, + notesCount = notesCount ?: 0.0, + usersCount = usersCount ?: 0.0, + banner = bannerUrl, + isFollowing = isFollowing, + isFavorited = isFavorited, ) diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/model/mapper/VVO.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/model/mapper/VVO.kt index e1396801f..07bd9e2cf 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/model/mapper/VVO.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/model/mapper/VVO.kt @@ -702,9 +702,8 @@ private fun replaceMentionAndHashtag( } } -internal fun MastodonList.render(): UiList = - UiList( +internal fun MastodonList.render(): UiList.List = + UiList.List( id = id.orEmpty(), title = title.orEmpty(), - platformType = PlatformType.Mastodon, ) diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/model/mapper/XQT.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/model/mapper/XQT.kt index 8096360a3..252431d99 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/model/mapper/XQT.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/model/mapper/XQT.kt @@ -912,7 +912,7 @@ internal fun parseXQTCustomDateTime(dateTimeStr: String): Instant? { } } -internal fun List.list(accountKey: MicroBlogKey): List = +internal fun List.list(accountKey: MicroBlogKey): List = flatMap { when (it) { is TimelineAddEntries -> @@ -946,7 +946,7 @@ internal fun List.list(accountKey: MicroBlogKey): List it.render(accountKey = accountKey) } -internal fun TwitterList.render(accountKey: MicroBlogKey): UiList { +internal fun TwitterList.render(accountKey: MicroBlogKey): UiList.List { val user = userResults?.result?.let { when (it) { @@ -954,11 +954,10 @@ internal fun TwitterList.render(accountKey: MicroBlogKey): UiList { else -> null } } - return UiList( + return UiList.List( id = idStr.orEmpty(), title = name.orEmpty(), description = description.orEmpty(), - platformType = PlatformType.xQt, creator = user, avatar = customBannerMedia?.mediaInfo?.originalImgURL diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/home/bluesky/BlueskyFeedPresenter.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/home/bluesky/BlueskyFeedPresenter.kt index 5fc4e5853..b89a9a1f5 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/home/bluesky/BlueskyFeedPresenter.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/home/bluesky/BlueskyFeedPresenter.kt @@ -76,7 +76,7 @@ public class BlueskyFeedPresenter( timeline.refreshSuspend() } - override fun subscribe(list: UiList) { + override fun subscribe(list: UiList.Feed) { serviceState.onSuccess { scope.launch { require(it is BlueskyDataSource) @@ -85,7 +85,7 @@ public class BlueskyFeedPresenter( } } - override fun unsubscribe(list: UiList) { + override fun unsubscribe(list: UiList.Feed) { serviceState.onSuccess { scope.launch { require(it is BlueskyDataSource) @@ -94,7 +94,7 @@ public class BlueskyFeedPresenter( } } - override fun favorite(list: UiList) { + override fun favorite(list: UiList.Feed) { serviceState.onSuccess { scope.launch { require(it is BlueskyDataSource) @@ -103,7 +103,7 @@ public class BlueskyFeedPresenter( } } - override fun unfavorite(list: UiList) { + override fun unfavorite(list: UiList.Feed) { serviceState.onSuccess { scope.launch { require(it is BlueskyDataSource) @@ -117,17 +117,17 @@ public class BlueskyFeedPresenter( @Immutable public interface BlueskyFeedState { - public val info: UiState + public val info: UiState public val timeline: PagingState public val subscribed: UiState public suspend fun refreshSuspend() - public fun subscribe(list: UiList) + public fun subscribe(list: UiList.Feed) - public fun unsubscribe(list: UiList) + public fun unsubscribe(list: UiList.Feed) - public fun favorite(list: UiList) + public fun favorite(list: UiList.Feed) - public fun unfavorite(list: UiList) + public fun unfavorite(list: UiList.Feed) } diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/home/bluesky/BlueskyFeedsPresenter.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/home/bluesky/BlueskyFeedsPresenter.kt index 3f5441cfe..5daccd7ad 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/home/bluesky/BlueskyFeedsPresenter.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/home/bluesky/BlueskyFeedsPresenter.kt @@ -64,7 +64,7 @@ public class BlueskyFeedsPresenter( popularFeeds.refreshSuspend() } - override fun subscribe(list: UiList) { + override fun subscribe(list: UiList.Feed) { serviceState.onSuccess { scope.launch { require(it is BlueskyDataSource) @@ -73,7 +73,7 @@ public class BlueskyFeedsPresenter( } } - override fun unsubscribe(list: UiList) { + override fun unsubscribe(list: UiList.Feed) { serviceState.onSuccess { scope.launch { require(it is BlueskyDataSource) @@ -87,14 +87,14 @@ public class BlueskyFeedsPresenter( @Immutable public interface BlueskyFeedsState { - public val myFeeds: PagingState - public val popularFeeds: PagingState> + public val myFeeds: PagingState + public val popularFeeds: PagingState> public fun search(value: String) public suspend fun refreshSuspend() - public fun subscribe(list: UiList) + public fun subscribe(list: UiList.Feed) - public fun unsubscribe(list: UiList) + public fun unsubscribe(list: UiList.Feed) } diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/list/AllListPresenter.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/list/AllListPresenter.kt index c9fab9335..663aa10e7 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/list/AllListPresenter.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/list/AllListPresenter.kt @@ -62,7 +62,7 @@ public class AllListPresenter( @Immutable public interface AllListState { - public val items: PagingState + public val items: PagingState public val isRefreshing: Boolean public fun refresh() diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/list/AntennasListPresenter.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/list/AntennasListPresenter.kt index 5bfd7dd7d..f9bdb137a 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/list/AntennasListPresenter.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/list/AntennasListPresenter.kt @@ -26,7 +26,7 @@ public class AntennasListPresenter( @androidx.compose.runtime.Immutable public interface State { - public val data: PagingState + public val data: PagingState public fun refresh() @@ -46,7 +46,7 @@ public class AntennasListPresenter( }.collectAsLazyPagingItems() }.toPagingState() return object : State { - override val data: PagingState = data + override val data: PagingState = data override fun refresh() { scope.launch { diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/list/EditAccountListPresenter.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/list/EditAccountListPresenter.kt index 9d5091ae2..8a34fd534 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/list/EditAccountListPresenter.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/list/EditAccountListPresenter.kt @@ -92,12 +92,12 @@ public interface EditAccountListState { /** * All lists. */ - public val lists: PagingState + public val lists: PagingState /** * Lists that the user is a member of. */ - public val userLists: UiState> + public val userLists: UiState> public fun addList(list: UiList) diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/list/ListInfoPresenter.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/list/ListInfoPresenter.kt index c0298cccd..6b448f811 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/list/ListInfoPresenter.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/list/ListInfoPresenter.kt @@ -45,5 +45,5 @@ public class ListInfoPresenter( @Immutable public interface ListInfoState { - public val listInfo: UiState + public val listInfo: UiState } diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/list/PinnableTimelineTabPresenter.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/list/PinnableTimelineTabPresenter.kt index 6714e5fa0..df485393e 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/list/PinnableTimelineTabPresenter.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/list/PinnableTimelineTabPresenter.kt @@ -36,18 +36,18 @@ public class PinnableTimelineTabPresenter( @Immutable public interface State { public sealed interface Tab { - public val data: PagingState + public val data: PagingState public data class List( - override val data: PagingState, + override val data: PagingState, ) : Tab public data class Feed( - override val data: PagingState, + override val data: PagingState, ) : Tab public data class Antenna( - override val data: PagingState, + override val data: PagingState, ) : Tab } From 9a97c38ca8c03cf695c9f862d19bff4f2a418744 Mon Sep 17 00:00:00 2001 From: Tlaster Date: Thu, 12 Feb 2026 18:34:30 +0900 Subject: [PATCH 02/14] [WIP] rework for datasource layer --- .../dimension/flare/data/model/TabSettings.kt | 30 ++ .../dev/dimension/flare/ui/model/UiListExt.kt | 19 +- .../flare/common/BasePagingSource.kt | 20 ++ .../flare/common/BaseRemoteMediator.kt | 128 --------- .../data/database/cache/CacaheDatabase.kt | 8 +- .../flare/data/database/cache/dao/ListDao.kt | 89 ++++++ .../flare/data/database/cache/model/DbList.kt | 115 ++++++++ .../datasource/bluesky/BlueskyDataSource.kt | 2 +- .../bluesky/BookmarkTimelineRemoteMediator.kt | 19 +- .../bluesky/FeedTimelineRemoteMediator.kt | 21 +- .../bluesky/HomeTimelineRemoteMediator.kt | 19 +- .../bluesky/ListTimelineRemoteMediator.kt | 21 +- .../bluesky/NotificationRemoteMediator.kt | 19 +- .../bluesky/SearchStatusRemoteMediator.kt | 19 +- .../bluesky/StatusDetailRemoteMediator.kt | 23 +- .../UserLikesTimelineRemoteMediator.kt | 21 +- .../bluesky/UserTimelineRemoteMediator.kt | 21 +- .../guest/mastodon/GuestMastodonDataSource.kt | 2 +- .../BookmarkTimelineRemoteMediator.kt | 19 +- .../mastodon/DiscoverStatusRemoteMediator.kt | 15 +- .../FavouriteTimelineRemoteMediator.kt | 19 +- .../mastodon/HomeTimelineRemoteMediator.kt | 17 +- .../mastodon/ListTimelineRemoteMediator.kt | 19 +- .../datasource/mastodon/MastodonDataSource.kt | 268 +----------------- .../datasource/mastodon/MastodonListLoader.kt | 67 +++++ .../mastodon/MastodonListMemberLoader.kt | 97 +++++++ .../mastodon/MentionRemoteMediator.kt | 17 +- .../mastodon/NotificationRemoteMediator.kt | 17 +- .../mastodon/PublicTimelineRemoteMediator.kt | 19 +- .../mastodon/SearchStatusPagingSource.kt | 19 +- .../mastodon/StatusDetailRemoteMediator.kt | 23 +- .../mastodon/UserTimelineRemoteMediator.kt | 19 +- .../datasource/microblog/ListDataSource.kt | 64 ----- .../microblog/MicroblogDataSource.kt | 2 +- .../microblog/MixedRemoteMediator.kt | 9 +- .../flare/data/datasource/microblog/Paging.kt | 2 +- .../data/datasource/microblog/ProfileTab.kt | 2 +- .../microblog/list/ListDataSource.kt | 10 + .../datasource/microblog/list/ListHandler.kt | 165 +++++++++++ .../datasource/microblog/list/ListLoader.kt | 27 ++ .../microblog/list/ListMemberHandler.kt | 156 ++++++++++ .../microblog/list/ListMemberLoader.kt | 31 ++ .../datasource/microblog/list/ListMetaData.kt | 9 + .../microblog/list/ListMetaDataType.kt | 7 + .../paging/BasePagingRemoteMediator.kt | 107 +++++++ .../paging/BaseTimelineRemoteMediator.kt | 52 ++++ .../microblog/paging/PagingRequest.kt | 13 + .../microblog/paging/PagingResult.kt | 18 ++ .../misskey/AntennasListPagingSource.kt | 32 --- .../misskey/AntennasListRemoteMediator.kt | 31 ++ .../misskey/AntennasTimelineRemoteMediator.kt | 19 +- .../misskey/ChannelTimelineRemoteMediator.kt | 69 +++++ .../misskey/DiscoverStatusRemoteMediator.kt | 17 +- .../FavouriteTimelineRemoteMediator.kt | 17 +- .../misskey/FeaturedChannelPagingSource.kt | 6 +- .../misskey/HomeTimelineRemoteMediator.kt | 17 +- .../misskey/HybridTimelineRemoteMediator.kt | 17 +- .../misskey/ListTimelineRemoteMediator.kt | 19 +- .../misskey/LocalTimelineRemoteMediator.kt | 17 +- .../misskey/MentionTimelineRemoteMediator.kt | 17 +- .../datasource/misskey/MisskeyDataSource.kt | 13 +- .../misskey/NotificationRemoteMediator.kt | 17 +- .../misskey/PublicTimelineRemoteMediator.kt | 17 +- .../misskey/SearchStatusRemoteMediator.kt | 19 +- .../misskey/StatusDetailRemoteMediator.kt | 23 +- .../misskey/UserTimelineRemoteMediator.kt | 17 +- .../rss/RssTimelineRemoteMediator.kt | 11 +- .../vvo/CommentChildRemoteMediator.kt | 19 +- .../vvo/DiscoverStatusRemoteMediator.kt | 25 +- .../datasource/vvo/FavouriteRemoteMediator.kt | 21 +- .../vvo/HomeTimelineRemoteMediator.kt | 19 +- .../data/datasource/vvo/LikeRemoteMediator.kt | 21 +- .../datasource/vvo/MentionRemoteMediator.kt | 25 +- .../vvo/SearchStatusRemoteMediator.kt | 25 +- .../vvo/StatusCommentRemoteMediator.kt | 19 +- .../vvo/StatusRepostRemoteMediator.kt | 25 +- .../vvo/UserTimelineRemoteMediator.kt | 21 +- .../data/datasource/vvo/VVODataSource.kt | 2 +- .../xqt/DeviceFollowRemoteMediator.kt | 19 +- .../xqt/HomeTimelineRemoteMediator.kt | 47 +-- .../xqt/ListTimelineRemoteMediator.kt | 14 +- .../datasource/xqt/MentionRemoteMediator.kt | 19 +- .../xqt/SearchStatusPagingSource.kt | 19 +- .../xqt/StatusDetailRemoteMediator.kt | 23 +- .../xqt/UserLikesTimelineRemoteMediator.kt | 21 +- .../xqt/UserMediaTimelineRemoteMediator.kt | 21 +- .../xqt/UserRepliesTimelineRemoteMediator.kt | 21 +- .../xqt/UserTimelineRemoteMediator.kt | 21 +- .../data/datasource/xqt/XQTDataSource.kt | 2 +- .../data/network/misskey/api/NotesApi.kt | 2 + .../dev/dimension/flare/ui/model/UiList.kt | 18 +- .../dev/dimension/flare/ui/model/UiNumber.kt | 4 + .../presenter/home/HomeTimelinePresenter.kt | 2 +- .../presenter/home/MixedTimelinePresenter.kt | 4 +- .../home/SearchStatusTimelinePresenter.kt | 2 +- .../ui/presenter/home/TimelinePresenter.kt | 6 +- .../BlueskyBookmarkTimelinePresenter.kt | 2 +- .../bluesky/BlueskyFeedTimelinePresenter.kt | 2 +- .../MastodonBookmarkTimelinePresenter.kt | 2 +- .../MastodonFavouriteTimelinePresenter.kt | 2 +- .../MastodonLocalTimelinePresenter.kt | 2 +- .../MastodonPublicTimelinePresenter.kt | 2 +- .../misskey/MissKeyLocalTimelinePresenter.kt | 2 +- .../misskey/MissKeyPublicTimelinePresenter.kt | 2 +- .../MisskeyFavouriteTimelinePresenter.kt | 2 +- .../misskey/MisskeyHybridTimelinePresenter.kt | 2 +- .../home/rss/RssTimelinePresenter.kt | 2 +- .../home/vvo/VVOFavouriteTimelinePresenter.kt | 2 +- .../home/vvo/VVOLikeTimelinePresenter.kt | 2 +- .../home/xqt/XQTBookmarkTimelinePresenter.kt | 2 +- .../xqt/XQTDeviceFollowTimelinePresenter.kt | 2 +- .../home/xqt/XQTFeaturedTimelinePresenter.kt | 2 +- .../list/AntennasTimelinePresenter.kt | 2 +- .../list/ChannelTimelinePresenter.kt | 30 ++ .../ui/presenter/list/ListInfoPresenter.kt | 1 + .../presenter/list/ListTimelinePresenter.kt | 2 +- .../list/PinnableTimelineTabPresenter.kt | 19 ++ .../profile/ProfileMediaPresenter.kt | 2 +- .../ui/presenter/profile/ProfilePresenter.kt | 2 +- .../status/StatusContextPresenter.kt | 2 +- .../rss/RssTimelineRemoteMediatorTest.kt | 2 +- 121 files changed, 1880 insertions(+), 1001 deletions(-) create mode 100644 shared/src/commonMain/kotlin/dev/dimension/flare/common/BasePagingSource.kt create mode 100644 shared/src/commonMain/kotlin/dev/dimension/flare/data/database/cache/dao/ListDao.kt create mode 100644 shared/src/commonMain/kotlin/dev/dimension/flare/data/database/cache/model/DbList.kt create mode 100644 shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/mastodon/MastodonListLoader.kt create mode 100644 shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/mastodon/MastodonListMemberLoader.kt delete mode 100644 shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/microblog/ListDataSource.kt create mode 100644 shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/microblog/list/ListDataSource.kt create mode 100644 shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/microblog/list/ListHandler.kt create mode 100644 shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/microblog/list/ListLoader.kt create mode 100644 shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/microblog/list/ListMemberHandler.kt create mode 100644 shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/microblog/list/ListMemberLoader.kt create mode 100644 shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/microblog/list/ListMetaData.kt create mode 100644 shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/microblog/list/ListMetaDataType.kt create mode 100644 shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/microblog/paging/BasePagingRemoteMediator.kt create mode 100644 shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/microblog/paging/BaseTimelineRemoteMediator.kt create mode 100644 shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/microblog/paging/PagingRequest.kt create mode 100644 shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/microblog/paging/PagingResult.kt delete mode 100644 shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/misskey/AntennasListPagingSource.kt create mode 100644 shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/misskey/AntennasListRemoteMediator.kt create mode 100644 shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/misskey/ChannelTimelineRemoteMediator.kt create mode 100644 shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/list/ChannelTimelinePresenter.kt diff --git a/compose-ui/src/commonMain/kotlin/dev/dimension/flare/data/model/TabSettings.kt b/compose-ui/src/commonMain/kotlin/dev/dimension/flare/data/model/TabSettings.kt index 40e545f48..2654b85c5 100644 --- a/compose-ui/src/commonMain/kotlin/dev/dimension/flare/data/model/TabSettings.kt +++ b/compose-ui/src/commonMain/kotlin/dev/dimension/flare/data/model/TabSettings.kt @@ -851,6 +851,36 @@ public object Misskey { override fun update(metaData: TabMetaData): TabItem = copy(metaData = metaData) } + + @Immutable + @Serializable + public data class ChannelTimelineTabItem( + val channelId: String, + override val account: AccountType, + override val metaData: TabMetaData, + ) : TimelineTabItem() { + public constructor(accountKey: MicroBlogKey, data: UiList) : this( + channelId = data.id, + account = AccountType.Specific(accountKey), + metaData = + TabMetaData( + title = TitleType.Text(data.title), + icon = + IconType.Mixed( + icon = IconType.Material.MaterialIcon.List, + userKey = accountKey, + ), + ), + ) + + override val key: String = "channel_${account}_$channelId" + + override fun createPresenter(): TimelinePresenter = + dev.dimension.flare.ui.presenter.list + .ChannelTimelinePresenter(account, channelId) + + override fun update(metaData: TabMetaData): TabItem = copy(metaData = metaData) + } } public object XQT { diff --git a/compose-ui/src/commonMain/kotlin/dev/dimension/flare/ui/model/UiListExt.kt b/compose-ui/src/commonMain/kotlin/dev/dimension/flare/ui/model/UiListExt.kt index 3046dab31..67393f621 100644 --- a/compose-ui/src/commonMain/kotlin/dev/dimension/flare/ui/model/UiListExt.kt +++ b/compose-ui/src/commonMain/kotlin/dev/dimension/flare/ui/model/UiListExt.kt @@ -52,14 +52,13 @@ public fun UiList.toTabItem(accountKey: MicroBlogKey): TabItem = ) is UiList.Channel -> - TODO() -// Misskey.ChannelTimelineTabItem( -// account = AccountType.Specific(accountKey), -// channelId = id, -// metaData = -// TabMetaData( -// title = TitleType.Text(title), -// icon = IconType.Material(IconType.Material.MaterialIcon.List), -// ), -// ) + Misskey.ChannelTimelineTabItem( + account = AccountType.Specific(accountKey), + channelId = id, + metaData = + TabMetaData( + title = TitleType.Text(title), + icon = IconType.Material(IconType.Material.MaterialIcon.List), + ), + ) } diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/common/BasePagingSource.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/common/BasePagingSource.kt new file mode 100644 index 000000000..8265db11f --- /dev/null +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/common/BasePagingSource.kt @@ -0,0 +1,20 @@ +package dev.dimension.flare.common + +import androidx.paging.PagingSource +import dev.dimension.flare.data.repository.DebugRepository + +internal abstract class BasePagingSource : PagingSource() { + override suspend fun load(params: LoadParams): LoadResult = + try { + doLoad(params) + } catch (e: Throwable) { + onError(e) + DebugRepository.error(e) + LoadResult.Error(e) + } + + abstract suspend fun doLoad(params: LoadParams): LoadResult + + protected open fun onError(e: Throwable) { + } +} diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/common/BaseRemoteMediator.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/common/BaseRemoteMediator.kt index 4fbe1f565..4441fc4f1 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/common/BaseRemoteMediator.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/common/BaseRemoteMediator.kt @@ -2,16 +2,9 @@ package dev.dimension.flare.common import androidx.paging.ExperimentalPagingApi import androidx.paging.LoadType -import androidx.paging.PagingSource import androidx.paging.PagingState import androidx.paging.RemoteMediator -import dev.dimension.flare.data.database.cache.CacheDatabase -import dev.dimension.flare.data.database.cache.connect -import dev.dimension.flare.data.database.cache.mapper.saveToDatabase -import dev.dimension.flare.data.database.cache.model.DbPagingKey -import dev.dimension.flare.data.database.cache.model.DbPagingTimelineWithStatus import dev.dimension.flare.data.repository.DebugRepository -import dev.dimension.flare.ui.model.UiTimeline @OptIn(ExperimentalPagingApi::class) internal abstract class BaseRemoteMediator : RemoteMediator() { @@ -35,124 +28,3 @@ internal abstract class BaseRemoteMediator : RemoteMedia protected open fun onError(e: Throwable) { } } - -internal sealed interface BaseTimelineLoader { - data object NotSupported : BaseTimelineLoader -} - -@OptIn(ExperimentalPagingApi::class) -internal abstract class BaseTimelineRemoteMediator( - private val database: CacheDatabase, -) : BaseRemoteMediator(), - BaseTimelineLoader { - abstract val pagingKey: String - - final override suspend fun doLoad( - loadType: LoadType, - state: PagingState, - ): MediatorResult { - val request: Request = - when (loadType) { - LoadType.REFRESH -> Request.Refresh - LoadType.PREPEND -> { - val previousKey = - database.pagingTimelineDao().getPagingKey(pagingKey)?.prevKey - ?: return MediatorResult.Success(endOfPaginationReached = true) - Request.Prepend(previousKey) - } - LoadType.APPEND -> { - val nextKey = - database.pagingTimelineDao().getPagingKey(pagingKey)?.nextKey - ?: return MediatorResult.Success(endOfPaginationReached = true) - Request.Append(nextKey) - } - } - - val result = - timeline( - pageSize = state.config.pageSize, - request = request, - ) - database.connect { - if (loadType == LoadType.REFRESH) { - result.data.groupBy { it.timeline.pagingKey }.keys.forEach { key -> - database - .pagingTimelineDao() - .delete(pagingKey = key) - } - database.pagingTimelineDao().deletePagingKey(pagingKey) - database.pagingTimelineDao().insertPagingKey( - DbPagingKey( - pagingKey = pagingKey, - nextKey = result.nextKey, - prevKey = result.previousKey, - ), - ) - } else if (loadType == LoadType.PREPEND && result.previousKey != null) { - database.pagingTimelineDao().updatePagingKeyPrevKey( - pagingKey = pagingKey, - prevKey = result.previousKey, - ) - } else if (loadType == LoadType.APPEND && result.nextKey != null) { - database.pagingTimelineDao().updatePagingKeyNextKey( - pagingKey = pagingKey, - nextKey = result.nextKey, - ) - } - saveToDatabase(database, result.data) - } - return MediatorResult.Success( - endOfPaginationReached = - result.endOfPaginationReached || - when (loadType) { - LoadType.REFRESH -> false - LoadType.PREPEND -> result.previousKey == null - LoadType.APPEND -> result.nextKey == null - }, - ) - } - - abstract suspend fun timeline( - pageSize: Int, - request: Request, - ): Result - - data class Result( - val endOfPaginationReached: Boolean, - val data: List = emptyList(), - val nextKey: String? = null, - val previousKey: String? = null, - ) - - sealed interface Request { - data object Refresh : Request - - data class Prepend( - val previousKey: String, - ) : Request - - data class Append( - val nextKey: String, - ) : Request - } -} - -internal fun interface BaseTimelinePagingSourceFactory : BaseTimelineLoader { - abstract fun create(): BasePagingSource -} - -internal abstract class BasePagingSource : PagingSource() { - override suspend fun load(params: LoadParams): LoadResult = - try { - doLoad(params) - } catch (e: Throwable) { - onError(e) - DebugRepository.error(e) - LoadResult.Error(e) - } - - abstract suspend fun doLoad(params: LoadParams): LoadResult - - protected open fun onError(e: Throwable) { - } -} diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/database/cache/CacaheDatabase.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/database/cache/CacaheDatabase.kt index ce4a34ce9..130cee269 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/data/database/cache/CacaheDatabase.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/data/database/cache/CacaheDatabase.kt @@ -8,7 +8,7 @@ import androidx.room.TypeConverters import androidx.room.immediateTransaction import androidx.room.useWriterConnection -internal const val CACHE_DATABASE_VERSION = 22 +internal const val CACHE_DATABASE_VERSION = 23 @Database( entities = [ @@ -24,6 +24,9 @@ internal const val CACHE_DATABASE_VERSION = 22 dev.dimension.flare.data.database.cache.model.DbUserHistory::class, dev.dimension.flare.data.database.cache.model.DbEmojiHistory::class, dev.dimension.flare.data.database.cache.model.DbPagingKey::class, + dev.dimension.flare.data.database.cache.model.DbList::class, + dev.dimension.flare.data.database.cache.model.DbListPaging::class, + dev.dimension.flare.data.database.cache.model.DbListMember::class, ], version = CACHE_DATABASE_VERSION, exportSchema = false, @@ -36,6 +39,7 @@ internal const val CACHE_DATABASE_VERSION = 22 dev.dimension.flare.data.database.cache.model.StatusConverter::class, dev.dimension.flare.data.database.cache.model.UserContentConverters::class, dev.dimension.flare.data.database.cache.model.MessageContentConverters::class, + dev.dimension.flare.data.database.cache.model.ListContentConverters::class, ) @ConstructedBy(CacheDatabaseConstructor::class) internal abstract class CacheDatabase : RoomDatabase() { @@ -50,6 +54,8 @@ internal abstract class CacheDatabase : RoomDatabase() { abstract fun pagingTimelineDao(): dev.dimension.flare.data.database.cache.dao.PagingTimelineDao abstract fun messageDao(): dev.dimension.flare.data.database.cache.dao.MessageDao + + abstract fun listDao(): dev.dimension.flare.data.database.cache.dao.ListDao } // The Room compiler generates the `actual` implementations. diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/database/cache/dao/ListDao.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/database/cache/dao/ListDao.kt new file mode 100644 index 000000000..91b2ec720 --- /dev/null +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/data/database/cache/dao/ListDao.kt @@ -0,0 +1,89 @@ +package dev.dimension.flare.data.database.cache.dao + +import androidx.paging.PagingSource +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import androidx.room.Transaction +import dev.dimension.flare.data.database.cache.model.DbList +import dev.dimension.flare.data.database.cache.model.DbListMember +import dev.dimension.flare.data.database.cache.model.DbListMemberWithContent +import dev.dimension.flare.data.database.cache.model.DbListPaging +import dev.dimension.flare.data.database.cache.model.DbListWithContent +import dev.dimension.flare.data.database.cache.model.DbUserWithListMembership +import dev.dimension.flare.model.DbAccountType +import dev.dimension.flare.model.MicroBlogKey +import kotlinx.coroutines.flow.Flow + +@Dao +internal interface ListDao { + @Transaction + @Query( + "SELECT * FROM DbListPaging WHERE pagingKey = :pagingKey", + ) + fun getPagingSource(pagingKey: String): PagingSource + + @Query("SELECT * FROM DbList WHERE listKey = :listKey AND accountType = :accountType") + fun getList( + listKey: MicroBlogKey, + accountType: DbAccountType, + ): Flow + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insertAll(timelines: List) + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insertAll(lists: List) + + @Query("DELETE FROM DbListPaging WHERE pagingKey = :pagingKey") + suspend fun deleteByPagingKey(pagingKey: String) + + @Query("DELETE FROM DbList WHERE accountType = :accountType") + suspend fun deleteByAccountType(accountType: String) + + @Query("DELETE FROM DbList WHERE listKey = :listKey AND accountType = :accountType") + suspend fun deleteByListKey( + listKey: MicroBlogKey, + accountType: DbAccountType, + ) + + @Query("DELETE FROM DbListPaging WHERE listKey = :listKey AND accountType = :accountType") + suspend fun deletePagingByListKey( + listKey: MicroBlogKey, + accountType: DbAccountType, + ) + + @Query("DELETE FROM DbList") + suspend fun clearAllLists() + + @Query("DELETE FROM DbListPaging") + suspend fun clearAllListPaging() + + @Query("UPDATE DbList SET content = :content WHERE listKey = :listKey AND accountType = :accountType") + suspend fun updateListContent( + listKey: MicroBlogKey, + accountType: DbAccountType, + content: DbList.ListContent, + ) + + @Transaction + @Query("SELECT * FROM DbListMember WHERE listKey = :listKey") + fun getListMembers(listKey: MicroBlogKey): PagingSource + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insertAll(members: List) + + @Query("DELETE FROM DbListMember WHERE listKey = :listKey") + suspend fun deleteMembersByListKey(listKey: MicroBlogKey) + + @Query("DELETE FROM DbListMember WHERE memberKey = :memberKey AND listKey = :listKey") + suspend fun deleteMemberFromList( + memberKey: MicroBlogKey, + listKey: MicroBlogKey, + ) + + @Transaction + @Query("SELECT * FROM DbUser WHERE userKey = :userKey") + fun getUserByKey(userKey: MicroBlogKey): PagingSource +} diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/database/cache/model/DbList.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/database/cache/model/DbList.kt new file mode 100644 index 000000000..88938acce --- /dev/null +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/data/database/cache/model/DbList.kt @@ -0,0 +1,115 @@ +package dev.dimension.flare.data.database.cache.model + +import androidx.room.Embedded +import androidx.room.Entity +import androidx.room.Index +import androidx.room.PrimaryKey +import androidx.room.Relation +import androidx.room.TypeConverter +import dev.dimension.flare.common.decodeJson +import dev.dimension.flare.common.encodeJson +import dev.dimension.flare.data.database.cache.model.DbList.ListContent +import dev.dimension.flare.model.DbAccountType +import dev.dimension.flare.model.MicroBlogKey +import dev.dimension.flare.ui.model.UiList +import kotlinx.serialization.Serializable +import kotlin.uuid.Uuid + +@Entity( + indices = [Index(value = ["listKey", "accountType"], unique = true)], +) +internal data class DbList( + val listKey: MicroBlogKey, + val accountType: DbAccountType, + val content: ListContent, + @PrimaryKey + val id: String = "${accountType}_$listKey", +) { + @Serializable + data class ListContent( + val data: UiList, + ) +} + +internal class ListContentConverters { + @TypeConverter + fun fromMessageContent(content: ListContent): String = content.encodeJson() + + @TypeConverter + fun toMessageContent(value: String): ListContent = value.decodeJson() +} + +@Entity( + indices = [ + Index( + value = ["accountType", "listKey", "pagingKey"], + unique = true, + ), + ], +) +internal data class DbListPaging( + val accountType: DbAccountType, + val pagingKey: String, + val listKey: MicroBlogKey, + @PrimaryKey + val _id: String = Uuid.random().toString(), +) + +internal data class DbListWithContent( + @Embedded + val paging: DbListPaging, + @Relation( + parentColumn = "listKey", + entityColumn = "listKey", + entity = DbList::class, + ) + val list: DbList, +) + +@Entity( + indices = [ + Index( + value = ["listKey", "memberKey"], + unique = true, + ), + ], +) +internal data class DbListMember( + val listKey: MicroBlogKey, + val memberKey: MicroBlogKey, + @PrimaryKey + val id: String = "${listKey}_$memberKey", +) + +internal data class DbListMemberWithContent( + @Embedded + val member: DbListMember, + @Relation( + parentColumn = "memberKey", + entityColumn = "userKey", + entity = DbUser::class, + ) + val user: DbUser, +) + +internal data class DbListMemberWithList( + @Embedded + val member: DbListMember, + @Relation( + parentColumn = "listKey", + entityColumn = "listKey", + entity = DbList::class, + ) + val list: DbList, +) + +internal data class DbUserWithListMembership( + @Embedded + val user: DbUser, + @Relation( + parentColumn = "userKey", + entityColumn = "memberKey", + entity = DbListMember::class, + ) + val listMemberships: List, +) diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/bluesky/BlueskyDataSource.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/bluesky/BlueskyDataSource.kt index 12536c656..ea5c618be 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/bluesky/BlueskyDataSource.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/bluesky/BlueskyDataSource.kt @@ -61,7 +61,6 @@ import com.atproto.repo.PutRecordRequest import com.atproto.repo.StrongRef import dev.dimension.flare.common.BasePagingSource import dev.dimension.flare.common.BaseRemoteMediator -import dev.dimension.flare.common.BaseTimelineLoader import dev.dimension.flare.common.CacheData import dev.dimension.flare.common.Cacheable import dev.dimension.flare.common.FileItem @@ -94,6 +93,7 @@ import dev.dimension.flare.data.datasource.microblog.RelationDataSource import dev.dimension.flare.data.datasource.microblog.StatusEvent import dev.dimension.flare.data.datasource.microblog.createSendingDirectMessage import dev.dimension.flare.data.datasource.microblog.memoryPager +import dev.dimension.flare.data.datasource.microblog.paging.BaseTimelineLoader import dev.dimension.flare.data.datasource.microblog.pagingConfig import dev.dimension.flare.data.datasource.microblog.relationKeyWithUserKey import dev.dimension.flare.data.datasource.microblog.timelinePager diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/bluesky/BookmarkTimelineRemoteMediator.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/bluesky/BookmarkTimelineRemoteMediator.kt index a23aea899..67dbbdf14 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/bluesky/BookmarkTimelineRemoteMediator.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/bluesky/BookmarkTimelineRemoteMediator.kt @@ -2,9 +2,12 @@ package dev.dimension.flare.data.datasource.bluesky import androidx.paging.ExperimentalPagingApi import app.bsky.bookmark.GetBookmarksQueryParams -import dev.dimension.flare.common.BaseTimelineRemoteMediator import dev.dimension.flare.data.database.cache.CacheDatabase import dev.dimension.flare.data.database.cache.mapper.toDb +import dev.dimension.flare.data.database.cache.model.DbPagingTimelineWithStatus +import dev.dimension.flare.data.datasource.microblog.paging.BaseTimelineRemoteMediator +import dev.dimension.flare.data.datasource.microblog.paging.PagingRequest +import dev.dimension.flare.data.datasource.microblog.paging.PagingResult import dev.dimension.flare.data.network.bluesky.BlueskyService import dev.dimension.flare.model.MicroBlogKey @@ -20,11 +23,11 @@ internal class BookmarkTimelineRemoteMediator( override suspend fun timeline( pageSize: Int, - request: Request, - ): Result { + request: PagingRequest, + ): PagingResult { val response = when (request) { - Request.Refresh -> { + PagingRequest.Refresh -> { service .getBookmarks( GetBookmarksQueryParams( @@ -33,13 +36,13 @@ internal class BookmarkTimelineRemoteMediator( ).requireResponse() } - is Request.Prepend -> { - return Result( + is PagingRequest.Prepend -> { + return PagingResult( endOfPaginationReached = true, ) } - is Request.Append -> { + is PagingRequest.Append -> { service .getBookmarks( GetBookmarksQueryParams( @@ -50,7 +53,7 @@ internal class BookmarkTimelineRemoteMediator( } } - return Result( + return PagingResult( endOfPaginationReached = response.bookmarks.isEmpty() || response.cursor == null, data = response.bookmarks.toDb( diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/bluesky/FeedTimelineRemoteMediator.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/bluesky/FeedTimelineRemoteMediator.kt index af48bf349..c9a575767 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/bluesky/FeedTimelineRemoteMediator.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/bluesky/FeedTimelineRemoteMediator.kt @@ -2,9 +2,12 @@ package dev.dimension.flare.data.datasource.bluesky import androidx.paging.ExperimentalPagingApi import app.bsky.feed.GetFeedQueryParams -import dev.dimension.flare.common.BaseTimelineRemoteMediator import dev.dimension.flare.data.database.cache.CacheDatabase import dev.dimension.flare.data.database.cache.mapper.toDbPagingTimeline +import dev.dimension.flare.data.database.cache.model.DbPagingTimelineWithStatus +import dev.dimension.flare.data.datasource.microblog.paging.BaseTimelineRemoteMediator +import dev.dimension.flare.data.datasource.microblog.paging.PagingRequest +import dev.dimension.flare.data.datasource.microblog.paging.PagingResult import dev.dimension.flare.data.network.bluesky.BlueskyService import dev.dimension.flare.model.MicroBlogKey import sh.christian.ozone.api.AtUri @@ -22,11 +25,11 @@ internal class FeedTimelineRemoteMediator( override suspend fun timeline( pageSize: Int, - request: Request, - ): Result { + request: PagingRequest, + ): PagingResult { val response = when (request) { - Request.Refresh -> + PagingRequest.Refresh -> service .getFeed( GetFeedQueryParams( @@ -35,13 +38,13 @@ internal class FeedTimelineRemoteMediator( ), ).maybeResponse() - is Request.Prepend -> { - return Result( + is PagingRequest.Prepend -> { + return PagingResult( endOfPaginationReached = true, ) } - is Request.Append -> { + is PagingRequest.Append -> { service .getFeed( GetFeedQueryParams( @@ -51,11 +54,11 @@ internal class FeedTimelineRemoteMediator( ), ).maybeResponse() } - } ?: return Result( + } ?: return PagingResult( endOfPaginationReached = true, ) - return Result( + return PagingResult( endOfPaginationReached = response.cursor == null, data = response.feed.toDbPagingTimeline( diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/bluesky/HomeTimelineRemoteMediator.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/bluesky/HomeTimelineRemoteMediator.kt index ce797734a..24e5eaffb 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/bluesky/HomeTimelineRemoteMediator.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/bluesky/HomeTimelineRemoteMediator.kt @@ -2,11 +2,14 @@ package dev.dimension.flare.data.datasource.bluesky import androidx.paging.ExperimentalPagingApi import app.bsky.feed.GetTimelineQueryParams -import dev.dimension.flare.common.BaseTimelineRemoteMediator import dev.dimension.flare.common.InAppNotification import dev.dimension.flare.common.Message import dev.dimension.flare.data.database.cache.CacheDatabase import dev.dimension.flare.data.database.cache.mapper.toDbPagingTimeline +import dev.dimension.flare.data.database.cache.model.DbPagingTimelineWithStatus +import dev.dimension.flare.data.datasource.microblog.paging.BaseTimelineRemoteMediator +import dev.dimension.flare.data.datasource.microblog.paging.PagingRequest +import dev.dimension.flare.data.datasource.microblog.paging.PagingResult import dev.dimension.flare.data.network.bluesky.BlueskyService import dev.dimension.flare.data.repository.LoginExpiredException import dev.dimension.flare.model.MicroBlogKey @@ -27,15 +30,15 @@ internal class HomeTimelineRemoteMediator( override suspend fun timeline( pageSize: Int, - request: Request, - ): Result { + request: PagingRequest, + ): PagingResult { val response = when (request) { - is Request.Prepend -> return Result( + is PagingRequest.Prepend -> return PagingResult( endOfPaginationReached = true, ) - Request.Refresh -> { + PagingRequest.Refresh -> { service .getTimeline( GetTimelineQueryParams( @@ -44,7 +47,7 @@ internal class HomeTimelineRemoteMediator( ).maybeResponse() } - is Request.Append -> { + is PagingRequest.Append -> { service .getTimeline( GetTimelineQueryParams( @@ -53,10 +56,10 @@ internal class HomeTimelineRemoteMediator( ), ).maybeResponse() } - } ?: return Result( + } ?: return PagingResult( endOfPaginationReached = true, ) - return Result( + return PagingResult( endOfPaginationReached = response.cursor == null, data = response.feed.toDbPagingTimeline( diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/bluesky/ListTimelineRemoteMediator.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/bluesky/ListTimelineRemoteMediator.kt index 077f6b328..ca81b15fa 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/bluesky/ListTimelineRemoteMediator.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/bluesky/ListTimelineRemoteMediator.kt @@ -2,9 +2,12 @@ package dev.dimension.flare.data.datasource.bluesky import androidx.paging.ExperimentalPagingApi import app.bsky.feed.GetListFeedQueryParams -import dev.dimension.flare.common.BaseTimelineRemoteMediator import dev.dimension.flare.data.database.cache.CacheDatabase import dev.dimension.flare.data.database.cache.mapper.toDbPagingTimeline +import dev.dimension.flare.data.database.cache.model.DbPagingTimelineWithStatus +import dev.dimension.flare.data.datasource.microblog.paging.BaseTimelineRemoteMediator +import dev.dimension.flare.data.datasource.microblog.paging.PagingRequest +import dev.dimension.flare.data.datasource.microblog.paging.PagingResult import dev.dimension.flare.data.network.bluesky.BlueskyService import dev.dimension.flare.model.MicroBlogKey import sh.christian.ozone.api.AtUri @@ -22,11 +25,11 @@ internal class ListTimelineRemoteMediator( override suspend fun timeline( pageSize: Int, - request: Request, - ): Result { + request: PagingRequest, + ): PagingResult { val response = when (request) { - Request.Refresh -> + PagingRequest.Refresh -> service .getListFeed( GetListFeedQueryParams( @@ -35,13 +38,13 @@ internal class ListTimelineRemoteMediator( ), ).maybeResponse() - is Request.Prepend -> { - return Result( + is PagingRequest.Prepend -> { + return PagingResult( endOfPaginationReached = true, ) } - is Request.Append -> { + is PagingRequest.Append -> { service .getListFeed( GetListFeedQueryParams( @@ -51,11 +54,11 @@ internal class ListTimelineRemoteMediator( ), ).maybeResponse() } - } ?: return Result( + } ?: return PagingResult( endOfPaginationReached = true, ) - return Result( + return PagingResult( endOfPaginationReached = response.cursor == null, data = response.feed.toDbPagingTimeline( diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/bluesky/NotificationRemoteMediator.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/bluesky/NotificationRemoteMediator.kt index 26e4d9166..0ccc8f05d 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/bluesky/NotificationRemoteMediator.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/bluesky/NotificationRemoteMediator.kt @@ -7,9 +7,12 @@ import app.bsky.feed.Repost import app.bsky.notification.ListNotificationsNotificationReason import app.bsky.notification.ListNotificationsQueryParams import app.bsky.notification.UpdateSeenRequest -import dev.dimension.flare.common.BaseTimelineRemoteMediator import dev.dimension.flare.data.database.cache.CacheDatabase import dev.dimension.flare.data.database.cache.mapper.toDb +import dev.dimension.flare.data.database.cache.model.DbPagingTimelineWithStatus +import dev.dimension.flare.data.datasource.microblog.paging.BaseTimelineRemoteMediator +import dev.dimension.flare.data.datasource.microblog.paging.PagingRequest +import dev.dimension.flare.data.datasource.microblog.paging.PagingResult import dev.dimension.flare.data.network.bluesky.BlueskyService import dev.dimension.flare.model.MicroBlogKey import kotlinx.collections.immutable.toImmutableList @@ -33,11 +36,11 @@ internal class NotificationRemoteMediator( override suspend fun timeline( pageSize: Int, - request: Request, - ): Result { + request: PagingRequest, + ): PagingResult { val response = when (request) { - Request.Refresh -> { + PagingRequest.Refresh -> { service .listNotifications( ListNotificationsQueryParams( @@ -55,7 +58,7 @@ internal class NotificationRemoteMediator( } } - is Request.Append -> { + is PagingRequest.Append -> { service .listNotifications( ListNotificationsQueryParams( @@ -66,11 +69,11 @@ internal class NotificationRemoteMediator( } else -> { - return Result( + return PagingResult( endOfPaginationReached = true, ) } - } ?: return Result( + } ?: return PagingResult( endOfPaginationReached = true, ) @@ -111,7 +114,7 @@ internal class NotificationRemoteMediator( .orEmpty() .associateBy { it.uri } .toImmutableMap() - return Result( + return PagingResult( endOfPaginationReached = response.cursor == null, data = response.notifications.toDb( diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/bluesky/SearchStatusRemoteMediator.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/bluesky/SearchStatusRemoteMediator.kt index e20b6583b..c43ce26bb 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/bluesky/SearchStatusRemoteMediator.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/bluesky/SearchStatusRemoteMediator.kt @@ -2,9 +2,12 @@ package dev.dimension.flare.data.datasource.bluesky import androidx.paging.ExperimentalPagingApi import app.bsky.feed.SearchPostsQueryParams -import dev.dimension.flare.common.BaseTimelineRemoteMediator import dev.dimension.flare.data.database.cache.CacheDatabase import dev.dimension.flare.data.database.cache.mapper.toDb +import dev.dimension.flare.data.database.cache.model.DbPagingTimelineWithStatus +import dev.dimension.flare.data.datasource.microblog.paging.BaseTimelineRemoteMediator +import dev.dimension.flare.data.datasource.microblog.paging.PagingRequest +import dev.dimension.flare.data.datasource.microblog.paging.PagingResult import dev.dimension.flare.data.network.bluesky.BlueskyService import dev.dimension.flare.model.MicroBlogKey @@ -26,16 +29,16 @@ internal class SearchStatusRemoteMediator( override suspend fun timeline( pageSize: Int, - request: Request, - ): Result { + request: PagingRequest, + ): PagingResult { val response = when (request) { - is Request.Prepend -> { - return Result( + is PagingRequest.Prepend -> { + return PagingResult( endOfPaginationReached = true, ) } - Request.Refresh -> { + PagingRequest.Refresh -> { service.searchPosts( SearchPostsQueryParams( q = query, @@ -44,7 +47,7 @@ internal class SearchStatusRemoteMediator( ) } - is Request.Append -> { + is PagingRequest.Append -> { service.searchPosts( SearchPostsQueryParams( q = query, @@ -55,7 +58,7 @@ internal class SearchStatusRemoteMediator( } }.requireResponse() - return Result( + return PagingResult( endOfPaginationReached = response.cursor == null, data = response.posts.toDb( diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/bluesky/StatusDetailRemoteMediator.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/bluesky/StatusDetailRemoteMediator.kt index f38b786f5..9dd7b3e6a 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/bluesky/StatusDetailRemoteMediator.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/bluesky/StatusDetailRemoteMediator.kt @@ -11,10 +11,13 @@ import app.bsky.feed.ReplyRefRootUnion import app.bsky.feed.ThreadViewPost import app.bsky.feed.ThreadViewPostParentUnion import app.bsky.feed.ThreadViewPostReplieUnion -import dev.dimension.flare.common.BaseTimelineRemoteMediator import dev.dimension.flare.data.database.cache.CacheDatabase import dev.dimension.flare.data.database.cache.mapper.toDbPagingTimeline import dev.dimension.flare.data.database.cache.model.DbPagingTimeline +import dev.dimension.flare.data.database.cache.model.DbPagingTimelineWithStatus +import dev.dimension.flare.data.datasource.microblog.paging.BaseTimelineRemoteMediator +import dev.dimension.flare.data.datasource.microblog.paging.PagingRequest +import dev.dimension.flare.data.datasource.microblog.paging.PagingResult import dev.dimension.flare.data.network.bluesky.BlueskyService import dev.dimension.flare.model.AccountType import dev.dimension.flare.model.MicroBlogKey @@ -45,13 +48,13 @@ internal class StatusDetailRemoteMediator( override suspend fun timeline( pageSize: Int, - request: Request, - ): Result { + request: PagingRequest, + ): PagingResult { val result = when (request) { - is Request.Append -> { + is PagingRequest.Append -> { if (statusOnly) { - return Result( + return PagingResult( endOfPaginationReached = true, ) } else { @@ -129,12 +132,12 @@ internal class StatusDetailRemoteMediator( } } } - is Request.Prepend -> { - return Result( + is PagingRequest.Prepend -> { + return PagingResult( endOfPaginationReached = true, ) } - Request.Refresh -> { + PagingRequest.Refresh -> { if (!database.pagingTimelineDao().existsPaging(accountKey, pagingKey)) { database.statusDao().get(statusKey, AccountType.Specific(accountKey)).firstOrNull()?.let { database @@ -165,8 +168,8 @@ internal class StatusDetailRemoteMediator( } } - val shouldLoadMore = !(request is Request.Append || statusOnly) - return Result( + val shouldLoadMore = !(request is PagingRequest.Append || statusOnly) + return PagingResult( endOfPaginationReached = !shouldLoadMore, data = result.toDbPagingTimeline( diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/bluesky/UserLikesTimelineRemoteMediator.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/bluesky/UserLikesTimelineRemoteMediator.kt index 872b39dfa..76041f6c0 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/bluesky/UserLikesTimelineRemoteMediator.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/bluesky/UserLikesTimelineRemoteMediator.kt @@ -2,9 +2,12 @@ package dev.dimension.flare.data.datasource.bluesky import androidx.paging.ExperimentalPagingApi import app.bsky.feed.GetActorLikesQueryParams -import dev.dimension.flare.common.BaseTimelineRemoteMediator import dev.dimension.flare.data.database.cache.CacheDatabase import dev.dimension.flare.data.database.cache.mapper.toDbPagingTimeline +import dev.dimension.flare.data.database.cache.model.DbPagingTimelineWithStatus +import dev.dimension.flare.data.datasource.microblog.paging.BaseTimelineRemoteMediator +import dev.dimension.flare.data.datasource.microblog.paging.PagingRequest +import dev.dimension.flare.data.datasource.microblog.paging.PagingResult import dev.dimension.flare.data.network.bluesky.BlueskyService import dev.dimension.flare.model.MicroBlogKey import sh.christian.ozone.api.Did @@ -21,11 +24,11 @@ internal class UserLikesTimelineRemoteMediator( override suspend fun timeline( pageSize: Int, - request: Request, - ): Result { + request: PagingRequest, + ): PagingResult { val response = when (request) { - Request.Refresh -> + PagingRequest.Refresh -> service .getActorLikes( GetActorLikesQueryParams( @@ -34,13 +37,13 @@ internal class UserLikesTimelineRemoteMediator( ), ).maybeResponse() - is Request.Prepend -> { - return Result( + is PagingRequest.Prepend -> { + return PagingResult( endOfPaginationReached = true, ) } - is Request.Append -> { + is PagingRequest.Append -> { service .getActorLikes( GetActorLikesQueryParams( @@ -50,11 +53,11 @@ internal class UserLikesTimelineRemoteMediator( ), ).maybeResponse() } - } ?: return Result( + } ?: return PagingResult( endOfPaginationReached = true, ) - return Result( + return PagingResult( endOfPaginationReached = response.cursor == null, data = response.feed.toDbPagingTimeline( diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/bluesky/UserTimelineRemoteMediator.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/bluesky/UserTimelineRemoteMediator.kt index e154e1d53..593e16555 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/bluesky/UserTimelineRemoteMediator.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/bluesky/UserTimelineRemoteMediator.kt @@ -3,9 +3,12 @@ package dev.dimension.flare.data.datasource.bluesky import androidx.paging.ExperimentalPagingApi import app.bsky.feed.GetAuthorFeedFilter import app.bsky.feed.GetAuthorFeedQueryParams -import dev.dimension.flare.common.BaseTimelineRemoteMediator import dev.dimension.flare.data.database.cache.CacheDatabase import dev.dimension.flare.data.database.cache.mapper.toDbPagingTimeline +import dev.dimension.flare.data.database.cache.model.DbPagingTimelineWithStatus +import dev.dimension.flare.data.datasource.microblog.paging.BaseTimelineRemoteMediator +import dev.dimension.flare.data.datasource.microblog.paging.PagingRequest +import dev.dimension.flare.data.datasource.microblog.paging.PagingResult import dev.dimension.flare.data.network.bluesky.BlueskyService import dev.dimension.flare.model.MicroBlogKey import sh.christian.ozone.api.Did @@ -36,8 +39,8 @@ internal class UserTimelineRemoteMediator( override suspend fun timeline( pageSize: Int, - request: Request, - ): Result { + request: PagingRequest, + ): PagingResult { val filter = when { onlyMedia -> GetAuthorFeedFilter.PostsWithMedia @@ -46,7 +49,7 @@ internal class UserTimelineRemoteMediator( } val response = when (request) { - Request.Refresh -> + PagingRequest.Refresh -> service .getAuthorFeed( GetAuthorFeedQueryParams( @@ -57,13 +60,13 @@ internal class UserTimelineRemoteMediator( ), ).maybeResponse() - is Request.Prepend -> { - return Result( + is PagingRequest.Prepend -> { + return PagingResult( endOfPaginationReached = true, ) } - is Request.Append -> { + is PagingRequest.Append -> { service .getAuthorFeed( GetAuthorFeedQueryParams( @@ -75,11 +78,11 @@ internal class UserTimelineRemoteMediator( ), ).maybeResponse() } - } ?: return Result( + } ?: return PagingResult( endOfPaginationReached = true, ) - return Result( + return PagingResult( endOfPaginationReached = response.cursor == null, data = response.feed.toDbPagingTimeline( diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/guest/mastodon/GuestMastodonDataSource.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/guest/mastodon/GuestMastodonDataSource.kt index ee43c6936..abf46597b 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/guest/mastodon/GuestMastodonDataSource.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/guest/mastodon/GuestMastodonDataSource.kt @@ -3,7 +3,6 @@ package dev.dimension.flare.data.datasource.guest.mastodon import androidx.paging.Pager import androidx.paging.PagingData import androidx.paging.cachedIn -import dev.dimension.flare.common.BaseTimelinePagingSourceFactory import dev.dimension.flare.common.CacheData import dev.dimension.flare.common.Cacheable import dev.dimension.flare.common.MemCacheable @@ -16,6 +15,7 @@ import dev.dimension.flare.data.datasource.mastodon.SearchUserPagingSource import dev.dimension.flare.data.datasource.mastodon.TrendHashtagPagingSource import dev.dimension.flare.data.datasource.microblog.MicroblogDataSource import dev.dimension.flare.data.datasource.microblog.ProfileTab +import dev.dimension.flare.data.datasource.microblog.paging.BaseTimelinePagingSourceFactory import dev.dimension.flare.data.datasource.microblog.pagingConfig import dev.dimension.flare.data.network.mastodon.GuestMastodonService import dev.dimension.flare.model.MicroBlogKey diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/mastodon/BookmarkTimelineRemoteMediator.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/mastodon/BookmarkTimelineRemoteMediator.kt index 331d4620f..dcde86c39 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/mastodon/BookmarkTimelineRemoteMediator.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/mastodon/BookmarkTimelineRemoteMediator.kt @@ -2,9 +2,12 @@ package dev.dimension.flare.data.datasource.mastodon import SnowflakeIdGenerator import androidx.paging.ExperimentalPagingApi -import dev.dimension.flare.common.BaseTimelineRemoteMediator import dev.dimension.flare.data.database.cache.CacheDatabase import dev.dimension.flare.data.database.cache.mapper.toDbPagingTimeline +import dev.dimension.flare.data.database.cache.model.DbPagingTimelineWithStatus +import dev.dimension.flare.data.datasource.microblog.paging.BaseTimelineRemoteMediator +import dev.dimension.flare.data.datasource.microblog.paging.PagingRequest +import dev.dimension.flare.data.datasource.microblog.paging.PagingResult import dev.dimension.flare.data.network.mastodon.MastodonService import dev.dimension.flare.model.MicroBlogKey @@ -20,24 +23,24 @@ internal class BookmarkTimelineRemoteMediator( override suspend fun timeline( pageSize: Int, - request: Request, - ): Result { + request: PagingRequest, + ): PagingResult { val response = when (request) { - Request.Refresh -> { + PagingRequest.Refresh -> { service .bookmarks( limit = pageSize, ) } - is Request.Prepend -> { - return Result( + is PagingRequest.Prepend -> { + return PagingResult( endOfPaginationReached = true, ) } - is Request.Append -> { + is PagingRequest.Append -> { service.bookmarks( limit = pageSize, max_id = request.nextKey, @@ -45,7 +48,7 @@ internal class BookmarkTimelineRemoteMediator( } } - return Result( + return PagingResult( endOfPaginationReached = response.isEmpty() || response.next == null, data = response.toDbPagingTimeline( diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/mastodon/DiscoverStatusRemoteMediator.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/mastodon/DiscoverStatusRemoteMediator.kt index b1b2219af..ce429243c 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/mastodon/DiscoverStatusRemoteMediator.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/mastodon/DiscoverStatusRemoteMediator.kt @@ -1,9 +1,12 @@ package dev.dimension.flare.data.datasource.mastodon import androidx.paging.ExperimentalPagingApi -import dev.dimension.flare.common.BaseTimelineRemoteMediator import dev.dimension.flare.data.database.cache.CacheDatabase import dev.dimension.flare.data.database.cache.mapper.toDbPagingTimeline +import dev.dimension.flare.data.database.cache.model.DbPagingTimelineWithStatus +import dev.dimension.flare.data.datasource.microblog.paging.BaseTimelineRemoteMediator +import dev.dimension.flare.data.datasource.microblog.paging.PagingRequest +import dev.dimension.flare.data.datasource.microblog.paging.PagingResult import dev.dimension.flare.data.network.mastodon.MastodonService import dev.dimension.flare.model.MicroBlogKey @@ -19,22 +22,22 @@ internal class DiscoverStatusRemoteMediator( override suspend fun timeline( pageSize: Int, - request: Request, - ): Result { + request: PagingRequest, + ): PagingResult { val response = when (request) { - Request.Refresh -> { + PagingRequest.Refresh -> { service.trendsStatuses() } else -> { - return Result( + return PagingResult( endOfPaginationReached = true, ) } } - return Result( + return PagingResult( endOfPaginationReached = true, data = response.toDbPagingTimeline( diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/mastodon/FavouriteTimelineRemoteMediator.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/mastodon/FavouriteTimelineRemoteMediator.kt index 017d3b474..829afb139 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/mastodon/FavouriteTimelineRemoteMediator.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/mastodon/FavouriteTimelineRemoteMediator.kt @@ -1,9 +1,12 @@ package dev.dimension.flare.data.datasource.mastodon import androidx.paging.ExperimentalPagingApi -import dev.dimension.flare.common.BaseTimelineRemoteMediator import dev.dimension.flare.data.database.cache.CacheDatabase import dev.dimension.flare.data.database.cache.mapper.toDbPagingTimeline +import dev.dimension.flare.data.database.cache.model.DbPagingTimelineWithStatus +import dev.dimension.flare.data.datasource.microblog.paging.BaseTimelineRemoteMediator +import dev.dimension.flare.data.datasource.microblog.paging.PagingRequest +import dev.dimension.flare.data.datasource.microblog.paging.PagingResult import dev.dimension.flare.data.network.mastodon.MastodonService import dev.dimension.flare.model.MicroBlogKey @@ -19,24 +22,24 @@ internal class FavouriteTimelineRemoteMediator( override suspend fun timeline( pageSize: Int, - request: Request, - ): Result { + request: PagingRequest, + ): PagingResult { val response = when (request) { - Request.Refresh -> { + PagingRequest.Refresh -> { service .favorites( limit = pageSize, ) } - is Request.Prepend -> { - return Result( + is PagingRequest.Prepend -> { + return PagingResult( endOfPaginationReached = true, ) } - is Request.Append -> { + is PagingRequest.Append -> { service.favorites( limit = pageSize, max_id = request.nextKey, @@ -44,7 +47,7 @@ internal class FavouriteTimelineRemoteMediator( } } - return Result( + return PagingResult( endOfPaginationReached = response.isEmpty() || response.next == null, data = response.toDbPagingTimeline( diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/mastodon/HomeTimelineRemoteMediator.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/mastodon/HomeTimelineRemoteMediator.kt index 6ae953376..2177a747b 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/mastodon/HomeTimelineRemoteMediator.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/mastodon/HomeTimelineRemoteMediator.kt @@ -1,9 +1,12 @@ package dev.dimension.flare.data.datasource.mastodon import androidx.paging.ExperimentalPagingApi -import dev.dimension.flare.common.BaseTimelineRemoteMediator import dev.dimension.flare.data.database.cache.CacheDatabase import dev.dimension.flare.data.database.cache.mapper.toDbPagingTimeline +import dev.dimension.flare.data.database.cache.model.DbPagingTimelineWithStatus +import dev.dimension.flare.data.datasource.microblog.paging.BaseTimelineRemoteMediator +import dev.dimension.flare.data.datasource.microblog.paging.PagingRequest +import dev.dimension.flare.data.datasource.microblog.paging.PagingResult import dev.dimension.flare.data.network.mastodon.MastodonService import dev.dimension.flare.model.MicroBlogKey @@ -21,25 +24,25 @@ internal class HomeTimelineRemoteMediator( override suspend fun timeline( pageSize: Int, - request: Request, - ): Result { + request: PagingRequest, + ): PagingResult { val response = when (request) { - Request.Refresh -> { + PagingRequest.Refresh -> { service .homeTimeline( limit = pageSize, ) } - is Request.Prepend -> { + is PagingRequest.Prepend -> { service.homeTimeline( limit = pageSize, min_id = request.previousKey, ) } - is Request.Append -> { + is PagingRequest.Append -> { service.homeTimeline( limit = pageSize, max_id = request.nextKey, @@ -47,7 +50,7 @@ internal class HomeTimelineRemoteMediator( } } - return Result( + return PagingResult( endOfPaginationReached = response.isEmpty(), data = response.toDbPagingTimeline( diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/mastodon/ListTimelineRemoteMediator.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/mastodon/ListTimelineRemoteMediator.kt index 7050aec90..03f6c3bcf 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/mastodon/ListTimelineRemoteMediator.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/mastodon/ListTimelineRemoteMediator.kt @@ -1,9 +1,12 @@ package dev.dimension.flare.data.datasource.mastodon import androidx.paging.ExperimentalPagingApi -import dev.dimension.flare.common.BaseTimelineRemoteMediator import dev.dimension.flare.data.database.cache.CacheDatabase import dev.dimension.flare.data.database.cache.mapper.toDbPagingTimeline +import dev.dimension.flare.data.database.cache.model.DbPagingTimelineWithStatus +import dev.dimension.flare.data.datasource.microblog.paging.BaseTimelineRemoteMediator +import dev.dimension.flare.data.datasource.microblog.paging.PagingRequest +import dev.dimension.flare.data.datasource.microblog.paging.PagingResult import dev.dimension.flare.data.network.mastodon.MastodonService import dev.dimension.flare.model.MicroBlogKey @@ -20,11 +23,11 @@ internal class ListTimelineRemoteMediator( override suspend fun timeline( pageSize: Int, - request: Request, - ): Result { + request: PagingRequest, + ): PagingResult { val response = when (request) { - Request.Refresh -> { + PagingRequest.Refresh -> { service .listTimeline( listId = listId, @@ -32,13 +35,13 @@ internal class ListTimelineRemoteMediator( ) } - is Request.Prepend -> { - return Result( + is PagingRequest.Prepend -> { + return PagingResult( endOfPaginationReached = true, ) } - is Request.Append -> { + is PagingRequest.Append -> { service.listTimeline( listId = listId, limit = pageSize, @@ -47,7 +50,7 @@ internal class ListTimelineRemoteMediator( } } - return Result( + return PagingResult( endOfPaginationReached = response.isEmpty(), data = response.toDbPagingTimeline( diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/mastodon/MastodonDataSource.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/mastodon/MastodonDataSource.kt index ee887780e..6da4b2d9f 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/mastodon/MastodonDataSource.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/mastodon/MastodonDataSource.kt @@ -1,12 +1,9 @@ package dev.dimension.flare.data.datasource.mastodon import androidx.paging.ExperimentalPagingApi -import androidx.paging.LoadType import androidx.paging.Pager import androidx.paging.PagingData -import androidx.paging.PagingState import androidx.paging.cachedIn -import dev.dimension.flare.common.BaseRemoteMediator import dev.dimension.flare.common.CacheData import dev.dimension.flare.common.Cacheable import dev.dimension.flare.common.FileType @@ -23,23 +20,19 @@ import dev.dimension.flare.data.datasource.microblog.ComposeConfig import dev.dimension.flare.data.datasource.microblog.ComposeData import dev.dimension.flare.data.datasource.microblog.ComposeProgress import dev.dimension.flare.data.datasource.microblog.ComposeType -import dev.dimension.flare.data.datasource.microblog.ListDataSource -import dev.dimension.flare.data.datasource.microblog.ListMetaData -import dev.dimension.flare.data.datasource.microblog.ListMetaDataType -import dev.dimension.flare.data.datasource.microblog.MemoryPagingSource import dev.dimension.flare.data.datasource.microblog.NotificationFilter import dev.dimension.flare.data.datasource.microblog.ProfileAction import dev.dimension.flare.data.datasource.microblog.ProfileTab import dev.dimension.flare.data.datasource.microblog.RelationDataSource import dev.dimension.flare.data.datasource.microblog.StatusEvent -import dev.dimension.flare.data.datasource.microblog.memoryPager +import dev.dimension.flare.data.datasource.microblog.list.ListDataSource +import dev.dimension.flare.data.datasource.microblog.list.ListLoader +import dev.dimension.flare.data.datasource.microblog.list.ListMemberLoader import dev.dimension.flare.data.datasource.microblog.pagingConfig import dev.dimension.flare.data.datasource.microblog.relationKeyWithUserKey import dev.dimension.flare.data.datasource.microblog.timelinePager import dev.dimension.flare.data.datasource.pleroma.PleromaDataSource import dev.dimension.flare.data.network.mastodon.MastodonService -import dev.dimension.flare.data.network.mastodon.api.model.PostAccounts -import dev.dimension.flare.data.network.mastodon.api.model.PostList import dev.dimension.flare.data.network.mastodon.api.model.PostPoll import dev.dimension.flare.data.network.mastodon.api.model.PostReport import dev.dimension.flare.data.network.mastodon.api.model.PostStatus @@ -55,7 +48,6 @@ import dev.dimension.flare.shared.image.ImageCompressor import dev.dimension.flare.ui.model.UiAccount import dev.dimension.flare.ui.model.UiEmoji import dev.dimension.flare.ui.model.UiHashtag -import dev.dimension.flare.ui.model.UiList import dev.dimension.flare.ui.model.UiProfile import dev.dimension.flare.ui.model.UiRelation import dev.dimension.flare.ui.model.UiState @@ -67,7 +59,6 @@ import dev.dimension.flare.ui.model.toUi import dev.dimension.flare.ui.presenter.compose.ComposeStatus import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableMap -import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableMap import kotlinx.collections.immutable.toPersistentList @@ -915,255 +906,18 @@ internal open class MastodonDataSource( }, ) - private val listKey: String - get() = "allLists_$accountKey" - - override fun myList(scope: CoroutineScope): Flow> = - memoryPager( - pageSize = 20, - pagingKey = listKey, - scope = scope, - mediator = - object : BaseRemoteMediator() { - override suspend fun doLoad( - loadType: LoadType, - state: PagingState, - ): MediatorResult { - if (loadType == LoadType.PREPEND) { - return MediatorResult.Success(endOfPaginationReached = true) - } - val result = - service - .lists() - .mapNotNull { - it.id?.let { it1 -> - it.render() - } - }.toImmutableList() - - MemoryPagingSource.update( - key = listKey, - value = result, - ) - - return MediatorResult.Success( - endOfPaginationReached = true, - ) - } - }, + override val listLoader: ListLoader by lazy { + MastodonListLoader( + service = service, + accountKey = accountKey, ) - - suspend fun createList(title: String) { - tryRun { - service.createList(PostList(title = title)) - }.onSuccess { response -> - if (response.id != null) { - MemoryPagingSource.updateWith( - key = listKey, - ) { - it - .plus( - UiList.List( - id = response.id, - title = title, - ), - ).toImmutableList() - } - } - } } - override suspend fun deleteList(listId: String) { - tryRun { - service.deleteList(listId) - }.onSuccess { - MemoryPagingSource.updateWith( - key = listKey, - ) { - it - .filter { list -> list.id != listId } - .toImmutableList() - } - } - } - - private suspend fun updateList( - listId: String, - title: String, - ) { - tryRun { - service.updateList(listId, PostList(title = title)) - }.onSuccess { - MemoryPagingSource.updateWith( - key = listKey, - ) { - it - .map { list -> - if (list.id == listId) { - list.copy(title = title) - } else { - list - } - }.toImmutableList() - } - } - } - - override fun listInfo(listId: String): CacheData = - MemCacheable( - key = "listInfo_$listId", - fetchSource = { - service.getList(listId).render() - }, - ) - - private fun listMemberKey(listId: String) = "listMembers_$listId" - - override fun listMembers( - listId: String, - scope: CoroutineScope, - pageSize: Int, - ): Flow> = - memoryPager( - pageSize = pageSize, - pagingKey = listMemberKey(listId), - scope = scope, - mediator = - object : BaseRemoteMediator() { - override suspend fun doLoad( - loadType: LoadType, - state: PagingState, - ): MediatorResult { - if (loadType == LoadType.PREPEND) { - return MediatorResult.Success(endOfPaginationReached = true) - } - val key = - if (loadType == LoadType.REFRESH) { - null - } else { - MemoryPagingSource - .get(key = listMemberKey(listId)) - ?.lastOrNull() - ?.key - ?.id - } - val result = - service - .listMembers(listId, limit = pageSize, max_id = key) - .map { - it.toDbUser(accountKey.host).render(accountKey) - } - - if (loadType == LoadType.REFRESH) { - MemoryPagingSource.update( - key = listMemberKey(listId), - value = result.toImmutableList(), - ) - } else if (loadType == LoadType.APPEND) { - MemoryPagingSource.append( - key = listMemberKey(listId), - value = result.toImmutableList(), - ) - } - - return MediatorResult.Success( - endOfPaginationReached = result.isEmpty(), - ) - } - }, + override val listMemberLoader: ListMemberLoader by lazy { + MastodonListMemberLoader( + service = service, + accountKey = accountKey, ) - - override suspend fun addMember( - listId: String, - userKey: MicroBlogKey, - ) { - tryRun { - service.addMember( - listId, - PostAccounts(listOf(userKey.id)), - ) - val user = - service - .lookupUser(userKey.id) - .toDbUser(accountKey.host) - .render(accountKey) - MemoryPagingSource.updateWith( - key = listMemberKey(listId), - ) { - (listOf(user) + it) - .distinctBy { - it.key - }.toImmutableList() - } - val list = service.getList(listId) - if (list.id != null) { - MemCacheable.updateWith>( - key = userListsKey(userKey), - ) { - it - .plus(list.render()) - .toImmutableList() - } - } - } - } - - override suspend fun removeMember( - listId: String, - userKey: MicroBlogKey, - ) { - tryRun { - service.removeMember( - listId, - PostAccounts(listOf(userKey.id)), - ) - MemoryPagingSource.updateWith( - key = listMemberKey(listId), - ) { - it - .filter { user -> user.key.id != userKey.id } - .toImmutableList() - } - MemCacheable.updateWith>( - key = userListsKey(userKey), - ) { - it - .filter { list -> list.id != listId } - .toImmutableList() - } - } - } - - override fun listMemberCache(listId: String): Flow> = - MemoryPagingSource.getFlow(listMemberKey(listId)) - - private fun userListsKey(userKey: MicroBlogKey) = "userLists_${userKey.id}" - - override fun userLists(userKey: MicroBlogKey): MemCacheable> = - MemCacheable( - key = userListsKey(userKey), - ) { - service - .accountLists(userKey.id) - .mapNotNull { - it.id?.let { _ -> - it.render() - } - }.toImmutableList() - } - - override val supportedMetaData: ImmutableList - get() = persistentListOf(ListMetaDataType.TITLE) - - override suspend fun createList(metaData: ListMetaData) { - createList(metaData.title) - } - - override suspend fun updateList( - listId: String, - metaData: ListMetaData, - ) { - updateList(listId, metaData.title) } private val notificationMarkerKey: String diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/mastodon/MastodonListLoader.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/mastodon/MastodonListLoader.kt new file mode 100644 index 000000000..23c25eafa --- /dev/null +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/mastodon/MastodonListLoader.kt @@ -0,0 +1,67 @@ +package dev.dimension.flare.data.datasource.mastodon + +import dev.dimension.flare.data.datasource.microblog.list.ListLoader +import dev.dimension.flare.data.datasource.microblog.list.ListMetaData +import dev.dimension.flare.data.datasource.microblog.list.ListMetaDataType +import dev.dimension.flare.data.datasource.microblog.paging.PagingRequest +import dev.dimension.flare.data.datasource.microblog.paging.PagingResult +import dev.dimension.flare.data.network.mastodon.MastodonService +import dev.dimension.flare.data.network.mastodon.api.model.MastodonList +import dev.dimension.flare.data.network.mastodon.api.model.PostList +import dev.dimension.flare.model.MicroBlogKey +import dev.dimension.flare.ui.model.UiList +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf + +internal class MastodonListLoader( + private val service: MastodonService, + private val accountKey: MicroBlogKey, +) : ListLoader { + override suspend fun load( + pageSize: Int, + request: PagingRequest, + ): PagingResult { + if (request is PagingRequest.Prepend) { + return PagingResult() + } + val lists = service.lists() + return PagingResult( + data = lists.mapNotNull { it.toUiList(accountKey) }, + ) + } + + override suspend fun info(listKey: MicroBlogKey): UiList = + service.getList(listKey.id).toUiList(accountKey) + ?: error("Failed to parse list info") + + override suspend fun create(metaData: ListMetaData): UiList = + service + .createList(PostList(title = metaData.title)) + .toUiList(accountKey) + ?: error("Failed to parse created list") + + override suspend fun update( + listKey: MicroBlogKey, + metaData: ListMetaData, + ): UiList = + service + .updateList(listKey.id, PostList(title = metaData.title)) + .toUiList(accountKey) + ?: error("Failed to parse updated list") + + override suspend fun delete(listKey: MicroBlogKey) { + service.deleteList(listKey.id) + } + + override val supportedMetaData: ImmutableList + get() = persistentListOf(ListMetaDataType.TITLE) + + private fun MastodonList.toUiList(accountKey: MicroBlogKey): UiList.List? { + val id = id ?: return null + val title = title ?: return null + return UiList.List( + key = MicroBlogKey(id = id, host = accountKey.host), + title = title, + ) + } +} diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/mastodon/MastodonListMemberLoader.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/mastodon/MastodonListMemberLoader.kt new file mode 100644 index 000000000..0f608149c --- /dev/null +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/mastodon/MastodonListMemberLoader.kt @@ -0,0 +1,97 @@ +package dev.dimension.flare.data.datasource.mastodon + +import dev.dimension.flare.data.database.cache.mapper.toDbUser +import dev.dimension.flare.data.database.cache.model.DbUser +import dev.dimension.flare.data.datasource.microblog.list.ListMemberLoader +import dev.dimension.flare.data.datasource.microblog.paging.PagingRequest +import dev.dimension.flare.data.datasource.microblog.paging.PagingResult +import dev.dimension.flare.data.network.mastodon.MastodonService +import dev.dimension.flare.data.network.mastodon.api.model.MastodonList +import dev.dimension.flare.data.network.mastodon.api.model.PostAccounts +import dev.dimension.flare.model.MicroBlogKey +import dev.dimension.flare.ui.model.UiList + +internal class MastodonListMemberLoader( + private val service: MastodonService, + private val accountKey: MicroBlogKey, +) : ListMemberLoader { + override suspend fun loadMembers( + pageSize: Int, + request: PagingRequest, + listKey: MicroBlogKey, + ): PagingResult { + val maxId = + when (request) { + is PagingRequest.Append -> request.nextKey + is PagingRequest.Refresh -> null + is PagingRequest.Prepend -> return PagingResult() + } + + val response = + service.listMembers( + listId = listKey.id, + limit = pageSize, + max_id = maxId, + ) + + val users = + response.map { + it.toDbUser(accountKey.host) + } + + return PagingResult( + data = users, + nextKey = response.next, + ) + } + + override suspend fun addMember( + listKey: MicroBlogKey, + userKey: MicroBlogKey, + ): DbUser { + service.addMember( + listId = listKey.id, + accounts = PostAccounts(listOf(userKey.id)), + ) + return service + .lookupUser(userKey.id) + .toDbUser(accountKey.host) + } + + override suspend fun removeMember( + listKey: MicroBlogKey, + userKey: MicroBlogKey, + ) { + service.removeMember( + listId = listKey.id, + accounts = PostAccounts(listOf(userKey.id)), + ) + } + + override suspend fun loadUserLists( + pageSize: Int, + request: PagingRequest, + userKey: MicroBlogKey, + ): PagingResult { + if (request is PagingRequest.Prepend) { + return PagingResult() + } + val response = service.accountLists(userKey.id) + val lists = + response.mapNotNull { + it.toUiList(accountKey) + } + return PagingResult( + data = lists, + ) + } + + private fun MastodonList.toUiList(accountKey: MicroBlogKey): UiList.List? { + val id = id ?: return null + val title = title ?: return null + return UiList.List( + key = MicroBlogKey(id = id, host = accountKey.host), + title = title, + ) + } +} diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/mastodon/MentionRemoteMediator.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/mastodon/MentionRemoteMediator.kt index af8331c65..1d1b32614 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/mastodon/MentionRemoteMediator.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/mastodon/MentionRemoteMediator.kt @@ -1,9 +1,12 @@ package dev.dimension.flare.data.datasource.mastodon import androidx.paging.ExperimentalPagingApi -import dev.dimension.flare.common.BaseTimelineRemoteMediator import dev.dimension.flare.data.database.cache.CacheDatabase import dev.dimension.flare.data.database.cache.mapper.toDb +import dev.dimension.flare.data.database.cache.model.DbPagingTimelineWithStatus +import dev.dimension.flare.data.datasource.microblog.paging.BaseTimelineRemoteMediator +import dev.dimension.flare.data.datasource.microblog.paging.PagingRequest +import dev.dimension.flare.data.datasource.microblog.paging.PagingResult import dev.dimension.flare.data.network.mastodon.MastodonService import dev.dimension.flare.data.network.mastodon.api.model.NotificationTypes import dev.dimension.flare.model.MicroBlogKey @@ -20,11 +23,11 @@ internal class MentionRemoteMediator( override suspend fun timeline( pageSize: Int, - request: Request, - ): Result { + request: PagingRequest, + ): PagingResult { val response = when (request) { - Request.Refresh -> { + PagingRequest.Refresh -> { service .notification( limit = pageSize, @@ -32,7 +35,7 @@ internal class MentionRemoteMediator( ) } - is Request.Prepend -> { + is PagingRequest.Prepend -> { service.notification( limit = pageSize, min_id = request.previousKey, @@ -40,7 +43,7 @@ internal class MentionRemoteMediator( ) } - is Request.Append -> { + is PagingRequest.Append -> { service.notification( limit = pageSize, max_id = request.nextKey, @@ -49,7 +52,7 @@ internal class MentionRemoteMediator( } } - return Result( + return PagingResult( endOfPaginationReached = response.isEmpty(), data = response.toDb( diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/mastodon/NotificationRemoteMediator.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/mastodon/NotificationRemoteMediator.kt index 00d934f1d..71a596eec 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/mastodon/NotificationRemoteMediator.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/mastodon/NotificationRemoteMediator.kt @@ -1,9 +1,12 @@ package dev.dimension.flare.data.datasource.mastodon import androidx.paging.ExperimentalPagingApi -import dev.dimension.flare.common.BaseTimelineRemoteMediator import dev.dimension.flare.data.database.cache.CacheDatabase import dev.dimension.flare.data.database.cache.mapper.toDb +import dev.dimension.flare.data.database.cache.model.DbPagingTimelineWithStatus +import dev.dimension.flare.data.datasource.microblog.paging.BaseTimelineRemoteMediator +import dev.dimension.flare.data.datasource.microblog.paging.PagingRequest +import dev.dimension.flare.data.datasource.microblog.paging.PagingResult import dev.dimension.flare.data.network.mastodon.MastodonService import dev.dimension.flare.data.network.mastodon.api.model.MarkerUpdate import dev.dimension.flare.data.network.mastodon.api.model.UpdateContent @@ -22,11 +25,11 @@ internal class NotificationRemoteMediator( override suspend fun timeline( pageSize: Int, - request: Request, - ): Result { + request: PagingRequest, + ): PagingResult { val response = when (request) { - Request.Refresh -> { + PagingRequest.Refresh -> { service .notification( limit = pageSize, @@ -38,14 +41,14 @@ internal class NotificationRemoteMediator( } } - is Request.Prepend -> { + is PagingRequest.Prepend -> { service.notification( limit = pageSize, min_id = request.previousKey, ) } - is Request.Append -> { + is PagingRequest.Append -> { service.notification( limit = pageSize, max_id = request.nextKey, @@ -53,7 +56,7 @@ internal class NotificationRemoteMediator( } } - return Result( + return PagingResult( endOfPaginationReached = response.isEmpty(), data = response.toDb( diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/mastodon/PublicTimelineRemoteMediator.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/mastodon/PublicTimelineRemoteMediator.kt index 1118a4906..a2856540a 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/mastodon/PublicTimelineRemoteMediator.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/mastodon/PublicTimelineRemoteMediator.kt @@ -1,9 +1,12 @@ package dev.dimension.flare.data.datasource.mastodon import androidx.paging.ExperimentalPagingApi -import dev.dimension.flare.common.BaseTimelineRemoteMediator import dev.dimension.flare.data.database.cache.CacheDatabase import dev.dimension.flare.data.database.cache.mapper.toDbPagingTimeline +import dev.dimension.flare.data.database.cache.model.DbPagingTimelineWithStatus +import dev.dimension.flare.data.datasource.microblog.paging.BaseTimelineRemoteMediator +import dev.dimension.flare.data.datasource.microblog.paging.PagingRequest +import dev.dimension.flare.data.datasource.microblog.paging.PagingResult import dev.dimension.flare.data.network.mastodon.MastodonService import dev.dimension.flare.model.MicroBlogKey @@ -27,11 +30,11 @@ internal class PublicTimelineRemoteMediator( override suspend fun timeline( pageSize: Int, - request: Request, - ): Result { + request: PagingRequest, + ): PagingResult { val response = when (request) { - Request.Refresh -> { + PagingRequest.Refresh -> { service .publicTimeline( limit = pageSize, @@ -39,13 +42,13 @@ internal class PublicTimelineRemoteMediator( ) } - is Request.Prepend -> { - return Result( + is PagingRequest.Prepend -> { + return PagingResult( endOfPaginationReached = true, ) } - is Request.Append -> { + is PagingRequest.Append -> { service.publicTimeline( limit = pageSize, max_id = request.nextKey, @@ -54,7 +57,7 @@ internal class PublicTimelineRemoteMediator( } } - return Result( + return PagingResult( endOfPaginationReached = response.isEmpty(), data = response.toDbPagingTimeline( diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/mastodon/SearchStatusPagingSource.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/mastodon/SearchStatusPagingSource.kt index 767bb69c4..2ffdc03b5 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/mastodon/SearchStatusPagingSource.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/mastodon/SearchStatusPagingSource.kt @@ -1,9 +1,12 @@ package dev.dimension.flare.data.datasource.mastodon import androidx.paging.ExperimentalPagingApi -import dev.dimension.flare.common.BaseTimelineRemoteMediator import dev.dimension.flare.data.database.cache.CacheDatabase import dev.dimension.flare.data.database.cache.mapper.toDbPagingTimeline +import dev.dimension.flare.data.database.cache.model.DbPagingTimelineWithStatus +import dev.dimension.flare.data.datasource.microblog.paging.BaseTimelineRemoteMediator +import dev.dimension.flare.data.datasource.microblog.paging.PagingRequest +import dev.dimension.flare.data.datasource.microblog.paging.PagingResult import dev.dimension.flare.data.network.mastodon.MastodonService import dev.dimension.flare.model.MicroBlogKey @@ -25,17 +28,17 @@ internal class SearchStatusPagingSource( override suspend fun timeline( pageSize: Int, - request: Request, - ): Result { + request: PagingRequest, + ): PagingResult { val response = when (request) { - is Request.Prepend -> { - return Result( + is PagingRequest.Prepend -> { + return PagingResult( endOfPaginationReached = true, ) } - Request.Refresh -> { + PagingRequest.Refresh -> { if (query.startsWith("#")) { service.hashtagTimeline( hashtag = query.removePrefix("#"), @@ -51,7 +54,7 @@ internal class SearchStatusPagingSource( } } - is Request.Append -> { + is PagingRequest.Append -> { if (query.startsWith("#")) { service.hashtagTimeline( hashtag = query.removePrefix("#"), @@ -70,7 +73,7 @@ internal class SearchStatusPagingSource( } } ?: emptyList() - return Result( + return PagingResult( endOfPaginationReached = response.isEmpty(), data = response.toDbPagingTimeline( diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/mastodon/StatusDetailRemoteMediator.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/mastodon/StatusDetailRemoteMediator.kt index 34d1da636..bb6b09f3c 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/mastodon/StatusDetailRemoteMediator.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/mastodon/StatusDetailRemoteMediator.kt @@ -1,11 +1,14 @@ package dev.dimension.flare.data.datasource.mastodon import androidx.paging.ExperimentalPagingApi -import dev.dimension.flare.common.BaseTimelineRemoteMediator import dev.dimension.flare.data.database.cache.CacheDatabase import dev.dimension.flare.data.database.cache.connect import dev.dimension.flare.data.database.cache.mapper.toDbPagingTimeline import dev.dimension.flare.data.database.cache.model.DbPagingTimeline +import dev.dimension.flare.data.database.cache.model.DbPagingTimelineWithStatus +import dev.dimension.flare.data.datasource.microblog.paging.BaseTimelineRemoteMediator +import dev.dimension.flare.data.datasource.microblog.paging.PagingRequest +import dev.dimension.flare.data.datasource.microblog.paging.PagingResult import dev.dimension.flare.data.network.mastodon.MastodonService import dev.dimension.flare.model.AccountType import dev.dimension.flare.model.MicroBlogKey @@ -34,13 +37,13 @@ internal class StatusDetailRemoteMediator( override suspend fun timeline( pageSize: Int, - request: Request, - ): Result { + request: PagingRequest, + ): PagingResult { val result = when (request) { - is Request.Append -> { + is PagingRequest.Append -> { if (statusOnly) { - return Result( + return PagingResult( endOfPaginationReached = true, ) } else { @@ -55,11 +58,11 @@ internal class StatusDetailRemoteMediator( context.ancestors.orEmpty() + listOf(current) + context.descendants.orEmpty() } } - is Request.Prepend -> - return Result( + is PagingRequest.Prepend -> + return PagingResult( endOfPaginationReached = true, ) - Request.Refresh -> { + PagingRequest.Refresh -> { val exists = database.pagingTimelineDao().existsPaging(accountKey, pagingKey) if (!exists) { val status = database.statusDao().get(statusKey, AccountType.Specific(accountKey)).firstOrNull() @@ -87,9 +90,9 @@ internal class StatusDetailRemoteMediator( listOf(current) } } - val shouldLoadMore = !(request is Request.Append || statusOnly) + val shouldLoadMore = !(request is PagingRequest.Append || statusOnly) - return Result( + return PagingResult( endOfPaginationReached = !shouldLoadMore, data = result.toDbPagingTimeline( diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/mastodon/UserTimelineRemoteMediator.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/mastodon/UserTimelineRemoteMediator.kt index e60a8b2a6..8e38aa7aa 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/mastodon/UserTimelineRemoteMediator.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/mastodon/UserTimelineRemoteMediator.kt @@ -1,9 +1,12 @@ package dev.dimension.flare.data.datasource.mastodon import androidx.paging.ExperimentalPagingApi -import dev.dimension.flare.common.BaseTimelineRemoteMediator import dev.dimension.flare.data.database.cache.CacheDatabase import dev.dimension.flare.data.database.cache.mapper.toDbPagingTimeline +import dev.dimension.flare.data.database.cache.model.DbPagingTimelineWithStatus +import dev.dimension.flare.data.datasource.microblog.paging.BaseTimelineRemoteMediator +import dev.dimension.flare.data.datasource.microblog.paging.PagingRequest +import dev.dimension.flare.data.datasource.microblog.paging.PagingResult import dev.dimension.flare.data.network.mastodon.MastodonService import dev.dimension.flare.model.MicroBlogKey @@ -37,11 +40,11 @@ internal class UserTimelineRemoteMediator( override suspend fun timeline( pageSize: Int, - request: Request, - ): Result { + request: PagingRequest, + ): PagingResult { val response = when (request) { - Request.Refresh -> { + PagingRequest.Refresh -> { val pinned = if (withPinned) { service.userTimeline( @@ -61,13 +64,13 @@ internal class UserTimelineRemoteMediator( ) + pinned } - is Request.Prepend -> { - return Result( + is PagingRequest.Prepend -> { + return PagingResult( endOfPaginationReached = true, ) } - is Request.Append -> { + is PagingRequest.Append -> { service.userTimeline( user_id = userKey.id, limit = pageSize, @@ -78,7 +81,7 @@ internal class UserTimelineRemoteMediator( } } - return Result( + return PagingResult( endOfPaginationReached = response.isEmpty(), data = response.toDbPagingTimeline( diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/microblog/ListDataSource.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/microblog/ListDataSource.kt deleted file mode 100644 index 53b6e99a8..000000000 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/microblog/ListDataSource.kt +++ /dev/null @@ -1,64 +0,0 @@ -package dev.dimension.flare.data.datasource.microblog - -import androidx.paging.PagingData -import dev.dimension.flare.common.BaseTimelineLoader -import dev.dimension.flare.common.CacheData -import dev.dimension.flare.common.FileItem -import dev.dimension.flare.common.MemCacheable -import dev.dimension.flare.model.MicroBlogKey -import dev.dimension.flare.ui.model.UiList -import dev.dimension.flare.ui.model.UiUserV2 -import kotlinx.collections.immutable.ImmutableList -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.flow.Flow - -internal interface ListDataSource { - fun myList(scope: CoroutineScope): Flow> - - fun listInfo(listId: String): CacheData - - fun listMembers( - listId: String, - scope: CoroutineScope, - pageSize: Int = 20, - ): Flow> - - suspend fun addMember( - listId: String, - userKey: MicroBlogKey, - ) - - suspend fun removeMember( - listId: String, - userKey: MicroBlogKey, - ) - - fun listTimeline(listId: String): BaseTimelineLoader - - suspend fun deleteList(listId: String) - - val supportedMetaData: ImmutableList - - suspend fun updateList( - listId: String, - metaData: ListMetaData, - ) - - suspend fun createList(metaData: ListMetaData) - - fun listMemberCache(listId: String): Flow> - - fun userLists(userKey: MicroBlogKey): MemCacheable> -} - -public data class ListMetaData( - val title: String, - val description: String? = null, - val avatar: FileItem? = null, -) - -public enum class ListMetaDataType { - TITLE, - DESCRIPTION, - AVATAR, -} diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/microblog/MicroblogDataSource.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/microblog/MicroblogDataSource.kt index 42fd3f531..0c11b6fe3 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/microblog/MicroblogDataSource.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/microblog/MicroblogDataSource.kt @@ -1,8 +1,8 @@ package dev.dimension.flare.data.datasource.microblog import androidx.paging.PagingData -import dev.dimension.flare.common.BaseTimelineLoader import dev.dimension.flare.common.CacheData +import dev.dimension.flare.data.datasource.microblog.paging.BaseTimelineLoader import dev.dimension.flare.model.MicroBlogKey import dev.dimension.flare.ui.model.UiHashtag import dev.dimension.flare.ui.model.UiProfile diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/microblog/MixedRemoteMediator.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/microblog/MixedRemoteMediator.kt index 22f0a3539..d7de007b8 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/microblog/MixedRemoteMediator.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/microblog/MixedRemoteMediator.kt @@ -2,9 +2,12 @@ package dev.dimension.flare.data.datasource.microblog import SnowflakeIdGenerator import androidx.paging.ExperimentalPagingApi -import dev.dimension.flare.common.BaseTimelineRemoteMediator import dev.dimension.flare.data.database.cache.CacheDatabase import dev.dimension.flare.data.database.cache.connect +import dev.dimension.flare.data.database.cache.model.DbPagingTimelineWithStatus +import dev.dimension.flare.data.datasource.microblog.paging.BaseTimelineRemoteMediator +import dev.dimension.flare.data.datasource.microblog.paging.PagingRequest +import dev.dimension.flare.data.datasource.microblog.paging.PagingResult import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll import kotlinx.coroutines.coroutineScope @@ -26,8 +29,8 @@ internal class MixedRemoteMediator( @OptIn(ExperimentalPagingApi::class) override suspend fun timeline( pageSize: Int, - request: Request, - ): Result = + request: PagingRequest, + ): PagingResult = coroutineScope { if (request is Request.Prepend) { Result(endOfPaginationReached = true) diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/microblog/Paging.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/microblog/Paging.kt index 1f8dadb9f..46bd26409 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/microblog/Paging.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/microblog/Paging.kt @@ -10,8 +10,8 @@ import androidx.paging.filter import androidx.paging.map import dev.dimension.flare.common.BasePagingSource import dev.dimension.flare.common.BaseRemoteMediator -import dev.dimension.flare.common.BaseTimelineRemoteMediator import dev.dimension.flare.data.database.cache.CacheDatabase +import dev.dimension.flare.data.datasource.microblog.paging.BaseTimelineRemoteMediator import dev.dimension.flare.data.repository.AccountRepository import dev.dimension.flare.model.AccountType import dev.dimension.flare.ui.model.UiTimeline diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/microblog/ProfileTab.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/microblog/ProfileTab.kt index 435804598..81b5475c4 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/microblog/ProfileTab.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/microblog/ProfileTab.kt @@ -1,7 +1,7 @@ package dev.dimension.flare.data.datasource.microblog import androidx.compose.runtime.Immutable -import dev.dimension.flare.common.BaseTimelineLoader +import dev.dimension.flare.data.datasource.microblog.paging.BaseTimelineLoader @Immutable public sealed interface ProfileTab { diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/microblog/list/ListDataSource.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/microblog/list/ListDataSource.kt new file mode 100644 index 000000000..6b6ba68fe --- /dev/null +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/microblog/list/ListDataSource.kt @@ -0,0 +1,10 @@ +package dev.dimension.flare.data.datasource.microblog.list + +import dev.dimension.flare.data.datasource.microblog.paging.BaseTimelineLoader + +internal interface ListDataSource { + fun listTimeline(listId: String): BaseTimelineLoader + + val listLoader: ListLoader + val listMemberLoader: ListMemberLoader +} diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/microblog/list/ListHandler.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/microblog/list/ListHandler.kt new file mode 100644 index 000000000..7e202b257 --- /dev/null +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/microblog/list/ListHandler.kt @@ -0,0 +1,165 @@ +package dev.dimension.flare.data.datasource.microblog.list + +import androidx.paging.ExperimentalPagingApi +import androidx.paging.Pager +import androidx.paging.map +import dev.dimension.flare.common.CacheData +import dev.dimension.flare.common.Cacheable +import dev.dimension.flare.data.database.cache.CacheDatabase +import dev.dimension.flare.data.database.cache.connect +import dev.dimension.flare.data.database.cache.model.DbList +import dev.dimension.flare.data.database.cache.model.DbListPaging +import dev.dimension.flare.data.datasource.microblog.paging.createPagingRemoteMediator +import dev.dimension.flare.data.datasource.microblog.pagingConfig +import dev.dimension.flare.data.repository.tryRun +import dev.dimension.flare.model.AccountType +import dev.dimension.flare.model.DbAccountType +import dev.dimension.flare.model.MicroBlogKey +import dev.dimension.flare.ui.model.UiList +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.mapNotNull +import org.koin.core.component.KoinComponent +import org.koin.core.component.inject + +@OptIn(ExperimentalPagingApi::class) +internal class ListHandler( + private val pagingKey: String, + private val accountKey: MicroBlogKey, + private val loader: ListLoader, +) : KoinComponent { + private val accountType: DbAccountType = AccountType.Specific(accountKey) + val database: CacheDatabase by inject() + val data by lazy { + Pager( + config = pagingConfig, + remoteMediator = + createPagingRemoteMediator( + pagingKey = pagingKey, + database = database, + onLoad = { pageSize, request -> + loader.load(pageSize, request) + }, + onSave = { request, data -> + database.listDao().deleteByPagingKey(pagingKey) + database.listDao().insertAll( + data.map { item -> + DbList( + listKey = item.key, + accountType = accountType, + content = DbList.ListContent(item), + ) + }, + ) + + database.listDao().insertAll( + data.map { item -> + DbListPaging( + accountType = accountType, + pagingKey = pagingKey, + listKey = item.key, + ) + }, + ) + }, + ), + pagingSourceFactory = { + database.listDao().getPagingSource( + pagingKey = pagingKey, + ) + }, + ).flow.map { + it.map { + it.list.content.data + } + } + } + + fun listInfo(listKey: MicroBlogKey): CacheData = + Cacheable( + fetchSource = { + val info = loader.info(listKey) + database.connect { + database.listDao().insertAll( + listOf( + DbList( + listKey = info.key, + accountType = accountType, + content = DbList.ListContent(info), + ), + ), + ) + } + }, + cacheSource = { + database + .listDao() + .getList( + listKey = listKey, + accountType = accountType, + ).mapNotNull { dbList -> + dbList?.content?.data + } + }, + ) + + suspend fun create(metaData: ListMetaData) { + tryRun { + loader.create(metaData) + }.onSuccess { result -> + database.connect { + database.listDao().insertAll( + listOf( + DbList( + listKey = result.key, + accountType = accountType, + content = DbList.ListContent(result), + ), + ), + ) + database.listDao().insertAll( + listOf( + DbListPaging( + accountType = accountType, + pagingKey = pagingKey, + listKey = result.key, + ), + ), + ) + } + } + } + + suspend fun update( + listKey: MicroBlogKey, + metaData: ListMetaData, + ) { + tryRun { + loader.update(listKey, metaData) + }.onSuccess { result -> + database.connect { + database.listDao().updateListContent( + listKey = listKey, + accountType = accountType, + content = DbList.ListContent(result), + ) + } + } + } + + suspend fun delete(listKey: MicroBlogKey) { + tryRun { + loader.delete(listKey) + }.onSuccess { + database.connect { + database.listDao().deleteByListKey( + listKey = listKey, + accountType = accountType, + ) + database.listDao().deletePagingByListKey( + listKey = listKey, + accountType = accountType, + ) + } + } + } +} diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/microblog/list/ListLoader.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/microblog/list/ListLoader.kt new file mode 100644 index 000000000..c4ec64e51 --- /dev/null +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/microblog/list/ListLoader.kt @@ -0,0 +1,27 @@ +package dev.dimension.flare.data.datasource.microblog.list + +import dev.dimension.flare.data.datasource.microblog.paging.PagingRequest +import dev.dimension.flare.data.datasource.microblog.paging.PagingResult +import dev.dimension.flare.model.MicroBlogKey +import dev.dimension.flare.ui.model.UiList +import kotlinx.collections.immutable.ImmutableList + +internal interface ListLoader { + suspend fun load( + pageSize: Int, + request: PagingRequest, + ): PagingResult + + suspend fun info(listKey: MicroBlogKey): UiList + + suspend fun create(metaData: ListMetaData): UiList + + suspend fun update( + listKey: MicroBlogKey, + metaData: ListMetaData, + ): UiList + + suspend fun delete(listKey: MicroBlogKey) + + val supportedMetaData: ImmutableList +} diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/microblog/list/ListMemberHandler.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/microblog/list/ListMemberHandler.kt new file mode 100644 index 000000000..c559bd146 --- /dev/null +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/microblog/list/ListMemberHandler.kt @@ -0,0 +1,156 @@ +package dev.dimension.flare.data.datasource.microblog.list + +import androidx.paging.ExperimentalPagingApi +import androidx.paging.Pager +import androidx.paging.flatMap +import androidx.paging.map +import dev.dimension.flare.data.database.cache.CacheDatabase +import dev.dimension.flare.data.database.cache.connect +import dev.dimension.flare.data.database.cache.model.DbList +import dev.dimension.flare.data.database.cache.model.DbListMember +import dev.dimension.flare.data.datasource.microblog.paging.createPagingRemoteMediator +import dev.dimension.flare.data.datasource.microblog.pagingConfig +import dev.dimension.flare.data.repository.tryRun +import dev.dimension.flare.model.AccountType +import dev.dimension.flare.model.DbAccountType +import dev.dimension.flare.model.MicroBlogKey +import dev.dimension.flare.ui.model.mapper.render +import kotlinx.coroutines.flow.map +import org.koin.core.component.KoinComponent +import org.koin.core.component.inject + +@OptIn(ExperimentalPagingApi::class) +internal class ListMemberHandler( + private val pagingKey: String, + private val accountKey: MicroBlogKey, + private val loader: ListMemberLoader, +) : KoinComponent { + private val accountType: DbAccountType = AccountType.Specific(accountKey) + val database: CacheDatabase by inject() + private val memberPagingKey: String + get() = "${pagingKey}_members" + + fun listMembers(listKey: MicroBlogKey) = + Pager( + config = pagingConfig, + remoteMediator = + createPagingRemoteMediator( + pagingKey = memberPagingKey, + database = database, + onLoad = { pageSize, request -> + loader.loadMembers( + pageSize = pageSize, + request = request, + listKey = listKey, + ) + }, + onSave = { request, data -> + database.listDao().insertAll( + data.map { item -> + DbListMember( + listKey = listKey, + memberKey = item.userKey, + ) + }, + ) + database.userDao().insertAll(data) + }, + ), + pagingSourceFactory = { + database.listDao().getListMembers( + listKey = listKey, + ) + }, + ).flow.map { + it.map { + it.user.render(accountKey) + } + } + + suspend fun addMember( + listKey: MicroBlogKey, + userKey: MicroBlogKey, + ) { + tryRun { + loader.addMember(listKey, userKey) + }.onSuccess { user -> + database.connect { + database.listDao().insertAll( + listOf( + DbListMember( + listKey = listKey, + memberKey = userKey, + ), + ), + ) + database.userDao().insertAll( + listOf(user), + ) + } + } + } + + suspend fun removeMember( + listKey: MicroBlogKey, + userKey: MicroBlogKey, + ) { + tryRun { + loader.removeMember(listKey, userKey) + }.onSuccess { + database.connect { + database.listDao().deleteMemberFromList( + listKey = listKey, + memberKey = userKey, + ) + } + } + } + + private val userListsPagingKey: String + get() = "${pagingKey}_user_lists" + + fun userLists(userKey: MicroBlogKey) = + Pager( + config = pagingConfig, + pagingSourceFactory = { + database.listDao().getUserByKey(userKey) + }, + remoteMediator = + createPagingRemoteMediator( + pagingKey = userListsPagingKey, + database = database, + onLoad = { pageSize, request -> + loader.loadUserLists( + pageSize = pageSize, + request = request, + userKey = userKey, + ) + }, + onSave = { request, data -> + database.listDao().insertAll( + data.map { item -> + DbList( + listKey = item.key, + accountType = accountType, + content = DbList.ListContent(item), + ) + }, + ) + database.listDao().insertAll( + data.map { item -> + DbListMember( + listKey = item.key, + memberKey = userKey, + ) + }, + ) + }, + ), + ).flow.map { + it.flatMap { + it.listMemberships.map { + it.list.content.data + } + } + } +} diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/microblog/list/ListMemberLoader.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/microblog/list/ListMemberLoader.kt new file mode 100644 index 000000000..76c842ddd --- /dev/null +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/microblog/list/ListMemberLoader.kt @@ -0,0 +1,31 @@ +package dev.dimension.flare.data.datasource.microblog.list + +import dev.dimension.flare.data.database.cache.model.DbUser +import dev.dimension.flare.data.datasource.microblog.paging.PagingRequest +import dev.dimension.flare.data.datasource.microblog.paging.PagingResult +import dev.dimension.flare.model.MicroBlogKey +import dev.dimension.flare.ui.model.UiList + +internal interface ListMemberLoader { + suspend fun loadMembers( + pageSize: Int, + request: PagingRequest, + listKey: MicroBlogKey, + ): PagingResult + + suspend fun addMember( + listKey: MicroBlogKey, + userKey: MicroBlogKey, + ): DbUser + + suspend fun removeMember( + listKey: MicroBlogKey, + userKey: MicroBlogKey, + ) + + suspend fun loadUserLists( + pageSize: Int, + request: PagingRequest, + userKey: MicroBlogKey, + ): PagingResult +} diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/microblog/list/ListMetaData.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/microblog/list/ListMetaData.kt new file mode 100644 index 000000000..ea22cc3ad --- /dev/null +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/microblog/list/ListMetaData.kt @@ -0,0 +1,9 @@ +package dev.dimension.flare.data.datasource.microblog.list + +import dev.dimension.flare.common.FileItem + +public data class ListMetaData( + val title: String, + val description: String? = null, + val avatar: FileItem? = null, +) diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/microblog/list/ListMetaDataType.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/microblog/list/ListMetaDataType.kt new file mode 100644 index 000000000..1c7d85142 --- /dev/null +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/microblog/list/ListMetaDataType.kt @@ -0,0 +1,7 @@ +package dev.dimension.flare.data.datasource.microblog.list + +public enum class ListMetaDataType { + TITLE, + DESCRIPTION, + AVATAR, +} diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/microblog/paging/BasePagingRemoteMediator.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/microblog/paging/BasePagingRemoteMediator.kt new file mode 100644 index 000000000..513eac7e1 --- /dev/null +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/microblog/paging/BasePagingRemoteMediator.kt @@ -0,0 +1,107 @@ +package dev.dimension.flare.data.datasource.microblog.paging + +import androidx.paging.ExperimentalPagingApi +import androidx.paging.LoadType +import androidx.paging.PagingState +import dev.dimension.flare.common.BaseRemoteMediator +import dev.dimension.flare.data.database.cache.CacheDatabase +import dev.dimension.flare.data.database.cache.connect +import dev.dimension.flare.data.database.cache.model.DbPagingKey + +internal fun createPagingRemoteMediator( + database: CacheDatabase, + pagingKey: String, + onLoad: suspend (pageSize: Int, request: PagingRequest) -> PagingResult, + onSave: suspend (request: PagingRequest, data: List) -> Unit, +): BasePagingRemoteMediator = + object : BasePagingRemoteMediator(database) { + override val pagingKey: String = pagingKey + + override suspend fun load( + pageSize: Int, + request: PagingRequest, + ): PagingResult = onLoad(pageSize, request) + + override suspend fun onSaveCache( + request: PagingRequest, + data: List, + ) { + onSave(request, data) + } + } + +internal abstract class BasePagingRemoteMediator( + private val database: CacheDatabase, +) : BaseRemoteMediator() { + abstract val pagingKey: String + + @OptIn(ExperimentalPagingApi::class) + override suspend fun doLoad( + loadType: LoadType, + state: PagingState, + ): MediatorResult { + val request: PagingRequest = + when (loadType) { + LoadType.REFRESH -> PagingRequest.Refresh + LoadType.PREPEND -> { + val previousKey = + database.pagingTimelineDao().getPagingKey(pagingKey)?.prevKey + ?: return MediatorResult.Success(endOfPaginationReached = true) + PagingRequest.Prepend(previousKey) + } + LoadType.APPEND -> { + val nextKey = + database.pagingTimelineDao().getPagingKey(pagingKey)?.nextKey + ?: return MediatorResult.Success(endOfPaginationReached = true) + PagingRequest.Append(nextKey) + } + } + + val result = + load( + pageSize = state.config.pageSize, + request = request, + ) + database.connect { + if (loadType == LoadType.REFRESH) { + database.pagingTimelineDao().deletePagingKey(pagingKey) + database.pagingTimelineDao().insertPagingKey( + DbPagingKey( + pagingKey = pagingKey, + nextKey = result.nextKey, + prevKey = result.previousKey, + ), + ) + } else if (loadType == LoadType.PREPEND && result.previousKey != null) { + database.pagingTimelineDao().updatePagingKeyPrevKey( + pagingKey = pagingKey, + prevKey = result.previousKey, + ) + } else if (loadType == LoadType.APPEND && result.nextKey != null) { + database.pagingTimelineDao().updatePagingKeyNextKey( + pagingKey = pagingKey, + nextKey = result.nextKey, + ) + } + onSaveCache(request, result.data) + } + return MediatorResult.Success( + endOfPaginationReached = + when (loadType) { + LoadType.REFRESH -> false + LoadType.PREPEND -> result.previousKey == null + LoadType.APPEND -> result.nextKey == null + }, + ) + } + + protected abstract suspend fun load( + pageSize: Int, + request: PagingRequest, + ): PagingResult + + protected abstract suspend fun onSaveCache( + request: PagingRequest, + data: List, + ) +} diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/microblog/paging/BaseTimelineRemoteMediator.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/microblog/paging/BaseTimelineRemoteMediator.kt new file mode 100644 index 000000000..74cc588b9 --- /dev/null +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/microblog/paging/BaseTimelineRemoteMediator.kt @@ -0,0 +1,52 @@ +package dev.dimension.flare.data.datasource.microblog.paging + +import androidx.paging.ExperimentalPagingApi +import dev.dimension.flare.common.BasePagingSource +import dev.dimension.flare.data.database.cache.CacheDatabase +import dev.dimension.flare.data.database.cache.mapper.saveToDatabase +import dev.dimension.flare.data.database.cache.model.DbPagingTimelineWithStatus +import dev.dimension.flare.ui.model.UiTimeline + +internal sealed interface BaseTimelineLoader { + data object NotSupported : BaseTimelineLoader +} + +internal fun interface BaseTimelinePagingSourceFactory : BaseTimelineLoader { + abstract fun create(): BasePagingSource +} + +@OptIn(ExperimentalPagingApi::class) +internal abstract class BaseTimelineRemoteMediator( + private val database: CacheDatabase, +) : BasePagingRemoteMediator( + database = database, + ), + BaseTimelineLoader { + override suspend fun load( + pageSize: Int, + request: PagingRequest, + ): PagingResult = + timeline( + pageSize = pageSize, + request = request, + ) + + abstract suspend fun timeline( + pageSize: Int, + request: PagingRequest, + ): PagingResult + + override suspend fun onSaveCache( + request: PagingRequest, + data: List, + ) { + if (request is PagingRequest.Refresh) { + data.groupBy { it.timeline.pagingKey }.keys.forEach { key -> + database + .pagingTimelineDao() + .delete(pagingKey = key) + } + } + saveToDatabase(database, data) + } +} diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/microblog/paging/PagingRequest.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/microblog/paging/PagingRequest.kt new file mode 100644 index 000000000..3c855bd16 --- /dev/null +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/microblog/paging/PagingRequest.kt @@ -0,0 +1,13 @@ +package dev.dimension.flare.data.datasource.microblog.paging + +internal sealed interface PagingRequest { + data object Refresh : PagingRequest + + data class Prepend( + val previousKey: String, + ) : PagingRequest + + data class Append( + val nextKey: String, + ) : PagingRequest +} diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/microblog/paging/PagingResult.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/microblog/paging/PagingResult.kt new file mode 100644 index 000000000..e8db47dea --- /dev/null +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/microblog/paging/PagingResult.kt @@ -0,0 +1,18 @@ +package dev.dimension.flare.data.datasource.microblog.paging + +internal data class PagingResult( + val data: List = emptyList(), + val nextKey: String? = null, + val previousKey: String? = null, +) { + constructor( + endOfPaginationReached: Boolean, + data: List = emptyList(), + nextKey: String? = null, + previousKey: String? = null, + ) : this( + data = data.toList(), + nextKey = if (endOfPaginationReached) null else nextKey, + previousKey = previousKey, + ) +} diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/misskey/AntennasListPagingSource.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/misskey/AntennasListPagingSource.kt deleted file mode 100644 index 830d8d9ae..000000000 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/misskey/AntennasListPagingSource.kt +++ /dev/null @@ -1,32 +0,0 @@ -package dev.dimension.flare.data.datasource.misskey - -import androidx.paging.PagingState -import dev.dimension.flare.common.BasePagingSource -import dev.dimension.flare.data.network.misskey.MisskeyService -import dev.dimension.flare.data.repository.tryRun -import dev.dimension.flare.ui.model.UiList -import dev.dimension.flare.ui.model.mapper.render - -internal class AntennasListPagingSource( - private val service: MisskeyService, -) : BasePagingSource() { - override suspend fun doLoad(params: LoadParams): LoadResult = - tryRun { - service.antennasList().map { - it.render() - } - }.fold( - onSuccess = { antennas -> - LoadResult.Page( - data = antennas, - prevKey = null, - nextKey = null, - ) - }, - onFailure = { error -> - LoadResult.Error(error) - }, - ) - - override fun getRefreshKey(state: PagingState): Int? = null -} diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/misskey/AntennasListRemoteMediator.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/misskey/AntennasListRemoteMediator.kt new file mode 100644 index 000000000..0c7ffab1e --- /dev/null +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/misskey/AntennasListRemoteMediator.kt @@ -0,0 +1,31 @@ +package dev.dimension.flare.data.datasource.misskey + +import dev.dimension.flare.data.database.cache.CacheDatabase +import dev.dimension.flare.data.datasource.microblog.BaseListRemoteMediator +import dev.dimension.flare.data.network.misskey.MisskeyService +import dev.dimension.flare.model.AccountType +import dev.dimension.flare.model.MicroBlogKey +import dev.dimension.flare.ui.model.mapper.render + +internal class AntennasListRemoteMediator( + private val service: MisskeyService, + database: CacheDatabase, + accountKey: MicroBlogKey, +) : BaseListRemoteMediator(database) { + override val accountType = AccountType.Specific(accountKey) + override val pagingKey = "antennas_list_$accountKey" + + override suspend fun load( + pageSize: Int, + request: Request, + ): Result = + service + .antennasList() + .map { + it.render() + }.let { antennas -> + PagingResult( + data = antennas, + ) + } +} diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/misskey/AntennasTimelineRemoteMediator.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/misskey/AntennasTimelineRemoteMediator.kt index 651b3ab2b..c0dfadbbb 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/misskey/AntennasTimelineRemoteMediator.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/misskey/AntennasTimelineRemoteMediator.kt @@ -1,9 +1,12 @@ package dev.dimension.flare.data.datasource.misskey import androidx.paging.ExperimentalPagingApi -import dev.dimension.flare.common.BaseTimelineRemoteMediator import dev.dimension.flare.data.database.cache.CacheDatabase import dev.dimension.flare.data.database.cache.mapper.toDbPagingTimeline +import dev.dimension.flare.data.database.cache.model.DbPagingTimelineWithStatus +import dev.dimension.flare.data.datasource.microblog.paging.BaseTimelineRemoteMediator +import dev.dimension.flare.data.datasource.microblog.paging.PagingRequest +import dev.dimension.flare.data.datasource.microblog.paging.PagingResult import dev.dimension.flare.data.network.misskey.MisskeyService import dev.dimension.flare.data.network.misskey.api.model.AntennasNotesRequest import dev.dimension.flare.model.MicroBlogKey @@ -21,11 +24,11 @@ internal class AntennasTimelineRemoteMediator( override suspend fun timeline( pageSize: Int, - request: Request, - ): Result { + request: PagingRequest, + ): PagingResult { val response = when (request) { - Request.Refresh -> { + PagingRequest.Refresh -> { service .antennasNotes( AntennasNotesRequest( @@ -35,13 +38,13 @@ internal class AntennasTimelineRemoteMediator( ) } - is Request.Prepend -> { - return Result( + is PagingRequest.Prepend -> { + return PagingResult( endOfPaginationReached = true, ) } - is Request.Append -> { + is PagingRequest.Append -> { service .antennasNotes( AntennasNotesRequest( @@ -53,7 +56,7 @@ internal class AntennasTimelineRemoteMediator( } } - return Result( + return PagingResult( endOfPaginationReached = response.isEmpty(), data = response.toDbPagingTimeline( diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/misskey/ChannelTimelineRemoteMediator.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/misskey/ChannelTimelineRemoteMediator.kt new file mode 100644 index 000000000..dd103d3d4 --- /dev/null +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/misskey/ChannelTimelineRemoteMediator.kt @@ -0,0 +1,69 @@ +package dev.dimension.flare.data.datasource.misskey + +import dev.dimension.flare.data.database.cache.CacheDatabase +import dev.dimension.flare.data.database.cache.mapper.toDbPagingTimeline +import dev.dimension.flare.data.database.cache.model.DbPagingTimelineWithStatus +import dev.dimension.flare.data.datasource.microblog.paging.BaseTimelineRemoteMediator +import dev.dimension.flare.data.datasource.microblog.paging.PagingRequest +import dev.dimension.flare.data.datasource.microblog.paging.PagingResult +import dev.dimension.flare.data.network.misskey.MisskeyService +import dev.dimension.flare.data.network.misskey.api.model.ChannelsTimelineRequest +import dev.dimension.flare.model.MicroBlogKey + +internal class ChannelTimelineRemoteMediator( + private val service: MisskeyService, + database: CacheDatabase, + private val accountKey: MicroBlogKey, + private val id: String, +) : BaseTimelineRemoteMediator( + database = database, + ) { + override val pagingKey = "channel_${id}_$accountKey" + + override suspend fun timeline( + pageSize: Int, + request: PagingRequest, + ): PagingResult { + val response = + when (request) { + PagingRequest.Refresh -> { + service + .channelsTimeline( + channelsTimelineRequest = + ChannelsTimelineRequest( + channelId = id, + limit = pageSize, + ), + ) + } + + is PagingRequest.Prepend -> { + return PagingResult( + endOfPaginationReached = true, + ) + } + + is PagingRequest.Append -> { + service + .channelsTimeline( + channelsTimelineRequest = + ChannelsTimelineRequest( + channelId = id, + limit = pageSize, + untilId = request.nextKey, + ), + ) + } + } + + return PagingResult( + endOfPaginationReached = response.isEmpty(), + data = + response.toDbPagingTimeline( + accountKey = accountKey, + pagingKey = pagingKey, + ), + nextKey = response.lastOrNull()?.id, + ) + } +} diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/misskey/DiscoverStatusRemoteMediator.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/misskey/DiscoverStatusRemoteMediator.kt index 27f05e13d..c7002f0d8 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/misskey/DiscoverStatusRemoteMediator.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/misskey/DiscoverStatusRemoteMediator.kt @@ -1,9 +1,12 @@ package dev.dimension.flare.data.datasource.misskey import androidx.paging.ExperimentalPagingApi -import dev.dimension.flare.common.BaseTimelineRemoteMediator import dev.dimension.flare.data.database.cache.CacheDatabase import dev.dimension.flare.data.database.cache.mapper.toDbPagingTimeline +import dev.dimension.flare.data.database.cache.model.DbPagingTimelineWithStatus +import dev.dimension.flare.data.datasource.microblog.paging.BaseTimelineRemoteMediator +import dev.dimension.flare.data.datasource.microblog.paging.PagingRequest +import dev.dimension.flare.data.datasource.microblog.paging.PagingResult import dev.dimension.flare.data.network.misskey.MisskeyService import dev.dimension.flare.data.network.misskey.api.model.NotesFeaturedRequest import dev.dimension.flare.model.MicroBlogKey @@ -20,15 +23,15 @@ internal class DiscoverStatusRemoteMediator( override suspend fun timeline( pageSize: Int, - request: Request, - ): Result { + request: PagingRequest, + ): PagingResult { val response = when (request) { - Request.Refresh -> { + PagingRequest.Refresh -> { service.notesFeatured(NotesFeaturedRequest(limit = pageSize)) } - is Request.Append -> { + is PagingRequest.Append -> { service.notesFeatured( NotesFeaturedRequest( limit = pageSize, @@ -38,13 +41,13 @@ internal class DiscoverStatusRemoteMediator( } else -> { - return Result( + return PagingResult( endOfPaginationReached = true, ) } } - return Result( + return PagingResult( endOfPaginationReached = true, data = response.toDbPagingTimeline( diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/misskey/FavouriteTimelineRemoteMediator.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/misskey/FavouriteTimelineRemoteMediator.kt index f1401662b..4b835565a 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/misskey/FavouriteTimelineRemoteMediator.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/misskey/FavouriteTimelineRemoteMediator.kt @@ -1,9 +1,12 @@ package dev.dimension.flare.data.datasource.misskey import androidx.paging.ExperimentalPagingApi -import dev.dimension.flare.common.BaseTimelineRemoteMediator import dev.dimension.flare.data.database.cache.CacheDatabase import dev.dimension.flare.data.database.cache.mapper.toDbPagingTimeline +import dev.dimension.flare.data.database.cache.model.DbPagingTimelineWithStatus +import dev.dimension.flare.data.datasource.microblog.paging.BaseTimelineRemoteMediator +import dev.dimension.flare.data.datasource.microblog.paging.PagingRequest +import dev.dimension.flare.data.datasource.microblog.paging.PagingResult import dev.dimension.flare.data.network.misskey.MisskeyService import dev.dimension.flare.data.network.misskey.api.model.AdminAdListRequest import dev.dimension.flare.model.MicroBlogKey @@ -26,15 +29,15 @@ internal class FavouriteTimelineRemoteMediator( override suspend fun timeline( pageSize: Int, - request: Request, - ): Result { + request: PagingRequest, + ): PagingResult { val response = when (request) { - is Request.Prepend -> return Result( + is PagingRequest.Prepend -> return PagingResult( endOfPaginationReached = true, ) - Request.Refresh -> { + PagingRequest.Refresh -> { service.iFavorites( AdminAdListRequest( limit = pageSize, @@ -42,7 +45,7 @@ internal class FavouriteTimelineRemoteMediator( ) } - is Request.Append -> { + is PagingRequest.Append -> { service.iFavorites( AdminAdListRequest( limit = pageSize, @@ -64,7 +67,7 @@ internal class FavouriteTimelineRemoteMediator( }, ) - return Result( + return PagingResult( endOfPaginationReached = response.isEmpty(), data = data, nextKey = response.lastOrNull()?.noteId, diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/misskey/FeaturedChannelPagingSource.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/misskey/FeaturedChannelPagingSource.kt index 919489f0f..6610032c7 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/misskey/FeaturedChannelPagingSource.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/misskey/FeaturedChannelPagingSource.kt @@ -10,8 +10,8 @@ import dev.dimension.flare.ui.model.mapper.render internal class FeaturedChannelPagingSource( private val service: MisskeyService, -) : BasePagingSource() { - override suspend fun doLoad(params: LoadParams): LoadResult = +) : BasePagingSource() { + override suspend fun doLoad(params: LoadParams): LoadResult = tryRun { service .channelsFeatured( @@ -32,5 +32,5 @@ internal class FeaturedChannelPagingSource( }, ) - override fun getRefreshKey(state: PagingState): Int? = null + override fun getRefreshKey(state: PagingState): Int? = null } diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/misskey/HomeTimelineRemoteMediator.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/misskey/HomeTimelineRemoteMediator.kt index 20f1f57d4..7f27bfdda 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/misskey/HomeTimelineRemoteMediator.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/misskey/HomeTimelineRemoteMediator.kt @@ -1,9 +1,12 @@ package dev.dimension.flare.data.datasource.misskey import androidx.paging.ExperimentalPagingApi -import dev.dimension.flare.common.BaseTimelineRemoteMediator import dev.dimension.flare.data.database.cache.CacheDatabase import dev.dimension.flare.data.database.cache.mapper.toDbPagingTimeline +import dev.dimension.flare.data.database.cache.model.DbPagingTimelineWithStatus +import dev.dimension.flare.data.datasource.microblog.paging.BaseTimelineRemoteMediator +import dev.dimension.flare.data.datasource.microblog.paging.PagingRequest +import dev.dimension.flare.data.datasource.microblog.paging.PagingResult import dev.dimension.flare.data.network.misskey.MisskeyService import dev.dimension.flare.data.network.misskey.api.model.NotesHybridTimelineRequest import dev.dimension.flare.model.MicroBlogKey @@ -22,15 +25,15 @@ internal class HomeTimelineRemoteMediator( override suspend fun timeline( pageSize: Int, - request: Request, - ): Result { + request: PagingRequest, + ): PagingResult { val response = when (request) { - is Request.Prepend -> return Result( + is PagingRequest.Prepend -> return PagingResult( endOfPaginationReached = true, ) - Request.Refresh -> { + PagingRequest.Refresh -> { service.notesTimeline( NotesHybridTimelineRequest( limit = pageSize, @@ -38,7 +41,7 @@ internal class HomeTimelineRemoteMediator( ) } - is Request.Append -> { + is PagingRequest.Append -> { service.notesTimeline( NotesHybridTimelineRequest( limit = pageSize, @@ -48,7 +51,7 @@ internal class HomeTimelineRemoteMediator( } } - return Result( + return PagingResult( endOfPaginationReached = response.isEmpty(), data = response.toDbPagingTimeline( diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/misskey/HybridTimelineRemoteMediator.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/misskey/HybridTimelineRemoteMediator.kt index 86e654165..5cd2f29e9 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/misskey/HybridTimelineRemoteMediator.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/misskey/HybridTimelineRemoteMediator.kt @@ -1,8 +1,11 @@ package dev.dimension.flare.data.datasource.misskey -import dev.dimension.flare.common.BaseTimelineRemoteMediator import dev.dimension.flare.data.database.cache.CacheDatabase import dev.dimension.flare.data.database.cache.mapper.toDbPagingTimeline +import dev.dimension.flare.data.database.cache.model.DbPagingTimelineWithStatus +import dev.dimension.flare.data.datasource.microblog.paging.BaseTimelineRemoteMediator +import dev.dimension.flare.data.datasource.microblog.paging.PagingRequest +import dev.dimension.flare.data.datasource.microblog.paging.PagingResult import dev.dimension.flare.data.network.misskey.MisskeyService import dev.dimension.flare.data.network.misskey.api.model.NotesHybridTimelineRequest import dev.dimension.flare.model.MicroBlogKey @@ -18,14 +21,14 @@ internal class HybridTimelineRemoteMediator( override suspend fun timeline( pageSize: Int, - request: Request, - ): Result { + request: PagingRequest, + ): PagingResult { val response = when (request) { - is Request.Prepend -> return Result( + is PagingRequest.Prepend -> return PagingResult( endOfPaginationReached = true, ) - Request.Refresh -> { + PagingRequest.Refresh -> { service.notesHybridTimeline( NotesHybridTimelineRequest( limit = pageSize, @@ -33,7 +36,7 @@ internal class HybridTimelineRemoteMediator( ) } - is Request.Append -> { + is PagingRequest.Append -> { service.notesHybridTimeline( NotesHybridTimelineRequest( limit = pageSize, @@ -43,7 +46,7 @@ internal class HybridTimelineRemoteMediator( } } - return Result( + return PagingResult( endOfPaginationReached = response.isEmpty(), data = response.toDbPagingTimeline( diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/misskey/ListTimelineRemoteMediator.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/misskey/ListTimelineRemoteMediator.kt index 87eb512bb..f06b9a4a4 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/misskey/ListTimelineRemoteMediator.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/misskey/ListTimelineRemoteMediator.kt @@ -1,9 +1,12 @@ package dev.dimension.flare.data.datasource.misskey import androidx.paging.ExperimentalPagingApi -import dev.dimension.flare.common.BaseTimelineRemoteMediator import dev.dimension.flare.data.database.cache.CacheDatabase import dev.dimension.flare.data.database.cache.mapper.toDbPagingTimeline +import dev.dimension.flare.data.database.cache.model.DbPagingTimelineWithStatus +import dev.dimension.flare.data.datasource.microblog.paging.BaseTimelineRemoteMediator +import dev.dimension.flare.data.datasource.microblog.paging.PagingRequest +import dev.dimension.flare.data.datasource.microblog.paging.PagingResult import dev.dimension.flare.data.network.misskey.MisskeyService import dev.dimension.flare.data.network.misskey.api.model.NotesUserListTimelineRequest import dev.dimension.flare.model.MicroBlogKey @@ -21,11 +24,11 @@ internal class ListTimelineRemoteMediator( override suspend fun timeline( pageSize: Int, - request: Request, - ): Result { + request: PagingRequest, + ): PagingResult { val response = when (request) { - Request.Refresh -> { + PagingRequest.Refresh -> { service .notesUserListTimeline( NotesUserListTimelineRequest( @@ -37,13 +40,13 @@ internal class ListTimelineRemoteMediator( ) } - is Request.Prepend -> { - return Result( + is PagingRequest.Prepend -> { + return PagingResult( endOfPaginationReached = true, ) } - is Request.Append -> { + is PagingRequest.Append -> { service .notesUserListTimeline( NotesUserListTimelineRequest( @@ -57,7 +60,7 @@ internal class ListTimelineRemoteMediator( } } - return Result( + return PagingResult( endOfPaginationReached = response.isEmpty(), data = response.toDbPagingTimeline( diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/misskey/LocalTimelineRemoteMediator.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/misskey/LocalTimelineRemoteMediator.kt index b5b6fdc34..9ba41549c 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/misskey/LocalTimelineRemoteMediator.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/misskey/LocalTimelineRemoteMediator.kt @@ -1,9 +1,12 @@ package dev.dimension.flare.data.datasource.misskey import androidx.paging.ExperimentalPagingApi -import dev.dimension.flare.common.BaseTimelineRemoteMediator import dev.dimension.flare.data.database.cache.CacheDatabase import dev.dimension.flare.data.database.cache.mapper.toDbPagingTimeline +import dev.dimension.flare.data.database.cache.model.DbPagingTimelineWithStatus +import dev.dimension.flare.data.datasource.microblog.paging.BaseTimelineRemoteMediator +import dev.dimension.flare.data.datasource.microblog.paging.PagingRequest +import dev.dimension.flare.data.datasource.microblog.paging.PagingResult import dev.dimension.flare.data.network.misskey.MisskeyService import dev.dimension.flare.data.network.misskey.api.model.NotesLocalTimelineRequest import dev.dimension.flare.model.MicroBlogKey @@ -20,14 +23,14 @@ internal class LocalTimelineRemoteMediator( override suspend fun timeline( pageSize: Int, - request: Request, - ): Result { + request: PagingRequest, + ): PagingResult { val response = when (request) { - is Request.Prepend -> return Result( + is PagingRequest.Prepend -> return PagingResult( endOfPaginationReached = true, ) - Request.Refresh -> { + PagingRequest.Refresh -> { service.notesLocalTimeline( NotesLocalTimelineRequest( limit = pageSize, @@ -35,7 +38,7 @@ internal class LocalTimelineRemoteMediator( ) } - is Request.Append -> { + is PagingRequest.Append -> { service.notesLocalTimeline( NotesLocalTimelineRequest( limit = pageSize, @@ -45,7 +48,7 @@ internal class LocalTimelineRemoteMediator( } } - return Result( + return PagingResult( endOfPaginationReached = response.isEmpty(), data = response.toDbPagingTimeline( diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/misskey/MentionTimelineRemoteMediator.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/misskey/MentionTimelineRemoteMediator.kt index d91ca4941..596559a4f 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/misskey/MentionTimelineRemoteMediator.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/misskey/MentionTimelineRemoteMediator.kt @@ -1,9 +1,12 @@ package dev.dimension.flare.data.datasource.misskey import androidx.paging.ExperimentalPagingApi -import dev.dimension.flare.common.BaseTimelineRemoteMediator import dev.dimension.flare.data.database.cache.CacheDatabase import dev.dimension.flare.data.database.cache.mapper.toDbPagingTimeline +import dev.dimension.flare.data.database.cache.model.DbPagingTimelineWithStatus +import dev.dimension.flare.data.datasource.microblog.paging.BaseTimelineRemoteMediator +import dev.dimension.flare.data.datasource.microblog.paging.PagingRequest +import dev.dimension.flare.data.datasource.microblog.paging.PagingResult import dev.dimension.flare.data.network.misskey.MisskeyService import dev.dimension.flare.data.network.misskey.api.model.NotesMentionsRequest import dev.dimension.flare.model.MicroBlogKey @@ -20,14 +23,14 @@ internal class MentionTimelineRemoteMediator( override suspend fun timeline( pageSize: Int, - request: Request, - ): Result { + request: PagingRequest, + ): PagingResult { val response = when (request) { - is Request.Prepend -> return Result( + is PagingRequest.Prepend -> return PagingResult( endOfPaginationReached = true, ) - Request.Refresh -> { + PagingRequest.Refresh -> { service.notesMentions( NotesMentionsRequest( limit = pageSize, @@ -35,7 +38,7 @@ internal class MentionTimelineRemoteMediator( ) } - is Request.Append -> { + is PagingRequest.Append -> { service.notesMentions( NotesMentionsRequest( limit = pageSize, @@ -45,7 +48,7 @@ internal class MentionTimelineRemoteMediator( } } - return Result( + return PagingResult( endOfPaginationReached = response.isEmpty(), data = response.toDbPagingTimeline( diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/misskey/MisskeyDataSource.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/misskey/MisskeyDataSource.kt index 9dcabc5c3..3184b03ef 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/misskey/MisskeyDataSource.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/misskey/MisskeyDataSource.kt @@ -1374,10 +1374,7 @@ internal class MisskeyDataSource( } } - fun antennasList( - scope: CoroutineScope, - pageSize: Int = 20, - ): Flow> = + fun antennasList(scope: CoroutineScope): Flow> = Pager( config = pagingConfig, ) { @@ -1407,4 +1404,12 @@ internal class MisskeyDataSource( accountKey = accountKey, id = id, ) + + fun channelTimelineLoader(id: String) = + ChannelTimelineRemoteMediator( + service = service, + database = database, + accountKey = accountKey, + id = id, + ) } diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/misskey/NotificationRemoteMediator.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/misskey/NotificationRemoteMediator.kt index 843c42ffc..7aba2c3d9 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/misskey/NotificationRemoteMediator.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/misskey/NotificationRemoteMediator.kt @@ -1,9 +1,12 @@ package dev.dimension.flare.data.datasource.misskey import androidx.paging.ExperimentalPagingApi -import dev.dimension.flare.common.BaseTimelineRemoteMediator import dev.dimension.flare.data.database.cache.CacheDatabase import dev.dimension.flare.data.database.cache.mapper.toDb +import dev.dimension.flare.data.database.cache.model.DbPagingTimelineWithStatus +import dev.dimension.flare.data.datasource.microblog.paging.BaseTimelineRemoteMediator +import dev.dimension.flare.data.datasource.microblog.paging.PagingRequest +import dev.dimension.flare.data.datasource.microblog.paging.PagingResult import dev.dimension.flare.data.network.misskey.MisskeyService import dev.dimension.flare.data.network.misskey.api.model.INotificationsRequest import dev.dimension.flare.model.MicroBlogKey @@ -20,14 +23,14 @@ internal class NotificationRemoteMediator( override suspend fun timeline( pageSize: Int, - request: Request, - ): Result { + request: PagingRequest, + ): PagingResult { val response = when (request) { - is Request.Prepend -> return Result( + is PagingRequest.Prepend -> return PagingResult( endOfPaginationReached = true, ) - Request.Refresh -> { + PagingRequest.Refresh -> { service.iNotifications( INotificationsRequest( limit = pageSize, @@ -35,7 +38,7 @@ internal class NotificationRemoteMediator( ) } - is Request.Append -> { + is PagingRequest.Append -> { service.iNotifications( INotificationsRequest( limit = pageSize, @@ -45,7 +48,7 @@ internal class NotificationRemoteMediator( } } - return Result( + return PagingResult( endOfPaginationReached = response.isEmpty(), data = response.toDb( diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/misskey/PublicTimelineRemoteMediator.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/misskey/PublicTimelineRemoteMediator.kt index a02a9de24..0e54e4601 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/misskey/PublicTimelineRemoteMediator.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/misskey/PublicTimelineRemoteMediator.kt @@ -1,9 +1,12 @@ package dev.dimension.flare.data.datasource.misskey import androidx.paging.ExperimentalPagingApi -import dev.dimension.flare.common.BaseTimelineRemoteMediator import dev.dimension.flare.data.database.cache.CacheDatabase import dev.dimension.flare.data.database.cache.mapper.toDbPagingTimeline +import dev.dimension.flare.data.database.cache.model.DbPagingTimelineWithStatus +import dev.dimension.flare.data.datasource.microblog.paging.BaseTimelineRemoteMediator +import dev.dimension.flare.data.datasource.microblog.paging.PagingRequest +import dev.dimension.flare.data.datasource.microblog.paging.PagingResult import dev.dimension.flare.data.network.misskey.MisskeyService import dev.dimension.flare.data.network.misskey.api.model.NotesGlobalTimelineRequest import dev.dimension.flare.model.MicroBlogKey @@ -20,14 +23,14 @@ internal class PublicTimelineRemoteMediator( override suspend fun timeline( pageSize: Int, - request: Request, - ): Result { + request: PagingRequest, + ): PagingResult { val response = when (request) { - is Request.Prepend -> return Result( + is PagingRequest.Prepend -> return PagingResult( endOfPaginationReached = true, ) - Request.Refresh -> { + PagingRequest.Refresh -> { service.notesGlobalTimeline( NotesGlobalTimelineRequest( limit = pageSize, @@ -35,7 +38,7 @@ internal class PublicTimelineRemoteMediator( ) } - is Request.Append -> { + is PagingRequest.Append -> { service.notesGlobalTimeline( NotesGlobalTimelineRequest( limit = pageSize, @@ -45,7 +48,7 @@ internal class PublicTimelineRemoteMediator( } } - return Result( + return PagingResult( endOfPaginationReached = response.isEmpty(), data = response.toDbPagingTimeline( diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/misskey/SearchStatusRemoteMediator.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/misskey/SearchStatusRemoteMediator.kt index 1d78a4a9a..ab5f7e6c7 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/misskey/SearchStatusRemoteMediator.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/misskey/SearchStatusRemoteMediator.kt @@ -1,9 +1,12 @@ package dev.dimension.flare.data.datasource.misskey import androidx.paging.ExperimentalPagingApi -import dev.dimension.flare.common.BaseTimelineRemoteMediator import dev.dimension.flare.data.database.cache.CacheDatabase import dev.dimension.flare.data.database.cache.mapper.toDbPagingTimeline +import dev.dimension.flare.data.database.cache.model.DbPagingTimelineWithStatus +import dev.dimension.flare.data.datasource.microblog.paging.BaseTimelineRemoteMediator +import dev.dimension.flare.data.datasource.microblog.paging.PagingRequest +import dev.dimension.flare.data.datasource.microblog.paging.PagingResult import dev.dimension.flare.data.network.misskey.MisskeyService import dev.dimension.flare.data.network.misskey.api.model.NotesSearchRequest import dev.dimension.flare.model.MicroBlogKey @@ -26,17 +29,17 @@ internal class SearchStatusRemoteMediator( override suspend fun timeline( pageSize: Int, - request: Request, - ): Result { + request: PagingRequest, + ): PagingResult { val response = when (request) { - is Request.Prepend -> { - return Result( + is PagingRequest.Prepend -> { + return PagingResult( endOfPaginationReached = true, ) } - Request.Refresh -> { + PagingRequest.Refresh -> { service .notesSearch( NotesSearchRequest( @@ -46,7 +49,7 @@ internal class SearchStatusRemoteMediator( ) } - is Request.Append -> { + is PagingRequest.Append -> { service.notesSearch( NotesSearchRequest( query = query, @@ -57,7 +60,7 @@ internal class SearchStatusRemoteMediator( } } - return Result( + return PagingResult( endOfPaginationReached = response.isEmpty(), data = response.toDbPagingTimeline( diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/misskey/StatusDetailRemoteMediator.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/misskey/StatusDetailRemoteMediator.kt index 404b1b3ca..76105d9b9 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/misskey/StatusDetailRemoteMediator.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/misskey/StatusDetailRemoteMediator.kt @@ -2,11 +2,14 @@ package dev.dimension.flare.data.datasource.misskey import SnowflakeIdGenerator import androidx.paging.ExperimentalPagingApi -import dev.dimension.flare.common.BaseTimelineRemoteMediator import dev.dimension.flare.data.database.cache.CacheDatabase import dev.dimension.flare.data.database.cache.connect import dev.dimension.flare.data.database.cache.mapper.toDbPagingTimeline import dev.dimension.flare.data.database.cache.model.DbPagingTimeline +import dev.dimension.flare.data.database.cache.model.DbPagingTimelineWithStatus +import dev.dimension.flare.data.datasource.microblog.paging.BaseTimelineRemoteMediator +import dev.dimension.flare.data.datasource.microblog.paging.PagingRequest +import dev.dimension.flare.data.datasource.microblog.paging.PagingResult import dev.dimension.flare.data.network.misskey.MisskeyService import dev.dimension.flare.data.network.misskey.api.model.IPinRequest import dev.dimension.flare.data.network.misskey.api.model.NotesChildrenRequest @@ -37,13 +40,13 @@ internal class StatusDetailRemoteMediator( override suspend fun timeline( pageSize: Int, - request: Request, - ): Result { + request: PagingRequest, + ): PagingResult { val result = when (request) { - is Request.Append -> { + is PagingRequest.Append -> { if (statusOnly) { - return Result( + return PagingResult( endOfPaginationReached = true, ) } @@ -57,12 +60,12 @@ internal class StatusDetailRemoteMediator( ) } - is Request.Prepend -> - return Result( + is PagingRequest.Prepend -> + return PagingResult( endOfPaginationReached = true, ) - Request.Refresh -> { + PagingRequest.Refresh -> { if (!database.pagingTimelineDao().existsPaging(accountKey, pagingKey)) { val status = database @@ -99,7 +102,7 @@ internal class StatusDetailRemoteMediator( } } - return Result( + return PagingResult( endOfPaginationReached = statusOnly || result.isEmpty(), data = result.toDbPagingTimeline( @@ -110,7 +113,7 @@ internal class StatusDetailRemoteMediator( }, ), nextKey = - if (request == Request.Refresh) { + if (request == PagingRequest.Refresh) { "" } else { result.lastOrNull()?.id diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/misskey/UserTimelineRemoteMediator.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/misskey/UserTimelineRemoteMediator.kt index 1325d12ce..5eccba686 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/misskey/UserTimelineRemoteMediator.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/misskey/UserTimelineRemoteMediator.kt @@ -1,9 +1,12 @@ package dev.dimension.flare.data.datasource.misskey import androidx.paging.ExperimentalPagingApi -import dev.dimension.flare.common.BaseTimelineRemoteMediator import dev.dimension.flare.data.database.cache.CacheDatabase import dev.dimension.flare.data.database.cache.mapper.toDbPagingTimeline +import dev.dimension.flare.data.database.cache.model.DbPagingTimelineWithStatus +import dev.dimension.flare.data.datasource.microblog.paging.BaseTimelineRemoteMediator +import dev.dimension.flare.data.datasource.microblog.paging.PagingRequest +import dev.dimension.flare.data.datasource.microblog.paging.PagingResult import dev.dimension.flare.data.network.misskey.MisskeyService import dev.dimension.flare.data.network.misskey.api.model.UsersNotesRequest import dev.dimension.flare.data.network.misskey.api.model.UsersShowRequest @@ -43,15 +46,15 @@ internal class UserTimelineRemoteMediator( override suspend fun timeline( pageSize: Int, - request: Request, - ): Result { + request: PagingRequest, + ): PagingResult { val response = when (request) { - is Request.Prepend -> return Result( + is PagingRequest.Prepend -> return PagingResult( endOfPaginationReached = true, ) - Request.Refresh -> { + PagingRequest.Refresh -> { val pinned = if (withPinned) { service @@ -91,7 +94,7 @@ internal class UserTimelineRemoteMediator( } } - is Request.Append -> { + is PagingRequest.Append -> { service .usersNotes( UsersNotesRequest( @@ -117,7 +120,7 @@ internal class UserTimelineRemoteMediator( } } - return Result( + return PagingResult( endOfPaginationReached = response.isEmpty(), data = response.toDbPagingTimeline( diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/rss/RssTimelineRemoteMediator.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/rss/RssTimelineRemoteMediator.kt index f00399ab6..ad664fce1 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/rss/RssTimelineRemoteMediator.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/rss/RssTimelineRemoteMediator.kt @@ -1,13 +1,16 @@ package dev.dimension.flare.data.datasource.rss import androidx.paging.ExperimentalPagingApi -import dev.dimension.flare.common.BaseTimelineRemoteMediator import dev.dimension.flare.data.database.app.model.DbRssSources import dev.dimension.flare.data.database.cache.CacheDatabase import dev.dimension.flare.data.database.cache.mapper.createDbPagingTimelineWithStatus +import dev.dimension.flare.data.database.cache.model.DbPagingTimelineWithStatus import dev.dimension.flare.data.database.cache.model.DbStatus import dev.dimension.flare.data.database.cache.model.DbStatusWithUser import dev.dimension.flare.data.database.cache.model.StatusContent +import dev.dimension.flare.data.datasource.microblog.paging.BaseTimelineRemoteMediator +import dev.dimension.flare.data.datasource.microblog.paging.PagingRequest +import dev.dimension.flare.data.datasource.microblog.paging.PagingResult import dev.dimension.flare.data.network.rss.RssService import dev.dimension.flare.data.network.rss.model.Feed import dev.dimension.flare.model.AccountType @@ -37,8 +40,8 @@ internal class RssTimelineRemoteMediator( override suspend fun timeline( pageSize: Int, - request: Request, - ): Result { + request: PagingRequest, + ): PagingResult { val rssSource = fetchSource(url) val response = fetchFeed(url) val title = rssSource?.title ?: response.title @@ -159,7 +162,7 @@ internal class RssTimelineRemoteMediator( ) } - return Result( + return PagingResult( endOfPaginationReached = true, data = content, ) diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/vvo/CommentChildRemoteMediator.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/vvo/CommentChildRemoteMediator.kt index c2da6d2ad..97c79b417 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/vvo/CommentChildRemoteMediator.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/vvo/CommentChildRemoteMediator.kt @@ -2,9 +2,12 @@ package dev.dimension.flare.data.datasource.vvo import SnowflakeIdGenerator import androidx.paging.ExperimentalPagingApi -import dev.dimension.flare.common.BaseTimelineRemoteMediator import dev.dimension.flare.data.database.cache.CacheDatabase import dev.dimension.flare.data.database.cache.mapper.toDbPagingTimeline +import dev.dimension.flare.data.database.cache.model.DbPagingTimelineWithStatus +import dev.dimension.flare.data.datasource.microblog.paging.BaseTimelineRemoteMediator +import dev.dimension.flare.data.datasource.microblog.paging.PagingRequest +import dev.dimension.flare.data.datasource.microblog.paging.PagingResult import dev.dimension.flare.data.network.vvo.VVOService import dev.dimension.flare.data.repository.LoginExpiredException import dev.dimension.flare.model.MicroBlogKey @@ -23,8 +26,8 @@ internal class CommentChildRemoteMediator( override suspend fun timeline( pageSize: Int, - request: Request, - ): Result { + request: PagingRequest, + ): PagingResult { val config = service.config() if (config.data?.login != true) { throw LoginExpiredException( @@ -34,20 +37,20 @@ internal class CommentChildRemoteMediator( } val response = when (request) { - Request.Refresh -> { + PagingRequest.Refresh -> { service .getHotFlowChild( cid = commentKey.id, ) } - is Request.Prepend -> { - return Result( + is PagingRequest.Prepend -> { + return PagingResult( endOfPaginationReached = true, ) } - is Request.Append -> { + is PagingRequest.Append -> { service.getHotFlowChild( cid = commentKey.id, maxId = request.nextKey.toLongOrNull(), @@ -69,7 +72,7 @@ internal class CommentChildRemoteMediator( ) } - return Result( + return PagingResult( endOfPaginationReached = maxId == null, data = data, nextKey = maxId?.toString(), diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/vvo/DiscoverStatusRemoteMediator.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/vvo/DiscoverStatusRemoteMediator.kt index df15d4d33..d232937de 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/vvo/DiscoverStatusRemoteMediator.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/vvo/DiscoverStatusRemoteMediator.kt @@ -2,9 +2,12 @@ package dev.dimension.flare.data.datasource.vvo import SnowflakeIdGenerator import androidx.paging.ExperimentalPagingApi -import dev.dimension.flare.common.BaseTimelineRemoteMediator import dev.dimension.flare.data.database.cache.CacheDatabase import dev.dimension.flare.data.database.cache.mapper.toDbPagingTimeline +import dev.dimension.flare.data.database.cache.model.DbPagingTimelineWithStatus +import dev.dimension.flare.data.datasource.microblog.paging.BaseTimelineRemoteMediator +import dev.dimension.flare.data.datasource.microblog.paging.PagingRequest +import dev.dimension.flare.data.datasource.microblog.paging.PagingResult import dev.dimension.flare.data.network.vvo.VVOService import dev.dimension.flare.data.repository.LoginExpiredException import dev.dimension.flare.model.MicroBlogKey @@ -23,8 +26,8 @@ internal class DiscoverStatusRemoteMediator( override suspend fun timeline( pageSize: Int, - request: Request, - ): Result { + request: PagingRequest, + ): PagingResult { val config = service.config() if (config.data?.login != true) { throw LoginExpiredException( @@ -35,24 +38,24 @@ internal class DiscoverStatusRemoteMediator( val page = when (request) { - is Request.Append -> request.nextKey.toIntOrNull() ?: 0 - is Request.Prepend -> 0 - Request.Refresh -> 0 + is PagingRequest.Append -> request.nextKey.toIntOrNull() ?: 0 + is PagingRequest.Prepend -> 0 + PagingRequest.Refresh -> 0 } val response = when (request) { - Request.Refresh -> { + PagingRequest.Refresh -> { service.getContainerIndex(containerId = containerId) } - is Request.Prepend -> { - return Result( + is PagingRequest.Prepend -> { + return PagingResult( endOfPaginationReached = true, ) } - is Request.Append -> { + is PagingRequest.Append -> { service.getContainerIndex(containerId = containerId, sinceId = page.toString()) } } @@ -74,7 +77,7 @@ internal class DiscoverStatusRemoteMediator( ) } - return Result( + return PagingResult( endOfPaginationReached = status.isEmpty(), data = data, nextKey = (page + 1).toString(), diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/vvo/FavouriteRemoteMediator.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/vvo/FavouriteRemoteMediator.kt index 8eec32443..f3953ae16 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/vvo/FavouriteRemoteMediator.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/vvo/FavouriteRemoteMediator.kt @@ -1,9 +1,12 @@ package dev.dimension.flare.data.datasource.vvo import SnowflakeIdGenerator -import dev.dimension.flare.common.BaseTimelineRemoteMediator import dev.dimension.flare.data.database.cache.CacheDatabase import dev.dimension.flare.data.database.cache.mapper.toDbPagingTimeline +import dev.dimension.flare.data.database.cache.model.DbPagingTimelineWithStatus +import dev.dimension.flare.data.datasource.microblog.paging.BaseTimelineRemoteMediator +import dev.dimension.flare.data.datasource.microblog.paging.PagingRequest +import dev.dimension.flare.data.datasource.microblog.paging.PagingResult import dev.dimension.flare.data.network.vvo.VVOService import dev.dimension.flare.data.repository.LoginExpiredException import dev.dimension.flare.model.MicroBlogKey @@ -21,8 +24,8 @@ internal class FavouriteRemoteMediator( override suspend fun timeline( pageSize: Int, - request: Request, - ): Result { + request: PagingRequest, + ): PagingResult { val config = service.config() if (config.data?.login != true) { throw LoginExpiredException( @@ -33,23 +36,23 @@ internal class FavouriteRemoteMediator( val page = when (request) { - is Request.Append -> request.nextKey.toIntOrNull() + is PagingRequest.Append -> request.nextKey.toIntOrNull() else -> null } val response = when (request) { - Request.Refresh -> { + PagingRequest.Refresh -> { service.getContainerIndex(containerId = containerId) } - is Request.Prepend -> { - return Result( + is PagingRequest.Prepend -> { + return PagingResult( endOfPaginationReached = true, ) } - is Request.Append -> { + is PagingRequest.Append -> { service.getContainerIndex( containerId = containerId, page = page, @@ -77,7 +80,7 @@ internal class FavouriteRemoteMediator( } val nextKey = response.data?.cardlistInfo?.page - return Result( + return PagingResult( endOfPaginationReached = nextKey == null, data = data, nextKey = nextKey?.toString(), diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/vvo/HomeTimelineRemoteMediator.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/vvo/HomeTimelineRemoteMediator.kt index 2b48785e2..b4d54c12a 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/vvo/HomeTimelineRemoteMediator.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/vvo/HomeTimelineRemoteMediator.kt @@ -1,11 +1,14 @@ package dev.dimension.flare.data.datasource.vvo import androidx.paging.ExperimentalPagingApi -import dev.dimension.flare.common.BaseTimelineRemoteMediator import dev.dimension.flare.common.InAppNotification import dev.dimension.flare.common.Message import dev.dimension.flare.data.database.cache.CacheDatabase import dev.dimension.flare.data.database.cache.mapper.toDbPagingTimeline +import dev.dimension.flare.data.database.cache.model.DbPagingTimelineWithStatus +import dev.dimension.flare.data.datasource.microblog.paging.BaseTimelineRemoteMediator +import dev.dimension.flare.data.datasource.microblog.paging.PagingRequest +import dev.dimension.flare.data.datasource.microblog.paging.PagingResult import dev.dimension.flare.data.network.vvo.VVOService import dev.dimension.flare.data.repository.LoginExpiredException import dev.dimension.flare.model.MicroBlogKey @@ -26,8 +29,8 @@ internal class HomeTimelineRemoteMediator( override suspend fun timeline( pageSize: Int, - request: Request, - ): Result { + request: PagingRequest, + ): PagingResult { val config = service.config() if (config.data?.login != true) { inAppNotification.onError( @@ -45,17 +48,17 @@ internal class HomeTimelineRemoteMediator( val response = when (request) { - Request.Refresh -> { + PagingRequest.Refresh -> { service.getFriendsTimeline() } - is Request.Prepend -> { - return Result( + is PagingRequest.Prepend -> { + return PagingResult( endOfPaginationReached = true, ) } - is Request.Append -> { + is PagingRequest.Append -> { service.getFriendsTimeline( maxId = request.nextKey, ) @@ -71,7 +74,7 @@ internal class HomeTimelineRemoteMediator( ) } - return Result( + return PagingResult( endOfPaginationReached = response.data?.nextCursorStr == null, data = data, nextKey = response.data?.nextCursorStr, diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/vvo/LikeRemoteMediator.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/vvo/LikeRemoteMediator.kt index f300e16fa..d61610edc 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/vvo/LikeRemoteMediator.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/vvo/LikeRemoteMediator.kt @@ -1,9 +1,12 @@ package dev.dimension.flare.data.datasource.vvo import SnowflakeIdGenerator -import dev.dimension.flare.common.BaseTimelineRemoteMediator import dev.dimension.flare.data.database.cache.CacheDatabase import dev.dimension.flare.data.database.cache.mapper.toDbPagingTimeline +import dev.dimension.flare.data.database.cache.model.DbPagingTimelineWithStatus +import dev.dimension.flare.data.datasource.microblog.paging.BaseTimelineRemoteMediator +import dev.dimension.flare.data.datasource.microblog.paging.PagingRequest +import dev.dimension.flare.data.datasource.microblog.paging.PagingResult import dev.dimension.flare.data.network.vvo.VVOService import dev.dimension.flare.data.repository.LoginExpiredException import dev.dimension.flare.model.MicroBlogKey @@ -21,8 +24,8 @@ internal class LikeRemoteMediator( override suspend fun timeline( pageSize: Int, - request: Request, - ): Result { + request: PagingRequest, + ): PagingResult { val config = service.config() if (config.data?.login != true) { throw LoginExpiredException( @@ -33,23 +36,23 @@ internal class LikeRemoteMediator( val page = when (request) { - is Request.Append -> request.nextKey.toIntOrNull() + is PagingRequest.Append -> request.nextKey.toIntOrNull() else -> null } val response = when (request) { - Request.Refresh -> { + PagingRequest.Refresh -> { service.getContainerIndex(containerId = containerId) } - is Request.Prepend -> { - return Result( + is PagingRequest.Prepend -> { + return PagingResult( endOfPaginationReached = true, ) } - is Request.Append -> { + is PagingRequest.Append -> { service.getContainerIndex( containerId = containerId, page = page, @@ -84,7 +87,7 @@ internal class LikeRemoteMediator( } val nextKey = response.data?.cardlistInfo?.page - return Result( + return PagingResult( endOfPaginationReached = nextKey == null, data = data, nextKey = nextKey?.toString(), diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/vvo/MentionRemoteMediator.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/vvo/MentionRemoteMediator.kt index 5ca83079c..452203a9e 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/vvo/MentionRemoteMediator.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/vvo/MentionRemoteMediator.kt @@ -1,9 +1,12 @@ package dev.dimension.flare.data.datasource.vvo import androidx.paging.ExperimentalPagingApi -import dev.dimension.flare.common.BaseTimelineRemoteMediator import dev.dimension.flare.data.database.cache.CacheDatabase import dev.dimension.flare.data.database.cache.mapper.toDbPagingTimeline +import dev.dimension.flare.data.database.cache.model.DbPagingTimelineWithStatus +import dev.dimension.flare.data.datasource.microblog.paging.BaseTimelineRemoteMediator +import dev.dimension.flare.data.datasource.microblog.paging.PagingRequest +import dev.dimension.flare.data.datasource.microblog.paging.PagingResult import dev.dimension.flare.data.network.vvo.VVOService import dev.dimension.flare.data.repository.LoginExpiredException import dev.dimension.flare.model.MicroBlogKey @@ -22,8 +25,8 @@ internal class MentionRemoteMediator( override suspend fun timeline( pageSize: Int, - request: Request, - ): Result { + request: PagingRequest, + ): PagingResult { val config = service.config() if (config.data?.login != true) { throw LoginExpiredException( @@ -34,16 +37,16 @@ internal class MentionRemoteMediator( val page = when (request) { - Request.Refresh -> 0 - is Request.Prepend -> return Result( + PagingRequest.Refresh -> 0 + is PagingRequest.Prepend -> return PagingResult( endOfPaginationReached = true, ) - is Request.Append -> request.nextKey.toIntOrNull() ?: 0 + is PagingRequest.Append -> request.nextKey.toIntOrNull() ?: 0 } val response = when (request) { - Request.Refresh -> { + PagingRequest.Refresh -> { val result = service .getMentionsAt( @@ -53,13 +56,13 @@ internal class MentionRemoteMediator( result } - is Request.Prepend -> { - return Result( + is PagingRequest.Prepend -> { + return PagingResult( endOfPaginationReached = true, ) } - is Request.Append -> { + is PagingRequest.Append -> { service.getMentionsAt( page = page, ) @@ -75,7 +78,7 @@ internal class MentionRemoteMediator( ) } - return Result( + return PagingResult( endOfPaginationReached = response.data.isNullOrEmpty(), data = data, nextKey = (page + 1).toString(), diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/vvo/SearchStatusRemoteMediator.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/vvo/SearchStatusRemoteMediator.kt index 99dc1a300..c272dadbb 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/vvo/SearchStatusRemoteMediator.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/vvo/SearchStatusRemoteMediator.kt @@ -1,9 +1,12 @@ package dev.dimension.flare.data.datasource.vvo import androidx.paging.ExperimentalPagingApi -import dev.dimension.flare.common.BaseTimelineRemoteMediator import dev.dimension.flare.data.database.cache.CacheDatabase import dev.dimension.flare.data.database.cache.mapper.toDbPagingTimeline +import dev.dimension.flare.data.database.cache.model.DbPagingTimelineWithStatus +import dev.dimension.flare.data.datasource.microblog.paging.BaseTimelineRemoteMediator +import dev.dimension.flare.data.datasource.microblog.paging.PagingRequest +import dev.dimension.flare.data.datasource.microblog.paging.PagingResult import dev.dimension.flare.data.network.vvo.VVOService import dev.dimension.flare.data.repository.LoginExpiredException import dev.dimension.flare.model.MicroBlogKey @@ -31,8 +34,8 @@ internal class SearchStatusRemoteMediator( override suspend fun timeline( pageSize: Int, - request: Request, - ): Result { + request: PagingRequest, + ): PagingResult { val config = service.config() if (config.data?.login != true) { throw LoginExpiredException( @@ -43,14 +46,14 @@ internal class SearchStatusRemoteMediator( val page = when (request) { - is Request.Append -> request.nextKey.toIntOrNull() ?: 1 - is Request.Prepend -> 1 - Request.Refresh -> 1 + is PagingRequest.Append -> request.nextKey.toIntOrNull() ?: 1 + is PagingRequest.Prepend -> 1 + PagingRequest.Refresh -> 1 } val response = when (request) { - Request.Refresh -> { + PagingRequest.Refresh -> { service .getContainerIndex( containerId = containerId, @@ -58,13 +61,13 @@ internal class SearchStatusRemoteMediator( ) } - is Request.Prepend -> { - return Result( + is PagingRequest.Prepend -> { + return PagingResult( endOfPaginationReached = true, ) } - is Request.Append -> { + is PagingRequest.Append -> { service.getContainerIndex( containerId = containerId, pageType = "searchall", @@ -91,7 +94,7 @@ internal class SearchStatusRemoteMediator( ) } - return Result( + return PagingResult( endOfPaginationReached = status.isEmpty(), data = data, nextKey = (page + 1).toString(), diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/vvo/StatusCommentRemoteMediator.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/vvo/StatusCommentRemoteMediator.kt index c6fe3c3ef..7add6deaa 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/vvo/StatusCommentRemoteMediator.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/vvo/StatusCommentRemoteMediator.kt @@ -2,9 +2,12 @@ package dev.dimension.flare.data.datasource.vvo import SnowflakeIdGenerator import androidx.paging.ExperimentalPagingApi -import dev.dimension.flare.common.BaseTimelineRemoteMediator import dev.dimension.flare.data.database.cache.CacheDatabase import dev.dimension.flare.data.database.cache.mapper.toDbPagingTimeline +import dev.dimension.flare.data.database.cache.model.DbPagingTimelineWithStatus +import dev.dimension.flare.data.datasource.microblog.paging.BaseTimelineRemoteMediator +import dev.dimension.flare.data.datasource.microblog.paging.PagingRequest +import dev.dimension.flare.data.datasource.microblog.paging.PagingResult import dev.dimension.flare.data.network.vvo.VVOService import dev.dimension.flare.data.repository.LoginExpiredException import dev.dimension.flare.model.MicroBlogKey @@ -23,8 +26,8 @@ internal class StatusCommentRemoteMediator( override suspend fun timeline( pageSize: Int, - request: Request, - ): Result { + request: PagingRequest, + ): PagingResult { val config = service.config() if (config.data?.login != true) { throw LoginExpiredException( @@ -35,7 +38,7 @@ internal class StatusCommentRemoteMediator( val response = when (request) { - Request.Refresh -> { + PagingRequest.Refresh -> { service .getHotComments( id = statusKey.id, @@ -44,13 +47,13 @@ internal class StatusCommentRemoteMediator( ) } - is Request.Prepend -> { - return Result( + is PagingRequest.Prepend -> { + return PagingResult( endOfPaginationReached = true, ) } - is Request.Append -> { + is PagingRequest.Append -> { service.getHotComments( id = statusKey.id, mid = statusKey.id, @@ -73,7 +76,7 @@ internal class StatusCommentRemoteMediator( ) } - return Result( + return PagingResult( endOfPaginationReached = maxId == null, data = data, nextKey = maxId?.toString(), diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/vvo/StatusRepostRemoteMediator.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/vvo/StatusRepostRemoteMediator.kt index c4a630437..95ea72b06 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/vvo/StatusRepostRemoteMediator.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/vvo/StatusRepostRemoteMediator.kt @@ -1,9 +1,12 @@ package dev.dimension.flare.data.datasource.vvo import androidx.paging.ExperimentalPagingApi -import dev.dimension.flare.common.BaseTimelineRemoteMediator import dev.dimension.flare.data.database.cache.CacheDatabase import dev.dimension.flare.data.database.cache.mapper.toDbPagingTimeline +import dev.dimension.flare.data.database.cache.model.DbPagingTimelineWithStatus +import dev.dimension.flare.data.datasource.microblog.paging.BaseTimelineRemoteMediator +import dev.dimension.flare.data.datasource.microblog.paging.PagingRequest +import dev.dimension.flare.data.datasource.microblog.paging.PagingResult import dev.dimension.flare.data.network.vvo.VVOService import dev.dimension.flare.data.repository.LoginExpiredException import dev.dimension.flare.model.MicroBlogKey @@ -22,8 +25,8 @@ internal class StatusRepostRemoteMediator( override suspend fun timeline( pageSize: Int, - request: Request, - ): Result { + request: PagingRequest, + ): PagingResult { val config = service.config() if (config.data?.login != true) { throw LoginExpiredException( @@ -34,14 +37,14 @@ internal class StatusRepostRemoteMediator( val page = when (request) { - Request.Refresh -> 1 - is Request.Append -> request.nextKey.toIntOrNull() ?: 1 - is Request.Prepend -> return Result(endOfPaginationReached = true) + PagingRequest.Refresh -> 1 + is PagingRequest.Append -> request.nextKey.toIntOrNull() ?: 1 + is PagingRequest.Prepend -> return PagingResult(endOfPaginationReached = true) } val response = when (request) { - Request.Refresh -> { + PagingRequest.Refresh -> { service .getRepostTimeline( id = statusKey.id, @@ -49,13 +52,13 @@ internal class StatusRepostRemoteMediator( ) } - is Request.Prepend -> { - return Result( + is PagingRequest.Prepend -> { + return PagingResult( endOfPaginationReached = true, ) } - is Request.Append -> { + is PagingRequest.Append -> { service.getRepostTimeline( id = statusKey.id, page = page, @@ -80,7 +83,7 @@ internal class StatusRepostRemoteMediator( ) } - return Result( + return PagingResult( endOfPaginationReached = statuses.isEmpty(), data = data, nextKey = (page + 1).toString(), diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/vvo/UserTimelineRemoteMediator.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/vvo/UserTimelineRemoteMediator.kt index 4bd2f48eb..212370de3 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/vvo/UserTimelineRemoteMediator.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/vvo/UserTimelineRemoteMediator.kt @@ -2,9 +2,12 @@ package dev.dimension.flare.data.datasource.vvo import SnowflakeIdGenerator import androidx.paging.ExperimentalPagingApi -import dev.dimension.flare.common.BaseTimelineRemoteMediator import dev.dimension.flare.data.database.cache.CacheDatabase import dev.dimension.flare.data.database.cache.mapper.toDbPagingTimeline +import dev.dimension.flare.data.database.cache.model.DbPagingTimelineWithStatus +import dev.dimension.flare.data.datasource.microblog.paging.BaseTimelineRemoteMediator +import dev.dimension.flare.data.datasource.microblog.paging.PagingRequest +import dev.dimension.flare.data.datasource.microblog.paging.PagingResult import dev.dimension.flare.data.network.vvo.VVOService import dev.dimension.flare.data.repository.LoginExpiredException import dev.dimension.flare.model.MicroBlogKey @@ -34,11 +37,11 @@ internal class UserTimelineRemoteMediator( override suspend fun timeline( pageSize: Int, - request: Request, - ): Result { + request: PagingRequest, + ): PagingResult { if (mediaOnly) { // Not supported yet - return Result( + return PagingResult( endOfPaginationReached = true, ) } @@ -63,7 +66,7 @@ internal class UserTimelineRemoteMediator( } val response = when (request) { - Request.Refresh -> { + PagingRequest.Refresh -> { service .getContainerIndex( type = "uid", @@ -72,13 +75,13 @@ internal class UserTimelineRemoteMediator( ) } - is Request.Prepend -> { - return Result( + is PagingRequest.Prepend -> { + return PagingResult( endOfPaginationReached = true, ) } - is Request.Append -> { + is PagingRequest.Append -> { service.getContainerIndex( type = "uid", value = userKey.id, @@ -104,7 +107,7 @@ internal class UserTimelineRemoteMediator( ) } - return Result( + return PagingResult( endOfPaginationReached = response.data?.cardlistInfo?.sinceID == null, data = data, nextKey = diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/vvo/VVODataSource.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/vvo/VVODataSource.kt index 35b764204..5f0a8dcc9 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/vvo/VVODataSource.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/vvo/VVODataSource.kt @@ -4,7 +4,6 @@ import androidx.paging.ExperimentalPagingApi import androidx.paging.Pager import androidx.paging.PagingData import androidx.paging.cachedIn -import dev.dimension.flare.common.BaseTimelineLoader import dev.dimension.flare.common.CacheData import dev.dimension.flare.common.Cacheable import dev.dimension.flare.common.FileItem @@ -29,6 +28,7 @@ import dev.dimension.flare.data.datasource.microblog.NotificationFilter import dev.dimension.flare.data.datasource.microblog.ProfileAction import dev.dimension.flare.data.datasource.microblog.ProfileTab import dev.dimension.flare.data.datasource.microblog.StatusEvent +import dev.dimension.flare.data.datasource.microblog.paging.BaseTimelineLoader import dev.dimension.flare.data.datasource.microblog.pagingConfig import dev.dimension.flare.data.datasource.microblog.relationKeyWithUserKey import dev.dimension.flare.data.datasource.microblog.timelinePager diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/xqt/DeviceFollowRemoteMediator.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/xqt/DeviceFollowRemoteMediator.kt index c76fa1078..43f8fcabb 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/xqt/DeviceFollowRemoteMediator.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/xqt/DeviceFollowRemoteMediator.kt @@ -1,11 +1,14 @@ package dev.dimension.flare.data.datasource.xqt import androidx.paging.ExperimentalPagingApi -import dev.dimension.flare.common.BaseTimelineRemoteMediator import dev.dimension.flare.data.database.cache.CacheDatabase import dev.dimension.flare.data.database.cache.mapper.cursor import dev.dimension.flare.data.database.cache.mapper.toDbPagingTimeline import dev.dimension.flare.data.database.cache.mapper.tweets +import dev.dimension.flare.data.database.cache.model.DbPagingTimelineWithStatus +import dev.dimension.flare.data.datasource.microblog.paging.BaseTimelineRemoteMediator +import dev.dimension.flare.data.datasource.microblog.paging.PagingRequest +import dev.dimension.flare.data.datasource.microblog.paging.PagingResult import dev.dimension.flare.data.network.xqt.XQTService import dev.dimension.flare.model.MicroBlogKey @@ -21,24 +24,24 @@ internal class DeviceFollowRemoteMediator( override suspend fun timeline( pageSize: Int, - request: Request, - ): Result { + request: PagingRequest, + ): PagingResult { val response = when (request) { - Request.Refresh -> { + PagingRequest.Refresh -> { service .getNotificationsDeviceFollow( count = pageSize, ) } - is Request.Prepend -> { - return Result( + is PagingRequest.Prepend -> { + return PagingResult( endOfPaginationReached = true, ) } - is Request.Append -> { + is PagingRequest.Append -> { service.getNotificationsDeviceFollow( count = pageSize, cursor = request.nextKey, @@ -49,7 +52,7 @@ internal class DeviceFollowRemoteMediator( val data = tweets.mapNotNull { it.toDbPagingTimeline(accountKey, pagingKey) } - return Result( + return PagingResult( endOfPaginationReached = tweets.isEmpty(), data = data, nextKey = response.cursor(), diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/xqt/HomeTimelineRemoteMediator.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/xqt/HomeTimelineRemoteMediator.kt index 20a666b1b..59b4c00a1 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/xqt/HomeTimelineRemoteMediator.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/xqt/HomeTimelineRemoteMediator.kt @@ -1,7 +1,6 @@ package dev.dimension.flare.data.datasource.xqt import androidx.paging.ExperimentalPagingApi -import dev.dimension.flare.common.BaseTimelineRemoteMediator import dev.dimension.flare.common.InAppNotification import dev.dimension.flare.common.Message import dev.dimension.flare.common.encodeJson @@ -9,6 +8,10 @@ import dev.dimension.flare.data.database.cache.CacheDatabase import dev.dimension.flare.data.database.cache.mapper.cursor import dev.dimension.flare.data.database.cache.mapper.toDbPagingTimeline import dev.dimension.flare.data.database.cache.mapper.tweets +import dev.dimension.flare.data.database.cache.model.DbPagingTimelineWithStatus +import dev.dimension.flare.data.datasource.microblog.paging.BaseTimelineRemoteMediator +import dev.dimension.flare.data.datasource.microblog.paging.PagingRequest +import dev.dimension.flare.data.datasource.microblog.paging.PagingResult import dev.dimension.flare.data.network.xqt.XQTService import dev.dimension.flare.data.repository.LoginExpiredException import dev.dimension.flare.model.MicroBlogKey @@ -30,11 +33,11 @@ internal class HomeTimelineRemoteMediator( override suspend fun timeline( pageSize: Int, - request: Request, - ): Result { + request: PagingRequest, + ): PagingResult { val response = when (request) { - Request.Refresh -> { + PagingRequest.Refresh -> { service .getHomeLatestTimeline( variables = @@ -44,13 +47,13 @@ internal class HomeTimelineRemoteMediator( ) } - is Request.Prepend -> { - return Result( + is PagingRequest.Prepend -> { + return PagingResult( endOfPaginationReached = true, ) } - is Request.Append -> { + is PagingRequest.Append -> { service.getHomeLatestTimeline( variables = HomeTimelineRequest( @@ -72,7 +75,7 @@ internal class HomeTimelineRemoteMediator( val data = tweet.mapNotNull { it.toDbPagingTimeline(accountKey, pagingKey) } - return Result( + return PagingResult( endOfPaginationReached = tweet.isEmpty(), data = data, nextKey = cursor, @@ -104,11 +107,11 @@ internal class FeaturedTimelineRemoteMediator( override suspend fun timeline( pageSize: Int, - request: Request, - ): Result { + request: PagingRequest, + ): PagingResult { val response = when (request) { - Request.Refresh -> { + PagingRequest.Refresh -> { service .getHomeTimeline( variables = @@ -118,13 +121,13 @@ internal class FeaturedTimelineRemoteMediator( ) } - is Request.Prepend -> { - return Result( + is PagingRequest.Prepend -> { + return PagingResult( endOfPaginationReached = true, ) } - is Request.Append -> { + is PagingRequest.Append -> { service.getHomeTimeline( variables = HomeTimelineRequest( @@ -145,7 +148,7 @@ internal class FeaturedTimelineRemoteMediator( val data = tweet.mapNotNull { it.toDbPagingTimeline(accountKey, pagingKey) } - return Result( + return PagingResult( endOfPaginationReached = tweet.isEmpty(), data = data, nextKey = instructions.cursor(), @@ -165,11 +168,11 @@ internal class BookmarkTimelineRemoteMediator( override suspend fun timeline( pageSize: Int, - request: Request, - ): Result { + request: PagingRequest, + ): PagingResult { val response = when (request) { - Request.Refresh -> { + PagingRequest.Refresh -> { service .getBookmarks( variables = @@ -179,13 +182,13 @@ internal class BookmarkTimelineRemoteMediator( ) } - is Request.Prepend -> { - return Result( + is PagingRequest.Prepend -> { + return PagingResult( endOfPaginationReached = true, ) } - is Request.Append -> { + is PagingRequest.Append -> { service.getBookmarks( variables = HomeTimelineRequest( @@ -206,7 +209,7 @@ internal class BookmarkTimelineRemoteMediator( val data = tweet.mapNotNull { it.toDbPagingTimeline(accountKey, pagingKey) } - return Result( + return PagingResult( endOfPaginationReached = tweet.isEmpty(), data = data, nextKey = instructions.cursor(), diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/xqt/ListTimelineRemoteMediator.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/xqt/ListTimelineRemoteMediator.kt index 36639c509..e73e80576 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/xqt/ListTimelineRemoteMediator.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/xqt/ListTimelineRemoteMediator.kt @@ -1,13 +1,13 @@ package dev.dimension.flare.data.datasource.xqt import androidx.paging.ExperimentalPagingApi -import dev.dimension.flare.common.BaseTimelineRemoteMediator -import dev.dimension.flare.common.BaseTimelineRemoteMediator.Request import dev.dimension.flare.common.encodeJson import dev.dimension.flare.data.database.cache.CacheDatabase import dev.dimension.flare.data.database.cache.mapper.cursor import dev.dimension.flare.data.database.cache.mapper.toDbPagingTimeline import dev.dimension.flare.data.database.cache.mapper.tweets +import dev.dimension.flare.data.datasource.microblog.paging.BaseTimelineRemoteMediator +import dev.dimension.flare.data.datasource.microblog.paging.BaseTimelineRemoteMediator.Request import dev.dimension.flare.data.network.xqt.XQTService import dev.dimension.flare.model.MicroBlogKey import kotlinx.serialization.SerialName @@ -38,7 +38,7 @@ internal class ListTimelineRemoteMediator( ): Result { val response = when (request) { - BaseTimelineRemoteMediator.Request.Refresh -> { + BaseTimelineRemoteMediator.PagingRequest.Refresh -> { service .getListLatestTweetsTimeline( variables = @@ -49,13 +49,13 @@ internal class ListTimelineRemoteMediator( ) } - is Request.Prepend -> { - return Result( + is PagingRequest.Prepend -> { + return PagingResult( endOfPaginationReached = true, ) } - is Request.Append -> { + is PagingRequest.Append -> { service.getListLatestTweetsTimeline( variables = Request( @@ -70,7 +70,7 @@ internal class ListTimelineRemoteMediator( val data = result.mapNotNull { it.toDbPagingTimeline(accountKey, pagingKey) } - return Result( + return PagingResult( endOfPaginationReached = response.isEmpty(), data = data, nextKey = response.cursor(), diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/xqt/MentionRemoteMediator.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/xqt/MentionRemoteMediator.kt index 857ad5696..d56913f1d 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/xqt/MentionRemoteMediator.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/xqt/MentionRemoteMediator.kt @@ -1,11 +1,14 @@ package dev.dimension.flare.data.datasource.xqt import androidx.paging.ExperimentalPagingApi -import dev.dimension.flare.common.BaseTimelineRemoteMediator import dev.dimension.flare.data.database.cache.CacheDatabase import dev.dimension.flare.data.database.cache.mapper.cursor import dev.dimension.flare.data.database.cache.mapper.toDbPagingTimeline import dev.dimension.flare.data.database.cache.mapper.tweets +import dev.dimension.flare.data.database.cache.model.DbPagingTimelineWithStatus +import dev.dimension.flare.data.datasource.microblog.paging.BaseTimelineRemoteMediator +import dev.dimension.flare.data.datasource.microblog.paging.PagingRequest +import dev.dimension.flare.data.datasource.microblog.paging.PagingResult import dev.dimension.flare.data.network.xqt.XQTService import dev.dimension.flare.model.MicroBlogKey @@ -21,24 +24,24 @@ internal class MentionRemoteMediator( override suspend fun timeline( pageSize: Int, - request: Request, - ): Result { + request: PagingRequest, + ): PagingResult { val response = when (request) { - Request.Refresh -> { + PagingRequest.Refresh -> { service .getNotificationsMentions( count = pageSize, ) } - is Request.Prepend -> { - return Result( + is PagingRequest.Prepend -> { + return PagingResult( endOfPaginationReached = true, ) } - is Request.Append -> { + is PagingRequest.Append -> { service.getNotificationsMentions( count = pageSize, cursor = request.nextKey, @@ -49,7 +52,7 @@ internal class MentionRemoteMediator( val data = tweets.mapNotNull { it.toDbPagingTimeline(accountKey, pagingKey) } - return Result( + return PagingResult( endOfPaginationReached = tweets.isEmpty(), data = data, nextKey = response.cursor(), diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/xqt/SearchStatusPagingSource.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/xqt/SearchStatusPagingSource.kt index 9790dcd28..638051d11 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/xqt/SearchStatusPagingSource.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/xqt/SearchStatusPagingSource.kt @@ -1,13 +1,16 @@ package dev.dimension.flare.data.datasource.xqt import androidx.paging.ExperimentalPagingApi -import dev.dimension.flare.common.BaseTimelineRemoteMediator import dev.dimension.flare.common.encodeJson import dev.dimension.flare.data.database.cache.CacheDatabase import dev.dimension.flare.data.database.cache.mapper.cursor import dev.dimension.flare.data.database.cache.mapper.isBottomEnd import dev.dimension.flare.data.database.cache.mapper.toDbPagingTimeline import dev.dimension.flare.data.database.cache.mapper.tweets +import dev.dimension.flare.data.database.cache.model.DbPagingTimelineWithStatus +import dev.dimension.flare.data.datasource.microblog.paging.BaseTimelineRemoteMediator +import dev.dimension.flare.data.datasource.microblog.paging.PagingRequest +import dev.dimension.flare.data.datasource.microblog.paging.PagingResult import dev.dimension.flare.data.network.xqt.XQTService import dev.dimension.flare.model.MicroBlogKey import io.ktor.http.encodeURLQueryComponent @@ -27,11 +30,11 @@ internal class SearchStatusPagingSource( override suspend fun timeline( pageSize: Int, - request: Request, - ): Result { + request: PagingRequest, + ): PagingResult { val response = when (request) { - Request.Refresh -> { + PagingRequest.Refresh -> { service .getSearchTimeline( variables = @@ -43,13 +46,13 @@ internal class SearchStatusPagingSource( ) } - is Request.Prepend -> { - return Result( + is PagingRequest.Prepend -> { + return PagingResult( endOfPaginationReached = true, ) } - is Request.Append -> { + is PagingRequest.Append -> { service.getSearchTimeline( variables = SearchRequest( @@ -65,7 +68,7 @@ internal class SearchStatusPagingSource( val data = tweets.mapNotNull { it.toDbPagingTimeline(accountKey, pagingKey) } - return Result( + return PagingResult( endOfPaginationReached = response.isBottomEnd(), data = data, nextKey = response.cursor(), diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/xqt/StatusDetailRemoteMediator.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/xqt/StatusDetailRemoteMediator.kt index b7bbd8822..b21bdbf85 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/xqt/StatusDetailRemoteMediator.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/xqt/StatusDetailRemoteMediator.kt @@ -1,7 +1,6 @@ package dev.dimension.flare.data.datasource.xqt import androidx.paging.ExperimentalPagingApi -import dev.dimension.flare.common.BaseTimelineRemoteMediator import dev.dimension.flare.common.encodeJson import dev.dimension.flare.data.database.cache.CacheDatabase import dev.dimension.flare.data.database.cache.connect @@ -10,7 +9,11 @@ import dev.dimension.flare.data.database.cache.mapper.isBottomEnd import dev.dimension.flare.data.database.cache.mapper.toDbPagingTimeline import dev.dimension.flare.data.database.cache.mapper.tweets import dev.dimension.flare.data.database.cache.model.DbPagingTimeline +import dev.dimension.flare.data.database.cache.model.DbPagingTimelineWithStatus import dev.dimension.flare.data.datasource.microblog.StatusEvent +import dev.dimension.flare.data.datasource.microblog.paging.BaseTimelineRemoteMediator +import dev.dimension.flare.data.datasource.microblog.paging.PagingRequest +import dev.dimension.flare.data.datasource.microblog.paging.PagingResult import dev.dimension.flare.data.network.xqt.XQTService import dev.dimension.flare.data.network.xqt.model.Tweet import dev.dimension.flare.data.network.xqt.model.TweetTombstone @@ -47,12 +50,12 @@ internal class StatusDetailRemoteMediator( override suspend fun timeline( pageSize: Int, - request: Request, - ): Result { + request: PagingRequest, + ): PagingResult { when (request) { - is Request.Append -> { + is PagingRequest.Append -> { if (statusOnly) { - return Result( + return PagingResult( endOfPaginationReached = true, ) } else { @@ -107,19 +110,19 @@ internal class StatusDetailRemoteMediator( } val result = actualTweet.mapNotNull { it.toDbPagingTimeline(accountKey, pagingKey) } - return Result( + return PagingResult( endOfPaginationReached = response.isBottomEnd() || actualTweet.size == 1 || response.cursor() == null, data = result, nextKey = response.cursor(), ) } } - is Request.Prepend -> { - return Result( + is PagingRequest.Prepend -> { + return PagingResult( endOfPaginationReached = true, ) } - Request.Refresh -> { + PagingRequest.Refresh -> { if (!database.pagingTimelineDao().existsPaging(accountKey, pagingKey)) { database.statusDao().get(statusKey, AccountType.Specific(accountKey)).firstOrNull()?.let { database.connect { @@ -156,7 +159,7 @@ internal class StatusDetailRemoteMediator( val item = tweet.firstOrNull { it.id == statusKey.id } val result = listOf(item).mapNotNull { it?.toDbPagingTimeline(accountKey, pagingKey) } - return Result( + return PagingResult( endOfPaginationReached = statusOnly, data = result, nextKey = "", diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/xqt/UserLikesTimelineRemoteMediator.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/xqt/UserLikesTimelineRemoteMediator.kt index 680efc024..19e3a6fed 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/xqt/UserLikesTimelineRemoteMediator.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/xqt/UserLikesTimelineRemoteMediator.kt @@ -1,12 +1,15 @@ package dev.dimension.flare.data.datasource.xqt import androidx.paging.ExperimentalPagingApi -import dev.dimension.flare.common.BaseTimelineRemoteMediator import dev.dimension.flare.common.encodeJson import dev.dimension.flare.data.database.cache.CacheDatabase import dev.dimension.flare.data.database.cache.mapper.cursor import dev.dimension.flare.data.database.cache.mapper.toDbPagingTimeline import dev.dimension.flare.data.database.cache.mapper.tweets +import dev.dimension.flare.data.database.cache.model.DbPagingTimelineWithStatus +import dev.dimension.flare.data.datasource.microblog.paging.BaseTimelineRemoteMediator +import dev.dimension.flare.data.datasource.microblog.paging.PagingRequest +import dev.dimension.flare.data.datasource.microblog.paging.PagingResult import dev.dimension.flare.data.network.xqt.XQTService import dev.dimension.flare.model.MicroBlogKey @@ -23,11 +26,11 @@ internal class UserLikesTimelineRemoteMediator( override suspend fun timeline( pageSize: Int, - request: Request, - ): Result { + request: PagingRequest, + ): PagingResult { val response = when (request) { - Request.Refresh -> { + PagingRequest.Refresh -> { service .getLikes( variables = @@ -38,13 +41,13 @@ internal class UserLikesTimelineRemoteMediator( ) } - is Request.Prepend -> { - return Result( + is PagingRequest.Prepend -> { + return PagingResult( endOfPaginationReached = true, ) } - is Request.Append -> { + is PagingRequest.Append -> { service.getLikes( variables = UserTimelineRequest( @@ -66,12 +69,12 @@ internal class UserLikesTimelineRemoteMediator( .orEmpty() val tweet = instructions.tweets( - includePin = request is Request.Refresh, + includePin = request is PagingRequest.Refresh, ) val data = tweet.mapNotNull { it.toDbPagingTimeline(accountKey, pagingKey) } - return Result( + return PagingResult( endOfPaginationReached = tweet.isEmpty(), data = data, nextKey = instructions.cursor(), diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/xqt/UserMediaTimelineRemoteMediator.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/xqt/UserMediaTimelineRemoteMediator.kt index 668433f1c..865e0d162 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/xqt/UserMediaTimelineRemoteMediator.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/xqt/UserMediaTimelineRemoteMediator.kt @@ -1,12 +1,15 @@ package dev.dimension.flare.data.datasource.xqt import androidx.paging.ExperimentalPagingApi -import dev.dimension.flare.common.BaseTimelineRemoteMediator import dev.dimension.flare.common.encodeJson import dev.dimension.flare.data.database.cache.CacheDatabase import dev.dimension.flare.data.database.cache.mapper.cursor import dev.dimension.flare.data.database.cache.mapper.toDbPagingTimeline import dev.dimension.flare.data.database.cache.mapper.tweets +import dev.dimension.flare.data.database.cache.model.DbPagingTimelineWithStatus +import dev.dimension.flare.data.datasource.microblog.paging.BaseTimelineRemoteMediator +import dev.dimension.flare.data.datasource.microblog.paging.PagingRequest +import dev.dimension.flare.data.datasource.microblog.paging.PagingResult import dev.dimension.flare.data.network.xqt.XQTService import dev.dimension.flare.model.MicroBlogKey @@ -23,11 +26,11 @@ internal class UserMediaTimelineRemoteMediator( override suspend fun timeline( pageSize: Int, - request: Request, - ): Result { + request: PagingRequest, + ): PagingResult { val response = when (request) { - Request.Refresh -> { + PagingRequest.Refresh -> { service .getUserMedia( variables = @@ -38,13 +41,13 @@ internal class UserMediaTimelineRemoteMediator( ) } - is Request.Prepend -> { - return Result( + is PagingRequest.Prepend -> { + return PagingResult( endOfPaginationReached = true, ) } - is Request.Append -> { + is PagingRequest.Append -> { service.getUserMedia( variables = UserTimelineRequest( @@ -66,7 +69,7 @@ internal class UserMediaTimelineRemoteMediator( .orEmpty() val tweet = instructions.tweets( - includePin = request is Request.Refresh, + includePin = request is PagingRequest.Refresh, ) val data = @@ -80,7 +83,7 @@ internal class UserMediaTimelineRemoteMediator( ) } - return Result( + return PagingResult( endOfPaginationReached = tweet.isEmpty(), data = data, nextKey = instructions.cursor(), diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/xqt/UserRepliesTimelineRemoteMediator.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/xqt/UserRepliesTimelineRemoteMediator.kt index 202876185..fe2897bac 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/xqt/UserRepliesTimelineRemoteMediator.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/xqt/UserRepliesTimelineRemoteMediator.kt @@ -1,12 +1,15 @@ package dev.dimension.flare.data.datasource.xqt import androidx.paging.ExperimentalPagingApi -import dev.dimension.flare.common.BaseTimelineRemoteMediator import dev.dimension.flare.common.encodeJson import dev.dimension.flare.data.database.cache.CacheDatabase import dev.dimension.flare.data.database.cache.mapper.cursor import dev.dimension.flare.data.database.cache.mapper.toDbPagingTimeline import dev.dimension.flare.data.database.cache.mapper.tweets +import dev.dimension.flare.data.database.cache.model.DbPagingTimelineWithStatus +import dev.dimension.flare.data.datasource.microblog.paging.BaseTimelineRemoteMediator +import dev.dimension.flare.data.datasource.microblog.paging.PagingRequest +import dev.dimension.flare.data.datasource.microblog.paging.PagingResult import dev.dimension.flare.data.network.xqt.XQTService import dev.dimension.flare.model.MicroBlogKey @@ -23,11 +26,11 @@ internal class UserRepliesTimelineRemoteMediator( override suspend fun timeline( pageSize: Int, - request: Request, - ): Result { + request: PagingRequest, + ): PagingResult { val response = when (request) { - Request.Refresh -> { + PagingRequest.Refresh -> { service .getUserTweetsAndReplies( variables = @@ -38,13 +41,13 @@ internal class UserRepliesTimelineRemoteMediator( ) } - is Request.Prepend -> { - return Result( + is PagingRequest.Prepend -> { + return PagingResult( endOfPaginationReached = true, ) } - is Request.Append -> { + is PagingRequest.Append -> { service.getUserTweetsAndReplies( variables = UserTimelineRequest( @@ -66,12 +69,12 @@ internal class UserRepliesTimelineRemoteMediator( .orEmpty() val tweet = instructions.tweets( - includePin = request is Request.Refresh, + includePin = request is PagingRequest.Refresh, ) val data = tweet.mapNotNull { it.toDbPagingTimeline(accountKey, pagingKey) } - return Result( + return PagingResult( endOfPaginationReached = tweet.isEmpty(), data = data, nextKey = instructions.cursor(), diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/xqt/UserTimelineRemoteMediator.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/xqt/UserTimelineRemoteMediator.kt index 9092fcc34..24ca48625 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/xqt/UserTimelineRemoteMediator.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/xqt/UserTimelineRemoteMediator.kt @@ -1,12 +1,15 @@ package dev.dimension.flare.data.datasource.xqt import androidx.paging.ExperimentalPagingApi -import dev.dimension.flare.common.BaseTimelineRemoteMediator import dev.dimension.flare.common.encodeJson import dev.dimension.flare.data.database.cache.CacheDatabase import dev.dimension.flare.data.database.cache.mapper.cursor import dev.dimension.flare.data.database.cache.mapper.toDbPagingTimeline import dev.dimension.flare.data.database.cache.mapper.tweets +import dev.dimension.flare.data.database.cache.model.DbPagingTimelineWithStatus +import dev.dimension.flare.data.datasource.microblog.paging.BaseTimelineRemoteMediator +import dev.dimension.flare.data.datasource.microblog.paging.PagingRequest +import dev.dimension.flare.data.datasource.microblog.paging.PagingResult import dev.dimension.flare.data.network.xqt.XQTService import dev.dimension.flare.model.MicroBlogKey import kotlinx.serialization.Required @@ -26,11 +29,11 @@ internal class UserTimelineRemoteMediator( override suspend fun timeline( pageSize: Int, - request: Request, - ): Result { + request: PagingRequest, + ): PagingResult { val response = when (request) { - Request.Refresh -> { + PagingRequest.Refresh -> { service .getUserTweets( variables = @@ -41,13 +44,13 @@ internal class UserTimelineRemoteMediator( ) } - is Request.Prepend -> { - return Result( + is PagingRequest.Prepend -> { + return PagingResult( endOfPaginationReached = true, ) } - is Request.Append -> { + is PagingRequest.Append -> { service.getUserTweets( variables = UserTimelineRequest( @@ -69,12 +72,12 @@ internal class UserTimelineRemoteMediator( .orEmpty() val tweet = instructions.tweets( - includePin = request is Request.Refresh, + includePin = request is PagingRequest.Refresh, ) val data = tweet.mapNotNull { it.toDbPagingTimeline(accountKey, pagingKey) } - return Result( + return PagingResult( endOfPaginationReached = tweet.isEmpty(), data = data, nextKey = instructions.cursor(), diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/xqt/XQTDataSource.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/xqt/XQTDataSource.kt index df5e788b7..1f4563e00 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/xqt/XQTDataSource.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/xqt/XQTDataSource.kt @@ -8,7 +8,6 @@ import androidx.paging.PagingState import androidx.paging.cachedIn import androidx.paging.map import dev.dimension.flare.common.BaseRemoteMediator -import dev.dimension.flare.common.BaseTimelineLoader import dev.dimension.flare.common.CacheData import dev.dimension.flare.common.Cacheable import dev.dimension.flare.common.FileType @@ -45,6 +44,7 @@ import dev.dimension.flare.data.datasource.microblog.RelationDataSource import dev.dimension.flare.data.datasource.microblog.StatusEvent import dev.dimension.flare.data.datasource.microblog.createSendingDirectMessage import dev.dimension.flare.data.datasource.microblog.memoryPager +import dev.dimension.flare.data.datasource.microblog.paging.BaseTimelineLoader import dev.dimension.flare.data.datasource.microblog.pagingConfig import dev.dimension.flare.data.datasource.microblog.relationKeyWithUserKey import dev.dimension.flare.data.datasource.microblog.timelinePager diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/network/misskey/api/NotesApi.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/network/misskey/api/NotesApi.kt index 8bba1675d..747c67494 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/data/network/misskey/api/NotesApi.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/data/network/misskey/api/NotesApi.kt @@ -1,6 +1,7 @@ package dev.dimension.flare.data.network.misskey.api import de.jensklingenberg.ktorfit.http.Body +import de.jensklingenberg.ktorfit.http.Header import de.jensklingenberg.ktorfit.http.POST import dev.dimension.flare.data.network.misskey.api.model.ChannelsTimelineRequest import dev.dimension.flare.data.network.misskey.api.model.IPinRequest @@ -42,6 +43,7 @@ internal interface NotesApi { */ @POST("channels/timeline") suspend fun channelsTimeline( + @Header("Content-Type") contentType: kotlin.String = "application/json", @Body channelsTimelineRequest: ChannelsTimelineRequest, ): kotlin.collections.List diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/model/UiList.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/model/UiList.kt index f16b83691..4cc6d066a 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/model/UiList.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/model/UiList.kt @@ -1,15 +1,20 @@ package dev.dimension.flare.ui.model import androidx.compose.runtime.Immutable +import dev.dimension.flare.model.MicroBlogKey +import kotlinx.serialization.Serializable +@Serializable @Immutable public sealed class UiList { - public abstract val id: String +// public abstract val id: String + public abstract val key: MicroBlogKey public abstract val title: String + @Serializable @Immutable public data class List( - override val id: String, + override val key: MicroBlogKey, override val title: String, val description: String? = null, val avatar: String? = null, @@ -17,9 +22,10 @@ public sealed class UiList { val readonly: Boolean = false, ) : UiList() + @Serializable @Immutable public data class Feed( - override val id: String, + override val key: MicroBlogKey, override val title: String, val description: String? = null, val avatar: String? = null, @@ -28,15 +34,17 @@ public sealed class UiList { val liked: Boolean = false, ) : UiList() + @Serializable @Immutable public data class Antenna( - override val id: String, + override val key: MicroBlogKey, override val title: String, ) : UiList() + @Serializable @Immutable public data class Channel( - override val id: String, + override val key: MicroBlogKey, override val title: String, val isArchived: Boolean, val notesCount: Double, diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/model/UiNumber.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/model/UiNumber.kt index 7fd60b850..c0257990c 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/model/UiNumber.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/model/UiNumber.kt @@ -2,10 +2,14 @@ package dev.dimension.flare.ui.model import androidx.compose.runtime.Immutable import dev.dimension.flare.ui.humanizer.Formatter.humanize +import kotlinx.serialization.Serializable +import kotlinx.serialization.Transient +@Serializable @Immutable public data class UiNumber internal constructor( public val value: Long, ) { + @Transient public val humanized: String = value.takeIf { it > 0 }?.humanize().orEmpty() } diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/home/HomeTimelinePresenter.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/home/HomeTimelinePresenter.kt index f00016954..93cf941e9 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/home/HomeTimelinePresenter.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/home/HomeTimelinePresenter.kt @@ -1,6 +1,6 @@ package dev.dimension.flare.ui.presenter.home -import dev.dimension.flare.common.BaseTimelineLoader +import dev.dimension.flare.data.datasource.microblog.paging.BaseTimelineLoader import dev.dimension.flare.data.repository.AccountRepository import dev.dimension.flare.data.repository.accountServiceFlow import dev.dimension.flare.model.AccountType diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/home/MixedTimelinePresenter.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/home/MixedTimelinePresenter.kt index 3bf330414..4ca072041 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/home/MixedTimelinePresenter.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/home/MixedTimelinePresenter.kt @@ -1,9 +1,9 @@ package dev.dimension.flare.ui.presenter.home -import dev.dimension.flare.common.BaseTimelineLoader -import dev.dimension.flare.common.BaseTimelineRemoteMediator import dev.dimension.flare.data.database.cache.CacheDatabase import dev.dimension.flare.data.datasource.microblog.MixedRemoteMediator +import dev.dimension.flare.data.datasource.microblog.paging.BaseTimelineLoader +import dev.dimension.flare.data.datasource.microblog.paging.BaseTimelineRemoteMediator import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine import org.koin.core.component.KoinComponent diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/home/SearchStatusTimelinePresenter.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/home/SearchStatusTimelinePresenter.kt index 5e9354bb6..692d4d8a7 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/home/SearchStatusTimelinePresenter.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/home/SearchStatusTimelinePresenter.kt @@ -1,6 +1,6 @@ package dev.dimension.flare.ui.presenter.home -import dev.dimension.flare.common.BaseTimelineLoader +import dev.dimension.flare.data.datasource.microblog.paging.BaseTimelineLoader import dev.dimension.flare.data.repository.AccountRepository import dev.dimension.flare.data.repository.accountServiceFlow import dev.dimension.flare.model.AccountType diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/home/TimelinePresenter.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/home/TimelinePresenter.kt index 760ccb45f..6e5c1dca8 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/home/TimelinePresenter.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/home/TimelinePresenter.kt @@ -10,9 +10,6 @@ import androidx.paging.PagingData import androidx.paging.cachedIn import androidx.paging.filter import androidx.paging.map -import dev.dimension.flare.common.BaseTimelineLoader -import dev.dimension.flare.common.BaseTimelinePagingSourceFactory -import dev.dimension.flare.common.BaseTimelineRemoteMediator import dev.dimension.flare.common.PagingState import dev.dimension.flare.common.cachePagingState import dev.dimension.flare.common.emptyFlow @@ -21,6 +18,9 @@ import dev.dimension.flare.common.onError import dev.dimension.flare.common.onSuccess import dev.dimension.flare.data.database.cache.CacheDatabase import dev.dimension.flare.data.datasource.microblog.contains +import dev.dimension.flare.data.datasource.microblog.paging.BaseTimelineLoader +import dev.dimension.flare.data.datasource.microblog.paging.BaseTimelinePagingSourceFactory +import dev.dimension.flare.data.datasource.microblog.paging.BaseTimelineRemoteMediator import dev.dimension.flare.data.datasource.microblog.pagingConfig import dev.dimension.flare.data.repository.AccountRepository import dev.dimension.flare.data.repository.LocalFilterRepository diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/home/bluesky/BlueskyBookmarkTimelinePresenter.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/home/bluesky/BlueskyBookmarkTimelinePresenter.kt index 2dda67b5b..0469e671a 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/home/bluesky/BlueskyBookmarkTimelinePresenter.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/home/bluesky/BlueskyBookmarkTimelinePresenter.kt @@ -1,7 +1,7 @@ package dev.dimension.flare.ui.presenter.home.bluesky -import dev.dimension.flare.common.BaseTimelineLoader import dev.dimension.flare.data.datasource.bluesky.BlueskyDataSource +import dev.dimension.flare.data.datasource.microblog.paging.BaseTimelineLoader import dev.dimension.flare.data.repository.AccountRepository import dev.dimension.flare.data.repository.accountServiceFlow import dev.dimension.flare.model.AccountType diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/home/bluesky/BlueskyFeedTimelinePresenter.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/home/bluesky/BlueskyFeedTimelinePresenter.kt index 2705c22ac..a9e1f72b0 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/home/bluesky/BlueskyFeedTimelinePresenter.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/home/bluesky/BlueskyFeedTimelinePresenter.kt @@ -1,7 +1,7 @@ package dev.dimension.flare.ui.presenter.home.bluesky -import dev.dimension.flare.common.BaseTimelineLoader import dev.dimension.flare.data.datasource.bluesky.BlueskyDataSource +import dev.dimension.flare.data.datasource.microblog.paging.BaseTimelineLoader import dev.dimension.flare.data.repository.AccountRepository import dev.dimension.flare.data.repository.accountServiceFlow import dev.dimension.flare.model.AccountType diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/home/mastodon/MastodonBookmarkTimelinePresenter.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/home/mastodon/MastodonBookmarkTimelinePresenter.kt index 73f7218a1..d5240fc34 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/home/mastodon/MastodonBookmarkTimelinePresenter.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/home/mastodon/MastodonBookmarkTimelinePresenter.kt @@ -1,7 +1,7 @@ package dev.dimension.flare.ui.presenter.home.mastodon -import dev.dimension.flare.common.BaseTimelineLoader import dev.dimension.flare.data.datasource.mastodon.MastodonDataSource +import dev.dimension.flare.data.datasource.microblog.paging.BaseTimelineLoader import dev.dimension.flare.data.repository.AccountRepository import dev.dimension.flare.data.repository.accountServiceFlow import dev.dimension.flare.model.AccountType diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/home/mastodon/MastodonFavouriteTimelinePresenter.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/home/mastodon/MastodonFavouriteTimelinePresenter.kt index dfd5621aa..76445ec03 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/home/mastodon/MastodonFavouriteTimelinePresenter.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/home/mastodon/MastodonFavouriteTimelinePresenter.kt @@ -1,7 +1,7 @@ package dev.dimension.flare.ui.presenter.home.mastodon -import dev.dimension.flare.common.BaseTimelineLoader import dev.dimension.flare.data.datasource.mastodon.MastodonDataSource +import dev.dimension.flare.data.datasource.microblog.paging.BaseTimelineLoader import dev.dimension.flare.data.repository.AccountRepository import dev.dimension.flare.data.repository.accountServiceFlow import dev.dimension.flare.model.AccountType diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/home/mastodon/MastodonLocalTimelinePresenter.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/home/mastodon/MastodonLocalTimelinePresenter.kt index 5e827e2eb..a7d7a52ac 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/home/mastodon/MastodonLocalTimelinePresenter.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/home/mastodon/MastodonLocalTimelinePresenter.kt @@ -1,7 +1,7 @@ package dev.dimension.flare.ui.presenter.home.mastodon -import dev.dimension.flare.common.BaseTimelineLoader import dev.dimension.flare.data.datasource.mastodon.MastodonDataSource +import dev.dimension.flare.data.datasource.microblog.paging.BaseTimelineLoader import dev.dimension.flare.data.repository.AccountRepository import dev.dimension.flare.data.repository.accountServiceFlow import dev.dimension.flare.model.AccountType diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/home/mastodon/MastodonPublicTimelinePresenter.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/home/mastodon/MastodonPublicTimelinePresenter.kt index b9e839f64..406d35b51 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/home/mastodon/MastodonPublicTimelinePresenter.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/home/mastodon/MastodonPublicTimelinePresenter.kt @@ -1,7 +1,7 @@ package dev.dimension.flare.ui.presenter.home.mastodon -import dev.dimension.flare.common.BaseTimelineLoader import dev.dimension.flare.data.datasource.mastodon.MastodonDataSource +import dev.dimension.flare.data.datasource.microblog.paging.BaseTimelineLoader import dev.dimension.flare.data.repository.AccountRepository import dev.dimension.flare.data.repository.accountServiceFlow import dev.dimension.flare.model.AccountType diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/home/misskey/MissKeyLocalTimelinePresenter.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/home/misskey/MissKeyLocalTimelinePresenter.kt index 532bcf7af..6457ab242 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/home/misskey/MissKeyLocalTimelinePresenter.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/home/misskey/MissKeyLocalTimelinePresenter.kt @@ -1,6 +1,6 @@ package dev.dimension.flare.ui.presenter.home.misskey -import dev.dimension.flare.common.BaseTimelineLoader +import dev.dimension.flare.data.datasource.microblog.paging.BaseTimelineLoader import dev.dimension.flare.data.datasource.misskey.MisskeyDataSource import dev.dimension.flare.data.repository.AccountRepository import dev.dimension.flare.data.repository.accountServiceFlow diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/home/misskey/MissKeyPublicTimelinePresenter.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/home/misskey/MissKeyPublicTimelinePresenter.kt index 1a3e0723b..27e1538c4 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/home/misskey/MissKeyPublicTimelinePresenter.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/home/misskey/MissKeyPublicTimelinePresenter.kt @@ -1,6 +1,6 @@ package dev.dimension.flare.ui.presenter.home.misskey -import dev.dimension.flare.common.BaseTimelineLoader +import dev.dimension.flare.data.datasource.microblog.paging.BaseTimelineLoader import dev.dimension.flare.data.datasource.misskey.MisskeyDataSource import dev.dimension.flare.data.repository.AccountRepository import dev.dimension.flare.data.repository.accountServiceFlow diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/home/misskey/MisskeyFavouriteTimelinePresenter.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/home/misskey/MisskeyFavouriteTimelinePresenter.kt index 400fb30f7..d17697092 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/home/misskey/MisskeyFavouriteTimelinePresenter.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/home/misskey/MisskeyFavouriteTimelinePresenter.kt @@ -1,6 +1,6 @@ package dev.dimension.flare.ui.presenter.home.misskey -import dev.dimension.flare.common.BaseTimelineLoader +import dev.dimension.flare.data.datasource.microblog.paging.BaseTimelineLoader import dev.dimension.flare.data.datasource.misskey.MisskeyDataSource import dev.dimension.flare.data.repository.AccountRepository import dev.dimension.flare.data.repository.accountServiceFlow diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/home/misskey/MisskeyHybridTimelinePresenter.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/home/misskey/MisskeyHybridTimelinePresenter.kt index 5878858c6..50e0e30c5 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/home/misskey/MisskeyHybridTimelinePresenter.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/home/misskey/MisskeyHybridTimelinePresenter.kt @@ -1,6 +1,6 @@ package dev.dimension.flare.ui.presenter.home.misskey -import dev.dimension.flare.common.BaseTimelineLoader +import dev.dimension.flare.data.datasource.microblog.paging.BaseTimelineLoader import dev.dimension.flare.data.datasource.misskey.MisskeyDataSource import dev.dimension.flare.data.repository.AccountRepository import dev.dimension.flare.data.repository.accountServiceFlow diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/home/rss/RssTimelinePresenter.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/home/rss/RssTimelinePresenter.kt index 1a4130f5a..c8998e2e5 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/home/rss/RssTimelinePresenter.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/home/rss/RssTimelinePresenter.kt @@ -3,10 +3,10 @@ package dev.dimension.flare.ui.presenter.home.rss import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.remember -import dev.dimension.flare.common.BaseTimelineLoader import dev.dimension.flare.data.database.app.AppDatabase import dev.dimension.flare.data.database.cache.CacheDatabase import dev.dimension.flare.data.datasource.microblog.MixedRemoteMediator +import dev.dimension.flare.data.datasource.microblog.paging.BaseTimelineLoader import dev.dimension.flare.data.datasource.rss.RssDataSource import dev.dimension.flare.ui.model.UiRssSource import dev.dimension.flare.ui.model.UiState diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/home/vvo/VVOFavouriteTimelinePresenter.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/home/vvo/VVOFavouriteTimelinePresenter.kt index 4b86ca2fa..aefa5e8cf 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/home/vvo/VVOFavouriteTimelinePresenter.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/home/vvo/VVOFavouriteTimelinePresenter.kt @@ -1,6 +1,6 @@ package dev.dimension.flare.ui.presenter.home.vvo -import dev.dimension.flare.common.BaseTimelineLoader +import dev.dimension.flare.data.datasource.microblog.paging.BaseTimelineLoader import dev.dimension.flare.data.datasource.vvo.VVODataSource import dev.dimension.flare.data.repository.AccountRepository import dev.dimension.flare.data.repository.accountServiceFlow diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/home/vvo/VVOLikeTimelinePresenter.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/home/vvo/VVOLikeTimelinePresenter.kt index d095d10d1..29123a8dc 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/home/vvo/VVOLikeTimelinePresenter.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/home/vvo/VVOLikeTimelinePresenter.kt @@ -1,6 +1,6 @@ package dev.dimension.flare.ui.presenter.home.vvo -import dev.dimension.flare.common.BaseTimelineLoader +import dev.dimension.flare.data.datasource.microblog.paging.BaseTimelineLoader import dev.dimension.flare.data.datasource.vvo.VVODataSource import dev.dimension.flare.data.repository.AccountRepository import dev.dimension.flare.data.repository.accountServiceFlow diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/home/xqt/XQTBookmarkTimelinePresenter.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/home/xqt/XQTBookmarkTimelinePresenter.kt index f0a653448..ea74eda26 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/home/xqt/XQTBookmarkTimelinePresenter.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/home/xqt/XQTBookmarkTimelinePresenter.kt @@ -1,6 +1,6 @@ package dev.dimension.flare.ui.presenter.home.xqt -import dev.dimension.flare.common.BaseTimelineLoader +import dev.dimension.flare.data.datasource.microblog.paging.BaseTimelineLoader import dev.dimension.flare.data.datasource.xqt.XQTDataSource import dev.dimension.flare.data.repository.AccountRepository import dev.dimension.flare.data.repository.accountServiceFlow diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/home/xqt/XQTDeviceFollowTimelinePresenter.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/home/xqt/XQTDeviceFollowTimelinePresenter.kt index 9b0804baf..3a5554f80 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/home/xqt/XQTDeviceFollowTimelinePresenter.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/home/xqt/XQTDeviceFollowTimelinePresenter.kt @@ -1,6 +1,6 @@ package dev.dimension.flare.ui.presenter.home.xqt -import dev.dimension.flare.common.BaseTimelineLoader +import dev.dimension.flare.data.datasource.microblog.paging.BaseTimelineLoader import dev.dimension.flare.data.datasource.xqt.XQTDataSource import dev.dimension.flare.data.repository.AccountRepository import dev.dimension.flare.data.repository.accountServiceFlow diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/home/xqt/XQTFeaturedTimelinePresenter.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/home/xqt/XQTFeaturedTimelinePresenter.kt index 47ad92b02..710a3ec02 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/home/xqt/XQTFeaturedTimelinePresenter.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/home/xqt/XQTFeaturedTimelinePresenter.kt @@ -1,6 +1,6 @@ package dev.dimension.flare.ui.presenter.home.xqt -import dev.dimension.flare.common.BaseTimelineLoader +import dev.dimension.flare.data.datasource.microblog.paging.BaseTimelineLoader import dev.dimension.flare.data.datasource.xqt.XQTDataSource import dev.dimension.flare.data.repository.AccountRepository import dev.dimension.flare.data.repository.accountServiceFlow diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/list/AntennasTimelinePresenter.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/list/AntennasTimelinePresenter.kt index 0b7719d5f..5962c897c 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/list/AntennasTimelinePresenter.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/list/AntennasTimelinePresenter.kt @@ -1,6 +1,6 @@ package dev.dimension.flare.ui.presenter.list -import dev.dimension.flare.common.BaseTimelineLoader +import dev.dimension.flare.data.datasource.microblog.paging.BaseTimelineLoader import dev.dimension.flare.data.datasource.misskey.MisskeyDataSource import dev.dimension.flare.data.repository.AccountRepository import dev.dimension.flare.data.repository.accountServiceFlow diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/list/ChannelTimelinePresenter.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/list/ChannelTimelinePresenter.kt new file mode 100644 index 000000000..687ccbec6 --- /dev/null +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/list/ChannelTimelinePresenter.kt @@ -0,0 +1,30 @@ +package dev.dimension.flare.ui.presenter.list + +import dev.dimension.flare.data.datasource.microblog.paging.BaseTimelineLoader +import dev.dimension.flare.data.datasource.misskey.MisskeyDataSource +import dev.dimension.flare.data.repository.AccountRepository +import dev.dimension.flare.data.repository.accountServiceFlow +import dev.dimension.flare.model.AccountType +import dev.dimension.flare.ui.presenter.home.TimelinePresenter +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map +import org.koin.core.component.KoinComponent +import org.koin.core.component.inject + +public class ChannelTimelinePresenter( + private val accountType: AccountType, + private val id: String, +) : TimelinePresenter(), + KoinComponent { + private val accountRepository: AccountRepository by inject() + + override val loader: Flow by lazy { + accountServiceFlow( + accountType = accountType, + repository = accountRepository, + ).map { service -> + require(service is MisskeyDataSource) + service.channelTimelineLoader(id) + } + } +} diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/list/ListInfoPresenter.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/list/ListInfoPresenter.kt index 6b448f811..91064dfd1 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/list/ListInfoPresenter.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/list/ListInfoPresenter.kt @@ -5,6 +5,7 @@ import androidx.compose.runtime.Immutable import androidx.compose.runtime.remember import dev.dimension.flare.common.collectAsState import dev.dimension.flare.data.datasource.microblog.ListDataSource +import dev.dimension.flare.data.datasource.microblog.list.ListDataSource import dev.dimension.flare.data.repository.AccountRepository import dev.dimension.flare.data.repository.accountServiceProvider import dev.dimension.flare.model.AccountType diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/list/ListTimelinePresenter.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/list/ListTimelinePresenter.kt index 49477d4d9..bc4b36a63 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/list/ListTimelinePresenter.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/list/ListTimelinePresenter.kt @@ -1,7 +1,7 @@ package dev.dimension.flare.ui.presenter.list -import dev.dimension.flare.common.BaseTimelineLoader import dev.dimension.flare.data.datasource.microblog.ListDataSource +import dev.dimension.flare.data.datasource.microblog.paging.BaseTimelineLoader import dev.dimension.flare.data.repository.AccountRepository import dev.dimension.flare.data.repository.accountServiceFlow import dev.dimension.flare.model.AccountType diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/list/PinnableTimelineTabPresenter.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/list/PinnableTimelineTabPresenter.kt index df485393e..8f981e562 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/list/PinnableTimelineTabPresenter.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/list/PinnableTimelineTabPresenter.kt @@ -49,6 +49,10 @@ public class PinnableTimelineTabPresenter( public data class Antenna( override val data: PagingState, ) : Tab + + public data class Channel( + override val data: PagingState, + ) : Tab } public val tabs: UiState> @@ -90,6 +94,16 @@ public class PinnableTimelineTabPresenter( }.collectAsLazyPagingItems() }.toPagingState() + val channel = + serviceState + .mapNotNull { + it as? MisskeyDataSource + }.mapNotNull { service -> + remember(service) { + service.channelsList(scope = scope) + }.collectAsLazyPagingItems() + }.toPagingState() + val tabs = serviceState.map { service -> remember( @@ -113,6 +127,11 @@ public class PinnableTimelineTabPresenter( } else { null }, + if (service is MisskeyDataSource) { + State.Tab.Channel(channel) + } else { + null + }, ).toImmutableList().toImmutableListWrapper() } } diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/profile/ProfileMediaPresenter.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/profile/ProfileMediaPresenter.kt index 1131a2619..ee5720f40 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/profile/ProfileMediaPresenter.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/profile/ProfileMediaPresenter.kt @@ -7,10 +7,10 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.paging.PagingData import androidx.paging.compose.collectAsLazyPagingItems import androidx.paging.flatMap -import dev.dimension.flare.common.BaseTimelineLoader import dev.dimension.flare.common.PagingState import dev.dimension.flare.common.toPagingState import dev.dimension.flare.data.datasource.microblog.AuthenticatedMicroblogDataSource +import dev.dimension.flare.data.datasource.microblog.paging.BaseTimelineLoader import dev.dimension.flare.data.repository.AccountRepository import dev.dimension.flare.data.repository.NoActiveAccountException import dev.dimension.flare.data.repository.accountServiceFlow diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/profile/ProfilePresenter.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/profile/ProfilePresenter.kt index f412e19ec..51a217e04 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/profile/ProfilePresenter.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/profile/ProfilePresenter.kt @@ -5,7 +5,6 @@ import androidx.compose.runtime.Immutable import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope -import dev.dimension.flare.common.BaseTimelineLoader import dev.dimension.flare.common.collectAsState import dev.dimension.flare.data.datasource.microblog.ActionMenu import dev.dimension.flare.data.datasource.microblog.AuthenticatedMicroblogDataSource @@ -13,6 +12,7 @@ import dev.dimension.flare.data.datasource.microblog.DirectMessageDataSource import dev.dimension.flare.data.datasource.microblog.ListDataSource import dev.dimension.flare.data.datasource.microblog.ProfileAction import dev.dimension.flare.data.datasource.microblog.ProfileTab +import dev.dimension.flare.data.datasource.microblog.paging.BaseTimelineLoader import dev.dimension.flare.data.repository.AccountRepository import dev.dimension.flare.data.repository.NoActiveAccountException import dev.dimension.flare.data.repository.accountServiceFlow diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/status/StatusContextPresenter.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/status/StatusContextPresenter.kt index 9621da8ad..06da54523 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/status/StatusContextPresenter.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/status/StatusContextPresenter.kt @@ -4,7 +4,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.Immutable import androidx.compose.runtime.getValue import androidx.compose.runtime.remember -import dev.dimension.flare.common.BaseTimelineLoader +import dev.dimension.flare.data.datasource.microblog.paging.BaseTimelineLoader import dev.dimension.flare.data.repository.AccountRepository import dev.dimension.flare.data.repository.accountServiceFlow import dev.dimension.flare.model.AccountType diff --git a/shared/src/commonTest/kotlin/dev/dimension/flare/data/datasource/rss/RssTimelineRemoteMediatorTest.kt b/shared/src/commonTest/kotlin/dev/dimension/flare/data/datasource/rss/RssTimelineRemoteMediatorTest.kt index 103cf1619..6eabcefeb 100644 --- a/shared/src/commonTest/kotlin/dev/dimension/flare/data/datasource/rss/RssTimelineRemoteMediatorTest.kt +++ b/shared/src/commonTest/kotlin/dev/dimension/flare/data/datasource/rss/RssTimelineRemoteMediatorTest.kt @@ -3,8 +3,8 @@ package dev.dimension.flare.data.datasource.rss import androidx.room.Room import androidx.sqlite.driver.bundled.BundledSQLiteDriver import dev.dimension.flare.RobolectricTest -import dev.dimension.flare.common.BaseTimelineRemoteMediator import dev.dimension.flare.data.database.cache.CacheDatabase +import dev.dimension.flare.data.datasource.microblog.paging.BaseTimelineRemoteMediator import dev.dimension.flare.data.network.rss.model.Feed import dev.dimension.flare.memoryDatabaseBuilder import dev.dimension.flare.ui.model.mapper.parseRssDateToInstant From a21c3d0a56a39cedbdf2bcc780c30427665aee0b Mon Sep 17 00:00:00 2001 From: Tlaster Date: Fri, 13 Feb 2026 18:46:26 +0900 Subject: [PATCH 03/14] [WIP] migrate list to loader --- .../flare/data/database/cache/dao/ListDao.kt | 4 + .../data/database/cache/mapper/Bluesky.kt | 6 +- .../data/database/cache/mapper/Misskey.kt | 2 +- .../datasource/bluesky/BlueskyDataSource.kt | 481 +----------------- .../datasource/bluesky/BlueskyListLoader.kt | 212 ++++++++ .../bluesky/BlueskyListMemberLoader.kt | 187 +++++++ .../datasource/mastodon/MastodonDataSource.kt | 26 +- .../microblog/MixedRemoteMediator.kt | 36 +- .../microblog/list/ListDataSource.kt | 7 +- .../datasource/microblog/list/ListHandler.kt | 7 +- .../microblog/list/ListMemberHandler.kt | 13 +- .../datasource/misskey/MisskeyDataSource.kt | 316 ++---------- .../datasource/misskey/MisskeyListLoader.kt | 91 ++++ .../misskey/MisskeyListMemberLoader.kt | 113 ++++ .../data/datasource/xqt/XQTDataSource.kt | 392 +------------- .../data/datasource/xqt/XQTListLoader.kt | 140 +++++ .../datasource/xqt/XQTListMemberLoader.kt | 126 +++++ .../dev/dimension/flare/ui/model/UiList.kt | 6 +- .../ui/presenter/list/AllListPresenter.kt | 7 +- .../ui/presenter/list/CreateListPresenter.kt | 10 +- .../ui/presenter/list/DeleteListPresenter.kt | 8 +- .../list/EditAccountListPresenter.kt | 31 +- .../presenter/list/EditListMemberPresenter.kt | 17 +- .../ui/presenter/list/ListEditPresenter.kt | 25 +- .../ui/presenter/list/ListInfoPresenter.kt | 8 +- .../ui/presenter/list/ListMembersPresenter.kt | 14 +- .../presenter/list/ListTimelinePresenter.kt | 7 +- .../list/PinnableTimelineTabPresenter.kt | 4 +- 28 files changed, 1095 insertions(+), 1201 deletions(-) create mode 100644 shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/bluesky/BlueskyListLoader.kt create mode 100644 shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/bluesky/BlueskyListMemberLoader.kt create mode 100644 shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/misskey/MisskeyListLoader.kt create mode 100644 shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/misskey/MisskeyListMemberLoader.kt create mode 100644 shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/xqt/XQTListLoader.kt create mode 100644 shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/xqt/XQTListMemberLoader.kt diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/database/cache/dao/ListDao.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/database/cache/dao/ListDao.kt index 91b2ec720..a2f5d0338 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/data/database/cache/dao/ListDao.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/data/database/cache/dao/ListDao.kt @@ -71,6 +71,10 @@ internal interface ListDao { @Query("SELECT * FROM DbListMember WHERE listKey = :listKey") fun getListMembers(listKey: MicroBlogKey): PagingSource + @Transaction + @Query("SELECT * FROM DbListMember WHERE listKey = :listKey") + fun getListMembersFlow(listKey: MicroBlogKey): Flow> + @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun insertAll(members: List) diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/database/cache/mapper/Bluesky.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/database/cache/mapper/Bluesky.kt index 220faf50d..468cda920 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/data/database/cache/mapper/Bluesky.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/data/database/cache/mapper/Bluesky.kt @@ -688,7 +688,7 @@ private fun PostView.toDbStatusWithUser(accountKey: MicroBlogKey): DbStatusWithU ) } -private fun ProfileView.toDbUser(host: String) = +internal fun ProfileView.toDbUser(host: String) = DbUser( userKey = MicroBlogKey( @@ -710,7 +710,7 @@ private fun ProfileView.toDbUser(host: String) = ), ) -private fun ProfileViewBasic.toDbUser(host: String) = +internal fun ProfileViewBasic.toDbUser(host: String) = DbUser( userKey = MicroBlogKey( @@ -724,7 +724,7 @@ private fun ProfileViewBasic.toDbUser(host: String) = content = UserContent.BlueskyLite(this), ) -private fun chat.bsky.actor.ProfileViewBasic.toDbUser(host: String) = +internal fun chat.bsky.actor.ProfileViewBasic.toDbUser(host: String) = DbUser( userKey = MicroBlogKey( diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/database/cache/mapper/Misskey.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/database/cache/mapper/Misskey.kt index 6da40888f..e06011326 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/data/database/cache/mapper/Misskey.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/data/database/cache/mapper/Misskey.kt @@ -135,7 +135,7 @@ private fun Note.toDbStatusWithUser( ) } -private fun UserLite.toDbUser(accountHost: String) = +internal fun UserLite.toDbUser(accountHost: String) = DbUser( userKey = MicroBlogKey( diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/bluesky/BlueskyDataSource.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/bluesky/BlueskyDataSource.kt index ea5c618be..f64854624 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/bluesky/BlueskyDataSource.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/bluesky/BlueskyDataSource.kt @@ -1,7 +1,6 @@ package dev.dimension.flare.data.datasource.bluesky import androidx.paging.ExperimentalPagingApi -import androidx.paging.LoadType import androidx.paging.Pager import androidx.paging.PagingData import androidx.paging.PagingState @@ -24,9 +23,6 @@ import app.bsky.feed.Post import app.bsky.feed.PostEmbedUnion import app.bsky.feed.PostReplyRef import app.bsky.feed.ViewerState -import app.bsky.graph.GetListQueryParams -import app.bsky.graph.GetListsQueryParams -import app.bsky.graph.Listitem import app.bsky.graph.MuteActorRequest import app.bsky.graph.UnmuteActorRequest import app.bsky.notification.ListNotificationsQueryParams @@ -49,21 +45,13 @@ import com.atproto.identity.ResolveHandleQueryParams import com.atproto.moderation.CreateReportRequest import com.atproto.moderation.CreateReportRequestSubjectUnion import com.atproto.moderation.Token -import com.atproto.repo.ApplyWritesDelete -import com.atproto.repo.ApplyWritesRequest -import com.atproto.repo.ApplyWritesRequestWriteUnion import com.atproto.repo.CreateRecordRequest import com.atproto.repo.CreateRecordResponse import com.atproto.repo.DeleteRecordRequest -import com.atproto.repo.ListRecordsQueryParams -import com.atproto.repo.ListRecordsRecord -import com.atproto.repo.PutRecordRequest import com.atproto.repo.StrongRef import dev.dimension.flare.common.BasePagingSource -import dev.dimension.flare.common.BaseRemoteMediator import dev.dimension.flare.common.CacheData import dev.dimension.flare.common.Cacheable -import dev.dimension.flare.common.FileItem import dev.dimension.flare.common.FileType import dev.dimension.flare.common.InAppNotification import dev.dimension.flare.common.MemCacheable @@ -82,17 +70,17 @@ import dev.dimension.flare.data.datasource.microblog.ComposeData import dev.dimension.flare.data.datasource.microblog.ComposeProgress import dev.dimension.flare.data.datasource.microblog.ComposeType import dev.dimension.flare.data.datasource.microblog.DirectMessageDataSource -import dev.dimension.flare.data.datasource.microblog.ListDataSource -import dev.dimension.flare.data.datasource.microblog.ListMetaData -import dev.dimension.flare.data.datasource.microblog.ListMetaDataType -import dev.dimension.flare.data.datasource.microblog.MemoryPagingSource import dev.dimension.flare.data.datasource.microblog.NotificationFilter import dev.dimension.flare.data.datasource.microblog.ProfileAction import dev.dimension.flare.data.datasource.microblog.ProfileTab import dev.dimension.flare.data.datasource.microblog.RelationDataSource import dev.dimension.flare.data.datasource.microblog.StatusEvent import dev.dimension.flare.data.datasource.microblog.createSendingDirectMessage -import dev.dimension.flare.data.datasource.microblog.memoryPager +import dev.dimension.flare.data.datasource.microblog.list.ListDataSource +import dev.dimension.flare.data.datasource.microblog.list.ListHandler +import dev.dimension.flare.data.datasource.microblog.list.ListLoader +import dev.dimension.flare.data.datasource.microblog.list.ListMemberHandler +import dev.dimension.flare.data.datasource.microblog.list.ListMemberLoader import dev.dimension.flare.data.datasource.microblog.paging.BaseTimelineLoader import dev.dimension.flare.data.datasource.microblog.pagingConfig import dev.dimension.flare.data.datasource.microblog.relationKeyWithUserKey @@ -1365,461 +1353,46 @@ internal class BlueskyDataSource( } } - private val myListKey = "my_list_$accountKey" - - override fun myList(scope: CoroutineScope): Flow> = - memoryPager( - pageSize = 20, - pagingKey = myListKey, - scope = scope, - mediator = - object : BaseRemoteMediator() { - var cursor: String? = null - - override suspend fun doLoad( - loadType: LoadType, - state: PagingState, - ): MediatorResult { - val result = - service - .getLists( - params = - GetListsQueryParams( - actor = Did(did = accountKey.id), - cursor = cursor, - ), - ).requireResponse() - val items = - result - .lists - .map { - it.render(accountKey) - }.toImmutableList() - cursor = result.cursor - MemoryPagingSource.update( - key = myListKey, - value = items, - ) - return MediatorResult.Success( - endOfPaginationReached = cursor == null, - ) - } - }, - ) - - private fun listInfoKey(uri: String) = "list_info_$uri" - - override fun listInfo(listId: String): CacheData = - MemCacheable( - key = listInfoKey(listId), - ) { - service - .getList( - GetListQueryParams( - list = AtUri(listId), - ), - ).requireResponse() - .list - .render(accountKey) - } - - override fun listTimeline(listId: String) = + override fun listTimeline(listKey: MicroBlogKey) = ListTimelineRemoteMediator( service = service, accountKey = accountKey, database = database, - uri = listId, + uri = listKey.id, ) - private suspend fun createList( - title: String, - description: String?, - icon: FileItem?, - ) { - tryRun { - val iconInfo = - if (icon != null) { - service.uploadBlob(icon.readBytes()).maybeResponse() - } else { - null - } - val record = - app.bsky.graph.List( - purpose = app.bsky.graph.Token.Curatelist, - name = title, - description = description, - avatar = iconInfo?.blob, - createdAt = Clock.System.now(), - ) - service.createRecord( - request = - CreateRecordRequest( - repo = Did(did = accountKey.id), - collection = Nsid("app.bsky.graph.list"), - record = record.bskyJson(), - ), - ) - }.onSuccess { - val uri = it.requireResponse().uri - service - .getList( - params = - GetListQueryParams( - list = uri, - ), - ).requireResponse() - .list - .render(accountKey) - .let { list -> - MemoryPagingSource.updateWith( - key = myListKey, - ) { - (listOf(list) + it).toImmutableList() - } - } - } - } - - override suspend fun deleteList(listId: String) { - tryRun { - val id = listId.substringAfterLast('/') - service.applyWrites( - request = - ApplyWritesRequest( - repo = Did(did = accountKey.id), - writes = - persistentListOf( - ApplyWritesRequestWriteUnion.Delete( - value = - ApplyWritesDelete( - collection = Nsid("app.bsky.graph.list"), - rkey = RKey(id), - ), - ), - ), - ), - ) - }.onSuccess { - MemoryPagingSource.updateWith( - key = myListKey, - ) { - it.filterNot { item -> item.id == listId }.toImmutableList() - } - } - } - - private suspend fun updateList( - uri: String, - title: String, - description: String?, - icon: FileItem?, - ) { - tryRun { - val currentInfo: app.bsky.graph.List = - service - .getRecord( - params = - com.atproto.repo.GetRecordQueryParams( - collection = Nsid("app.bsky.graph.list"), - repo = Did(did = accountKey.id), - rkey = RKey(uri.substringAfterLast('/')), - ), - ).requireResponse() - .value - .decodeAs() - - val iconInfo = - if (icon != null) { - service.uploadBlob(icon.readBytes()).maybeResponse() - } else { - null - } - val newRecord = - currentInfo - .copy( - name = title, - description = description, - ).let { - if (iconInfo != null) { - it.copy(avatar = iconInfo.blob) - } else { - it - } - } - service.putRecord( - request = - PutRecordRequest( - repo = Did(did = accountKey.id), - collection = Nsid("app.bsky.graph.list"), - rkey = RKey(uri.substringAfterLast('/')), - record = newRecord.bskyJson(), - ), - ) - }.onSuccess { - MemoryPagingSource.updateWith( - key = myListKey, - ) { - it - .map { item -> - if (item.id == uri) { - item.copy( - title = title, - description = description, - ) - } else { - item - } - }.toImmutableList() - } - } - } - - private fun listMemberKey(listId: String) = "listMembers_$listId" - - override fun listMembers( - listId: String, - scope: CoroutineScope, - pageSize: Int, - ): Flow> = - memoryPager( - pageSize = pageSize, - pagingKey = listMemberKey(listId), - scope = scope, - mediator = - object : BaseRemoteMediator() { - private var cursor: String? = null - - override suspend fun doLoad( - loadType: LoadType, - state: PagingState, - ): MediatorResult { - if (loadType == LoadType.PREPEND) { - return MediatorResult.Success(endOfPaginationReached = true) - } - if (loadType == LoadType.REFRESH) { - cursor = null - } - val response = - service - .getList( - params = - GetListQueryParams( - list = AtUri(listId), - cursor = cursor, - limit = state.config.pageSize.toLong(), - ), - ).maybeResponse() - cursor = response?.cursor - val result = - response - ?.items - ?.map { - it.subject.render(accountKey) - } ?: emptyList() - - if (loadType == LoadType.REFRESH) { - MemoryPagingSource.update( - key = listMemberKey(listId), - value = result.toImmutableList(), - ) - } else if (loadType == LoadType.APPEND) { - MemoryPagingSource.append( - key = listMemberKey(listId), - value = result.toImmutableList(), - ) - } + private val myListKey = "my_list_$accountKey" - return MediatorResult.Success( - endOfPaginationReached = cursor == null, - ) - } - }, + val listLoader: ListLoader by lazy { + BlueskyListLoader( + service = service, + accountKey = accountKey, ) - - override suspend fun addMember( - listId: String, - userKey: MicroBlogKey, - ) { - tryRun { - val user = - service - .getProfile(GetProfileQueryParams(actor = Did(did = userKey.id))) - .requireResponse() - .render(accountKey) - - MemoryPagingSource.updateWith( - key = listMemberKey(listId), - ) { - (listOf(user) + it) - .distinctBy { - it.key - }.toImmutableList() - } - val list = - service - .getList( - params = - GetListQueryParams( - list = AtUri(listId), - ), - ).requireResponse() - .list - .render(accountKey) - MemCacheable.updateWith>(userListsKey(userKey)) { - (it + list).toImmutableList() - } - service.createRecord( - CreateRecordRequest( - repo = Did(did = accountKey.id), - collection = Nsid("app.bsky.graph.listitem"), - record = - app.bsky.graph - .Listitem( - list = AtUri(listId), - subject = Did(userKey.id), - createdAt = Clock.System.now(), - ).bskyJson(), - ), - ) - } } - override suspend fun removeMember( - listId: String, - userKey: MicroBlogKey, - ) { - tryRun { - MemoryPagingSource.updateWith( - key = listMemberKey(listId), - ) { - it - .filter { user -> user.key.id != userKey.id } - .toImmutableList() - } - MemCacheable.updateWith>(userListsKey(userKey)) { - it - .filter { list -> list.id != listId } - .toImmutableList() - } - var record: ListRecordsRecord? = null - var cursor: String? = null - while (record == null) { - val response = - service - .listRecords( - params = - ListRecordsQueryParams( - repo = Did(did = accountKey.id), - collection = Nsid("app.bsky.graph.listitem"), - limit = 100, - cursor = cursor, - ), - ).requireResponse() - if (response.cursor == null || response.records.isEmpty()) { - break - } - cursor = response.cursor - record = - response.records - .firstOrNull { - val item: app.bsky.graph.Listitem = it.value.decodeAs() - item.list.atUri == listId && item.subject.did == userKey.id - } - } - if (record != null) { - service.deleteRecord( - DeleteRecordRequest( - repo = Did(did = accountKey.id), - collection = Nsid("app.bsky.graph.listitem"), - rkey = RKey(record.uri.atUri.substringAfterLast('/')), - ), - ) - } - } + val listMemberLoader: ListMemberLoader by lazy { + BlueskyListMemberLoader( + service = service, + accountKey = accountKey, + ) } - override val supportedMetaData: ImmutableList - get() = - persistentListOf( - ListMetaDataType.TITLE, - ListMetaDataType.DESCRIPTION, - ListMetaDataType.AVATAR, - ) - - override suspend fun updateList( - listId: String, - metaData: ListMetaData, - ) { - updateList( - uri = listId, - title = metaData.title, - description = metaData.description, - icon = metaData.avatar, + override val listHandler: ListHandler by lazy { + ListHandler( + pagingKey = myListKey, + accountKey = accountKey, + loader = listLoader, ) } - override suspend fun createList(metaData: ListMetaData) { - createList( - title = metaData.title, - description = metaData.description, - icon = metaData.avatar, + override val listMemberHandler: ListMemberHandler by lazy { + ListMemberHandler( + pagingKey = "list_members_$accountKey", + accountKey = accountKey, + loader = listMemberLoader, ) } - override fun listMemberCache(listId: String): Flow> = - MemoryPagingSource.getFlow(listMemberKey(listId)) - - private fun userListsKey(userKey: MicroBlogKey) = "userLists_${userKey.id}" - - override fun userLists(userKey: MicroBlogKey): MemCacheable> = - MemCacheable( - key = userListsKey(userKey), - ) { - var cursor: String? = null - val lists = mutableListOf() - val allLists = - service - .getLists( - params = - GetListsQueryParams( - actor = Did(did = accountKey.id), - limit = 100, - ), - ).requireResponse() - .lists - .map { - it.render(accountKey) - } - while (true) { - val response = - service - .listRecords( - params = - ListRecordsQueryParams( - repo = Did(did = accountKey.id), - collection = Nsid("app.bsky.graph.listitem"), - limit = 100, - cursor = cursor, - ), - ).requireResponse() - lists.addAll( - response.records - .filter { - val item: Listitem = it.value.decodeAs() - item.subject.did == userKey.id - }.mapNotNull { - val item: Listitem = it.value.decodeAs() - allLists.firstOrNull { it.id == item.list.atUri } - }, - ) - cursor = response.cursor - if (cursor == null) { - break - } - } - lists.toImmutableList() - } - private val notificationMarkerKey: String get() = "notificationBadgeCount_$accountKey" diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/bluesky/BlueskyListLoader.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/bluesky/BlueskyListLoader.kt new file mode 100644 index 000000000..431851378 --- /dev/null +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/bluesky/BlueskyListLoader.kt @@ -0,0 +1,212 @@ +package dev.dimension.flare.data.datasource.bluesky + +import app.bsky.graph.GetListQueryParams +import app.bsky.graph.GetListsQueryParams +import com.atproto.repo.ApplyWritesDelete +import com.atproto.repo.ApplyWritesRequest +import com.atproto.repo.ApplyWritesRequestWriteUnion +import com.atproto.repo.CreateRecordRequest +import com.atproto.repo.PutRecordRequest +import dev.dimension.flare.common.FileItem +import dev.dimension.flare.data.datasource.microblog.list.ListLoader +import dev.dimension.flare.data.datasource.microblog.list.ListMetaData +import dev.dimension.flare.data.datasource.microblog.list.ListMetaDataType +import dev.dimension.flare.data.datasource.microblog.paging.PagingRequest +import dev.dimension.flare.data.datasource.microblog.paging.PagingResult +import dev.dimension.flare.data.network.bluesky.BlueskyService +import dev.dimension.flare.model.MicroBlogKey +import dev.dimension.flare.ui.model.UiList +import dev.dimension.flare.ui.model.mapper.render +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toImmutableList +import sh.christian.ozone.api.AtUri +import sh.christian.ozone.api.Did +import sh.christian.ozone.api.Nsid +import sh.christian.ozone.api.RKey +import kotlin.time.Clock + +internal class BlueskyListLoader( + private val service: BlueskyService, + private val accountKey: MicroBlogKey, +) : ListLoader { + override suspend fun load( + pageSize: Int, + request: PagingRequest, + ): PagingResult { + val cursor = + when (request) { + is PagingRequest.Append -> request.nextKey + is PagingRequest.Refresh -> null + is PagingRequest.Prepend -> return PagingResult() + } + val result = + service + .getLists( + params = + GetListsQueryParams( + actor = Did(did = accountKey.id), + cursor = cursor, + limit = pageSize.toLong(), + ), + ).requireResponse() + val items = + result + .lists + .map { + it.render(accountKey) + }.toImmutableList() + return PagingResult( + data = items, + nextKey = result.cursor, + ) + } + + override suspend fun info(listKey: MicroBlogKey): UiList = + service + .getList( + GetListQueryParams( + list = AtUri(listKey.id), + ), + ).requireResponse() + .list + .render(accountKey) + + override val supportedMetaData: ImmutableList + get() = + persistentListOf( + ListMetaDataType.TITLE, + ListMetaDataType.DESCRIPTION, + ListMetaDataType.AVATAR, + ) + + override suspend fun create(metaData: ListMetaData): UiList = + createList( + title = metaData.title, + description = metaData.description, + icon = metaData.avatar, + ) + + private suspend fun createList( + title: String, + description: String?, + icon: FileItem?, + ): UiList { + val iconInfo = + if (icon != null) { + service.uploadBlob(icon.readBytes()).maybeResponse() + } else { + null + } + val record = + app.bsky.graph.List( + purpose = app.bsky.graph.Token.Curatelist, + name = title, + description = description, + avatar = iconInfo?.blob, + createdAt = Clock.System.now(), + ) + val response = + service + .createRecord( + request = + CreateRecordRequest( + repo = Did(did = accountKey.id), + collection = Nsid("app.bsky.graph.list"), + record = record.bskyJson(), + ), + ).requireResponse() + + val uri = response.uri + return service + .getList( + params = + GetListQueryParams( + list = uri, + ), + ).requireResponse() + .list + .render(accountKey) + } + + override suspend fun update( + listKey: MicroBlogKey, + metaData: ListMetaData, + ): UiList { + updateList( + uri = listKey.id, + title = metaData.title, + description = metaData.description, + icon = metaData.avatar, + ) + return info(listKey) + } + + private suspend fun updateList( + uri: String, + title: String, + description: String?, + icon: FileItem?, + ) { + val currentInfo: app.bsky.graph.List = + service + .getRecord( + params = + com.atproto.repo.GetRecordQueryParams( + collection = Nsid("app.bsky.graph.list"), + repo = Did(did = accountKey.id), + rkey = RKey(uri.substringAfterLast('/')), + ), + ).requireResponse() + .value + .decodeAs() + + val iconInfo = + if (icon != null) { + service.uploadBlob(icon.readBytes()).maybeResponse() + } else { + null + } + val newRecord = + currentInfo + .copy( + name = title, + description = description, + ).let { + if (iconInfo != null) { + it.copy(avatar = iconInfo.blob) + } else { + it + } + } + service.putRecord( + request = + PutRecordRequest( + repo = Did(did = accountKey.id), + collection = Nsid("app.bsky.graph.list"), + rkey = RKey(uri.substringAfterLast('/')), + record = newRecord.bskyJson(), + ), + ) + } + + override suspend fun delete(listKey: MicroBlogKey) { + val id = listKey.id.substringAfterLast('/') + service.applyWrites( + request = + ApplyWritesRequest( + repo = Did(did = accountKey.id), + writes = + persistentListOf( + ApplyWritesRequestWriteUnion.Delete( + value = + ApplyWritesDelete( + collection = Nsid("app.bsky.graph.list"), + rkey = RKey(id), + ), + ), + ), + ), + ) + } +} diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/bluesky/BlueskyListMemberLoader.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/bluesky/BlueskyListMemberLoader.kt new file mode 100644 index 000000000..37875aea2 --- /dev/null +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/bluesky/BlueskyListMemberLoader.kt @@ -0,0 +1,187 @@ +package dev.dimension.flare.data.datasource.bluesky + +import app.bsky.actor.GetProfileQueryParams +import app.bsky.graph.GetListQueryParams +import app.bsky.graph.GetListsQueryParams +import app.bsky.graph.Listitem +import com.atproto.repo.CreateRecordRequest +import com.atproto.repo.DeleteRecordRequest +import com.atproto.repo.ListRecordsQueryParams +import dev.dimension.flare.data.database.cache.mapper.toDbUser +import dev.dimension.flare.data.database.cache.model.DbUser +import dev.dimension.flare.data.datasource.microblog.list.ListMemberLoader +import dev.dimension.flare.data.datasource.microblog.paging.PagingRequest +import dev.dimension.flare.data.datasource.microblog.paging.PagingResult +import dev.dimension.flare.data.network.bluesky.BlueskyService +import dev.dimension.flare.model.MicroBlogKey +import dev.dimension.flare.ui.model.UiList +import dev.dimension.flare.ui.model.mapper.render +import sh.christian.ozone.api.AtUri +import sh.christian.ozone.api.Did +import sh.christian.ozone.api.Nsid +import sh.christian.ozone.api.RKey +import kotlin.time.Clock + +internal class BlueskyListMemberLoader( + private val service: BlueskyService, + private val accountKey: MicroBlogKey, +) : ListMemberLoader { + override suspend fun loadMembers( + pageSize: Int, + request: PagingRequest, + listKey: MicroBlogKey, + ): PagingResult { + val cursor = + when (request) { + is PagingRequest.Append -> request.nextKey + is PagingRequest.Refresh -> null + is PagingRequest.Prepend -> return PagingResult() + } + val response = + service + .getList( + params = + GetListQueryParams( + list = AtUri(listKey.id), + cursor = cursor, + limit = pageSize.toLong(), + ), + ).requireResponse() + val users = + response.items + .map { + it.subject.toDbUser(accountKey.host) + } + return PagingResult( + data = users, + nextKey = response.cursor, + ) + } + + override suspend fun addMember( + listKey: MicroBlogKey, + userKey: MicroBlogKey, + ): DbUser { + val user = + service + .getProfile(GetProfileQueryParams(actor = Did(did = userKey.id))) + .requireResponse() + + service.createRecord( + CreateRecordRequest( + repo = Did(did = accountKey.id), + collection = Nsid("app.bsky.graph.listitem"), + record = + app.bsky.graph + .Listitem( + list = AtUri(listKey.id), + subject = Did(userKey.id), + createdAt = Clock.System.now(), + ).bskyJson(), + ), + ) + return user.toDbUser(accountKey.host) + } + + override suspend fun removeMember( + listKey: MicroBlogKey, + userKey: MicroBlogKey, + ) { + var record: com.atproto.repo.ListRecordsRecord? = null + var cursor: String? = null + while (record == null) { + val response = + service + .listRecords( + params = + ListRecordsQueryParams( + repo = Did(did = accountKey.id), + collection = Nsid("app.bsky.graph.listitem"), + limit = 100, + cursor = cursor, + ), + ).requireResponse() + if (response.cursor == null || response.records.isEmpty()) { + break + } + cursor = response.cursor + record = + response.records + .firstOrNull { + val item: Listitem = it.value.decodeAs() + item.list.atUri == listKey.id && item.subject.did == userKey.id + } + } + if (record != null) { + service.deleteRecord( + DeleteRecordRequest( + repo = Did(did = accountKey.id), + collection = Nsid("app.bsky.graph.listitem"), + rkey = RKey(record.uri.atUri.substringAfterLast('/')), + ), + ) + } + } + + override suspend fun loadUserLists( + pageSize: Int, + request: PagingRequest, + userKey: MicroBlogKey, + ): PagingResult { + if (request is PagingRequest.Prepend) { + return PagingResult() + } + // Bluesky doesn't have an endpoint to get lists a user is in, so we have to iterate through all lists + // Since we can't easily pagination this, we will load everything at once for the first page + if (request is PagingRequest.Append) { + return PagingResult(nextKey = null) + } + + var cursor: String? = null + val lists = mutableListOf() + val allLists = + service + .getLists( + params = + GetListsQueryParams( + actor = Did(did = accountKey.id), + limit = 100, + ), + ).requireResponse() + .lists + .map { + it.render(accountKey) + } + while (true) { + val response = + service + .listRecords( + params = + ListRecordsQueryParams( + repo = Did(did = accountKey.id), + collection = Nsid("app.bsky.graph.listitem"), + limit = 100, + cursor = cursor, + ), + ).requireResponse() + lists.addAll( + response.records + .filter { + val item: Listitem = it.value.decodeAs() + item.subject.did == userKey.id + }.mapNotNull { + val item: Listitem = it.value.decodeAs() + allLists.firstOrNull { list -> list.key.id == item.list.atUri } + }, + ) + cursor = response.cursor + if (cursor == null) { + break + } + } + return PagingResult( + data = lists, + nextKey = null, + ) + } +} diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/mastodon/MastodonDataSource.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/mastodon/MastodonDataSource.kt index 6da4b2d9f..16b361d61 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/mastodon/MastodonDataSource.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/mastodon/MastodonDataSource.kt @@ -26,7 +26,9 @@ import dev.dimension.flare.data.datasource.microblog.ProfileTab import dev.dimension.flare.data.datasource.microblog.RelationDataSource import dev.dimension.flare.data.datasource.microblog.StatusEvent import dev.dimension.flare.data.datasource.microblog.list.ListDataSource +import dev.dimension.flare.data.datasource.microblog.list.ListHandler import dev.dimension.flare.data.datasource.microblog.list.ListLoader +import dev.dimension.flare.data.datasource.microblog.list.ListMemberHandler import dev.dimension.flare.data.datasource.microblog.list.ListMemberLoader import dev.dimension.flare.data.datasource.microblog.pagingConfig import dev.dimension.flare.data.datasource.microblog.relationKeyWithUserKey @@ -156,9 +158,9 @@ internal open class MastodonDataSource( accountKey, ) - override fun listTimeline(listId: String) = + override fun listTimeline(listKey: MicroBlogKey) = ListTimelineRemoteMediator( - listId, + listKey.id, service, database, accountKey, @@ -906,20 +908,36 @@ internal open class MastodonDataSource( }, ) - override val listLoader: ListLoader by lazy { + val listLoader: ListLoader by lazy { MastodonListLoader( service = service, accountKey = accountKey, ) } - override val listMemberLoader: ListMemberLoader by lazy { + val listMemberLoader: ListMemberLoader by lazy { MastodonListMemberLoader( service = service, accountKey = accountKey, ) } + override val listHandler: ListHandler by lazy { + ListHandler( + pagingKey = "lists_$accountKey", + accountKey = accountKey, + loader = listLoader, + ) + } + + override val listMemberHandler: ListMemberHandler by lazy { + ListMemberHandler( + pagingKey = "list_members_$accountKey", + accountKey = accountKey, + loader = listMemberLoader, + ) + } + private val notificationMarkerKey: String get() = "notificationBadgeCount_$accountKey" diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/microblog/MixedRemoteMediator.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/microblog/MixedRemoteMediator.kt index d7de007b8..d6b1ddecb 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/microblog/MixedRemoteMediator.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/microblog/MixedRemoteMediator.kt @@ -32,10 +32,10 @@ internal class MixedRemoteMediator( request: PagingRequest, ): PagingResult = coroutineScope { - if (request is Request.Prepend) { - Result(endOfPaginationReached = true) + if (request is PagingRequest.Prepend) { + PagingResult(endOfPaginationReached = true) } else { - if (request is Request.Refresh) { + if (request is PagingRequest.Refresh) { currentMediators = mediators } val response = @@ -47,7 +47,7 @@ internal class MixedRemoteMediator( runCatching { subRequest.load(pageSize) }.getOrElse { - Result(endOfPaginationReached = true) + PagingResult(endOfPaginationReached = true) }.let { SubResponse(subRequest.mediator, it) } @@ -80,14 +80,14 @@ internal class MixedRemoteMediator( currentMediators = response.mapNotNull { - if (it.result.endOfPaginationReached) { + if (it.result.nextKey == null) { null } else { it.mediator } } - Result( + PagingResult( endOfPaginationReached = currentMediators.isEmpty(), data = mixedTimelineResult + timelineResult, nextKey = if (currentMediators.isEmpty()) null else "mixed_next_key", @@ -97,46 +97,46 @@ internal class MixedRemoteMediator( } private suspend fun getSubRequest( - request: Request, + request: PagingRequest, mediator: BaseTimelineRemoteMediator, ): SubRequest? = when (request) { - is Request.Append -> { + is PagingRequest.Append -> { database .pagingTimelineDao() .getPagingKey(mediator.pagingKey) ?.nextKey - ?.let(Request::Append) + ?.let(PagingRequest::Append) } - is Request.Prepend -> + is PagingRequest.Prepend -> database .pagingTimelineDao() .getPagingKey(mediator.pagingKey) ?.prevKey - ?.let(Request::Prepend) + ?.let(PagingRequest::Prepend) - is Request.Refresh -> Request.Refresh + is PagingRequest.Refresh -> PagingRequest.Refresh }?.let { SubRequest(mediator, it) } private suspend fun saveSubResponse( - request: Request, + request: PagingRequest, subResponse: SubResponse, ) { val (mediator, result) = subResponse - if (request is Request.Prepend && result.previousKey != null) { + if (request is PagingRequest.Prepend && result.previousKey != null) { database.pagingTimelineDao().updatePagingKeyPrevKey( pagingKey = mediator.pagingKey, prevKey = result.previousKey, ) - } else if (request is Request.Append && result.nextKey != null) { + } else if (request is PagingRequest.Append && result.nextKey != null) { database.pagingTimelineDao().updatePagingKeyNextKey( pagingKey = mediator.pagingKey, nextKey = result.nextKey, ) - } else if (request is Request.Refresh) { + } else if (request is PagingRequest.Refresh) { database.pagingTimelineDao().deletePagingKey(mediator.pagingKey) database.pagingTimelineDao().insertPagingKey( dev.dimension.flare.data.database.cache.model.DbPagingKey( @@ -150,13 +150,13 @@ internal class MixedRemoteMediator( private data class SubRequest( val mediator: BaseTimelineRemoteMediator, - val request: Request, + val request: PagingRequest, ) { suspend fun load(pageSize: Int) = mediator.timeline(pageSize, request) } private data class SubResponse( val mediator: BaseTimelineRemoteMediator, - val result: Result, + val result: PagingResult, ) } diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/microblog/list/ListDataSource.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/microblog/list/ListDataSource.kt index 6b6ba68fe..b86b99b19 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/microblog/list/ListDataSource.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/microblog/list/ListDataSource.kt @@ -1,10 +1,11 @@ package dev.dimension.flare.data.datasource.microblog.list import dev.dimension.flare.data.datasource.microblog.paging.BaseTimelineLoader +import dev.dimension.flare.model.MicroBlogKey internal interface ListDataSource { - fun listTimeline(listId: String): BaseTimelineLoader + fun listTimeline(listKey: MicroBlogKey): BaseTimelineLoader - val listLoader: ListLoader - val listMemberLoader: ListMemberLoader + val listHandler: ListHandler + val listMemberHandler: ListMemberHandler } diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/microblog/list/ListHandler.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/microblog/list/ListHandler.kt index 7e202b257..d225d3995 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/microblog/list/ListHandler.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/microblog/list/ListHandler.kt @@ -16,6 +16,7 @@ import dev.dimension.flare.model.AccountType import dev.dimension.flare.model.DbAccountType import dev.dimension.flare.model.MicroBlogKey import dev.dimension.flare.ui.model.UiList +import kotlinx.collections.immutable.ImmutableList import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.mapNotNull import org.koin.core.component.KoinComponent @@ -28,7 +29,11 @@ internal class ListHandler( private val loader: ListLoader, ) : KoinComponent { private val accountType: DbAccountType = AccountType.Specific(accountKey) - val database: CacheDatabase by inject() + private val database: CacheDatabase by inject() + + val supportedMetaData: ImmutableList by lazy { + loader.supportedMetaData + } val data by lazy { Pager( config = pagingConfig, diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/microblog/list/ListMemberHandler.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/microblog/list/ListMemberHandler.kt index c559bd146..3be0609e6 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/microblog/list/ListMemberHandler.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/microblog/list/ListMemberHandler.kt @@ -26,7 +26,7 @@ internal class ListMemberHandler( private val loader: ListMemberLoader, ) : KoinComponent { private val accountType: DbAccountType = AccountType.Specific(accountKey) - val database: CacheDatabase by inject() + private val database: CacheDatabase by inject() private val memberPagingKey: String get() = "${pagingKey}_members" @@ -67,6 +67,17 @@ internal class ListMemberHandler( } } + fun listMembersListFlow(listKey: MicroBlogKey) = + database + .listDao() + .getListMembersFlow( + listKey = listKey, + ).map { members -> + members.map { member -> + member.user.render(accountKey) + } + } + suspend fun addMember( listKey: MicroBlogKey, userKey: MicroBlogKey, diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/misskey/MisskeyDataSource.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/misskey/MisskeyDataSource.kt index 3184b03ef..2befa94bd 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/misskey/MisskeyDataSource.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/misskey/MisskeyDataSource.kt @@ -1,12 +1,9 @@ package dev.dimension.flare.data.datasource.misskey import androidx.paging.ExperimentalPagingApi -import androidx.paging.LoadType import androidx.paging.Pager import androidx.paging.PagingData -import androidx.paging.PagingState import androidx.paging.cachedIn -import dev.dimension.flare.common.BaseRemoteMediator import dev.dimension.flare.common.CacheData import dev.dimension.flare.common.Cacheable import dev.dimension.flare.common.FileType @@ -23,17 +20,17 @@ import dev.dimension.flare.data.datasource.microblog.ComposeConfig import dev.dimension.flare.data.datasource.microblog.ComposeData import dev.dimension.flare.data.datasource.microblog.ComposeProgress import dev.dimension.flare.data.datasource.microblog.ComposeType -import dev.dimension.flare.data.datasource.microblog.ListDataSource -import dev.dimension.flare.data.datasource.microblog.ListMetaData -import dev.dimension.flare.data.datasource.microblog.ListMetaDataType -import dev.dimension.flare.data.datasource.microblog.MemoryPagingSource import dev.dimension.flare.data.datasource.microblog.NotificationFilter import dev.dimension.flare.data.datasource.microblog.ProfileAction import dev.dimension.flare.data.datasource.microblog.ProfileTab import dev.dimension.flare.data.datasource.microblog.ReactionDataSource import dev.dimension.flare.data.datasource.microblog.RelationDataSource import dev.dimension.flare.data.datasource.microblog.StatusEvent -import dev.dimension.flare.data.datasource.microblog.memoryPager +import dev.dimension.flare.data.datasource.microblog.list.ListDataSource +import dev.dimension.flare.data.datasource.microblog.list.ListHandler +import dev.dimension.flare.data.datasource.microblog.list.ListLoader +import dev.dimension.flare.data.datasource.microblog.list.ListMemberHandler +import dev.dimension.flare.data.datasource.microblog.list.ListMemberLoader import dev.dimension.flare.data.datasource.microblog.pagingConfig import dev.dimension.flare.data.datasource.microblog.relationKeyWithUserKey import dev.dimension.flare.data.datasource.microblog.timelinePager @@ -44,13 +41,6 @@ import dev.dimension.flare.data.network.misskey.api.model.NotesCreateRequest import dev.dimension.flare.data.network.misskey.api.model.NotesCreateRequestPoll import dev.dimension.flare.data.network.misskey.api.model.NotesPollsVoteRequest import dev.dimension.flare.data.network.misskey.api.model.NotesReactionsCreateRequest -import dev.dimension.flare.data.network.misskey.api.model.UsersListsCreateRequest -import dev.dimension.flare.data.network.misskey.api.model.UsersListsDeleteRequest -import dev.dimension.flare.data.network.misskey.api.model.UsersListsListRequest -import dev.dimension.flare.data.network.misskey.api.model.UsersListsMembershipRequest -import dev.dimension.flare.data.network.misskey.api.model.UsersListsPullRequest -import dev.dimension.flare.data.network.misskey.api.model.UsersListsShowRequest -import dev.dimension.flare.data.network.misskey.api.model.UsersListsUpdateRequest import dev.dimension.flare.data.network.misskey.api.model.UsersShowRequest import dev.dimension.flare.data.repository.AccountRepository import dev.dimension.flare.data.repository.LocalFilterRepository @@ -74,7 +64,6 @@ import dev.dimension.flare.ui.model.toUi import dev.dimension.flare.ui.presenter.compose.ComposeStatus import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableMap -import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableMap import kotlinx.collections.immutable.toPersistentList @@ -1019,284 +1008,43 @@ internal class MisskeyDataSource( private val listKey: String get() = "allLists_$accountKey" - override fun myList(scope: CoroutineScope): Flow> = - memoryPager( - pageSize = 20, - pagingKey = listKey, - scope = scope, - mediator = - object : BaseRemoteMediator() { - override suspend fun doLoad( - loadType: LoadType, - state: PagingState, - ): MediatorResult { - if (loadType == LoadType.PREPEND) { - return MediatorResult.Success(endOfPaginationReached = true) - } - val result = - service - .usersListsList( - UsersListsListRequest(), - ).orEmpty() - .map { - it.render() - }.toImmutableList() - - MemoryPagingSource.update( - key = listKey, - value = result.toImmutableList(), - ) - - return MediatorResult.Success( - endOfPaginationReached = true, - ) - } - }, + override fun listTimeline(listKey: MicroBlogKey) = + ListTimelineRemoteMediator( + listKey.id, + service, + database, + accountKey, ) - override suspend fun createList(metaData: ListMetaData) { - tryRun { - service - .usersListsCreate( - UsersListsCreateRequest( - name = metaData.title, - ), - ) - }.onSuccess { response -> - MemoryPagingSource.updateWith( - key = listKey, - ) { - it - .plus( - UiList.List( - id = response.id, - title = metaData.title, - ), - ).toImmutableList() - } - } - } - - override suspend fun deleteList(listId: String) { - tryRun { - service.usersListsDelete( - UsersListsDeleteRequest(listId = listId), - ) - }.onSuccess { - MemoryPagingSource.updateWith( - key = listKey, - ) { - it - .filter { list -> list.id != listId } - .toImmutableList() - } - } - } - - override suspend fun updateList( - listId: String, - metaData: ListMetaData, - ) { - tryRun { - service.usersListsUpdate( - UsersListsUpdateRequest( - listId = listId, - name = metaData.title, - ), - ) - }.onSuccess { - MemoryPagingSource.updateWith( - key = listKey, - ) { - it - .map { list -> - if (list.id == listId) { - list.copy(title = metaData.title) - } else { - list - } - }.toImmutableList() - } - } - } - - override fun listInfo(listId: String): CacheData = - MemCacheable( - key = "listInfo_$listId", - fetchSource = { - service - .usersListsShow( - UsersListsShowRequest( - listId = listId, - ), - ).render() - }, + val listLoader: ListLoader by lazy { + MisskeyListLoader( + service = service, + accountKey = accountKey, ) + } - override fun listMembers( - listId: String, - scope: CoroutineScope, - pageSize: Int, - ): Flow> = - memoryPager( - pageSize = pageSize, - pagingKey = listMemberKey(listId), - scope = scope, - mediator = - object : BaseRemoteMediator() { - override suspend fun doLoad( - loadType: LoadType, - state: PagingState, - ): MediatorResult { - if (loadType == LoadType.PREPEND) { - return MediatorResult.Success(endOfPaginationReached = true) - } - val key = - if (loadType == LoadType.REFRESH) { - null - } else { - MemoryPagingSource - .get(key = listMemberKey(listId)) - ?.lastOrNull() - ?.key - ?.id - } - val result = - service - .usersListsGetMemberships( - UsersListsMembershipRequest( - listId = listId, - untilId = key, - limit = state.config.pageSize, - ), - ).orEmpty() - .map { - it.user.render(accountKey) - } - - if (loadType == LoadType.REFRESH) { - MemoryPagingSource.update( - key = listMemberKey(listId), - value = result.toImmutableList(), - ) - } else if (loadType == LoadType.APPEND) { - MemoryPagingSource.append( - key = listMemberKey(listId), - value = result.toImmutableList(), - ) - } - - return MediatorResult.Success( - endOfPaginationReached = result.isEmpty(), - ) - } - }, + val listMemberLoader: ListMemberLoader by lazy { + MisskeyListMemberLoader( + service = service, + accountKey = accountKey, ) - - private fun listMemberKey(listId: String) = "listMembers_$listId" - - private fun userListsKey(userKey: MicroBlogKey) = "userLists_${userKey.id}" - - override suspend fun addMember( - listId: String, - userKey: MicroBlogKey, - ) { - tryRun { - service.usersListsPush( - UsersListsPullRequest( - listId = listId, - userId = userKey.id, - ), - ) - val user = - service - .usersShow( - UsersShowRequest( - userId = userKey.id, - ), - ).toDbUser(accountKey.host) - .render(accountKey) - MemoryPagingSource.updateWith( - key = listMemberKey(listId), - ) { - (listOfNotNull(user) + it) - .distinctBy { - it.key - }.toImmutableList() - } - val list = - service - .usersListsShow( - UsersListsShowRequest( - listId = listId, - ), - ) - MemCacheable.updateWith>( - key = userListsKey(userKey), - ) { - it - .plus(list.render()) - .toImmutableList() - } - } } - override suspend fun removeMember( - listId: String, - userKey: MicroBlogKey, - ) { - tryRun { - service.usersListsPull( - UsersListsPullRequest( - listId = listId, - userId = userKey.id, - ), - ) - MemoryPagingSource.updateWith( - key = listMemberKey(listId), - ) { - it - .filter { user -> user.key.id != userKey.id } - .toImmutableList() - } - MemCacheable.updateWith>( - key = userListsKey(userKey), - ) { - it - .filter { list -> list.id != listId } - .toImmutableList() - } - } + override val listHandler: ListHandler by lazy { + ListHandler( + pagingKey = listKey, + accountKey = accountKey, + loader = listLoader, + ) } - override fun listTimeline(listId: String) = - ListTimelineRemoteMediator( - listId, - service, - database, - accountKey, + override val listMemberHandler: ListMemberHandler by lazy { + ListMemberHandler( + pagingKey = "list_members_$accountKey", + accountKey = accountKey, + loader = listMemberLoader, ) - - override fun listMemberCache(listId: String): Flow> = - MemoryPagingSource.getFlow(listMemberKey(listId)) - - override fun userLists(userKey: MicroBlogKey): MemCacheable> = - MemCacheable( - key = userListsKey(userKey), - ) { - service - .usersListsList( - UsersListsListRequest(), - ).orEmpty() - .filter { - it.userIds?.contains(userKey.id) == true - }.map { - it.render() - }.toImmutableList() - } - - override val supportedMetaData: ImmutableList - get() = persistentListOf(ListMetaDataType.TITLE) + } override fun acceptFollowRequest( userKey: MicroBlogKey, diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/misskey/MisskeyListLoader.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/misskey/MisskeyListLoader.kt new file mode 100644 index 000000000..73b729d1e --- /dev/null +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/misskey/MisskeyListLoader.kt @@ -0,0 +1,91 @@ +package dev.dimension.flare.data.datasource.misskey + +import dev.dimension.flare.data.datasource.microblog.list.ListLoader +import dev.dimension.flare.data.datasource.microblog.list.ListMetaData +import dev.dimension.flare.data.datasource.microblog.list.ListMetaDataType +import dev.dimension.flare.data.datasource.microblog.paging.PagingRequest +import dev.dimension.flare.data.datasource.microblog.paging.PagingResult +import dev.dimension.flare.data.network.misskey.MisskeyService +import dev.dimension.flare.data.network.misskey.api.model.UsersListsCreateRequest +import dev.dimension.flare.data.network.misskey.api.model.UsersListsDeleteRequest +import dev.dimension.flare.data.network.misskey.api.model.UsersListsListRequest +import dev.dimension.flare.data.network.misskey.api.model.UsersListsShowRequest +import dev.dimension.flare.data.network.misskey.api.model.UsersListsUpdateRequest +import dev.dimension.flare.model.MicroBlogKey +import dev.dimension.flare.ui.model.UiList +import dev.dimension.flare.ui.model.mapper.render +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toImmutableList + +internal class MisskeyListLoader( + private val service: MisskeyService, + private val accountKey: MicroBlogKey, +) : ListLoader { + override suspend fun load( + pageSize: Int, + request: PagingRequest, + ): PagingResult { + if (request is PagingRequest.Prepend) { + return PagingResult() + } + val result = + service + .usersListsList( + UsersListsListRequest(), + ).orEmpty() + .map { + it.render() + }.toImmutableList() + + return PagingResult( + data = result, + nextKey = null, + ) + } + + override suspend fun info(listKey: MicroBlogKey): UiList = + service + .usersListsShow( + UsersListsShowRequest( + listId = listKey.id, + ), + ).render() + + override suspend fun create(metaData: ListMetaData): UiList { + val response = + service.usersListsCreate( + UsersListsCreateRequest( + name = metaData.title, + ), + ) + return UiList.List( + key = MicroBlogKey(response.id, accountKey.host), + title = metaData.title, + description = null, + avatar = null, + creator = null, + ) + } + + override suspend fun update( + listKey: MicroBlogKey, + metaData: ListMetaData, + ): UiList = + service + .usersListsUpdate( + UsersListsUpdateRequest( + listId = listKey.id, + name = metaData.title, + ), + ).render() + + override suspend fun delete(listKey: MicroBlogKey) { + service.usersListsDelete( + UsersListsDeleteRequest(listId = listKey.id), + ) + } + + override val supportedMetaData: ImmutableList + get() = persistentListOf(ListMetaDataType.TITLE) +} diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/misskey/MisskeyListMemberLoader.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/misskey/MisskeyListMemberLoader.kt new file mode 100644 index 000000000..8c4ade5fc --- /dev/null +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/misskey/MisskeyListMemberLoader.kt @@ -0,0 +1,113 @@ +package dev.dimension.flare.data.datasource.misskey + +import dev.dimension.flare.data.database.cache.mapper.toDbUser +import dev.dimension.flare.data.database.cache.model.DbUser +import dev.dimension.flare.data.datasource.microblog.list.ListMemberLoader +import dev.dimension.flare.data.datasource.microblog.paging.PagingRequest +import dev.dimension.flare.data.datasource.microblog.paging.PagingResult +import dev.dimension.flare.data.network.misskey.MisskeyService +import dev.dimension.flare.data.network.misskey.api.model.UsersListsListRequest +import dev.dimension.flare.data.network.misskey.api.model.UsersListsMembershipRequest +import dev.dimension.flare.data.network.misskey.api.model.UsersListsPullRequest +import dev.dimension.flare.data.network.misskey.api.model.UsersShowRequest +import dev.dimension.flare.model.MicroBlogKey +import dev.dimension.flare.ui.model.UiList +import dev.dimension.flare.ui.model.mapper.render +import kotlinx.collections.immutable.toImmutableList + +internal class MisskeyListMemberLoader( + private val service: MisskeyService, + private val accountKey: MicroBlogKey, +) : ListMemberLoader { + override suspend fun loadMembers( + pageSize: Int, + request: PagingRequest, + listKey: MicroBlogKey, + ): PagingResult { + val cursor = + when (request) { + is PagingRequest.Append -> request.nextKey + is PagingRequest.Refresh -> null + is PagingRequest.Prepend -> return PagingResult() + } + + val response = + service + .usersListsGetMemberships( + UsersListsMembershipRequest( + listId = listKey.id, + untilId = cursor, + limit = pageSize, + ), + ).orEmpty() + + val users = + response.map { + it.user.toDbUser(accountKey.host) + } + + return PagingResult( + data = users, + nextKey = response.lastOrNull()?.id, + ) + } + + override suspend fun addMember( + listKey: MicroBlogKey, + userKey: MicroBlogKey, + ): DbUser { + service.usersListsPush( + UsersListsPullRequest( + listId = listKey.id, + userId = userKey.id, + ), + ) + return service + .usersShow( + UsersShowRequest( + userId = userKey.id, + ), + ).toDbUser(accountKey.host) + } + + override suspend fun removeMember( + listKey: MicroBlogKey, + userKey: MicroBlogKey, + ) { + service.usersListsPull( + UsersListsPullRequest( + listId = listKey.id, + userId = userKey.id, + ), + ) + } + + override suspend fun loadUserLists( + pageSize: Int, + request: PagingRequest, + userKey: MicroBlogKey, + ): PagingResult { + if (request is PagingRequest.Prepend) { + return PagingResult() + } + if (request is PagingRequest.Append) { + return PagingResult(nextKey = null) + } + + val result = + service + .usersListsList( + UsersListsListRequest(), + ).orEmpty() + .filter { + it.userIds?.contains(userKey.id) == true + }.map { + it.render() + }.toImmutableList() + + return PagingResult( + data = result, + nextKey = null, + ) + } +} diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/xqt/XQTDataSource.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/xqt/XQTDataSource.kt index 1f4563e00..c68c6e9a3 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/xqt/XQTDataSource.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/xqt/XQTDataSource.kt @@ -1,13 +1,10 @@ package dev.dimension.flare.data.datasource.xqt import androidx.paging.ExperimentalPagingApi -import androidx.paging.LoadType import androidx.paging.Pager import androidx.paging.PagingData -import androidx.paging.PagingState import androidx.paging.cachedIn import androidx.paging.map -import dev.dimension.flare.common.BaseRemoteMediator import dev.dimension.flare.common.CacheData import dev.dimension.flare.common.Cacheable import dev.dimension.flare.common.FileType @@ -18,10 +15,8 @@ import dev.dimension.flare.common.encodeJson import dev.dimension.flare.data.database.cache.CacheDatabase import dev.dimension.flare.data.database.cache.connect import dev.dimension.flare.data.database.cache.mapper.XQT -import dev.dimension.flare.data.database.cache.mapper.cursor import dev.dimension.flare.data.database.cache.mapper.toDbUser import dev.dimension.flare.data.database.cache.mapper.tweets -import dev.dimension.flare.data.database.cache.mapper.users import dev.dimension.flare.data.database.cache.model.DbMessageItem import dev.dimension.flare.data.database.cache.model.DbPagingTimeline import dev.dimension.flare.data.database.cache.model.MessageContent @@ -33,27 +28,23 @@ import dev.dimension.flare.data.datasource.microblog.ComposeData import dev.dimension.flare.data.datasource.microblog.ComposeProgress import dev.dimension.flare.data.datasource.microblog.ComposeType import dev.dimension.flare.data.datasource.microblog.DirectMessageDataSource -import dev.dimension.flare.data.datasource.microblog.ListDataSource -import dev.dimension.flare.data.datasource.microblog.ListMetaData -import dev.dimension.flare.data.datasource.microblog.ListMetaDataType -import dev.dimension.flare.data.datasource.microblog.MemoryPagingSource import dev.dimension.flare.data.datasource.microblog.NotificationFilter import dev.dimension.flare.data.datasource.microblog.ProfileAction import dev.dimension.flare.data.datasource.microblog.ProfileTab import dev.dimension.flare.data.datasource.microblog.RelationDataSource import dev.dimension.flare.data.datasource.microblog.StatusEvent import dev.dimension.flare.data.datasource.microblog.createSendingDirectMessage -import dev.dimension.flare.data.datasource.microblog.memoryPager +import dev.dimension.flare.data.datasource.microblog.list.ListDataSource +import dev.dimension.flare.data.datasource.microblog.list.ListHandler +import dev.dimension.flare.data.datasource.microblog.list.ListMemberHandler import dev.dimension.flare.data.datasource.microblog.paging.BaseTimelineLoader import dev.dimension.flare.data.datasource.microblog.pagingConfig import dev.dimension.flare.data.datasource.microblog.relationKeyWithUserKey import dev.dimension.flare.data.datasource.microblog.timelinePager import dev.dimension.flare.data.network.xqt.XQTService -import dev.dimension.flare.data.network.xqt.model.AddMemberRequest import dev.dimension.flare.data.network.xqt.model.AddToConversationRequest import dev.dimension.flare.data.network.xqt.model.CreateBookmarkRequest import dev.dimension.flare.data.network.xqt.model.CreateBookmarkRequestVariables -import dev.dimension.flare.data.network.xqt.model.CreateListRequest import dev.dimension.flare.data.network.xqt.model.DeleteBookmarkRequest import dev.dimension.flare.data.network.xqt.model.DeleteBookmarkRequestVariables import dev.dimension.flare.data.network.xqt.model.LiveVideoStreamStatusResponse @@ -72,9 +63,6 @@ import dev.dimension.flare.data.network.xqt.model.PostDmNew2Request import dev.dimension.flare.data.network.xqt.model.PostFavoriteTweetRequest import dev.dimension.flare.data.network.xqt.model.PostMediaMetadataCreateRequest import dev.dimension.flare.data.network.xqt.model.PostUnfavoriteTweetRequest -import dev.dimension.flare.data.network.xqt.model.RemoveListRequest -import dev.dimension.flare.data.network.xqt.model.RemoveMemberRequest -import dev.dimension.flare.data.network.xqt.model.UpdateListRequest import dev.dimension.flare.data.network.xqt.model.User import dev.dimension.flare.data.network.xqt.model.UserUnavailable import dev.dimension.flare.data.repository.AccountRepository @@ -88,20 +76,17 @@ import dev.dimension.flare.ui.model.UiAccount import dev.dimension.flare.ui.model.UiDMItem import dev.dimension.flare.ui.model.UiDMRoom import dev.dimension.flare.ui.model.UiHashtag -import dev.dimension.flare.ui.model.UiList import dev.dimension.flare.ui.model.UiPodcast import dev.dimension.flare.ui.model.UiProfile import dev.dimension.flare.ui.model.UiRelation import dev.dimension.flare.ui.model.UiState import dev.dimension.flare.ui.model.UiTimeline import dev.dimension.flare.ui.model.UiUserV2 -import dev.dimension.flare.ui.model.mapper.list import dev.dimension.flare.ui.model.mapper.render import dev.dimension.flare.ui.model.mapper.toUi import dev.dimension.flare.ui.model.toUi import dev.dimension.flare.ui.presenter.compose.ComposeStatus import kotlinx.collections.immutable.ImmutableList -import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toPersistentList import kotlinx.coroutines.CoroutineScope @@ -156,6 +141,24 @@ internal class XQTDataSource( ) } + private val listLoader = XQTListLoader(service, accountKey) + + private val listMemberLoader = XQTListMemberLoader(service, accountKey) + + override val listHandler = + ListHandler( + pagingKey = "list_$accountKey", + accountKey = accountKey, + loader = listLoader, + ) + + override val listMemberHandler = + ListMemberHandler( + pagingKey = "list_member_$accountKey", + accountKey = accountKey, + loader = listMemberLoader, + ) + override fun homeTimeline() = HomeTimelineRemoteMediator( service, @@ -1201,358 +1204,9 @@ internal class XQTDataSource( }, ).toPersistentList() - private val listKey: String - get() = "allLists_$accountKey" - - override fun myList(scope: CoroutineScope): Flow> = - memoryPager( - pageSize = 20, - pagingKey = listKey, - scope = scope, - mediator = - object : BaseRemoteMediator() { - var cursor: String? = null - - override suspend fun doLoad( - loadType: LoadType, - state: PagingState, - ): MediatorResult { - if (loadType == LoadType.PREPEND) { - return MediatorResult.Success(endOfPaginationReached = true) - } - if (loadType == LoadType.REFRESH) { - cursor = null - } - val response = - service - .getListsManagementPageTimeline( - variables = - buildString { - append("{\"count\":20") - if (cursor != null) { - append(",\"cursor\":\"${cursor}\"") - } - append("}") - }, - ).body() - ?.data - ?.viewer - ?.listManagementTimeline - ?.timeline - ?.instructions - - cursor = response?.cursor() - - val result = - response - ?.list(accountKey = accountKey) - .orEmpty() - .toImmutableList() - - if (loadType == LoadType.REFRESH) { - MemoryPagingSource.update( - key = listKey, - value = result, - ) - } else if (loadType == LoadType.APPEND) { - MemoryPagingSource.append( - key = listKey, - value = result, - ) - } - - return MediatorResult.Success( - endOfPaginationReached = result.isEmpty(), - ) - } - }, - ) - - override suspend fun createList(metaData: ListMetaData) { - tryRun { - service.createList( - request = - CreateListRequest( - variables = - CreateListRequest.Variables( - name = metaData.title, - description = metaData.description.orEmpty(), - isPrivate = false, - ), - ), - ) - }.onSuccess { response -> - val data = response.body()?.data?.list - if (data?.idStr != null) { - MemoryPagingSource.updateWith( - key = listKey, - ) { - it - .plus( - UiList.List( - id = data.idStr, - title = metaData.title, - description = metaData.description, - ), - ).toImmutableList() - } - } - } - } - - override suspend fun deleteList(listId: String) { - tryRun { - service.deleteList( - request = - RemoveListRequest( - variables = - RemoveListRequest.Variables( - listID = listId, - ), - ), - ) - }.onSuccess { - MemoryPagingSource.updateWith( - key = listKey, - ) { - it - .filter { list -> list.id != listId } - .toImmutableList() - } - } - } - - override suspend fun updateList( - listId: String, - metaData: ListMetaData, - ) { - tryRun { - service.updateList( - request = - UpdateListRequest( - variables = - UpdateListRequest.Variables( - listID = listId, - name = metaData.title, - description = metaData.description.orEmpty(), - isPrivate = false, - ), - ), - ) - }.onSuccess { - MemoryPagingSource.updateWith( - key = listKey, - ) { - it - .map { list -> - if (list.id == listId) { - list.copy( - title = metaData.title, - description = metaData.description, - ) - } else { - list - } - }.toImmutableList() - } - } - } - - override fun listInfo(listId: String): CacheData = - MemCacheable( - key = "listInfo_$listId", - fetchSource = { - getListInfo(listId) - ?: throw Exception("List not found") - }, - ) - - private suspend fun getListInfo(listId: String) = - service - .getListByRestId( - variables = "{\"listId\":\"${listId}\"}", - ).body() - ?.data - ?.list - ?.render(accountKey = accountKey) - - private fun listMemberKey(listId: String) = "listMembers_$listId" - - override fun listMembers( - listId: String, - scope: CoroutineScope, - pageSize: Int, - ): Flow> = - memoryPager( - pageSize = pageSize, - pagingKey = listMemberKey(listId), - scope = scope, - mediator = - object : BaseRemoteMediator() { - var cursor: String? = null - - override suspend fun doLoad( - loadType: LoadType, - state: PagingState, - ): MediatorResult { - if (loadType == LoadType.PREPEND) { - return MediatorResult.Success(endOfPaginationReached = true) - } - if (loadType == LoadType.REFRESH) { - cursor = null - } - val response = - service - .getListMembers( - variables = - buildString { - append("{\"listId\":\"${listId}\",\"count\":$pageSize") - if (cursor != null) { - append(",\"cursor\":\"${cursor}\"") - } - append("}") - }, - ).body() - ?.data - ?.list - ?.membersTimeline - ?.timeline - ?.instructions - - cursor = response?.cursor() - - val result = - response?.users().orEmpty().map { - it.render(accountKey = accountKey) - } - - if (loadType == LoadType.REFRESH) { - MemoryPagingSource.update( - key = listMemberKey(listId), - value = result.toImmutableList(), - ) - } else if (loadType == LoadType.APPEND) { - MemoryPagingSource.append( - key = listMemberKey(listId), - value = result.toImmutableList(), - ) - } - - return MediatorResult.Success( - endOfPaginationReached = result.isEmpty(), - ) - } - }, - ) - - private fun userListsKey(userKey: MicroBlogKey) = "userLists_${userKey.id}" - - override suspend fun addMember( - listId: String, - userKey: MicroBlogKey, - ) { - tryRun { - service.addMember( - request = - AddMemberRequest( - variables = - AddMemberRequest.Variables( - listID = listId, - userID = userKey.id, - ), - ), - ) - val user = - service - .userById(userKey.id) - .body() - ?.data - ?.user - ?.result - ?.let { - when (it) { - is User -> it - is UserUnavailable -> null - } - }?.toDbUser(accountKey) - ?.render(accountKey = accountKey) ?: throw Exception("User not found") - MemoryPagingSource.updateWith( - key = listMemberKey(listId), - ) { - (listOf(user) + it) - .distinctBy { - it.key - }.toImmutableList() - } - val list = getListInfo(listId) - if (list?.id != null) { - MemCacheable.updateWith>( - key = userListsKey(userKey), - ) { - it - .plus(list) - .toImmutableList() - } - } - } - } - - override suspend fun removeMember( - listId: String, - userKey: MicroBlogKey, - ) { - tryRun { - service.removeMember( - request = - RemoveMemberRequest( - variables = - RemoveMemberRequest.Variables( - listID = listId, - userID = userKey.id, - ), - ), - ) - MemoryPagingSource.updateWith( - key = listMemberKey(listId), - ) { - it - .filter { user -> user.key.id != userKey.id } - .toImmutableList() - } - MemCacheable.updateWith>( - key = userListsKey(userKey), - ) { - it - .filter { list -> list.id != listId } - .toImmutableList() - } - } - } - - override fun listMemberCache(listId: String): Flow> = - MemoryPagingSource.getFlow(listMemberKey(listId)) - - override fun userLists(userKey: MicroBlogKey): MemCacheable> = - MemCacheable( - key = userListsKey(userKey), - ) { - service - .getListsMemberships( - userId = userKey.id, - ).body() - ?.lists - ?.mapNotNull { - it.render(accountKey = accountKey) - }.orEmpty() - .toImmutableList() - } - - override val supportedMetaData: ImmutableList - get() = persistentListOf(ListMetaDataType.TITLE, ListMetaDataType.DESCRIPTION) - - override fun listTimeline(listId: String) = + override fun listTimeline(listKey: MicroBlogKey) = ListTimelineRemoteMediator( - listId, + listKey.id, service, database, accountKey, diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/xqt/XQTListLoader.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/xqt/XQTListLoader.kt new file mode 100644 index 000000000..26c18c4de --- /dev/null +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/xqt/XQTListLoader.kt @@ -0,0 +1,140 @@ +package dev.dimension.flare.data.datasource.xqt + +import dev.dimension.flare.data.database.cache.mapper.cursor +import dev.dimension.flare.data.datasource.microblog.list.ListLoader +import dev.dimension.flare.data.datasource.microblog.list.ListMetaData +import dev.dimension.flare.data.datasource.microblog.list.ListMetaDataType +import dev.dimension.flare.data.datasource.microblog.paging.PagingRequest +import dev.dimension.flare.data.datasource.microblog.paging.PagingResult +import dev.dimension.flare.data.network.xqt.XQTService +import dev.dimension.flare.data.network.xqt.model.CreateListRequest +import dev.dimension.flare.data.network.xqt.model.RemoveListRequest +import dev.dimension.flare.data.network.xqt.model.UpdateListRequest +import dev.dimension.flare.model.MicroBlogKey +import dev.dimension.flare.ui.model.UiList +import dev.dimension.flare.ui.model.mapper.list +import dev.dimension.flare.ui.model.mapper.render +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf + +internal class XQTListLoader( + private val service: XQTService, + private val accountKey: MicroBlogKey, +) : ListLoader { + override suspend fun load( + pageSize: Int, + request: PagingRequest, + ): PagingResult { + val cursor = (request as? PagingRequest.Append)?.nextKey + val response = + service + .getListsManagementPageTimeline( + variables = + buildString { + append("{\"count\":$pageSize") + if (cursor != null) { + append(",\"cursor\":\"${cursor}\"") + } + append("}") + }, + ).body() + ?.data + ?.viewer + ?.listManagementTimeline + ?.timeline + ?.instructions + + val nextCursor = response?.cursor() + + val result = + response + ?.list(accountKey = accountKey) + .orEmpty() + + return PagingResult( + data = result, + nextKey = nextCursor, + ) + } + + override suspend fun info(listKey: MicroBlogKey): UiList = + service + .getListByRestId( + variables = "{\"listId\":\"${listKey.id}\"}", + ).body() + ?.data + ?.list + ?.render(accountKey = accountKey) + ?: throw Exception("List not found") + + override suspend fun create(metaData: ListMetaData): UiList { + val response = + service.createList( + request = + CreateListRequest( + variables = + CreateListRequest.Variables( + name = metaData.title, + description = metaData.description.orEmpty(), + isPrivate = false, + ), + ), + ) + val data = response.body()?.data?.list + if (data?.idStr != null) { + return UiList.List( + key = MicroBlogKey(data.idStr, accountKey.host), + title = metaData.title, + description = metaData.description, + creator = null, + avatar = null, + readonly = false, + ) + } else { + throw Exception("Failed to create list") + } + } + + override suspend fun update( + listKey: MicroBlogKey, + metaData: ListMetaData, + ): UiList { + service.updateList( + request = + UpdateListRequest( + variables = + UpdateListRequest.Variables( + listID = listKey.id, + name = metaData.title, + description = metaData.description.orEmpty(), + isPrivate = false, + ), + ), + ) + return info(listKey).let { + if (it is UiList.List) { + it.copy( + title = metaData.title, + description = metaData.description, + ) + } else { + it + } + } + } + + override suspend fun delete(listKey: MicroBlogKey) { + service.deleteList( + request = + RemoveListRequest( + variables = + RemoveListRequest.Variables( + listID = listKey.id, + ), + ), + ) + } + + override val supportedMetaData: ImmutableList + get() = persistentListOf(ListMetaDataType.TITLE, ListMetaDataType.DESCRIPTION) +} diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/xqt/XQTListMemberLoader.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/xqt/XQTListMemberLoader.kt new file mode 100644 index 000000000..30480e812 --- /dev/null +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/xqt/XQTListMemberLoader.kt @@ -0,0 +1,126 @@ +package dev.dimension.flare.data.datasource.xqt + +import dev.dimension.flare.data.database.cache.mapper.cursor +import dev.dimension.flare.data.database.cache.mapper.toDbUser +import dev.dimension.flare.data.database.cache.mapper.users +import dev.dimension.flare.data.database.cache.model.DbUser +import dev.dimension.flare.data.datasource.microblog.list.ListMemberLoader +import dev.dimension.flare.data.datasource.microblog.paging.PagingRequest +import dev.dimension.flare.data.datasource.microblog.paging.PagingResult +import dev.dimension.flare.data.network.xqt.XQTService +import dev.dimension.flare.data.network.xqt.model.AddMemberRequest +import dev.dimension.flare.data.network.xqt.model.RemoveMemberRequest +import dev.dimension.flare.data.network.xqt.model.User +import dev.dimension.flare.data.network.xqt.model.UserUnavailable +import dev.dimension.flare.model.MicroBlogKey +import dev.dimension.flare.ui.model.UiList +import dev.dimension.flare.ui.model.mapper.render + +internal class XQTListMemberLoader( + private val service: XQTService, + private val accountKey: MicroBlogKey, +) : ListMemberLoader { + override suspend fun loadMembers( + pageSize: Int, + request: PagingRequest, + listKey: MicroBlogKey, + ): PagingResult { + val cursor = (request as? PagingRequest.Append)?.nextKey + val response = + service + .getListMembers( + variables = + buildString { + append("{\"listId\":\"${listKey.id}\",\"count\":$pageSize") + if (cursor != null) { + append(",\"cursor\":\"${cursor}\"") + } + append("}") + }, + ).body() + ?.data + ?.list + ?.membersTimeline + ?.timeline + ?.instructions + + val nextCursor = response?.cursor() + + val result = + response?.users().orEmpty().map { + it.toDbUser(accountKey = accountKey) + } + + return PagingResult( + data = result, + nextKey = nextCursor, + ) + } + + override suspend fun addMember( + listKey: MicroBlogKey, + userKey: MicroBlogKey, + ): DbUser { + service.addMember( + request = + AddMemberRequest( + variables = + AddMemberRequest.Variables( + listID = listKey.id, + userID = userKey.id, + ), + ), + ) + return service + .userById(userKey.id) + .body() + ?.data + ?.user + ?.result + ?.let { + when (it) { + is User -> it + is UserUnavailable -> null + } + }?.toDbUser(accountKey) + ?: throw Exception("User not found") + } + + override suspend fun removeMember( + listKey: MicroBlogKey, + userKey: MicroBlogKey, + ) { + service.removeMember( + request = + RemoveMemberRequest( + variables = + RemoveMemberRequest.Variables( + listID = listKey.id, + userID = userKey.id, + ), + ), + ) + } + + override suspend fun loadUserLists( + pageSize: Int, + request: PagingRequest, + userKey: MicroBlogKey, + ): PagingResult { + // XQT getListsMemberships seems to return all lists or doesn't support pagination in the used endpoint/method signature easily? + // The original implementation didn't use pagination. + val result = + service + .getListsMemberships( + userId = userKey.id, + ).body() + ?.lists + ?.map { + it.render(accountKey = accountKey) + }.orEmpty() + + return PagingResult( + data = result, + ) + } +} diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/model/UiList.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/model/UiList.kt index 4cc6d066a..69e4bf230 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/model/UiList.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/model/UiList.kt @@ -10,6 +10,7 @@ public sealed class UiList { // public abstract val id: String public abstract val key: MicroBlogKey public abstract val title: String + public abstract val readonly: Boolean @Serializable @Immutable @@ -19,7 +20,7 @@ public sealed class UiList { val description: String? = null, val avatar: String? = null, val creator: UiUserV2? = null, - val readonly: Boolean = false, + override val readonly: Boolean = false, ) : UiList() @Serializable @@ -32,6 +33,7 @@ public sealed class UiList { val creator: UiUserV2? = null, val likedCount: UiNumber = UiNumber(0), val liked: Boolean = false, + override val readonly: Boolean = false, ) : UiList() @Serializable @@ -39,6 +41,7 @@ public sealed class UiList { public data class Antenna( override val key: MicroBlogKey, override val title: String, + override val readonly: Boolean = false, ) : UiList() @Serializable @@ -53,5 +56,6 @@ public sealed class UiList { val banner: String? = null, val isFollowing: Boolean? = null, val isFavorited: Boolean? = null, + override val readonly: Boolean = false, ) : UiList() } diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/list/AllListPresenter.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/list/AllListPresenter.kt index 663aa10e7..e4ce16a00 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/list/AllListPresenter.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/list/AllListPresenter.kt @@ -4,12 +4,13 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.Immutable import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope +import androidx.paging.cachedIn import androidx.paging.compose.collectAsLazyPagingItems import dev.dimension.flare.common.PagingState import dev.dimension.flare.common.isRefreshing import dev.dimension.flare.common.refreshSuspend import dev.dimension.flare.common.toPagingState -import dev.dimension.flare.data.datasource.microblog.ListDataSource +import dev.dimension.flare.data.datasource.microblog.list.ListDataSource import dev.dimension.flare.data.repository.AccountRepository import dev.dimension.flare.data.repository.accountServiceProvider import dev.dimension.flare.model.AccountType @@ -39,7 +40,7 @@ public class AllListPresenter( .map { service -> remember(service) { require(service is ListDataSource) - service.myList(scope = scope) + service.listHandler.data.cachedIn(scope) }.collectAsLazyPagingItems() }.toPagingState() return object : AllListState { @@ -62,7 +63,7 @@ public class AllListPresenter( @Immutable public interface AllListState { - public val items: PagingState + public val items: PagingState public val isRefreshing: Boolean public fun refresh() diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/list/CreateListPresenter.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/list/CreateListPresenter.kt index 77bf67627..95496aef7 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/list/CreateListPresenter.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/list/CreateListPresenter.kt @@ -2,9 +2,9 @@ package dev.dimension.flare.ui.presenter.list import androidx.compose.runtime.Composable import androidx.compose.runtime.Immutable -import dev.dimension.flare.data.datasource.microblog.ListDataSource -import dev.dimension.flare.data.datasource.microblog.ListMetaData -import dev.dimension.flare.data.datasource.microblog.ListMetaDataType +import dev.dimension.flare.data.datasource.microblog.list.ListDataSource +import dev.dimension.flare.data.datasource.microblog.list.ListMetaData +import dev.dimension.flare.data.datasource.microblog.list.ListMetaDataType import dev.dimension.flare.data.repository.AccountRepository import dev.dimension.flare.data.repository.accountServiceProvider import dev.dimension.flare.model.AccountType @@ -33,13 +33,13 @@ public class CreateListPresenter( override val supportedMetaData = serviceState.map { require(it is ListDataSource) - it.supportedMetaData + it.listHandler.supportedMetaData } override suspend fun createList(listMetaData: ListMetaData) { serviceState.onSuccess { require(it is ListDataSource) - it.createList(listMetaData) + it.listHandler.create(listMetaData) } } } diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/list/DeleteListPresenter.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/list/DeleteListPresenter.kt index 4a3da3a00..2a7f2fe8e 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/list/DeleteListPresenter.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/list/DeleteListPresenter.kt @@ -2,10 +2,11 @@ package dev.dimension.flare.ui.presenter.list import androidx.compose.runtime.Composable import androidx.compose.runtime.Immutable -import dev.dimension.flare.data.datasource.microblog.ListDataSource +import dev.dimension.flare.data.datasource.microblog.list.ListDataSource import dev.dimension.flare.data.repository.AccountRepository import dev.dimension.flare.data.repository.accountServiceFlow import dev.dimension.flare.model.AccountType +import dev.dimension.flare.model.MicroBlogKey import dev.dimension.flare.ui.presenter.PresenterBase import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.first @@ -19,7 +20,7 @@ import org.koin.core.component.inject */ public class DeleteListPresenter( private val accountType: AccountType, - private val listId: String, + private val listKey: MicroBlogKey, ) : PresenterBase(), KoinComponent { private val scope by inject() @@ -37,7 +38,8 @@ public class DeleteListPresenter( require(it is ListDataSource) it }.first() - .deleteList(listId) + .listHandler + .delete(listKey) } } } diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/list/EditAccountListPresenter.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/list/EditAccountListPresenter.kt index 8a34fd534..371f3b771 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/list/EditAccountListPresenter.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/list/EditAccountListPresenter.kt @@ -4,24 +4,20 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.Immutable import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope +import androidx.paging.cachedIn import androidx.paging.compose.collectAsLazyPagingItems import androidx.paging.filter import dev.dimension.flare.common.PagingState -import dev.dimension.flare.common.collectAsState import dev.dimension.flare.common.toPagingState -import dev.dimension.flare.data.datasource.microblog.ListDataSource +import dev.dimension.flare.data.datasource.microblog.list.ListDataSource import dev.dimension.flare.data.repository.AccountRepository import dev.dimension.flare.data.repository.accountServiceProvider import dev.dimension.flare.model.AccountType import dev.dimension.flare.model.MicroBlogKey import dev.dimension.flare.ui.model.UiList -import dev.dimension.flare.ui.model.UiState -import dev.dimension.flare.ui.model.flatMap import dev.dimension.flare.ui.model.map import dev.dimension.flare.ui.model.onSuccess -import dev.dimension.flare.ui.model.toUi import dev.dimension.flare.ui.presenter.PresenterBase -import kotlinx.collections.immutable.ImmutableList import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch import org.koin.core.component.KoinComponent @@ -47,7 +43,7 @@ public class EditAccountListPresenter( .map { service -> require(service is ListDataSource) remember(service) { - service.myList(scope = scope).map { + service.listHandler.data.cachedIn(scope).map { it.filter { !it.readonly } @@ -55,12 +51,13 @@ public class EditAccountListPresenter( }.collectAsLazyPagingItems() }.toPagingState() val userLists = - serviceState.flatMap { service -> - require(service is ListDataSource) - remember(service) { - service.userLists(userKey) - }.collectAsState().toUi() - } + serviceState + .map { service -> + require(service is ListDataSource) + remember(service) { + service.listMemberHandler.userLists(userKey).cachedIn(scope) + }.collectAsLazyPagingItems() + }.toPagingState() return object : EditAccountListState { override val lists = allList @@ -70,7 +67,7 @@ public class EditAccountListPresenter( serviceState.onSuccess { require(it is ListDataSource) scope.launch { - it.addMember(listId = list.id, userKey = userKey) + it.listMemberHandler.addMember(list.key, userKey = userKey) } } } @@ -79,7 +76,7 @@ public class EditAccountListPresenter( serviceState.onSuccess { require(it is ListDataSource) scope.launch { - it.removeMember(listId = list.id, userKey = userKey) + it.listMemberHandler.removeMember(list.key, userKey = userKey) } } } @@ -92,12 +89,12 @@ public interface EditAccountListState { /** * All lists. */ - public val lists: PagingState + public val lists: PagingState /** * Lists that the user is a member of. */ - public val userLists: UiState> + public val userLists: PagingState public fun addList(list: UiList) diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/list/EditListMemberPresenter.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/list/EditListMemberPresenter.kt index 62f925de5..1daaa9389 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/list/EditListMemberPresenter.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/list/EditListMemberPresenter.kt @@ -11,7 +11,7 @@ import androidx.paging.compose.collectAsLazyPagingItems import androidx.paging.map import dev.dimension.flare.common.PagingState import dev.dimension.flare.common.toPagingState -import dev.dimension.flare.data.datasource.microblog.ListDataSource +import dev.dimension.flare.data.datasource.microblog.list.ListDataSource import dev.dimension.flare.data.repository.AccountRepository import dev.dimension.flare.data.repository.accountServiceProvider import dev.dimension.flare.model.AccountType @@ -32,7 +32,7 @@ import org.koin.core.component.inject */ public class EditListMemberPresenter( private val accountType: AccountType, - private val listId: String, + private val listKey: MicroBlogKey, ) : PresenterBase(), KoinComponent { private val accountRepository: AccountRepository by inject() @@ -52,10 +52,11 @@ public class EditListMemberPresenter( require(service is ListDataSource) combine( service.searchUser(query = filter), - service.listMemberCache(listId), - ) { pagingData, cache -> - pagingData.map { user -> - user to cache.any { it.key == user.key } + service.listMemberHandler.listMembersListFlow(listKey), + ) { users, members -> + users.map { user -> + val isMember = members.any { it.key == user.key } + Pair(user, isMember) } } }.collectAsLazyPagingItems().let { @@ -74,7 +75,7 @@ public class EditListMemberPresenter( serviceState.onSuccess { scope.launch { require(it is ListDataSource) - it.addMember(listId, userKey) + it.listMemberHandler.addMember(listKey, userKey) } } } @@ -83,7 +84,7 @@ public class EditListMemberPresenter( serviceState.onSuccess { scope.launch { require(it is ListDataSource) - it.removeMember(listId, userKey) + it.listMemberHandler.removeMember(listKey, userKey) } } } diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/list/ListEditPresenter.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/list/ListEditPresenter.kt index 87b6e67bf..dd32222d6 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/list/ListEditPresenter.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/list/ListEditPresenter.kt @@ -5,12 +5,13 @@ import androidx.compose.runtime.Immutable import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import dev.dimension.flare.common.refreshSuspend -import dev.dimension.flare.data.datasource.microblog.ListDataSource -import dev.dimension.flare.data.datasource.microblog.ListMetaData -import dev.dimension.flare.data.datasource.microblog.ListMetaDataType +import dev.dimension.flare.data.datasource.microblog.list.ListDataSource +import dev.dimension.flare.data.datasource.microblog.list.ListMetaData +import dev.dimension.flare.data.datasource.microblog.list.ListMetaDataType import dev.dimension.flare.data.repository.AccountRepository import dev.dimension.flare.data.repository.accountServiceProvider import dev.dimension.flare.model.AccountType +import dev.dimension.flare.model.MicroBlogKey import dev.dimension.flare.ui.model.UiState import dev.dimension.flare.ui.model.map import dev.dimension.flare.ui.model.onSuccess @@ -25,7 +26,7 @@ import org.koin.core.component.inject */ public class ListEditPresenter( private val accountType: AccountType, - private val listId: String, + private val listKey: MicroBlogKey, ) : PresenterBase(), KoinComponent { @Immutable @@ -49,23 +50,23 @@ public class ListEditPresenter( val listInfoState = remember( accountType, - listId, + listKey, ) { - ListInfoPresenter(accountType, listId) + ListInfoPresenter(accountType, listKey) }.body() val state = remember( accountType, - listId, + listKey, ) { - EditListMemberPresenter(accountType, listId) + EditListMemberPresenter(accountType, listKey) }.body() val memberState = remember( accountType, - listId, + listKey, ) { - ListMembersPresenter(accountType, listId) + ListMembersPresenter(accountType, listKey) }.body() return object : State, @@ -75,7 +76,7 @@ public class ListEditPresenter( override val supportedMetaData = serviceState.map { require(it is ListDataSource) - it.supportedMetaData + it.listHandler.supportedMetaData } override fun refresh() { @@ -87,7 +88,7 @@ public class ListEditPresenter( override suspend fun updateList(listMetaData: ListMetaData) { serviceState.onSuccess { require(it is ListDataSource) - it.updateList(listId, listMetaData) + it.listHandler.update(listKey, listMetaData) } } } diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/list/ListInfoPresenter.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/list/ListInfoPresenter.kt index 91064dfd1..0752c5627 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/list/ListInfoPresenter.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/list/ListInfoPresenter.kt @@ -4,11 +4,11 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.Immutable import androidx.compose.runtime.remember import dev.dimension.flare.common.collectAsState -import dev.dimension.flare.data.datasource.microblog.ListDataSource import dev.dimension.flare.data.datasource.microblog.list.ListDataSource import dev.dimension.flare.data.repository.AccountRepository import dev.dimension.flare.data.repository.accountServiceProvider import dev.dimension.flare.model.AccountType +import dev.dimension.flare.model.MicroBlogKey import dev.dimension.flare.ui.model.UiList import dev.dimension.flare.ui.model.UiState import dev.dimension.flare.ui.model.flatMap @@ -22,7 +22,7 @@ import org.koin.core.component.inject */ public class ListInfoPresenter( private val accountType: AccountType, - private val listId: String, + private val listKey: MicroBlogKey, ) : PresenterBase(), KoinComponent { private val accountRepository: AccountRepository by inject() @@ -34,7 +34,7 @@ public class ListInfoPresenter( serviceState.flatMap { remember(it) { require(it is ListDataSource) - it.listInfo(listId) + it.listHandler.listInfo(listKey) }.collectAsState().toUi() } @@ -46,5 +46,5 @@ public class ListInfoPresenter( @Immutable public interface ListInfoState { - public val listInfo: UiState + public val listInfo: UiState } diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/list/ListMembersPresenter.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/list/ListMembersPresenter.kt index b7bc2b420..a2c4488d5 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/list/ListMembersPresenter.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/list/ListMembersPresenter.kt @@ -4,14 +4,16 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.Immutable import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope +import androidx.paging.cachedIn import androidx.paging.compose.collectAsLazyPagingItems import dev.dimension.flare.common.PagingState import dev.dimension.flare.common.toPagingState -import dev.dimension.flare.data.datasource.microblog.ListDataSource +import dev.dimension.flare.data.datasource.microblog.list.ListDataSource import dev.dimension.flare.data.repository.AccountRepository import dev.dimension.flare.data.repository.accountServiceProvider import dev.dimension.flare.model.AccountType -import dev.dimension.flare.ui.model.UiUserV2 +import dev.dimension.flare.model.MicroBlogKey +import dev.dimension.flare.ui.model.UiProfile import dev.dimension.flare.ui.model.map import dev.dimension.flare.ui.presenter.PresenterBase import org.koin.core.component.KoinComponent @@ -22,7 +24,7 @@ import org.koin.core.component.inject */ public class ListMembersPresenter( private val accountType: AccountType, - private val listId: String, + private val listKey: MicroBlogKey, ) : PresenterBase(), KoinComponent { private val accountRepository: AccountRepository by inject() @@ -34,9 +36,9 @@ public class ListMembersPresenter( val memberInfo = serviceState .map { - remember(it, listId) { + remember(it, listKey) { require(it is ListDataSource) - it.listMembers(listId, scope = scope) + it.listMemberHandler.listMembers(listKey).cachedIn(scope) }.collectAsLazyPagingItems() }.toPagingState() @@ -48,5 +50,5 @@ public class ListMembersPresenter( @Immutable public interface ListMembersState { - public val memberInfo: PagingState + public val memberInfo: PagingState } diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/list/ListTimelinePresenter.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/list/ListTimelinePresenter.kt index bc4b36a63..06e394b46 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/list/ListTimelinePresenter.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/list/ListTimelinePresenter.kt @@ -1,10 +1,11 @@ package dev.dimension.flare.ui.presenter.list -import dev.dimension.flare.data.datasource.microblog.ListDataSource +import dev.dimension.flare.data.datasource.microblog.list.ListDataSource import dev.dimension.flare.data.datasource.microblog.paging.BaseTimelineLoader import dev.dimension.flare.data.repository.AccountRepository import dev.dimension.flare.data.repository.accountServiceFlow import dev.dimension.flare.model.AccountType +import dev.dimension.flare.model.MicroBlogKey import dev.dimension.flare.ui.presenter.home.TimelinePresenter import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map @@ -16,7 +17,7 @@ import org.koin.core.component.inject */ public class ListTimelinePresenter( private val accountType: AccountType, - private val listId: String, + private val listKey: MicroBlogKey, ) : TimelinePresenter(), KoinComponent { private val accountRepository: AccountRepository by inject() @@ -27,7 +28,7 @@ public class ListTimelinePresenter( repository = accountRepository, ).map { service -> require(service is ListDataSource) - service.listTimeline(listId = listId) + service.listTimeline(listKey = listKey) } } } diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/list/PinnableTimelineTabPresenter.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/list/PinnableTimelineTabPresenter.kt index 8f981e562..3fd8a5c4c 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/list/PinnableTimelineTabPresenter.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/list/PinnableTimelineTabPresenter.kt @@ -4,6 +4,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.Immutable import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope +import androidx.paging.cachedIn import androidx.paging.compose.collectAsLazyPagingItems import dev.dimension.flare.common.ImmutableListWrapper import dev.dimension.flare.common.PagingState @@ -11,6 +12,7 @@ import dev.dimension.flare.common.toImmutableListWrapper import dev.dimension.flare.common.toPagingState import dev.dimension.flare.data.datasource.bluesky.BlueskyDataSource import dev.dimension.flare.data.datasource.microblog.ListDataSource +import dev.dimension.flare.data.datasource.microblog.list.ListDataSource import dev.dimension.flare.data.datasource.misskey.MisskeyDataSource import dev.dimension.flare.data.repository.AccountRepository import dev.dimension.flare.data.repository.accountServiceProvider @@ -70,7 +72,7 @@ public class PinnableTimelineTabPresenter( it as? ListDataSource }.map { service -> remember(service) { - service.myList(scope = scope) + service.listHandler.data.cachedIn(scope) }.collectAsLazyPagingItems() }.toPagingState() From 7d70278870f1fd111ea5ffe40c7689e034803317 Mon Sep 17 00:00:00 2001 From: Tlaster Date: Mon, 16 Feb 2026 16:07:07 +0900 Subject: [PATCH 04/14] migrate bluesky feed to list handler --- .../flare/data/database/cache/dao/ListDao.kt | 3 + .../datasource/bluesky/BlueskyDataSource.kt | 211 ++------------ .../datasource/bluesky/BlueskyFeedLoader.kt | 267 ++++++++++++++++++ .../datasource/microblog/list/ListHandler.kt | 40 +++ .../misskey/AntennasListPagingSource.kt | 32 +++ .../misskey/AntennasListRemoteMediator.kt | 31 -- .../datasource/misskey/MisskeyDataSource.kt | 18 +- .../xqt/ListTimelineRemoteMediator.kt | 10 +- .../home/bluesky/BlueskyFeedPresenter.kt | 6 +- .../home/bluesky/BlueskyFeedsPresenter.kt | 8 +- .../presenter/list/AntennasListPresenter.kt | 7 +- .../list/PinnableTimelineTabPresenter.kt | 21 +- .../ui/presenter/profile/ProfilePresenter.kt | 2 +- 13 files changed, 405 insertions(+), 251 deletions(-) create mode 100644 shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/bluesky/BlueskyFeedLoader.kt create mode 100644 shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/misskey/AntennasListPagingSource.kt delete mode 100644 shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/misskey/AntennasListRemoteMediator.kt diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/database/cache/dao/ListDao.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/database/cache/dao/ListDao.kt index a2f5d0338..04401b8c3 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/data/database/cache/dao/ListDao.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/data/database/cache/dao/ListDao.kt @@ -24,6 +24,9 @@ internal interface ListDao { ) fun getPagingSource(pagingKey: String): PagingSource + @Query("SELECT listKey FROM DbListPaging WHERE pagingKey = :pagingKey") + fun getListKeysFlow(pagingKey: String): Flow> + @Query("SELECT * FROM DbList WHERE listKey = :listKey AND accountType = :accountType") fun getList( listKey: MicroBlogKey, diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/bluesky/BlueskyDataSource.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/bluesky/BlueskyDataSource.kt index f64854624..b4e820e2f 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/bluesky/BlueskyDataSource.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/bluesky/BlueskyDataSource.kt @@ -8,16 +8,12 @@ import androidx.paging.cachedIn import androidx.paging.map import app.bsky.actor.GetProfileQueryParams import app.bsky.actor.PreferencesUnion -import app.bsky.actor.PutPreferencesRequest -import app.bsky.actor.SavedFeed -import app.bsky.actor.SavedFeedType import app.bsky.bookmark.CreateBookmarkRequest import app.bsky.bookmark.DeleteBookmarkRequest import app.bsky.embed.Images import app.bsky.embed.ImagesImage import app.bsky.embed.Record import app.bsky.feed.GetFeedGeneratorQueryParams -import app.bsky.feed.GetFeedGeneratorsQueryParams import app.bsky.feed.GetPostsQueryParams import app.bsky.feed.Post import app.bsky.feed.PostEmbedUnion @@ -110,6 +106,7 @@ import dev.dimension.flare.ui.model.mapper.render import dev.dimension.flare.ui.model.toUi import dev.dimension.flare.ui.presenter.compose.ComposeStatus import dev.dimension.flare.ui.presenter.status.action.BlueskyReportStatusState +import kotlin.time.Clock import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList @@ -133,8 +130,6 @@ import sh.christian.ozone.api.Nsid import sh.christian.ozone.api.RKey import sh.christian.ozone.api.model.JsonContent import sh.christian.ozone.api.model.JsonContent.Companion.encodeAsJsonContent -import kotlin.time.Clock -import kotlin.uuid.Uuid @OptIn(ExperimentalPagingApi::class) internal class BlueskyDataSource( @@ -1080,43 +1075,21 @@ internal class BlueskyDataSource( .orEmpty() } } - private val myFeedsKey = "my_feeds_$accountKey" - val myFeeds: CacheData> by lazy { - MemCacheable( - key = myFeedsKey, - ) { - val preferences = - service - .getPreferencesForActor() - .maybeResponse() - ?.preferences - .orEmpty() - val items = - preferences - .filterIsInstance() - .firstOrNull() - ?.value - ?.items - ?.filter { - it.type == SavedFeedType.Feed - }.orEmpty() - service - .getFeedGenerators( - GetFeedGeneratorsQueryParams( - feeds = - items - .map { AtUri(it.value) } - .toImmutableList(), - ), - ).maybeResponse() - ?.feeds - ?.map { - it.render(accountKey) - }.orEmpty() - .toImmutableList() - } + internal val feedLoader by lazy { + BlueskyFeedLoader( + service = service, + accountKey = accountKey, + ) + } + + val feedHandler by lazy { + ListHandler( + pagingKey = myFeedsKey, + accountKey = accountKey, + loader = feedLoader, + ) } fun popularFeeds( @@ -1156,10 +1129,10 @@ internal class BlueskyDataSource( .let { feeds -> combine( feeds, - MemCacheable.subscribe>(myFeedsKey), + database.listDao().getListKeysFlow(myFeedsKey), ) { popular, my -> popular.map { item -> - item to my.any { it.id == item.id } + item to my.any { it == item.key } } } }.cachedIn(scope) @@ -1203,153 +1176,29 @@ internal class BlueskyDataSource( ) suspend fun subscribeFeed(data: UiList.Feed) { - MemCacheable.updateWith>( - key = myFeedsKey, - ) { - (it + data).toImmutableList() - } tryRun { - val currentPreferences = service.getPreferencesForActor().requireResponse() - val feedInfo = - service - .getFeedGenerator(GetFeedGeneratorQueryParams(feed = AtUri(data.id))) - .requireResponse() - val newPreferences = currentPreferences.preferences.toMutableList() - val prefIndex = newPreferences.indexOfFirst { it is PreferencesUnion.SavedFeedsPref } - if (prefIndex != -1) { - val pref = newPreferences[prefIndex] as PreferencesUnion.SavedFeedsPref - val newPref = - pref.value.copy( - saved = (pref.value.saved + feedInfo.view.uri).toImmutableList(), - pinned = (pref.value.pinned + feedInfo.view.uri).toImmutableList(), - ) - newPreferences[prefIndex] = PreferencesUnion.SavedFeedsPref(newPref) - } - val prefV2Index = - newPreferences.indexOfFirst { it is PreferencesUnion.SavedFeedsPrefV2 } - if (prefV2Index != -1) { - val pref = newPreferences[prefV2Index] as PreferencesUnion.SavedFeedsPrefV2 - val newPref = - pref.value.copy( - items = - ( - pref.value.items + - SavedFeed( - type = SavedFeedType.Feed, - value = feedInfo.view.uri.atUri, - pinned = true, - id = Uuid.random().toString(), - ) - ).toImmutableList(), - ) - newPreferences[prefV2Index] = PreferencesUnion.SavedFeedsPrefV2(newPref) - } - - service.putPreferences( - request = - PutPreferencesRequest( - preferences = newPreferences.toImmutableList(), - ), - ) - myFeeds.refresh() - }.onFailure { - MemCacheable.updateWith>( - key = myFeedsKey, - ) { - it.filterNot { item -> item.id == data.id }.toImmutableList() - } + feedLoader.subscribe(data.key) + feedHandler.insertToDatabase(data) } } suspend fun unsubscribeFeed(data: UiList.Feed) { - MemCacheable.updateWith>( - key = myFeedsKey, - ) { - it.filterNot { item -> item.id == data.id }.toImmutableList() - } - tryRun { - val currentPreferences = service.getPreferencesForActor().requireResponse() - val feedInfo = - service - .getFeedGenerator(GetFeedGeneratorQueryParams(feed = AtUri(data.id))) - .requireResponse() - val newPreferences = currentPreferences.preferences.toMutableList() - val prefIndex = newPreferences.indexOfFirst { it is PreferencesUnion.SavedFeedsPref } - if (prefIndex != -1) { - val pref = newPreferences[prefIndex] as PreferencesUnion.SavedFeedsPref - val newPref = - pref.value.copy( - saved = - pref.value.saved - .filterNot { it == feedInfo.view.uri } - .toImmutableList(), - pinned = - pref.value.pinned - .filterNot { it == feedInfo.view.uri } - .toImmutableList(), - ) - newPreferences[prefIndex] = PreferencesUnion.SavedFeedsPref(newPref) - } - val prefV2Index = - newPreferences.indexOfFirst { it is PreferencesUnion.SavedFeedsPrefV2 } - if (prefV2Index != -1) { - val pref = newPreferences[prefV2Index] as PreferencesUnion.SavedFeedsPrefV2 - val newPref = - pref.value.copy( - items = - pref.value.items - .filterNot { it.value == feedInfo.view.uri.atUri } - .toImmutableList(), - ) - newPreferences[prefV2Index] = PreferencesUnion.SavedFeedsPrefV2(newPref) - } - service.putPreferences( - request = - PutPreferencesRequest( - preferences = newPreferences.toImmutableList(), - ), - ) - myFeeds.refresh() - }.onFailure { - MemCacheable.updateWith>( - key = myFeedsKey, - ) { - (it + data).toImmutableList() - } - } + feedHandler.delete(data.key) } suspend fun favouriteFeed(data: UiList.Feed) { - MemCacheable.update( - key = feedInfoKey(data.id), - value = - data.copy( - liked = !data.liked, - ), - ) - - tryRun { - val feedInfo = - service - .getFeedGenerator(GetFeedGeneratorQueryParams(feed = AtUri(data.id))) - .requireResponse() - val likedUri = - feedInfo.view.viewer - ?.like - ?.atUri - if (likedUri != null) { - deleteLikeRecord(likedUri) - } else { - createLikeRecord(cid = feedInfo.view.cid.cid, uri = feedInfo.view.uri.atUri) + feedHandler.withDatabase { updataCallback -> + val newData = data.copy(liked = !data.liked) + updataCallback(newData) + tryRun { + if (newData.liked) { + feedLoader.favourite(data.key) + } else { + feedLoader.unfavourite(data.key) + } + }.onFailure { + updataCallback(data) } - }.onFailure { - MemCacheable.update( - key = feedInfoKey(data.id), - value = - data.copy( - liked = data.liked, - ), - ) } } diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/bluesky/BlueskyFeedLoader.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/bluesky/BlueskyFeedLoader.kt new file mode 100644 index 000000000..228834a5d --- /dev/null +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/bluesky/BlueskyFeedLoader.kt @@ -0,0 +1,267 @@ +package dev.dimension.flare.data.datasource.bluesky + +import app.bsky.actor.PreferencesUnion +import app.bsky.actor.PutPreferencesRequest +import app.bsky.actor.SavedFeed +import app.bsky.actor.SavedFeedType +import app.bsky.feed.GetFeedGeneratorQueryParams +import app.bsky.feed.GetFeedGeneratorsQueryParams +import com.atproto.repo.CreateRecordRequest +import com.atproto.repo.DeleteRecordRequest +import com.atproto.repo.StrongRef +import dev.dimension.flare.data.datasource.microblog.list.ListLoader +import dev.dimension.flare.data.datasource.microblog.list.ListMetaData +import dev.dimension.flare.data.datasource.microblog.list.ListMetaDataType +import dev.dimension.flare.data.datasource.microblog.paging.PagingRequest +import dev.dimension.flare.data.datasource.microblog.paging.PagingResult +import dev.dimension.flare.data.network.bluesky.BlueskyService +import dev.dimension.flare.model.MicroBlogKey +import dev.dimension.flare.ui.model.UiList +import dev.dimension.flare.ui.model.mapper.render +import kotlin.time.Clock +import kotlin.uuid.Uuid +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toImmutableList +import sh.christian.ozone.api.AtUri +import sh.christian.ozone.api.Cid +import sh.christian.ozone.api.Did +import sh.christian.ozone.api.Nsid +import sh.christian.ozone.api.RKey + +internal class BlueskyFeedLoader( + private val service: BlueskyService, + private val accountKey: MicroBlogKey, +) : ListLoader { + override suspend fun load( + pageSize: Int, + request: PagingRequest, + ): PagingResult { + val cursor = + when (request) { + is PagingRequest.Append -> request.nextKey + is PagingRequest.Refresh -> null + is PagingRequest.Prepend -> return PagingResult() + } + + // Feed loading usually doesn't support pagination in the same way lists do via preferences + // But we can implement a basic fetch. + // Bluesky preferences don't really support pagination for saved feeds. + // We'll load all if cursor is null, otherwise return empty (end of list). + if (cursor != null) { + return PagingResult( + data = persistentListOf(), + nextKey = null, + ) + } + + val preferences = + service + .getPreferencesForActor() + .requireResponse() + .preferences + + val items = + preferences + .filterIsInstance() + .firstOrNull() + ?.value + ?.items + ?.filter { + it.type == SavedFeedType.Feed + }.orEmpty() + + val feeds = service + .getFeedGenerators( + GetFeedGeneratorsQueryParams( + feeds = + items + .map { AtUri(it.value) } + .toImmutableList(), + ), + ).requireResponse() + .feeds + .map { + it.render(accountKey) + }.toImmutableList() + + return PagingResult( + data = feeds, + nextKey = null, + ) + } + + override suspend fun info(listKey: MicroBlogKey): UiList = + service + .getFeedGenerator( + GetFeedGeneratorQueryParams( + feed = AtUri(listKey.id), + ), + ).requireResponse() + .view + .render(accountKey) + + override val supportedMetaData: ImmutableList + get() = persistentListOf() + + override suspend fun create(metaData: ListMetaData): UiList = + throw UnsupportedOperationException("Create feed is not supported") + + override suspend fun update( + listKey: MicroBlogKey, + metaData: ListMetaData, + ): UiList = throw UnsupportedOperationException("Update feed is not supported") + + override suspend fun delete(listKey: MicroBlogKey) { + val currentPreferences = service.getPreferencesForActor().requireResponse() + val feedInfo = + service + .getFeedGenerator(GetFeedGeneratorQueryParams(feed = AtUri(listKey.id))) + .requireResponse() + val newPreferences = currentPreferences.preferences.toMutableList() + val prefIndex = newPreferences.indexOfFirst { it is PreferencesUnion.SavedFeedsPref } + if (prefIndex != -1) { + val pref = newPreferences[prefIndex] as PreferencesUnion.SavedFeedsPref + val newPref = + pref.value.copy( + saved = + pref.value.saved + .filterNot { it == feedInfo.view.uri } + .toImmutableList(), + pinned = + pref.value.pinned + .filterNot { it == feedInfo.view.uri } + .toImmutableList(), + ) + newPreferences[prefIndex] = PreferencesUnion.SavedFeedsPref(newPref) + } + val prefV2Index = + newPreferences.indexOfFirst { it is PreferencesUnion.SavedFeedsPrefV2 } + if (prefV2Index != -1) { + val pref = newPreferences[prefV2Index] as PreferencesUnion.SavedFeedsPrefV2 + val newPref = + pref.value.copy( + items = + pref.value.items + .filterNot { it.value == feedInfo.view.uri.atUri } + .toImmutableList(), + ) + newPreferences[prefV2Index] = PreferencesUnion.SavedFeedsPrefV2(newPref) + } + service.putPreferences( + request = + PutPreferencesRequest( + preferences = newPreferences.toImmutableList(), + ), + ) + } + + suspend fun subscribe(feedKey: MicroBlogKey) { + val currentPreferences = service.getPreferencesForActor().requireResponse() + val feedInfo = + service + .getFeedGenerator(GetFeedGeneratorQueryParams(feed = AtUri(feedKey.id))) + .requireResponse() + val newPreferences = currentPreferences.preferences.toMutableList() + val prefIndex = newPreferences.indexOfFirst { it is PreferencesUnion.SavedFeedsPref } + if (prefIndex != -1) { + val pref = newPreferences[prefIndex] as PreferencesUnion.SavedFeedsPref + val newPref = + pref.value.copy( + saved = (pref.value.saved + feedInfo.view.uri).toImmutableList(), + pinned = (pref.value.pinned + feedInfo.view.uri).toImmutableList(), + ) + newPreferences[prefIndex] = PreferencesUnion.SavedFeedsPref(newPref) + } + val prefV2Index = + newPreferences.indexOfFirst { it is PreferencesUnion.SavedFeedsPrefV2 } + if (prefV2Index != -1) { + val pref = newPreferences[prefV2Index] as PreferencesUnion.SavedFeedsPrefV2 + val newPref = + pref.value.copy( + items = + ( + pref.value.items + + SavedFeed( + type = SavedFeedType.Feed, + value = feedInfo.view.uri.atUri, + pinned = true, + id = Uuid.random().toString(), + ) + ).toImmutableList(), + ) + newPreferences[prefV2Index] = PreferencesUnion.SavedFeedsPrefV2(newPref) + } + + service.putPreferences( + request = + PutPreferencesRequest( + preferences = newPreferences.toImmutableList(), + ), + ) + } + + suspend fun favourite(feedKey: MicroBlogKey) { + val feedInfo = + service + .getFeedGenerator(GetFeedGeneratorQueryParams(feed = AtUri(feedKey.id))) + .requireResponse() + val likedUri = + feedInfo.view.viewer + ?.like + ?.atUri + + // If already liked, technically we shouldn't be here if called correctly, or we treat as idempotent? + // User request said "favourite" and "unfavourite" separate methods. + if (likedUri == null) { + createLikeRecord(cid = feedInfo.view.cid.cid, uri = feedInfo.view.uri.atUri) + } + } + + suspend fun unfavourite(feedKey: MicroBlogKey) { + val feedInfo = + service + .getFeedGenerator(GetFeedGeneratorQueryParams(feed = AtUri(feedKey.id))) + .requireResponse() + val likedUri = + feedInfo.view.viewer + ?.like + ?.atUri + + if (likedUri != null) { + deleteLikeRecord(likedUri) + } + } + + private suspend fun createLikeRecord( + cid: String, + uri: String, + ) { + service + .createRecord( + CreateRecordRequest( + repo = Did(did = accountKey.id), + collection = Nsid("app.bsky.feed.like"), + record = + app.bsky.feed + .Like( + subject = + StrongRef( + uri = AtUri(uri), + cid = Cid(cid), + ), + createdAt = Clock.System.now(), + ).bskyJson(), + ), + ).requireResponse() + } + + private suspend fun deleteLikeRecord(likedUri: String) = + service.deleteRecord( + DeleteRecordRequest( + repo = Did(did = accountKey.id), + collection = Nsid("app.bsky.feed.like"), + rkey = RKey(likedUri.substringAfterLast('/')), + ), + ) +} diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/microblog/list/ListHandler.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/microblog/list/ListHandler.kt index d225d3995..a58ed0883 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/microblog/list/ListHandler.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/microblog/list/ListHandler.kt @@ -167,4 +167,44 @@ internal class ListHandler( } } } + + suspend fun insertToDatabase( + data: UiList, + ) { + database.connect { + database.listDao().insertAll( + listOf( + DbList( + listKey = data.key, + accountType = AccountType.Specific(accountKey), + content = DbList.ListContent(data), + ), + ), + ) + + database.listDao().insertAll( + listOf( + DbListPaging( + accountType = AccountType.Specific(accountKey), + pagingKey = pagingKey, + listKey = data.key, + ), + ), + ) + } + } + + suspend fun withDatabase( + block: suspend (update: suspend (UiList) -> Unit) -> Unit, + ) { + block.invoke { data -> + database.connect { + database.listDao().updateListContent( + listKey = data.key, + accountType = AccountType.Specific(accountKey), + content = DbList.ListContent(data), + ) + } + } + } } diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/misskey/AntennasListPagingSource.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/misskey/AntennasListPagingSource.kt new file mode 100644 index 000000000..7f0c8c90d --- /dev/null +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/misskey/AntennasListPagingSource.kt @@ -0,0 +1,32 @@ +package dev.dimension.flare.data.datasource.misskey + +import androidx.paging.PagingState +import dev.dimension.flare.common.BasePagingSource +import dev.dimension.flare.data.network.misskey.MisskeyService +import dev.dimension.flare.data.repository.tryRun +import dev.dimension.flare.ui.model.UiList +import dev.dimension.flare.ui.model.mapper.render + +internal class AntennasListPagingSource( + private val service: MisskeyService, +) : BasePagingSource() { + override suspend fun doLoad(params: LoadParams): LoadResult = + tryRun { + service.antennasList().map { + it.render() + } + }.fold( + onSuccess = { antennas -> + LoadResult.Page( + data = antennas, + prevKey = null, + nextKey = null, + ) + }, + onFailure = { error -> + LoadResult.Error(error) + }, + ) + + override fun getRefreshKey(state: PagingState): Int? = null +} \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/misskey/AntennasListRemoteMediator.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/misskey/AntennasListRemoteMediator.kt deleted file mode 100644 index 0c7ffab1e..000000000 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/misskey/AntennasListRemoteMediator.kt +++ /dev/null @@ -1,31 +0,0 @@ -package dev.dimension.flare.data.datasource.misskey - -import dev.dimension.flare.data.database.cache.CacheDatabase -import dev.dimension.flare.data.datasource.microblog.BaseListRemoteMediator -import dev.dimension.flare.data.network.misskey.MisskeyService -import dev.dimension.flare.model.AccountType -import dev.dimension.flare.model.MicroBlogKey -import dev.dimension.flare.ui.model.mapper.render - -internal class AntennasListRemoteMediator( - private val service: MisskeyService, - database: CacheDatabase, - accountKey: MicroBlogKey, -) : BaseListRemoteMediator(database) { - override val accountType = AccountType.Specific(accountKey) - override val pagingKey = "antennas_list_$accountKey" - - override suspend fun load( - pageSize: Int, - request: Request, - ): Result = - service - .antennasList() - .map { - it.render() - }.let { antennas -> - PagingResult( - data = antennas, - ) - } -} diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/misskey/MisskeyDataSource.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/misskey/MisskeyDataSource.kt index 2befa94bd..28547e107 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/misskey/MisskeyDataSource.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/misskey/MisskeyDataSource.kt @@ -1122,28 +1122,14 @@ internal class MisskeyDataSource( } } - fun antennasList(scope: CoroutineScope): Flow> = + fun antennasList(): Flow> = Pager( config = pagingConfig, ) { AntennasListPagingSource( service = service, ) - }.flow.cachedIn(scope) - - fun antennasTimeline( - id: String, - scope: CoroutineScope, - pageSize: Int = 20, - ): Flow> = - timelinePager( - pageSize = pageSize, - database = database, - scope = scope, - filterFlow = localFilterRepository.getFlow(forTimeline = true), - accountRepository = accountRepository, - mediator = antennasTimelineLoader(id), - ) + }.flow fun antennasTimelineLoader(id: String) = AntennasTimelineRemoteMediator( diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/xqt/ListTimelineRemoteMediator.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/xqt/ListTimelineRemoteMediator.kt index e73e80576..896f825ee 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/xqt/ListTimelineRemoteMediator.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/xqt/ListTimelineRemoteMediator.kt @@ -6,8 +6,10 @@ import dev.dimension.flare.data.database.cache.CacheDatabase import dev.dimension.flare.data.database.cache.mapper.cursor import dev.dimension.flare.data.database.cache.mapper.toDbPagingTimeline import dev.dimension.flare.data.database.cache.mapper.tweets +import dev.dimension.flare.data.database.cache.model.DbPagingTimelineWithStatus import dev.dimension.flare.data.datasource.microblog.paging.BaseTimelineRemoteMediator -import dev.dimension.flare.data.datasource.microblog.paging.BaseTimelineRemoteMediator.Request +import dev.dimension.flare.data.datasource.microblog.paging.PagingRequest +import dev.dimension.flare.data.datasource.microblog.paging.PagingResult import dev.dimension.flare.data.network.xqt.XQTService import dev.dimension.flare.model.MicroBlogKey import kotlinx.serialization.SerialName @@ -34,11 +36,11 @@ internal class ListTimelineRemoteMediator( override suspend fun timeline( pageSize: Int, - request: BaseTimelineRemoteMediator.Request, - ): Result { + request: PagingRequest, + ): PagingResult { val response = when (request) { - BaseTimelineRemoteMediator.PagingRequest.Refresh -> { + PagingRequest.Refresh -> { service .getListLatestTweetsTimeline( variables = diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/home/bluesky/BlueskyFeedPresenter.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/home/bluesky/BlueskyFeedPresenter.kt index b89a9a1f5..c781a40fd 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/home/bluesky/BlueskyFeedPresenter.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/home/bluesky/BlueskyFeedPresenter.kt @@ -2,8 +2,10 @@ package dev.dimension.flare.ui.presenter.home.bluesky import androidx.compose.runtime.Composable import androidx.compose.runtime.Immutable +import androidx.compose.runtime.collectAsState import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope +import androidx.paging.cachedIn import androidx.paging.compose.collectAsLazyPagingItems import dev.dimension.flare.common.PagingState import dev.dimension.flare.common.collectAsState @@ -56,8 +58,8 @@ public class BlueskyFeedPresenter( .flatMap { require(it is BlueskyDataSource) remember(it) { - it.myFeeds - }.collectAsState().toUi() + it.feedHandler.data.cachedIn(scope) + }.collectAsLazyPagingItems() }.map { it.any { it.id == uri } } diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/home/bluesky/BlueskyFeedsPresenter.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/home/bluesky/BlueskyFeedsPresenter.kt index 5daccd7ad..57252652a 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/home/bluesky/BlueskyFeedsPresenter.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/home/bluesky/BlueskyFeedsPresenter.kt @@ -7,6 +7,7 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue +import androidx.paging.cachedIn import androidx.paging.compose.collectAsLazyPagingItems import dev.dimension.flare.common.PagingState import dev.dimension.flare.common.refreshSuspend @@ -38,9 +39,10 @@ public class BlueskyFeedsPresenter( serviceState .map { service -> require(service is BlueskyDataSource) - remember(service) { - service.myFeeds + val flow = remember(service) { + service.feedHandler.data.cachedIn(scope) } + flow.collectAsLazyPagingItems() }.toPagingState() val popularFeeds = serviceState @@ -87,7 +89,7 @@ public class BlueskyFeedsPresenter( @Immutable public interface BlueskyFeedsState { - public val myFeeds: PagingState + public val myFeeds: PagingState public val popularFeeds: PagingState> public fun search(value: String) diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/list/AntennasListPresenter.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/list/AntennasListPresenter.kt index f9bdb137a..bb05e58f1 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/list/AntennasListPresenter.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/list/AntennasListPresenter.kt @@ -3,6 +3,7 @@ package dev.dimension.flare.ui.presenter.list import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope +import androidx.paging.cachedIn import androidx.paging.compose.collectAsLazyPagingItems import dev.dimension.flare.common.PagingState import dev.dimension.flare.common.refreshSuspend @@ -26,7 +27,7 @@ public class AntennasListPresenter( @androidx.compose.runtime.Immutable public interface State { - public val data: PagingState + public val data: PagingState public fun refresh() @@ -42,11 +43,11 @@ public class AntennasListPresenter( .map { remember { require(it is MisskeyDataSource) - it.antennasList(scope) + it.antennasList().cachedIn(scope) }.collectAsLazyPagingItems() }.toPagingState() return object : State { - override val data: PagingState = data + override val data: PagingState = data override fun refresh() { scope.launch { diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/list/PinnableTimelineTabPresenter.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/list/PinnableTimelineTabPresenter.kt index 3fd8a5c4c..670c2f371 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/list/PinnableTimelineTabPresenter.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/list/PinnableTimelineTabPresenter.kt @@ -6,6 +6,7 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.paging.cachedIn import androidx.paging.compose.collectAsLazyPagingItems +import androidx.paging.map import dev.dimension.flare.common.ImmutableListWrapper import dev.dimension.flare.common.PagingState import dev.dimension.flare.common.toImmutableListWrapper @@ -23,6 +24,7 @@ import dev.dimension.flare.ui.model.map import dev.dimension.flare.ui.model.mapNotNull import dev.dimension.flare.ui.presenter.PresenterBase import kotlinx.collections.immutable.toImmutableList +import kotlinx.coroutines.flow.map import org.koin.core.component.KoinComponent import org.koin.core.component.inject @@ -41,19 +43,19 @@ public class PinnableTimelineTabPresenter( public val data: PagingState public data class List( - override val data: PagingState, + override val data: PagingState, ) : Tab public data class Feed( - override val data: PagingState, + override val data: PagingState, ) : Tab public data class Antenna( - override val data: PagingState, + override val data: PagingState, ) : Tab public data class Channel( - override val data: PagingState, + override val data: PagingState, ) : Tab } @@ -75,15 +77,14 @@ public class PinnableTimelineTabPresenter( service.listHandler.data.cachedIn(scope) }.collectAsLazyPagingItems() }.toPagingState() - - val feeds = - serviceState + val feeds = serviceState .mapNotNull { it as? BlueskyDataSource }.mapNotNull { service -> - remember(service) { - service.myFeeds + val flow = remember(service) { + service.feedHandler.data.cachedIn(scope) } + flow.collectAsLazyPagingItems() }.toPagingState() val antenna = @@ -92,7 +93,7 @@ public class PinnableTimelineTabPresenter( it as? MisskeyDataSource }.mapNotNull { service -> remember(service) { - service.antennasList(scope = scope) + service.antennasList().cachedIn(scope) }.collectAsLazyPagingItems() }.toPagingState() diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/profile/ProfilePresenter.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/profile/ProfilePresenter.kt index 51a217e04..e7d6172c2 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/profile/ProfilePresenter.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/profile/ProfilePresenter.kt @@ -9,9 +9,9 @@ import dev.dimension.flare.common.collectAsState import dev.dimension.flare.data.datasource.microblog.ActionMenu import dev.dimension.flare.data.datasource.microblog.AuthenticatedMicroblogDataSource import dev.dimension.flare.data.datasource.microblog.DirectMessageDataSource -import dev.dimension.flare.data.datasource.microblog.ListDataSource import dev.dimension.flare.data.datasource.microblog.ProfileAction import dev.dimension.flare.data.datasource.microblog.ProfileTab +import dev.dimension.flare.data.datasource.microblog.list.ListDataSource import dev.dimension.flare.data.datasource.microblog.paging.BaseTimelineLoader import dev.dimension.flare.data.repository.AccountRepository import dev.dimension.flare.data.repository.NoActiveAccountException From 2481d0cccb97873af22be4f29c51d71a5a58e728 Mon Sep 17 00:00:00 2001 From: Tlaster Date: Mon, 16 Feb 2026 18:27:01 +0900 Subject: [PATCH 05/14] update lists usage --- .../flare/ui/screen/list/CreateListDialog.kt | 4 +- .../ui/screen/list/EditAccountListScreen.kt | 2 + .../flare/ui/screen/list/EditListScreen.kt | 4 +- .../ui/screen/settings/TabAddBottomSheet.kt | 3 + app/src/main/res/values/strings.xml | 1 + .../bluesky/BlueskyFeedsWithTabsPresenter.kt | 12 +-- .../screen/list/AllListWithTabsPresenter.kt | 12 +-- .../MisskeyAntennasListWithTabsPresenter.kt | 10 +-- .../main/composeResources/values/strings.xml | 2 + .../flare/ui/screen/home/AddTabDialog.kt | 4 + .../flare/data/database/cache/dao/ListDao.kt | 11 +-- .../datasource/bluesky/BlueskyDataSource.kt | 63 +++------------- .../datasource/bluesky/BlueskyFeedLoader.kt | 74 +++++++++---------- .../datasource/bluesky/BlueskyListLoader.kt | 14 ++-- .../bluesky/BlueskyListMemberLoader.kt | 14 ++-- .../datasource/mastodon/MastodonDataSource.kt | 4 +- .../datasource/mastodon/MastodonListLoader.kt | 14 ++-- .../mastodon/MastodonListMemberLoader.kt | 14 ++-- .../microblog/list/ListDataSource.kt | 3 +- .../datasource/microblog/list/ListHandler.kt | 68 +++++++++-------- .../datasource/microblog/list/ListLoader.kt | 7 +- .../microblog/list/ListMemberHandler.kt | 33 +++++---- .../microblog/list/ListMemberLoader.kt | 6 +- .../misskey/AntennasListPagingSource.kt | 2 +- .../datasource/misskey/MisskeyDataSource.kt | 4 +- .../datasource/misskey/MisskeyListLoader.kt | 14 ++-- .../misskey/MisskeyListMemberLoader.kt | 12 +-- .../data/datasource/xqt/XQTDataSource.kt | 4 +- .../data/datasource/xqt/XQTListLoader.kt | 16 ++-- .../datasource/xqt/XQTListMemberLoader.kt | 12 +-- .../api/model/ChannelsFeaturedRequest.kt | 2 +- .../elonmusk114514/ElonMusk1145141919810.kt | 2 +- .../dev/dimension/flare/ui/model/UiList.kt | 11 ++- .../flare/ui/model/mapper/Mastodon.kt | 8 ++ .../dimension/flare/ui/model/mapper/VVO.kt | 8 -- .../home/bluesky/BlueskyFeedPresenter.kt | 69 +++++++++-------- .../home/bluesky/BlueskyFeedsPresenter.kt | 7 +- .../ui/presenter/list/DeleteListPresenter.kt | 5 +- .../list/EditAccountListPresenter.kt | 4 +- .../presenter/list/EditListMemberPresenter.kt | 8 +- .../ui/presenter/list/ListEditPresenter.kt | 17 ++--- .../ui/presenter/list/ListInfoPresenter.kt | 5 +- .../ui/presenter/list/ListMembersPresenter.kt | 7 +- .../presenter/list/ListTimelinePresenter.kt | 5 +- .../list/PinnableTimelineTabPresenter.kt | 41 +++++----- 45 files changed, 314 insertions(+), 328 deletions(-) diff --git a/app/src/main/java/dev/dimension/flare/ui/screen/list/CreateListDialog.kt b/app/src/main/java/dev/dimension/flare/ui/screen/list/CreateListDialog.kt index 840fea08e..ccecece83 100644 --- a/app/src/main/java/dev/dimension/flare/ui/screen/list/CreateListDialog.kt +++ b/app/src/main/java/dev/dimension/flare/ui/screen/list/CreateListDialog.kt @@ -38,8 +38,8 @@ import compose.icons.fontawesomeicons.Solid import compose.icons.fontawesomeicons.solid.Rss import dev.dimension.flare.R import dev.dimension.flare.common.FileItem -import dev.dimension.flare.data.datasource.microblog.ListMetaData -import dev.dimension.flare.data.datasource.microblog.ListMetaDataType +import dev.dimension.flare.data.datasource.microblog.list.ListMetaData +import dev.dimension.flare.data.datasource.microblog.list.ListMetaDataType import dev.dimension.flare.model.AccountType import dev.dimension.flare.ui.component.AvatarComponentDefaults import dev.dimension.flare.ui.component.FAIcon diff --git a/app/src/main/java/dev/dimension/flare/ui/screen/list/EditAccountListScreen.kt b/app/src/main/java/dev/dimension/flare/ui/screen/list/EditAccountListScreen.kt index 0e90837b5..88aedb69c 100644 --- a/app/src/main/java/dev/dimension/flare/ui/screen/list/EditAccountListScreen.kt +++ b/app/src/main/java/dev/dimension/flare/ui/screen/list/EditAccountListScreen.kt @@ -18,6 +18,8 @@ import compose.icons.fontawesomeicons.Solid import compose.icons.fontawesomeicons.solid.Plus import compose.icons.fontawesomeicons.solid.Trash import dev.dimension.flare.R +import dev.dimension.flare.common.onLoading +import dev.dimension.flare.common.onSuccess import dev.dimension.flare.model.AccountType import dev.dimension.flare.model.MicroBlogKey import dev.dimension.flare.ui.component.BackButton diff --git a/app/src/main/java/dev/dimension/flare/ui/screen/list/EditListScreen.kt b/app/src/main/java/dev/dimension/flare/ui/screen/list/EditListScreen.kt index 61d5f5dfc..1d0246ff2 100644 --- a/app/src/main/java/dev/dimension/flare/ui/screen/list/EditListScreen.kt +++ b/app/src/main/java/dev/dimension/flare/ui/screen/list/EditListScreen.kt @@ -53,8 +53,8 @@ import dev.dimension.flare.common.onEmpty import dev.dimension.flare.common.onError import dev.dimension.flare.common.onLoading import dev.dimension.flare.common.onSuccess -import dev.dimension.flare.data.datasource.microblog.ListMetaData -import dev.dimension.flare.data.datasource.microblog.ListMetaDataType +import dev.dimension.flare.data.datasource.microblog.list.ListMetaData +import dev.dimension.flare.data.datasource.microblog.list.ListMetaDataType import dev.dimension.flare.model.AccountType import dev.dimension.flare.ui.component.AvatarComponentDefaults import dev.dimension.flare.ui.component.BackButton diff --git a/app/src/main/java/dev/dimension/flare/ui/screen/settings/TabAddBottomSheet.kt b/app/src/main/java/dev/dimension/flare/ui/screen/settings/TabAddBottomSheet.kt index 10d4b6318..7167bc5d0 100644 --- a/app/src/main/java/dev/dimension/flare/ui/screen/settings/TabAddBottomSheet.kt +++ b/app/src/main/java/dev/dimension/flare/ui/screen/settings/TabAddBottomSheet.kt @@ -238,6 +238,9 @@ internal fun TabAddBottomSheet( is PinnableTimelineTabPresenter.State.Tab.Antenna -> R.string.home_tab_antennas_title + + is PinnableTimelineTabPresenter.State.Tab.Channel -> + R.string.channel_title } }.map { stringResource(id = it) } ButtonGroup( diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 1e196702e..ed7905e45 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -419,4 +419,5 @@ Group name New Group Theme + Channel diff --git a/compose-ui/src/commonMain/kotlin/dev/dimension/flare/ui/screen/bluesky/BlueskyFeedsWithTabsPresenter.kt b/compose-ui/src/commonMain/kotlin/dev/dimension/flare/ui/screen/bluesky/BlueskyFeedsWithTabsPresenter.kt index 9017b36cb..f62711fc3 100644 --- a/compose-ui/src/commonMain/kotlin/dev/dimension/flare/ui/screen/bluesky/BlueskyFeedsWithTabsPresenter.kt +++ b/compose-ui/src/commonMain/kotlin/dev/dimension/flare/ui/screen/bluesky/BlueskyFeedsWithTabsPresenter.kt @@ -24,10 +24,10 @@ public class BlueskyFeedsWithTabsPresenter( private val accountType: AccountType, ) : PresenterBase() { private val pinTabsPresenter by lazy { - object : PinTabsPresenter() { + object : PinTabsPresenter() { override fun List.filterPinned(): List = filterIsInstance().map { it.uri } - override fun getTimelineTabItem(item: UiList.Feed): TimelineTabItem = + override fun getTimelineTabItem(item: UiList): TimelineTabItem = Bluesky.FeedTabItem( account = accountType, uri = item.id, @@ -35,13 +35,13 @@ public class BlueskyFeedsWithTabsPresenter( TabMetaData( title = TitleType.Text(item.title), icon = - item.avatar?.let { + item.let { it as? UiList.Feed }?.avatar?.let { IconType.Url(it) } ?: IconType.Material(IconType.Material.MaterialIcon.Feeds), ), ) - override fun List.filter(item: UiList.Feed): List = + override fun List.filter(item: UiList): List = filter { if (it is Bluesky.FeedTabItem) { it.uri != item.id @@ -61,7 +61,7 @@ public class BlueskyFeedsWithTabsPresenter( BlueskyFeedsPresenter(accountType = accountType) }.invoke() val tabState = pinTabsPresenter.invoke() - return object : State, BlueskyFeedsState by state, PinTabsPresenter.State by tabState { + return object : State, BlueskyFeedsState by state, PinTabsPresenter.State by tabState { override val isRefreshing: Boolean get() = isRefreshing @@ -77,7 +77,7 @@ public class BlueskyFeedsWithTabsPresenter( public interface State : BlueskyFeedsState, - PinTabsPresenter.State { + PinTabsPresenter.State { public val isRefreshing: Boolean public fun refresh() diff --git a/compose-ui/src/commonMain/kotlin/dev/dimension/flare/ui/screen/list/AllListWithTabsPresenter.kt b/compose-ui/src/commonMain/kotlin/dev/dimension/flare/ui/screen/list/AllListWithTabsPresenter.kt index ee4fefa05..6d2721ac5 100644 --- a/compose-ui/src/commonMain/kotlin/dev/dimension/flare/ui/screen/list/AllListWithTabsPresenter.kt +++ b/compose-ui/src/commonMain/kotlin/dev/dimension/flare/ui/screen/list/AllListWithTabsPresenter.kt @@ -19,12 +19,12 @@ public class AllListWithTabsPresenter( private val accountType: AccountType, ) : PresenterBase() { private val pinTabsPresenter by lazy { - object : PinTabsPresenter() { + object : PinTabsPresenter() { override fun List.filterPinned(): List = filterIsInstance() .map { it.listId } - override fun getTimelineTabItem(item: UiList.List): TimelineTabItem = + override fun getTimelineTabItem(item: UiList): TimelineTabItem = ListTimelineTabItem( account = accountType, listId = item.id, @@ -32,13 +32,13 @@ public class AllListWithTabsPresenter( TabMetaData( title = TitleType.Text(item.title), icon = - item.avatar?.let { + item.let { it as? UiList.List }?.avatar?.let { IconType.Url(it) } ?: IconType.Material(IconType.Material.MaterialIcon.List), ), ) - override fun List.filter(item: UiList.List): List = + override fun List.filter(item: UiList): List = filter { if (it is ListTimelineTabItem) { it.listId != item.id @@ -61,11 +61,11 @@ public class AllListWithTabsPresenter( return object : State, AllListState by state, - PinTabsPresenter.State by pinState { + PinTabsPresenter.State by pinState { } } public interface State : AllListState, - PinTabsPresenter.State + PinTabsPresenter.State } diff --git a/compose-ui/src/commonMain/kotlin/dev/dimension/flare/ui/screen/misskey/MisskeyAntennasListWithTabsPresenter.kt b/compose-ui/src/commonMain/kotlin/dev/dimension/flare/ui/screen/misskey/MisskeyAntennasListWithTabsPresenter.kt index 2ad3af500..76cf94c7b 100644 --- a/compose-ui/src/commonMain/kotlin/dev/dimension/flare/ui/screen/misskey/MisskeyAntennasListWithTabsPresenter.kt +++ b/compose-ui/src/commonMain/kotlin/dev/dimension/flare/ui/screen/misskey/MisskeyAntennasListWithTabsPresenter.kt @@ -17,12 +17,12 @@ public class MisskeyAntennasListWithTabsPresenter( private val accountType: AccountType, ) : PresenterBase() { private val pinTabsPresenter by lazy { - object : PinTabsPresenter() { + object : PinTabsPresenter() { override fun List.filterPinned(): List = filterIsInstance() .map { it.antennasId } - override fun getTimelineTabItem(item: UiList.Antenna): TimelineTabItem = + override fun getTimelineTabItem(item: UiList): TimelineTabItem = Misskey.AntennasTimelineTabItem( account = accountType, antennasId = item.id, @@ -33,7 +33,7 @@ public class MisskeyAntennasListWithTabsPresenter( ), ) - override fun List.filter(item: UiList.Antenna): List = + override fun List.filter(item: UiList): List = filter { if (it is Misskey.AntennasTimelineTabItem) { it.antennasId != item.id @@ -55,10 +55,10 @@ public class MisskeyAntennasListWithTabsPresenter( return object : State, AntennasListPresenter.State by state, - PinTabsPresenter.State by pinTabsState {} + PinTabsPresenter.State by pinTabsState {} } public interface State : - PinTabsPresenter.State, + PinTabsPresenter.State, AntennasListPresenter.State } diff --git a/desktopApp/src/main/composeResources/values/strings.xml b/desktopApp/src/main/composeResources/values/strings.xml index 67710ec7d..7ebade850 100644 --- a/desktopApp/src/main/composeResources/values/strings.xml +++ b/desktopApp/src/main/composeResources/values/strings.xml @@ -364,4 +364,6 @@ Failed to import data Confirm Import This will import data from the file. Existing records with matching IDs will be replaced. Do you want to continue? + + Channel diff --git a/desktopApp/src/main/kotlin/dev/dimension/flare/ui/screen/home/AddTabDialog.kt b/desktopApp/src/main/kotlin/dev/dimension/flare/ui/screen/home/AddTabDialog.kt index 7fccfd79e..fa8580b2a 100644 --- a/desktopApp/src/main/kotlin/dev/dimension/flare/ui/screen/home/AddTabDialog.kt +++ b/desktopApp/src/main/kotlin/dev/dimension/flare/ui/screen/home/AddTabDialog.kt @@ -33,6 +33,7 @@ import compose.icons.fontawesomeicons.solid.Plus import dev.dimension.flare.Res import dev.dimension.flare.add_rss_source import dev.dimension.flare.antenna_title +import dev.dimension.flare.channel_title import dev.dimension.flare.data.model.TabItem import dev.dimension.flare.ok import dev.dimension.flare.rss_title @@ -226,6 +227,9 @@ internal fun AddTabDialog( is PinnableTimelineTabPresenter.State.Tab.Antenna -> Res.string.antenna_title + + is PinnableTimelineTabPresenter.State.Tab.Channel -> + Res.string.channel_title } }.map { stringResource(it) } LiteFilter( diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/database/cache/dao/ListDao.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/database/cache/dao/ListDao.kt index 04401b8c3..6b4589ad4 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/data/database/cache/dao/ListDao.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/data/database/cache/dao/ListDao.kt @@ -24,8 +24,9 @@ internal interface ListDao { ) fun getPagingSource(pagingKey: String): PagingSource - @Query("SELECT listKey FROM DbListPaging WHERE pagingKey = :pagingKey") - fun getListKeysFlow(pagingKey: String): Flow> + @Transaction + @Query("SELECT * FROM DbListPaging WHERE pagingKey = :pagingKey") + fun getListKeysFlow(pagingKey: String): Flow> @Query("SELECT * FROM DbList WHERE listKey = :listKey AND accountType = :accountType") fun getList( @@ -34,10 +35,10 @@ internal interface ListDao { ): Flow @Insert(onConflict = OnConflictStrategy.REPLACE) - suspend fun insertAll(timelines: List) + suspend fun insertAllPaging(timelines: List) @Insert(onConflict = OnConflictStrategy.REPLACE) - suspend fun insertAll(lists: List) + suspend fun insertAllList(lists: List) @Query("DELETE FROM DbListPaging WHERE pagingKey = :pagingKey") suspend fun deleteByPagingKey(pagingKey: String) @@ -79,7 +80,7 @@ internal interface ListDao { fun getListMembersFlow(listKey: MicroBlogKey): Flow> @Insert(onConflict = OnConflictStrategy.REPLACE) - suspend fun insertAll(members: List) + suspend fun insertAllMember(members: List) @Query("DELETE FROM DbListMember WHERE listKey = :listKey") suspend fun deleteMembersByListKey(listKey: MicroBlogKey) diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/bluesky/BlueskyDataSource.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/bluesky/BlueskyDataSource.kt index b4e820e2f..129e5f670 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/bluesky/BlueskyDataSource.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/bluesky/BlueskyDataSource.kt @@ -7,13 +7,11 @@ import androidx.paging.PagingState import androidx.paging.cachedIn import androidx.paging.map import app.bsky.actor.GetProfileQueryParams -import app.bsky.actor.PreferencesUnion import app.bsky.bookmark.CreateBookmarkRequest import app.bsky.bookmark.DeleteBookmarkRequest import app.bsky.embed.Images import app.bsky.embed.ImagesImage import app.bsky.embed.Record -import app.bsky.feed.GetFeedGeneratorQueryParams import app.bsky.feed.GetPostsQueryParams import app.bsky.feed.Post import app.bsky.feed.PostEmbedUnion @@ -106,7 +104,6 @@ import dev.dimension.flare.ui.model.mapper.render import dev.dimension.flare.ui.model.toUi import dev.dimension.flare.ui.presenter.compose.ComposeStatus import dev.dimension.flare.ui.presenter.status.action.BlueskyReportStatusState -import kotlin.time.Clock import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList @@ -130,6 +127,7 @@ import sh.christian.ozone.api.Nsid import sh.christian.ozone.api.RKey import sh.christian.ozone.api.model.JsonContent import sh.christian.ozone.api.model.JsonContent.Companion.encodeAsJsonContent +import kotlin.time.Clock @OptIn(ExperimentalPagingApi::class) internal class BlueskyDataSource( @@ -1064,17 +1062,6 @@ internal class BlueskyDataSource( }, ) - private val preferences: MemCacheable> by lazy { - MemCacheable( - key = "preferences_$accountKey", - ) { - service - .getPreferencesForActor() - .maybeResponse() - ?.preferences - .orEmpty() - } - } private val myFeedsKey = "my_feeds_$accountKey" internal val feedLoader by lazy { @@ -1129,44 +1116,14 @@ internal class BlueskyDataSource( .let { feeds -> combine( feeds, - database.listDao().getListKeysFlow(myFeedsKey), + feedHandler.cacheData, ) { popular, my -> popular.map { item -> - item to my.any { it == item.key } + item to my.any { it.id == item.id } } } }.cachedIn(scope) - private fun feedInfoKey(uri: String) = "feed_info_$uri" - - fun feedInfo(uri: String): MemCacheable = - MemCacheable( - key = feedInfoKey(uri), - ) { - service - .getFeedGenerator( - GetFeedGeneratorQueryParams( - feed = AtUri(uri), - ), - ).requireResponse() - .view - .render(accountKey) - } - - fun feedTimeline( - uri: String, - pageSize: Int = 20, - scope: CoroutineScope, - ): Flow> = - timelinePager( - pageSize = pageSize, - database = database, - scope = scope, - filterFlow = localFilterRepository.getFlow(forTimeline = true), - accountRepository = accountRepository, - mediator = feedTimelineLoader(uri), - ) - fun feedTimelineLoader(uri: String) = FeedTimelineRemoteMediator( service = service, @@ -1177,24 +1134,24 @@ internal class BlueskyDataSource( suspend fun subscribeFeed(data: UiList.Feed) { tryRun { - feedLoader.subscribe(data.key) + feedLoader.subscribe(data.id) feedHandler.insertToDatabase(data) } } suspend fun unsubscribeFeed(data: UiList.Feed) { - feedHandler.delete(data.key) + feedHandler.delete(data.id) } suspend fun favouriteFeed(data: UiList.Feed) { - feedHandler.withDatabase { updataCallback -> + feedHandler.withDatabase { updataCallback -> val newData = data.copy(liked = !data.liked) updataCallback(newData) tryRun { if (newData.liked) { - feedLoader.favourite(data.key) + feedLoader.favourite(data.id) } else { - feedLoader.unfavourite(data.key) + feedLoader.unfavourite(data.id) } }.onFailure { updataCallback(data) @@ -1202,12 +1159,12 @@ internal class BlueskyDataSource( } } - override fun listTimeline(listKey: MicroBlogKey) = + override fun listTimeline(listId: String) = ListTimelineRemoteMediator( service = service, accountKey = accountKey, database = database, - uri = listKey.id, + uri = listId, ) private val myListKey = "my_list_$accountKey" diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/bluesky/BlueskyFeedLoader.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/bluesky/BlueskyFeedLoader.kt index 228834a5d..933f46f00 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/bluesky/BlueskyFeedLoader.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/bluesky/BlueskyFeedLoader.kt @@ -18,8 +18,6 @@ import dev.dimension.flare.data.network.bluesky.BlueskyService import dev.dimension.flare.model.MicroBlogKey import dev.dimension.flare.ui.model.UiList import dev.dimension.flare.ui.model.mapper.render -import kotlin.time.Clock -import kotlin.uuid.Uuid import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList @@ -28,6 +26,8 @@ import sh.christian.ozone.api.Cid import sh.christian.ozone.api.Did import sh.christian.ozone.api.Nsid import sh.christian.ozone.api.RKey +import kotlin.time.Clock +import kotlin.uuid.Uuid internal class BlueskyFeedLoader( private val service: BlueskyService, @@ -45,7 +45,7 @@ internal class BlueskyFeedLoader( } // Feed loading usually doesn't support pagination in the same way lists do via preferences - // But we can implement a basic fetch. + // But we can implement a basic fetch. // Bluesky preferences don't really support pagination for saved feeds. // We'll load all if cursor is null, otherwise return empty (end of list). if (cursor != null) { @@ -71,19 +71,20 @@ internal class BlueskyFeedLoader( it.type == SavedFeedType.Feed }.orEmpty() - val feeds = service - .getFeedGenerators( - GetFeedGeneratorsQueryParams( - feeds = - items - .map { AtUri(it.value) } - .toImmutableList(), - ), - ).requireResponse() - .feeds - .map { - it.render(accountKey) - }.toImmutableList() + val feeds = + service + .getFeedGenerators( + GetFeedGeneratorsQueryParams( + feeds = + items + .map { AtUri(it.value) } + .toImmutableList(), + ), + ).requireResponse() + .feeds + .map { + it.render(accountKey) + }.toImmutableList() return PagingResult( data = feeds, @@ -91,11 +92,11 @@ internal class BlueskyFeedLoader( ) } - override suspend fun info(listKey: MicroBlogKey): UiList = + override suspend fun info(listId: String): UiList = service .getFeedGenerator( GetFeedGeneratorQueryParams( - feed = AtUri(listKey.id), + feed = AtUri(listId), ), ).requireResponse() .view @@ -104,19 +105,18 @@ internal class BlueskyFeedLoader( override val supportedMetaData: ImmutableList get() = persistentListOf() - override suspend fun create(metaData: ListMetaData): UiList = - throw UnsupportedOperationException("Create feed is not supported") + override suspend fun create(metaData: ListMetaData): UiList = throw UnsupportedOperationException("Create feed is not supported") override suspend fun update( - listKey: MicroBlogKey, + listId: String, metaData: ListMetaData, ): UiList = throw UnsupportedOperationException("Update feed is not supported") - override suspend fun delete(listKey: MicroBlogKey) { + override suspend fun delete(listId: String) { val currentPreferences = service.getPreferencesForActor().requireResponse() val feedInfo = service - .getFeedGenerator(GetFeedGeneratorQueryParams(feed = AtUri(listKey.id))) + .getFeedGenerator(GetFeedGeneratorQueryParams(feed = AtUri(listId))) .requireResponse() val newPreferences = currentPreferences.preferences.toMutableList() val prefIndex = newPreferences.indexOfFirst { it is PreferencesUnion.SavedFeedsPref } @@ -156,11 +156,11 @@ internal class BlueskyFeedLoader( ) } - suspend fun subscribe(feedKey: MicroBlogKey) { + suspend fun subscribe(feedUri: String) { val currentPreferences = service.getPreferencesForActor().requireResponse() val feedInfo = service - .getFeedGenerator(GetFeedGeneratorQueryParams(feed = AtUri(feedKey.id))) + .getFeedGenerator(GetFeedGeneratorQueryParams(feed = AtUri(feedUri))) .requireResponse() val newPreferences = currentPreferences.preferences.toMutableList() val prefIndex = newPreferences.indexOfFirst { it is PreferencesUnion.SavedFeedsPref } @@ -181,14 +181,14 @@ internal class BlueskyFeedLoader( pref.value.copy( items = ( - pref.value.items + - SavedFeed( - type = SavedFeedType.Feed, - value = feedInfo.view.uri.atUri, - pinned = true, - id = Uuid.random().toString(), - ) - ).toImmutableList(), + pref.value.items + + SavedFeed( + type = SavedFeedType.Feed, + value = feedInfo.view.uri.atUri, + pinned = true, + id = Uuid.random().toString(), + ) + ).toImmutableList(), ) newPreferences[prefV2Index] = PreferencesUnion.SavedFeedsPrefV2(newPref) } @@ -201,10 +201,10 @@ internal class BlueskyFeedLoader( ) } - suspend fun favourite(feedKey: MicroBlogKey) { + suspend fun favourite(feedUri: String) { val feedInfo = service - .getFeedGenerator(GetFeedGeneratorQueryParams(feed = AtUri(feedKey.id))) + .getFeedGenerator(GetFeedGeneratorQueryParams(feed = AtUri(feedUri))) .requireResponse() val likedUri = feedInfo.view.viewer @@ -218,10 +218,10 @@ internal class BlueskyFeedLoader( } } - suspend fun unfavourite(feedKey: MicroBlogKey) { + suspend fun unfavourite(feedUri: String) { val feedInfo = service - .getFeedGenerator(GetFeedGeneratorQueryParams(feed = AtUri(feedKey.id))) + .getFeedGenerator(GetFeedGeneratorQueryParams(feed = AtUri(feedUri))) .requireResponse() val likedUri = feedInfo.view.viewer diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/bluesky/BlueskyListLoader.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/bluesky/BlueskyListLoader.kt index 431851378..1de8baf62 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/bluesky/BlueskyListLoader.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/bluesky/BlueskyListLoader.kt @@ -62,11 +62,11 @@ internal class BlueskyListLoader( ) } - override suspend fun info(listKey: MicroBlogKey): UiList = + override suspend fun info(listId: String): UiList = service .getList( GetListQueryParams( - list = AtUri(listKey.id), + list = AtUri(listId), ), ).requireResponse() .list @@ -130,16 +130,16 @@ internal class BlueskyListLoader( } override suspend fun update( - listKey: MicroBlogKey, + listId: String, metaData: ListMetaData, ): UiList { updateList( - uri = listKey.id, + uri = listId, title = metaData.title, description = metaData.description, icon = metaData.avatar, ) - return info(listKey) + return info(listId) } private suspend fun updateList( @@ -190,8 +190,8 @@ internal class BlueskyListLoader( ) } - override suspend fun delete(listKey: MicroBlogKey) { - val id = listKey.id.substringAfterLast('/') + override suspend fun delete(listId: String) { + val id = listId.substringAfterLast('/') service.applyWrites( request = ApplyWritesRequest( diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/bluesky/BlueskyListMemberLoader.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/bluesky/BlueskyListMemberLoader.kt index 37875aea2..19a904134 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/bluesky/BlueskyListMemberLoader.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/bluesky/BlueskyListMemberLoader.kt @@ -29,7 +29,7 @@ internal class BlueskyListMemberLoader( override suspend fun loadMembers( pageSize: Int, request: PagingRequest, - listKey: MicroBlogKey, + listId: String, ): PagingResult { val cursor = when (request) { @@ -42,7 +42,7 @@ internal class BlueskyListMemberLoader( .getList( params = GetListQueryParams( - list = AtUri(listKey.id), + list = AtUri(listId), cursor = cursor, limit = pageSize.toLong(), ), @@ -59,7 +59,7 @@ internal class BlueskyListMemberLoader( } override suspend fun addMember( - listKey: MicroBlogKey, + listId: String, userKey: MicroBlogKey, ): DbUser { val user = @@ -74,7 +74,7 @@ internal class BlueskyListMemberLoader( record = app.bsky.graph .Listitem( - list = AtUri(listKey.id), + list = AtUri(listId), subject = Did(userKey.id), createdAt = Clock.System.now(), ).bskyJson(), @@ -84,7 +84,7 @@ internal class BlueskyListMemberLoader( } override suspend fun removeMember( - listKey: MicroBlogKey, + listId: String, userKey: MicroBlogKey, ) { var record: com.atproto.repo.ListRecordsRecord? = null @@ -109,7 +109,7 @@ internal class BlueskyListMemberLoader( response.records .firstOrNull { val item: Listitem = it.value.decodeAs() - item.list.atUri == listKey.id && item.subject.did == userKey.id + item.list.atUri == listId && item.subject.did == userKey.id } } if (record != null) { @@ -171,7 +171,7 @@ internal class BlueskyListMemberLoader( item.subject.did == userKey.id }.mapNotNull { val item: Listitem = it.value.decodeAs() - allLists.firstOrNull { list -> list.key.id == item.list.atUri } + allLists.firstOrNull { list -> list.id == item.list.atUri } }, ) cursor = response.cursor diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/mastodon/MastodonDataSource.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/mastodon/MastodonDataSource.kt index 16b361d61..279a103e0 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/mastodon/MastodonDataSource.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/mastodon/MastodonDataSource.kt @@ -158,9 +158,9 @@ internal open class MastodonDataSource( accountKey, ) - override fun listTimeline(listKey: MicroBlogKey) = + override fun listTimeline(listId: String) = ListTimelineRemoteMediator( - listKey.id, + listId, service, database, accountKey, diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/mastodon/MastodonListLoader.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/mastodon/MastodonListLoader.kt index 23c25eafa..8b4f180ea 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/mastodon/MastodonListLoader.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/mastodon/MastodonListLoader.kt @@ -30,8 +30,8 @@ internal class MastodonListLoader( ) } - override suspend fun info(listKey: MicroBlogKey): UiList = - service.getList(listKey.id).toUiList(accountKey) + override suspend fun info(listId: String): UiList = + service.getList(listId).toUiList(accountKey) ?: error("Failed to parse list info") override suspend fun create(metaData: ListMetaData): UiList = @@ -41,16 +41,16 @@ internal class MastodonListLoader( ?: error("Failed to parse created list") override suspend fun update( - listKey: MicroBlogKey, + listId: String, metaData: ListMetaData, ): UiList = service - .updateList(listKey.id, PostList(title = metaData.title)) + .updateList(listId, PostList(title = metaData.title)) .toUiList(accountKey) ?: error("Failed to parse updated list") - override suspend fun delete(listKey: MicroBlogKey) { - service.deleteList(listKey.id) + override suspend fun delete(listId: String) { + service.deleteList(listId) } override val supportedMetaData: ImmutableList @@ -60,7 +60,7 @@ internal class MastodonListLoader( val id = id ?: return null val title = title ?: return null return UiList.List( - key = MicroBlogKey(id = id, host = accountKey.host), + id = id, title = title, ) } diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/mastodon/MastodonListMemberLoader.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/mastodon/MastodonListMemberLoader.kt index 0f608149c..5dda7a782 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/mastodon/MastodonListMemberLoader.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/mastodon/MastodonListMemberLoader.kt @@ -18,7 +18,7 @@ internal class MastodonListMemberLoader( override suspend fun loadMembers( pageSize: Int, request: PagingRequest, - listKey: MicroBlogKey, + listId: String, ): PagingResult { val maxId = when (request) { @@ -29,7 +29,7 @@ internal class MastodonListMemberLoader( val response = service.listMembers( - listId = listKey.id, + listId = listId, limit = pageSize, max_id = maxId, ) @@ -46,11 +46,11 @@ internal class MastodonListMemberLoader( } override suspend fun addMember( - listKey: MicroBlogKey, + listId: String, userKey: MicroBlogKey, ): DbUser { service.addMember( - listId = listKey.id, + listId = listId, accounts = PostAccounts(listOf(userKey.id)), ) return service @@ -59,11 +59,11 @@ internal class MastodonListMemberLoader( } override suspend fun removeMember( - listKey: MicroBlogKey, + listId: String, userKey: MicroBlogKey, ) { service.removeMember( - listId = listKey.id, + listId = listId, accounts = PostAccounts(listOf(userKey.id)), ) } @@ -90,7 +90,7 @@ internal class MastodonListMemberLoader( val id = id ?: return null val title = title ?: return null return UiList.List( - key = MicroBlogKey(id = id, host = accountKey.host), + id = id, title = title, ) } diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/microblog/list/ListDataSource.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/microblog/list/ListDataSource.kt index b86b99b19..c7b454834 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/microblog/list/ListDataSource.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/microblog/list/ListDataSource.kt @@ -1,10 +1,9 @@ package dev.dimension.flare.data.datasource.microblog.list import dev.dimension.flare.data.datasource.microblog.paging.BaseTimelineLoader -import dev.dimension.flare.model.MicroBlogKey internal interface ListDataSource { - fun listTimeline(listKey: MicroBlogKey): BaseTimelineLoader + fun listTimeline(listId: String): BaseTimelineLoader val listHandler: ListHandler val listMemberHandler: ListMemberHandler diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/microblog/list/ListHandler.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/microblog/list/ListHandler.kt index a58ed0883..303af9172 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/microblog/list/ListHandler.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/microblog/list/ListHandler.kt @@ -46,22 +46,22 @@ internal class ListHandler( }, onSave = { request, data -> database.listDao().deleteByPagingKey(pagingKey) - database.listDao().insertAll( + database.listDao().insertAllList( data.map { item -> DbList( - listKey = item.key, + listKey = MicroBlogKey(item.id, accountKey.host), accountType = accountType, content = DbList.ListContent(item), ) }, ) - database.listDao().insertAll( + database.listDao().insertAllPaging( data.map { item -> DbListPaging( accountType = accountType, pagingKey = pagingKey, - listKey = item.key, + listKey = MicroBlogKey(item.id, accountKey.host), ) }, ) @@ -79,15 +79,24 @@ internal class ListHandler( } } - fun listInfo(listKey: MicroBlogKey): CacheData = - Cacheable( + val cacheData by lazy { + database.listDao().getListKeysFlow(pagingKey).map { + it.map { + it.list.content.data + } + } + } + + fun listInfo(listId: String): CacheData { + val listKey = MicroBlogKey(listId, accountKey.host) + return Cacheable( fetchSource = { - val info = loader.info(listKey) + val info = loader.info(listId) database.connect { - database.listDao().insertAll( + database.listDao().insertAllList( listOf( DbList( - listKey = info.key, + listKey = MicroBlogKey(info.id, accountKey.host), accountType = accountType, content = DbList.ListContent(info), ), @@ -106,27 +115,28 @@ internal class ListHandler( } }, ) + } suspend fun create(metaData: ListMetaData) { tryRun { loader.create(metaData) }.onSuccess { result -> database.connect { - database.listDao().insertAll( + database.listDao().insertAllList( listOf( DbList( - listKey = result.key, + listKey = MicroBlogKey(result.id, accountKey.host), accountType = accountType, content = DbList.ListContent(result), ), ), ) - database.listDao().insertAll( + database.listDao().insertAllPaging( listOf( DbListPaging( accountType = accountType, pagingKey = pagingKey, - listKey = result.key, + listKey = MicroBlogKey(result.id, accountKey.host), ), ), ) @@ -135,11 +145,12 @@ internal class ListHandler( } suspend fun update( - listKey: MicroBlogKey, + listId: String, metaData: ListMetaData, ) { + val listKey = MicroBlogKey(listId, accountKey.host) tryRun { - loader.update(listKey, metaData) + loader.update(listId, metaData) }.onSuccess { result -> database.connect { database.listDao().updateListContent( @@ -151,9 +162,10 @@ internal class ListHandler( } } - suspend fun delete(listKey: MicroBlogKey) { + suspend fun delete(listId: String) { + val listKey = MicroBlogKey(listId, accountKey.host) tryRun { - loader.delete(listKey) + loader.delete(listId) }.onSuccess { database.connect { database.listDao().deleteByListKey( @@ -168,39 +180,37 @@ internal class ListHandler( } } - suspend fun insertToDatabase( - data: UiList, - ) { + suspend fun insertToDatabase(data: UiList) { + val listKey = MicroBlogKey(data.id, accountKey.host) database.connect { - database.listDao().insertAll( + database.listDao().insertAllList( listOf( DbList( - listKey = data.key, + listKey = listKey, accountType = AccountType.Specific(accountKey), content = DbList.ListContent(data), ), ), ) - database.listDao().insertAll( + database.listDao().insertAllPaging( listOf( DbListPaging( accountType = AccountType.Specific(accountKey), pagingKey = pagingKey, - listKey = data.key, + listKey = listKey, ), ), ) } } - suspend fun withDatabase( - block: suspend (update: suspend (UiList) -> Unit) -> Unit, - ) { - block.invoke { data -> + suspend fun withDatabase(block: suspend (update: suspend (UiList) -> Unit) -> Unit) { + block.invoke { data -> + val listKey = MicroBlogKey(data.id, accountKey.host) database.connect { database.listDao().updateListContent( - listKey = data.key, + listKey = listKey, accountType = AccountType.Specific(accountKey), content = DbList.ListContent(data), ) diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/microblog/list/ListLoader.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/microblog/list/ListLoader.kt index c4ec64e51..5e3cabfd2 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/microblog/list/ListLoader.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/microblog/list/ListLoader.kt @@ -2,7 +2,6 @@ package dev.dimension.flare.data.datasource.microblog.list import dev.dimension.flare.data.datasource.microblog.paging.PagingRequest import dev.dimension.flare.data.datasource.microblog.paging.PagingResult -import dev.dimension.flare.model.MicroBlogKey import dev.dimension.flare.ui.model.UiList import kotlinx.collections.immutable.ImmutableList @@ -12,16 +11,16 @@ internal interface ListLoader { request: PagingRequest, ): PagingResult - suspend fun info(listKey: MicroBlogKey): UiList + suspend fun info(listId: String): UiList suspend fun create(metaData: ListMetaData): UiList suspend fun update( - listKey: MicroBlogKey, + listId: String, metaData: ListMetaData, ): UiList - suspend fun delete(listKey: MicroBlogKey) + suspend fun delete(listId: String) val supportedMetaData: ImmutableList } diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/microblog/list/ListMemberHandler.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/microblog/list/ListMemberHandler.kt index 3be0609e6..4ea3b4a78 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/microblog/list/ListMemberHandler.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/microblog/list/ListMemberHandler.kt @@ -30,7 +30,7 @@ internal class ListMemberHandler( private val memberPagingKey: String get() = "${pagingKey}_members" - fun listMembers(listKey: MicroBlogKey) = + fun listMembers(listId: String) = Pager( config = pagingConfig, remoteMediator = @@ -41,11 +41,12 @@ internal class ListMemberHandler( loader.loadMembers( pageSize = pageSize, request = request, - listKey = listKey, + listId = listId, ) }, onSave = { request, data -> - database.listDao().insertAll( + val listKey = MicroBlogKey(listId, accountKey.host) + database.listDao().insertAllMember( data.map { item -> DbListMember( listKey = listKey, @@ -58,7 +59,7 @@ internal class ListMemberHandler( ), pagingSourceFactory = { database.listDao().getListMembers( - listKey = listKey, + listKey = MicroBlogKey(listId, accountKey.host), ) }, ).flow.map { @@ -67,11 +68,11 @@ internal class ListMemberHandler( } } - fun listMembersListFlow(listKey: MicroBlogKey) = + fun listMembersListFlow(listId: String) = database .listDao() .getListMembersFlow( - listKey = listKey, + listKey = MicroBlogKey(listId, accountKey.host), ).map { members -> members.map { member -> member.user.render(accountKey) @@ -79,14 +80,15 @@ internal class ListMemberHandler( } suspend fun addMember( - listKey: MicroBlogKey, + listId: String, userKey: MicroBlogKey, ) { + val listKey = MicroBlogKey(listId, accountKey.host) tryRun { - loader.addMember(listKey, userKey) + loader.addMember(listId, userKey) }.onSuccess { user -> database.connect { - database.listDao().insertAll( + database.listDao().insertAllMember( listOf( DbListMember( listKey = listKey, @@ -102,11 +104,12 @@ internal class ListMemberHandler( } suspend fun removeMember( - listKey: MicroBlogKey, + listId: String, userKey: MicroBlogKey, ) { + val listKey = MicroBlogKey(listId, accountKey.host) tryRun { - loader.removeMember(listKey, userKey) + loader.removeMember(listId, userKey) }.onSuccess { database.connect { database.listDao().deleteMemberFromList( @@ -138,19 +141,19 @@ internal class ListMemberHandler( ) }, onSave = { request, data -> - database.listDao().insertAll( + database.listDao().insertAllList( data.map { item -> DbList( - listKey = item.key, + listKey = MicroBlogKey(item.id, accountKey.host), accountType = accountType, content = DbList.ListContent(item), ) }, ) - database.listDao().insertAll( + database.listDao().insertAllMember( data.map { item -> DbListMember( - listKey = item.key, + listKey = MicroBlogKey(item.id, accountKey.host), memberKey = userKey, ) }, diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/microblog/list/ListMemberLoader.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/microblog/list/ListMemberLoader.kt index 76c842ddd..1dee97911 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/microblog/list/ListMemberLoader.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/microblog/list/ListMemberLoader.kt @@ -10,16 +10,16 @@ internal interface ListMemberLoader { suspend fun loadMembers( pageSize: Int, request: PagingRequest, - listKey: MicroBlogKey, + listId: String, ): PagingResult suspend fun addMember( - listKey: MicroBlogKey, + listId: String, userKey: MicroBlogKey, ): DbUser suspend fun removeMember( - listKey: MicroBlogKey, + listId: String, userKey: MicroBlogKey, ) diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/misskey/AntennasListPagingSource.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/misskey/AntennasListPagingSource.kt index 7f0c8c90d..e403b1c9e 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/misskey/AntennasListPagingSource.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/misskey/AntennasListPagingSource.kt @@ -29,4 +29,4 @@ internal class AntennasListPagingSource( ) override fun getRefreshKey(state: PagingState): Int? = null -} \ No newline at end of file +} diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/misskey/MisskeyDataSource.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/misskey/MisskeyDataSource.kt index 28547e107..19cd447f0 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/misskey/MisskeyDataSource.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/misskey/MisskeyDataSource.kt @@ -1008,9 +1008,9 @@ internal class MisskeyDataSource( private val listKey: String get() = "allLists_$accountKey" - override fun listTimeline(listKey: MicroBlogKey) = + override fun listTimeline(listId: String) = ListTimelineRemoteMediator( - listKey.id, + listId, service, database, accountKey, diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/misskey/MisskeyListLoader.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/misskey/MisskeyListLoader.kt index 73b729d1e..4a48ef8b4 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/misskey/MisskeyListLoader.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/misskey/MisskeyListLoader.kt @@ -44,11 +44,11 @@ internal class MisskeyListLoader( ) } - override suspend fun info(listKey: MicroBlogKey): UiList = + override suspend fun info(listId: String): UiList = service .usersListsShow( UsersListsShowRequest( - listId = listKey.id, + listId = listId, ), ).render() @@ -60,7 +60,7 @@ internal class MisskeyListLoader( ), ) return UiList.List( - key = MicroBlogKey(response.id, accountKey.host), + id = response.id, title = metaData.title, description = null, avatar = null, @@ -69,20 +69,20 @@ internal class MisskeyListLoader( } override suspend fun update( - listKey: MicroBlogKey, + listId: String, metaData: ListMetaData, ): UiList = service .usersListsUpdate( UsersListsUpdateRequest( - listId = listKey.id, + listId = listId, name = metaData.title, ), ).render() - override suspend fun delete(listKey: MicroBlogKey) { + override suspend fun delete(listId: String) { service.usersListsDelete( - UsersListsDeleteRequest(listId = listKey.id), + UsersListsDeleteRequest(listId = listId), ) } diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/misskey/MisskeyListMemberLoader.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/misskey/MisskeyListMemberLoader.kt index 8c4ade5fc..3b7fc3f42 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/misskey/MisskeyListMemberLoader.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/misskey/MisskeyListMemberLoader.kt @@ -22,7 +22,7 @@ internal class MisskeyListMemberLoader( override suspend fun loadMembers( pageSize: Int, request: PagingRequest, - listKey: MicroBlogKey, + listId: String, ): PagingResult { val cursor = when (request) { @@ -35,7 +35,7 @@ internal class MisskeyListMemberLoader( service .usersListsGetMemberships( UsersListsMembershipRequest( - listId = listKey.id, + listId = listId, untilId = cursor, limit = pageSize, ), @@ -53,12 +53,12 @@ internal class MisskeyListMemberLoader( } override suspend fun addMember( - listKey: MicroBlogKey, + listId: String, userKey: MicroBlogKey, ): DbUser { service.usersListsPush( UsersListsPullRequest( - listId = listKey.id, + listId = listId, userId = userKey.id, ), ) @@ -71,12 +71,12 @@ internal class MisskeyListMemberLoader( } override suspend fun removeMember( - listKey: MicroBlogKey, + listId: String, userKey: MicroBlogKey, ) { service.usersListsPull( UsersListsPullRequest( - listId = listKey.id, + listId = listId, userId = userKey.id, ), ) diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/xqt/XQTDataSource.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/xqt/XQTDataSource.kt index c68c6e9a3..a537259ea 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/xqt/XQTDataSource.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/xqt/XQTDataSource.kt @@ -1204,9 +1204,9 @@ internal class XQTDataSource( }, ).toPersistentList() - override fun listTimeline(listKey: MicroBlogKey) = + override fun listTimeline(listId: String) = ListTimelineRemoteMediator( - listKey.id, + listId, service, database, accountKey, diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/xqt/XQTListLoader.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/xqt/XQTListLoader.kt index 26c18c4de..fe6b95286 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/xqt/XQTListLoader.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/xqt/XQTListLoader.kt @@ -57,10 +57,10 @@ internal class XQTListLoader( ) } - override suspend fun info(listKey: MicroBlogKey): UiList = + override suspend fun info(listId: String): UiList = service .getListByRestId( - variables = "{\"listId\":\"${listKey.id}\"}", + variables = "{\"listId\":\"${listId}\"}", ).body() ?.data ?.list @@ -83,7 +83,7 @@ internal class XQTListLoader( val data = response.body()?.data?.list if (data?.idStr != null) { return UiList.List( - key = MicroBlogKey(data.idStr, accountKey.host), + id = data.idStr, title = metaData.title, description = metaData.description, creator = null, @@ -96,7 +96,7 @@ internal class XQTListLoader( } override suspend fun update( - listKey: MicroBlogKey, + listId: String, metaData: ListMetaData, ): UiList { service.updateList( @@ -104,14 +104,14 @@ internal class XQTListLoader( UpdateListRequest( variables = UpdateListRequest.Variables( - listID = listKey.id, + listID = listId, name = metaData.title, description = metaData.description.orEmpty(), isPrivate = false, ), ), ) - return info(listKey).let { + return info(listId).let { if (it is UiList.List) { it.copy( title = metaData.title, @@ -123,13 +123,13 @@ internal class XQTListLoader( } } - override suspend fun delete(listKey: MicroBlogKey) { + override suspend fun delete(listId: String) { service.deleteList( request = RemoveListRequest( variables = RemoveListRequest.Variables( - listID = listKey.id, + listID = listId, ), ), ) diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/xqt/XQTListMemberLoader.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/xqt/XQTListMemberLoader.kt index 30480e812..f35b3eb9a 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/xqt/XQTListMemberLoader.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/xqt/XQTListMemberLoader.kt @@ -23,7 +23,7 @@ internal class XQTListMemberLoader( override suspend fun loadMembers( pageSize: Int, request: PagingRequest, - listKey: MicroBlogKey, + listId: String, ): PagingResult { val cursor = (request as? PagingRequest.Append)?.nextKey val response = @@ -31,7 +31,7 @@ internal class XQTListMemberLoader( .getListMembers( variables = buildString { - append("{\"listId\":\"${listKey.id}\",\"count\":$pageSize") + append("{\"listId\":\"${listId}\",\"count\":$pageSize") if (cursor != null) { append(",\"cursor\":\"${cursor}\"") } @@ -58,7 +58,7 @@ internal class XQTListMemberLoader( } override suspend fun addMember( - listKey: MicroBlogKey, + listId: String, userKey: MicroBlogKey, ): DbUser { service.addMember( @@ -66,7 +66,7 @@ internal class XQTListMemberLoader( AddMemberRequest( variables = AddMemberRequest.Variables( - listID = listKey.id, + listID = listId, userID = userKey.id, ), ), @@ -87,7 +87,7 @@ internal class XQTListMemberLoader( } override suspend fun removeMember( - listKey: MicroBlogKey, + listId: String, userKey: MicroBlogKey, ) { service.removeMember( @@ -95,7 +95,7 @@ internal class XQTListMemberLoader( RemoveMemberRequest( variables = RemoveMemberRequest.Variables( - listID = listKey.id, + listID = listId, userID = userKey.id, ), ), diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/network/misskey/api/model/ChannelsFeaturedRequest.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/network/misskey/api/model/ChannelsFeaturedRequest.kt index 7d7a05f7d..d68bb4515 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/data/network/misskey/api/model/ChannelsFeaturedRequest.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/data/network/misskey/api/model/ChannelsFeaturedRequest.kt @@ -7,4 +7,4 @@ import kotlinx.serialization.Serializable internal data class ChannelsFeaturedRequest( @SerialName(value = "limit") val limit: Int? = 10, @SerialName(value = "allowPartial") val allowPartial: Boolean? = true, -) \ No newline at end of file +) diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/network/xqt/elonmusk114514/ElonMusk1145141919810.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/network/xqt/elonmusk114514/ElonMusk1145141919810.kt index a54d7410e..77b85ff99 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/data/network/xqt/elonmusk114514/ElonMusk1145141919810.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/data/network/xqt/elonmusk114514/ElonMusk1145141919810.kt @@ -6,11 +6,11 @@ import dev.whyoleg.cryptography.CryptographyProvider import dev.whyoleg.cryptography.algorithms.SHA256 import io.ktor.client.request.get import io.ktor.client.statement.bodyAsText +import kotlinx.serialization.Serializable import kotlin.experimental.xor import kotlin.io.encoding.Base64 import kotlin.random.Random import kotlin.time.Clock -import kotlinx.serialization.Serializable internal object ElonMusk1145141919810 { @Serializable diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/model/UiList.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/model/UiList.kt index 69e4bf230..8624d6983 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/model/UiList.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/model/UiList.kt @@ -1,21 +1,20 @@ package dev.dimension.flare.ui.model import androidx.compose.runtime.Immutable -import dev.dimension.flare.model.MicroBlogKey import kotlinx.serialization.Serializable @Serializable @Immutable public sealed class UiList { // public abstract val id: String - public abstract val key: MicroBlogKey + public abstract val id: String public abstract val title: String public abstract val readonly: Boolean @Serializable @Immutable public data class List( - override val key: MicroBlogKey, + override val id: String, override val title: String, val description: String? = null, val avatar: String? = null, @@ -26,7 +25,7 @@ public sealed class UiList { @Serializable @Immutable public data class Feed( - override val key: MicroBlogKey, + override val id: String, override val title: String, val description: String? = null, val avatar: String? = null, @@ -39,7 +38,7 @@ public sealed class UiList { @Serializable @Immutable public data class Antenna( - override val key: MicroBlogKey, + override val id: String, override val title: String, override val readonly: Boolean = false, ) : UiList() @@ -47,7 +46,7 @@ public sealed class UiList { @Serializable @Immutable public data class Channel( - override val key: MicroBlogKey, + override val id: String, override val title: String, val isArchived: Boolean, val notesCount: Double, diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/model/mapper/Mastodon.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/model/mapper/Mastodon.kt index cfc8f9823..abcc181f4 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/model/mapper/Mastodon.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/model/mapper/Mastodon.kt @@ -11,6 +11,7 @@ import dev.dimension.flare.data.datasource.microblog.userActionsMenu import dev.dimension.flare.data.network.mastodon.api.model.Account import dev.dimension.flare.data.network.mastodon.api.model.Attachment import dev.dimension.flare.data.network.mastodon.api.model.InstanceData +import dev.dimension.flare.data.network.mastodon.api.model.MastodonList import dev.dimension.flare.data.network.mastodon.api.model.MediaType import dev.dimension.flare.data.network.mastodon.api.model.Mention import dev.dimension.flare.data.network.mastodon.api.model.Notification @@ -27,6 +28,7 @@ import dev.dimension.flare.ui.model.UiCard import dev.dimension.flare.ui.model.UiEmoji import dev.dimension.flare.ui.model.UiInstance import dev.dimension.flare.ui.model.UiInstanceMetadata +import dev.dimension.flare.ui.model.UiList import dev.dimension.flare.ui.model.UiMedia import dev.dimension.flare.ui.model.UiNumber import dev.dimension.flare.ui.model.UiPoll @@ -1173,3 +1175,9 @@ internal fun InstanceData.render(): UiInstanceMetadata { configuration = configuration, ) } + +internal fun MastodonList.render(): UiList.List = + UiList.List( + id = id.toString(), + title = title.orEmpty(), + ) diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/model/mapper/VVO.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/model/mapper/VVO.kt index 07bd9e2cf..78ad6ba4b 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/model/mapper/VVO.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/model/mapper/VVO.kt @@ -5,7 +5,6 @@ import com.fleeksoft.ksoup.nodes.Node import com.fleeksoft.ksoup.nodes.TextNode import dev.dimension.flare.data.datasource.microblog.ActionMenu import dev.dimension.flare.data.datasource.microblog.StatusEvent -import dev.dimension.flare.data.network.mastodon.api.model.MastodonList import dev.dimension.flare.data.network.vvo.model.Attitude import dev.dimension.flare.data.network.vvo.model.Comment import dev.dimension.flare.data.network.vvo.model.Status @@ -16,7 +15,6 @@ import dev.dimension.flare.model.PlatformType import dev.dimension.flare.model.vvoHost import dev.dimension.flare.model.vvoHostLong import dev.dimension.flare.model.vvoHostShort -import dev.dimension.flare.ui.model.UiList import dev.dimension.flare.ui.model.UiMedia import dev.dimension.flare.ui.model.UiNumber import dev.dimension.flare.ui.model.UiProfile @@ -701,9 +699,3 @@ private fun replaceMentionAndHashtag( } } } - -internal fun MastodonList.render(): UiList.List = - UiList.List( - id = id.orEmpty(), - title = title.orEmpty(), - ) diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/home/bluesky/BlueskyFeedPresenter.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/home/bluesky/BlueskyFeedPresenter.kt index c781a40fd..25e2d7bf6 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/home/bluesky/BlueskyFeedPresenter.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/home/bluesky/BlueskyFeedPresenter.kt @@ -2,27 +2,31 @@ package dev.dimension.flare.ui.presenter.home.bluesky import androidx.compose.runtime.Composable import androidx.compose.runtime.Immutable -import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope -import androidx.paging.cachedIn -import androidx.paging.compose.collectAsLazyPagingItems import dev.dimension.flare.common.PagingState -import dev.dimension.flare.common.collectAsState import dev.dimension.flare.common.refreshSuspend -import dev.dimension.flare.common.toPagingState import dev.dimension.flare.data.datasource.bluesky.BlueskyDataSource import dev.dimension.flare.data.repository.AccountRepository +import dev.dimension.flare.data.repository.accountServiceFlow import dev.dimension.flare.data.repository.accountServiceProvider import dev.dimension.flare.model.AccountType import dev.dimension.flare.ui.model.UiList import dev.dimension.flare.ui.model.UiState import dev.dimension.flare.ui.model.UiTimeline +import dev.dimension.flare.ui.model.collectAsUiState import dev.dimension.flare.ui.model.flatMap +import dev.dimension.flare.ui.model.flattenUiState import dev.dimension.flare.ui.model.map +import dev.dimension.flare.ui.model.mapNotNull import dev.dimension.flare.ui.model.onSuccess import dev.dimension.flare.ui.model.toUi import dev.dimension.flare.ui.presenter.PresenterBase +import dev.dimension.flare.ui.presenter.home.TimelinePresenter +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch import org.koin.core.component.KoinComponent import org.koin.core.component.inject @@ -34,47 +38,54 @@ public class BlueskyFeedPresenter( KoinComponent { private val accountRepository: AccountRepository by inject() + private val timelinePresenter by lazy { + object : TimelinePresenter() { + override val loader by lazy { + accountServiceFlow(accountType, accountRepository) + .map { + require(it is BlueskyDataSource) + it.feedTimelineLoader(uri) + } + } + } + } + + @OptIn(ExperimentalCoroutinesApi::class) + private val infoFlow by lazy { + accountServiceFlow(accountType, accountRepository) + .flatMapLatest { + require(it is BlueskyDataSource) + it.feedHandler.listInfo(uri).toUi() + }.map { + it.mapNotNull { it as? UiList.Feed } + } + } + @Composable override fun body(): BlueskyFeedState { val scope = rememberCoroutineScope() val serviceState = accountServiceProvider(accountType = accountType, repository = accountRepository) - val timeline = - serviceState - .map { - require(it is BlueskyDataSource) - remember(it, uri) { - it.feedTimeline(uri = uri, scope = scope) - }.collectAsLazyPagingItems() - }.toPagingState() - val info = - serviceState.map { - require(it is BlueskyDataSource) - remember(it, uri) { - it.feedInfo(uri = uri) - }.collectAsState() - } + val timeline = timelinePresenter.body().listState + val info by infoFlow.flattenUiState() val subscribed = serviceState .flatMap { require(it is BlueskyDataSource) remember(it) { - it.feedHandler.data.cachedIn(scope) - }.collectAsLazyPagingItems() + it.feedHandler.cacheData + }.collectAsUiState().value }.map { it.any { it.id == uri } } return object : BlueskyFeedState { - override val info = - info.flatMap { - it.toUi() - } + override val info = info override val timeline = timeline override val subscribed = subscribed override suspend fun refreshSuspend() { - info.onSuccess { - it.refresh() - } +// info.onSuccess { +// it.refresh() +// } timeline.refreshSuspend() } diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/home/bluesky/BlueskyFeedsPresenter.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/home/bluesky/BlueskyFeedsPresenter.kt index 57252652a..84289902f 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/home/bluesky/BlueskyFeedsPresenter.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/home/bluesky/BlueskyFeedsPresenter.kt @@ -39,9 +39,10 @@ public class BlueskyFeedsPresenter( serviceState .map { service -> require(service is BlueskyDataSource) - val flow = remember(service) { - service.feedHandler.data.cachedIn(scope) - } + val flow = + remember(service) { + service.feedHandler.data.cachedIn(scope) + } flow.collectAsLazyPagingItems() }.toPagingState() val popularFeeds = diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/list/DeleteListPresenter.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/list/DeleteListPresenter.kt index 2a7f2fe8e..67264ae0d 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/list/DeleteListPresenter.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/list/DeleteListPresenter.kt @@ -6,7 +6,6 @@ import dev.dimension.flare.data.datasource.microblog.list.ListDataSource import dev.dimension.flare.data.repository.AccountRepository import dev.dimension.flare.data.repository.accountServiceFlow import dev.dimension.flare.model.AccountType -import dev.dimension.flare.model.MicroBlogKey import dev.dimension.flare.ui.presenter.PresenterBase import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.first @@ -20,7 +19,7 @@ import org.koin.core.component.inject */ public class DeleteListPresenter( private val accountType: AccountType, - private val listKey: MicroBlogKey, + private val listId: String, ) : PresenterBase(), KoinComponent { private val scope by inject() @@ -39,7 +38,7 @@ public class DeleteListPresenter( it }.first() .listHandler - .delete(listKey) + .delete(listId) } } } diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/list/EditAccountListPresenter.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/list/EditAccountListPresenter.kt index 371f3b771..d06f398be 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/list/EditAccountListPresenter.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/list/EditAccountListPresenter.kt @@ -67,7 +67,7 @@ public class EditAccountListPresenter( serviceState.onSuccess { require(it is ListDataSource) scope.launch { - it.listMemberHandler.addMember(list.key, userKey = userKey) + it.listMemberHandler.addMember(list.id, userKey = userKey) } } } @@ -76,7 +76,7 @@ public class EditAccountListPresenter( serviceState.onSuccess { require(it is ListDataSource) scope.launch { - it.listMemberHandler.removeMember(list.key, userKey = userKey) + it.listMemberHandler.removeMember(list.id, userKey = userKey) } } } diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/list/EditListMemberPresenter.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/list/EditListMemberPresenter.kt index 1daaa9389..3ba269462 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/list/EditListMemberPresenter.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/list/EditListMemberPresenter.kt @@ -32,7 +32,7 @@ import org.koin.core.component.inject */ public class EditListMemberPresenter( private val accountType: AccountType, - private val listKey: MicroBlogKey, + private val listId: String, ) : PresenterBase(), KoinComponent { private val accountRepository: AccountRepository by inject() @@ -52,7 +52,7 @@ public class EditListMemberPresenter( require(service is ListDataSource) combine( service.searchUser(query = filter), - service.listMemberHandler.listMembersListFlow(listKey), + service.listMemberHandler.listMembersListFlow(listId), ) { users, members -> users.map { user -> val isMember = members.any { it.key == user.key } @@ -75,7 +75,7 @@ public class EditListMemberPresenter( serviceState.onSuccess { scope.launch { require(it is ListDataSource) - it.listMemberHandler.addMember(listKey, userKey) + it.listMemberHandler.addMember(listId, userKey) } } } @@ -84,7 +84,7 @@ public class EditListMemberPresenter( serviceState.onSuccess { scope.launch { require(it is ListDataSource) - it.listMemberHandler.removeMember(listKey, userKey) + it.listMemberHandler.removeMember(listId, userKey) } } } diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/list/ListEditPresenter.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/list/ListEditPresenter.kt index dd32222d6..198fba132 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/list/ListEditPresenter.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/list/ListEditPresenter.kt @@ -11,7 +11,6 @@ import dev.dimension.flare.data.datasource.microblog.list.ListMetaDataType import dev.dimension.flare.data.repository.AccountRepository import dev.dimension.flare.data.repository.accountServiceProvider import dev.dimension.flare.model.AccountType -import dev.dimension.flare.model.MicroBlogKey import dev.dimension.flare.ui.model.UiState import dev.dimension.flare.ui.model.map import dev.dimension.flare.ui.model.onSuccess @@ -26,7 +25,7 @@ import org.koin.core.component.inject */ public class ListEditPresenter( private val accountType: AccountType, - private val listKey: MicroBlogKey, + private val listId: String, ) : PresenterBase(), KoinComponent { @Immutable @@ -50,23 +49,23 @@ public class ListEditPresenter( val listInfoState = remember( accountType, - listKey, + listId, ) { - ListInfoPresenter(accountType, listKey) + ListInfoPresenter(accountType, listId) }.body() val state = remember( accountType, - listKey, + listId, ) { - EditListMemberPresenter(accountType, listKey) + EditListMemberPresenter(accountType, listId) }.body() val memberState = remember( accountType, - listKey, + listId, ) { - ListMembersPresenter(accountType, listKey) + ListMembersPresenter(accountType, listId) }.body() return object : State, @@ -88,7 +87,7 @@ public class ListEditPresenter( override suspend fun updateList(listMetaData: ListMetaData) { serviceState.onSuccess { require(it is ListDataSource) - it.listHandler.update(listKey, listMetaData) + it.listHandler.update(listId, listMetaData) } } } diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/list/ListInfoPresenter.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/list/ListInfoPresenter.kt index 0752c5627..600ef0fc0 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/list/ListInfoPresenter.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/list/ListInfoPresenter.kt @@ -8,7 +8,6 @@ import dev.dimension.flare.data.datasource.microblog.list.ListDataSource import dev.dimension.flare.data.repository.AccountRepository import dev.dimension.flare.data.repository.accountServiceProvider import dev.dimension.flare.model.AccountType -import dev.dimension.flare.model.MicroBlogKey import dev.dimension.flare.ui.model.UiList import dev.dimension.flare.ui.model.UiState import dev.dimension.flare.ui.model.flatMap @@ -22,7 +21,7 @@ import org.koin.core.component.inject */ public class ListInfoPresenter( private val accountType: AccountType, - private val listKey: MicroBlogKey, + private val listId: String, ) : PresenterBase(), KoinComponent { private val accountRepository: AccountRepository by inject() @@ -34,7 +33,7 @@ public class ListInfoPresenter( serviceState.flatMap { remember(it) { require(it is ListDataSource) - it.listHandler.listInfo(listKey) + it.listHandler.listInfo(listId) }.collectAsState().toUi() } diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/list/ListMembersPresenter.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/list/ListMembersPresenter.kt index a2c4488d5..37a8ce2ce 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/list/ListMembersPresenter.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/list/ListMembersPresenter.kt @@ -12,7 +12,6 @@ import dev.dimension.flare.data.datasource.microblog.list.ListDataSource import dev.dimension.flare.data.repository.AccountRepository import dev.dimension.flare.data.repository.accountServiceProvider import dev.dimension.flare.model.AccountType -import dev.dimension.flare.model.MicroBlogKey import dev.dimension.flare.ui.model.UiProfile import dev.dimension.flare.ui.model.map import dev.dimension.flare.ui.presenter.PresenterBase @@ -24,7 +23,7 @@ import org.koin.core.component.inject */ public class ListMembersPresenter( private val accountType: AccountType, - private val listKey: MicroBlogKey, + private val listId: String, ) : PresenterBase(), KoinComponent { private val accountRepository: AccountRepository by inject() @@ -36,9 +35,9 @@ public class ListMembersPresenter( val memberInfo = serviceState .map { - remember(it, listKey) { + remember(it, listId) { require(it is ListDataSource) - it.listMemberHandler.listMembers(listKey).cachedIn(scope) + it.listMemberHandler.listMembers(listId).cachedIn(scope) }.collectAsLazyPagingItems() }.toPagingState() diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/list/ListTimelinePresenter.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/list/ListTimelinePresenter.kt index 06e394b46..57b128992 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/list/ListTimelinePresenter.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/list/ListTimelinePresenter.kt @@ -5,7 +5,6 @@ import dev.dimension.flare.data.datasource.microblog.paging.BaseTimelineLoader import dev.dimension.flare.data.repository.AccountRepository import dev.dimension.flare.data.repository.accountServiceFlow import dev.dimension.flare.model.AccountType -import dev.dimension.flare.model.MicroBlogKey import dev.dimension.flare.ui.presenter.home.TimelinePresenter import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map @@ -17,7 +16,7 @@ import org.koin.core.component.inject */ public class ListTimelinePresenter( private val accountType: AccountType, - private val listKey: MicroBlogKey, + private val listId: String, ) : TimelinePresenter(), KoinComponent { private val accountRepository: AccountRepository by inject() @@ -28,7 +27,7 @@ public class ListTimelinePresenter( repository = accountRepository, ).map { service -> require(service is ListDataSource) - service.listTimeline(listKey = listKey) + service.listTimeline(listId = listId) } } } diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/list/PinnableTimelineTabPresenter.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/list/PinnableTimelineTabPresenter.kt index 670c2f371..4617b6daf 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/list/PinnableTimelineTabPresenter.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/list/PinnableTimelineTabPresenter.kt @@ -6,13 +6,11 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.paging.cachedIn import androidx.paging.compose.collectAsLazyPagingItems -import androidx.paging.map import dev.dimension.flare.common.ImmutableListWrapper import dev.dimension.flare.common.PagingState import dev.dimension.flare.common.toImmutableListWrapper import dev.dimension.flare.common.toPagingState import dev.dimension.flare.data.datasource.bluesky.BlueskyDataSource -import dev.dimension.flare.data.datasource.microblog.ListDataSource import dev.dimension.flare.data.datasource.microblog.list.ListDataSource import dev.dimension.flare.data.datasource.misskey.MisskeyDataSource import dev.dimension.flare.data.repository.AccountRepository @@ -24,7 +22,6 @@ import dev.dimension.flare.ui.model.map import dev.dimension.flare.ui.model.mapNotNull import dev.dimension.flare.ui.presenter.PresenterBase import kotlinx.collections.immutable.toImmutableList -import kotlinx.coroutines.flow.map import org.koin.core.component.KoinComponent import org.koin.core.component.inject @@ -77,13 +74,15 @@ public class PinnableTimelineTabPresenter( service.listHandler.data.cachedIn(scope) }.collectAsLazyPagingItems() }.toPagingState() - val feeds = serviceState + val feeds = + serviceState .mapNotNull { it as? BlueskyDataSource }.mapNotNull { service -> - val flow = remember(service) { - service.feedHandler.data.cachedIn(scope) - } + val flow = + remember(service) { + service.feedHandler.data.cachedIn(scope) + } flow.collectAsLazyPagingItems() }.toPagingState() @@ -97,15 +96,15 @@ public class PinnableTimelineTabPresenter( }.collectAsLazyPagingItems() }.toPagingState() - val channel = - serviceState - .mapNotNull { - it as? MisskeyDataSource - }.mapNotNull { service -> - remember(service) { - service.channelsList(scope = scope) - }.collectAsLazyPagingItems() - }.toPagingState() +// val channel = +// serviceState +// .mapNotNull { +// it as? MisskeyDataSource +// }.mapNotNull { service -> +// remember(service) { +// service.channelsList(scope = scope) +// }.collectAsLazyPagingItems() +// }.toPagingState() val tabs = serviceState.map { service -> @@ -130,11 +129,11 @@ public class PinnableTimelineTabPresenter( } else { null }, - if (service is MisskeyDataSource) { - State.Tab.Channel(channel) - } else { - null - }, +// if (service is MisskeyDataSource) { +// State.Tab.Channel(channel) +// } else { +// null +// }, ).toImmutableList().toImmutableListWrapper() } } From ba6aa3479536faccbb6428a8c78152537f3bbce6 Mon Sep 17 00:00:00 2001 From: Tlaster Date: Tue, 17 Feb 2026 13:29:23 +0900 Subject: [PATCH 06/14] add test for ListHandler and ListMemberHandler --- .../microblog/list/ListHandlerTest.kt | 372 ++++++++++++++++++ .../microblog/list/ListMemberHandlerTest.kt | 346 ++++++++++++++++ .../rss/RssTimelineRemoteMediatorTest.kt | 4 +- 3 files changed, 720 insertions(+), 2 deletions(-) create mode 100644 shared/src/commonTest/kotlin/dev/dimension/flare/data/datasource/microblog/list/ListHandlerTest.kt create mode 100644 shared/src/commonTest/kotlin/dev/dimension/flare/data/datasource/microblog/list/ListMemberHandlerTest.kt diff --git a/shared/src/commonTest/kotlin/dev/dimension/flare/data/datasource/microblog/list/ListHandlerTest.kt b/shared/src/commonTest/kotlin/dev/dimension/flare/data/datasource/microblog/list/ListHandlerTest.kt new file mode 100644 index 000000000..ef3150732 --- /dev/null +++ b/shared/src/commonTest/kotlin/dev/dimension/flare/data/datasource/microblog/list/ListHandlerTest.kt @@ -0,0 +1,372 @@ +package dev.dimension.flare.data.datasource.microblog.list + +import androidx.paging.testing.asSnapshot +import androidx.room.Room +import androidx.sqlite.driver.bundled.BundledSQLiteDriver +import dev.dimension.flare.RobolectricTest +import dev.dimension.flare.data.database.cache.CacheDatabase +import dev.dimension.flare.data.datasource.microblog.paging.PagingRequest +import dev.dimension.flare.data.datasource.microblog.paging.PagingResult +import dev.dimension.flare.memoryDatabaseBuilder +import dev.dimension.flare.model.AccountType +import dev.dimension.flare.model.MicroBlogKey +import dev.dimension.flare.ui.model.UiList +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.test.runTest +import org.koin.core.context.startKoin +import org.koin.core.context.stopKoin +import org.koin.dsl.module +import kotlin.test.AfterTest +import kotlin.test.BeforeTest +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotNull +import kotlin.test.assertNull +import kotlin.test.assertTrue + +@OptIn(ExperimentalCoroutinesApi::class) +class ListHandlerTest : RobolectricTest() { + private lateinit var db: CacheDatabase + private lateinit var fakeLoader: FakeListLoader + private lateinit var handler: ListHandler + + private val accountKey = MicroBlogKey(id = "testuser", host = "test.social") + private val pagingKey = "test_lists" + + @BeforeTest + fun setup() { + db = + Room + .memoryDatabaseBuilder() + .setDriver(BundledSQLiteDriver()) + .setQueryCoroutineContext(Dispatchers.Unconfined) + .build() + + fakeLoader = FakeListLoader() + + startKoin { + modules( + module { + single { db } + }, + ) + } + + handler = + ListHandler( + pagingKey = pagingKey, + accountKey = accountKey, + loader = fakeLoader, + ) + } + + @AfterTest + fun tearDown() { + db.close() + stopKoin() + } + + @Test + fun createInsertsIntoDatabase() = + runTest { + val metaData = ListMetaData(title = "My List", description = "Description") + fakeLoader.nextCreateResult = + UiList.List( + id = "list-1", + title = "My List", + description = "Description", + ) + + handler.create(metaData) + + // Verify that the list was inserted into the database + val listKey = MicroBlogKey("list-1", accountKey.host) + val dbList = + db + .listDao() + .getList(listKey, AccountType.Specific(accountKey)) + .first() + assertNotNull(dbList) + assertEquals("list-1", dbList.content.data.id) + assertEquals("My List", dbList.content.data.title) + + // Verify paging entry exists + val pagingEntries = db.listDao().getListKeysFlow(pagingKey).first() + assertEquals(1, pagingEntries.size) + assertEquals( + "list-1", + pagingEntries + .first() + .list.content.data.id, + ) + } + + @Test + fun updateModifiesDatabaseContent() = + runTest { + // First, create a list + val createResult = + UiList.List( + id = "list-2", + title = "Original Title", + description = "Original", + ) + fakeLoader.nextCreateResult = createResult + handler.create(ListMetaData(title = "Original Title")) + + // Now update it + val updatedResult = + UiList.List( + id = "list-2", + title = "Updated Title", + description = "Updated Desc", + ) + fakeLoader.nextUpdateResult = updatedResult + + handler.update("list-2", ListMetaData(title = "Updated Title", description = "Updated Desc")) + + // Verify the database content was updated + val listKey = MicroBlogKey("list-2", accountKey.host) + val dbList = + db + .listDao() + .getList(listKey, AccountType.Specific(accountKey)) + .first() + assertNotNull(dbList) + assertEquals("Updated Title", dbList.content.data.title) + } + + @Test + fun deleteRemovesFromDatabase() = + runTest { + // Create a list first + fakeLoader.nextCreateResult = + UiList.List( + id = "list-3", + title = "To Delete", + ) + handler.create(ListMetaData(title = "To Delete")) + + // Verify it exists + val listKey = MicroBlogKey("list-3", accountKey.host) + val before = + db + .listDao() + .getList(listKey, AccountType.Specific(accountKey)) + .first() + assertNotNull(before) + + // Delete it + handler.delete("list-3") + + // Verify the list was removed + val after = + db + .listDao() + .getList(listKey, AccountType.Specific(accountKey)) + .first() + assertNull(after) + + // Verify paging entry was removed + val pagingEntries = db.listDao().getListKeysFlow(pagingKey).first() + assertTrue(pagingEntries.isEmpty()) + } + + @Test + fun insertToDatabaseWritesDirectly() = + runTest { + val list = + UiList.List( + id = "list-4", + title = "Direct Insert", + description = "Directly inserted", + ) + + handler.insertToDatabase(list) + + // Verify the list was inserted + val listKey = MicroBlogKey("list-4", accountKey.host) + val dbList = + db + .listDao() + .getList(listKey, AccountType.Specific(accountKey)) + .first() + assertNotNull(dbList) + assertEquals("Direct Insert", dbList.content.data.title) + + // Verify paging entry exists + val pagingEntries = db.listDao().getListKeysFlow(pagingKey).first() + assertEquals(1, pagingEntries.size) + } + + @Test + fun withDatabaseUpdatesContent() = + runTest { + // Insert a list first + val original = + UiList.List( + id = "list-5", + title = "Before Update", + ) + handler.insertToDatabase(original) + + // Use withDatabase to update content + handler.withDatabase { update -> + update( + UiList.List( + id = "list-5", + title = "After Update", + description = "Updated via withDatabase", + ), + ) + } + + // Verify the content was updated + val listKey = MicroBlogKey("list-5", accountKey.host) + val dbList = + db + .listDao() + .getList(listKey, AccountType.Specific(accountKey)) + .first() + assertNotNull(dbList) + assertEquals("After Update", dbList.content.data.title) + } + + @Test + fun createWithLoaderFailureDoesNotInsert() = + runTest { + fakeLoader.shouldFail = true + + handler.create(ListMetaData(title = "Should Fail")) + + // Verify nothing was inserted into the database + val pagingEntries = db.listDao().getListKeysFlow(pagingKey).first() + assertTrue(pagingEntries.isEmpty()) + } + + @Test + fun deleteWithLoaderFailureDoesNotRemove() = + runTest { + // Create a list first + fakeLoader.nextCreateResult = + UiList.List( + id = "list-6", + title = "Should Survive", + ) + handler.create(ListMetaData(title = "Should Survive")) + + // Now make loader fail and try to delete + fakeLoader.shouldFail = true + handler.delete("list-6") + + // Verify the list is still in the database + val listKey = MicroBlogKey("list-6", accountKey.host) + val dbList = + db + .listDao() + .getList(listKey, AccountType.Specific(accountKey)) + .first() + assertNotNull(dbList) + assertEquals("Should Survive", dbList.content.data.title) + } + + @Test + fun dataLoadsThroughPagerAndSavesToDatabase() = + runTest { + // Pre-populate the fake loader with items + fakeLoader.nextCreateResult = + UiList.List(id = "paged-1", title = "First") + fakeLoader.create(ListMetaData(title = "First")) + fakeLoader.nextCreateResult = + UiList.List(id = "paged-2", title = "Second") + fakeLoader.create(ListMetaData(title = "Second")) + + // Collect the paging data — this triggers the remote mediator + val snapshot = handler.data.asSnapshot() + + assertEquals(2, snapshot.size) + val titles = snapshot.map { it.title }.toSet() + assertTrue(titles.contains("First")) + assertTrue(titles.contains("Second")) + + // Verify items were persisted to the database by the remote mediator + val dbEntries = db.listDao().getListKeysFlow(pagingKey).first() + assertEquals(2, dbEntries.size) + } + + @Test + fun cacheDataReflectsDatabaseState() = + runTest { + // Initially empty + val initial = handler.cacheData.first() + assertTrue(initial.isEmpty()) + + // Insert items via insertToDatabase + handler.insertToDatabase( + UiList.List(id = "cache-1", title = "Cached A"), + ) + handler.insertToDatabase( + UiList.List(id = "cache-2", title = "Cached B"), + ) + + // cacheData should now reflect the two inserted items + val cached = handler.cacheData.first() + assertEquals(2, cached.size) + val titles = cached.map { it.title }.toSet() + assertTrue(titles.contains("Cached A")) + assertTrue(titles.contains("Cached B")) + + // Delete one and verify cacheData updates + fakeLoader.shouldFail = false + handler.delete("cache-1") + val afterDelete = handler.cacheData.first() + assertEquals(1, afterDelete.size) + assertEquals("Cached B", afterDelete.first().title) + } +} + +private class FakeListLoader : ListLoader { + var nextCreateResult: UiList = UiList.List(id = "default", title = "Default") + var nextUpdateResult: UiList = UiList.List(id = "default", title = "Default") + var shouldFail: Boolean = false + + private val items = mutableListOf() + + override val supportedMetaData: ImmutableList = + persistentListOf(ListMetaDataType.TITLE, ListMetaDataType.DESCRIPTION) + + override suspend fun load( + pageSize: Int, + request: PagingRequest, + ): PagingResult = + PagingResult( + data = items.toList(), + endOfPaginationReached = true, + ) + + override suspend fun info(listId: String): UiList = items.first { it.id == listId } + + override suspend fun create(metaData: ListMetaData): UiList { + if (shouldFail) throw RuntimeException("Fake loader failure") + items.add(nextCreateResult) + return nextCreateResult + } + + override suspend fun update( + listId: String, + metaData: ListMetaData, + ): UiList { + if (shouldFail) throw RuntimeException("Fake loader failure") + items.replaceAll { if (it.id == listId) nextUpdateResult else it } + return nextUpdateResult + } + + override suspend fun delete(listId: String) { + if (shouldFail) throw RuntimeException("Fake loader failure") + items.removeAll { it.id == listId } + } +} diff --git a/shared/src/commonTest/kotlin/dev/dimension/flare/data/datasource/microblog/list/ListMemberHandlerTest.kt b/shared/src/commonTest/kotlin/dev/dimension/flare/data/datasource/microblog/list/ListMemberHandlerTest.kt new file mode 100644 index 000000000..867ae15cd --- /dev/null +++ b/shared/src/commonTest/kotlin/dev/dimension/flare/data/datasource/microblog/list/ListMemberHandlerTest.kt @@ -0,0 +1,346 @@ +package dev.dimension.flare.data.datasource.microblog.list + +import androidx.paging.testing.asSnapshot +import androidx.room.Room +import androidx.sqlite.driver.bundled.BundledSQLiteDriver +import dev.dimension.flare.RobolectricTest +import dev.dimension.flare.data.database.cache.CacheDatabase +import dev.dimension.flare.data.database.cache.model.DbUser +import dev.dimension.flare.data.database.cache.model.UserContent +import dev.dimension.flare.data.datasource.microblog.paging.PagingRequest +import dev.dimension.flare.data.datasource.microblog.paging.PagingResult +import dev.dimension.flare.data.network.mastodon.api.model.Account +import dev.dimension.flare.memoryDatabaseBuilder +import dev.dimension.flare.model.MicroBlogKey +import dev.dimension.flare.model.PlatformType +import dev.dimension.flare.ui.humanizer.PlatformFormatter +import dev.dimension.flare.ui.model.UiList +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.test.runTest +import org.koin.core.context.startKoin +import org.koin.core.context.stopKoin +import org.koin.dsl.module +import kotlin.test.AfterTest +import kotlin.test.BeforeTest +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertTrue +import kotlin.time.Instant + +@OptIn(ExperimentalCoroutinesApi::class) +class ListMemberHandlerTest : RobolectricTest() { + private lateinit var db: CacheDatabase + private lateinit var fakeLoader: FakeListMemberLoader + private lateinit var handler: ListMemberHandler + + private val accountKey = MicroBlogKey(id = "testuser", host = "test.social") + private val pagingKey = "test_members" + + @BeforeTest + fun setup() { + db = + Room + .memoryDatabaseBuilder() + .setDriver(BundledSQLiteDriver()) + .setQueryCoroutineContext(Dispatchers.Unconfined) + .build() + + fakeLoader = FakeListMemberLoader() + + startKoin { + modules( + module { + single { db } + single { + object : PlatformFormatter { + override fun formatNumber(number: Long) = number.toString() + + override fun formatRelativeInstant(instant: Instant) = instant.toString() + + override fun formatFullInstant(instant: Instant) = instant.toString() + + override fun formatAbsoluteInstant(instant: Instant) = instant.toString() + } + } + }, + ) + } + + handler = + ListMemberHandler( + pagingKey = pagingKey, + accountKey = accountKey, + loader = fakeLoader, + ) + } + + @AfterTest + fun tearDown() { + db.close() + stopKoin() + } + + @Test + fun addMemberInsertsMemberAndUserIntoDatabase() = + runTest { + val userKey = MicroBlogKey(id = "user-1", host = "test.social") + val listId = "list-1" + fakeLoader.nextAddMemberResult = createDbUser(userKey) + + handler.addMember(listId, userKey) + + // Verify DbListMember was inserted + val listKey = MicroBlogKey(listId, accountKey.host) + val members = db.listDao().getListMembersFlow(listKey).first() + assertEquals(1, members.size) + assertEquals(userKey, members.first().member.memberKey) + + // Verify DbUser was inserted + val savedUser = db.userDao().findByKey(userKey).first() + assertEquals("user-1", savedUser?.userKey?.id) + } + + @Test + fun addMultipleMembersToSameList() = + runTest { + val listId = "list-2" + val userKey1 = MicroBlogKey(id = "user-a", host = "test.social") + val userKey2 = MicroBlogKey(id = "user-b", host = "test.social") + + fakeLoader.nextAddMemberResult = createDbUser(userKey1) + handler.addMember(listId, userKey1) + + fakeLoader.nextAddMemberResult = createDbUser(userKey2) + handler.addMember(listId, userKey2) + + val listKey = MicroBlogKey(listId, accountKey.host) + val members = db.listDao().getListMembersFlow(listKey).first() + assertEquals(2, members.size) + val memberKeys = members.map { it.member.memberKey }.toSet() + assertTrue(memberKeys.contains(userKey1)) + assertTrue(memberKeys.contains(userKey2)) + } + + @Test + fun removeMemberDeletesFromDatabase() = + runTest { + val userKey = MicroBlogKey(id = "user-3", host = "test.social") + val listId = "list-3" + fakeLoader.nextAddMemberResult = createDbUser(userKey) + + // Add first + handler.addMember(listId, userKey) + + val listKey = MicroBlogKey(listId, accountKey.host) + val before = db.listDao().getListMembersFlow(listKey).first() + assertEquals(1, before.size) + + // Remove + handler.removeMember(listId, userKey) + + val after = db.listDao().getListMembersFlow(listKey).first() + assertTrue(after.isEmpty()) + } + + @Test + fun removeMemberOnlyRemovesTargetMember() = + runTest { + val listId = "list-4" + val userKey1 = MicroBlogKey(id = "user-keep", host = "test.social") + val userKey2 = MicroBlogKey(id = "user-remove", host = "test.social") + + fakeLoader.nextAddMemberResult = createDbUser(userKey1) + handler.addMember(listId, userKey1) + fakeLoader.nextAddMemberResult = createDbUser(userKey2) + handler.addMember(listId, userKey2) + + // Remove only userKey2 + handler.removeMember(listId, userKey2) + + val listKey = MicroBlogKey(listId, accountKey.host) + val remaining = db.listDao().getListMembersFlow(listKey).first() + assertEquals(1, remaining.size) + assertEquals(userKey1, remaining.first().member.memberKey) + } + + @Test + fun addMemberWithLoaderFailureDoesNotInsert() = + runTest { + val userKey = MicroBlogKey(id = "user-fail", host = "test.social") + fakeLoader.shouldFail = true + + handler.addMember("list-5", userKey) + + val listKey = MicroBlogKey("list-5", accountKey.host) + val members = db.listDao().getListMembersFlow(listKey).first() + assertTrue(members.isEmpty()) + } + + @Test + fun removeMemberWithLoaderFailureDoesNotDelete() = + runTest { + val userKey = MicroBlogKey(id = "user-survive", host = "test.social") + val listId = "list-6" + fakeLoader.nextAddMemberResult = createDbUser(userKey) + handler.addMember(listId, userKey) + + // Now make loader fail and try to remove + fakeLoader.shouldFail = true + handler.removeMember(listId, userKey) + + // Member should still be there + val listKey = MicroBlogKey(listId, accountKey.host) + val members = db.listDao().getListMembersFlow(listKey).first() + assertEquals(1, members.size) + assertEquals(userKey, members.first().member.memberKey) + } + + @Test + fun listMembersFlowReflectsDatabaseState() = + runTest { + val listId = "list-7" + val listKey = MicroBlogKey(listId, accountKey.host) + val userKey1 = MicroBlogKey(id = "member-1", host = "test.social") + val userKey2 = MicroBlogKey(id = "member-2", host = "test.social") + + // Initially empty + val initial = db.listDao().getListMembersFlow(listKey).first() + assertTrue(initial.isEmpty()) + + // Add members + fakeLoader.nextAddMemberResult = createDbUser(userKey1, name = "Alice") + handler.addMember(listId, userKey1) + fakeLoader.nextAddMemberResult = createDbUser(userKey2, name = "Bob") + handler.addMember(listId, userKey2) + + // Flow should reflect 2 members + val members = db.listDao().getListMembersFlow(listKey).first() + assertEquals(2, members.size) + val memberKeys = members.map { it.member.memberKey }.toSet() + assertTrue(memberKeys.contains(userKey1)) + assertTrue(memberKeys.contains(userKey2)) + + // Remove one, flow should reflect 1 member + fakeLoader.shouldFail = false + handler.removeMember(listId, userKey1) + val afterRemove = db.listDao().getListMembersFlow(listKey).first() + assertEquals(1, afterRemove.size) + assertEquals(userKey2, afterRemove.first().member.memberKey) + } + + @Test + fun listMembersListFlowRendersUserProfiles() = + runTest { + val listId = "list-render" + val userKey1 = MicroBlogKey(id = "render-1", host = "test.social") + val userKey2 = MicroBlogKey(id = "render-2", host = "test.social") + + fakeLoader.nextAddMemberResult = createDbUser(userKey1, name = "Alice") + handler.addMember(listId, userKey1) + fakeLoader.nextAddMemberResult = createDbUser(userKey2, name = "Bob") + handler.addMember(listId, userKey2) + + // listMembersListFlow calls render() — verify it returns UiUserV2 items + val rendered = handler.listMembersListFlow(listId).first() + assertEquals(2, rendered.size) + val names = rendered.map { it.name.raw }.toSet() + assertTrue(names.contains("Alice")) + assertTrue(names.contains("Bob")) + } + + @Test + fun listMembersPagerLoadsAndSavesToDatabase() = + runTest { + val listId = "list-pager" + val userKey1 = MicroBlogKey(id = "paged-1", host = "test.social") + val userKey2 = MicroBlogKey(id = "paged-2", host = "test.social") + + // Pre-populate the fake loader with members + fakeLoader.nextAddMemberResult = createDbUser(userKey1, name = "Paged Alice") + fakeLoader.addMember(listId, userKey1) + fakeLoader.nextAddMemberResult = createDbUser(userKey2, name = "Paged Bob") + fakeLoader.addMember(listId, userKey2) + + // Collect the paging data — this triggers the remote mediator + val snapshot = handler.listMembers(listId).asSnapshot() + + assertEquals(2, snapshot.size) + val names = snapshot.map { it.name.raw }.toSet() + assertTrue(names.contains("Paged Alice")) + assertTrue(names.contains("Paged Bob")) + + // Verify members were persisted to the database + val listKey = MicroBlogKey(listId, accountKey.host) + val dbMembers = db.listDao().getListMembersFlow(listKey).first() + assertEquals(2, dbMembers.size) + } + + private fun createDbUser( + userKey: MicroBlogKey, + name: String = userKey.id, + ): DbUser = + DbUser( + userKey = userKey, + platformType = PlatformType.Mastodon, + name = name, + handle = userKey.id, + host = userKey.host, + content = + UserContent.Mastodon( + Account( + id = userKey.id, + username = userKey.id, + acct = "${userKey.id}@${userKey.host}", + displayName = name, + url = "https://${userKey.host}/@${userKey.id}", + ), + ), + ) +} + +private class FakeListMemberLoader : ListMemberLoader { + var nextAddMemberResult: DbUser? = null + var shouldFail: Boolean = false + + private val members = mutableMapOf>() + + override suspend fun loadMembers( + pageSize: Int, + request: PagingRequest, + listId: String, + ): PagingResult = + PagingResult( + data = members[listId]?.toList() ?: emptyList(), + endOfPaginationReached = true, + ) + + override suspend fun addMember( + listId: String, + userKey: MicroBlogKey, + ): DbUser { + if (shouldFail) throw RuntimeException("Fake loader failure") + val user = nextAddMemberResult ?: throw IllegalStateException("nextAddMemberResult not set") + members.getOrPut(listId) { mutableListOf() }.add(user) + return user + } + + override suspend fun removeMember( + listId: String, + userKey: MicroBlogKey, + ) { + if (shouldFail) throw RuntimeException("Fake loader failure") + members[listId]?.removeAll { it.userKey == userKey } + } + + override suspend fun loadUserLists( + pageSize: Int, + request: PagingRequest, + userKey: MicroBlogKey, + ): PagingResult = + PagingResult( + data = emptyList(), + endOfPaginationReached = true, + ) +} diff --git a/shared/src/commonTest/kotlin/dev/dimension/flare/data/datasource/rss/RssTimelineRemoteMediatorTest.kt b/shared/src/commonTest/kotlin/dev/dimension/flare/data/datasource/rss/RssTimelineRemoteMediatorTest.kt index 6eabcefeb..562e2067d 100644 --- a/shared/src/commonTest/kotlin/dev/dimension/flare/data/datasource/rss/RssTimelineRemoteMediatorTest.kt +++ b/shared/src/commonTest/kotlin/dev/dimension/flare/data/datasource/rss/RssTimelineRemoteMediatorTest.kt @@ -4,7 +4,7 @@ import androidx.room.Room import androidx.sqlite.driver.bundled.BundledSQLiteDriver import dev.dimension.flare.RobolectricTest import dev.dimension.flare.data.database.cache.CacheDatabase -import dev.dimension.flare.data.datasource.microblog.paging.BaseTimelineRemoteMediator +import dev.dimension.flare.data.datasource.microblog.paging.PagingRequest import dev.dimension.flare.data.network.rss.model.Feed import dev.dimension.flare.memoryDatabaseBuilder import dev.dimension.flare.ui.model.mapper.parseRssDateToInstant @@ -86,7 +86,7 @@ class RssTimelineRemoteMediatorTest : RobolectricTest() { val result = mediator.timeline( pageSize = 10, - request = BaseTimelineRemoteMediator.Request.Refresh, + request = PagingRequest.Refresh, ) val sortIds = result.data.map { it.timeline.sortId }.sortedDescending() From a7b7ca2aab581d84e328b50bee01503c6a4d16b6 Mon Sep 17 00:00:00 2001 From: Tlaster Date: Tue, 17 Feb 2026 14:39:07 +0900 Subject: [PATCH 07/14] add misskey channel presenters --- .../misskey/MisskeyChannelLoader.kt | 113 ++++++++++++++ .../datasource/misskey/MisskeyDataSource.kt | 126 +++++++++++++++ .../data/network/misskey/api/ChannelsApi.kt | 2 +- .../misskey/MisskeyBaseChannelPresenter.kt | 83 ++++++++++ .../home/misskey/MisskeyChannelPresenter.kt | 143 ++++++++++++++++++ .../MisskeyChannelTimelinePresenter.kt | 30 ++++ .../home/misskey/MisskeyChannelsState.kt | 20 +++ .../MisskeyFavoriteChannelsPresenter.kt | 33 ++++ .../MisskeyFeaturedChannelsPresenter.kt | 32 ++++ .../MisskeyFollowedChannelsPresenter.kt | 33 ++++ .../misskey/MisskeyOwnedChannelsPresenter.kt | 33 ++++ .../list/PinnableTimelineTabPresenter.kt | 28 ++-- 12 files changed, 661 insertions(+), 15 deletions(-) create mode 100644 shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/misskey/MisskeyChannelLoader.kt create mode 100644 shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/home/misskey/MisskeyBaseChannelPresenter.kt create mode 100644 shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/home/misskey/MisskeyChannelPresenter.kt create mode 100644 shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/home/misskey/MisskeyChannelTimelinePresenter.kt create mode 100644 shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/home/misskey/MisskeyChannelsState.kt create mode 100644 shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/home/misskey/MisskeyFavoriteChannelsPresenter.kt create mode 100644 shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/home/misskey/MisskeyFeaturedChannelsPresenter.kt create mode 100644 shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/home/misskey/MisskeyFollowedChannelsPresenter.kt create mode 100644 shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/home/misskey/MisskeyOwnedChannelsPresenter.kt diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/misskey/MisskeyChannelLoader.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/misskey/MisskeyChannelLoader.kt new file mode 100644 index 000000000..c41461343 --- /dev/null +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/misskey/MisskeyChannelLoader.kt @@ -0,0 +1,113 @@ +package dev.dimension.flare.data.datasource.misskey + +import dev.dimension.flare.data.datasource.microblog.list.ListLoader +import dev.dimension.flare.data.datasource.microblog.list.ListMetaData +import dev.dimension.flare.data.datasource.microblog.list.ListMetaDataType +import dev.dimension.flare.data.datasource.microblog.paging.PagingRequest +import dev.dimension.flare.data.datasource.microblog.paging.PagingResult +import dev.dimension.flare.data.network.misskey.MisskeyService +import dev.dimension.flare.data.network.misskey.api.model.ChannelsCreateRequest +import dev.dimension.flare.data.network.misskey.api.model.ChannelsFollowRequest +import dev.dimension.flare.data.network.misskey.api.model.ChannelsFollowedRequest +import dev.dimension.flare.data.network.misskey.api.model.ChannelsUpdateRequest +import dev.dimension.flare.model.MicroBlogKey +import dev.dimension.flare.ui.model.UiList +import dev.dimension.flare.ui.model.mapper.render +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toImmutableList + +internal class MisskeyChannelLoader( + private val service: MisskeyService, + private val accountKey: MicroBlogKey, + private val source: Source, +) : ListLoader { + enum class Source { + Followed, + MyFavorites, + Owned, + } + + override suspend fun load( + pageSize: Int, + request: PagingRequest, + ): PagingResult { + if (request is PagingRequest.Prepend) { + return PagingResult() + } + val untilId = + when (request) { + is PagingRequest.Append -> request.nextKey + else -> null + } + + val result = + when (source) { + Source.Followed -> + service.channelsFollowed( + ChannelsFollowedRequest( + untilId = untilId, + limit = pageSize, + ), + ) + + Source.MyFavorites -> + service.channelsMyFavorites( + ChannelsFollowedRequest( + untilId = untilId, + limit = pageSize, + ), + ) + + Source.Owned -> + service.channelsOwned( + ChannelsFollowedRequest( + untilId = untilId, + limit = pageSize, + ), + ) + }.map { + it.render() + }.toImmutableList() + + return PagingResult( + data = result, + nextKey = result.lastOrNull()?.id, + ) + } + + override suspend fun info(listId: String): UiList = + service + .channelsShow( + ChannelsFollowRequest( + channelId = listId, + ), + ).render() + + override suspend fun create(metaData: ListMetaData): UiList = + service + .channelsCreate( + ChannelsCreateRequest( + name = metaData.title, + description = metaData.description, + ), + ).render() + + override suspend fun update( + listId: String, + metaData: ListMetaData, + ): UiList = + service + .channelsUpdate( + ChannelsUpdateRequest( + channelId = listId, + name = metaData.title, + description = metaData.description, + ), + ).render() + + override suspend fun delete(listId: String): Unit = throw UnsupportedOperationException("Delete channel is not supported") + + override val supportedMetaData: ImmutableList + get() = persistentListOf(ListMetaDataType.TITLE, ListMetaDataType.DESCRIPTION) +} diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/misskey/MisskeyDataSource.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/misskey/MisskeyDataSource.kt index 19cd447f0..3f3a3318c 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/misskey/MisskeyDataSource.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/misskey/MisskeyDataSource.kt @@ -3,7 +3,10 @@ package dev.dimension.flare.data.datasource.misskey import androidx.paging.ExperimentalPagingApi import androidx.paging.Pager import androidx.paging.PagingData +import androidx.paging.PagingState import androidx.paging.cachedIn +import androidx.paging.map +import dev.dimension.flare.common.BasePagingSource import dev.dimension.flare.common.CacheData import dev.dimension.flare.common.Cacheable import dev.dimension.flare.common.FileType @@ -35,6 +38,8 @@ import dev.dimension.flare.data.datasource.microblog.pagingConfig import dev.dimension.flare.data.datasource.microblog.relationKeyWithUserKey import dev.dimension.flare.data.datasource.microblog.timelinePager import dev.dimension.flare.data.network.misskey.api.model.AdminAccountsDeleteRequest +import dev.dimension.flare.data.network.misskey.api.model.ChannelsFeaturedRequest +import dev.dimension.flare.data.network.misskey.api.model.ChannelsFollowRequest import dev.dimension.flare.data.network.misskey.api.model.IPinRequest import dev.dimension.flare.data.network.misskey.api.model.MuteCreateRequest import dev.dimension.flare.data.network.misskey.api.model.NotesCreateRequest @@ -69,6 +74,7 @@ import kotlinx.collections.immutable.toImmutableMap import kotlinx.collections.immutable.toPersistentList import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.map @@ -156,6 +162,51 @@ internal class MisskeyDataSource( database, ) + fun featuredChannels(scope: CoroutineScope): Flow> = + Pager( + config = pagingConfig, + ) { + object : BasePagingSource() { + override fun getRefreshKey(state: PagingState): String? = null + + override suspend fun doLoad(params: LoadParams): LoadResult { + val result = + service + .channelsFeatured( + request = + ChannelsFeaturedRequest( + limit = params.loadSize, + ), + ) + return LoadResult.Page( + data = + result.map { + it.render() + }, + prevKey = null, + nextKey = null, + ) + } + } + }.flow + .cachedIn(scope) + .let { channels -> + combine( + channels, + channelHandler.cacheData, + ) { featured, followed -> + featured.map { item -> + if (item is UiList.Channel) { + item.copy( + isFollowing = followed.any { it.id == item.id }, + ) + } else { + item + } + } + } + }.cachedIn(scope) + override fun notification( type: NotificationFilter, pageSize: Int, @@ -1046,6 +1097,81 @@ internal class MisskeyDataSource( ) } + val channelHandler: ListHandler by lazy { + ListHandler( + pagingKey = "followedChannels_$accountKey", + accountKey = accountKey, + loader = + MisskeyChannelLoader( + service = service, + accountKey = accountKey, + source = MisskeyChannelLoader.Source.Followed, + ), + ) + } + + val myFavoriteChannelHandler: ListHandler by lazy { + ListHandler( + pagingKey = "myFavoriteChannels_$accountKey", + accountKey = accountKey, + loader = + MisskeyChannelLoader( + service = service, + accountKey = accountKey, + source = MisskeyChannelLoader.Source.MyFavorites, + ), + ) + } + + val ownedChannelHandler: ListHandler by lazy { + ListHandler( + pagingKey = "ownedChannels_$accountKey", + accountKey = accountKey, + loader = + MisskeyChannelLoader( + service = service, + accountKey = accountKey, + source = MisskeyChannelLoader.Source.Owned, + ), + ) + } + + suspend fun followChannel(data: UiList) { + tryRun { + service.channelsFollow( + ChannelsFollowRequest(channelId = data.id), + ) + channelHandler.insertToDatabase(data) + } + } + + suspend fun unfollowChannel(data: UiList) { + tryRun { + service.channelsUnfollow( + ChannelsFollowRequest(channelId = data.id), + ) + } + channelHandler.delete(data.id) + } + + suspend fun favoriteChannel(data: UiList) { + tryRun { + service.channelsFavorite( + ChannelsFollowRequest(channelId = data.id), + ) + myFavoriteChannelHandler.insertToDatabase(data) + } + } + + suspend fun unfavoriteChannel(data: UiList) { + tryRun { + service.channelsUnfavorite( + ChannelsFollowRequest(channelId = data.id), + ) + } + myFavoriteChannelHandler.delete(data.id) + } + override fun acceptFollowRequest( userKey: MicroBlogKey, notificationStatusKey: MicroBlogKey, diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/network/misskey/api/ChannelsApi.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/network/misskey/api/ChannelsApi.kt index 111257bbe..0c327604f 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/data/network/misskey/api/ChannelsApi.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/data/network/misskey/api/ChannelsApi.kt @@ -119,7 +119,7 @@ internal interface ChannelsApi { */ @POST("channels/my-favorites") suspend fun channelsMyFavorites( - @Body body: kotlin.Any, + @Body channelsFollowedRequest: ChannelsFollowedRequest, ): kotlin.collections.List /** diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/home/misskey/MisskeyBaseChannelPresenter.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/home/misskey/MisskeyBaseChannelPresenter.kt new file mode 100644 index 000000000..031784730 --- /dev/null +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/home/misskey/MisskeyBaseChannelPresenter.kt @@ -0,0 +1,83 @@ +package dev.dimension.flare.ui.presenter.home.misskey + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.rememberCoroutineScope +import dev.dimension.flare.common.PagingState +import dev.dimension.flare.common.refreshSuspend +import dev.dimension.flare.data.datasource.microblog.MicroblogDataSource +import dev.dimension.flare.data.datasource.misskey.MisskeyDataSource +import dev.dimension.flare.data.repository.AccountRepository +import dev.dimension.flare.data.repository.accountServiceProvider +import dev.dimension.flare.model.AccountType +import dev.dimension.flare.ui.model.UiList +import dev.dimension.flare.ui.model.UiState +import dev.dimension.flare.ui.model.onSuccess +import dev.dimension.flare.ui.presenter.PresenterBase +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch +import org.koin.core.component.KoinComponent +import org.koin.core.component.inject + +public abstract class MisskeyBaseChannelPresenter( + private val accountType: AccountType, +) : PresenterBase(), + KoinComponent { + private val accountRepository: AccountRepository by inject() + + @Composable + internal abstract fun getPagingData( + scope: CoroutineScope, + serviceState: UiState, + ): PagingState + + @Composable + override fun body(): MisskeyChannelsState { + val scope = rememberCoroutineScope() + val serviceState = accountServiceProvider(accountType = accountType, repository = accountRepository) + val data = getPagingData(scope, serviceState) + + return object : MisskeyChannelsState { + override val data = data + + override suspend fun refreshSuspend() { + data.refreshSuspend() + } + + override fun follow(list: UiList) { + serviceState.onSuccess { + scope.launch { + require(it is MisskeyDataSource) + it.followChannel(list) + } + } + } + + override fun unfollow(list: UiList) { + serviceState.onSuccess { + scope.launch { + require(it is MisskeyDataSource) + it.unfollowChannel(list) + } + } + } + + override fun favorite(list: UiList) { + serviceState.onSuccess { + scope.launch { + require(it is MisskeyDataSource) + it.favoriteChannel(list) + } + } + } + + override fun unfavorite(list: UiList) { + serviceState.onSuccess { + scope.launch { + require(it is MisskeyDataSource) + it.unfavoriteChannel(list) + } + } + } + } + } +} diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/home/misskey/MisskeyChannelPresenter.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/home/misskey/MisskeyChannelPresenter.kt new file mode 100644 index 000000000..56c3b4a67 --- /dev/null +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/home/misskey/MisskeyChannelPresenter.kt @@ -0,0 +1,143 @@ +package dev.dimension.flare.ui.presenter.home.misskey + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Immutable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import dev.dimension.flare.common.PagingState +import dev.dimension.flare.common.refreshSuspend +import dev.dimension.flare.data.datasource.misskey.MisskeyDataSource +import dev.dimension.flare.data.repository.AccountRepository +import dev.dimension.flare.data.repository.accountServiceFlow +import dev.dimension.flare.data.repository.accountServiceProvider +import dev.dimension.flare.model.AccountType +import dev.dimension.flare.ui.model.UiList +import dev.dimension.flare.ui.model.UiState +import dev.dimension.flare.ui.model.UiTimeline +import dev.dimension.flare.ui.model.collectAsUiState +import dev.dimension.flare.ui.model.flatMap +import dev.dimension.flare.ui.model.flattenUiState +import dev.dimension.flare.ui.model.map +import dev.dimension.flare.ui.model.mapNotNull +import dev.dimension.flare.ui.model.onSuccess +import dev.dimension.flare.ui.model.toUi +import dev.dimension.flare.ui.presenter.PresenterBase +import dev.dimension.flare.ui.presenter.home.TimelinePresenter +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.launch +import org.koin.core.component.KoinComponent +import org.koin.core.component.inject + +public class MisskeyChannelPresenter( + private val accountType: AccountType, + private val channelId: String, +) : PresenterBase(), + KoinComponent { + private val accountRepository: AccountRepository by inject() + + private val timelinePresenter by lazy { + object : TimelinePresenter() { + override val loader by lazy { + accountServiceFlow(accountType, accountRepository) + .map { + require(it is MisskeyDataSource) + it.channelTimelineLoader(channelId) + } + } + } + } + + @OptIn(ExperimentalCoroutinesApi::class) + private val infoFlow by lazy { + accountServiceFlow(accountType, accountRepository) + .flatMapLatest { + require(it is MisskeyDataSource) + it.channelHandler.listInfo(channelId).toUi() + }.map { + it.mapNotNull { it as? UiList.Channel } + } + } + + @Composable + override fun body(): MisskeyChannelState { + val scope = rememberCoroutineScope() + val serviceState = accountServiceProvider(accountType = accountType, repository = accountRepository) + val timeline = timelinePresenter.body().listState + val info by infoFlow.flattenUiState() + val followed = + serviceState + .flatMap { + require(it is MisskeyDataSource) + remember(it) { + it.channelHandler.cacheData + }.collectAsUiState().value + }.map { + it.any { it.id == channelId } + } + return object : MisskeyChannelState { + override val info = info + override val timeline = timeline + override val followed = followed + + override suspend fun refreshSuspend() { + timeline.refreshSuspend() + } + + override fun follow(list: UiList) { + serviceState.onSuccess { + scope.launch { + require(it is MisskeyDataSource) + it.followChannel(list) + } + } + } + + override fun unfollow(list: UiList) { + serviceState.onSuccess { + scope.launch { + require(it is MisskeyDataSource) + it.unfollowChannel(list) + } + } + } + + override fun favorite(list: UiList) { + serviceState.onSuccess { + scope.launch { + require(it is MisskeyDataSource) + it.favoriteChannel(list) + } + } + } + + override fun unfavorite(list: UiList) { + serviceState.onSuccess { + scope.launch { + require(it is MisskeyDataSource) + it.unfavoriteChannel(list) + } + } + } + } + } +} + +@Immutable +public interface MisskeyChannelState { + public val info: UiState + public val timeline: PagingState + public val followed: UiState + + public suspend fun refreshSuspend() + + public fun follow(list: UiList) + + public fun unfollow(list: UiList) + + public fun favorite(list: UiList) + + public fun unfavorite(list: UiList) +} diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/home/misskey/MisskeyChannelTimelinePresenter.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/home/misskey/MisskeyChannelTimelinePresenter.kt new file mode 100644 index 000000000..07e081242 --- /dev/null +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/home/misskey/MisskeyChannelTimelinePresenter.kt @@ -0,0 +1,30 @@ +package dev.dimension.flare.ui.presenter.home.misskey + +import dev.dimension.flare.data.datasource.microblog.paging.BaseTimelineLoader +import dev.dimension.flare.data.datasource.misskey.MisskeyDataSource +import dev.dimension.flare.data.repository.AccountRepository +import dev.dimension.flare.data.repository.accountServiceFlow +import dev.dimension.flare.model.AccountType +import dev.dimension.flare.ui.presenter.home.TimelinePresenter +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map +import org.koin.core.component.KoinComponent +import org.koin.core.component.inject + +public class MisskeyChannelTimelinePresenter( + private val accountType: AccountType, + private val channelId: String, +) : TimelinePresenter(), + KoinComponent { + private val accountRepository: AccountRepository by inject() + + override val loader: Flow by lazy { + accountServiceFlow( + accountType = accountType, + repository = accountRepository, + ).map { service -> + require(service is MisskeyDataSource) + service.channelTimelineLoader(channelId) + } + } +} diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/home/misskey/MisskeyChannelsState.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/home/misskey/MisskeyChannelsState.kt new file mode 100644 index 000000000..c665071fa --- /dev/null +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/home/misskey/MisskeyChannelsState.kt @@ -0,0 +1,20 @@ +package dev.dimension.flare.ui.presenter.home.misskey + +import androidx.compose.runtime.Immutable +import dev.dimension.flare.common.PagingState +import dev.dimension.flare.ui.model.UiList + +@Immutable +public interface MisskeyChannelsState { + public val data: PagingState + + public suspend fun refreshSuspend() + + public fun follow(list: UiList) + + public fun unfollow(list: UiList) + + public fun favorite(list: UiList) + + public fun unfavorite(list: UiList) +} diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/home/misskey/MisskeyFavoriteChannelsPresenter.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/home/misskey/MisskeyFavoriteChannelsPresenter.kt new file mode 100644 index 000000000..8364d33ca --- /dev/null +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/home/misskey/MisskeyFavoriteChannelsPresenter.kt @@ -0,0 +1,33 @@ +package dev.dimension.flare.ui.presenter.home.misskey + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.paging.cachedIn +import androidx.paging.compose.LazyPagingItems +import androidx.paging.compose.collectAsLazyPagingItems +import dev.dimension.flare.common.PagingState +import dev.dimension.flare.common.toPagingState +import dev.dimension.flare.data.datasource.microblog.MicroblogDataSource +import dev.dimension.flare.data.datasource.misskey.MisskeyDataSource +import dev.dimension.flare.model.AccountType +import dev.dimension.flare.ui.model.UiList +import dev.dimension.flare.ui.model.UiState +import dev.dimension.flare.ui.model.map +import kotlinx.coroutines.CoroutineScope + +public class MisskeyFavoriteChannelsPresenter( + accountType: AccountType, +) : MisskeyBaseChannelPresenter(accountType) { + @Composable + internal override fun getPagingData( + scope: CoroutineScope, + serviceState: UiState, + ): PagingState = + serviceState + .map> { service -> + require(service is MisskeyDataSource) + remember(service) { + service.myFavoriteChannelHandler.data.cachedIn(scope) + }.collectAsLazyPagingItems() + }.toPagingState() +} diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/home/misskey/MisskeyFeaturedChannelsPresenter.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/home/misskey/MisskeyFeaturedChannelsPresenter.kt new file mode 100644 index 000000000..7fcd8d0b4 --- /dev/null +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/home/misskey/MisskeyFeaturedChannelsPresenter.kt @@ -0,0 +1,32 @@ +package dev.dimension.flare.ui.presenter.home.misskey + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.paging.compose.LazyPagingItems +import androidx.paging.compose.collectAsLazyPagingItems +import dev.dimension.flare.common.PagingState +import dev.dimension.flare.common.toPagingState +import dev.dimension.flare.data.datasource.microblog.MicroblogDataSource +import dev.dimension.flare.data.datasource.misskey.MisskeyDataSource +import dev.dimension.flare.model.AccountType +import dev.dimension.flare.ui.model.UiList +import dev.dimension.flare.ui.model.UiState +import dev.dimension.flare.ui.model.map +import kotlinx.coroutines.CoroutineScope + +public class MisskeyFeaturedChannelsPresenter( + accountType: AccountType, +) : MisskeyBaseChannelPresenter(accountType) { + @Composable + internal override fun getPagingData( + scope: CoroutineScope, + serviceState: UiState, + ): PagingState = + serviceState + .map> { service -> + require(service is MisskeyDataSource) + remember(service) { + service.featuredChannels(scope) + }.collectAsLazyPagingItems() + }.toPagingState() +} diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/home/misskey/MisskeyFollowedChannelsPresenter.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/home/misskey/MisskeyFollowedChannelsPresenter.kt new file mode 100644 index 000000000..fe236adc8 --- /dev/null +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/home/misskey/MisskeyFollowedChannelsPresenter.kt @@ -0,0 +1,33 @@ +package dev.dimension.flare.ui.presenter.home.misskey + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.paging.cachedIn +import androidx.paging.compose.LazyPagingItems +import androidx.paging.compose.collectAsLazyPagingItems +import dev.dimension.flare.common.PagingState +import dev.dimension.flare.common.toPagingState +import dev.dimension.flare.data.datasource.microblog.MicroblogDataSource +import dev.dimension.flare.data.datasource.misskey.MisskeyDataSource +import dev.dimension.flare.model.AccountType +import dev.dimension.flare.ui.model.UiList +import dev.dimension.flare.ui.model.UiState +import dev.dimension.flare.ui.model.map +import kotlinx.coroutines.CoroutineScope + +public class MisskeyFollowedChannelsPresenter( + accountType: AccountType, +) : MisskeyBaseChannelPresenter(accountType) { + @Composable + internal override fun getPagingData( + scope: CoroutineScope, + serviceState: UiState, + ): PagingState = + serviceState + .map> { service -> + require(service is MisskeyDataSource) + remember(service) { + service.channelHandler.data.cachedIn(scope) + }.collectAsLazyPagingItems() + }.toPagingState() +} diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/home/misskey/MisskeyOwnedChannelsPresenter.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/home/misskey/MisskeyOwnedChannelsPresenter.kt new file mode 100644 index 000000000..00ab308a0 --- /dev/null +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/home/misskey/MisskeyOwnedChannelsPresenter.kt @@ -0,0 +1,33 @@ +package dev.dimension.flare.ui.presenter.home.misskey + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.paging.cachedIn +import androidx.paging.compose.LazyPagingItems +import androidx.paging.compose.collectAsLazyPagingItems +import dev.dimension.flare.common.PagingState +import dev.dimension.flare.common.toPagingState +import dev.dimension.flare.data.datasource.microblog.MicroblogDataSource +import dev.dimension.flare.data.datasource.misskey.MisskeyDataSource +import dev.dimension.flare.model.AccountType +import dev.dimension.flare.ui.model.UiList +import dev.dimension.flare.ui.model.UiState +import dev.dimension.flare.ui.model.map +import kotlinx.coroutines.CoroutineScope + +public class MisskeyOwnedChannelsPresenter( + accountType: AccountType, +) : MisskeyBaseChannelPresenter(accountType) { + @Composable + internal override fun getPagingData( + scope: CoroutineScope, + serviceState: UiState, + ): PagingState = + serviceState + .map> { service -> + require(service is MisskeyDataSource) + remember(service) { + service.ownedChannelHandler.data.cachedIn(scope) + }.collectAsLazyPagingItems() + }.toPagingState() +} diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/list/PinnableTimelineTabPresenter.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/list/PinnableTimelineTabPresenter.kt index 4617b6daf..2c41b29df 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/list/PinnableTimelineTabPresenter.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/list/PinnableTimelineTabPresenter.kt @@ -96,15 +96,15 @@ public class PinnableTimelineTabPresenter( }.collectAsLazyPagingItems() }.toPagingState() -// val channel = -// serviceState -// .mapNotNull { -// it as? MisskeyDataSource -// }.mapNotNull { service -> -// remember(service) { -// service.channelsList(scope = scope) -// }.collectAsLazyPagingItems() -// }.toPagingState() + val channel = + serviceState + .mapNotNull { + it as? MisskeyDataSource + }.mapNotNull { service -> + remember(service) { + service.channelHandler.data.cachedIn(scope) + }.collectAsLazyPagingItems() + }.toPagingState() val tabs = serviceState.map { service -> @@ -129,11 +129,11 @@ public class PinnableTimelineTabPresenter( } else { null }, -// if (service is MisskeyDataSource) { -// State.Tab.Channel(channel) -// } else { -// null -// }, + if (service is MisskeyDataSource) { + State.Tab.Channel(channel) + } else { + null + }, ).toImmutableList().toImmutableListWrapper() } } From a7bc33f438445e5acff3fcddd064c75ef38f4dc6 Mon Sep 17 00:00:00 2001 From: Tlaster Date: Tue, 17 Feb 2026 17:07:57 +0900 Subject: [PATCH 08/14] update misskey channel ui --- .../dev/dimension/flare/ui/route/Route.kt | 14 ++ .../flare/ui/screen/home/HomeScreen.kt | 1 + .../ui/screen/misskey/ChannelListScreen.kt | 128 +++++++++++++++ .../ui/screen/misskey/MisskeyEntryBuilder.kt | 61 +++++++ app/src/main/res/values/strings.xml | 5 + .../composeResources/values/strings.xml | 4 + .../dimension/flare/data/model/TabSettings.kt | 21 +++ .../flare/ui/component/ErrorContent.kt | 154 ++++++++++++++++++ .../dimension/flare/ui/component/TabIcon.kt | 5 +- .../flare/ui/component/UiListItemComponent.kt | 21 +-- .../ui/component/status/LazyStatusItems.kt | 73 +-------- .../main/composeResources/values/strings.xml | 5 + .../main/kotlin/dev/dimension/flare/App.kt | 1 + .../dev/dimension/flare/ui/route/Route.kt | 10 ++ .../dev/dimension/flare/ui/route/Router.kt | 43 +++++ .../flare/ui/screen/feeds/FeedListScreen.kt | 1 + .../ui/screen/misskey/ChannelListScreen.kt | 122 ++++++++++++++ iosApp/flare/Localizable.xcstrings | 50 ++++++ iosApp/flare/UI/Route/TabItemView.swift | 2 + .../flare/UI/Screen/ChannelListScreen.swift | 85 ++++++++++ .../network/misskey/MisskeyOauthService.kt | 2 + .../data/network/misskey/MisskeyService.kt | 17 ++ .../data/network/misskey/api/ChannelsApi.kt | 13 +- .../misskey/api/model/MisskeyException.kt | 16 ++ ...nter.kt => MisskeyChannelListPresenter.kt} | 80 +++++++-- .../home/misskey/MisskeyChannelsState.kt | 20 --- .../MisskeyFavoriteChannelsPresenter.kt | 33 ---- .../MisskeyFeaturedChannelsPresenter.kt | 32 ---- .../MisskeyFollowedChannelsPresenter.kt | 33 ---- .../misskey/MisskeyOwnedChannelsPresenter.kt | 33 ---- 30 files changed, 835 insertions(+), 250 deletions(-) create mode 100644 app/src/main/java/dev/dimension/flare/ui/screen/misskey/ChannelListScreen.kt create mode 100644 compose-ui/src/commonMain/kotlin/dev/dimension/flare/ui/component/ErrorContent.kt create mode 100644 desktopApp/src/main/kotlin/dev/dimension/flare/ui/screen/misskey/ChannelListScreen.kt create mode 100644 iosApp/flare/UI/Screen/ChannelListScreen.swift create mode 100644 shared/src/commonMain/kotlin/dev/dimension/flare/data/network/misskey/api/model/MisskeyException.kt rename shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/home/misskey/{MisskeyBaseChannelPresenter.kt => MisskeyChannelListPresenter.kt} (50%) delete mode 100644 shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/home/misskey/MisskeyChannelsState.kt delete mode 100644 shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/home/misskey/MisskeyFavoriteChannelsPresenter.kt delete mode 100644 shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/home/misskey/MisskeyFeaturedChannelsPresenter.kt delete mode 100644 shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/home/misskey/MisskeyFollowedChannelsPresenter.kt delete mode 100644 shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/home/misskey/MisskeyOwnedChannelsPresenter.kt diff --git a/app/src/main/java/dev/dimension/flare/ui/route/Route.kt b/app/src/main/java/dev/dimension/flare/ui/route/Route.kt index d310ba1bb..482cf3bb0 100644 --- a/app/src/main/java/dev/dimension/flare/ui/route/Route.kt +++ b/app/src/main/java/dev/dimension/flare/ui/route/Route.kt @@ -364,6 +364,20 @@ internal sealed interface Route : NavKey { val title: String, ) : Misskey, WithAccountType + + @Serializable + data class ChannelList( + override val accountType: AccountType, + ) : Misskey, + WithAccountType + + @Serializable + data class ChannelTimeline( + override val accountType: AccountType, + val channelId: String, + val title: String, + ) : Misskey, + WithAccountType } @Serializable diff --git a/app/src/main/java/dev/dimension/flare/ui/screen/home/HomeScreen.kt b/app/src/main/java/dev/dimension/flare/ui/screen/home/HomeScreen.kt index 52cfbba5a..bed8529c5 100644 --- a/app/src/main/java/dev/dimension/flare/ui/screen/home/HomeScreen.kt +++ b/app/src/main/java/dev/dimension/flare/ui/screen/home/HomeScreen.kt @@ -536,6 +536,7 @@ private fun getDirection( is DirectMessageTabItem -> Route.DM.List(accountType) is RssTabItem -> Route.Rss.Sources is Misskey.AntennasListTabItem -> Route.Misskey.AntennasList(accountType) + is Misskey.ChannelListTabItem -> Route.Misskey.ChannelList(accountType) } @Composable diff --git a/app/src/main/java/dev/dimension/flare/ui/screen/misskey/ChannelListScreen.kt b/app/src/main/java/dev/dimension/flare/ui/screen/misskey/ChannelListScreen.kt new file mode 100644 index 000000000..64cc570ef --- /dev/null +++ b/app/src/main/java/dev/dimension/flare/ui/screen/misskey/ChannelListScreen.kt @@ -0,0 +1,128 @@ +package dev.dimension.flare.ui.screen.misskey + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.ListItemDefaults +import androidx.compose.material3.SecondaryScrollableTabRow +import androidx.compose.material3.Tab +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import dev.dimension.flare.R +import dev.dimension.flare.common.isRefreshing +import dev.dimension.flare.model.AccountType +import dev.dimension.flare.ui.common.plus +import dev.dimension.flare.ui.component.BackButton +import dev.dimension.flare.ui.component.FlareLargeFlexibleTopAppBar +import dev.dimension.flare.ui.component.FlareScaffold +import dev.dimension.flare.ui.component.RefreshContainer +import dev.dimension.flare.ui.component.TabRowIndicator +import dev.dimension.flare.ui.component.uiListItemComponent +import dev.dimension.flare.ui.model.UiList +import dev.dimension.flare.ui.presenter.home.misskey.MisskeyChannelListPresenter +import dev.dimension.flare.ui.presenter.invoke +import dev.dimension.flare.ui.theme.screenHorizontalPadding +import moe.tlaster.precompose.molecule.producePresenter + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +internal fun ChannelListScreen( + accountType: AccountType, + toTimeline: (UiList) -> Unit, + onBack: () -> Unit, +) { + val state by producePresenter { + remember { + MisskeyChannelListPresenter(accountType) + }.invoke() + } + val topAppBarScrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior() + + FlareScaffold( + modifier = Modifier.nestedScroll(topAppBarScrollBehavior.nestedScrollConnection), + topBar = { + FlareLargeFlexibleTopAppBar( + title = { + Text(text = stringResource(id = R.string.home_tab_channels_title)) + }, + subtitle = { + SecondaryScrollableTabRow( + containerColor = Color.Transparent, + modifier = + Modifier + .fillMaxWidth(), + indicator = { + TabRowIndicator( + selectedIndex = state.allTypes.indexOf(state.type), + ) + }, + minTabWidth = 48.dp, + selectedTabIndex = state.allTypes.indexOf(state.type), + divider = {}, + edgePadding = screenHorizontalPadding, + ) { + state.allTypes.forEach { type -> + Tab( + selected = state.type == type, + onClick = { + state.setType(type) + }, + text = { + Text( + stringResource( + when (type) { + MisskeyChannelListPresenter.State.Type.Following -> + R.string.misskey_channel_tab_following + MisskeyChannelListPresenter.State.Type.Favorites -> + R.string.misskey_channel_tab_favorites + MisskeyChannelListPresenter.State.Type.Owned -> + R.string.misskey_channel_tab_owned + MisskeyChannelListPresenter.State.Type.Featured -> + R.string.misskey_channel_tab_featured + }, + ), + ) + }, + ) + } + } + }, + scrollBehavior = topAppBarScrollBehavior, + navigationIcon = { + BackButton(onBack = onBack) + }, + ) + }, + ) { contentPadding -> + RefreshContainer( + isRefreshing = state.data.isRefreshing, + onRefresh = { state.refresh() }, + indicatorPadding = contentPadding, + content = { + LazyColumn( + contentPadding = + contentPadding + + PaddingValues( + vertical = 16.dp, + ), + verticalArrangement = Arrangement.spacedBy(ListItemDefaults.SegmentedGap), + ) { + uiListItemComponent( + items = state.data, + onClicked = toTimeline, + ) + } + }, + ) + } +} diff --git a/app/src/main/java/dev/dimension/flare/ui/screen/misskey/MisskeyEntryBuilder.kt b/app/src/main/java/dev/dimension/flare/ui/screen/misskey/MisskeyEntryBuilder.kt index ebe7bdc4f..c5df70cea 100644 --- a/app/src/main/java/dev/dimension/flare/ui/screen/misskey/MisskeyEntryBuilder.kt +++ b/app/src/main/java/dev/dimension/flare/ui/screen/misskey/MisskeyEntryBuilder.kt @@ -18,6 +18,7 @@ import androidx.navigation3.runtime.NavKey import compose.icons.FontAwesomeIcons import compose.icons.fontawesomeicons.Solid import compose.icons.fontawesomeicons.solid.SquareRss +import compose.icons.fontawesomeicons.solid.List import dev.dimension.flare.data.model.IconType import dev.dimension.flare.data.model.Misskey import dev.dimension.flare.data.model.TabMetaData @@ -68,6 +69,43 @@ internal fun EntryProviderScope.misskeyEntryBuilder( onBack = onBack, ) } + + entry( + metadata = ListDetailSceneStrategy.listPane( + sceneKey = "misskey_channels_list", + detailPlaceholder = { + ChannelsPlaceholder() + } + ) + ) { args -> + ChannelListScreen( + accountType = args.accountType, + toTimeline = { + navigate(Route.Misskey.ChannelTimeline(args.accountType, it.id, it.title)) + }, + onBack = onBack + ) + } + + entry( + metadata = ListDetailSceneStrategy.detailPane( + sceneKey = "misskey_channels_list", + ) + ) { args -> + TimelineScreen( + tabItem = remember(args) { + Misskey.ChannelTimelineTabItem( + channelId = args.channelId, + account = args.accountType, + metaData = TabMetaData( + title = TitleType.Text(args.title), + icon = IconType.Material(IconType.Material.MaterialIcon.List), + ), + ) + }, + onBack = onBack, + ) + } } @@ -93,3 +131,26 @@ internal fun AntennasPlaceholder( } } } + +@Composable +internal fun ChannelsPlaceholder( + modifier: Modifier = Modifier, +) { + FlareScaffold( + modifier = modifier, + ) { + Column( + modifier = Modifier + .fillMaxSize() + .padding(it), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(8.dp, alignment = Alignment.CenterVertically), + ) { + FAIcon( + FontAwesomeIcons.Solid.List, + contentDescription = null, + modifier = Modifier.size(64.dp) + ) + } + } +} diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index ed7905e45..b2e234127 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -374,6 +374,11 @@ Podcast live Antenna + Channels + Following + Favorites + Owned + Featured No antennas Mixed Add mixed timeline tab diff --git a/compose-ui/src/commonMain/composeResources/values/strings.xml b/compose-ui/src/commonMain/composeResources/values/strings.xml index cfa6d96ac..043c2c0f5 100644 --- a/compose-ui/src/commonMain/composeResources/values/strings.xml +++ b/compose-ui/src/commonMain/composeResources/values/strings.xml @@ -83,6 +83,9 @@ Click to login again Login again + Permission Denied + You need re-login in order to access this page + Local only Only users on this instance can see this post @@ -490,4 +493,5 @@ Crowdin Help us translate Flare Privacy Policy + Channel diff --git a/compose-ui/src/commonMain/kotlin/dev/dimension/flare/data/model/TabSettings.kt b/compose-ui/src/commonMain/kotlin/dev/dimension/flare/data/model/TabSettings.kt index 2654b85c5..2ba63a2d7 100644 --- a/compose-ui/src/commonMain/kotlin/dev/dimension/flare/data/model/TabSettings.kt +++ b/compose-ui/src/commonMain/kotlin/dev/dimension/flare/data/model/TabSettings.kt @@ -92,6 +92,7 @@ public sealed class TitleType { Liked, AllRssFeeds, Posts, + Channel, } } } @@ -142,6 +143,7 @@ public sealed class IconType { Messages, Rss, Weibo, + Channel, } } @@ -424,6 +426,14 @@ public sealed class TimelineTabItem : TabItem() { icon = IconType.Mixed(IconType.Material.MaterialIcon.Rss, accountKey), ), ), + Misskey.ChannelListTabItem( + account = AccountType.Specific(accountKey), + metaData = + TabMetaData( + title = TitleType.Localized(TitleType.Localized.LocalizedKey.Channel), + icon = IconType.Mixed(IconType.Material.MaterialIcon.Channel, accountKey), + ), + ), ) private fun bluesky(accountKey: MicroBlogKey) = @@ -881,6 +891,17 @@ public object Misskey { override fun update(metaData: TabMetaData): TabItem = copy(metaData = metaData) } + + @Immutable + @Serializable + public data class ChannelListTabItem( + override val account: AccountType, + override val metaData: TabMetaData, + ) : TabItem() { + override val key: String = "channels_$account" + + override fun update(metaData: TabMetaData): TabItem = copy(metaData = metaData) + } } public object XQT { diff --git a/compose-ui/src/commonMain/kotlin/dev/dimension/flare/ui/component/ErrorContent.kt b/compose-ui/src/commonMain/kotlin/dev/dimension/flare/ui/component/ErrorContent.kt new file mode 100644 index 000000000..791d3976c --- /dev/null +++ b/compose-ui/src/commonMain/kotlin/dev/dimension/flare/ui/component/ErrorContent.kt @@ -0,0 +1,154 @@ +package dev.dimension.flare.ui.component + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalUriHandler +import androidx.compose.ui.unit.dp +import compose.icons.FontAwesomeIcons +import compose.icons.fontawesomeicons.Solid +import compose.icons.fontawesomeicons.solid.CircleExclamation +import compose.icons.fontawesomeicons.solid.FileCircleExclamation +import dev.dimension.flare.compose.ui.Res +import dev.dimension.flare.compose.ui.login_expired +import dev.dimension.flare.compose.ui.login_expired_message +import dev.dimension.flare.compose.ui.permission_denied_message +import dev.dimension.flare.compose.ui.permission_denied_title +import dev.dimension.flare.compose.ui.status_loadmore_error +import dev.dimension.flare.data.network.misskey.api.model.MisskeyException +import dev.dimension.flare.data.repository.LoginExpiredException +import dev.dimension.flare.ui.component.platform.PlatformText +import dev.dimension.flare.ui.route.DeeplinkRoute +import dev.dimension.flare.ui.route.toUri +import org.jetbrains.compose.resources.stringResource + +@Composable +public fun ErrorContent( + error: Throwable, + onRetry: () -> Unit, + modifier: Modifier = Modifier, +) { + when (error) { + is LoginExpiredException -> { + LoginExpiredError(error, modifier) + } + + is MisskeyException -> { + MisskeyError( + error = error, + modifier = modifier, + onRetry = onRetry, + ) + } + + else -> { + CommonError( + error = error, + onRetry = onRetry, + modifier = modifier, + ) + } + } +} + +@Composable +private fun CommonError( + error: Throwable, + onRetry: () -> Unit, + modifier: Modifier = Modifier, +) { + Column( + modifier = + modifier + .clickable { + onRetry.invoke() + }.fillMaxWidth() + .padding(16.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(8.dp, Alignment.CenterVertically), + ) { + FAIcon( + imageVector = FontAwesomeIcons.Solid.FileCircleExclamation, + contentDescription = null, + modifier = Modifier.size(48.dp), + ) + PlatformText(text = stringResource(Res.string.status_loadmore_error)) + error.message?.let { PlatformText(text = it) } + } +} + +@Composable +private fun MisskeyError( + error: MisskeyException, + onRetry: () -> Unit, + modifier: Modifier = Modifier, +) { + if (error.error?.code == "PERMISSION_DENIED") { + val uriHandler = LocalUriHandler.current + Column( + modifier = + modifier + .clickable { + uriHandler.openUri(DeeplinkRoute.Login.toUri()) + }, + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(8.dp, Alignment.CenterVertically), + ) { + FAIcon( + imageVector = FontAwesomeIcons.Solid.CircleExclamation, + contentDescription = null, + modifier = Modifier.size(48.dp), + ) + PlatformText( + text = stringResource(resource = Res.string.permission_denied_title), + ) + PlatformText( + text = stringResource(resource = Res.string.permission_denied_message), + ) + } + } else { + CommonError( + error = error, + onRetry = onRetry, + modifier = modifier, + ) + } +} + +@Composable +private fun LoginExpiredError( + error: LoginExpiredException, + modifier: Modifier = Modifier, +) { + val uriHandler = LocalUriHandler.current + Column( + modifier = + modifier + .clickable { + uriHandler.openUri(DeeplinkRoute.Login.toUri()) + }, + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(8.dp, Alignment.CenterVertically), + ) { + FAIcon( + imageVector = FontAwesomeIcons.Solid.CircleExclamation, + contentDescription = null, + modifier = Modifier.size(48.dp), + ) + PlatformText( + text = stringResource(resource = Res.string.login_expired), + ) + PlatformText( + text = stringResource(resource = Res.string.login_expired_message), + ) + PlatformText( + text = error.accountKey.toString(), + ) + } +} diff --git a/compose-ui/src/commonMain/kotlin/dev/dimension/flare/ui/component/TabIcon.kt b/compose-ui/src/commonMain/kotlin/dev/dimension/flare/ui/component/TabIcon.kt index c843a9252..09561aebc 100644 --- a/compose-ui/src/commonMain/kotlin/dev/dimension/flare/ui/component/TabIcon.kt +++ b/compose-ui/src/commonMain/kotlin/dev/dimension/flare/ui/component/TabIcon.kt @@ -36,10 +36,12 @@ import compose.icons.fontawesomeicons.solid.MagnifyingGlass import compose.icons.fontawesomeicons.solid.Message import compose.icons.fontawesomeicons.solid.RectangleList import compose.icons.fontawesomeicons.solid.SquareRss +import compose.icons.fontawesomeicons.solid.Tv import compose.icons.fontawesomeicons.solid.Users import dev.dimension.flare.compose.ui.Res import dev.dimension.flare.compose.ui.all_rss_feeds_title import dev.dimension.flare.compose.ui.antenna_title +import dev.dimension.flare.compose.ui.channel_title import dev.dimension.flare.compose.ui.dm_list_title import dev.dimension.flare.compose.ui.home_tab_bookmarks_title import dev.dimension.flare.compose.ui.home_tab_discover_title @@ -62,7 +64,6 @@ import dev.dimension.flare.data.model.IconType import dev.dimension.flare.data.model.TabItem import dev.dimension.flare.data.model.TitleType import dev.dimension.flare.model.AccountType -import dev.dimension.flare.ui.component.placeholder import dev.dimension.flare.ui.component.platform.PlatformText import dev.dimension.flare.ui.component.platform.PlatformTextStyle import dev.dimension.flare.ui.icons.Misskey @@ -283,6 +284,7 @@ internal val TitleType.Localized.res: StringResource TitleType.Localized.LocalizedKey.Liked -> Res.string.liked_title TitleType.Localized.LocalizedKey.AllRssFeeds -> Res.string.all_rss_feeds_title TitleType.Localized.LocalizedKey.Posts -> Res.string.posts_title + TitleType.Localized.LocalizedKey.Channel -> Res.string.channel_title } internal fun IconType.Material.MaterialIcon.toIcon(): ImageVector = @@ -306,4 +308,5 @@ internal fun IconType.Material.MaterialIcon.toIcon(): ImageVector = IconType.Material.MaterialIcon.Messages -> FontAwesomeIcons.Solid.Message IconType.Material.MaterialIcon.Rss -> FontAwesomeIcons.Solid.SquareRss IconType.Material.MaterialIcon.Weibo -> FontAwesomeIcons.Brands.Weibo + IconType.Material.MaterialIcon.Channel -> FontAwesomeIcons.Solid.Tv } diff --git a/compose-ui/src/commonMain/kotlin/dev/dimension/flare/ui/component/UiListItemComponent.kt b/compose-ui/src/commonMain/kotlin/dev/dimension/flare/ui/component/UiListItemComponent.kt index d4bfee21b..00664d174 100644 --- a/compose-ui/src/commonMain/kotlin/dev/dimension/flare/ui/component/UiListItemComponent.kt +++ b/compose-ui/src/commonMain/kotlin/dev/dimension/flare/ui/component/UiListItemComponent.kt @@ -18,14 +18,12 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.unit.dp import compose.icons.FontAwesomeIcons import compose.icons.fontawesomeicons.Solid -import compose.icons.fontawesomeicons.solid.CircleExclamation import compose.icons.fontawesomeicons.solid.List import compose.icons.fontawesomeicons.solid.Rss import dev.dimension.flare.common.PagingState import dev.dimension.flare.compose.ui.Res import dev.dimension.flare.compose.ui.feeds_discover_feeds_created_by import dev.dimension.flare.compose.ui.list_empty -import dev.dimension.flare.compose.ui.list_error import dev.dimension.flare.ui.common.itemsIndexed import dev.dimension.flare.ui.component.platform.PlatformListItem import dev.dimension.flare.ui.component.platform.PlatformSegmentedListItem @@ -75,21 +73,12 @@ public fun LazyListScope.uiListItemComponent( ) }, errorContent = { - Column( + ErrorContent( + error = it, modifier = Modifier.fillParentMaxSize(), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.spacedBy(8.dp, Alignment.CenterVertically), - ) { - FAIcon( - imageVector = FontAwesomeIcons.Solid.CircleExclamation, - contentDescription = stringResource(Res.string.list_error), - modifier = Modifier.size(48.dp), - ) - PlatformText( - text = stringResource(Res.string.list_error), - style = PlatformTheme.typography.title, - ) - } + onRetry = { + }, + ) }, ) { index, itemCount, item -> UiListItem( diff --git a/compose-ui/src/commonMain/kotlin/dev/dimension/flare/ui/component/status/LazyStatusItems.kt b/compose-ui/src/commonMain/kotlin/dev/dimension/flare/ui/component/status/LazyStatusItems.kt index 3c71b8266..5bb744168 100644 --- a/compose-ui/src/commonMain/kotlin/dev/dimension/flare/ui/component/status/LazyStatusItems.kt +++ b/compose-ui/src/commonMain/kotlin/dev/dimension/flare/ui/component/status/LazyStatusItems.kt @@ -18,14 +18,11 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip -import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import compose.icons.FontAwesomeIcons import compose.icons.fontawesomeicons.Solid -import compose.icons.fontawesomeicons.solid.CircleExclamation import compose.icons.fontawesomeicons.solid.File -import compose.icons.fontawesomeicons.solid.FileCircleExclamation import dev.dimension.flare.common.PagingState import dev.dimension.flare.common.onEmpty import dev.dimension.flare.common.onEndOfList @@ -33,19 +30,14 @@ import dev.dimension.flare.common.onError import dev.dimension.flare.common.onLoading import dev.dimension.flare.common.onSuccess import dev.dimension.flare.compose.ui.Res -import dev.dimension.flare.compose.ui.login_expired -import dev.dimension.flare.compose.ui.login_expired_message import dev.dimension.flare.compose.ui.status_empty import dev.dimension.flare.compose.ui.status_loadmore_end -import dev.dimension.flare.compose.ui.status_loadmore_error -import dev.dimension.flare.data.repository.LoginExpiredException import dev.dimension.flare.model.MicroBlogKey +import dev.dimension.flare.ui.component.ErrorContent import dev.dimension.flare.ui.component.FAIcon import dev.dimension.flare.ui.component.placeholder import dev.dimension.flare.ui.component.platform.PlatformText import dev.dimension.flare.ui.model.UiTimeline -import dev.dimension.flare.ui.route.DeeplinkRoute -import dev.dimension.flare.ui.route.toUri import dev.dimension.flare.ui.theme.PlatformTheme import dev.dimension.flare.ui.theme.screenHorizontalPadding import org.jetbrains.compose.resources.stringResource @@ -221,64 +213,11 @@ private fun OnError( onRetry: () -> Unit, modifier: Modifier = Modifier, ) { - when (error) { - is LoginExpiredException -> { - LoginExpiredError(error, modifier) - } - - else -> { - Column( - modifier = - modifier - .clickable { - onRetry.invoke() - }.fillMaxWidth() - .padding(16.dp), - verticalArrangement = Arrangement.spacedBy(8.dp), - horizontalAlignment = Alignment.CenterHorizontally, - ) { - FAIcon( - imageVector = FontAwesomeIcons.Solid.FileCircleExclamation, - contentDescription = null, - modifier = Modifier.size(48.dp), - ) - PlatformText(text = stringResource(Res.string.status_loadmore_error)) - error.message?.let { PlatformText(text = it) } - } - } - } -} - -@Composable -private fun LoginExpiredError( - exception: LoginExpiredException, - modifier: Modifier = Modifier, -) { - val uriHandler = LocalUriHandler.current - Column( - modifier = - modifier - .clickable { - uriHandler.openUri(DeeplinkRoute.Login.toUri()) - }, - verticalArrangement = Arrangement.spacedBy(8.dp, Alignment.CenterVertically), - horizontalAlignment = Alignment.CenterHorizontally, - ) { - FAIcon( - imageVector = FontAwesomeIcons.Solid.CircleExclamation, - contentDescription = null, - modifier = Modifier.size(48.dp), - ) - PlatformText( - text = stringResource(resource = Res.string.login_expired), - ) - PlatformText( - text = stringResource(resource = Res.string.login_expired_message), - ) - PlatformText( - text = exception.accountKey.toString(), - ) - } + ErrorContent( + error = error, + onRetry = onRetry, + modifier = modifier, + ) } @Composable diff --git a/desktopApp/src/main/composeResources/values/strings.xml b/desktopApp/src/main/composeResources/values/strings.xml index 7ebade850..6744f437d 100644 --- a/desktopApp/src/main/composeResources/values/strings.xml +++ b/desktopApp/src/main/composeResources/values/strings.xml @@ -366,4 +366,9 @@ This will import data from the file. Existing records with matching IDs will be replaced. Do you want to continue? Channel + + Following + Favorites + Owned + Featured diff --git a/desktopApp/src/main/kotlin/dev/dimension/flare/App.kt b/desktopApp/src/main/kotlin/dev/dimension/flare/App.kt index c47cf5af4..0a33048ea 100644 --- a/desktopApp/src/main/kotlin/dev/dimension/flare/App.kt +++ b/desktopApp/src/main/kotlin/dev/dimension/flare/App.kt @@ -428,6 +428,7 @@ private fun getRoute(tab: TabItem): Route = is DirectMessageTabItem -> Route.DmList(tab.account) is RssTabItem -> Route.RssList is Misskey.AntennasListTabItem -> Route.MisskeyAntennas(tab.account) + is Misskey.ChannelListTabItem -> Route.MisskeyChannelList(tab.account) } @Composable diff --git a/desktopApp/src/main/kotlin/dev/dimension/flare/ui/route/Route.kt b/desktopApp/src/main/kotlin/dev/dimension/flare/ui/route/Route.kt index fae47be1d..ca866a9ed 100644 --- a/desktopApp/src/main/kotlin/dev/dimension/flare/ui/route/Route.kt +++ b/desktopApp/src/main/kotlin/dev/dimension/flare/ui/route/Route.kt @@ -199,6 +199,16 @@ internal sealed interface Route : NavKey { val accountType: AccountType, ) : ScreenRoute + data class MisskeyChannelList( + val accountType: AccountType, + ) : ScreenRoute + + data class MisskeyChannelTimeline( + val accountType: AccountType, + val channelId: String, + val title: String, + ) : ScreenRoute + data object TabSetting : ScreenRoute data class TabGroupConfig( diff --git a/desktopApp/src/main/kotlin/dev/dimension/flare/ui/route/Router.kt b/desktopApp/src/main/kotlin/dev/dimension/flare/ui/route/Router.kt index 8737df3bf..6368db6c3 100644 --- a/desktopApp/src/main/kotlin/dev/dimension/flare/ui/route/Router.kt +++ b/desktopApp/src/main/kotlin/dev/dimension/flare/ui/route/Router.kt @@ -69,6 +69,7 @@ import dev.dimension.flare.ui.screen.list.AllListScreen import dev.dimension.flare.ui.screen.media.RawMediaScreen import dev.dimension.flare.ui.screen.media.StatusMediaScreen import dev.dimension.flare.ui.screen.misskey.AntennasListScreen +import dev.dimension.flare.ui.screen.misskey.ChannelListScreen import dev.dimension.flare.ui.screen.rss.EditRssSourceScreen import dev.dimension.flare.ui.screen.rss.ImportOPMLScreen import dev.dimension.flare.ui.screen.rss.RssListScreen @@ -832,6 +833,48 @@ internal fun WindowScope.Router( onBack = onBack, ) } + entry( + metadata = + ListDetailSceneStrategy.listPane( + sceneKey = "misskey_channels_list", + detailPlaceholder = { + Box( + modifier = + Modifier + .fillMaxSize() + .background(FluentTheme.colors.background.solid.base), + ) + }, + ), + ) { args -> + ChannelListScreen( + accountType = args.accountType, + toTimeline = { + navigate(Route.MisskeyChannelTimeline(args.accountType, it.id, it.title)) + }, + ) + } + entry( + metadata = + ListDetailSceneStrategy.detailPane( + sceneKey = "misskey_channels_list", + ), + ) { args -> + TimelineScreen( + tabItem = + remember(args) { + Misskey.ChannelTimelineTabItem( + channelId = args.channelId, + account = args.accountType, + metaData = + TabMetaData( + title = TitleType.Text(args.title), + icon = IconType.Material(IconType.Material.MaterialIcon.Channel), + ), + ) + }, + ) + } }, ) } diff --git a/desktopApp/src/main/kotlin/dev/dimension/flare/ui/screen/feeds/FeedListScreen.kt b/desktopApp/src/main/kotlin/dev/dimension/flare/ui/screen/feeds/FeedListScreen.kt index 154d4ade2..3e3842ddf 100644 --- a/desktopApp/src/main/kotlin/dev/dimension/flare/ui/screen/feeds/FeedListScreen.kt +++ b/desktopApp/src/main/kotlin/dev/dimension/flare/ui/screen/feeds/FeedListScreen.kt @@ -52,6 +52,7 @@ internal fun FeedListScreen( adapter = scrollbarAdapter, ) { LazyColumn( + state = listState, contentPadding = PaddingValues( vertical = 8.dp, diff --git a/desktopApp/src/main/kotlin/dev/dimension/flare/ui/screen/misskey/ChannelListScreen.kt b/desktopApp/src/main/kotlin/dev/dimension/flare/ui/screen/misskey/ChannelListScreen.kt new file mode 100644 index 000000000..857b6ce6e --- /dev/null +++ b/desktopApp/src/main/kotlin/dev/dimension/flare/ui/screen/misskey/ChannelListScreen.kt @@ -0,0 +1,122 @@ +package dev.dimension.flare.ui.screen.misskey + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.rememberScrollbarAdapter +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import dev.dimension.flare.LocalWindowPadding +import dev.dimension.flare.RegisterTabCallback +import dev.dimension.flare.Res +import dev.dimension.flare.common.isRefreshing +import dev.dimension.flare.misskey_channel_tab_favorites +import dev.dimension.flare.misskey_channel_tab_featured +import dev.dimension.flare.misskey_channel_tab_following +import dev.dimension.flare.misskey_channel_tab_owned +import dev.dimension.flare.model.AccountType +import dev.dimension.flare.ui.common.plus +import dev.dimension.flare.ui.component.uiListItemComponent +import dev.dimension.flare.ui.model.UiList +import dev.dimension.flare.ui.presenter.home.misskey.MisskeyChannelListPresenter +import dev.dimension.flare.ui.presenter.invoke +import dev.dimension.flare.ui.theme.screenHorizontalPadding +import io.github.composefluent.component.LiteFilter +import io.github.composefluent.component.PillButton +import io.github.composefluent.component.ProgressBar +import io.github.composefluent.component.ScrollbarContainer +import io.github.composefluent.component.Text +import moe.tlaster.precompose.molecule.producePresenter +import org.jetbrains.compose.resources.stringResource + +@Composable +internal fun ChannelListScreen( + accountType: AccountType, + toTimeline: (UiList) -> Unit, +) { + val state by producePresenter { + remember { + MisskeyChannelListPresenter(accountType) + }.invoke() + } + + val listState = rememberLazyListState() + val scrollbarAdapter = rememberScrollbarAdapter(listState) + RegisterTabCallback(listState, onRefresh = { state.refresh() }) + + Box { + Column( + verticalArrangement = Arrangement.spacedBy(8.dp), + modifier = + Modifier + .padding(LocalWindowPadding.current), + ) { + LiteFilter( + modifier = Modifier.padding(horizontal = screenHorizontalPadding), + ) { + state.allTypes.forEach { type -> + PillButton( + selected = state.type == type, + onSelectedChanged = { + state.setType(type) + }, + ) { + Text( + text = + stringResource( + when (type) { + MisskeyChannelListPresenter.State.Type.Following -> + Res.string.misskey_channel_tab_following + MisskeyChannelListPresenter.State.Type.Favorites -> + Res.string.misskey_channel_tab_favorites + MisskeyChannelListPresenter.State.Type.Owned -> + Res.string.misskey_channel_tab_owned + MisskeyChannelListPresenter.State.Type.Featured -> + Res.string.misskey_channel_tab_featured + }, + ), + ) + } + } + } + ScrollbarContainer( + adapter = scrollbarAdapter, + ) { + LazyColumn( + state = listState, + contentPadding = + PaddingValues( + horizontal = screenHorizontalPadding, + ), + modifier = + Modifier + .fillMaxSize(), + verticalArrangement = Arrangement.spacedBy(2.dp), + ) { + uiListItemComponent( + items = state.data, + onClicked = toTimeline, + ) + } + } + } + if (state.data.isRefreshing) { + ProgressBar( + modifier = + Modifier + .align(Alignment.TopCenter) + .fillMaxWidth(), + ) + } + } +} diff --git a/iosApp/flare/Localizable.xcstrings b/iosApp/flare/Localizable.xcstrings index da4a6192b..54d9c18af 100644 --- a/iosApp/flare/Localizable.xcstrings +++ b/iosApp/flare/Localizable.xcstrings @@ -92762,6 +92762,56 @@ } } } + }, + "channels_title" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Channels" + } + } + } + }, + "misskey_channel_tab_following" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Following" + } + } + } + }, + "misskey_channel_tab_favorites" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Favorites" + } + } + } + }, + "misskey_channel_tab_owned" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Owned" + } + } + } + }, + "misskey_channel_tab_featured" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Featured" + } + } + } } }, "version" : "1.1" diff --git a/iosApp/flare/UI/Route/TabItemView.swift b/iosApp/flare/UI/Route/TabItemView.swift index d99ffc9ab..6890f8144 100644 --- a/iosApp/flare/UI/Route/TabItemView.swift +++ b/iosApp/flare/UI/Route/TabItemView.swift @@ -39,6 +39,8 @@ extension TabItem { DMListScreen(accountType: directMessageTabItem.account) case .antennasListTabItem(let antennasListTabItem): AntennasListScreen(accountType: antennasListTabItem.account) + case .channelListTabItem(let channelListTabItem): + ChannelListScreen(accountType: channelListTabItem.account) } } } diff --git a/iosApp/flare/UI/Screen/ChannelListScreen.swift b/iosApp/flare/UI/Screen/ChannelListScreen.swift new file mode 100644 index 000000000..50bc08605 --- /dev/null +++ b/iosApp/flare/UI/Screen/ChannelListScreen.swift @@ -0,0 +1,85 @@ +import SwiftUI +@preconcurrency import KotlinSharedUI + +struct ChannelListScreen: View { + let accountType: AccountType + @State private var selectedTab: ChannelTab = .following + + var body: some View { + VStack { + Picker("Tabs", selection: $selectedTab) { + ForEach(ChannelTab.allCases, id: \.self) { tab in + Text(tab.localizedName).tag(tab) + } + } + .pickerStyle(.segmented) + .padding() + + switch selectedTab { + case .following: + ChannelListOfType(accountType: accountType, type: .following) + case .favorites: + ChannelListOfType(accountType: accountType, type: .favorites) + case .owned: + ChannelListOfType(accountType: accountType, type: .owned) + case .featured: + ChannelListOfType(accountType: accountType, type: .featured) + } + } + .navigationTitle(NSLocalizedString("channels_title", comment: "")) + } +} + +enum ChannelTab: CaseIterable { + case following, favorites, owned, featured + + var localizedName: String { + switch self { + case .following: return NSLocalizedString("misskey_channel_tab_following", comment: "") + case .favorites: return NSLocalizedString("misskey_channel_tab_favorites", comment: "") + case .owned: return NSLocalizedString("misskey_channel_tab_owned", comment: "") + case .featured: return NSLocalizedString("misskey_channel_tab_featured", comment: "") + } + } +} + +struct ChannelListOfType: View { + let accountType: AccountType + @StateObject private var presenter: KotlinPresenter + + init(accountType: AccountType, type: ChannelTab) { + self.accountType = accountType + let p: MisskeyBaseChannelPresenter + switch type { + case .following: p = MisskeyFollowedChannelsPresenter(accountType: accountType) + case .favorites: p = MisskeyFavoriteChannelsPresenter(accountType: accountType) + case .owned: p = MisskeyOwnedChannelsPresenter(accountType: accountType) + case .featured: p = MisskeyFeaturedChannelsPresenter(accountType: accountType) + } + _presenter = StateObject(wrappedValue: KotlinPresenter(p)) + } + + var body: some View { + List { + PagingView(data: presenter.state.data) { item in + NavigationLink(value: Route.timeline( + Misskey.ChannelTimelineTabItem( + channelId: item.id, + account: accountType, + metaData: TabMetaData( + title: TitleType.Text(content: item.title), + icon: IconType.Material(icon: .list) + ) + ) + )) { + UiListView(data: item) + } + } loadingContent: { + UiListPlaceholder() + } + } + .refreshable { + try? await presenter.state.refreshSuspend() + } + } +} diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/network/misskey/MisskeyOauthService.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/network/misskey/MisskeyOauthService.kt index 7f39fd727..9402af7dd 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/data/network/misskey/MisskeyOauthService.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/data/network/misskey/MisskeyOauthService.kt @@ -36,6 +36,8 @@ private val defaultPermission = "read:page-likes", "write:gallery-likes", "read:gallery-likes", + "read:channels", + "write:channels", ) internal class MisskeyOauthService( diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/network/misskey/MisskeyService.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/network/misskey/MisskeyService.kt index a8d290600..ceb482d86 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/data/network/misskey/MisskeyService.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/data/network/misskey/MisskeyService.kt @@ -1,5 +1,6 @@ package dev.dimension.flare.data.network.misskey +import dev.dimension.flare.common.decodeJson import dev.dimension.flare.data.network.ktorfit import dev.dimension.flare.data.network.misskey.api.AccountApi import dev.dimension.flare.data.network.misskey.api.AntennasApi @@ -24,10 +25,13 @@ import dev.dimension.flare.data.network.misskey.api.createNotesApi import dev.dimension.flare.data.network.misskey.api.createReactionsApi import dev.dimension.flare.data.network.misskey.api.createUsersApi import dev.dimension.flare.data.network.misskey.api.model.DriveFile +import dev.dimension.flare.data.network.misskey.api.model.MisskeyException import io.ktor.client.plugins.DefaultRequest +import io.ktor.client.plugins.HttpResponseValidator import io.ktor.client.request.forms.MultiPartFormDataContent import io.ktor.client.request.forms.formData import io.ktor.client.request.header +import io.ktor.client.statement.bodyAsText import io.ktor.http.ContentType import io.ktor.http.Headers import io.ktor.http.HttpHeaders @@ -51,6 +55,19 @@ private fun config( header(HttpHeaders.ContentType, ContentType.Application.Json) } } + HttpResponseValidator { + validateResponse { + runCatching { + it + .bodyAsText() + .decodeJson() + }.getOrNull() + ?.takeIf { it.error != null } + ?.let { + throw it + } + } + } }, ) diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/network/misskey/api/ChannelsApi.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/network/misskey/api/ChannelsApi.kt index 0c327604f..4e74fbe7a 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/data/network/misskey/api/ChannelsApi.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/data/network/misskey/api/ChannelsApi.kt @@ -29,6 +29,7 @@ internal interface ChannelsApi { @POST("channels/create") suspend fun channelsCreate( @Body channelsCreateRequest: ChannelsCreateRequest, + @Header("Content-Type") contentType: kotlin.String = "application/json", ): Channel /** @@ -47,6 +48,7 @@ internal interface ChannelsApi { @POST("channels/favorite") suspend fun channelsFavorite( @Body channelsFollowRequest: ChannelsFollowRequest, + @Header("Content-Type") contentType: kotlin.String = "application/json", ): Unit /** @@ -64,8 +66,8 @@ internal interface ChannelsApi { */ @POST("channels/featured") suspend fun channelsFeatured( - @Header("Content-Type") contentType: kotlin.String = "application/json", @Body request: ChannelsFeaturedRequest, + @Header("Content-Type") contentType: kotlin.String = "application/json", ): kotlin.collections.List /** @@ -84,6 +86,7 @@ internal interface ChannelsApi { @POST("channels/follow") suspend fun channelsFollow( @Body channelsFollowRequest: ChannelsFollowRequest, + @Header("Content-Type") contentType: kotlin.String = "application/json", ): Unit /** @@ -102,6 +105,7 @@ internal interface ChannelsApi { @POST("channels/followed") suspend fun channelsFollowed( @Body channelsFollowedRequest: ChannelsFollowedRequest, + @Header("Content-Type") contentType: kotlin.String = "application/json", ): kotlin.collections.List /** @@ -120,6 +124,7 @@ internal interface ChannelsApi { @POST("channels/my-favorites") suspend fun channelsMyFavorites( @Body channelsFollowedRequest: ChannelsFollowedRequest, + @Header("Content-Type") contentType: kotlin.String = "application/json", ): kotlin.collections.List /** @@ -138,6 +143,7 @@ internal interface ChannelsApi { @POST("channels/owned") suspend fun channelsOwned( @Body channelsFollowedRequest: ChannelsFollowedRequest, + @Header("Content-Type") contentType: kotlin.String = "application/json", ): kotlin.collections.List /** @@ -156,6 +162,7 @@ internal interface ChannelsApi { @POST("channels/search") suspend fun channelsSearch( @Body channelsSearchRequest: ChannelsSearchRequest, + @Header("Content-Type") contentType: kotlin.String = "application/json", ): kotlin.collections.List /** @@ -174,6 +181,7 @@ internal interface ChannelsApi { @POST("channels/show") suspend fun channelsShow( @Body channelsFollowRequest: ChannelsFollowRequest, + @Header("Content-Type") contentType: kotlin.String = "application/json", ): Channel /** @@ -192,6 +200,7 @@ internal interface ChannelsApi { @POST("channels/unfavorite") suspend fun channelsUnfavorite( @Body channelsFollowRequest: ChannelsFollowRequest, + @Header("Content-Type") contentType: kotlin.String = "application/json", ): Unit /** @@ -210,6 +219,7 @@ internal interface ChannelsApi { @POST("channels/unfollow") suspend fun channelsUnfollow( @Body channelsFollowRequest: ChannelsFollowRequest, + @Header("Content-Type") contentType: kotlin.String = "application/json", ): Unit /** @@ -228,5 +238,6 @@ internal interface ChannelsApi { @POST("channels/update") suspend fun channelsUpdate( @Body channelsUpdateRequest: ChannelsUpdateRequest, + @Header("Content-Type") contentType: kotlin.String = "application/json", ): Channel } diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/network/misskey/api/model/MisskeyException.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/network/misskey/api/model/MisskeyException.kt new file mode 100644 index 000000000..c3c63da52 --- /dev/null +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/data/network/misskey/api/model/MisskeyException.kt @@ -0,0 +1,16 @@ +package dev.dimension.flare.data.network.misskey.api.model + +import kotlinx.serialization.Serializable + +@Serializable +public data class MisskeyException( + val error: Error? = null, +) : Throwable(error?.message ?: "Unknown error") { + @Serializable + public data class Error( + val message: String? = null, + val code: String? = null, + val id: String? = null, + val kind: String? = null, + ) +} diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/home/misskey/MisskeyBaseChannelPresenter.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/home/misskey/MisskeyChannelListPresenter.kt similarity index 50% rename from shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/home/misskey/MisskeyBaseChannelPresenter.kt rename to shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/home/misskey/MisskeyChannelListPresenter.kt index 031784730..5e6062b6a 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/home/misskey/MisskeyBaseChannelPresenter.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/home/misskey/MisskeyChannelListPresenter.kt @@ -1,48 +1,100 @@ package dev.dimension.flare.ui.presenter.home.misskey import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.paging.cachedIn +import androidx.paging.compose.collectAsLazyPagingItems import dev.dimension.flare.common.PagingState import dev.dimension.flare.common.refreshSuspend -import dev.dimension.flare.data.datasource.microblog.MicroblogDataSource +import dev.dimension.flare.common.toPagingState import dev.dimension.flare.data.datasource.misskey.MisskeyDataSource import dev.dimension.flare.data.repository.AccountRepository import dev.dimension.flare.data.repository.accountServiceProvider import dev.dimension.flare.model.AccountType import dev.dimension.flare.ui.model.UiList -import dev.dimension.flare.ui.model.UiState +import dev.dimension.flare.ui.model.map import dev.dimension.flare.ui.model.onSuccess import dev.dimension.flare.ui.presenter.PresenterBase -import kotlinx.coroutines.CoroutineScope +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.launch import org.koin.core.component.KoinComponent import org.koin.core.component.inject -public abstract class MisskeyBaseChannelPresenter( +public class MisskeyChannelListPresenter( private val accountType: AccountType, -) : PresenterBase(), +) : PresenterBase(), KoinComponent { private val accountRepository: AccountRepository by inject() - @Composable - internal abstract fun getPagingData( - scope: CoroutineScope, - serviceState: UiState, - ): PagingState + public interface State { + public val type: Type + public val data: PagingState + + public suspend fun refreshSuspend() + + public fun refresh() + + public fun follow(list: UiList) + + public fun unfollow(list: UiList) + + public fun favorite(list: UiList) + + public fun unfavorite(list: UiList) + + public fun setType(data: Type) + + public val allTypes: ImmutableList get() = Type.entries.toImmutableList() + + public enum class Type { + Following, + Favorites, + Owned, + Featured, + } + } @Composable - override fun body(): MisskeyChannelsState { + override fun body(): State { + var type by remember { mutableStateOf(State.Type.Following) } val scope = rememberCoroutineScope() val serviceState = accountServiceProvider(accountType = accountType, repository = accountRepository) - val data = getPagingData(scope, serviceState) - - return object : MisskeyChannelsState { + val data = + serviceState + .map { service -> + require(service is MisskeyDataSource) + remember(type) { + when (type) { + State.Type.Following -> service.channelHandler.data.cachedIn(scope) + State.Type.Favorites -> service.myFavoriteChannelHandler.data.cachedIn(scope) + State.Type.Owned -> service.ownedChannelHandler.data.cachedIn(scope) + State.Type.Featured -> service.featuredChannels(scope) + } + }.collectAsLazyPagingItems() + }.toPagingState() + return object : State { override val data = data + override val type = type + + override fun setType(data: State.Type) { + type = data + } override suspend fun refreshSuspend() { data.refreshSuspend() } + override fun refresh() { + scope.launch { + data.refreshSuspend() + } + } + override fun follow(list: UiList) { serviceState.onSuccess { scope.launch { diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/home/misskey/MisskeyChannelsState.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/home/misskey/MisskeyChannelsState.kt deleted file mode 100644 index c665071fa..000000000 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/home/misskey/MisskeyChannelsState.kt +++ /dev/null @@ -1,20 +0,0 @@ -package dev.dimension.flare.ui.presenter.home.misskey - -import androidx.compose.runtime.Immutable -import dev.dimension.flare.common.PagingState -import dev.dimension.flare.ui.model.UiList - -@Immutable -public interface MisskeyChannelsState { - public val data: PagingState - - public suspend fun refreshSuspend() - - public fun follow(list: UiList) - - public fun unfollow(list: UiList) - - public fun favorite(list: UiList) - - public fun unfavorite(list: UiList) -} diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/home/misskey/MisskeyFavoriteChannelsPresenter.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/home/misskey/MisskeyFavoriteChannelsPresenter.kt deleted file mode 100644 index 8364d33ca..000000000 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/home/misskey/MisskeyFavoriteChannelsPresenter.kt +++ /dev/null @@ -1,33 +0,0 @@ -package dev.dimension.flare.ui.presenter.home.misskey - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember -import androidx.paging.cachedIn -import androidx.paging.compose.LazyPagingItems -import androidx.paging.compose.collectAsLazyPagingItems -import dev.dimension.flare.common.PagingState -import dev.dimension.flare.common.toPagingState -import dev.dimension.flare.data.datasource.microblog.MicroblogDataSource -import dev.dimension.flare.data.datasource.misskey.MisskeyDataSource -import dev.dimension.flare.model.AccountType -import dev.dimension.flare.ui.model.UiList -import dev.dimension.flare.ui.model.UiState -import dev.dimension.flare.ui.model.map -import kotlinx.coroutines.CoroutineScope - -public class MisskeyFavoriteChannelsPresenter( - accountType: AccountType, -) : MisskeyBaseChannelPresenter(accountType) { - @Composable - internal override fun getPagingData( - scope: CoroutineScope, - serviceState: UiState, - ): PagingState = - serviceState - .map> { service -> - require(service is MisskeyDataSource) - remember(service) { - service.myFavoriteChannelHandler.data.cachedIn(scope) - }.collectAsLazyPagingItems() - }.toPagingState() -} diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/home/misskey/MisskeyFeaturedChannelsPresenter.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/home/misskey/MisskeyFeaturedChannelsPresenter.kt deleted file mode 100644 index 7fcd8d0b4..000000000 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/home/misskey/MisskeyFeaturedChannelsPresenter.kt +++ /dev/null @@ -1,32 +0,0 @@ -package dev.dimension.flare.ui.presenter.home.misskey - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember -import androidx.paging.compose.LazyPagingItems -import androidx.paging.compose.collectAsLazyPagingItems -import dev.dimension.flare.common.PagingState -import dev.dimension.flare.common.toPagingState -import dev.dimension.flare.data.datasource.microblog.MicroblogDataSource -import dev.dimension.flare.data.datasource.misskey.MisskeyDataSource -import dev.dimension.flare.model.AccountType -import dev.dimension.flare.ui.model.UiList -import dev.dimension.flare.ui.model.UiState -import dev.dimension.flare.ui.model.map -import kotlinx.coroutines.CoroutineScope - -public class MisskeyFeaturedChannelsPresenter( - accountType: AccountType, -) : MisskeyBaseChannelPresenter(accountType) { - @Composable - internal override fun getPagingData( - scope: CoroutineScope, - serviceState: UiState, - ): PagingState = - serviceState - .map> { service -> - require(service is MisskeyDataSource) - remember(service) { - service.featuredChannels(scope) - }.collectAsLazyPagingItems() - }.toPagingState() -} diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/home/misskey/MisskeyFollowedChannelsPresenter.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/home/misskey/MisskeyFollowedChannelsPresenter.kt deleted file mode 100644 index fe236adc8..000000000 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/home/misskey/MisskeyFollowedChannelsPresenter.kt +++ /dev/null @@ -1,33 +0,0 @@ -package dev.dimension.flare.ui.presenter.home.misskey - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember -import androidx.paging.cachedIn -import androidx.paging.compose.LazyPagingItems -import androidx.paging.compose.collectAsLazyPagingItems -import dev.dimension.flare.common.PagingState -import dev.dimension.flare.common.toPagingState -import dev.dimension.flare.data.datasource.microblog.MicroblogDataSource -import dev.dimension.flare.data.datasource.misskey.MisskeyDataSource -import dev.dimension.flare.model.AccountType -import dev.dimension.flare.ui.model.UiList -import dev.dimension.flare.ui.model.UiState -import dev.dimension.flare.ui.model.map -import kotlinx.coroutines.CoroutineScope - -public class MisskeyFollowedChannelsPresenter( - accountType: AccountType, -) : MisskeyBaseChannelPresenter(accountType) { - @Composable - internal override fun getPagingData( - scope: CoroutineScope, - serviceState: UiState, - ): PagingState = - serviceState - .map> { service -> - require(service is MisskeyDataSource) - remember(service) { - service.channelHandler.data.cachedIn(scope) - }.collectAsLazyPagingItems() - }.toPagingState() -} diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/home/misskey/MisskeyOwnedChannelsPresenter.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/home/misskey/MisskeyOwnedChannelsPresenter.kt deleted file mode 100644 index 00ab308a0..000000000 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/home/misskey/MisskeyOwnedChannelsPresenter.kt +++ /dev/null @@ -1,33 +0,0 @@ -package dev.dimension.flare.ui.presenter.home.misskey - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember -import androidx.paging.cachedIn -import androidx.paging.compose.LazyPagingItems -import androidx.paging.compose.collectAsLazyPagingItems -import dev.dimension.flare.common.PagingState -import dev.dimension.flare.common.toPagingState -import dev.dimension.flare.data.datasource.microblog.MicroblogDataSource -import dev.dimension.flare.data.datasource.misskey.MisskeyDataSource -import dev.dimension.flare.model.AccountType -import dev.dimension.flare.ui.model.UiList -import dev.dimension.flare.ui.model.UiState -import dev.dimension.flare.ui.model.map -import kotlinx.coroutines.CoroutineScope - -public class MisskeyOwnedChannelsPresenter( - accountType: AccountType, -) : MisskeyBaseChannelPresenter(accountType) { - @Composable - internal override fun getPagingData( - scope: CoroutineScope, - serviceState: UiState, - ): PagingState = - serviceState - .map> { service -> - require(service is MisskeyDataSource) - remember(service) { - service.ownedChannelHandler.data.cachedIn(scope) - }.collectAsLazyPagingItems() - }.toPagingState() -} From 064191ef1cfb55b21069515e14251db82cbf4e03 Mon Sep 17 00:00:00 2001 From: Tlaster Date: Tue, 17 Feb 2026 17:34:05 +0900 Subject: [PATCH 09/14] fix channel description --- .../flare/ui/screen/list/EditListScreen.kt | 2 +- .../flare/ui/component/UiListItemComponent.kt | 4 +-- .../datasource/microblog/list/ListHandler.kt | 5 ++- .../misskey/FeaturedChannelPagingSource.kt | 36 ------------------- .../misskey/MisskeyChannelLoader.kt | 8 ++--- .../datasource/misskey/MisskeyDataSource.kt | 2 +- .../dev/dimension/flare/ui/model/UiList.kt | 3 +- .../flare/ui/model/mapper/Misskey.kt | 10 ++++-- .../dimension/flare/ui/render/UiRichText.kt | 25 +++++++++++++ 9 files changed, 47 insertions(+), 48 deletions(-) delete mode 100644 shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/misskey/FeaturedChannelPagingSource.kt diff --git a/app/src/main/java/dev/dimension/flare/ui/screen/list/EditListScreen.kt b/app/src/main/java/dev/dimension/flare/ui/screen/list/EditListScreen.kt index 1d0246ff2..3dbe85c8d 100644 --- a/app/src/main/java/dev/dimension/flare/ui/screen/list/EditListScreen.kt +++ b/app/src/main/java/dev/dimension/flare/ui/screen/list/EditListScreen.kt @@ -391,7 +391,7 @@ private fun presenter( when (it) { is dev.dimension.flare.ui.model.UiList.List -> it.description is dev.dimension.flare.ui.model.UiList.Feed -> it.description - is dev.dimension.flare.ui.model.UiList.Channel -> it.description + is dev.dimension.flare.ui.model.UiList.Channel -> null is dev.dimension.flare.ui.model.UiList.Antenna -> null } if (desc != null) { diff --git a/compose-ui/src/commonMain/kotlin/dev/dimension/flare/ui/component/UiListItemComponent.kt b/compose-ui/src/commonMain/kotlin/dev/dimension/flare/ui/component/UiListItemComponent.kt index 00664d174..0ccbd402d 100644 --- a/compose-ui/src/commonMain/kotlin/dev/dimension/flare/ui/component/UiListItemComponent.kt +++ b/compose-ui/src/commonMain/kotlin/dev/dimension/flare/ui/component/UiListItemComponent.kt @@ -472,7 +472,7 @@ private fun UiChannelCard( modifier: Modifier = Modifier, ) { val description = item.description - if (!description.isNullOrEmpty()) { + if (description != null) { Column( modifier = modifier @@ -521,7 +521,7 @@ private fun UiChannelCard( } }, ) - PlatformText( + RichText( text = description, modifier = Modifier diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/microblog/list/ListHandler.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/microblog/list/ListHandler.kt index 303af9172..40c110017 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/microblog/list/ListHandler.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/microblog/list/ListHandler.kt @@ -9,6 +9,7 @@ import dev.dimension.flare.data.database.cache.CacheDatabase import dev.dimension.flare.data.database.cache.connect import dev.dimension.flare.data.database.cache.model.DbList import dev.dimension.flare.data.database.cache.model.DbListPaging +import dev.dimension.flare.data.datasource.microblog.paging.PagingRequest import dev.dimension.flare.data.datasource.microblog.paging.createPagingRemoteMediator import dev.dimension.flare.data.datasource.microblog.pagingConfig import dev.dimension.flare.data.repository.tryRun @@ -45,7 +46,9 @@ internal class ListHandler( loader.load(pageSize, request) }, onSave = { request, data -> - database.listDao().deleteByPagingKey(pagingKey) + if (request == PagingRequest.Refresh) { + database.listDao().deleteByPagingKey(pagingKey) + } database.listDao().insertAllList( data.map { item -> DbList( diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/misskey/FeaturedChannelPagingSource.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/misskey/FeaturedChannelPagingSource.kt deleted file mode 100644 index 6610032c7..000000000 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/misskey/FeaturedChannelPagingSource.kt +++ /dev/null @@ -1,36 +0,0 @@ -package dev.dimension.flare.data.datasource.misskey - -import androidx.paging.PagingState -import dev.dimension.flare.common.BasePagingSource -import dev.dimension.flare.data.network.misskey.MisskeyService -import dev.dimension.flare.data.network.misskey.api.model.ChannelsFeaturedRequest -import dev.dimension.flare.data.repository.tryRun -import dev.dimension.flare.ui.model.UiList -import dev.dimension.flare.ui.model.mapper.render - -internal class FeaturedChannelPagingSource( - private val service: MisskeyService, -) : BasePagingSource() { - override suspend fun doLoad(params: LoadParams): LoadResult = - tryRun { - service - .channelsFeatured( - request = ChannelsFeaturedRequest(), - ).map { - it.render() - } - }.fold( - onSuccess = { antennas -> - LoadResult.Page( - data = antennas, - prevKey = null, - nextKey = null, - ) - }, - onFailure = { error -> - LoadResult.Error(error) - }, - ) - - override fun getRefreshKey(state: PagingState): Int? = null -} diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/misskey/MisskeyChannelLoader.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/misskey/MisskeyChannelLoader.kt index c41461343..d69bdb181 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/misskey/MisskeyChannelLoader.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/misskey/MisskeyChannelLoader.kt @@ -67,7 +67,7 @@ internal class MisskeyChannelLoader( ), ) }.map { - it.render() + it.render(accountKey) }.toImmutableList() return PagingResult( @@ -82,7 +82,7 @@ internal class MisskeyChannelLoader( ChannelsFollowRequest( channelId = listId, ), - ).render() + ).render(accountKey) override suspend fun create(metaData: ListMetaData): UiList = service @@ -91,7 +91,7 @@ internal class MisskeyChannelLoader( name = metaData.title, description = metaData.description, ), - ).render() + ).render(accountKey) override suspend fun update( listId: String, @@ -104,7 +104,7 @@ internal class MisskeyChannelLoader( name = metaData.title, description = metaData.description, ), - ).render() + ).render(accountKey) override suspend fun delete(listId: String): Unit = throw UnsupportedOperationException("Delete channel is not supported") diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/misskey/MisskeyDataSource.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/misskey/MisskeyDataSource.kt index 3f3a3318c..afd1e7135 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/misskey/MisskeyDataSource.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/misskey/MisskeyDataSource.kt @@ -181,7 +181,7 @@ internal class MisskeyDataSource( return LoadResult.Page( data = result.map { - it.render() + it.render(accountKey) }, prevKey = null, nextKey = null, diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/model/UiList.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/model/UiList.kt index 8624d6983..fbc146f14 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/model/UiList.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/model/UiList.kt @@ -1,6 +1,7 @@ package dev.dimension.flare.ui.model import androidx.compose.runtime.Immutable +import dev.dimension.flare.ui.render.UiRichText import kotlinx.serialization.Serializable @Serializable @@ -51,7 +52,7 @@ public sealed class UiList { val isArchived: Boolean, val notesCount: Double, val usersCount: Double, - val description: String? = null, + val description: UiRichText? = null, val banner: String? = null, val isFollowing: Boolean? = null, val isFavorited: Boolean? = null, diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/model/mapper/Misskey.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/model/mapper/Misskey.kt index faf16216a..4117ad33a 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/model/mapper/Misskey.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/model/mapper/Misskey.kt @@ -1106,11 +1106,17 @@ internal fun Antenna.render(): UiList.Antenna = title = name, ) -internal fun Channel.render(): UiList.Channel = +internal fun Channel.render(accountKey: MicroBlogKey): UiList.Channel = UiList.Channel( id = id, title = name, - description = description, + description = + description + ?.takeIf { + it.isNotEmpty() + }?.let { + misskeyParser.parse(it).toHtml(accountKey, emptyMap(), accountKey.host).toUi() + }, isArchived = isArchived ?: false, notesCount = notesCount ?: 0.0, usersCount = usersCount ?: 0.0, diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/render/UiRichText.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/render/UiRichText.kt index 6abba5a0b..f6e89a93e 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/render/UiRichText.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/render/UiRichText.kt @@ -5,7 +5,14 @@ import com.fleeksoft.ksoup.Ksoup import com.fleeksoft.ksoup.nodes.Element import com.fleeksoft.ksoup.nodes.TextNode import de.cketti.codepoints.codePointCount +import kotlinx.serialization.KSerializer +import kotlinx.serialization.Serializable +import kotlinx.serialization.descriptors.PrimitiveKind +import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder +@Serializable(with = UiRichTextSerializer::class) @Immutable public data class UiRichText( val data: Element, @@ -34,6 +41,24 @@ public data class UiRichText( data.getElementsByTag("emoji").mapNotNull { it.attr("target").ifEmpty { null } } } +internal object UiRichTextSerializer : KSerializer { + override val descriptor by lazy { + PrimitiveSerialDescriptor("UiRichText", PrimitiveKind.STRING) + } + + override fun serialize( + encoder: Encoder, + value: UiRichText, + ) { + encoder.encodeString(value.html) + } + + override fun deserialize(decoder: Decoder): UiRichText { + val html = decoder.decodeString() + return parseHtml(html).toUi() + } +} + internal fun Element.toUi(): UiRichText = UiRichText( data = this, From f81e2b5a414ae71636b204789a5bd11e5df7d39f Mon Sep 17 00:00:00 2001 From: Tlaster Date: Wed, 18 Feb 2026 12:20:51 +0900 Subject: [PATCH 10/14] fix edit account list --- .../ui/screen/list/EditAccountListScreen.kt | 9 ++-- .../flare/data/database/cache/dao/ListDao.kt | 4 ++ .../microblog/list/ListMemberHandler.kt | 46 +++++++++---------- .../list/EditAccountListPresenter.kt | 43 +++++++++++++---- 4 files changed, 64 insertions(+), 38 deletions(-) diff --git a/app/src/main/java/dev/dimension/flare/ui/screen/list/EditAccountListScreen.kt b/app/src/main/java/dev/dimension/flare/ui/screen/list/EditAccountListScreen.kt index 88aedb69c..d76532da1 100644 --- a/app/src/main/java/dev/dimension/flare/ui/screen/list/EditAccountListScreen.kt +++ b/app/src/main/java/dev/dimension/flare/ui/screen/list/EditAccountListScreen.kt @@ -18,8 +18,6 @@ import compose.icons.fontawesomeicons.Solid import compose.icons.fontawesomeicons.solid.Plus import compose.icons.fontawesomeicons.solid.Trash import dev.dimension.flare.R -import dev.dimension.flare.common.onLoading -import dev.dimension.flare.common.onSuccess import dev.dimension.flare.model.AccountType import dev.dimension.flare.model.MicroBlogKey import dev.dimension.flare.ui.component.BackButton @@ -70,9 +68,10 @@ internal fun EditAccountListScreen( uiListItemComponent( state.lists, ) { item -> - state.userLists - .onSuccess { - if (it.any { list -> list.id == item.id }) { + state + .isInList(item) + .onSuccess { inList -> + if (inList) { IconButton( onClick = { state.removeList(item) }, ) { diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/database/cache/dao/ListDao.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/database/cache/dao/ListDao.kt index 6b4589ad4..d4956dc98 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/data/database/cache/dao/ListDao.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/data/database/cache/dao/ListDao.kt @@ -94,4 +94,8 @@ internal interface ListDao { @Transaction @Query("SELECT * FROM DbUser WHERE userKey = :userKey") fun getUserByKey(userKey: MicroBlogKey): PagingSource + + @Transaction + @Query("SELECT * FROM DbUser WHERE userKey = :userKey") + fun getUserByKeyFlow(userKey: MicroBlogKey): Flow } diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/microblog/list/ListMemberHandler.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/microblog/list/ListMemberHandler.kt index 4ea3b4a78..209382106 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/microblog/list/ListMemberHandler.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/microblog/list/ListMemberHandler.kt @@ -2,12 +2,13 @@ package dev.dimension.flare.data.datasource.microblog.list import androidx.paging.ExperimentalPagingApi import androidx.paging.Pager -import androidx.paging.flatMap import androidx.paging.map +import dev.dimension.flare.common.Cacheable import dev.dimension.flare.data.database.cache.CacheDatabase import dev.dimension.flare.data.database.cache.connect import dev.dimension.flare.data.database.cache.model.DbList import dev.dimension.flare.data.database.cache.model.DbListMember +import dev.dimension.flare.data.datasource.microblog.paging.PagingRequest import dev.dimension.flare.data.datasource.microblog.paging.createPagingRemoteMediator import dev.dimension.flare.data.datasource.microblog.pagingConfig import dev.dimension.flare.data.repository.tryRun @@ -124,23 +125,17 @@ internal class ListMemberHandler( get() = "${pagingKey}_user_lists" fun userLists(userKey: MicroBlogKey) = - Pager( - config = pagingConfig, - pagingSourceFactory = { - database.listDao().getUserByKey(userKey) - }, - remoteMediator = - createPagingRemoteMediator( - pagingKey = userListsPagingKey, - database = database, - onLoad = { pageSize, request -> + Cacheable( + fetchSource = { + tryRun { + val result = loader.loadUserLists( - pageSize = pageSize, - request = request, + pageSize = 100, + request = PagingRequest.Refresh, userKey = userKey, ) - }, - onSave = { request, data -> + val data = result.data + database.connect { database.listDao().insertAllList( data.map { item -> DbList( @@ -158,13 +153,18 @@ internal class ListMemberHandler( ) }, ) - }, - ), - ).flow.map { - it.flatMap { - it.listMemberships.map { - it.list.content.data + } } - } - } + }, + cacheSource = { + database + .listDao() + .getUserByKeyFlow(userKey) + .map { + it.listMemberships.map { + it.list.content.data + } + } + }, + ) } diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/list/EditAccountListPresenter.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/list/EditAccountListPresenter.kt index d06f398be..315f7eb88 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/list/EditAccountListPresenter.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/list/EditAccountListPresenter.kt @@ -2,6 +2,7 @@ package dev.dimension.flare.ui.presenter.list import androidx.compose.runtime.Composable import androidx.compose.runtime.Immutable +import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.paging.cachedIn @@ -11,13 +12,21 @@ import dev.dimension.flare.common.PagingState import dev.dimension.flare.common.toPagingState import dev.dimension.flare.data.datasource.microblog.list.ListDataSource import dev.dimension.flare.data.repository.AccountRepository +import dev.dimension.flare.data.repository.accountServiceFlow import dev.dimension.flare.data.repository.accountServiceProvider import dev.dimension.flare.model.AccountType import dev.dimension.flare.model.MicroBlogKey import dev.dimension.flare.ui.model.UiList +import dev.dimension.flare.ui.model.UiState +import dev.dimension.flare.ui.model.flattenUiState import dev.dimension.flare.ui.model.map import dev.dimension.flare.ui.model.onSuccess +import dev.dimension.flare.ui.model.toUi import dev.dimension.flare.ui.presenter.PresenterBase +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.toImmutableList +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch import org.koin.core.component.KoinComponent @@ -34,6 +43,19 @@ public class EditAccountListPresenter( KoinComponent { private val accountRepository: AccountRepository by inject() + @OptIn(ExperimentalCoroutinesApi::class) + private val userListFlow by lazy { + accountServiceFlow(accountType, accountRepository) + .flatMapLatest { service -> + require(service is ListDataSource) + service.listMemberHandler.userLists(userKey).toUi() + }.map { + it.map { + it.toImmutableList() + } + } + } + @Composable override fun body(): EditAccountListState { val scope = rememberCoroutineScope() @@ -50,15 +72,7 @@ public class EditAccountListPresenter( } }.collectAsLazyPagingItems() }.toPagingState() - val userLists = - serviceState - .map { service -> - require(service is ListDataSource) - remember(service) { - service.listMemberHandler.userLists(userKey).cachedIn(scope) - }.collectAsLazyPagingItems() - }.toPagingState() - + val userLists by userListFlow.flattenUiState() return object : EditAccountListState { override val lists = allList override val userLists = userLists @@ -80,6 +94,13 @@ public class EditAccountListPresenter( } } } + + override fun isInList(list: UiList): UiState = + userLists.map { item -> + item.any { + it.id == list.id + } + } } } } @@ -94,9 +115,11 @@ public interface EditAccountListState { /** * Lists that the user is a member of. */ - public val userLists: PagingState + public val userLists: UiState> public fun addList(list: UiList) public fun removeList(list: UiList) + + public fun isInList(list: UiList): UiState } From 3aae1718e572e237999fa2e4829e639053e3df50 Mon Sep 17 00:00:00 2001 From: Tlaster Date: Wed, 18 Feb 2026 14:13:56 +0900 Subject: [PATCH 11/14] Refactor user model from UiUserV2 to UiProfile across the application - Replaced all instances of UiUserV2 with UiProfile in data sources, UI models, and presenters. - Updated related methods and functions to accommodate the new user model. - Introduced ClickEvent for handling user interactions. - Adjusted tests to reflect changes in user model type. --- .../dimension/flare/ui/component/SearchBar.kt | 4 +- .../flare/ui/screen/home/HomeScreen.kt | 4 +- .../ui/screen/list/EditListMemberScreen.kt | 32 +++----- .../flare/ui/screen/media/PodcastScreen.kt | 4 +- .../flare/ui/screen/profile/UserListScreen.kt | 4 +- .../ui/screen/settings/AccountsScreen.kt | 12 +-- .../dimension/flare/data/model/TabSettings.kt | 6 +- .../dimension/flare/ui/component/dm/DMItem.kt | 4 +- .../status/CommonStatusHeaderComponent.kt | 4 +- .../status/StatusRetweetHeaderComponent.kt | 4 +- .../flare/ui/component/status/UserCompat.kt | 4 +- .../flare/ui/component/AccountItem.kt | 10 +-- .../flare/ui/screen/home/UserListScreen.kt | 4 +- iosApp/flare/UI/Component/PagingView.swift | 2 +- .../flare/UI/Component/UserCompatView.swift | 4 +- .../flare/UI/Component/UserOnelineView.swift | 4 +- iosApp/flare/UI/Screen/ProfileScreen.swift | 2 +- .../flare/common/SerializableImmutableList.kt | 62 ++++++++++++++++ .../datasource/bluesky/BlueskyDataSource.kt | 11 ++- .../datasource/bluesky/FansPagingSource.kt | 8 +- .../bluesky/FollowingPagingSource.kt | 8 +- .../bluesky/SearchUserPagingSource.kt | 8 +- .../bluesky/TrendsUserPagingSource.kt | 8 +- .../guest/mastodon/GuestMastodonDataSource.kt | 11 ++- .../datasource/mastodon/MastodonDataSource.kt | 13 ++-- .../mastodon/MastodonFansPagingSource.kt | 8 +- .../mastodon/MastodonFollowingPagingSource.kt | 8 +- .../mastodon/SearchUserPagingSource.kt | 8 +- .../mastodon/TrendsUserPagingSource.kt | 8 +- .../microblog/MicroblogDataSource.kt | 11 ++- .../datasource/misskey/FansPagingSource.kt | 8 +- .../misskey/FollowingPagingSource.kt | 8 +- .../datasource/misskey/MisskeyDataSource.kt | 11 ++- .../misskey/SearchUserPagingSource.kt | 8 +- .../misskey/TrendsUserPagingSource.kt | 8 +- .../data/datasource/vvo/FansPagingSource.kt | 8 +- .../datasource/vvo/FollowingPagingSource.kt | 8 +- .../datasource/vvo/SearchUserPagingSource.kt | 8 +- .../data/datasource/vvo/VVODataSource.kt | 11 ++- .../data/datasource/xqt/FansPagingSource.kt | 8 +- .../datasource/xqt/FollowingPagingSource.kt | 8 +- .../datasource/xqt/SearchUserPagingSource.kt | 8 +- .../datasource/xqt/TrendsUserPagingSource.kt | 8 +- .../data/datasource/xqt/XQTDataSource.kt | 11 ++- .../dimension/flare/ui/model/ClickContext.kt | 28 +++++++ .../dev/dimension/flare/ui/model/UiDMRoom.kt | 4 +- .../dev/dimension/flare/ui/model/UiList.kt | 4 +- .../dev/dimension/flare/ui/model/UiPodcast.kt | 8 +- .../dev/dimension/flare/ui/model/UiProfile.kt | 38 ++++++---- .../dev/dimension/flare/ui/model/UiStatus.kt | 2 +- .../dimension/flare/ui/model/UiTimeline.kt | 8 +- .../dev/dimension/flare/ui/model/UiUserV2.kt | 16 ---- .../flare/ui/model/mapper/Bluesky.kt | 22 +++--- .../flare/ui/model/mapper/Mastodon.kt | 8 +- .../flare/ui/model/mapper/Misskey.kt | 15 ++-- .../dimension/flare/ui/model/mapper/VVO.kt | 8 +- .../dimension/flare/ui/model/mapper/XQT.kt | 18 ++--- .../ui/presenter/compose/ComposePresenter.kt | 9 +-- .../presenter/dm/DMConversationPresenter.kt | 4 +- .../presenter/home/ActiveAccountPresenter.kt | 4 +- .../ui/presenter/home/DiscoverPresenter.kt | 3 +- .../ui/presenter/home/SearchPresenter.kt | 3 +- .../flare/ui/presenter/home/UserPresenter.kt | 7 -- .../presenter/list/EditListMemberPresenter.kt | 74 +++++++++++-------- .../presenter/profile/FollowingPresenter.kt | 6 +- .../ui/presenter/profile/ProfilePresenter.kt | 3 +- .../settings/LocalCacheSearchPresenter.kt | 14 ++-- .../microblog/list/ListHandlerTest.kt | 1 + .../microblog/list/ListMemberHandlerTest.kt | 2 +- 69 files changed, 387 insertions(+), 323 deletions(-) create mode 100644 shared/src/commonMain/kotlin/dev/dimension/flare/common/SerializableImmutableList.kt delete mode 100644 shared/src/commonMain/kotlin/dev/dimension/flare/ui/model/UiUserV2.kt diff --git a/app/src/main/java/dev/dimension/flare/ui/component/SearchBar.kt b/app/src/main/java/dev/dimension/flare/ui/component/SearchBar.kt index c25c683e4..32a6a4a80 100644 --- a/app/src/main/java/dev/dimension/flare/ui/component/SearchBar.kt +++ b/app/src/main/java/dev/dimension/flare/ui/component/SearchBar.kt @@ -47,10 +47,10 @@ import dev.dimension.flare.ui.component.status.AdaptiveCard import dev.dimension.flare.ui.component.status.CommonStatusHeaderComponent import dev.dimension.flare.ui.component.status.UserPlaceholder import dev.dimension.flare.ui.component.status.status +import dev.dimension.flare.ui.model.UiProfile import dev.dimension.flare.ui.model.UiSearchHistory import dev.dimension.flare.ui.model.UiState import dev.dimension.flare.ui.model.UiTimeline -import dev.dimension.flare.ui.model.UiUserV2 import dev.dimension.flare.ui.model.onSuccess import dev.dimension.flare.ui.presenter.home.SearchHistoryPresenter import dev.dimension.flare.ui.presenter.home.SearchHistoryState @@ -165,7 +165,7 @@ private fun SearchContent( } internal fun LazyStaggeredGridScope.searchContent( - searchUsers: PagingState, + searchUsers: PagingState, searchStatus: PagingState, toUser: (MicroBlogKey) -> Unit, ) { diff --git a/app/src/main/java/dev/dimension/flare/ui/screen/home/HomeScreen.kt b/app/src/main/java/dev/dimension/flare/ui/screen/home/HomeScreen.kt index bed8529c5..3c4a311d4 100644 --- a/app/src/main/java/dev/dimension/flare/ui/screen/home/HomeScreen.kt +++ b/app/src/main/java/dev/dimension/flare/ui/screen/home/HomeScreen.kt @@ -81,8 +81,8 @@ import dev.dimension.flare.ui.component.TabTitle import dev.dimension.flare.ui.component.TopLevelBackStack import dev.dimension.flare.ui.component.listCard import dev.dimension.flare.ui.component.platform.isBigScreen +import dev.dimension.flare.ui.model.UiProfile import dev.dimension.flare.ui.model.UiState -import dev.dimension.flare.ui.model.UiUserV2 import dev.dimension.flare.ui.model.isError import dev.dimension.flare.ui.model.isSuccess import dev.dimension.flare.ui.model.onLoading @@ -311,7 +311,7 @@ internal fun HomeScreen(afterInit: () -> Unit) { @OptIn(ExperimentalMaterial3ExpressiveApi::class, ExperimentalSharedTransitionApi::class) private fun HomeRailHeader( wideNavigationRailState: WideNavigationRailState, - userState: UiState, + userState: UiState, layoutType: NavigationSuiteType, currentRoute: Route, navigate: (Route) -> Unit, diff --git a/app/src/main/java/dev/dimension/flare/ui/screen/list/EditListMemberScreen.kt b/app/src/main/java/dev/dimension/flare/ui/screen/list/EditListMemberScreen.kt index 4578ab7d8..a1e39fd86 100644 --- a/app/src/main/java/dev/dimension/flare/ui/screen/list/EditListMemberScreen.kt +++ b/app/src/main/java/dev/dimension/flare/ui/screen/list/EditListMemberScreen.kt @@ -45,7 +45,6 @@ import dev.dimension.flare.ui.model.UiState import dev.dimension.flare.ui.presenter.invoke import dev.dimension.flare.ui.presenter.list.EditListMemberPresenter import dev.dimension.flare.ui.presenter.list.EditListMemberState -import dev.dimension.flare.ui.presenter.list.EmptyQueryException import dev.dimension.flare.ui.screen.settings.AccountItem import dev.dimension.flare.ui.theme.screenHorizontalPadding import moe.tlaster.precompose.molecule.producePresenter @@ -188,28 +187,15 @@ internal fun EditListMemberScreen( ) } }.onError { - if (it is EmptyQueryException) { - item { - ListItem( - headlineContent = { - Text(text = stringResource(id = R.string.edit_list_member_search_empty)) - }, - modifier = - Modifier - .listCard(), - ) - } - } else { - item { - ListItem( - headlineContent = { - Text(text = stringResource(id = R.string.edit_list_member_search_error)) - }, - modifier = - Modifier - .listCard(), - ) - } + item { + ListItem( + headlineContent = { + Text(text = stringResource(id = R.string.edit_list_member_search_error)) + }, + modifier = + Modifier + .listCard(), + ) } } } diff --git a/app/src/main/java/dev/dimension/flare/ui/screen/media/PodcastScreen.kt b/app/src/main/java/dev/dimension/flare/ui/screen/media/PodcastScreen.kt index 2950bd277..5757eedad 100644 --- a/app/src/main/java/dev/dimension/flare/ui/screen/media/PodcastScreen.kt +++ b/app/src/main/java/dev/dimension/flare/ui/screen/media/PodcastScreen.kt @@ -28,8 +28,8 @@ import dev.dimension.flare.model.MicroBlogKey import dev.dimension.flare.ui.component.AvatarComponent import dev.dimension.flare.ui.component.RichText import dev.dimension.flare.ui.model.UiPodcast +import dev.dimension.flare.ui.model.UiProfile import dev.dimension.flare.ui.model.UiState -import dev.dimension.flare.ui.model.UiUserV2 import dev.dimension.flare.ui.model.map import dev.dimension.flare.ui.model.onError import dev.dimension.flare.ui.model.onLoading @@ -150,7 +150,7 @@ internal fun ColumnScope.PodcastContent( @Composable private fun UserItem( - item: UiUserV2, + item: UiProfile, modifier: Modifier = Modifier, bottom: @Composable () -> Unit = {}, ) { diff --git a/app/src/main/java/dev/dimension/flare/ui/screen/profile/UserListScreen.kt b/app/src/main/java/dev/dimension/flare/ui/screen/profile/UserListScreen.kt index 5b9d15cb1..4b0139d12 100644 --- a/app/src/main/java/dev/dimension/flare/ui/screen/profile/UserListScreen.kt +++ b/app/src/main/java/dev/dimension/flare/ui/screen/profile/UserListScreen.kt @@ -26,8 +26,8 @@ import dev.dimension.flare.ui.component.FlareLargeFlexibleTopAppBar import dev.dimension.flare.ui.component.FlareScaffold import dev.dimension.flare.ui.component.RefreshContainer import dev.dimension.flare.ui.component.listCard +import dev.dimension.flare.ui.model.UiProfile import dev.dimension.flare.ui.model.UiState -import dev.dimension.flare.ui.model.UiUserV2 import dev.dimension.flare.ui.presenter.invoke import dev.dimension.flare.ui.presenter.profile.FansPresenter import dev.dimension.flare.ui.presenter.profile.FollowingPresenter @@ -112,7 +112,7 @@ internal fun FansScreen( @OptIn(ExperimentalMaterial3Api::class) @Composable internal fun UserListScreen( - data: PagingState, + data: PagingState, title: @Composable () -> Unit, onBack: () -> Unit, isRefreshing: Boolean, diff --git a/app/src/main/java/dev/dimension/flare/ui/screen/settings/AccountsScreen.kt b/app/src/main/java/dev/dimension/flare/ui/screen/settings/AccountsScreen.kt index bcb6390d0..b65e84e6f 100644 --- a/app/src/main/java/dev/dimension/flare/ui/screen/settings/AccountsScreen.kt +++ b/app/src/main/java/dev/dimension/flare/ui/screen/settings/AccountsScreen.kt @@ -59,8 +59,8 @@ import dev.dimension.flare.ui.component.RichText import dev.dimension.flare.ui.component.ThemeIconData import dev.dimension.flare.ui.component.ThemedIcon import dev.dimension.flare.ui.component.placeholder +import dev.dimension.flare.ui.model.UiProfile import dev.dimension.flare.ui.model.UiState -import dev.dimension.flare.ui.model.UiUserV2 import dev.dimension.flare.ui.model.isError import dev.dimension.flare.ui.model.isSuccess import dev.dimension.flare.ui.model.onError @@ -232,16 +232,16 @@ internal fun AccountsScreen( @OptIn(ExperimentalMaterial3ExpressiveApi::class) @Composable -fun AccountItem( - userState: UiState, +fun AccountItem( + userState: UiState, onClick: (MicroBlogKey) -> Unit, toLogin: () -> Unit, modifier: Modifier = Modifier, - trailingContent: @Composable (UiUserV2) -> Unit = { }, - headlineContent: @Composable (UiUserV2) -> Unit = { + trailingContent: @Composable (UiProfile) -> Unit = { }, + headlineContent: @Composable (UiProfile) -> Unit = { RichText(text = it.name, maxLines = 1) }, - supportingContent: @Composable (UiUserV2) -> Unit = { + supportingContent: @Composable (UiProfile) -> Unit = { Text(text = it.handle, maxLines = 1) }, avatarSize: Dp = AvatarComponentDefaults.size, diff --git a/compose-ui/src/commonMain/kotlin/dev/dimension/flare/data/model/TabSettings.kt b/compose-ui/src/commonMain/kotlin/dev/dimension/flare/data/model/TabSettings.kt index 2ba63a2d7..01d00f8b6 100644 --- a/compose-ui/src/commonMain/kotlin/dev/dimension/flare/data/model/TabSettings.kt +++ b/compose-ui/src/commonMain/kotlin/dev/dimension/flare/data/model/TabSettings.kt @@ -7,8 +7,8 @@ import dev.dimension.flare.model.MicroBlogKey import dev.dimension.flare.model.PlatformType import dev.dimension.flare.ui.model.UiAccount import dev.dimension.flare.ui.model.UiList +import dev.dimension.flare.ui.model.UiProfile import dev.dimension.flare.ui.model.UiRssSource -import dev.dimension.flare.ui.model.UiUserV2 import dev.dimension.flare.ui.presenter.home.HomeTimelinePresenter import dev.dimension.flare.ui.presenter.home.MixedTimelinePresenter import dev.dimension.flare.ui.presenter.home.TimelinePresenter @@ -254,7 +254,7 @@ public sealed class TimelineTabItem : TabItem() { SettingsTabItem, ) - public fun defaultPrimary(user: UiUserV2): ImmutableList = + public fun defaultPrimary(user: UiProfile): ImmutableList = when (user.platformType) { PlatformType.Mastodon -> mastodon(user.key) PlatformType.Misskey -> misskey(user.key) @@ -275,7 +275,7 @@ public sealed class TimelineTabItem : TabItem() { return result.toImmutableList() } - public fun secondaryFor(user: UiUserV2): ImmutableList = + public fun secondaryFor(user: UiProfile): ImmutableList = when (user.platformType) { PlatformType.Mastodon -> defaultMastodonSecondaryItems(user.key) PlatformType.Misskey -> defaultMisskeySecondaryItems(user.key) diff --git a/compose-ui/src/commonMain/kotlin/dev/dimension/flare/ui/component/dm/DMItem.kt b/compose-ui/src/commonMain/kotlin/dev/dimension/flare/ui/component/dm/DMItem.kt index ea3d4f282..ff85c59ef 100644 --- a/compose-ui/src/commonMain/kotlin/dev/dimension/flare/ui/component/dm/DMItem.kt +++ b/compose-ui/src/commonMain/kotlin/dev/dimension/flare/ui/component/dm/DMItem.kt @@ -36,7 +36,7 @@ import dev.dimension.flare.ui.component.status.CommonStatusComponent import dev.dimension.flare.ui.component.status.MediaItem import dev.dimension.flare.ui.model.UiDMItem import dev.dimension.flare.ui.model.UiMedia -import dev.dimension.flare.ui.model.UiUserV2 +import dev.dimension.flare.ui.model.UiProfile import dev.dimension.flare.ui.route.DeeplinkRoute import dev.dimension.flare.ui.route.toUri import dev.dimension.flare.ui.theme.PlatformTheme @@ -46,7 +46,7 @@ import org.jetbrains.compose.resources.stringResource public fun DMItem( item: UiDMItem, onRetry: () -> Unit, - onUserClicked: (UiUserV2) -> Unit, + onUserClicked: (UiProfile) -> Unit, modifier: Modifier = Modifier, ) { val uriHandler = LocalUriHandler.current diff --git a/compose-ui/src/commonMain/kotlin/dev/dimension/flare/ui/component/status/CommonStatusHeaderComponent.kt b/compose-ui/src/commonMain/kotlin/dev/dimension/flare/ui/component/status/CommonStatusHeaderComponent.kt index fc11b1936..4d2c7173c 100644 --- a/compose-ui/src/commonMain/kotlin/dev/dimension/flare/ui/component/status/CommonStatusHeaderComponent.kt +++ b/compose-ui/src/commonMain/kotlin/dev/dimension/flare/ui/component/status/CommonStatusHeaderComponent.kt @@ -20,12 +20,12 @@ import dev.dimension.flare.model.MicroBlogKey import dev.dimension.flare.ui.component.AvatarComponent import dev.dimension.flare.ui.component.RichText import dev.dimension.flare.ui.component.platform.PlatformText -import dev.dimension.flare.ui.model.UiUserV2 +import dev.dimension.flare.ui.model.UiProfile import dev.dimension.flare.ui.theme.PlatformTheme @Composable public fun CommonStatusHeaderComponent( - data: UiUserV2, + data: UiProfile, onUserClick: (MicroBlogKey) -> Unit, modifier: Modifier = Modifier, leadingContent: @Composable (RowScope.() -> Unit)? = { diff --git a/compose-ui/src/commonMain/kotlin/dev/dimension/flare/ui/component/status/StatusRetweetHeaderComponent.kt b/compose-ui/src/commonMain/kotlin/dev/dimension/flare/ui/component/status/StatusRetweetHeaderComponent.kt index bdbafdeac..8a0a6aad3 100644 --- a/compose-ui/src/commonMain/kotlin/dev/dimension/flare/ui/component/status/StatusRetweetHeaderComponent.kt +++ b/compose-ui/src/commonMain/kotlin/dev/dimension/flare/ui/component/status/StatusRetweetHeaderComponent.kt @@ -14,14 +14,14 @@ import androidx.compose.ui.unit.dp import dev.dimension.flare.ui.component.FAIcon import dev.dimension.flare.ui.component.RichText import dev.dimension.flare.ui.component.platform.PlatformText -import dev.dimension.flare.ui.model.UiUserV2 +import dev.dimension.flare.ui.model.UiProfile import dev.dimension.flare.ui.theme.PlatformContentColor import dev.dimension.flare.ui.theme.PlatformTheme @Composable internal fun StatusRetweetHeaderComponent( icon: ImageVector, - user: UiUserV2?, + user: UiProfile?, text: String, modifier: Modifier = Modifier, textStyle: TextStyle = PlatformTheme.typography.caption, diff --git a/compose-ui/src/commonMain/kotlin/dev/dimension/flare/ui/component/status/UserCompat.kt b/compose-ui/src/commonMain/kotlin/dev/dimension/flare/ui/component/status/UserCompat.kt index 728f6e13f..9decec3db 100644 --- a/compose-ui/src/commonMain/kotlin/dev/dimension/flare/ui/component/status/UserCompat.kt +++ b/compose-ui/src/commonMain/kotlin/dev/dimension/flare/ui/component/status/UserCompat.kt @@ -14,12 +14,12 @@ import dev.dimension.flare.ui.component.AvatarComponent import dev.dimension.flare.ui.component.AvatarComponentDefaults import dev.dimension.flare.ui.component.RichText import dev.dimension.flare.ui.component.platform.PlatformText -import dev.dimension.flare.ui.model.UiUserV2 +import dev.dimension.flare.ui.model.UiProfile import dev.dimension.flare.ui.theme.PlatformTheme @Composable internal fun UserCompat( - user: UiUserV2, + user: UiProfile, modifier: Modifier = Modifier, onUserClick: (MicroBlogKey) -> Unit = {}, leading: @Composable (RowScope.() -> Unit)? = { diff --git a/desktopApp/src/main/kotlin/dev/dimension/flare/ui/component/AccountItem.kt b/desktopApp/src/main/kotlin/dev/dimension/flare/ui/component/AccountItem.kt index a7ee57195..05855f3e6 100644 --- a/desktopApp/src/main/kotlin/dev/dimension/flare/ui/component/AccountItem.kt +++ b/desktopApp/src/main/kotlin/dev/dimension/flare/ui/component/AccountItem.kt @@ -16,8 +16,8 @@ import dev.dimension.flare.login_expired import dev.dimension.flare.login_expired_relogin import dev.dimension.flare.model.MicroBlogKey import dev.dimension.flare.ui.component.placeholder +import dev.dimension.flare.ui.model.UiProfile import dev.dimension.flare.ui.model.UiState -import dev.dimension.flare.ui.model.UiUserV2 import dev.dimension.flare.ui.model.onError import dev.dimension.flare.ui.model.onLoading import dev.dimension.flare.ui.model.onSuccess @@ -27,16 +27,16 @@ import io.github.composefluent.component.Text import org.jetbrains.compose.resources.stringResource @Composable -fun AccountItem( +fun AccountItem( userState: UiState, onClick: (MicroBlogKey) -> Unit, toLogin: () -> Unit, modifier: Modifier = Modifier, - trailingContent: @Composable (UiUserV2?) -> Unit = { }, - headlineContent: @Composable (UiUserV2) -> Unit = { + trailingContent: @Composable (UiProfile?) -> Unit = { }, + headlineContent: @Composable (UiProfile) -> Unit = { RichText(text = it.name, maxLines = 1) }, - supportingContent: @Composable (UiUserV2) -> Unit = { + supportingContent: @Composable (UiProfile) -> Unit = { Text(text = it.handle, maxLines = 1) }, avatarSize: Dp = 24.dp, diff --git a/desktopApp/src/main/kotlin/dev/dimension/flare/ui/screen/home/UserListScreen.kt b/desktopApp/src/main/kotlin/dev/dimension/flare/ui/screen/home/UserListScreen.kt index 08ffa8003..21f471ef3 100644 --- a/desktopApp/src/main/kotlin/dev/dimension/flare/ui/screen/home/UserListScreen.kt +++ b/desktopApp/src/main/kotlin/dev/dimension/flare/ui/screen/home/UserListScreen.kt @@ -15,8 +15,8 @@ import dev.dimension.flare.model.AccountType import dev.dimension.flare.model.MicroBlogKey import dev.dimension.flare.ui.common.itemsIndexed import dev.dimension.flare.ui.component.AccountItem +import dev.dimension.flare.ui.model.UiProfile import dev.dimension.flare.ui.model.UiState -import dev.dimension.flare.ui.model.UiUserV2 import dev.dimension.flare.ui.presenter.invoke import dev.dimension.flare.ui.presenter.profile.FansPresenter import dev.dimension.flare.ui.presenter.profile.FollowingPresenter @@ -85,7 +85,7 @@ internal fun FansScreen( @Composable private fun UserListScreen( - data: PagingState, + data: PagingState, onUserClick: (MicroBlogKey) -> Unit, ) { LazyColumn( diff --git a/iosApp/flare/UI/Component/PagingView.swift b/iosApp/flare/UI/Component/PagingView.swift index 46f499651..5c22c49ed 100644 --- a/iosApp/flare/UI/Component/PagingView.swift +++ b/iosApp/flare/UI/Component/PagingView.swift @@ -107,7 +107,7 @@ extension PagingView { struct UserPagingView: View { @Environment(\.openURL) private var openURL - let data: PagingState + let data: PagingState var body: some View { PagingView(data: data) { user in UserCompatView(data: user) diff --git a/iosApp/flare/UI/Component/UserCompatView.swift b/iosApp/flare/UI/Component/UserCompatView.swift index 8e684b0ce..5b6531388 100644 --- a/iosApp/flare/UI/Component/UserCompatView.swift +++ b/iosApp/flare/UI/Component/UserCompatView.swift @@ -3,7 +3,7 @@ import KotlinSharedUI struct UserCompatView: View { @Environment(\.openURL) private var openURL - let data: UiUserV2 + let data: UiProfile let trailing: () -> TrailingContent let onClicked: (() -> Void)? var body: some View { @@ -38,7 +38,7 @@ struct UserCompatView: View { } extension UserCompatView { - init(data: UiUserV2) where TrailingContent == EmptyView { + init(data: UiProfile) where TrailingContent == EmptyView { self.data = data self.trailing = { EmptyView() diff --git a/iosApp/flare/UI/Component/UserOnelineView.swift b/iosApp/flare/UI/Component/UserOnelineView.swift index ee587f659..edd015e11 100644 --- a/iosApp/flare/UI/Component/UserOnelineView.swift +++ b/iosApp/flare/UI/Component/UserOnelineView.swift @@ -3,7 +3,7 @@ import KotlinSharedUI struct UserOnelineView: View { @Environment(\.openURL) private var openURL - let data: UiUserV2 + let data: UiProfile let showAvatar: Bool let trailing: () -> TrailingContent let onClicked: (() -> Void)? @@ -39,7 +39,7 @@ struct UserOnelineView: View { } extension UserOnelineView { - init(data: UiUserV2) where TrailingContent == EmptyView { + init(data: UiProfile) where TrailingContent == EmptyView { self.data = data self.trailing = { EmptyView() diff --git a/iosApp/flare/UI/Screen/ProfileScreen.swift b/iosApp/flare/UI/Screen/ProfileScreen.swift index 6e65c3988..57807c30c 100644 --- a/iosApp/flare/UI/Screen/ProfileScreen.swift +++ b/iosApp/flare/UI/Screen/ProfileScreen.swift @@ -187,7 +187,7 @@ struct ProfileHeader: View { let user: UiState let relation: UiState let isMe: UiState - let onFollowClick: (UiUserV2, UiRelation) -> Void + let onFollowClick: (UiProfile, UiRelation) -> Void let onFollowingClick: (MicroBlogKey) -> Void let onFansClick: (MicroBlogKey) -> Void var body: some View { diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/common/SerializableImmutableList.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/common/SerializableImmutableList.kt new file mode 100644 index 000000000..31f65267e --- /dev/null +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/common/SerializableImmutableList.kt @@ -0,0 +1,62 @@ +package dev.dimension.flare.common + +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.ImmutableMap +import kotlinx.collections.immutable.toPersistentList +import kotlinx.collections.immutable.toPersistentMap +import kotlinx.serialization.KSerializer +import kotlinx.serialization.SealedSerializationApi +import kotlinx.serialization.Serializable +import kotlinx.serialization.builtins.ListSerializer +import kotlinx.serialization.builtins.MapSerializer +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.descriptors.serialDescriptor +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder + +// https://github.com/Kotlin/kotlinx.collections.immutable/issues/63 +internal typealias SerializableImmutableList = + @Serializable(ImmutableListSerializer::class) + ImmutableList + +internal class ImmutableListSerializer( + private val dataSerializer: KSerializer, +) : KSerializer> { + @OptIn(SealedSerializationApi::class) + private class PersistentListDescriptor : SerialDescriptor by serialDescriptor>() { + override val serialName: String = "kotlinx.serialization.immutable.ImmutableList" + } + + override val descriptor: SerialDescriptor = PersistentListDescriptor() + + override fun serialize( + encoder: Encoder, + value: ImmutableList, + ) = ListSerializer(dataSerializer).serialize(encoder, value.toList()) + + override fun deserialize(decoder: Decoder): ImmutableList = ListSerializer(dataSerializer).deserialize(decoder).toPersistentList() +} + +internal typealias SerializableImmutableMap = + @Serializable(ImmutableMapSerializer::class) + ImmutableMap + +internal class ImmutableMapSerializer( + private val keySerializer: KSerializer, + private val valueSerializer: KSerializer, +) : KSerializer> { + @OptIn(SealedSerializationApi::class) + private class PersistentMapDescriptor : SerialDescriptor by serialDescriptor>() { + override val serialName: String = "kotlinx.serialization.immutable.ImmutableMap" + } + + override val descriptor: SerialDescriptor = PersistentMapDescriptor() + + override fun serialize( + encoder: Encoder, + value: ImmutableMap, + ) = MapSerializer(keySerializer, valueSerializer).serialize(encoder, value.toMap()) + + override fun deserialize(decoder: Decoder): ImmutableMap = + MapSerializer(keySerializer, valueSerializer).deserialize(decoder).toPersistentMap() +} diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/bluesky/BlueskyDataSource.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/bluesky/BlueskyDataSource.kt index 129e5f670..ce00d3ab2 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/bluesky/BlueskyDataSource.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/bluesky/BlueskyDataSource.kt @@ -97,7 +97,6 @@ import dev.dimension.flare.ui.model.UiProfile import dev.dimension.flare.ui.model.UiRelation import dev.dimension.flare.ui.model.UiState import dev.dimension.flare.ui.model.UiTimeline -import dev.dimension.flare.ui.model.UiUserV2 import dev.dimension.flare.ui.model.mapper.bskyJson import dev.dimension.flare.ui.model.mapper.parseBskyFacets import dev.dimension.flare.ui.model.mapper.render @@ -219,7 +218,7 @@ internal class BlueskyDataSource( override val supportedNotificationFilter: List get() = listOf(NotificationFilter.All) - override fun userByAcct(acct: String): CacheData { + override fun userByAcct(acct: String): CacheData { val (name, host) = MicroBlogKey.valueOf(acct) return Cacheable( fetchSource = { @@ -980,7 +979,7 @@ internal class BlueskyDataSource( override fun searchUser( query: String, pageSize: Int, - ): Flow> = + ): Flow> = Pager( config = pagingConfig, ) { @@ -991,7 +990,7 @@ internal class BlueskyDataSource( ) }.flow - override fun discoverUsers(pageSize: Int): Flow> = + override fun discoverUsers(pageSize: Int): Flow> = Pager( config = pagingConfig, ) { @@ -1578,7 +1577,7 @@ internal class BlueskyDataSource( userKey: MicroBlogKey, scope: CoroutineScope, pageSize: Int, - ): Flow> = + ): Flow> = Pager( config = pagingConfig, ) { @@ -1593,7 +1592,7 @@ internal class BlueskyDataSource( userKey: MicroBlogKey, scope: CoroutineScope, pageSize: Int, - ): Flow> = + ): Flow> = Pager( config = pagingConfig, ) { diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/bluesky/FansPagingSource.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/bluesky/FansPagingSource.kt index cc223dd5d..074a1c19f 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/bluesky/FansPagingSource.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/bluesky/FansPagingSource.kt @@ -5,7 +5,7 @@ import app.bsky.graph.GetFollowersQueryParams import dev.dimension.flare.common.BasePagingSource import dev.dimension.flare.data.network.bluesky.BlueskyService import dev.dimension.flare.model.MicroBlogKey -import dev.dimension.flare.ui.model.UiUserV2 +import dev.dimension.flare.ui.model.UiProfile import dev.dimension.flare.ui.model.mapper.render import sh.christian.ozone.api.Did @@ -13,10 +13,10 @@ internal class FansPagingSource( private val service: BlueskyService, private val accountKey: MicroBlogKey, private val userKey: MicroBlogKey, -) : BasePagingSource() { - override fun getRefreshKey(state: PagingState): String? = null +) : BasePagingSource() { + override fun getRefreshKey(state: PagingState): String? = null - override suspend fun doLoad(params: LoadParams): LoadResult { + override suspend fun doLoad(params: LoadParams): LoadResult { val cursor = params.key val limit = params.loadSize val response = diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/bluesky/FollowingPagingSource.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/bluesky/FollowingPagingSource.kt index deac658dc..2bcbcf649 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/bluesky/FollowingPagingSource.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/bluesky/FollowingPagingSource.kt @@ -5,7 +5,7 @@ import app.bsky.graph.GetFollowsQueryParams import dev.dimension.flare.common.BasePagingSource import dev.dimension.flare.data.network.bluesky.BlueskyService import dev.dimension.flare.model.MicroBlogKey -import dev.dimension.flare.ui.model.UiUserV2 +import dev.dimension.flare.ui.model.UiProfile import dev.dimension.flare.ui.model.mapper.render import sh.christian.ozone.api.Did @@ -13,10 +13,10 @@ internal class FollowingPagingSource( private val service: BlueskyService, private val accountKey: MicroBlogKey, private val userKey: MicroBlogKey, -) : BasePagingSource() { - override fun getRefreshKey(state: PagingState): String? = null +) : BasePagingSource() { + override fun getRefreshKey(state: PagingState): String? = null - override suspend fun doLoad(params: LoadParams): LoadResult { + override suspend fun doLoad(params: LoadParams): LoadResult { val cursor = params.key val limit = params.loadSize val response = diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/bluesky/SearchUserPagingSource.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/bluesky/SearchUserPagingSource.kt index 3a3378663..93dad5e1c 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/bluesky/SearchUserPagingSource.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/bluesky/SearchUserPagingSource.kt @@ -5,17 +5,17 @@ import app.bsky.actor.SearchActorsQueryParams import dev.dimension.flare.common.BasePagingSource import dev.dimension.flare.data.network.bluesky.BlueskyService import dev.dimension.flare.model.MicroBlogKey -import dev.dimension.flare.ui.model.UiUserV2 +import dev.dimension.flare.ui.model.UiProfile import dev.dimension.flare.ui.model.mapper.render internal class SearchUserPagingSource( private val service: BlueskyService, private val accountKey: MicroBlogKey, private val query: String, -) : BasePagingSource() { - override fun getRefreshKey(state: PagingState): String? = null +) : BasePagingSource() { + override fun getRefreshKey(state: PagingState): String? = null - override suspend fun doLoad(params: LoadParams): LoadResult { + override suspend fun doLoad(params: LoadParams): LoadResult { service .searchActors( SearchActorsQueryParams(q = query, limit = params.loadSize.toLong(), cursor = params.key), diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/bluesky/TrendsUserPagingSource.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/bluesky/TrendsUserPagingSource.kt index 3f99bd285..bafea783d 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/bluesky/TrendsUserPagingSource.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/bluesky/TrendsUserPagingSource.kt @@ -5,16 +5,16 @@ import app.bsky.actor.GetSuggestionsQueryParams import dev.dimension.flare.common.BasePagingSource import dev.dimension.flare.data.network.bluesky.BlueskyService import dev.dimension.flare.model.MicroBlogKey -import dev.dimension.flare.ui.model.UiUserV2 +import dev.dimension.flare.ui.model.UiProfile import dev.dimension.flare.ui.model.mapper.render internal class TrendsUserPagingSource( private val service: BlueskyService, private val accountKey: MicroBlogKey, -) : BasePagingSource() { - override fun getRefreshKey(state: PagingState): String? = null +) : BasePagingSource() { + override fun getRefreshKey(state: PagingState): String? = null - override suspend fun doLoad(params: LoadParams): LoadResult { + override suspend fun doLoad(params: LoadParams): LoadResult { val response = service .getSuggestions(GetSuggestionsQueryParams(limit = params.loadSize.toLong(), cursor = params.key)) diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/guest/mastodon/GuestMastodonDataSource.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/guest/mastodon/GuestMastodonDataSource.kt index abf46597b..8e93aba32 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/guest/mastodon/GuestMastodonDataSource.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/guest/mastodon/GuestMastodonDataSource.kt @@ -23,7 +23,6 @@ import dev.dimension.flare.model.PlatformType import dev.dimension.flare.ui.model.UiHashtag import dev.dimension.flare.ui.model.UiProfile import dev.dimension.flare.ui.model.UiTimeline -import dev.dimension.flare.ui.model.UiUserV2 import dev.dimension.flare.ui.model.mapper.render import dev.dimension.flare.ui.model.mapper.renderGuest import kotlinx.collections.immutable.ImmutableList @@ -50,7 +49,7 @@ internal class GuestMastodonDataSource( GuestTimelinePagingSource(host = host, service = service) } - override fun userByAcct(acct: String): CacheData { + override fun userByAcct(acct: String): CacheData { val (name, host) = MicroBlogKey.valueOf(acct) return Cacheable( fetchSource = { @@ -144,7 +143,7 @@ internal class GuestMastodonDataSource( override fun searchUser( query: String, pageSize: Int, - ): Flow> = + ): Flow> = Pager( config = pagingConfig, ) { @@ -156,7 +155,7 @@ internal class GuestMastodonDataSource( ) }.flow - override fun discoverUsers(pageSize: Int): Flow> { + override fun discoverUsers(pageSize: Int): Flow> { // not supported throw UnsupportedOperationException("Discover users not supported") } @@ -179,7 +178,7 @@ internal class GuestMastodonDataSource( userKey: MicroBlogKey, scope: CoroutineScope, pageSize: Int, - ): Flow> = + ): Flow> = Pager( config = pagingConfig, ) { @@ -195,7 +194,7 @@ internal class GuestMastodonDataSource( userKey: MicroBlogKey, scope: CoroutineScope, pageSize: Int, - ): Flow> = + ): Flow> = Pager( config = pagingConfig, ) { diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/mastodon/MastodonDataSource.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/mastodon/MastodonDataSource.kt index 279a103e0..06a4db154 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/mastodon/MastodonDataSource.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/mastodon/MastodonDataSource.kt @@ -54,7 +54,6 @@ import dev.dimension.flare.ui.model.UiProfile import dev.dimension.flare.ui.model.UiRelation import dev.dimension.flare.ui.model.UiState import dev.dimension.flare.ui.model.UiTimeline -import dev.dimension.flare.ui.model.UiUserV2 import dev.dimension.flare.ui.model.mapper.render import dev.dimension.flare.ui.model.mapper.toUi import dev.dimension.flare.ui.model.toUi @@ -228,7 +227,7 @@ internal open class MastodonDataSource( NotificationFilter.Mention, ) - override fun userByAcct(acct: String): CacheData { + override fun userByAcct(acct: String): CacheData { val (name, host) = MicroBlogKey.valueOf(acct) return Cacheable( fetchSource = { @@ -770,7 +769,7 @@ internal open class MastodonDataSource( } } - override fun discoverUsers(pageSize: Int): Flow> = + override fun discoverUsers(pageSize: Int): Flow> = Pager( config = pagingConfig, ) { @@ -808,7 +807,7 @@ internal open class MastodonDataSource( override fun searchUser( query: String, pageSize: Int, - ): Flow> = + ): Flow> = Pager( config = pagingConfig, ) { @@ -824,7 +823,7 @@ internal open class MastodonDataSource( query: String, scope: CoroutineScope, pageSize: Int = 20, - ): Flow> = + ): Flow> = Pager( config = pagingConfig, ) { @@ -1045,7 +1044,7 @@ internal open class MastodonDataSource( userKey: MicroBlogKey, scope: CoroutineScope, pageSize: Int, - ): Flow> = + ): Flow> = Pager( config = pagingConfig, ) { @@ -1061,7 +1060,7 @@ internal open class MastodonDataSource( userKey: MicroBlogKey, scope: CoroutineScope, pageSize: Int, - ): Flow> = + ): Flow> = Pager( config = pagingConfig, ) { diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/mastodon/MastodonFansPagingSource.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/mastodon/MastodonFansPagingSource.kt index 3fbb8a53a..063b486fa 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/mastodon/MastodonFansPagingSource.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/mastodon/MastodonFansPagingSource.kt @@ -4,7 +4,7 @@ import androidx.paging.PagingState import dev.dimension.flare.common.BasePagingSource import dev.dimension.flare.data.network.mastodon.api.AccountResources import dev.dimension.flare.model.MicroBlogKey -import dev.dimension.flare.ui.model.UiUserV2 +import dev.dimension.flare.ui.model.UiProfile import dev.dimension.flare.ui.model.mapper.render internal class MastodonFansPagingSource( @@ -12,10 +12,10 @@ internal class MastodonFansPagingSource( private val accountKey: MicroBlogKey?, private val host: String, private val userKey: MicroBlogKey, -) : BasePagingSource() { - override fun getRefreshKey(state: PagingState): String? = null +) : BasePagingSource() { + override fun getRefreshKey(state: PagingState): String? = null - override suspend fun doLoad(params: LoadParams): LoadResult { + override suspend fun doLoad(params: LoadParams): LoadResult { val maxId = params.key val limit = params.loadSize val response = diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/mastodon/MastodonFollowingPagingSource.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/mastodon/MastodonFollowingPagingSource.kt index e8e739669..8931fae45 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/mastodon/MastodonFollowingPagingSource.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/mastodon/MastodonFollowingPagingSource.kt @@ -4,7 +4,7 @@ import androidx.paging.PagingState import dev.dimension.flare.common.BasePagingSource import dev.dimension.flare.data.network.mastodon.api.AccountResources import dev.dimension.flare.model.MicroBlogKey -import dev.dimension.flare.ui.model.UiUserV2 +import dev.dimension.flare.ui.model.UiProfile import dev.dimension.flare.ui.model.mapper.render internal class MastodonFollowingPagingSource( @@ -12,10 +12,10 @@ internal class MastodonFollowingPagingSource( private val accountKey: MicroBlogKey?, private val host: String, private val userKey: MicroBlogKey, -) : BasePagingSource() { - override fun getRefreshKey(state: PagingState): String? = null +) : BasePagingSource() { + override fun getRefreshKey(state: PagingState): String? = null - override suspend fun doLoad(params: LoadParams): LoadResult { + override suspend fun doLoad(params: LoadParams): LoadResult { val maxId = params.key val limit = params.loadSize val response = diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/mastodon/SearchUserPagingSource.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/mastodon/SearchUserPagingSource.kt index 5dcc2701d..50082516b 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/mastodon/SearchUserPagingSource.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/mastodon/SearchUserPagingSource.kt @@ -4,7 +4,7 @@ import androidx.paging.PagingState import dev.dimension.flare.common.BasePagingSource import dev.dimension.flare.data.network.mastodon.api.SearchResources import dev.dimension.flare.model.MicroBlogKey -import dev.dimension.flare.ui.model.UiUserV2 +import dev.dimension.flare.ui.model.UiProfile import dev.dimension.flare.ui.model.mapper.render internal class SearchUserPagingSource( @@ -14,10 +14,10 @@ internal class SearchUserPagingSource( private val query: String, private val following: Boolean = false, private val resolve: Boolean? = null, -) : BasePagingSource() { - override fun getRefreshKey(state: PagingState): String? = null +) : BasePagingSource() { + override fun getRefreshKey(state: PagingState): String? = null - override suspend fun doLoad(params: LoadParams): LoadResult { + override suspend fun doLoad(params: LoadParams): LoadResult { service .searchV2( query = query, diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/mastodon/TrendsUserPagingSource.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/mastodon/TrendsUserPagingSource.kt index 39bdf1424..1715e1dba 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/mastodon/TrendsUserPagingSource.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/mastodon/TrendsUserPagingSource.kt @@ -4,17 +4,17 @@ import androidx.paging.PagingState import dev.dimension.flare.common.BasePagingSource import dev.dimension.flare.data.network.mastodon.api.TrendsResources import dev.dimension.flare.model.MicroBlogKey -import dev.dimension.flare.ui.model.UiUserV2 +import dev.dimension.flare.ui.model.UiProfile import dev.dimension.flare.ui.model.mapper.render internal class TrendsUserPagingSource( private val service: TrendsResources, private val accountKey: MicroBlogKey?, private val host: String, -) : BasePagingSource() { - override fun getRefreshKey(state: PagingState): Int? = null +) : BasePagingSource() { + override fun getRefreshKey(state: PagingState): Int? = null - override suspend fun doLoad(params: LoadParams): LoadResult { + override suspend fun doLoad(params: LoadParams): LoadResult { service .suggestionsUsers() .mapNotNull { diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/microblog/MicroblogDataSource.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/microblog/MicroblogDataSource.kt index 0c11b6fe3..dbd5e612c 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/microblog/MicroblogDataSource.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/microblog/MicroblogDataSource.kt @@ -7,7 +7,6 @@ import dev.dimension.flare.model.MicroBlogKey import dev.dimension.flare.ui.model.UiHashtag import dev.dimension.flare.ui.model.UiProfile import dev.dimension.flare.ui.model.UiTimeline -import dev.dimension.flare.ui.model.UiUserV2 import kotlinx.collections.immutable.ImmutableList import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow @@ -15,7 +14,7 @@ import kotlinx.coroutines.flow.Flow internal interface MicroblogDataSource { fun homeTimeline(): BaseTimelineLoader - fun userByAcct(acct: String): CacheData + fun userByAcct(acct: String): CacheData fun userById(id: String): CacheData @@ -33,9 +32,9 @@ internal interface MicroblogDataSource { fun searchUser( query: String, pageSize: Int = 20, - ): Flow> + ): Flow> - fun discoverUsers(pageSize: Int = 20): Flow> + fun discoverUsers(pageSize: Int = 20): Flow> fun discoverStatuses(): BaseTimelineLoader @@ -45,13 +44,13 @@ internal interface MicroblogDataSource { userKey: MicroBlogKey, scope: CoroutineScope, pageSize: Int = 20, - ): Flow> + ): Flow> fun fans( userKey: MicroBlogKey, scope: CoroutineScope, pageSize: Int = 20, - ): Flow> + ): Flow> fun profileTabs(userKey: MicroBlogKey): ImmutableList } diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/misskey/FansPagingSource.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/misskey/FansPagingSource.kt index 456a42ec8..cca6dfacd 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/misskey/FansPagingSource.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/misskey/FansPagingSource.kt @@ -5,17 +5,17 @@ import dev.dimension.flare.common.BasePagingSource import dev.dimension.flare.data.network.misskey.MisskeyService import dev.dimension.flare.data.network.misskey.api.model.UsersFollowersRequest import dev.dimension.flare.model.MicroBlogKey -import dev.dimension.flare.ui.model.UiUserV2 +import dev.dimension.flare.ui.model.UiProfile import dev.dimension.flare.ui.model.mapper.render internal class FansPagingSource( private val service: MisskeyService, private val accountKey: MicroBlogKey, private val userKey: MicroBlogKey, -) : BasePagingSource() { - override fun getRefreshKey(state: PagingState): String? = null +) : BasePagingSource() { + override fun getRefreshKey(state: PagingState): String? = null - override suspend fun doLoad(params: LoadParams): LoadResult { + override suspend fun doLoad(params: LoadParams): LoadResult { val maxId = params.key val limit = params.loadSize val response = diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/misskey/FollowingPagingSource.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/misskey/FollowingPagingSource.kt index 82ff87f24..cd7c15e0c 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/misskey/FollowingPagingSource.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/misskey/FollowingPagingSource.kt @@ -5,17 +5,17 @@ import dev.dimension.flare.common.BasePagingSource import dev.dimension.flare.data.network.misskey.MisskeyService import dev.dimension.flare.data.network.misskey.api.model.UsersFollowersRequest import dev.dimension.flare.model.MicroBlogKey -import dev.dimension.flare.ui.model.UiUserV2 +import dev.dimension.flare.ui.model.UiProfile import dev.dimension.flare.ui.model.mapper.render internal class FollowingPagingSource( private val service: MisskeyService, private val accountKey: MicroBlogKey, private val userKey: MicroBlogKey, -) : BasePagingSource() { - override fun getRefreshKey(state: PagingState): String? = null +) : BasePagingSource() { + override fun getRefreshKey(state: PagingState): String? = null - override suspend fun doLoad(params: LoadParams): LoadResult { + override suspend fun doLoad(params: LoadParams): LoadResult { val maxId = params.key val limit = params.loadSize val response = diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/misskey/MisskeyDataSource.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/misskey/MisskeyDataSource.kt index afd1e7135..9b8da9c6d 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/misskey/MisskeyDataSource.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/misskey/MisskeyDataSource.kt @@ -62,7 +62,6 @@ import dev.dimension.flare.ui.model.UiProfile import dev.dimension.flare.ui.model.UiRelation import dev.dimension.flare.ui.model.UiState import dev.dimension.flare.ui.model.UiTimeline -import dev.dimension.flare.ui.model.UiUserV2 import dev.dimension.flare.ui.model.mapper.render import dev.dimension.flare.ui.model.mapper.toUi import dev.dimension.flare.ui.model.toUi @@ -245,7 +244,7 @@ internal class MisskeyDataSource( NotificationFilter.Mention, ) - override fun userByAcct(acct: String): CacheData { + override fun userByAcct(acct: String): CacheData { val (name, host) = MicroBlogKey.valueOf(acct) return Cacheable( fetchSource = { @@ -771,7 +770,7 @@ internal class MisskeyDataSource( override fun searchUser( query: String, pageSize: Int, - ): Flow> = + ): Flow> = Pager( config = pagingConfig, ) { @@ -782,7 +781,7 @@ internal class MisskeyDataSource( ) }.flow - override fun discoverUsers(pageSize: Int): Flow> = + override fun discoverUsers(pageSize: Int): Flow> = Pager( config = pagingConfig, ) { @@ -983,7 +982,7 @@ internal class MisskeyDataSource( userKey: MicroBlogKey, scope: CoroutineScope, pageSize: Int, - ): Flow> = + ): Flow> = Pager( config = pagingConfig, ) { @@ -998,7 +997,7 @@ internal class MisskeyDataSource( userKey: MicroBlogKey, scope: CoroutineScope, pageSize: Int, - ): Flow> = + ): Flow> = Pager( config = pagingConfig, ) { diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/misskey/SearchUserPagingSource.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/misskey/SearchUserPagingSource.kt index 8b5e2821d..e4bd78769 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/misskey/SearchUserPagingSource.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/misskey/SearchUserPagingSource.kt @@ -5,17 +5,17 @@ import dev.dimension.flare.common.BasePagingSource import dev.dimension.flare.data.network.misskey.MisskeyService import dev.dimension.flare.data.network.misskey.api.model.UsersSearchRequest import dev.dimension.flare.model.MicroBlogKey -import dev.dimension.flare.ui.model.UiUserV2 +import dev.dimension.flare.ui.model.UiProfile import dev.dimension.flare.ui.model.mapper.render internal class SearchUserPagingSource( private val service: MisskeyService, private val accountKey: MicroBlogKey, private val query: String, -) : BasePagingSource() { - override fun getRefreshKey(state: PagingState): Int? = null +) : BasePagingSource() { + override fun getRefreshKey(state: PagingState): Int? = null - override suspend fun doLoad(params: LoadParams): LoadResult { + override suspend fun doLoad(params: LoadParams): LoadResult { service .usersSearch( UsersSearchRequest( diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/misskey/TrendsUserPagingSource.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/misskey/TrendsUserPagingSource.kt index 7465cc77a..e1cdbdf7e 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/misskey/TrendsUserPagingSource.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/misskey/TrendsUserPagingSource.kt @@ -5,16 +5,16 @@ import dev.dimension.flare.common.BasePagingSource import dev.dimension.flare.data.network.misskey.MisskeyService import dev.dimension.flare.data.network.misskey.api.model.PinnedUsersRequest import dev.dimension.flare.model.MicroBlogKey -import dev.dimension.flare.ui.model.UiUserV2 +import dev.dimension.flare.ui.model.UiProfile import dev.dimension.flare.ui.model.mapper.render internal class TrendsUserPagingSource( private val service: MisskeyService, private val accountKey: MicroBlogKey, -) : BasePagingSource() { - override fun getRefreshKey(state: PagingState): Int? = null +) : BasePagingSource() { + override fun getRefreshKey(state: PagingState): Int? = null - override suspend fun doLoad(params: LoadParams): LoadResult { + override suspend fun doLoad(params: LoadParams): LoadResult { service .pinnedUsers(PinnedUsersRequest(limit = params.loadSize)) .map { diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/vvo/FansPagingSource.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/vvo/FansPagingSource.kt index a93ea0f75..02741a4a5 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/vvo/FansPagingSource.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/vvo/FansPagingSource.kt @@ -4,15 +4,15 @@ import androidx.paging.PagingState import dev.dimension.flare.common.BasePagingSource import dev.dimension.flare.data.network.vvo.VVOService import dev.dimension.flare.model.MicroBlogKey -import dev.dimension.flare.ui.model.UiUserV2 +import dev.dimension.flare.ui.model.UiProfile import dev.dimension.flare.ui.model.mapper.render internal class FansPagingSource( private val service: VVOService, private val accountKey: MicroBlogKey, private val userKey: MicroBlogKey, -) : BasePagingSource() { - override fun getRefreshKey(state: PagingState): Int? = null +) : BasePagingSource() { + override fun getRefreshKey(state: PagingState): Int? = null private val containerId by lazy { if (accountKey == userKey) { @@ -22,7 +22,7 @@ internal class FansPagingSource( } } - override suspend fun doLoad(params: LoadParams): LoadResult { + override suspend fun doLoad(params: LoadParams): LoadResult { val nextPage = params.key ?: 0 val limit = params.loadSize val users = diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/vvo/FollowingPagingSource.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/vvo/FollowingPagingSource.kt index cb1e9449d..e07644db7 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/vvo/FollowingPagingSource.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/vvo/FollowingPagingSource.kt @@ -4,15 +4,15 @@ import androidx.paging.PagingState import dev.dimension.flare.common.BasePagingSource import dev.dimension.flare.data.network.vvo.VVOService import dev.dimension.flare.model.MicroBlogKey -import dev.dimension.flare.ui.model.UiUserV2 +import dev.dimension.flare.ui.model.UiProfile import dev.dimension.flare.ui.model.mapper.render internal class FollowingPagingSource( private val service: VVOService, private val accountKey: MicroBlogKey, private val userKey: MicroBlogKey, -) : BasePagingSource() { - override fun getRefreshKey(state: PagingState): Int? = null +) : BasePagingSource() { + override fun getRefreshKey(state: PagingState): Int? = null private val containerId by lazy { if (accountKey == userKey) { @@ -22,7 +22,7 @@ internal class FollowingPagingSource( } } - override suspend fun doLoad(params: LoadParams): LoadResult { + override suspend fun doLoad(params: LoadParams): LoadResult { val nextPage = params.key ?: 1 val limit = params.loadSize val users = diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/vvo/SearchUserPagingSource.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/vvo/SearchUserPagingSource.kt index 939291430..542857b23 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/vvo/SearchUserPagingSource.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/vvo/SearchUserPagingSource.kt @@ -6,21 +6,21 @@ import dev.dimension.flare.data.network.vvo.VVOService import dev.dimension.flare.data.repository.LoginExpiredException import dev.dimension.flare.model.MicroBlogKey import dev.dimension.flare.model.PlatformType -import dev.dimension.flare.ui.model.UiUserV2 +import dev.dimension.flare.ui.model.UiProfile import dev.dimension.flare.ui.model.mapper.render internal class SearchUserPagingSource( private val service: VVOService, private val accountKey: MicroBlogKey, private val query: String, -) : BasePagingSource() { +) : BasePagingSource() { private val containerId by lazy { "100103type=3&q=$query&t=" } - override fun getRefreshKey(state: PagingState): Int? = null + override fun getRefreshKey(state: PagingState): Int? = null - override suspend fun doLoad(params: LoadParams): LoadResult { + override suspend fun doLoad(params: LoadParams): LoadResult { val config = service.config() if (config.data?.login != true) { return LoadResult.Error( diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/vvo/VVODataSource.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/vvo/VVODataSource.kt index 5f0a8dcc9..30830af66 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/vvo/VVODataSource.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/vvo/VVODataSource.kt @@ -49,7 +49,6 @@ import dev.dimension.flare.ui.model.UiProfile import dev.dimension.flare.ui.model.UiRelation import dev.dimension.flare.ui.model.UiState import dev.dimension.flare.ui.model.UiTimeline -import dev.dimension.flare.ui.model.UiUserV2 import dev.dimension.flare.ui.model.mapper.render import dev.dimension.flare.ui.model.mapper.toUi import dev.dimension.flare.ui.model.toUi @@ -161,7 +160,7 @@ internal class VVODataSource( NotificationFilter.Like, ) - override fun userByAcct(acct: String): CacheData { + override fun userByAcct(acct: String): CacheData { val (name, host) = MicroBlogKey.valueOf(acct.removePrefix("@")) return Cacheable( fetchSource = { @@ -443,7 +442,7 @@ internal class VVODataSource( override fun searchUser( query: String, pageSize: Int, - ): Flow> = + ): Flow> = Pager( config = pagingConfig, ) { @@ -454,7 +453,7 @@ internal class VVODataSource( ) }.flow - override fun discoverUsers(pageSize: Int): Flow> { + override fun discoverUsers(pageSize: Int): Flow> { TODO("Not yet implemented") } @@ -924,7 +923,7 @@ internal class VVODataSource( userKey: MicroBlogKey, scope: CoroutineScope, pageSize: Int, - ): Flow> = + ): Flow> = Pager( config = pagingConfig, ) { @@ -939,7 +938,7 @@ internal class VVODataSource( userKey: MicroBlogKey, scope: CoroutineScope, pageSize: Int, - ): Flow> = + ): Flow> = Pager( config = pagingConfig, ) { diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/xqt/FansPagingSource.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/xqt/FansPagingSource.kt index 84d30094c..314a116d0 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/xqt/FansPagingSource.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/xqt/FansPagingSource.kt @@ -7,7 +7,7 @@ import dev.dimension.flare.data.database.cache.mapper.cursor import dev.dimension.flare.data.database.cache.mapper.users import dev.dimension.flare.data.network.xqt.XQTService import dev.dimension.flare.model.MicroBlogKey -import dev.dimension.flare.ui.model.UiUserV2 +import dev.dimension.flare.ui.model.UiProfile import dev.dimension.flare.ui.model.mapper.render import kotlinx.serialization.Required import kotlinx.serialization.SerialName @@ -17,10 +17,10 @@ internal class FansPagingSource( private val service: XQTService, private val accountKey: MicroBlogKey, private val userKey: MicroBlogKey, -) : BasePagingSource() { - override fun getRefreshKey(state: PagingState): String? = null +) : BasePagingSource() { + override fun getRefreshKey(state: PagingState): String? = null - override suspend fun doLoad(params: LoadParams): LoadResult { + override suspend fun doLoad(params: LoadParams): LoadResult { val cursor = params.key val limit = params.loadSize val response = diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/xqt/FollowingPagingSource.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/xqt/FollowingPagingSource.kt index 072ab4f60..228c8dfcf 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/xqt/FollowingPagingSource.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/xqt/FollowingPagingSource.kt @@ -7,17 +7,17 @@ import dev.dimension.flare.data.database.cache.mapper.cursor import dev.dimension.flare.data.database.cache.mapper.users import dev.dimension.flare.data.network.xqt.XQTService import dev.dimension.flare.model.MicroBlogKey -import dev.dimension.flare.ui.model.UiUserV2 +import dev.dimension.flare.ui.model.UiProfile import dev.dimension.flare.ui.model.mapper.render internal class FollowingPagingSource( private val service: XQTService, private val accountKey: MicroBlogKey, private val userKey: MicroBlogKey, -) : BasePagingSource() { - override fun getRefreshKey(state: PagingState): String? = null +) : BasePagingSource() { + override fun getRefreshKey(state: PagingState): String? = null - override suspend fun doLoad(params: LoadParams): LoadResult { + override suspend fun doLoad(params: LoadParams): LoadResult { val cursor = params.key val limit = params.loadSize val response = diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/xqt/SearchUserPagingSource.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/xqt/SearchUserPagingSource.kt index d13f0fe57..e75837d14 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/xqt/SearchUserPagingSource.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/xqt/SearchUserPagingSource.kt @@ -8,7 +8,7 @@ import dev.dimension.flare.data.database.cache.mapper.isBottomEnd import dev.dimension.flare.data.database.cache.mapper.users import dev.dimension.flare.data.network.xqt.XQTService import dev.dimension.flare.model.MicroBlogKey -import dev.dimension.flare.ui.model.UiUserV2 +import dev.dimension.flare.ui.model.UiProfile import dev.dimension.flare.ui.model.mapper.render import io.ktor.http.encodeURLQueryComponent @@ -16,10 +16,10 @@ internal class SearchUserPagingSource( private val service: XQTService, private val accountKey: MicroBlogKey, private val query: String, -) : BasePagingSource() { - override fun getRefreshKey(state: PagingState): String? = null +) : BasePagingSource() { + override fun getRefreshKey(state: PagingState): String? = null - override suspend fun doLoad(params: LoadParams): LoadResult { + override suspend fun doLoad(params: LoadParams): LoadResult { val response = service .getSearchTimeline( diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/xqt/TrendsUserPagingSource.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/xqt/TrendsUserPagingSource.kt index 4f7821dd4..3c88d0764 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/xqt/TrendsUserPagingSource.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/xqt/TrendsUserPagingSource.kt @@ -5,16 +5,16 @@ import dev.dimension.flare.common.BasePagingSource import dev.dimension.flare.data.network.xqt.XQTService import dev.dimension.flare.data.network.xqt.model.User import dev.dimension.flare.model.MicroBlogKey -import dev.dimension.flare.ui.model.UiUserV2 +import dev.dimension.flare.ui.model.UiProfile import dev.dimension.flare.ui.model.mapper.render internal class TrendsUserPagingSource( private val service: XQTService, private val accountKey: MicroBlogKey, -) : BasePagingSource() { - override fun getRefreshKey(state: PagingState): Int? = null +) : BasePagingSource() { + override fun getRefreshKey(state: PagingState): Int? = null - override suspend fun doLoad(params: LoadParams): LoadResult { + override suspend fun doLoad(params: LoadParams): LoadResult { service .getUserRecommendations( limit = params.loadSize, diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/xqt/XQTDataSource.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/xqt/XQTDataSource.kt index a537259ea..8bf7b76ad 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/xqt/XQTDataSource.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/xqt/XQTDataSource.kt @@ -81,7 +81,6 @@ import dev.dimension.flare.ui.model.UiProfile import dev.dimension.flare.ui.model.UiRelation import dev.dimension.flare.ui.model.UiState import dev.dimension.flare.ui.model.UiTimeline -import dev.dimension.flare.ui.model.UiUserV2 import dev.dimension.flare.ui.model.mapper.render import dev.dimension.flare.ui.model.mapper.toUi import dev.dimension.flare.ui.model.toUi @@ -255,7 +254,7 @@ internal class XQTDataSource( override val supportedNotificationFilter: List get() = listOf(NotificationFilter.All, NotificationFilter.Mention) - override fun userByAcct(acct: String): CacheData { + override fun userByAcct(acct: String): CacheData { val (name, host) = MicroBlogKey.valueOf(acct.removePrefix("@")) return Cacheable( fetchSource = { @@ -649,7 +648,7 @@ internal class XQTDataSource( override fun searchUser( query: String, pageSize: Int, - ): Flow> = + ): Flow> = Pager( config = pagingConfig, ) { @@ -660,7 +659,7 @@ internal class XQTDataSource( ) }.flow - override fun discoverUsers(pageSize: Int): Flow> = + override fun discoverUsers(pageSize: Int): Flow> = Pager( config = pagingConfig, ) { @@ -1145,7 +1144,7 @@ internal class XQTDataSource( userKey: MicroBlogKey, scope: CoroutineScope, pageSize: Int, - ): Flow> = + ): Flow> = Pager( config = pagingConfig, ) { @@ -1160,7 +1159,7 @@ internal class XQTDataSource( userKey: MicroBlogKey, scope: CoroutineScope, pageSize: Int, - ): Flow> = + ): Flow> = Pager( config = pagingConfig, ) { diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/model/ClickContext.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/model/ClickContext.kt index 49795ed8c..807475aa9 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/model/ClickContext.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/model/ClickContext.kt @@ -1,5 +1,33 @@ package dev.dimension.flare.ui.model +import kotlinx.serialization.Serializable + public data class ClickContext( val launcher: UriLauncher, ) + +@Serializable +internal sealed interface ClickEvent { + @Serializable + data object Noop : ClickEvent + + @Serializable + data class Deeplink( + val url: String, + ) : ClickEvent +} + +internal val ClickEvent.onClicked: ClickContext.() -> Unit + get() { + when (this) { + is ClickEvent.Deeplink -> { + return { + launcher.launch(url) + } + } + + is ClickEvent.Noop -> { + return {} + } + } + } diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/model/UiDMRoom.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/model/UiDMRoom.kt index 67b733ee4..a05352d7c 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/model/UiDMRoom.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/model/UiDMRoom.kt @@ -9,7 +9,7 @@ import kotlinx.collections.immutable.ImmutableList @Immutable public data class UiDMRoom internal constructor( val key: MicroBlogKey, - val users: ImmutableList, + val users: ImmutableList, val lastMessage: UiDMItem?, val unreadCount: Long, ) { @@ -33,7 +33,7 @@ public data class UiDMRoom internal constructor( @Immutable public data class UiDMItem internal constructor( val key: MicroBlogKey, - val user: UiUserV2, + val user: UiProfile, val content: Message, val timestamp: UiDateTime, val isFromMe: Boolean, diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/model/UiList.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/model/UiList.kt index fbc146f14..3820cb244 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/model/UiList.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/model/UiList.kt @@ -19,7 +19,7 @@ public sealed class UiList { override val title: String, val description: String? = null, val avatar: String? = null, - val creator: UiUserV2? = null, + val creator: UiProfile? = null, override val readonly: Boolean = false, ) : UiList() @@ -30,7 +30,7 @@ public sealed class UiList { override val title: String, val description: String? = null, val avatar: String? = null, - val creator: UiUserV2? = null, + val creator: UiProfile? = null, val likedCount: UiNumber = UiNumber(0), val liked: Boolean = false, override val readonly: Boolean = false, diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/model/UiPodcast.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/model/UiPodcast.kt index 8d9177753..931196e4a 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/model/UiPodcast.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/model/UiPodcast.kt @@ -9,8 +9,8 @@ public data class UiPodcast( val title: String, val playbackUrl: String?, val ended: Boolean, - val creator: UiUserV2, - val hosts: ImmutableList, - val speakers: ImmutableList, - val listeners: ImmutableList, + val creator: UiProfile, + val hosts: ImmutableList, + val speakers: ImmutableList, + val listeners: ImmutableList, ) diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/model/UiProfile.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/model/UiProfile.kt index 514566068..f4f6a81bb 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/model/UiProfile.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/model/UiProfile.kt @@ -2,32 +2,33 @@ package dev.dimension.flare.ui.model import androidx.compose.runtime.Immutable import com.fleeksoft.ksoup.nodes.Element +import dev.dimension.flare.common.SerializableImmutableList +import dev.dimension.flare.common.SerializableImmutableMap import dev.dimension.flare.model.MicroBlogKey import dev.dimension.flare.model.PlatformType import dev.dimension.flare.ui.humanizer.Formatter.humanize -import dev.dimension.flare.ui.humanizer.humanize import dev.dimension.flare.ui.render.UiRichText import dev.dimension.flare.ui.render.toUi -import kotlinx.collections.immutable.ImmutableList -import kotlinx.collections.immutable.ImmutableMap import kotlinx.collections.immutable.persistentListOf +import kotlinx.serialization.Serializable +@Serializable @Immutable public data class UiProfile internal constructor( - override val key: MicroBlogKey, - override val handle: String, - override val avatar: String, + val key: MicroBlogKey, + val handle: String, + val avatar: String, private val nameInternal: UiRichText, - override val platformType: PlatformType, - override val onClicked: ClickContext.() -> Unit, + val platformType: PlatformType, + private val clickEvent: ClickEvent, public val banner: String?, public val description: UiRichText?, public val matrices: Matrices, - public val mark: ImmutableList, + public val mark: SerializableImmutableList, public val bottomContent: BottomContent?, -) : UiUserV2 { +) { // If name is blank, use handle without @ as display name - override val name: UiRichText by lazy { + val name: UiRichText by lazy { if (nameInternal.raw.isEmpty() || nameInternal.raw.isBlank()) { Element("span") .apply { @@ -38,6 +39,11 @@ public data class UiProfile internal constructor( } } + val onClicked: ClickContext.() -> Unit by lazy { + clickEvent.onClicked + } + + @Serializable @Immutable public data class Matrices internal constructor( val fansCount: Long, @@ -70,13 +76,16 @@ public data class UiProfile internal constructor( } } + @Serializable public sealed interface BottomContent { + @Serializable public data class Fields internal constructor( - val fields: ImmutableMap, + val fields: SerializableImmutableMap, ) : BottomContent + @Serializable public data class Iconify internal constructor( - val items: ImmutableMap, + val items: SerializableImmutableMap, ) : BottomContent { public enum class Icon { Location, @@ -86,6 +95,7 @@ public data class UiProfile internal constructor( } } + @Serializable public enum class Mark { Verified, Cat, @@ -101,7 +111,7 @@ public fun createSampleUser(): UiProfile = avatar = "https://example.com/avatar.jpg", nameInternal = Element("span").toUi(), platformType = PlatformType.Mastodon, - onClicked = { /* Handle click */ }, + clickEvent = ClickEvent.Noop, banner = "https://example.com/banner.jpg", description = null, matrices = diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/model/UiStatus.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/model/UiStatus.kt index bbffac9fc..0a128943e 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/model/UiStatus.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/model/UiStatus.kt @@ -89,7 +89,7 @@ private fun String.trimUrl(): String = } } -public fun createSampleStatus(user: UiUserV2): UiTimeline = +public fun createSampleStatus(user: UiProfile): UiTimeline = UiTimeline( topMessage = null, content = diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/model/UiTimeline.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/model/UiTimeline.kt index aa1782b3f..c06c1b42a 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/model/UiTimeline.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/model/UiTimeline.kt @@ -103,7 +103,7 @@ public data class UiTimeline internal constructor( val images: ImmutableList, val sensitive: Boolean, val contentWarning: UiRichText?, - val user: UiUserV2?, + val user: UiProfile?, val quote: ImmutableList, val content: UiRichText, val actions: ImmutableList, @@ -184,7 +184,7 @@ public data class UiTimeline internal constructor( @Immutable public data class User internal constructor( - val value: UiUserV2, + val value: UiProfile, val button: ImmutableList