refactor: Remove `TabData` type (#576)

`TabData` recorded the type of the timeline the user had added to a tab.
`TimelineKind` is another type that records general information about
configured timelines, with identical properties.

There's no need for both, so remove `TabData` and use `TimelineKind` in
its place.

`TimelineKind` is itself mis-named; it's not just the timeline's kind
but also holds data necessary to display that timeline (e.g., the list
ID if it's a `.UserList`, or the hashtags if it's a `.Hashtags`) so
rename to `Timeline` to better reflect its usage. Move it to a new
`core.model` module.
This commit is contained in:
Nik Clayton 2024-03-30 23:27:25 +01:00 committed by GitHub
parent 09ca6ae0a8
commit 8257ded395
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
39 changed files with 444 additions and 390 deletions

View File

@ -127,6 +127,7 @@ dependencies {
implementation(projects.core.data) implementation(projects.core.data)
implementation(projects.core.database) implementation(projects.core.database)
implementation(projects.core.designsystem) implementation(projects.core.designsystem)
implementation(projects.core.model)
implementation(projects.core.navigation) implementation(projects.core.navigation)
implementation(projects.core.network) implementation(projects.core.network)
implementation(projects.core.preferences) implementation(projects.core.preferences)

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<issues format="6" by="lint 8.3.0" type="baseline" client="gradle" dependencies="false" name="AGP (8.3.0)" variant="all" version="8.3.0"> <issues format="6" by="lint 8.3.1" type="baseline" client="gradle" dependencies="false" name="AGP (8.3.1)" variant="all" version="8.3.1">
<issue <issue
id="InvalidPackage" id="InvalidPackage"
@ -2617,7 +2617,7 @@
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location <location
file="src/main/java/app/pachli/util/ShareShortcutHelper.kt" file="src/main/java/app/pachli/util/ShareShortcutHelper.kt"
line="84" line="96"
column="5"/> column="5"/>
</issue> </issue>

View File

@ -47,7 +47,7 @@
-keepclassmembers class app.pachli.core.database.model.ConversationAccountEntity { *; } -keepclassmembers class app.pachli.core.database.model.ConversationAccountEntity { *; }
-keepclassmembers class app.pachli.core.database.model.DraftAttachment { *; } -keepclassmembers class app.pachli.core.database.model.DraftAttachment { *; }
-keep class app.pachli.core.database.model.TabDataJsonAdapter { *; } -keep class app.pachli.core.model.TimelineJsonAdapter { *; }
-keep enum app.pachli.core.database.model.DraftAttachment$Type { -keep enum app.pachli.core.database.model.DraftAttachment$Type {
public *; public *;

View File

@ -79,9 +79,9 @@ import app.pachli.core.data.repository.Lists
import app.pachli.core.data.repository.ListsRepository import app.pachli.core.data.repository.ListsRepository
import app.pachli.core.data.repository.ListsRepository.Companion.compareByListTitle import app.pachli.core.data.repository.ListsRepository.Companion.compareByListTitle
import app.pachli.core.database.model.AccountEntity import app.pachli.core.database.model.AccountEntity
import app.pachli.core.database.model.TabData
import app.pachli.core.designsystem.EmbeddedFontFamily import app.pachli.core.designsystem.EmbeddedFontFamily
import app.pachli.core.designsystem.R as DR import app.pachli.core.designsystem.R as DR
import app.pachli.core.model.Timeline
import app.pachli.core.navigation.AboutActivityIntent import app.pachli.core.navigation.AboutActivityIntent
import app.pachli.core.navigation.AccountActivityIntent import app.pachli.core.navigation.AccountActivityIntent
import app.pachli.core.navigation.AccountListActivityIntent import app.pachli.core.navigation.AccountListActivityIntent
@ -369,7 +369,7 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvider {
refreshMainDrawerItems(addSearchButton = hideTopToolbar) refreshMainDrawerItems(addSearchButton = hideTopToolbar)
// Any lists in tabs might have changed titles, update those // Any lists in tabs might have changed titles, update those
if (lists is Lists.Loaded && tabAdapter.tabs.any { it.tabData is TabData.UserList }) { if (lists is Lists.Loaded && tabAdapter.tabs.any { it.timeline is Timeline.UserList }) {
setupTabs(false) setupTabs(false)
} }
} }
@ -868,8 +868,8 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvider {
tabLayoutMediator = TabLayoutMediator(activeTabLayout, binding.viewPager, true) { tabLayoutMediator = TabLayoutMediator(activeTabLayout, binding.viewPager, true) {
tab: TabLayout.Tab, position: Int -> tab: TabLayout.Tab, position: Int ->
tab.icon = AppCompatResources.getDrawable(this@MainActivity, tabs[position].icon) tab.icon = AppCompatResources.getDrawable(this@MainActivity, tabs[position].icon)
tab.contentDescription = when (tabs[position].tabData) { tab.contentDescription = when (tabs[position].timeline) {
is TabData.UserList -> tabs[position].title(this@MainActivity) is Timeline.UserList -> tabs[position].title(this@MainActivity)
else -> getString(tabs[position].text) else -> getString(tabs[position].text)
} }
}.also { it.attach() } }.also { it.attach() }
@ -880,12 +880,12 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvider {
// - Tabs containing lists are compared by list ID, in case the list was renamed // - Tabs containing lists are compared by list ID, in case the list was renamed
// - Left-most tab // - Left-most tab
val position = if (selectNotificationTab) { val position = if (selectNotificationTab) {
tabs.indexOfFirst { it.tabData is TabData.Notifications } tabs.indexOfFirst { it.timeline is Timeline.Notifications }
} else { } else {
previousTab?.let { previousTab?.let {
tabs.indexOfFirst { tabs.indexOfFirst {
if (it.tabData is TabData.UserList && previousTab.tabData is TabData.UserList) { if (it.timeline is Timeline.UserList && previousTab.timeline is Timeline.UserList) {
it.tabData.listId == previousTab.tabData.listId it.timeline.listId == previousTab.timeline.listId
} else { } else {
it == previousTab it == previousTab
} }

View File

@ -28,6 +28,7 @@ import app.pachli.components.timeline.TimelineFragment
import app.pachli.core.activity.BottomSheetActivity import app.pachli.core.activity.BottomSheetActivity
import app.pachli.core.common.extensions.viewBinding import app.pachli.core.common.extensions.viewBinding
import app.pachli.core.common.util.unsafeLazy import app.pachli.core.common.util.unsafeLazy
import app.pachli.core.model.Timeline
import app.pachli.core.navigation.ComposeActivityIntent import app.pachli.core.navigation.ComposeActivityIntent
import app.pachli.core.navigation.ComposeActivityIntent.ComposeOptions import app.pachli.core.navigation.ComposeActivityIntent.ComposeOptions
import app.pachli.core.navigation.StatusListActivityIntent import app.pachli.core.navigation.StatusListActivityIntent
@ -36,7 +37,6 @@ import app.pachli.core.network.ServerOperation.ORG_JOINMASTODON_FILTERS_SERVER
import app.pachli.core.network.model.Filter import app.pachli.core.network.model.Filter
import app.pachli.core.network.model.FilterContext import app.pachli.core.network.model.FilterContext
import app.pachli.core.network.model.FilterV1 import app.pachli.core.network.model.FilterV1
import app.pachli.core.network.model.TimelineKind
import app.pachli.databinding.ActivityStatuslistBinding import app.pachli.databinding.ActivityStatuslistBinding
import app.pachli.interfaces.ActionButtonActivity import app.pachli.interfaces.ActionButtonActivity
import app.pachli.interfaces.AppBarLayoutHost import app.pachli.interfaces.AppBarLayoutHost
@ -66,7 +66,7 @@ class StatusListActivity : BottomSheetActivity(), AppBarLayoutHost, ActionButton
lateinit var serverRepository: ServerRepository lateinit var serverRepository: ServerRepository
private val binding: ActivityStatuslistBinding by viewBinding(ActivityStatuslistBinding::inflate) private val binding: ActivityStatuslistBinding by viewBinding(ActivityStatuslistBinding::inflate)
private lateinit var timelineKind: TimelineKind private lateinit var timeline: Timeline
override val appBarLayout: AppBarLayout override val appBarLayout: AppBarLayout
get() = binding.includedToolbar.appbar get() = binding.includedToolbar.appbar
@ -94,16 +94,16 @@ class StatusListActivity : BottomSheetActivity(), AppBarLayoutHost, ActionButton
setSupportActionBar(binding.includedToolbar.toolbar) setSupportActionBar(binding.includedToolbar.toolbar)
timelineKind = StatusListActivityIntent.getKind(intent) timeline = StatusListActivityIntent.getKind(intent)
val title = when (timelineKind) { val title = when (timeline) {
is TimelineKind.Favourites -> getString(R.string.title_favourites) is Timeline.Favourites -> getString(R.string.title_favourites)
is TimelineKind.Bookmarks -> getString(R.string.title_bookmarks) is Timeline.Bookmarks -> getString(R.string.title_bookmarks)
is TimelineKind.Tag -> { is Timeline.Hashtags -> {
hashtag = (timelineKind as TimelineKind.Tag).tags.first() hashtag = (timeline as Timeline.Hashtags).tags.first()
getString(R.string.title_tag).format(hashtag) getString(R.string.title_tag).format(hashtag)
} }
is TimelineKind.UserList -> (timelineKind as TimelineKind.UserList).title is Timeline.UserList -> (timeline as Timeline.UserList).title
else -> "Missing title!!!" else -> "Missing title!!!"
} }
@ -115,14 +115,14 @@ class StatusListActivity : BottomSheetActivity(), AppBarLayoutHost, ActionButton
if (supportFragmentManager.findFragmentById(R.id.fragmentContainer) == null) { if (supportFragmentManager.findFragmentById(R.id.fragmentContainer) == null) {
supportFragmentManager.commit { supportFragmentManager.commit {
val fragment = TimelineFragment.newInstance(timelineKind) val fragment = TimelineFragment.newInstance(timeline)
replace(R.id.fragmentContainer, fragment) replace(R.id.fragmentContainer, fragment)
} }
} }
val composeIntent = when (timelineKind) { val composeIntent = when (timeline) {
is TimelineKind.Tag -> { is Timeline.Hashtags -> {
val tag = (timelineKind as TimelineKind.Tag).tags.first() val tag = (timeline as Timeline.Hashtags).tags.first()
ComposeActivityIntent( ComposeActivityIntent(
this, this,
ComposeOptions( ComposeOptions(
@ -131,9 +131,9 @@ class StatusListActivity : BottomSheetActivity(), AppBarLayoutHost, ActionButton
), ),
) )
} }
is TimelineKind.Bookmarks, is Timeline.Bookmarks,
is TimelineKind.Favourites, is Timeline.Favourites,
is TimelineKind.UserList, is Timeline.UserList,
-> { -> {
ComposeActivityIntent(this, ComposeOptions()) ComposeActivityIntent(this, ComposeOptions())
} }
@ -150,7 +150,7 @@ class StatusListActivity : BottomSheetActivity(), AppBarLayoutHost, ActionButton
override fun onCreateOptionsMenu(menu: Menu): Boolean { override fun onCreateOptionsMenu(menu: Menu): Boolean {
val tag = hashtag val tag = hashtag
if (timelineKind is TimelineKind.Tag && tag != null) { if (timeline is Timeline.Hashtags && tag != null) {
lifecycleScope.launch { lifecycleScope.launch {
mastodonApi.tag(tag).fold( mastodonApi.tag(tag).fold(
{ tagEntity -> { tagEntity ->

View File

@ -46,8 +46,8 @@ import app.pachli.core.common.util.unsafeLazy
import app.pachli.core.data.repository.Lists import app.pachli.core.data.repository.Lists
import app.pachli.core.data.repository.ListsRepository import app.pachli.core.data.repository.ListsRepository
import app.pachli.core.data.repository.ListsRepository.Companion.compareByListTitle import app.pachli.core.data.repository.ListsRepository.Companion.compareByListTitle
import app.pachli.core.database.model.TabData
import app.pachli.core.designsystem.R as DR import app.pachli.core.designsystem.R as DR
import app.pachli.core.model.Timeline
import app.pachli.core.navigation.ListActivityIntent import app.pachli.core.navigation.ListActivityIntent
import app.pachli.core.network.model.MastoList import app.pachli.core.network.model.MastoList
import app.pachli.core.network.retrofit.MastodonApi import app.pachli.core.network.retrofit.MastodonApi
@ -119,7 +119,7 @@ class TabPreferenceActivity : BaseActivity(), ItemInteractionListener {
MaterialDividerItemDecoration(this, MaterialDividerItemDecoration.VERTICAL), MaterialDividerItemDecoration(this, MaterialDividerItemDecoration.VERTICAL),
) )
addTabAdapter = TabAdapter(listOf(TabViewData.from(TabData.Direct)), true, this) addTabAdapter = TabAdapter(listOf(TabViewData.from(Timeline.Conversations)), true, this)
binding.addTabRecyclerView.adapter = addTabAdapter binding.addTabRecyclerView.adapter = addTabAdapter
binding.addTabRecyclerView.layoutManager = LinearLayoutManager(this) binding.addTabRecyclerView.layoutManager = LinearLayoutManager(this)
@ -182,12 +182,12 @@ class TabPreferenceActivity : BaseActivity(), ItemInteractionListener {
override fun onTabAdded(tab: TabViewData) { override fun onTabAdded(tab: TabViewData) {
toggleFab(false) toggleFab(false)
if (tab.tabData is TabData.Hashtag) { if (tab.timeline is Timeline.Hashtags) {
showAddHashtagDialog() showAddHashtagDialog()
return return
} }
if (tab.tabData is TabData.UserList) { if (tab.timeline is Timeline.UserList) {
showSelectListDialog() showSelectListDialog()
return return
} }
@ -205,14 +205,14 @@ class TabPreferenceActivity : BaseActivity(), ItemInteractionListener {
saveTabs() saveTabs()
} }
override fun onActionChipClicked(tabData: TabData.Hashtag, tabPosition: Int) { override fun onActionChipClicked(timelineHashtags: Timeline.Hashtags, tabPosition: Int) {
showAddHashtagDialog(tabData, tabPosition) showAddHashtagDialog(timelineHashtags, tabPosition)
} }
override fun onChipClicked(tabData: TabData.Hashtag, tabPosition: Int, chipPosition: Int) { override fun onChipClicked(timeline: Timeline.Hashtags, tabPosition: Int, chipPosition: Int) {
currentTabs[tabPosition] = currentTabs[tabPosition].copy( currentTabs[tabPosition] = currentTabs[tabPosition].copy(
tabData = tabData.copy( timeline = timeline.copy(
tags = tabData.tags.filterIndexed { i, _ -> i != chipPosition }, tags = timeline.tags.filterIndexed { i, _ -> i != chipPosition },
), ),
) )
saveTabs() saveTabs()
@ -238,7 +238,7 @@ class TabPreferenceActivity : BaseActivity(), ItemInteractionListener {
onFabDismissedCallback.isEnabled = expand onFabDismissedCallback.isEnabled = expand
} }
private fun showAddHashtagDialog(tabData: TabData.Hashtag? = null, tabPosition: Int = 0) { private fun showAddHashtagDialog(timeline: Timeline.Hashtags? = null, tabPosition: Int = 0) {
val frameLayout = FrameLayout(this) val frameLayout = FrameLayout(this)
val padding = Utils.dpToPx(this, 8) val padding = Utils.dpToPx(this, 8)
frameLayout.updatePadding(left = padding, right = padding) frameLayout.updatePadding(left = padding, right = padding)
@ -254,13 +254,13 @@ class TabPreferenceActivity : BaseActivity(), ItemInteractionListener {
.setNegativeButton(android.R.string.cancel, null) .setNegativeButton(android.R.string.cancel, null)
.setPositiveButton(R.string.action_save) { _, _ -> .setPositiveButton(R.string.action_save) { _, _ ->
val input = editText.text.toString().trim() val input = editText.text.toString().trim()
if (tabData == null) { if (timeline == null) {
val newTab = TabViewData.from(TabData.Hashtag(listOf(input))) val newTab = TabViewData.from(Timeline.Hashtags(listOf(input)))
currentTabs.add(newTab) currentTabs.add(newTab)
currentTabsAdapter.notifyItemInserted(currentTabs.size - 1) currentTabsAdapter.notifyItemInserted(currentTabs.size - 1)
} else { } else {
currentTabs[tabPosition] = currentTabs[tabPosition].copy( currentTabs[tabPosition] = currentTabs[tabPosition].copy(
tabData = tabData.copy(tags = tabData.tags + input), timeline = timeline.copy(tags = timeline.tags + input),
) )
currentTabsAdapter.notifyItemChanged(tabPosition) currentTabsAdapter.notifyItemChanged(tabPosition)
@ -298,7 +298,7 @@ class TabPreferenceActivity : BaseActivity(), ItemInteractionListener {
.setView(selectListBinding.root) .setView(selectListBinding.root)
.setAdapter(adapter) { _, position -> .setAdapter(adapter) { _, position ->
adapter.getItem(position)?.let { item -> adapter.getItem(position)?.let { item ->
val newTab = TabViewData.from(TabData.UserList(item.id, item.title)) val newTab = TabViewData.from(Timeline.UserList(item.id, item.title))
currentTabs.add(newTab) currentTabs.add(newTab)
currentTabsAdapter.notifyItemInserted(currentTabs.size - 1) currentTabsAdapter.notifyItemInserted(currentTabs.size - 1)
updateAvailableTabs() updateAvailableTabs()
@ -349,45 +349,45 @@ class TabPreferenceActivity : BaseActivity(), ItemInteractionListener {
private fun updateAvailableTabs() { private fun updateAvailableTabs() {
val addableTabs: MutableList<TabViewData> = mutableListOf() val addableTabs: MutableList<TabViewData> = mutableListOf()
val homeTab = TabViewData.from(TabData.Home) val homeTab = TabViewData.from(Timeline.Home)
if (!currentTabs.contains(homeTab)) { if (!currentTabs.contains(homeTab)) {
addableTabs.add(homeTab) addableTabs.add(homeTab)
} }
val notificationTab = TabViewData.from(TabData.Notifications) val notificationTab = TabViewData.from(Timeline.Notifications)
if (!currentTabs.contains(notificationTab)) { if (!currentTabs.contains(notificationTab)) {
addableTabs.add(notificationTab) addableTabs.add(notificationTab)
} }
val localTab = TabViewData.from(TabData.Local) val localTab = TabViewData.from(Timeline.PublicLocal)
if (!currentTabs.contains(localTab)) { if (!currentTabs.contains(localTab)) {
addableTabs.add(localTab) addableTabs.add(localTab)
} }
val federatedTab = TabViewData.from(TabData.Federated) val federatedTab = TabViewData.from(Timeline.PublicFederated)
if (!currentTabs.contains(federatedTab)) { if (!currentTabs.contains(federatedTab)) {
addableTabs.add(federatedTab) addableTabs.add(federatedTab)
} }
val directMessagesTab = TabViewData.from(TabData.Direct) val directMessagesTab = TabViewData.from(Timeline.Conversations)
if (!currentTabs.contains(directMessagesTab)) { if (!currentTabs.contains(directMessagesTab)) {
addableTabs.add(directMessagesTab) addableTabs.add(directMessagesTab)
} }
val trendingTagsTab = TabViewData.from(TabData.TrendingTags) val trendingTagsTab = TabViewData.from(Timeline.TrendingHashtags)
if (!currentTabs.contains(trendingTagsTab)) { if (!currentTabs.contains(trendingTagsTab)) {
addableTabs.add(trendingTagsTab) addableTabs.add(trendingTagsTab)
} }
val trendingLinksTab = TabViewData.from(TabData.TrendingLinks) val trendingLinksTab = TabViewData.from(Timeline.TrendingLinks)
if (!currentTabs.contains(trendingLinksTab)) { if (!currentTabs.contains(trendingLinksTab)) {
addableTabs.add(trendingLinksTab) addableTabs.add(trendingLinksTab)
} }
val trendingStatusesTab = TabViewData.from(TabData.TrendingStatuses) val trendingStatusesTab = TabViewData.from(Timeline.TrendingStatuses)
if (!currentTabs.contains(trendingStatusesTab)) { if (!currentTabs.contains(trendingStatusesTab)) {
addableTabs.add(trendingStatusesTab) addableTabs.add(trendingStatusesTab)
} }
val bookmarksTab = TabViewData.from(TabData.Bookmarks) val bookmarksTab = TabViewData.from(Timeline.Bookmarks)
if (!currentTabs.contains(trendingTagsTab)) { if (!currentTabs.contains(trendingTagsTab)) {
addableTabs.add(bookmarksTab) addableTabs.add(bookmarksTab)
} }
addableTabs.add(TabViewData.from(TabData.Hashtag(emptyList()))) addableTabs.add(TabViewData.from(Timeline.Hashtags(emptyList())))
addableTabs.add(TabViewData.from(TabData.UserList("", ""))) addableTabs.add(TabViewData.from(Timeline.UserList("", "")))
addTabAdapter.updateData(addableTabs) addTabAdapter.updateData(addableTabs)
@ -405,7 +405,7 @@ class TabPreferenceActivity : BaseActivity(), ItemInteractionListener {
private fun saveTabs() { private fun saveTabs() {
accountManager.activeAccount?.let { accountManager.activeAccount?.let {
lifecycleScope.launch(Dispatchers.IO) { lifecycleScope.launch(Dispatchers.IO) {
it.tabPreferences = currentTabs.map { it.tabData } it.tabPreferences = currentTabs.map { it.timeline }
accountManager.saveAccount(it) accountManager.saveAccount(it)
} }
} }
@ -416,7 +416,7 @@ class TabPreferenceActivity : BaseActivity(), ItemInteractionListener {
super.onPause() super.onPause()
if (tabsChanged) { if (tabsChanged) {
lifecycleScope.launch { lifecycleScope.launch {
eventHub.dispatch(MainTabsChangedEvent(currentTabs.map { it.tabData })) eventHub.dispatch(MainTabsChangedEvent(currentTabs.map { it.timeline }))
} }
} }
} }

View File

@ -26,20 +26,20 @@ import app.pachli.components.notifications.NotificationsFragment
import app.pachli.components.timeline.TimelineFragment import app.pachli.components.timeline.TimelineFragment
import app.pachli.components.trending.TrendingLinksFragment import app.pachli.components.trending.TrendingLinksFragment
import app.pachli.components.trending.TrendingTagsFragment import app.pachli.components.trending.TrendingTagsFragment
import app.pachli.core.database.model.TabData import app.pachli.core.model.Timeline
import app.pachli.core.network.model.TimelineKind
/** /**
* Wrap a [TabData] with additional information to display a tab with that data. * Wrap a [Timeline] with additional information to display a tab with that
* timeline.
* *
* @param tabData wrapped [TabData] * @param timeline wrapped [Timeline]
* @param text text to use for this tab when displayed in lists * @param text text to use for this tab when displayed in lists
* @param icon icon to use when displaying the tab * @param icon icon to use when displaying the tab
* @param fragment [Fragment] to display the tab's contents * @param fragment [Fragment] to display the tab's contents
* @param title title to display in the action bar if this tab is active * @param title title to display in the action bar if this tab is active
*/ */
data class TabViewData( data class TabViewData(
val tabData: TabData, val timeline: Timeline,
@StringRes val text: Int, @StringRes val text: Int,
@DrawableRes val icon: Int, @DrawableRes val icon: Int,
val fragment: () -> Fragment, val fragment: () -> Fragment,
@ -51,70 +51,70 @@ data class TabViewData(
other as TabViewData other as TabViewData
if (tabData != other.tabData) return false if (timeline != other.timeline) return false
return true return true
} }
override fun hashCode() = tabData.hashCode() override fun hashCode() = timeline.hashCode()
companion object { companion object {
fun from(tabData: TabData) = when (tabData) { fun from(timeline: Timeline) = when (timeline) {
TabData.Home -> TabViewData( Timeline.Home -> TabViewData(
tabData = tabData, timeline = timeline,
text = R.string.title_home, text = R.string.title_home,
icon = R.drawable.ic_home_24dp, icon = R.drawable.ic_home_24dp,
fragment = { TimelineFragment.newInstance(TimelineKind.Home) }, fragment = { TimelineFragment.newInstance(timeline) },
) )
TabData.Notifications -> TabViewData( Timeline.Notifications -> TabViewData(
tabData = tabData, timeline = timeline,
text = R.string.title_notifications, text = R.string.title_notifications,
icon = R.drawable.ic_notifications_24dp, icon = R.drawable.ic_notifications_24dp,
fragment = { NotificationsFragment.newInstance() }, fragment = { NotificationsFragment.newInstance() },
) )
TabData.Local -> TabViewData( Timeline.PublicLocal -> TabViewData(
tabData = tabData, timeline = timeline,
text = R.string.title_public_local, text = R.string.title_public_local,
icon = R.drawable.ic_local_24dp, icon = R.drawable.ic_local_24dp,
fragment = { TimelineFragment.newInstance(TimelineKind.PublicLocal) }, fragment = { TimelineFragment.newInstance(timeline) },
) )
TabData.Federated -> TabViewData( Timeline.PublicFederated -> TabViewData(
tabData = tabData, timeline = timeline,
text = R.string.title_public_federated, text = R.string.title_public_federated,
icon = R.drawable.ic_public_24dp, icon = R.drawable.ic_public_24dp,
fragment = { TimelineFragment.newInstance(TimelineKind.PublicFederated) }, fragment = { TimelineFragment.newInstance(timeline) },
) )
TabData.Direct -> TabViewData( Timeline.Conversations -> TabViewData(
tabData = tabData, timeline = timeline,
text = R.string.title_direct_messages, text = R.string.title_direct_messages,
icon = R.drawable.ic_reblog_direct_24dp, icon = R.drawable.ic_reblog_direct_24dp,
fragment = { ConversationsFragment.newInstance() }, fragment = { ConversationsFragment.newInstance() },
) )
TabData.TrendingTags -> TabViewData( Timeline.TrendingHashtags -> TabViewData(
tabData = tabData, timeline = timeline,
text = R.string.title_public_trending_hashtags, text = R.string.title_public_trending_hashtags,
icon = R.drawable.ic_trending_up_24px, icon = R.drawable.ic_trending_up_24px,
fragment = { TrendingTagsFragment.newInstance() }, fragment = { TrendingTagsFragment.newInstance() },
) )
TabData.TrendingLinks -> TabViewData( Timeline.TrendingLinks -> TabViewData(
tabData = tabData, timeline = timeline,
text = R.string.title_public_trending_links, text = R.string.title_public_trending_links,
icon = R.drawable.ic_trending_up_24px, icon = R.drawable.ic_trending_up_24px,
fragment = { TrendingLinksFragment.newInstance() }, fragment = { TrendingLinksFragment.newInstance() },
) )
TabData.TrendingStatuses -> TabViewData( Timeline.TrendingStatuses -> TabViewData(
tabData = tabData, timeline = timeline,
text = R.string.title_public_trending_statuses, text = R.string.title_public_trending_statuses,
icon = R.drawable.ic_trending_up_24px, icon = R.drawable.ic_trending_up_24px,
fragment = { TimelineFragment.newInstance(TimelineKind.TrendingStatuses) }, fragment = { TimelineFragment.newInstance(timeline) },
) )
is TabData.Hashtag -> TabViewData( is Timeline.Hashtags -> TabViewData(
tabData = tabData, timeline = timeline,
text = R.string.hashtags, text = R.string.hashtags,
icon = R.drawable.ic_hashtag, icon = R.drawable.ic_hashtag,
fragment = { TimelineFragment.newInstance(TimelineKind.Tag(tabData.tags)) }, fragment = { TimelineFragment.newInstance(timeline) },
title = { context -> title = { context ->
tabData.tags.joinToString(separator = " ") { timeline.tags.joinToString(separator = " ") {
context.getString( context.getString(
R.string.title_tag, R.string.title_tag,
it, it,
@ -122,24 +122,23 @@ data class TabViewData(
} }
}, },
) )
is TabData.UserList -> TabViewData( is Timeline.UserList -> TabViewData(
tabData = tabData, timeline = timeline,
text = R.string.list, text = R.string.list,
icon = app.pachli.core.ui.R.drawable.ic_list, icon = app.pachli.core.ui.R.drawable.ic_list,
fragment = { fragment = { TimelineFragment.newInstance(timeline) },
TimelineFragment.newInstance( title = { timeline.title },
TimelineKind.UserList(tabData.listId, tabData.title),
)
},
title = { tabData.title },
) )
TabData.Bookmarks -> TabViewData( Timeline.Bookmarks -> TabViewData(
tabData = tabData, timeline = timeline,
text = R.string.title_bookmarks, text = R.string.title_bookmarks,
icon = R.drawable.ic_bookmark_active_24dp, icon = R.drawable.ic_bookmark_active_24dp,
fragment = { TimelineFragment.newInstance(TimelineKind.Bookmarks) }, fragment = { TimelineFragment.newInstance(timeline) },
) )
else -> throw IllegalArgumentException("unknown tab type: $tabData") Timeline.Favourites -> throw IllegalArgumentException("can't add to tab: $timeline")
is Timeline.User.Pinned -> throw IllegalArgumentException("can't add to tab: $timeline")
is Timeline.User.Posts -> throw IllegalArgumentException("can't add to tab: $timeline")
is Timeline.User.Replies -> throw IllegalArgumentException("can't add to tab: $timeline")
} }
} }
} }

View File

@ -26,8 +26,8 @@ import app.pachli.R
import app.pachli.TabViewData import app.pachli.TabViewData
import app.pachli.core.common.extensions.hide import app.pachli.core.common.extensions.hide
import app.pachli.core.common.extensions.show import app.pachli.core.common.extensions.show
import app.pachli.core.database.model.TabData
import app.pachli.core.designsystem.R as DR import app.pachli.core.designsystem.R as DR
import app.pachli.core.model.Timeline
import app.pachli.core.ui.BindingHolder import app.pachli.core.ui.BindingHolder
import app.pachli.databinding.ItemTabPreferenceBinding import app.pachli.databinding.ItemTabPreferenceBinding
import app.pachli.databinding.ItemTabPreferenceSmallBinding import app.pachli.databinding.ItemTabPreferenceSmallBinding
@ -39,8 +39,8 @@ interface ItemInteractionListener {
fun onTabRemoved(position: Int) fun onTabRemoved(position: Int)
fun onStartDelete(viewHolder: RecyclerView.ViewHolder) fun onStartDelete(viewHolder: RecyclerView.ViewHolder)
fun onStartDrag(viewHolder: RecyclerView.ViewHolder) fun onStartDrag(viewHolder: RecyclerView.ViewHolder)
fun onActionChipClicked(tabData: TabData.Hashtag, tabPosition: Int) fun onActionChipClicked(timelineHashtags: Timeline.Hashtags, tabPosition: Int)
fun onChipClicked(tabData: TabData.Hashtag, tabPosition: Int, chipPosition: Int) fun onChipClicked(timelineHashtags: Timeline.Hashtags, tabPosition: Int, chipPosition: Int)
} }
class TabAdapter( class TabAdapter(
@ -81,8 +81,8 @@ class TabAdapter(
} else { } else {
val binding = holder.binding as ItemTabPreferenceBinding val binding = holder.binding as ItemTabPreferenceBinding
if (tab.tabData is TabData.UserList) { if (tab.timeline is Timeline.UserList) {
binding.textView.text = tab.tabData.title binding.textView.text = tab.timeline.title
} else { } else {
binding.textView.setText(tab.text) binding.textView.setText(tab.text)
} }
@ -107,7 +107,7 @@ class TabAdapter(
(if (removeButtonEnabled) android.R.attr.textColorTertiary else DR.attr.textColorDisabled), (if (removeButtonEnabled) android.R.attr.textColorTertiary else DR.attr.textColorDisabled),
) )
if (tab.tabData is TabData.Hashtag) { if (tab.timeline is Timeline.Hashtags) {
binding.chipGroup.show() binding.chipGroup.show()
/* /*
@ -115,7 +115,7 @@ class TabAdapter(
* The other dynamic chips are inserted in front of the actionChip. * The other dynamic chips are inserted in front of the actionChip.
* This code tries to reuse already added chips to reduce the number of Views created. * This code tries to reuse already added chips to reduce the number of Views created.
*/ */
tab.tabData.tags.forEachIndexed { i, arg -> tab.timeline.tags.forEachIndexed { i, arg ->
val chip = binding.chipGroup.getChildAt(i).takeUnless { it.id == R.id.actionChip } as Chip? val chip = binding.chipGroup.getChildAt(i).takeUnless { it.id == R.id.actionChip } as Chip?
?: Chip(context).apply { ?: Chip(context).apply {
@ -126,23 +126,23 @@ class TabAdapter(
chip.text = arg chip.text = arg
if (tab.tabData.tags.size <= 1) { if (tab.timeline.tags.size <= 1) {
chip.isCloseIconVisible = false chip.isCloseIconVisible = false
chip.setOnClickListener(null) chip.setOnClickListener(null)
} else { } else {
chip.isCloseIconVisible = true chip.isCloseIconVisible = true
chip.setOnClickListener { chip.setOnClickListener {
listener.onChipClicked(tab.tabData, holder.bindingAdapterPosition, i) listener.onChipClicked(tab.timeline, holder.bindingAdapterPosition, i)
} }
} }
} }
while (binding.chipGroup.size - 1 > tab.tabData.tags.size) { while (binding.chipGroup.size - 1 > tab.timeline.tags.size) {
binding.chipGroup.removeViewAt(tab.tabData.tags.size) binding.chipGroup.removeViewAt(tab.timeline.tags.size)
} }
binding.actionChip.setOnClickListener { binding.actionChip.setOnClickListener {
listener.onActionChipClicked(tab.tabData, holder.bindingAdapterPosition) listener.onActionChipClicked(tab.timeline, holder.bindingAdapterPosition)
} }
} else { } else {
binding.chipGroup.hide() binding.chipGroup.hide()

View File

@ -1,6 +1,6 @@
package app.pachli.appstore package app.pachli.appstore
import app.pachli.core.database.model.TabData import app.pachli.core.model.Timeline
import app.pachli.core.network.model.Account import app.pachli.core.network.model.Account
import app.pachli.core.network.model.FilterContext import app.pachli.core.network.model.FilterContext
import app.pachli.core.network.model.Poll import app.pachli.core.network.model.Poll
@ -22,7 +22,7 @@ data object StatusScheduledEvent : Event
data class StatusEditedEvent(val originalId: String, val status: Status) : Event data class StatusEditedEvent(val originalId: String, val status: Status) : Event
data class ProfileEditedEvent(val newProfileData: Account) : Event data class ProfileEditedEvent(val newProfileData: Account) : Event
data class FilterChangedEvent(val filterContext: FilterContext) : Event data class FilterChangedEvent(val filterContext: FilterContext) : Event
data class MainTabsChangedEvent(val newTabs: List<TabData>) : Event data class MainTabsChangedEvent(val newTabs: List<Timeline>) : Event
data class PollVoteEvent(val statusId: String, val poll: Poll) : Event data class PollVoteEvent(val statusId: String, val poll: Poll) : Event
data class DomainMuteEvent(val instance: String) : Event data class DomainMuteEvent(val instance: String) : Event
data class AnnouncementReadEvent(val announcementId: String) : Event data class AnnouncementReadEvent(val announcementId: String) : Event

View File

@ -22,7 +22,7 @@ import app.pachli.components.account.media.AccountMediaFragment
import app.pachli.components.timeline.TimelineFragment import app.pachli.components.timeline.TimelineFragment
import app.pachli.core.activity.CustomFragmentStateAdapter import app.pachli.core.activity.CustomFragmentStateAdapter
import app.pachli.core.activity.RefreshableFragment import app.pachli.core.activity.RefreshableFragment
import app.pachli.core.network.model.TimelineKind import app.pachli.core.model.Timeline
class AccountPagerAdapter( class AccountPagerAdapter(
activity: FragmentActivity, activity: FragmentActivity,
@ -33,9 +33,9 @@ class AccountPagerAdapter(
override fun createFragment(position: Int): Fragment { override fun createFragment(position: Int): Fragment {
return when (position) { return when (position) {
0 -> TimelineFragment.newInstance(TimelineKind.User.Posts(accountId), false) 0 -> TimelineFragment.newInstance(Timeline.User.Posts(accountId), false)
1 -> TimelineFragment.newInstance(TimelineKind.User.Replies(accountId), false) 1 -> TimelineFragment.newInstance(Timeline.User.Replies(accountId), false)
2 -> TimelineFragment.newInstance(TimelineKind.User.Pinned(accountId), false) 2 -> TimelineFragment.newInstance(Timeline.User.Pinned(accountId), false)
3 -> AccountMediaFragment.newInstance(accountId) 3 -> AccountMediaFragment.newInstance(accountId)
else -> throw AssertionError("Page $position is out of AccountPagerAdapter bounds") else -> throw AssertionError("Page $position is out of AccountPagerAdapter bounds")
} }

View File

@ -33,7 +33,7 @@ import app.pachli.core.database.model.StatusViewDataEntity
import app.pachli.core.database.model.TimelineStatusWithAccount import app.pachli.core.database.model.TimelineStatusWithAccount
import app.pachli.core.database.model.TranslatedStatusEntity import app.pachli.core.database.model.TranslatedStatusEntity
import app.pachli.core.database.model.TranslationState import app.pachli.core.database.model.TranslationState
import app.pachli.core.network.model.TimelineKind import app.pachli.core.model.Timeline
import app.pachli.core.network.model.Translation import app.pachli.core.network.model.Translation
import app.pachli.core.network.retrofit.MastodonApi import app.pachli.core.network.retrofit.MastodonApi
import app.pachli.util.EmptyPagingSource import app.pachli.util.EmptyPagingSource
@ -76,7 +76,7 @@ class CachedTimelineRepository @Inject constructor(
/** @return flow of Mastodon [TimelineStatusWithAccount], loaded in [pageSize] increments */ /** @return flow of Mastodon [TimelineStatusWithAccount], loaded in [pageSize] increments */
@OptIn(ExperimentalPagingApi::class, ExperimentalCoroutinesApi::class) @OptIn(ExperimentalPagingApi::class, ExperimentalCoroutinesApi::class)
fun getStatusStream( fun getStatusStream(
kind: TimelineKind, kind: Timeline,
pageSize: Int = PAGE_SIZE, pageSize: Int = PAGE_SIZE,
initialKey: String? = null, initialKey: String? = null,
): Flow<PagingData<TimelineStatusWithAccount>> { ): Flow<PagingData<TimelineStatusWithAccount>> {

View File

@ -27,8 +27,8 @@ import app.pachli.components.timeline.viewmodel.NetworkTimelinePagingSource
import app.pachli.components.timeline.viewmodel.NetworkTimelineRemoteMediator import app.pachli.components.timeline.viewmodel.NetworkTimelineRemoteMediator
import app.pachli.components.timeline.viewmodel.PageCache import app.pachli.components.timeline.viewmodel.PageCache
import app.pachli.core.accounts.AccountManager import app.pachli.core.accounts.AccountManager
import app.pachli.core.model.Timeline
import app.pachli.core.network.model.Status import app.pachli.core.network.model.Status
import app.pachli.core.network.model.TimelineKind
import app.pachli.core.network.retrofit.MastodonApi import app.pachli.core.network.retrofit.MastodonApi
import app.pachli.util.getDomain import app.pachli.util.getDomain
import javax.inject.Inject import javax.inject.Inject
@ -81,7 +81,7 @@ class NetworkTimelineRepository @Inject constructor(
@OptIn(ExperimentalPagingApi::class) @OptIn(ExperimentalPagingApi::class)
fun getStatusStream( fun getStatusStream(
viewModelScope: CoroutineScope, viewModelScope: CoroutineScope,
kind: TimelineKind, kind: Timeline,
pageSize: Int = PAGE_SIZE, pageSize: Int = PAGE_SIZE,
initialKey: String? = null, initialKey: String? = null,
): Flow<PagingData<Status>> { ): Flow<PagingData<Status>> {

View File

@ -56,11 +56,11 @@ import app.pachli.core.common.extensions.show
import app.pachli.core.common.extensions.viewBinding import app.pachli.core.common.extensions.viewBinding
import app.pachli.core.common.extensions.visible import app.pachli.core.common.extensions.visible
import app.pachli.core.database.model.TranslationState import app.pachli.core.database.model.TranslationState
import app.pachli.core.model.Timeline
import app.pachli.core.navigation.AccountListActivityIntent import app.pachli.core.navigation.AccountListActivityIntent
import app.pachli.core.navigation.AttachmentViewData import app.pachli.core.navigation.AttachmentViewData
import app.pachli.core.network.model.Poll import app.pachli.core.network.model.Poll
import app.pachli.core.network.model.Status import app.pachli.core.network.model.Status
import app.pachli.core.network.model.TimelineKind
import app.pachli.core.ui.BackgroundMessage import app.pachli.core.ui.BackgroundMessage
import app.pachli.core.ui.extensions.getErrorString import app.pachli.core.ui.extensions.getErrorString
import app.pachli.databinding.FragmentTimelineBinding import app.pachli.databinding.FragmentTimelineBinding
@ -112,11 +112,11 @@ class TimelineFragment :
// If the navigation library was being used this would happen automatically, so this // If the navigation library was being used this would happen automatically, so this
// workaround can be removed when that change happens. // workaround can be removed when that change happens.
private val viewModel: TimelineViewModel by lazy { private val viewModel: TimelineViewModel by lazy {
if (timelineKind == TimelineKind.Home) { if (timeline == Timeline.Home) {
viewModels<CachedTimelineViewModel>( viewModels<CachedTimelineViewModel>(
extrasProducer = { extrasProducer = {
MutableCreationExtras(defaultViewModelCreationExtras).apply { MutableCreationExtras(defaultViewModelCreationExtras).apply {
set(DEFAULT_ARGS_KEY, TimelineViewModel.creationExtras(timelineKind)) set(DEFAULT_ARGS_KEY, TimelineViewModel.creationExtras(timeline))
} }
}, },
).value ).value
@ -124,7 +124,7 @@ class TimelineFragment :
viewModels<NetworkTimelineViewModel>( viewModels<NetworkTimelineViewModel>(
extrasProducer = { extrasProducer = {
MutableCreationExtras(defaultViewModelCreationExtras).apply { MutableCreationExtras(defaultViewModelCreationExtras).apply {
set(DEFAULT_ARGS_KEY, TimelineViewModel.creationExtras(timelineKind)) set(DEFAULT_ARGS_KEY, TimelineViewModel.creationExtras(timeline))
} }
}, },
).value ).value
@ -133,7 +133,7 @@ class TimelineFragment :
private val binding by viewBinding(FragmentTimelineBinding::bind) private val binding by viewBinding(FragmentTimelineBinding::bind)
private lateinit var timelineKind: TimelineKind private lateinit var timeline: Timeline
private lateinit var adapter: TimelinePagingAdapter private lateinit var adapter: TimelinePagingAdapter
@ -154,7 +154,7 @@ class TimelineFragment :
val arguments = requireArguments() val arguments = requireArguments()
timelineKind = arguments.getParcelable(KIND_ARG)!! timeline = arguments.getParcelable(KIND_ARG)!!
isSwipeToRefreshEnabled = arguments.getBoolean(ARG_ENABLE_SWIPE_TO_REFRESH, true) isSwipeToRefreshEnabled = arguments.getBoolean(ARG_ENABLE_SWIPE_TO_REFRESH, true)
@ -466,7 +466,7 @@ class TimelineFragment :
PresentationState.PRESENTED -> { PresentationState.PRESENTED -> {
if (adapter.itemCount == 0) { if (adapter.itemCount == 0) {
binding.statusView.setup(BackgroundMessage.Empty()) binding.statusView.setup(BackgroundMessage.Empty())
if (timelineKind == TimelineKind.Home) { if (timeline == Timeline.Home) {
binding.statusView.showHelp(R.string.help_empty_home) binding.statusView.showHelp(R.string.help_empty_home)
} }
binding.statusView.show() binding.statusView.show()
@ -642,7 +642,7 @@ class TimelineFragment :
} }
// Can only translate the home timeline at the moment // Can only translate the home timeline at the moment
override fun canTranslate() = timelineKind == TimelineKind.Home override fun canTranslate() = timeline == Timeline.Home
override fun onTranslate(statusViewData: StatusViewData) { override fun onTranslate(statusViewData: StatusViewData) {
viewModel.accept(StatusAction.Translate(statusViewData)) viewModel.accept(StatusAction.Translate(statusViewData))
@ -672,10 +672,10 @@ class TimelineFragment :
} }
override fun onViewTag(tag: String) { override fun onViewTag(tag: String) {
val timelineKind = viewModel.timelineKind val timelineKind = viewModel.timeline
// If already viewing a tag page, then ignore any request to view that tag again. // If already viewing a tag page, then ignore any request to view that tag again.
if (timelineKind is TimelineKind.Tag && timelineKind.tags.contains(tag)) { if ((timelineKind as? Timeline.Hashtags)?.tags?.contains(tag) == true) {
return return
} }
@ -683,10 +683,10 @@ class TimelineFragment :
} }
override fun onViewAccount(id: String) { override fun onViewAccount(id: String) {
val timelineKind = viewModel.timelineKind val timelineKind = viewModel.timeline
// Ignore request to view the account page we're currently viewing // Ignore request to view the account page we're currently viewing
if (timelineKind is TimelineKind.User && timelineKind.id == id) { if (timelineKind is Timeline.User && timelineKind.id == id) {
return return
} }
@ -703,21 +703,25 @@ class TimelineFragment :
* that have been made to it. * that have been made to it.
*/ */
private fun handleStatusSentOrEdit(status: Status) { private fun handleStatusSentOrEdit(status: Status) {
when (timelineKind) { when (timeline) {
is TimelineKind.User.Pinned -> return is Timeline.User.Pinned -> return
is TimelineKind.Home, is Timeline.Home,
is TimelineKind.PublicFederated, is Timeline.PublicFederated,
is TimelineKind.PublicLocal, is Timeline.PublicLocal,
-> adapter.refresh() -> adapter.refresh()
is TimelineKind.User -> if (status.account.id == (timelineKind as TimelineKind.User).id) { is Timeline.User -> if (status.account.id == (timeline as Timeline.User).id) {
adapter.refresh() adapter.refresh()
} }
is TimelineKind.Bookmarks, is Timeline.Bookmarks,
is TimelineKind.Favourites, is Timeline.Favourites,
is TimelineKind.Tag, is Timeline.Hashtags,
is TimelineKind.TrendingStatuses, is Timeline.TrendingStatuses,
is TimelineKind.UserList, is Timeline.UserList,
is Timeline.Conversations,
Timeline.Notifications,
Timeline.TrendingHashtags,
Timeline.TrendingLinks,
-> return -> return
} }
} }
@ -727,10 +731,10 @@ class TimelineFragment :
} }
private fun actionButtonPresent(): Boolean { private fun actionButtonPresent(): Boolean {
return viewModel.timelineKind !is TimelineKind.Tag && return viewModel.timeline !is Timeline.Hashtags &&
viewModel.timelineKind !is TimelineKind.Favourites && viewModel.timeline !is Timeline.Favourites &&
viewModel.timelineKind !is TimelineKind.Bookmarks && viewModel.timeline !is Timeline.Bookmarks &&
viewModel.timelineKind !is TimelineKind.TrendingStatuses && viewModel.timeline !is Timeline.TrendingStatuses &&
activity is ActionButtonActivity activity is ActionButtonActivity
} }
@ -771,12 +775,12 @@ class TimelineFragment :
private const val ARG_ENABLE_SWIPE_TO_REFRESH = "enableSwipeToRefresh" private const val ARG_ENABLE_SWIPE_TO_REFRESH = "enableSwipeToRefresh"
fun newInstance( fun newInstance(
timelineKind: TimelineKind, timeline: Timeline,
enableSwipeToRefresh: Boolean = true, enableSwipeToRefresh: Boolean = true,
): TimelineFragment { ): TimelineFragment {
val fragment = TimelineFragment() val fragment = TimelineFragment()
val arguments = Bundle(2) val arguments = Bundle(2)
arguments.putParcelable(KIND_ARG, timelineKind) arguments.putParcelable(KIND_ARG, timeline)
arguments.putBoolean(ARG_ENABLE_SWIPE_TO_REFRESH, enableSwipeToRefresh) arguments.putBoolean(ARG_ENABLE_SWIPE_TO_REFRESH, enableSwipeToRefresh)
fragment.arguments = arguments fragment.arguments = arguments
return fragment return fragment

View File

@ -82,12 +82,12 @@ class CachedTimelineViewModel @Inject constructor(
}.cachedIn(viewModelScope) }.cachedIn(viewModelScope)
} }
/** @return Flow of statuses that make up the timeline of [timelineKind] */ /** @return Flow of statuses that make up the timeline of [timeline] */
private fun getStatuses( private fun getStatuses(
initialKey: String? = null, initialKey: String? = null,
): Flow<PagingData<StatusViewData>> { ): Flow<PagingData<StatusViewData>> {
Timber.d("getStatuses: kind: %s, initialKey: %s", timelineKind, initialKey) Timber.d("getStatuses: kind: %s, initialKey: %s", timeline, initialKey)
return repository.getStatusStream(kind = timelineKind, initialKey = initialKey) return repository.getStatusStream(kind = timeline, initialKey = initialKey)
.map { pagingData -> .map { pagingData ->
pagingData pagingData
.map { .map {

View File

@ -24,8 +24,8 @@ import androidx.paging.PagingState
import androidx.paging.RemoteMediator import androidx.paging.RemoteMediator
import app.pachli.BuildConfig import app.pachli.BuildConfig
import app.pachli.core.accounts.AccountManager import app.pachli.core.accounts.AccountManager
import app.pachli.core.model.Timeline
import app.pachli.core.network.model.Status import app.pachli.core.network.model.Status
import app.pachli.core.network.model.TimelineKind
import app.pachli.core.network.retrofit.MastodonApi import app.pachli.core.network.retrofit.MastodonApi
import java.io.IOException import java.io.IOException
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
@ -41,7 +41,7 @@ class NetworkTimelineRemoteMediator(
accountManager: AccountManager, accountManager: AccountManager,
private val factory: InvalidatingPagingSourceFactory<String, Status>, private val factory: InvalidatingPagingSourceFactory<String, Status>,
private val pageCache: PageCache, private val pageCache: PageCache,
private val timelineKind: TimelineKind, private val timeline: Timeline,
) : RemoteMediator<String, Status>() { ) : RemoteMediator<String, Status>() {
private val activeAccount = accountManager.activeAccount!! private val activeAccount = accountManager.activeAccount!!
@ -116,7 +116,7 @@ class NetworkTimelineRemoteMediator(
Timber.d( Timber.d(
" Page %s complete for %s, now got %d pages", " Page %s complete for %s, now got %d pages",
loadType, loadType,
timelineKind, timeline,
pageCache.size, pageCache.size,
) )
pageCache.debug() pageCache.debug()
@ -132,7 +132,7 @@ class NetworkTimelineRemoteMediator(
} }
} }
@Throws(IOException::class, HttpException::class) @Throws(IOException::class, HttpException::class, IllegalStateException::class)
private suspend fun fetchStatusPageByKind(loadType: LoadType, key: String?, loadSize: Int): Response<List<Status>> { private suspend fun fetchStatusPageByKind(loadType: LoadType, key: String?, loadSize: Int): Response<List<Status>> {
val (maxId, minId) = when (loadType) { val (maxId, minId) = when (loadType) {
// When refreshing fetch a page of statuses that are immediately *newer* than the key // When refreshing fetch a page of statuses that are immediately *newer* than the key
@ -144,20 +144,20 @@ class NetworkTimelineRemoteMediator(
LoadType.PREPEND -> Pair(null, key) LoadType.PREPEND -> Pair(null, key)
} }
return when (timelineKind) { return when (timeline) {
TimelineKind.Bookmarks -> api.bookmarks(maxId = maxId, minId = minId, limit = loadSize) Timeline.Bookmarks -> api.bookmarks(maxId = maxId, minId = minId, limit = loadSize)
TimelineKind.Favourites -> api.favourites(maxId = maxId, minId = minId, limit = loadSize) Timeline.Favourites -> api.favourites(maxId = maxId, minId = minId, limit = loadSize)
TimelineKind.Home -> api.homeTimeline(maxId = maxId, minId = minId, limit = loadSize) Timeline.Home -> api.homeTimeline(maxId = maxId, minId = minId, limit = loadSize)
TimelineKind.PublicFederated -> api.publicTimeline(local = false, maxId = maxId, minId = minId, limit = loadSize) Timeline.PublicFederated -> api.publicTimeline(local = false, maxId = maxId, minId = minId, limit = loadSize)
TimelineKind.PublicLocal -> api.publicTimeline(local = true, maxId = maxId, minId = minId, limit = loadSize) Timeline.PublicLocal -> api.publicTimeline(local = true, maxId = maxId, minId = minId, limit = loadSize)
TimelineKind.TrendingStatuses -> api.trendingStatuses() Timeline.TrendingStatuses -> api.trendingStatuses()
is TimelineKind.Tag -> { is Timeline.Hashtags -> {
val firstHashtag = timelineKind.tags.first() val firstHashtag = timeline.tags.first()
val additionalHashtags = timelineKind.tags.subList(1, timelineKind.tags.size) val additionalHashtags = timeline.tags.subList(1, timeline.tags.size)
api.hashtagTimeline(firstHashtag, additionalHashtags, null, maxId = maxId, minId = minId, limit = loadSize) api.hashtagTimeline(firstHashtag, additionalHashtags, null, maxId = maxId, minId = minId, limit = loadSize)
} }
is TimelineKind.User.Pinned -> api.accountStatuses( is Timeline.User.Pinned -> api.accountStatuses(
timelineKind.id, timeline.id,
maxId = maxId, maxId = maxId,
minId = minId, minId = minId,
limit = loadSize, limit = loadSize,
@ -165,8 +165,8 @@ class NetworkTimelineRemoteMediator(
onlyMedia = null, onlyMedia = null,
pinned = true, pinned = true,
) )
is TimelineKind.User.Posts -> api.accountStatuses( is Timeline.User.Posts -> api.accountStatuses(
timelineKind.id, timeline.id,
maxId = maxId, maxId = maxId,
minId = minId, minId = minId,
limit = loadSize, limit = loadSize,
@ -174,8 +174,8 @@ class NetworkTimelineRemoteMediator(
onlyMedia = null, onlyMedia = null,
pinned = null, pinned = null,
) )
is TimelineKind.User.Replies -> api.accountStatuses( is Timeline.User.Replies -> api.accountStatuses(
timelineKind.id, timeline.id,
maxId = maxId, maxId = maxId,
minId = minId, minId = minId,
limit = loadSize, limit = loadSize,
@ -183,12 +183,13 @@ class NetworkTimelineRemoteMediator(
onlyMedia = null, onlyMedia = null,
pinned = null, pinned = null,
) )
is TimelineKind.UserList -> api.listTimeline( is Timeline.UserList -> api.listTimeline(
timelineKind.id, timeline.listId,
maxId = maxId, maxId = maxId,
minId = minId, minId = minId,
limit = loadSize, limit = loadSize,
) )
else -> throw IllegalStateException("NetworkTimelineRemoteMediator does not support $timeline")
} }
} }
} }

View File

@ -80,12 +80,12 @@ class NetworkTimelineViewModel @Inject constructor(
}.cachedIn(viewModelScope) }.cachedIn(viewModelScope)
} }
/** @return Flow of statuses that make up the timeline of [timelineKind] */ /** @return Flow of statuses that make up the timeline of [timeline] */
private fun getStatuses( private fun getStatuses(
initialKey: String? = null, initialKey: String? = null,
): Flow<PagingData<StatusViewData>> { ): Flow<PagingData<StatusViewData>> {
Timber.d("getStatuses: kind: %s, initialKey: %s", timelineKind, initialKey) Timber.d("getStatuses: kind: %s, initialKey: %s", timeline, initialKey)
return repository.getStatusStream(viewModelScope, kind = timelineKind, initialKey = initialKey) return repository.getStatusStream(viewModelScope, kind = timeline, initialKey = initialKey)
.map { pagingData -> .map { pagingData ->
pagingData.map { pagingData.map {
modifiedViewData[it.id] ?: StatusViewData.from( modifiedViewData[it.id] ?: StatusViewData.from(

View File

@ -44,11 +44,11 @@ import app.pachli.appstore.UnfollowEvent
import app.pachli.components.timeline.FilterKind import app.pachli.components.timeline.FilterKind
import app.pachli.components.timeline.FiltersRepository import app.pachli.components.timeline.FiltersRepository
import app.pachli.core.accounts.AccountManager import app.pachli.core.accounts.AccountManager
import app.pachli.core.model.Timeline
import app.pachli.core.network.model.Filter import app.pachli.core.network.model.Filter
import app.pachli.core.network.model.FilterContext import app.pachli.core.network.model.FilterContext
import app.pachli.core.network.model.Poll import app.pachli.core.network.model.Poll
import app.pachli.core.network.model.Status import app.pachli.core.network.model.Status
import app.pachli.core.network.model.TimelineKind
import app.pachli.core.preferences.PrefKeys import app.pachli.core.preferences.PrefKeys
import app.pachli.core.preferences.SharedPreferencesRepository import app.pachli.core.preferences.SharedPreferencesRepository
import app.pachli.network.FilterModel import app.pachli.network.FilterModel
@ -303,7 +303,7 @@ abstract class TimelineViewModel(
viewModelScope.launch { uiAction.emit(action) } viewModelScope.launch { uiAction.emit(action) }
} }
val timelineKind: TimelineKind = savedStateHandle.get<TimelineKind>(TIMELINE_KIND_TAG)!! val timeline: Timeline = savedStateHandle.get<Timeline>(TIMELINE_TAG)!!
private var filterRemoveReplies = false private var filterRemoveReplies = false
private var filterRemoveReblogs = false private var filterRemoveReblogs = false
@ -396,7 +396,7 @@ abstract class TimelineViewModel(
initialValue = UiState(showFabWhileScrolling = !sharedPreferencesRepository.getBoolean(PrefKeys.FAB_HIDE, false)), initialValue = UiState(showFabWhileScrolling = !sharedPreferencesRepository.getBoolean(PrefKeys.FAB_HIDE, false)),
) )
if (timelineKind is TimelineKind.Home) { if (timeline is Timeline.Home) {
// Note the variable is "true if filter" but the underlying preference/settings text is "true if show" // Note the variable is "true if filter" but the underlying preference/settings text is "true if show"
filterRemoveReplies = filterRemoveReplies =
!sharedPreferencesRepository.getBoolean(PrefKeys.TAB_FILTER_HOME_REPLIES, true) !sharedPreferencesRepository.getBoolean(PrefKeys.TAB_FILTER_HOME_REPLIES, true)
@ -407,7 +407,7 @@ abstract class TimelineViewModel(
} }
// Save the visible status ID (if it's the home timeline) // Save the visible status ID (if it's the home timeline)
if (timelineKind == TimelineKind.Home) { if (timeline == Timeline.Home) {
viewModelScope.launch { viewModelScope.launch {
uiAction uiAction
.filterIsInstance<InfallibleUiAction.SaveVisibleId>() .filterIsInstance<InfallibleUiAction.SaveVisibleId>()
@ -426,7 +426,7 @@ abstract class TimelineViewModel(
uiAction uiAction
.filterIsInstance<InfallibleUiAction.LoadNewest>() .filterIsInstance<InfallibleUiAction.LoadNewest>()
.collectLatest { .collectLatest {
if (timelineKind == TimelineKind.Home) { if (timeline == Timeline.Home) {
activeAccount.lastVisibleHomeTimelineStatusId = null activeAccount.lastVisibleHomeTimelineStatusId = null
accountManager.saveAccount(activeAccount) accountManager.saveAccount(activeAccount)
} }
@ -449,7 +449,7 @@ abstract class TimelineViewModel(
} }
fun getInitialKey(): String? { fun getInitialKey(): String? {
if (timelineKind != TimelineKind.Home) { if (timeline != Timeline.Home) {
return null return null
} }
@ -521,7 +521,7 @@ abstract class TimelineViewModel(
/** Updates the current set of filters if filter-related preferences change */ /** Updates the current set of filters if filter-related preferences change */
private fun updateFiltersFromPreferences() = eventHub.events private fun updateFiltersFromPreferences() = eventHub.events
.filterIsInstance<FilterChangedEvent>() .filterIsInstance<FilterChangedEvent>()
.filter { filterContextMatchesKind(timelineKind, listOf(it.filterContext)) } .filter { filterContextMatchesKind(timeline, listOf(it.filterContext)) }
.map { .map {
getFilters() getFilters()
Timber.d("Reload because FilterChangedEvent") Timber.d("Reload because FilterChangedEvent")
@ -534,10 +534,11 @@ abstract class TimelineViewModel(
viewModelScope.launch { viewModelScope.launch {
Timber.d("getFilters()") Timber.d("getFilters()")
try { try {
val filterContext = FilterContext.from(timelineKind) FilterContext.from(timeline)?.let { filterContext ->
filterModel = when (val filters = filtersRepository.getFilters()) { filterModel = when (val filters = filtersRepository.getFilters()) {
is FilterKind.V1 -> FilterModel(filterContext, filters.filters) is FilterKind.V1 -> FilterModel(filterContext, filters.filters)
is FilterKind.V2 -> FilterModel(filterContext) is FilterKind.V2 -> FilterModel(filterContext)
}
} }
} catch (throwable: Throwable) { } catch (throwable: Throwable) {
Timber.d(throwable, "updateFilter(): Error fetching filters") Timber.d(throwable, "updateFilter(): Error fetching filters")
@ -552,7 +553,7 @@ abstract class TimelineViewModel(
PrefKeys.TAB_FILTER_HOME_REPLIES -> { PrefKeys.TAB_FILTER_HOME_REPLIES -> {
val filter = sharedPreferencesRepository.getBoolean(PrefKeys.TAB_FILTER_HOME_REPLIES, true) val filter = sharedPreferencesRepository.getBoolean(PrefKeys.TAB_FILTER_HOME_REPLIES, true)
val oldRemoveReplies = filterRemoveReplies val oldRemoveReplies = filterRemoveReplies
filterRemoveReplies = timelineKind is TimelineKind.Home && !filter filterRemoveReplies = timeline is Timeline.Home && !filter
if (oldRemoveReplies != filterRemoveReplies) { if (oldRemoveReplies != filterRemoveReplies) {
Timber.d("Reload because TAB_FILTER_HOME_REPLIES changed") Timber.d("Reload because TAB_FILTER_HOME_REPLIES changed")
reloadKeepingReadingPosition() reloadKeepingReadingPosition()
@ -561,7 +562,7 @@ abstract class TimelineViewModel(
PrefKeys.TAB_FILTER_HOME_BOOSTS -> { PrefKeys.TAB_FILTER_HOME_BOOSTS -> {
val filter = sharedPreferencesRepository.getBoolean(PrefKeys.TAB_FILTER_HOME_BOOSTS, true) val filter = sharedPreferencesRepository.getBoolean(PrefKeys.TAB_FILTER_HOME_BOOSTS, true)
val oldRemoveReblogs = filterRemoveReblogs val oldRemoveReblogs = filterRemoveReblogs
filterRemoveReblogs = timelineKind is TimelineKind.Home && !filter filterRemoveReblogs = timeline is Timeline.Home && !filter
if (oldRemoveReblogs != filterRemoveReblogs) { if (oldRemoveReblogs != filterRemoveReblogs) {
Timber.d("Reload because TAB_FILTER_HOME_BOOSTS changed") Timber.d("Reload because TAB_FILTER_HOME_BOOSTS changed")
reloadKeepingReadingPosition() reloadKeepingReadingPosition()
@ -570,7 +571,7 @@ abstract class TimelineViewModel(
PrefKeys.TAB_SHOW_HOME_SELF_BOOSTS -> { PrefKeys.TAB_SHOW_HOME_SELF_BOOSTS -> {
val filter = sharedPreferencesRepository.getBoolean(PrefKeys.TAB_SHOW_HOME_SELF_BOOSTS, true) val filter = sharedPreferencesRepository.getBoolean(PrefKeys.TAB_SHOW_HOME_SELF_BOOSTS, true)
val oldRemoveSelfReblogs = filterRemoveSelfReblogs val oldRemoveSelfReblogs = filterRemoveSelfReblogs
filterRemoveSelfReblogs = timelineKind is TimelineKind.Home && !filter filterRemoveSelfReblogs = timeline is Timeline.Home && !filter
if (oldRemoveSelfReblogs != filterRemoveSelfReblogs) { if (oldRemoveSelfReblogs != filterRemoveSelfReblogs) {
Timber.d("Reload because TAB_SHOW_SOME_SELF_BOOSTS changed") Timber.d("Reload because TAB_SHOW_SOME_SELF_BOOSTS changed")
reloadKeepingReadingPosition() reloadKeepingReadingPosition()
@ -590,31 +591,31 @@ abstract class TimelineViewModel(
reloadKeepingReadingPosition() reloadKeepingReadingPosition()
} }
is UnfollowEvent -> { is UnfollowEvent -> {
if (timelineKind is TimelineKind.Home) { if (timeline is Timeline.Home) {
val id = event.accountId val id = event.accountId
removeAllByAccountId(id) removeAllByAccountId(id)
} }
} }
is BlockEvent -> { is BlockEvent -> {
if (timelineKind !is TimelineKind.User) { if (timeline !is Timeline.User) {
val id = event.accountId val id = event.accountId
removeAllByAccountId(id) removeAllByAccountId(id)
} }
} }
is MuteEvent -> { is MuteEvent -> {
if (timelineKind !is TimelineKind.User) { if (timeline !is Timeline.User) {
val id = event.accountId val id = event.accountId
removeAllByAccountId(id) removeAllByAccountId(id)
} }
} }
is DomainMuteEvent -> { is DomainMuteEvent -> {
if (timelineKind !is TimelineKind.User) { if (timeline !is Timeline.User) {
val instance = event.instance val instance = event.instance
removeAllByInstance(instance) removeAllByInstance(instance)
} }
} }
is StatusDeletedEvent -> { is StatusDeletedEvent -> {
if (timelineKind !is TimelineKind.User) { if (timeline !is Timeline.User) {
removeStatusWithId(event.statusId) removeStatusWithId(event.statusId)
} }
} }
@ -626,18 +627,18 @@ abstract class TimelineViewModel(
/** Tag for the timelineKind in `savedStateHandle` */ /** Tag for the timelineKind in `savedStateHandle` */
@VisibleForTesting(VisibleForTesting.PRIVATE) @VisibleForTesting(VisibleForTesting.PRIVATE)
const val TIMELINE_KIND_TAG = "timelineKind" const val TIMELINE_TAG = "timeline"
/** Create extras for this view model */ /** Create extras for this view model */
fun creationExtras(timelineKind: TimelineKind) = bundleOf( fun creationExtras(timeline: Timeline) = bundleOf(
TIMELINE_KIND_TAG to timelineKind, TIMELINE_TAG to timeline,
) )
fun filterContextMatchesKind( fun filterContextMatchesKind(
timelineKind: TimelineKind, timeline: Timeline,
filterContext: List<FilterContext>, filterContext: List<FilterContext>,
): Boolean { ): Boolean {
return filterContext.contains(FilterContext.from(timelineKind)) return filterContext.contains(FilterContext.from(timeline))
} }
} }
} }

View File

@ -30,7 +30,7 @@ import app.pachli.R
import app.pachli.components.timeline.TimelineFragment import app.pachli.components.timeline.TimelineFragment
import app.pachli.core.activity.BottomSheetActivity import app.pachli.core.activity.BottomSheetActivity
import app.pachli.core.common.extensions.viewBinding import app.pachli.core.common.extensions.viewBinding
import app.pachli.core.network.model.TimelineKind import app.pachli.core.model.Timeline
import app.pachli.core.ui.extensions.reduceSwipeSensitivity import app.pachli.core.ui.extensions.reduceSwipeSensitivity
import app.pachli.databinding.ActivityTrendingBinding import app.pachli.databinding.ActivityTrendingBinding
import app.pachli.interfaces.AppBarLayoutHost import app.pachli.interfaces.AppBarLayoutHost
@ -93,7 +93,7 @@ class TrendingFragmentAdapter(val activity: FragmentActivity) : FragmentStateAda
return when (position) { return when (position) {
0 -> TrendingTagsFragment.newInstance() 0 -> TrendingTagsFragment.newInstance()
1 -> TrendingLinksFragment.newInstance() 1 -> TrendingLinksFragment.newInstance()
2 -> TimelineFragment.newInstance(TimelineKind.TrendingStatuses) 2 -> TimelineFragment.newInstance(Timeline.TrendingStatuses)
else -> throw IllegalStateException() else -> throw IllegalStateException()
} }
} }

View File

@ -31,8 +31,8 @@ import app.pachli.components.notifications.createNotificationChannelsForAccount
import app.pachli.components.notifications.makeNotification import app.pachli.components.notifications.makeNotification
import app.pachli.core.accounts.AccountManager import app.pachli.core.accounts.AccountManager
import app.pachli.core.database.model.AccountEntity import app.pachli.core.database.model.AccountEntity
import app.pachli.core.database.model.TabData
import app.pachli.core.database.model.defaultTabs import app.pachli.core.database.model.defaultTabs
import app.pachli.core.model.Timeline
import app.pachli.core.navigation.AccountListActivityIntent import app.pachli.core.navigation.AccountListActivityIntent
import app.pachli.core.network.model.Account import app.pachli.core.network.model.Account
import app.pachli.core.network.model.Notification import app.pachli.core.network.model.Notification
@ -154,7 +154,7 @@ class MainActivityTest {
rule.launch(intent) rule.launch(intent)
rule.getScenario().onActivity { rule.getScenario().onActivity {
val currentTab = it.findViewById<ViewPager2>(R.id.viewPager).currentItem val currentTab = it.findViewById<ViewPager2>(R.id.viewPager).currentItem
val notificationTab = defaultTabs().indexOfFirst { it is TabData.Notifications } val notificationTab = defaultTabs().indexOfFirst { it is Timeline.Notifications }
assertEquals(currentTab, notificationTab) assertEquals(currentTab, notificationTab)
} }
} }

View File

@ -24,8 +24,8 @@ import app.pachli.appstore.EventHub
import app.pachli.components.timeline.viewmodel.CachedTimelineViewModel import app.pachli.components.timeline.viewmodel.CachedTimelineViewModel
import app.pachli.components.timeline.viewmodel.TimelineViewModel import app.pachli.components.timeline.viewmodel.TimelineViewModel
import app.pachli.core.accounts.AccountManager import app.pachli.core.accounts.AccountManager
import app.pachli.core.model.Timeline
import app.pachli.core.network.model.Account import app.pachli.core.network.model.Account
import app.pachli.core.network.model.TimelineKind
import app.pachli.core.network.model.nodeinfo.UnvalidatedJrd import app.pachli.core.network.model.nodeinfo.UnvalidatedJrd
import app.pachli.core.network.model.nodeinfo.UnvalidatedNodeInfo import app.pachli.core.network.model.nodeinfo.UnvalidatedNodeInfo
import app.pachli.core.network.retrofit.MastodonApi import app.pachli.core.network.retrofit.MastodonApi
@ -155,7 +155,7 @@ abstract class CachedTimelineViewModelTestBase {
timelineCases = mock() timelineCases = mock()
viewModel = CachedTimelineViewModel( viewModel = CachedTimelineViewModel(
SavedStateHandle(mapOf(TimelineViewModel.TIMELINE_KIND_TAG to TimelineKind.Home)), SavedStateHandle(mapOf(TimelineViewModel.TIMELINE_TAG to Timeline.Home)),
cachedTimelineRepository, cachedTimelineRepository,
timelineCases, timelineCases,
eventHub, eventHub,

View File

@ -18,7 +18,7 @@
package app.pachli.components.timeline package app.pachli.components.timeline
import app.pachli.components.timeline.viewmodel.InfallibleUiAction import app.pachli.components.timeline.viewmodel.InfallibleUiAction
import app.pachli.core.network.model.TimelineKind import app.pachli.core.model.Timeline
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import dagger.hilt.android.testing.HiltAndroidTest import dagger.hilt.android.testing.HiltAndroidTest
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
@ -32,7 +32,7 @@ class CachedTimelineViewModelTestVisibleId : CachedTimelineViewModelTestBase() {
// Given // Given
assertThat(accountManager.activeAccount?.lastVisibleHomeTimelineStatusId) assertThat(accountManager.activeAccount?.lastVisibleHomeTimelineStatusId)
.isNull() .isNull()
assertThat(viewModel.timelineKind).isEqualTo(TimelineKind.Home) assertThat(viewModel.timeline).isEqualTo(Timeline.Home)
// When // When
viewModel.accept(InfallibleUiAction.SaveVisibleId("1234")) viewModel.accept(InfallibleUiAction.SaveVisibleId("1234"))

View File

@ -30,8 +30,8 @@ import app.pachli.components.timeline.viewmodel.Page
import app.pachli.components.timeline.viewmodel.PageCache import app.pachli.components.timeline.viewmodel.PageCache
import app.pachli.core.accounts.AccountManager import app.pachli.core.accounts.AccountManager
import app.pachli.core.database.model.AccountEntity import app.pachli.core.database.model.AccountEntity
import app.pachli.core.model.Timeline
import app.pachli.core.network.model.Status import app.pachli.core.network.model.Status
import app.pachli.core.network.model.TimelineKind
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import okhttp3.Headers import okhttp3.Headers
@ -79,7 +79,7 @@ class NetworkTimelineRemoteMediatorTest {
accountManager = accountManager, accountManager = accountManager,
factory = pagingSourceFactory, factory = pagingSourceFactory,
pageCache = PageCache(), pageCache = PageCache(),
timelineKind = TimelineKind.Home, timeline = Timeline.Home,
) )
// When // When
@ -101,7 +101,7 @@ class NetworkTimelineRemoteMediatorTest {
accountManager, accountManager,
factory = pagingSourceFactory, factory = pagingSourceFactory,
pageCache = PageCache(), pageCache = PageCache(),
timelineKind = TimelineKind.Home, timeline = Timeline.Home,
) )
// When // When
@ -131,7 +131,7 @@ class NetworkTimelineRemoteMediatorTest {
accountManager = accountManager, accountManager = accountManager,
factory = pagingSourceFactory, factory = pagingSourceFactory,
pageCache = pages, pageCache = pages,
timelineKind = TimelineKind.Home, timeline = Timeline.Home,
) )
val state = state( val state = state(
@ -194,7 +194,7 @@ class NetworkTimelineRemoteMediatorTest {
accountManager = accountManager, accountManager = accountManager,
factory = pagingSourceFactory, factory = pagingSourceFactory,
pageCache = pages, pageCache = pages,
timelineKind = TimelineKind.Home, timeline = Timeline.Home,
) )
val state = state( val state = state(
@ -264,7 +264,7 @@ class NetworkTimelineRemoteMediatorTest {
accountManager = accountManager, accountManager = accountManager,
factory = pagingSourceFactory, factory = pagingSourceFactory,
pageCache = pages, pageCache = pages,
timelineKind = TimelineKind.Home, timeline = Timeline.Home,
) )
val state = state( val state = state(

View File

@ -23,8 +23,8 @@ import app.pachli.appstore.EventHub
import app.pachli.components.timeline.viewmodel.NetworkTimelineViewModel import app.pachli.components.timeline.viewmodel.NetworkTimelineViewModel
import app.pachli.components.timeline.viewmodel.TimelineViewModel import app.pachli.components.timeline.viewmodel.TimelineViewModel
import app.pachli.core.accounts.AccountManager import app.pachli.core.accounts.AccountManager
import app.pachli.core.model.Timeline
import app.pachli.core.network.model.Account import app.pachli.core.network.model.Account
import app.pachli.core.network.model.TimelineKind
import app.pachli.core.network.model.nodeinfo.UnvalidatedJrd import app.pachli.core.network.model.nodeinfo.UnvalidatedJrd
import app.pachli.core.network.model.nodeinfo.UnvalidatedNodeInfo import app.pachli.core.network.model.nodeinfo.UnvalidatedNodeInfo
import app.pachli.core.network.retrofit.MastodonApi import app.pachli.core.network.retrofit.MastodonApi
@ -145,7 +145,7 @@ abstract class NetworkTimelineViewModelTestBase {
timelineCases = mock() timelineCases = mock()
viewModel = NetworkTimelineViewModel( viewModel = NetworkTimelineViewModel(
SavedStateHandle(mapOf(TimelineViewModel.TIMELINE_KIND_TAG to TimelineKind.Bookmarks)), SavedStateHandle(mapOf(TimelineViewModel.TIMELINE_TAG to Timeline.Bookmarks)),
networkTimelineRepository, networkTimelineRepository,
timelineCases, timelineCases,
eventHub, eventHub,

View File

@ -18,7 +18,7 @@
package app.pachli.components.timeline package app.pachli.components.timeline
import app.pachli.components.timeline.viewmodel.InfallibleUiAction import app.pachli.components.timeline.viewmodel.InfallibleUiAction
import app.pachli.core.network.model.TimelineKind import app.pachli.core.model.Timeline
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import dagger.hilt.android.testing.HiltAndroidTest import dagger.hilt.android.testing.HiltAndroidTest
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
@ -32,8 +32,8 @@ class NetworkTimelineViewModelTestVisibleId : NetworkTimelineViewModelTestBase()
// Given // Given
assertThat(accountManager.activeAccount?.lastVisibleHomeTimelineStatusId) assertThat(accountManager.activeAccount?.lastVisibleHomeTimelineStatusId)
.isNull() .isNull()
assertThat(viewModel.timelineKind) assertThat(viewModel.timeline)
.isNotEqualTo(TimelineKind.Home) .isNotEqualTo(Timeline.Home)
// When // When
viewModel.accept(InfallibleUiAction.SaveVisibleId("1234")) viewModel.accept(InfallibleUiAction.SaveVisibleId("1234"))

View File

@ -32,6 +32,7 @@ dependencies {
implementation(projects.core.accounts) implementation(projects.core.accounts)
implementation(projects.core.common) implementation(projects.core.common)
implementation(projects.core.database) implementation(projects.core.database)
implementation(projects.core.model)
implementation(projects.core.network) implementation(projects.core.network)
testImplementation(libs.bundles.mockito) testImplementation(libs.bundles.mockito)

View File

@ -24,7 +24,7 @@ import app.pachli.core.data.repository.ListsError.Delete
import app.pachli.core.data.repository.ListsError.GetListsWithAccount import app.pachli.core.data.repository.ListsError.GetListsWithAccount
import app.pachli.core.data.repository.ListsError.Retrieve import app.pachli.core.data.repository.ListsError.Retrieve
import app.pachli.core.data.repository.ListsError.Update import app.pachli.core.data.repository.ListsError.Update
import app.pachli.core.database.model.TabData import app.pachli.core.model.Timeline
import app.pachli.core.network.model.MastoList import app.pachli.core.network.model.MastoList
import app.pachli.core.network.model.TimelineAccount import app.pachli.core.network.model.TimelineAccount
import app.pachli.core.network.model.UserListRepliesPolicy import app.pachli.core.network.model.UserListRepliesPolicy
@ -89,7 +89,7 @@ class NetworkListsRepository @Inject constructor(
var changed = false var changed = false
val newTabPreferences = buildList { val newTabPreferences = buildList {
for (oldPref in oldTabPreferences) { for (oldPref in oldTabPreferences) {
if (oldPref !is TabData.UserList) { if (oldPref !is Timeline.UserList) {
add(oldPref) add(oldPref)
continue continue
} }

View File

@ -32,6 +32,7 @@ android {
dependencies { dependencies {
implementation(projects.core.common) implementation(projects.core.common)
implementation(projects.core.model)
implementation(projects.core.network) implementation(projects.core.network)
implementation(projects.core.preferences) implementation(projects.core.preferences)

View File

@ -20,7 +20,7 @@ import androidx.room.ProvidedTypeConverter
import androidx.room.TypeConverter import androidx.room.TypeConverter
import app.pachli.core.database.model.ConversationAccountEntity import app.pachli.core.database.model.ConversationAccountEntity
import app.pachli.core.database.model.DraftAttachment import app.pachli.core.database.model.DraftAttachment
import app.pachli.core.database.model.TabData import app.pachli.core.model.Timeline
import app.pachli.core.network.model.Attachment import app.pachli.core.network.model.Attachment
import app.pachli.core.network.model.Emoji import app.pachli.core.network.model.Emoji
import app.pachli.core.network.model.FilterResult import app.pachli.core.network.model.FilterResult
@ -66,13 +66,13 @@ class Converters @Inject constructor(
} }
@TypeConverter @TypeConverter
fun stringToTabData(str: String?): List<TabData>? { fun stringToTimeline(str: String?): List<Timeline>? {
str ?: return null str ?: return null
// Two possible storage formats. Newer (from Pachli 2.4.0) is polymorphic // Two possible storage formats. Newer (from Pachli 2.4.0) is polymorphic
// JSON, and the first character will be a '[' // JSON, and the first character will be a '['
if (str.startsWith('[')) { if (str.startsWith('[')) {
return moshi.adapter<List<TabData>>().fromJson(str) return moshi.adapter<List<Timeline>>().fromJson(str)
} }
// Older is string of ';' delimited tuples, one per tab. // Older is string of ';' delimited tuples, one per tab.
@ -87,27 +87,27 @@ class Converters @Inject constructor(
val arguments = data.drop(1).map { s -> URLDecoder.decode(s, "UTF-8") } val arguments = data.drop(1).map { s -> URLDecoder.decode(s, "UTF-8") }
when (kind) { when (kind) {
"Home" -> TabData.Home "Home" -> Timeline.Home
"Notifications" -> TabData.Notifications "Notifications" -> Timeline.Notifications
"Local" -> TabData.Local "Local" -> Timeline.PublicLocal
"Federated" -> TabData.Federated "Federated" -> Timeline.PublicFederated
"Direct" -> TabData.Direct "Direct" -> Timeline.Conversations
// Work around for https://github.com/pachli/pachli-android/issues/329 // Work around for https://github.com/pachli/pachli-android/issues/329
// when the Trending... kinds may have been serialised without the '_' // when the Trending... kinds may have been serialised without the '_'
"TrendingTags", "Trending_Tags" -> TabData.TrendingTags "TrendingTags", "Trending_Tags" -> Timeline.TrendingHashtags
"TrendingLinks", "Trending_Links" -> TabData.TrendingLinks "TrendingLinks", "Trending_Links" -> Timeline.TrendingLinks
"TrendingStatuses", "Trending_Statuses" -> TabData.TrendingStatuses "TrendingStatuses", "Trending_Statuses" -> Timeline.TrendingStatuses
"Hashtag" -> TabData.Hashtag(arguments) "Hashtag" -> Timeline.Hashtags(arguments)
"List" -> TabData.UserList(arguments[0], arguments[1]) "List" -> Timeline.UserList(arguments[0], arguments[1])
"Bookmarks" -> TabData.Bookmarks "Bookmarks" -> Timeline.Bookmarks
else -> throw IllegalStateException("Unrecognised tab kind: $kind") else -> throw IllegalStateException("Unrecognised tab kind: $kind")
} }
} }
} }
@TypeConverter @TypeConverter
fun tabDataToString(tabData: List<TabData>?): String? { fun timelineToString(timelines: List<Timeline>?): String? {
return moshi.adapter<List<TabData>>().toJson(tabData) return moshi.adapter<List<Timeline>>().toJson(timelines)
} }
@TypeConverter @TypeConverter

View File

@ -23,6 +23,7 @@ import androidx.room.Index
import androidx.room.PrimaryKey import androidx.room.PrimaryKey
import androidx.room.TypeConverters import androidx.room.TypeConverters
import app.pachli.core.database.Converters import app.pachli.core.database.Converters
import app.pachli.core.model.Timeline
import app.pachli.core.network.model.Emoji import app.pachli.core.network.model.Emoji
import app.pachli.core.network.model.Status import app.pachli.core.network.model.Status
@ -92,7 +93,7 @@ data class AccountEntity(
@ColumnInfo(defaultValue = "0") @ColumnInfo(defaultValue = "0")
var notificationMarkerId: String = "0", var notificationMarkerId: String = "0",
var emojis: List<Emoji> = emptyList(), var emojis: List<Emoji> = emptyList(),
var tabPreferences: List<TabData> = defaultTabs(), var tabPreferences: List<Timeline> = defaultTabs(),
var notificationsFilter: String = "[\"follow_request\"]", var notificationsFilter: String = "[\"follow_request\"]",
// Scope cannot be changed without re-login, so store it in case // Scope cannot be changed without re-login, so store it in case
// the scope needs to be changed in the future // the scope needs to be changed in the future
@ -147,3 +148,10 @@ data class AccountEntity(
return result return result
} }
} }
fun defaultTabs() = listOf(
Timeline.Home,
Timeline.Notifications,
Timeline.PublicLocal,
Timeline.Conversations,
)

View File

@ -1,70 +0,0 @@
/*
* Copyright 2023 Pachli Association
*
* This file is a part of Pachli.
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* Pachli is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Pachli; if not,
* see <http://www.gnu.org/licenses>.
*/
package app.pachli.core.database.model
import com.squareup.moshi.JsonClass
import dev.zacsweers.moshix.sealed.annotations.TypeLabel
@JsonClass(generateAdapter = true, generator = "sealed:kind")
sealed interface TabData {
@TypeLabel("home")
data object Home : TabData
@TypeLabel("notifications")
data object Notifications : TabData
@TypeLabel("local")
data object Local : TabData
@TypeLabel("federated")
data object Federated : TabData
@TypeLabel("direct")
data object Direct : TabData
@TypeLabel("trending_tags")
data object TrendingTags : TabData
@TypeLabel("trending_links")
data object TrendingLinks : TabData
@TypeLabel("trending_statuses")
data object TrendingStatuses : TabData
/**
* @property tags List of one or more hashtags (without the leading '#')
* to show in the tab.
*/
@TypeLabel("hashtag")
@JsonClass(generateAdapter = true)
data class Hashtag(val tags: List<String>) : TabData
@TypeLabel("list")
@JsonClass(generateAdapter = true)
data class UserList(val listId: String, val title: String) : TabData
@TypeLabel("bookmarks")
data object Bookmarks : TabData
}
fun defaultTabs() = listOf(
TabData.Home,
TabData.Notifications,
TabData.Local,
TabData.Direct,
)

View File

@ -0,0 +1,39 @@
/*
* Copyright 2024 Pachli Association
*
* This file is a part of Pachli.
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* Pachli is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Pachli; if not,
* see <http://www.gnu.org/licenses>.
*/
plugins {
alias(libs.plugins.pachli.android.library)
alias(libs.plugins.pachli.android.hilt)
alias(libs.plugins.kotlin.parcelize)
}
android {
namespace = "app.pachli.core.model"
defaultConfig {
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
}
dependencies {
implementation(libs.moshi)
implementation(libs.moshi.adapters)
ksp(libs.moshi.codegen)
implementation(libs.moshix.sealed.runtime)
ksp(libs.moshix.sealed.codegen)
}

View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<issues format="6" by="lint 8.3.1" type="baseline" client="gradle" dependencies="false" name="AGP (8.3.1)" variant="all" version="8.3.1">
</issues>

View File

@ -0,0 +1,118 @@
/*
* Copyright 2024 Pachli Association
*
* This file is a part of Pachli.
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* Pachli is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Pachli; if not,
* see <http://www.gnu.org/licenses>.
*/
package app.pachli.core.model
import android.os.Parcelable
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import dev.zacsweers.moshix.sealed.annotations.NestedSealed
import dev.zacsweers.moshix.sealed.annotations.TypeLabel
import kotlinx.parcelize.Parcelize
/** A timeline's type. Hold's data necessary to display that timeline. */
@Parcelize
@JsonClass(generateAdapter = true, generator = "sealed:kind")
sealed interface Timeline : Parcelable {
@Parcelize
@TypeLabel("home")
data object Home : Timeline
@Parcelize
@TypeLabel("notifications")
data object Notifications : Timeline
/** Federated timeline */
@Parcelize
@TypeLabel("federated")
data object PublicFederated : Timeline
/** Local timeline of the user's server */
@Parcelize
@TypeLabel("local")
data object PublicLocal : Timeline
// TODO: LOCAL_REMOTE
@Parcelize
@TypeLabel("hashtag")
@JsonClass(generateAdapter = true)
data class Hashtags(val tags: List<String>) : Timeline
@Parcelize
@TypeLabel("direct")
data object Conversations : Timeline
/** Any timeline showing statuses from a single user */
@Parcelize
@NestedSealed
sealed interface User : Timeline {
val id: String
/** Timeline showing just a user's statuses (no replies) */
@Parcelize
@TypeLabel("userPosts")
@JsonClass(generateAdapter = true)
data class Posts(override val id: String) : User
/** Timeline showing a user's pinned statuses */
@Parcelize
@TypeLabel("userPinned")
@JsonClass(generateAdapter = true)
data class Pinned(override val id: String) : User
/** Timeline showing a user's top-level statuses and replies they have made */
@Parcelize
@TypeLabel("userReplies")
@JsonClass(generateAdapter = true)
data class Replies(override val id: String) : User
}
@Parcelize
@TypeLabel("favourites")
data object Favourites : Timeline
@Parcelize
@TypeLabel("bookmarks")
data object Bookmarks : Timeline
@Parcelize
@TypeLabel("list")
@JsonClass(generateAdapter = true)
data class UserList(
@Json(name = "listId")
val listId: String,
@Json(name = "title")
val title: String,
) : Timeline
@Parcelize
@TypeLabel("trending_tags")
data object TrendingHashtags : Timeline
@Parcelize
@TypeLabel("trending_links")
data object TrendingLinks : Timeline
@Parcelize
@TypeLabel("trending_statuses")
data object TrendingStatuses : Timeline
// TODO: DRAFTS
// TODO: SCHEDULED
}

View File

@ -31,6 +31,7 @@ android {
dependencies { dependencies {
implementation(projects.core.database) // For DraftAttachment, used in ComposeOptions implementation(projects.core.database) // For DraftAttachment, used in ComposeOptions
implementation(projects.core.model)
implementation(projects.core.network) // For Attachment, used in AttachmentViewData implementation(projects.core.network) // For Attachment, used in AttachmentViewData
implementation(libs.androidx.core.ktx) // IntentCompat implementation(libs.androidx.core.ktx) // IntentCompat

View File

@ -22,6 +22,7 @@ import android.content.Intent
import android.os.Parcelable import android.os.Parcelable
import androidx.core.content.IntentCompat import androidx.core.content.IntentCompat
import app.pachli.core.database.model.DraftAttachment import app.pachli.core.database.model.DraftAttachment
import app.pachli.core.model.Timeline
import app.pachli.core.navigation.LoginActivityIntent.LoginMode import app.pachli.core.navigation.LoginActivityIntent.LoginMode
import app.pachli.core.navigation.StatusListActivityIntent.Companion.bookmarks import app.pachli.core.navigation.StatusListActivityIntent.Companion.bookmarks
import app.pachli.core.navigation.StatusListActivityIntent.Companion.favourites import app.pachli.core.navigation.StatusListActivityIntent.Companion.favourites
@ -32,7 +33,6 @@ import app.pachli.core.network.model.Filter
import app.pachli.core.network.model.NewPoll import app.pachli.core.network.model.NewPoll
import app.pachli.core.network.model.Notification import app.pachli.core.network.model.Notification
import app.pachli.core.network.model.Status import app.pachli.core.network.model.Status
import app.pachli.core.network.model.TimelineKind
import com.gaelmarhic.quadrant.QuadrantConstants import com.gaelmarhic.quadrant.QuadrantConstants
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
@ -402,7 +402,7 @@ class StatusListActivityIntent private constructor(context: Context) : Intent()
* @param context * @param context
*/ */
fun bookmarks(context: Context) = StatusListActivityIntent(context).apply { fun bookmarks(context: Context) = StatusListActivityIntent(context).apply {
putExtra(EXTRA_KIND, TimelineKind.Bookmarks) putExtra(EXTRA_KIND, Timeline.Bookmarks)
} }
/** /**
@ -411,7 +411,7 @@ class StatusListActivityIntent private constructor(context: Context) : Intent()
* @param context * @param context
*/ */
fun favourites(context: Context) = StatusListActivityIntent(context).apply { fun favourites(context: Context) = StatusListActivityIntent(context).apply {
putExtra(EXTRA_KIND, TimelineKind.Favourites) putExtra(EXTRA_KIND, Timeline.Favourites)
} }
/** /**
@ -421,7 +421,7 @@ class StatusListActivityIntent private constructor(context: Context) : Intent()
* @param hashtag The hashtag to show, without the leading "`#`" * @param hashtag The hashtag to show, without the leading "`#`"
*/ */
fun hashtag(context: Context, hashtag: String) = StatusListActivityIntent(context).apply { fun hashtag(context: Context, hashtag: String) = StatusListActivityIntent(context).apply {
putExtra(EXTRA_KIND, TimelineKind.Tag(listOf(hashtag))) putExtra(EXTRA_KIND, Timeline.Hashtags(listOf(hashtag)))
} }
/** /**
@ -432,11 +432,11 @@ class StatusListActivityIntent private constructor(context: Context) : Intent()
* @param title The title to display * @param title The title to display
*/ */
fun list(context: Context, listId: String, title: String) = StatusListActivityIntent(context).apply { fun list(context: Context, listId: String, title: String) = StatusListActivityIntent(context).apply {
putExtra(EXTRA_KIND, TimelineKind.UserList(listId, title)) putExtra(EXTRA_KIND, Timeline.UserList(listId, title))
} }
/** @return The [TimelineKind] to show */ /** @return The [TimelineKind] to show */
fun getKind(intent: Intent) = IntentCompat.getParcelableExtra(intent, EXTRA_KIND, TimelineKind::class.java)!! fun getKind(intent: Intent) = IntentCompat.getParcelableExtra(intent, EXTRA_KIND, Timeline::class.java)!!
} }
} }

View File

@ -31,6 +31,7 @@ android {
dependencies { dependencies {
implementation(projects.core.common) implementation(projects.core.common)
implementation(projects.core.model)
implementation(projects.core.preferences) implementation(projects.core.preferences)
implementation(libs.moshi) implementation(libs.moshi)

View File

@ -17,6 +17,7 @@
package app.pachli.core.network.model package app.pachli.core.network.model
import app.pachli.core.model.Timeline
import app.pachli.core.network.json.Default import app.pachli.core.network.json.Default
import app.pachli.core.network.json.HasDefault import app.pachli.core.network.json.HasDefault
import com.squareup.moshi.Json import com.squareup.moshi.Json
@ -51,18 +52,28 @@ enum class FilterContext {
/** Filter applies when viewing a profile */ /** Filter applies when viewing a profile */
@Json(name = "account") @Json(name = "account")
ACCOUNT, ACCOUNT,
; ;
companion object { companion object {
fun from(kind: TimelineKind): FilterContext = when (kind) { /**
is TimelineKind.Home, is TimelineKind.UserList -> HOME * @return The filter context for [timeline], or null if filters are not applied
is TimelineKind.PublicFederated, * to this timeline.
is TimelineKind.PublicLocal, */
is TimelineKind.Tag, fun from(timeline: Timeline): FilterContext? = when (timeline) {
is TimelineKind.Favourites, is Timeline.Home, is Timeline.UserList -> HOME
is Timeline.User -> ACCOUNT
Timeline.Notifications -> NOTIFICATIONS
Timeline.Bookmarks,
Timeline.Favourites,
Timeline.PublicFederated,
Timeline.PublicLocal,
is Timeline.Hashtags,
Timeline.TrendingStatuses,
Timeline.TrendingHashtags,
Timeline.TrendingLinks,
-> PUBLIC -> PUBLIC
is TimelineKind.User -> ACCOUNT Timeline.Conversations -> null
else -> PUBLIC
} }
} }
} }

View File

@ -1,67 +0,0 @@
/*
* Copyright 2023 Pachli Association
*
* This file is a part of Pachli.
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* Pachli is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Pachli; if not,
* see <http://www.gnu.org/licenses>.
*/
package app.pachli.core.network.model
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
/** A timeline's type. Hold's data necessary to display that timeline. */
@Parcelize
sealed interface TimelineKind : Parcelable {
@Parcelize
data object Home : TimelineKind
@Parcelize
data object PublicFederated : TimelineKind
@Parcelize
data object PublicLocal : TimelineKind
@Parcelize
data class Tag(val tags: List<String>) : TimelineKind
/** Any timeline showing statuses from a single user */
@Parcelize
sealed interface User : TimelineKind {
val id: String
/** Timeline showing just the user's statuses (no replies) */
@Parcelize
data class Posts(override val id: String) : User
/** Timeline showing the user's pinned statuses */
@Parcelize
data class Pinned(override val id: String) : User
/** Timeline showing the user's top-level statuses and replies they have made */
@Parcelize
data class Replies(override val id: String) : User
}
@Parcelize
data object Favourites : TimelineKind
@Parcelize
data object Bookmarks : TimelineKind
@Parcelize
data class UserList(val id: String, val title: String) : TimelineKind
@Parcelize
data object TrendingStatuses : TimelineKind
}

View File

@ -57,6 +57,7 @@ include(":core:common")
include(":core:data") include(":core:data")
include(":core:database") include(":core:database")
include(":core:designsystem") include(":core:designsystem")
include(":core:model")
include(":core:preferences") include(":core:preferences")
include(":core:navigation") include(":core:navigation")
include(":core:network") include(":core:network")