From a973f67ac8aeecb520ffb3984bbb72ae4db7cacc Mon Sep 17 00:00:00 2001 From: Nik Clayton Date: Tue, 12 Mar 2024 10:43:33 +0100 Subject: [PATCH] refactor: Store tab preferences as polymorphic JSON --- app/src/main/java/app/pachli/MainActivity.kt | 8 +- .../java/app/pachli/TabPreferenceActivity.kt | 56 +++++++------ app/src/main/java/app/pachli/TabViewData.kt | 43 ++++------ .../java/app/pachli/adapter/TabAdapter.kt | 24 +++--- .../java/app/pachli/pager/MainPagerAdapter.kt | 2 +- .../test/java/app/pachli/MainActivityTest.kt | 4 +- core/database/build.gradle.kts | 3 + .../app/pachli/core/database/Converters.kt | 43 ++++++++-- .../app/pachli/core/database/model/TabData.kt | 82 ++++++++++--------- gradle/libs.versions.toml | 3 + 10 files changed, 152 insertions(+), 116 deletions(-) diff --git a/app/src/main/java/app/pachli/MainActivity.kt b/app/src/main/java/app/pachli/MainActivity.kt index 5821e3898..e02b84b90 100644 --- a/app/src/main/java/app/pachli/MainActivity.kt +++ b/app/src/main/java/app/pachli/MainActivity.kt @@ -77,7 +77,7 @@ import app.pachli.core.common.extensions.viewBinding import app.pachli.core.data.repository.Lists import app.pachli.core.data.repository.ListsRepository import app.pachli.core.database.model.AccountEntity -import app.pachli.core.database.model.TabKind +import app.pachli.core.database.model.TabData import app.pachli.core.designsystem.EmbeddedFontFamily import app.pachli.core.designsystem.R as DR import app.pachli.core.navigation.AboutActivityIntent @@ -866,8 +866,8 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvider { tabLayoutMediator = TabLayoutMediator(activeTabLayout, binding.viewPager, true) { tab: TabLayout.Tab, position: Int -> tab.icon = AppCompatResources.getDrawable(this@MainActivity, tabs[position].icon) - tab.contentDescription = when (tabs[position].kind) { - TabKind.LIST -> tabs[position].arguments[1] + tab.contentDescription = when (tabs[position].tabData) { + is TabData.UserList -> tabs[position].title(this@MainActivity) else -> getString(tabs[position].text) } }.also { it.attach() } @@ -877,7 +877,7 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvider { // - The previously selected tab (if it hasn't been removed) // - Left-most tab val position = if (selectNotificationTab) { - tabs.indexOfFirst { it.kind == TabKind.NOTIFICATIONS } + tabs.indexOfFirst { it.tabData is TabData.Notifications } } else { previousTab?.let { tabs.indexOfFirst { it == previousTab } } }.takeIf { it != -1 } ?: 0 diff --git a/app/src/main/java/app/pachli/TabPreferenceActivity.kt b/app/src/main/java/app/pachli/TabPreferenceActivity.kt index ad73b959b..b806e7040 100644 --- a/app/src/main/java/app/pachli/TabPreferenceActivity.kt +++ b/app/src/main/java/app/pachli/TabPreferenceActivity.kt @@ -49,7 +49,6 @@ import app.pachli.core.common.extensions.visible import app.pachli.core.data.repository.Lists import app.pachli.core.data.repository.ListsRepository import app.pachli.core.database.model.TabData -import app.pachli.core.database.model.TabKind import app.pachli.core.designsystem.R as DR import app.pachli.core.navigation.ListActivityIntent import app.pachli.core.network.model.MastoList @@ -126,7 +125,7 @@ class TabPreferenceActivity : BaseActivity(), ItemInteractionListener { MaterialDividerItemDecoration(this, MaterialDividerItemDecoration.VERTICAL), ) - addTabAdapter = TabAdapter(listOf(TabViewData.from(TabData(TabKind.DIRECT))), true, this) + addTabAdapter = TabAdapter(listOf(TabViewData.from(TabData.Direct)), true, this) binding.addTabRecyclerView.adapter = addTabAdapter binding.addTabRecyclerView.layoutManager = LinearLayoutManager(this) @@ -189,12 +188,12 @@ class TabPreferenceActivity : BaseActivity(), ItemInteractionListener { override fun onTabAdded(tab: TabViewData) { toggleFab(false) - if (tab.kind == TabKind.HASHTAG) { + if (tab.tabData is TabData.Hashtag) { showAddHashtagDialog() return } - if (tab.kind == TabKind.LIST) { + if (tab.tabData is TabData.UserList) { showSelectListDialog() return } @@ -212,14 +211,16 @@ class TabPreferenceActivity : BaseActivity(), ItemInteractionListener { saveTabs() } - override fun onActionChipClicked(tab: TabViewData, tabPosition: Int) { - showAddHashtagDialog(tab, tabPosition) + override fun onActionChipClicked(tabData: TabData.Hashtag, tabPosition: Int) { + showAddHashtagDialog(tabData, tabPosition) } - override fun onChipClicked(tab: TabViewData, tabPosition: Int, chipPosition: Int) { - val newArguments = tab.arguments.filterIndexed { i, _ -> i != chipPosition } - val newTab = tab.copy(tabData = tab.tabData.copy(arguments = newArguments)) - currentTabs[tabPosition] = newTab + override fun onChipClicked(tabData: TabData.Hashtag, tabPosition: Int, chipPosition: Int) { + currentTabs[tabPosition] = currentTabs[tabPosition].copy( + tabData = tabData.copy( + tags = tabData.tags.filterIndexed { i, _ -> i != chipPosition }, + ), + ) saveTabs() currentTabsAdapter.notifyItemChanged(tabPosition) @@ -243,7 +244,7 @@ class TabPreferenceActivity : BaseActivity(), ItemInteractionListener { onFabDismissedCallback.isEnabled = expand } - private fun showAddHashtagDialog(tab: TabViewData? = null, tabPosition: Int = 0) { + private fun showAddHashtagDialog(tabData: TabData.Hashtag? = null, tabPosition: Int = 0) { val frameLayout = FrameLayout(this) val padding = Utils.dpToPx(this, 8) frameLayout.updatePadding(left = padding, right = padding) @@ -259,13 +260,14 @@ class TabPreferenceActivity : BaseActivity(), ItemInteractionListener { .setNegativeButton(android.R.string.cancel, null) .setPositiveButton(R.string.action_save) { _, _ -> val input = editText.text.toString().trim() - if (tab == null) { - val newTab = TabViewData.from(TabData(TabKind.HASHTAG, listOf(input))) + if (tabData == null) { + val newTab = TabViewData.from(TabData.Hashtag(listOf(input))) currentTabs.add(newTab) currentTabsAdapter.notifyItemInserted(currentTabs.size - 1) } else { - val newTab = tab.copy(tabData = tab.tabData.copy(arguments = tab.arguments + input)) - currentTabs[tabPosition] = newTab + currentTabs[tabPosition] = currentTabs[tabPosition].copy( + tabData = tabData.copy(tags = tabData.tags + input), + ) currentTabsAdapter.notifyItemChanged(tabPosition) } @@ -315,7 +317,7 @@ class TabPreferenceActivity : BaseActivity(), ItemInteractionListener { .setView(statusLayout) .setAdapter(adapter) { _, position -> adapter.getItem(position)?.let { item -> - val newTab = TabViewData.from(TabData(TabKind.LIST, listOf(item.id, item.title))) + val newTab = TabViewData.from(TabData.UserList(item.id, item.title)) currentTabs.add(newTab) currentTabsAdapter.notifyItemInserted(currentTabs.size - 1) updateAvailableTabs() @@ -377,45 +379,45 @@ class TabPreferenceActivity : BaseActivity(), ItemInteractionListener { private fun updateAvailableTabs() { val addableTabs: MutableList = mutableListOf() - val homeTab = TabViewData.from(TabData(TabKind.HOME)) + val homeTab = TabViewData.from(TabData.Home) if (!currentTabs.contains(homeTab)) { addableTabs.add(homeTab) } - val notificationTab = TabViewData.from(TabData(TabKind.NOTIFICATIONS)) + val notificationTab = TabViewData.from(TabData.Notifications) if (!currentTabs.contains(notificationTab)) { addableTabs.add(notificationTab) } - val localTab = TabViewData.from(TabData(TabKind.LOCAL)) + val localTab = TabViewData.from(TabData.Local) if (!currentTabs.contains(localTab)) { addableTabs.add(localTab) } - val federatedTab = TabViewData.from(TabData(TabKind.FEDERATED)) + val federatedTab = TabViewData.from(TabData.Federated) if (!currentTabs.contains(federatedTab)) { addableTabs.add(federatedTab) } - val directMessagesTab = TabViewData.from(TabData(TabKind.DIRECT)) + val directMessagesTab = TabViewData.from(TabData.Direct) if (!currentTabs.contains(directMessagesTab)) { addableTabs.add(directMessagesTab) } - val trendingTagsTab = TabViewData.from(TabData(TabKind.TRENDING_TAGS)) + val trendingTagsTab = TabViewData.from(TabData.TrendingTags) if (!currentTabs.contains(trendingTagsTab)) { addableTabs.add(trendingTagsTab) } - val trendingLinksTab = TabViewData.from(TabData(TabKind.TRENDING_LINKS)) + val trendingLinksTab = TabViewData.from(TabData.TrendingLinks) if (!currentTabs.contains(trendingLinksTab)) { addableTabs.add(trendingLinksTab) } - val trendingStatusesTab = TabViewData.from(TabData(TabKind.TRENDING_STATUSES)) + val trendingStatusesTab = TabViewData.from(TabData.TrendingStatuses) if (!currentTabs.contains(trendingStatusesTab)) { addableTabs.add(trendingStatusesTab) } - val bookmarksTab = TabViewData.from(TabData(TabKind.BOOKMARKS)) + val bookmarksTab = TabViewData.from(TabData.Bookmarks) if (!currentTabs.contains(trendingTagsTab)) { addableTabs.add(bookmarksTab) } - addableTabs.add(TabViewData.from(TabData(TabKind.HASHTAG))) - addableTabs.add(TabViewData.from(TabData(TabKind.LIST))) + addableTabs.add(TabViewData.from(TabData.Hashtag(emptyList()))) + addableTabs.add(TabViewData.from(TabData.UserList("", ""))) addTabAdapter.updateData(addableTabs) diff --git a/app/src/main/java/app/pachli/TabViewData.kt b/app/src/main/java/app/pachli/TabViewData.kt index d93c96c97..11b6c9d1e 100644 --- a/app/src/main/java/app/pachli/TabViewData.kt +++ b/app/src/main/java/app/pachli/TabViewData.kt @@ -27,7 +27,6 @@ import app.pachli.components.timeline.TimelineFragment import app.pachli.components.trending.TrendingLinksFragment import app.pachli.components.trending.TrendingTagsFragment import app.pachli.core.database.model.TabData -import app.pachli.core.database.model.TabKind import app.pachli.core.network.model.TimelineKind /** @@ -43,13 +42,9 @@ data class TabViewData( val tabData: TabData, @StringRes val text: Int, @DrawableRes val icon: Int, - val fragment: (List) -> Fragment, + val fragment: () -> Fragment, val title: (Context) -> String = { context -> context.getString(text) }, ) { - val kind get() = this.tabData.kind - - val arguments get() = this.tabData.arguments - override fun equals(other: Any?): Boolean { if (this === other) return true if (javaClass != other?.javaClass) return false @@ -64,64 +59,62 @@ data class TabViewData( override fun hashCode() = tabData.hashCode() companion object { - fun from(tabKind: TabKind) = from(TabData.from(tabKind)) - - fun from(tabData: TabData) = when (tabData.kind) { - TabKind.HOME -> TabViewData( + fun from(tabData: TabData) = when (tabData) { + TabData.Home -> TabViewData( tabData = tabData, text = R.string.title_home, icon = R.drawable.ic_home_24dp, fragment = { TimelineFragment.newInstance(TimelineKind.Home) }, ) - TabKind.NOTIFICATIONS -> TabViewData( + TabData.Notifications -> TabViewData( tabData = tabData, text = R.string.title_notifications, icon = R.drawable.ic_notifications_24dp, fragment = { NotificationsFragment.newInstance() }, ) - TabKind.LOCAL -> TabViewData( + TabData.Local -> TabViewData( tabData = tabData, text = R.string.title_public_local, icon = R.drawable.ic_local_24dp, fragment = { TimelineFragment.newInstance(TimelineKind.PublicLocal) }, ) - TabKind.FEDERATED -> TabViewData( + TabData.Federated -> TabViewData( tabData = tabData, text = R.string.title_public_federated, icon = R.drawable.ic_public_24dp, fragment = { TimelineFragment.newInstance(TimelineKind.PublicFederated) }, ) - TabKind.DIRECT -> TabViewData( + TabData.Direct -> TabViewData( tabData = tabData, text = R.string.title_direct_messages, icon = R.drawable.ic_reblog_direct_24dp, fragment = { ConversationsFragment.newInstance() }, ) - TabKind.TRENDING_TAGS -> TabViewData( + TabData.TrendingTags -> TabViewData( tabData = tabData, text = R.string.title_public_trending_hashtags, icon = R.drawable.ic_trending_up_24px, fragment = { TrendingTagsFragment.newInstance() }, ) - TabKind.TRENDING_LINKS -> TabViewData( + TabData.TrendingLinks -> TabViewData( tabData = tabData, text = R.string.title_public_trending_links, icon = R.drawable.ic_trending_up_24px, fragment = { TrendingLinksFragment.newInstance() }, ) - TabKind.TRENDING_STATUSES -> TabViewData( + TabData.TrendingStatuses -> TabViewData( tabData = tabData, text = R.string.title_public_trending_statuses, icon = R.drawable.ic_trending_up_24px, fragment = { TimelineFragment.newInstance(TimelineKind.TrendingStatuses) }, ) - TabKind.HASHTAG -> TabViewData( + is TabData.Hashtag -> TabViewData( tabData = tabData, text = R.string.hashtags, icon = R.drawable.ic_hashtag, - fragment = { args -> TimelineFragment.newInstance(TimelineKind.Tag(args)) }, + fragment = { TimelineFragment.newInstance(TimelineKind.Tag(tabData.tags)) }, title = { context -> - tabData.arguments.joinToString(separator = " ") { + tabData.tags.joinToString(separator = " ") { context.getString( R.string.title_tag, it, @@ -129,18 +122,18 @@ data class TabViewData( } }, ) - TabKind.LIST -> TabViewData( + is TabData.UserList -> TabViewData( tabData = tabData, text = R.string.list, icon = R.drawable.ic_list, - fragment = { args -> + fragment = { TimelineFragment.newInstance( - TimelineKind.UserList(args.first(), args.last()), + TimelineKind.UserList(tabData.listId, tabData.title), ) }, - title = { tabData.arguments.getOrNull(1).orEmpty() }, + title = { tabData.title }, ) - TabKind.BOOKMARKS -> TabViewData( + TabData.Bookmarks -> TabViewData( tabData = tabData, text = R.string.title_bookmarks, icon = R.drawable.ic_bookmark_active_24dp, diff --git a/app/src/main/java/app/pachli/adapter/TabAdapter.kt b/app/src/main/java/app/pachli/adapter/TabAdapter.kt index 3b6add466..6c1844ac6 100644 --- a/app/src/main/java/app/pachli/adapter/TabAdapter.kt +++ b/app/src/main/java/app/pachli/adapter/TabAdapter.kt @@ -26,7 +26,7 @@ import app.pachli.R import app.pachli.TabViewData import app.pachli.core.common.extensions.hide import app.pachli.core.common.extensions.show -import app.pachli.core.database.model.TabKind +import app.pachli.core.database.model.TabData import app.pachli.core.designsystem.R as DR import app.pachli.databinding.ItemTabPreferenceBinding import app.pachli.databinding.ItemTabPreferenceSmallBinding @@ -39,8 +39,8 @@ interface ItemInteractionListener { fun onTabRemoved(position: Int) fun onStartDelete(viewHolder: RecyclerView.ViewHolder) fun onStartDrag(viewHolder: RecyclerView.ViewHolder) - fun onActionChipClicked(tab: TabViewData, tabPosition: Int) - fun onChipClicked(tab: TabViewData, tabPosition: Int, chipPosition: Int) + fun onActionChipClicked(tabData: TabData.Hashtag, tabPosition: Int) + fun onChipClicked(tabData: TabData.Hashtag, tabPosition: Int, chipPosition: Int) } class TabAdapter( @@ -81,8 +81,8 @@ class TabAdapter( } else { val binding = holder.binding as ItemTabPreferenceBinding - if (tab.kind == TabKind.LIST) { - binding.textView.text = tab.arguments.getOrNull(1).orEmpty() + if (tab.tabData is TabData.UserList) { + binding.textView.text = tab.tabData.title } else { binding.textView.setText(tab.text) } @@ -107,7 +107,7 @@ class TabAdapter( (if (removeButtonEnabled) android.R.attr.textColorTertiary else DR.attr.textColorDisabled), ) - if (tab.kind == TabKind.HASHTAG) { + if (tab.tabData is TabData.Hashtag) { binding.chipGroup.show() /* @@ -115,7 +115,7 @@ class TabAdapter( * 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. */ - tab.arguments.forEachIndexed { i, arg -> + tab.tabData.tags.forEachIndexed { i, arg -> val chip = binding.chipGroup.getChildAt(i).takeUnless { it.id == R.id.actionChip } as Chip? ?: Chip(context).apply { @@ -126,23 +126,23 @@ class TabAdapter( chip.text = arg - if (tab.arguments.size <= 1) { + if (tab.tabData.tags.size <= 1) { chip.isCloseIconVisible = false chip.setOnClickListener(null) } else { chip.isCloseIconVisible = true chip.setOnClickListener { - listener.onChipClicked(tab, holder.bindingAdapterPosition, i) + listener.onChipClicked(tab.tabData, holder.bindingAdapterPosition, i) } } } - while (binding.chipGroup.size - 1 > tab.arguments.size) { - binding.chipGroup.removeViewAt(tab.arguments.size) + while (binding.chipGroup.size - 1 > tab.tabData.tags.size) { + binding.chipGroup.removeViewAt(tab.tabData.tags.size) } binding.actionChip.setOnClickListener { - listener.onActionChipClicked(tab, holder.bindingAdapterPosition) + listener.onActionChipClicked(tab.tabData, holder.bindingAdapterPosition) } } else { binding.chipGroup.hide() diff --git a/app/src/main/java/app/pachli/pager/MainPagerAdapter.kt b/app/src/main/java/app/pachli/pager/MainPagerAdapter.kt index 3e06fcd0f..214763245 100644 --- a/app/src/main/java/app/pachli/pager/MainPagerAdapter.kt +++ b/app/src/main/java/app/pachli/pager/MainPagerAdapter.kt @@ -25,7 +25,7 @@ class MainPagerAdapter(var tabs: List, activity: FragmentActivity) override fun createFragment(position: Int): Fragment { val tab = tabs[position] - return tab.fragment(tab.arguments) + return tab.fragment() } override fun getItemCount() = tabs.size diff --git a/app/src/test/java/app/pachli/MainActivityTest.kt b/app/src/test/java/app/pachli/MainActivityTest.kt index 01a3adee2..86662d2cf 100644 --- a/app/src/test/java/app/pachli/MainActivityTest.kt +++ b/app/src/test/java/app/pachli/MainActivityTest.kt @@ -31,7 +31,7 @@ import app.pachli.components.notifications.createNotificationChannelsForAccount import app.pachli.components.notifications.makeNotification import app.pachli.core.accounts.AccountManager import app.pachli.core.database.model.AccountEntity -import app.pachli.core.database.model.TabKind +import app.pachli.core.database.model.TabData import app.pachli.core.database.model.defaultTabs import app.pachli.core.navigation.AccountListActivityIntent import app.pachli.core.network.model.Account @@ -154,7 +154,7 @@ class MainActivityTest { rule.launch(intent) rule.getScenario().onActivity { val currentTab = it.findViewById(R.id.viewPager).currentItem - val notificationTab = defaultTabs().indexOfFirst { it.kind == TabKind.NOTIFICATIONS } + val notificationTab = defaultTabs().indexOfFirst { it is TabData.Notifications } assertEquals(currentTab, notificationTab) } } diff --git a/core/database/build.gradle.kts b/core/database/build.gradle.kts index 5a71b7114..827bd148d 100644 --- a/core/database/build.gradle.kts +++ b/core/database/build.gradle.kts @@ -39,4 +39,7 @@ dependencies { implementation(libs.moshi) implementation(libs.moshi.adapters) ksp(libs.moshi.codegen) + + implementation(libs.moshix.sealed.runtime) + ksp(libs.moshix.sealed.codegen) } diff --git a/core/database/src/main/kotlin/app/pachli/core/database/Converters.kt b/core/database/src/main/kotlin/app/pachli/core/database/Converters.kt index 1e5da5ebf..3be775275 100644 --- a/core/database/src/main/kotlin/app/pachli/core/database/Converters.kt +++ b/core/database/src/main/kotlin/app/pachli/core/database/Converters.kt @@ -33,7 +33,6 @@ import app.pachli.core.network.model.TranslatedPoll import com.squareup.moshi.Moshi import com.squareup.moshi.adapter import java.net.URLDecoder -import java.net.URLEncoder import java.time.Instant import java.util.Date import javax.inject.Inject @@ -68,17 +67,47 @@ class Converters @Inject constructor( @TypeConverter fun stringToTabData(str: String?): List? { - return str?.split(";") - ?.map { - val data = it.split(":") - TabData.from(data[0], data.drop(1).map { s -> URLDecoder.decode(s, "UTF-8") }) + str ?: return null + + // Two possible storage formats. Newer (from Pachli 2.4.0) is polymorphic + // JSON, and the first character will be a '[' + if (str.startsWith('[')) { + return moshi.adapter>().fromJson(str) + } + + // Older is string of ';' delimited tuples, one per tab. + // The per-tab data is ':' delimited tuples where the first item is the tab's kind, + // any subsequent entries are tab-specific data. + // + // The "Trending_..." / "Trending..." is to work around + // https://github.com/pachli/pachli-android/issues/329 + return str.split(";").map { + val data = it.split(":") + val kind = data[0] + val arguments = data.drop(1).map { s -> URLDecoder.decode(s, "UTF-8") } + + when (kind) { + "Home" -> TabData.Home + "Notifications" -> TabData.Notifications + "Local" -> TabData.Local + "Federated" -> TabData.Federated + "Direct" -> TabData.Direct + // Work around for https://github.com/pachli/pachli-android/issues/329 + // when the Trending... kinds may have been serialised without the '_' + "TrendingTags", "Trending_Tags" -> TabData.TrendingTags + "TrendingLinks", "Trending_Links" -> TabData.TrendingLinks + "TrendingStatuses", "Trending_Statuses" -> TabData.TrendingStatuses + "Hashtag" -> TabData.Hashtag(arguments) + "List" -> TabData.UserList(arguments[0], arguments[1]) + "Bookmarks" -> TabData.Bookmarks + else -> throw IllegalStateException("Unrecognised tab kind: $kind") } + } } @TypeConverter fun tabDataToString(tabData: List?): String? { - // List name may include ":" - return tabData?.joinToString(";") { it.kind.repr + ":" + it.arguments.joinToString(":") { s -> URLEncoder.encode(s, "UTF-8") } } + return moshi.adapter>().toJson(tabData) } @TypeConverter diff --git a/core/database/src/main/kotlin/app/pachli/core/database/model/TabData.kt b/core/database/src/main/kotlin/app/pachli/core/database/model/TabData.kt index be5bebd78..4c31d45c7 100644 --- a/core/database/src/main/kotlin/app/pachli/core/database/model/TabData.kt +++ b/core/database/src/main/kotlin/app/pachli/core/database/model/TabData.kt @@ -17,48 +17,54 @@ package app.pachli.core.database.model -/** - * A tab's kind. - * - * @param repr String representation of the tab in the database - */ -enum class TabKind(val repr: String) { - HOME("Home"), - NOTIFICATIONS("Notifications"), - LOCAL("Local"), - FEDERATED("Federated"), - DIRECT("Direct"), - TRENDING_TAGS("Trending_Tags"), - TRENDING_LINKS("Trending_Links"), - TRENDING_STATUSES("Trending_Statuses"), - HASHTAG("Hashtag"), - LIST("List"), - BOOKMARKS("Bookmarks"), -} +import com.squareup.moshi.JsonClass +import dev.zacsweers.moshix.sealed.annotations.TypeLabel -/** this would be a good case for a sealed class, but that does not work nice with Room */ +@JsonClass(generateAdapter = true, generator = "sealed:kind") +sealed interface TabData { + @TypeLabel("home") + data object Home : TabData -data class TabData(val kind: TabKind, val arguments: List = emptyList()) { - companion object { - fun from(kind: TabKind, arguments: List = emptyList()) = - TabData(kind, arguments) + @TypeLabel("notifications") + data object Notifications : TabData - fun from(kind: String, arguments: List = emptyList()): TabData { - // Work around for https://github.com/pachli/pachli-android/issues/329, - // as the Trending... kinds may have been serialised without the `_` - return when (kind) { - "TrendingTags" -> TabData(TabKind.TRENDING_TAGS, arguments) - "TrendingLinks" -> TabData(TabKind.TRENDING_LINKS, arguments) - "TrendingStatuses" -> TabData(TabKind.TRENDING_STATUSES, arguments) - else -> TabData(TabKind.valueOf(kind.uppercase()), arguments) - } - } - } + @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) : 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.from(TabKind.HOME), - TabData.from(TabKind.NOTIFICATIONS), - TabData.from(TabKind.LOCAL), - TabData.from(TabKind.DIRECT), + TabData.Home, + TabData.Notifications, + TabData.Local, + TabData.Direct, ) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index b5d5d61a3..cfa291486 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -54,6 +54,7 @@ material-typeface = "4.0.0.2-kotlin" mockito-inline = "5.2.0" mockito-kotlin = "5.2.1" moshi = "1.15.1" +moshix = "0.25.1" networkresult-calladapter = "1.0.0" okhttp = "4.12.0" quadrant = "1.9.1" @@ -177,6 +178,8 @@ mockwebserver = { module = "com.squareup.okhttp3:mockwebserver", version.ref = " moshi = { module = "com.squareup.moshi:moshi", version.ref = "moshi" } moshi-adapters = { module = "com.squareup.moshi:moshi-adapters", version.ref = "moshi" } moshi-codegen = { module = "com.squareup.moshi:moshi-kotlin-codegen", version.ref = "moshi" } +moshix-sealed-runtime = { module = "dev.zacsweers.moshix:moshi-sealed-runtime", version.ref = "moshix" } +moshix-sealed-codegen = { module = "dev.zacsweers.moshix:moshi-sealed-codegen", version.ref = "moshix" } networkresult-calladapter = { module = "at.connyduck:networkresult-calladapter", version.ref = "networkresult-calladapter" } okhttp-core = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp" } okhttp-logging-interceptor = { module = "com.squareup.okhttp3:logging-interceptor", version.ref = "okhttp" }