Merge pull request #2130 from vitorpamplona/claude/polls-feed-tabs-Cr3Ow
Add tabbed interface to Polls screen for open/closed polls
This commit is contained in:
@@ -56,6 +56,8 @@ object ScrollStateKeys {
|
||||
const val DISCOVER_CHATS = "DiscoverChatsFeed"
|
||||
|
||||
const val POLLS_SCREEN = "PollsFeed"
|
||||
const val POLLS_OPEN = "PollsOpenFeed"
|
||||
const val POLLS_CLOSED = "PollsClosedFeed"
|
||||
const val PICTURES_SCREEN = "PicturesFeed"
|
||||
const val SHORTS_SCREEN = "ShortsFeed"
|
||||
const val LONGS_SCREEN = "LongsFeed"
|
||||
@@ -68,6 +70,7 @@ object ScrollStateKeys {
|
||||
object PagerStateKeys {
|
||||
const val HOME_SCREEN = "PagerHome"
|
||||
const val DISCOVER_SCREEN = "PagerDiscover"
|
||||
const val POLLS_SCREEN = "PagerPolls"
|
||||
}
|
||||
|
||||
@Composable
|
||||
|
||||
+8
@@ -46,6 +46,8 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.NotificationS
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.OpenPollsState
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.dal.NotificationFeedFilter
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.pictures.dal.PictureFeedFilter
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.polls.dal.ClosedPollsFeedFilter
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.polls.dal.OpenPollsFeedFilter
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.polls.dal.PollsFeedFilter
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.shorts.dal.ShortsFeedFilter
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.video.dal.VideoFeedFilter
|
||||
@@ -74,6 +76,8 @@ class AccountFeedContentStates(
|
||||
val discoverPublicChats = FeedContentState(DiscoverChatFeedFilter(account), scope, LocalCache)
|
||||
|
||||
val pollsFeed = FeedContentState(PollsFeedFilter(account), scope, LocalCache)
|
||||
val openPollsFeed = FeedContentState(OpenPollsFeedFilter(account), scope, LocalCache)
|
||||
val closedPollsFeed = FeedContentState(ClosedPollsFeedFilter(account), scope, LocalCache)
|
||||
|
||||
val picturesFeed = FeedContentState(PictureFeedFilter(account), scope, LocalCache)
|
||||
val shortsFeed = FeedContentState(ShortsFeedFilter(account), scope, LocalCache)
|
||||
@@ -114,6 +118,8 @@ class AccountFeedContentStates(
|
||||
discoverPublicChats.updateFeedWith(newNotes)
|
||||
|
||||
pollsFeed.updateFeedWith(newNotes)
|
||||
openPollsFeed.updateFeedWith(newNotes)
|
||||
closedPollsFeed.updateFeedWith(newNotes)
|
||||
|
||||
picturesFeed.updateFeedWith(newNotes)
|
||||
shortsFeed.updateFeedWith(newNotes)
|
||||
@@ -148,6 +154,8 @@ class AccountFeedContentStates(
|
||||
discoverPublicChats.deleteFromFeed(newNotes)
|
||||
|
||||
pollsFeed.deleteFromFeed(newNotes)
|
||||
openPollsFeed.deleteFromFeed(newNotes)
|
||||
closedPollsFeed.deleteFromFeed(newNotes)
|
||||
|
||||
picturesFeed.deleteFromFeed(newNotes)
|
||||
shortsFeed.deleteFromFeed(newNotes)
|
||||
|
||||
+110
-16
@@ -21,24 +21,41 @@
|
||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn.polls
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.pager.HorizontalPager
|
||||
import androidx.compose.foundation.pager.PagerState
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.SecondaryTabRow
|
||||
import androidx.compose.material3.Tab
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.commons.ui.feeds.FeedContentState
|
||||
import com.vitorpamplona.amethyst.ui.feeds.PagerStateKeys
|
||||
import com.vitorpamplona.amethyst.ui.feeds.RefresheableBox
|
||||
import com.vitorpamplona.amethyst.ui.feeds.RenderFeedContentState
|
||||
import com.vitorpamplona.amethyst.ui.feeds.SaveableFeedContentState
|
||||
import com.vitorpamplona.amethyst.ui.feeds.ScrollStateKeys
|
||||
import com.vitorpamplona.amethyst.ui.feeds.WatchLifecycleAndUpdateModel
|
||||
import com.vitorpamplona.amethyst.ui.feeds.rememberForeverPagerState
|
||||
import com.vitorpamplona.amethyst.ui.layouts.DisappearingScaffold
|
||||
import com.vitorpamplona.amethyst.ui.navigation.bottombars.AppBottomBar
|
||||
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.ui.navigation.routes.Route
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.polls.datasource.PollsFilterAssemblerSubscription
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.TabRowHeight
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Composable
|
||||
fun PollsScreen(
|
||||
@@ -46,7 +63,8 @@ fun PollsScreen(
|
||||
nav: INav,
|
||||
) {
|
||||
PollsScreen(
|
||||
pollsFeedContentState = accountViewModel.feedStates.pollsFeed,
|
||||
openPollsFeedContentState = accountViewModel.feedStates.openPollsFeed,
|
||||
closedPollsFeedContentState = accountViewModel.feedStates.closedPollsFeed,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
)
|
||||
@@ -54,23 +72,85 @@ fun PollsScreen(
|
||||
|
||||
@Composable
|
||||
fun PollsScreen(
|
||||
pollsFeedContentState: FeedContentState,
|
||||
openPollsFeedContentState: FeedContentState,
|
||||
closedPollsFeedContentState: FeedContentState,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
) {
|
||||
WatchLifecycleAndUpdateModel(pollsFeedContentState)
|
||||
WatchAccountForPollsScreen(pollsFeedContentState = pollsFeedContentState, accountViewModel = accountViewModel)
|
||||
WatchLifecycleAndUpdateModel(openPollsFeedContentState)
|
||||
WatchLifecycleAndUpdateModel(closedPollsFeedContentState)
|
||||
WatchAccountForPollsScreen(openPollsFeedContentState, closedPollsFeedContentState, accountViewModel)
|
||||
PollsFilterAssemblerSubscription(accountViewModel)
|
||||
|
||||
AssemblePollsTabs(openPollsFeedContentState, closedPollsFeedContentState) { pagerState, tabItems ->
|
||||
PollsPages(pagerState, tabItems, accountViewModel, nav)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun AssemblePollsTabs(
|
||||
openPollsFeedContentState: FeedContentState,
|
||||
closedPollsFeedContentState: FeedContentState,
|
||||
inner: @Composable (PagerState, ImmutableList<PollsTabItem>) -> Unit,
|
||||
) {
|
||||
val pagerState = rememberForeverPagerState(key = PagerStateKeys.POLLS_SCREEN) { 2 }
|
||||
|
||||
val tabs by
|
||||
remember(openPollsFeedContentState, closedPollsFeedContentState) {
|
||||
mutableStateOf(
|
||||
listOf(
|
||||
PollsTabItem(
|
||||
resource = R.string.open_polls,
|
||||
feedState = openPollsFeedContentState,
|
||||
routeForLastRead = "PollsOpenFeed",
|
||||
scrollStateKey = ScrollStateKeys.POLLS_OPEN,
|
||||
),
|
||||
PollsTabItem(
|
||||
resource = R.string.closed_polls,
|
||||
feedState = closedPollsFeedContentState,
|
||||
routeForLastRead = "PollsClosedFeed",
|
||||
scrollStateKey = ScrollStateKeys.POLLS_CLOSED,
|
||||
),
|
||||
).toImmutableList(),
|
||||
)
|
||||
}
|
||||
|
||||
inner(pagerState, tabs)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun PollsPages(
|
||||
pagerState: PagerState,
|
||||
tabs: ImmutableList<PollsTabItem>,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
) {
|
||||
DisappearingScaffold(
|
||||
isInvertedLayout = false,
|
||||
topBar = {
|
||||
PollsTopBar(accountViewModel, nav)
|
||||
Column {
|
||||
PollsTopBar(accountViewModel, nav)
|
||||
SecondaryTabRow(
|
||||
containerColor = Color.Transparent,
|
||||
contentColor = MaterialTheme.colorScheme.onBackground,
|
||||
modifier = TabRowHeight,
|
||||
selectedTabIndex = pagerState.currentPage,
|
||||
) {
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
tabs.forEachIndexed { index, tab ->
|
||||
Tab(
|
||||
selected = pagerState.currentPage == index,
|
||||
text = { Text(text = stringRes(tab.resource)) },
|
||||
onClick = { coroutineScope.launch { pagerState.animateScrollToPage(index) } },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
bottomBar = {
|
||||
AppBottomBar(Route.Polls, accountViewModel) { route ->
|
||||
if (route == Route.Polls) {
|
||||
pollsFeedContentState.sendToTop()
|
||||
tabs[pagerState.currentPage].feedState.sendToTop()
|
||||
} else {
|
||||
nav.newStack(route)
|
||||
}
|
||||
@@ -80,16 +160,20 @@ fun PollsScreen(
|
||||
NewPollButton(nav)
|
||||
},
|
||||
accountViewModel = accountViewModel,
|
||||
) { paddingValues ->
|
||||
Column(Modifier.padding(paddingValues)) {
|
||||
RefresheableBox(pollsFeedContentState, true) {
|
||||
SaveableFeedContentState(pollsFeedContentState, scrollStateKey = ScrollStateKeys.POLLS_SCREEN) { listState ->
|
||||
) {
|
||||
HorizontalPager(
|
||||
contentPadding = it,
|
||||
state = pagerState,
|
||||
userScrollEnabled = true,
|
||||
) { page ->
|
||||
RefresheableBox(tabs[page].feedState, true) {
|
||||
SaveableFeedContentState(tabs[page].feedState, scrollStateKey = tabs[page].scrollStateKey) { listState ->
|
||||
RenderFeedContentState(
|
||||
feedContentState = pollsFeedContentState,
|
||||
feedContentState = tabs[page].feedState,
|
||||
accountViewModel = accountViewModel,
|
||||
listState = listState,
|
||||
nav = nav,
|
||||
routeForLastRead = "PollsFeed",
|
||||
routeForLastRead = tabs[page].routeForLastRead,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -99,7 +183,8 @@ fun PollsScreen(
|
||||
|
||||
@Composable
|
||||
fun WatchAccountForPollsScreen(
|
||||
pollsFeedContentState: FeedContentState,
|
||||
openPollsFeedContentState: FeedContentState,
|
||||
closedPollsFeedContentState: FeedContentState,
|
||||
accountViewModel: AccountViewModel,
|
||||
) {
|
||||
val listState by accountViewModel.account.livePollsFollowLists.collectAsStateWithLifecycle()
|
||||
@@ -108,6 +193,15 @@ fun WatchAccountForPollsScreen(
|
||||
.collectAsStateWithLifecycle()
|
||||
|
||||
LaunchedEffect(accountViewModel, listState, hiddenUsers) {
|
||||
pollsFeedContentState.checkKeysInvalidateDataAndSendToTop()
|
||||
openPollsFeedContentState.checkKeysInvalidateDataAndSendToTop()
|
||||
closedPollsFeedContentState.checkKeysInvalidateDataAndSendToTop()
|
||||
}
|
||||
}
|
||||
|
||||
@Immutable
|
||||
class PollsTabItem(
|
||||
val resource: Int,
|
||||
val feedState: FeedContentState,
|
||||
val routeForLastRead: String,
|
||||
val scrollStateKey: String,
|
||||
)
|
||||
|
||||
+97
@@ -0,0 +1,97 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn.polls.dal
|
||||
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.model.TopFilter
|
||||
import com.vitorpamplona.amethyst.ui.dal.AdditiveFeedFilter
|
||||
import com.vitorpamplona.amethyst.ui.dal.DefaultFeedOrder
|
||||
import com.vitorpamplona.amethyst.ui.dal.FilterByListParams
|
||||
import com.vitorpamplona.quartz.experimental.zapPolls.ZapPollEvent
|
||||
import com.vitorpamplona.quartz.nip88Polls.poll.PollEvent
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
|
||||
class ClosedPollsFeedFilter(
|
||||
val account: Account,
|
||||
) : AdditiveFeedFilter<Note>() {
|
||||
override fun feedKey(): String = account.userProfile().pubkeyHex + "-" + followList().code + "-closed"
|
||||
|
||||
override fun limit() = 200
|
||||
|
||||
fun followList(): TopFilter = account.settings.defaultPollsFollowList.value
|
||||
|
||||
fun TopFilter.isMuteList() = this is TopFilter.MuteList
|
||||
|
||||
fun TopFilter.isBlockList() = this is TopFilter.PeopleList && this.address == account.blockPeopleList.getBlockListAddress()
|
||||
|
||||
fun TopFilter.wantsToSeeNegativeStuff() = isMuteList() || isBlockList()
|
||||
|
||||
override fun showHiddenKey(): Boolean = followList().wantsToSeeNegativeStuff()
|
||||
|
||||
private fun isClosed(note: Note): Boolean {
|
||||
val noteEvent = note.event
|
||||
return when (noteEvent) {
|
||||
is PollEvent -> {
|
||||
noteEvent.hasEnded()
|
||||
}
|
||||
|
||||
is ZapPollEvent -> {
|
||||
val closedAt = noteEvent.closedAt()
|
||||
closedAt != null && closedAt < TimeUtils.now()
|
||||
}
|
||||
|
||||
else -> {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun feed(): List<Note> {
|
||||
val params = buildFilterParams(account)
|
||||
val notes =
|
||||
LocalCache.notes.filterIntoSet { _, it ->
|
||||
val noteEvent = it.event
|
||||
(noteEvent is PollEvent || noteEvent is ZapPollEvent) && params.match(noteEvent, it.relays) && isClosed(it)
|
||||
}
|
||||
return sort(notes)
|
||||
}
|
||||
|
||||
override fun applyFilter(newItems: Set<Note>): Set<Note> = innerApplyFilter(newItems)
|
||||
|
||||
fun buildFilterParams(account: Account): FilterByListParams =
|
||||
FilterByListParams.create(
|
||||
account.livePollsFollowLists.value,
|
||||
account.hiddenUsers.flow.value,
|
||||
)
|
||||
|
||||
private fun innerApplyFilter(collection: Collection<Note>): Set<Note> {
|
||||
val params = buildFilterParams(account)
|
||||
|
||||
return collection.filterTo(HashSet()) {
|
||||
val noteEvent = it.event
|
||||
(noteEvent is PollEvent || noteEvent is ZapPollEvent) && params.match(noteEvent, it.relays) && isClosed(it)
|
||||
}
|
||||
}
|
||||
|
||||
override fun sort(items: Set<Note>): List<Note> = items.sortedWith(DefaultFeedOrder)
|
||||
}
|
||||
+97
@@ -0,0 +1,97 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn.polls.dal
|
||||
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.model.TopFilter
|
||||
import com.vitorpamplona.amethyst.ui.dal.AdditiveFeedFilter
|
||||
import com.vitorpamplona.amethyst.ui.dal.DefaultFeedOrder
|
||||
import com.vitorpamplona.amethyst.ui.dal.FilterByListParams
|
||||
import com.vitorpamplona.quartz.experimental.zapPolls.ZapPollEvent
|
||||
import com.vitorpamplona.quartz.nip88Polls.poll.PollEvent
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
|
||||
class OpenPollsFeedFilter(
|
||||
val account: Account,
|
||||
) : AdditiveFeedFilter<Note>() {
|
||||
override fun feedKey(): String = account.userProfile().pubkeyHex + "-" + followList().code + "-open"
|
||||
|
||||
override fun limit() = 200
|
||||
|
||||
fun followList(): TopFilter = account.settings.defaultPollsFollowList.value
|
||||
|
||||
fun TopFilter.isMuteList() = this is TopFilter.MuteList
|
||||
|
||||
fun TopFilter.isBlockList() = this is TopFilter.PeopleList && this.address == account.blockPeopleList.getBlockListAddress()
|
||||
|
||||
fun TopFilter.wantsToSeeNegativeStuff() = isMuteList() || isBlockList()
|
||||
|
||||
override fun showHiddenKey(): Boolean = followList().wantsToSeeNegativeStuff()
|
||||
|
||||
private fun isOpen(note: Note): Boolean {
|
||||
val noteEvent = note.event
|
||||
return when (noteEvent) {
|
||||
is PollEvent -> {
|
||||
!noteEvent.hasEnded()
|
||||
}
|
||||
|
||||
is ZapPollEvent -> {
|
||||
val closedAt = noteEvent.closedAt()
|
||||
closedAt == null || closedAt >= TimeUtils.now()
|
||||
}
|
||||
|
||||
else -> {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun feed(): List<Note> {
|
||||
val params = buildFilterParams(account)
|
||||
val notes =
|
||||
LocalCache.notes.filterIntoSet { _, it ->
|
||||
val noteEvent = it.event
|
||||
(noteEvent is PollEvent || noteEvent is ZapPollEvent) && params.match(noteEvent, it.relays) && isOpen(it)
|
||||
}
|
||||
return sort(notes)
|
||||
}
|
||||
|
||||
override fun applyFilter(newItems: Set<Note>): Set<Note> = innerApplyFilter(newItems)
|
||||
|
||||
fun buildFilterParams(account: Account): FilterByListParams =
|
||||
FilterByListParams.create(
|
||||
account.livePollsFollowLists.value,
|
||||
account.hiddenUsers.flow.value,
|
||||
)
|
||||
|
||||
private fun innerApplyFilter(collection: Collection<Note>): Set<Note> {
|
||||
val params = buildFilterParams(account)
|
||||
|
||||
return collection.filterTo(HashSet()) {
|
||||
val noteEvent = it.event
|
||||
(noteEvent is PollEvent || noteEvent is ZapPollEvent) && params.match(noteEvent, it.relays) && isOpen(it)
|
||||
}
|
||||
}
|
||||
|
||||
override fun sort(items: Set<Note>): List<Note> = items.sortedWith(DefaultFeedOrder)
|
||||
}
|
||||
@@ -416,6 +416,8 @@
|
||||
<string name="migrate_bookmarks_success">Bookmarks migrated successfully</string>
|
||||
<string name="drafts">Drafts</string>
|
||||
<string name="polls">Polls</string>
|
||||
<string name="open_polls">Open</string>
|
||||
<string name="closed_polls">Closed</string>
|
||||
<string name="pictures">Pictures</string>
|
||||
<string name="shorts">Shorts</string>
|
||||
<string name="longs">Videos</string>
|
||||
|
||||
Reference in New Issue
Block a user