fix: pagination in tabbed screens (#610)

This commit is contained in:
Diego Beraldin 2024-03-18 13:51:51 +01:00 committed by GitHub
parent daaabd9f3f
commit f14cafba22
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 161 additions and 168 deletions

View File

@ -68,7 +68,7 @@ for some technical notes.
- view post feed and comments with different listing and sort types; - view post feed and comments with different listing and sort types;
- possibility to upvote and downvote (with configurable swipe actions); - possibility to upvote and downvote (with configurable swipe actions);
- community and user detail (with info about moderators/moderated communities); - community and user detail (with info about moderators/moderated communities);
- user profile with one's own posts, comments and saved items; - review your posts and comments (created by you, bookmarked, liked/disliked);
- inbox with replies, mentions and direct messages; - inbox with replies, mentions and direct messages;
- global search with different result types (all, posts, comments, user, communities); - global search with different result types (all, posts, comments, user, communities);
- create and edit new posts (with optional images); - create and edit new posts (with optional images);
@ -85,56 +85,51 @@ for some technical notes.
- multi-community (community aggregation); - multi-community (community aggregation);
- view the moderation log; - view the moderation log;
- community moderation tool (examine and resolve reports, ban users, feature posts, block - community moderation tool (examine and resolve reports, ban users, feature posts, block
further comments from posts, mark comments as distinguished, remove posts/comments); further comments from posts, mark comments as distinguished, remove posts/comments, examine all posts/comments created
- save posts and comments you are creating as draft to edit them later. in your communities);
- save posts and comments you are creating as drafts to edit them later;
Most clients for Lemmy currently offer the first points (with various degrees of completion), so Most clients for Lemmy currently offer the first points (with various degrees of completion), so
there is nothing special about Raccoon for Lemmy, whereas the last ones are less common and are there is nothing special about Raccoon for Lemmy, whereas the last ones are less common and are
directed to more demanding users. directed to more demanding users.
Concerning customization, the ability to change some aspects like font face or size and app Concerning customization, the ability to change some aspects like font face or size and app colors, vote format, bar
colors, vote format, bar transparency and so on was of paramount importance from the very beginning. transparency and so on was of paramount importance from the very beginning. Similarly, users should be able to use the
Similarly, users should be able to use the app in their native language and change the UI language app in their native language and change the UI language independently of the system language.
independently from the system language.
This app is also intended for moderators who want to use their mobile device, offering moderation This app is also intended for moderators who want to use their mobile device, offering moderation tools (feature post,
tools (feature post, lock post, distinguish comment, remove post/comment, ban users) and the ability lock post, distinguish comment, remove post/comment, ban users) and the ability to revert any of these actions.
to revert any of these actions.
The project is under active development, so expect new features to be added over time. Have a The project is under active development, so expect new features to be added over time. Have a look on the issues labeled
look on the issues labeled with "feature" in the issue tracker to get an idea of what's going to with "feature" in the issue tracker to get an idea of what's going to come next.
come next.
If you have ideas, feedback, suggestions or comments remember to speak up and use your If you have ideas, feedback, suggestions or comments remember to speak up and use your voice. You can add reports or
voice. You can add reports or request features and they will be considered. request features and they will be considered.
## Why was the project started? ## Why was the project started?
Because raccoons are so adorable, aren't they? 🦝🦝🦝 Because raccoons are so adorable, aren't they? 🦝🦝🦝
Joking apart, one of the main goals was to experiment with KMP and learn how to properly deal Joking apart, one of the main goals was to experiment with KMP and learn how to properly deal with the challenges of a
with the challenges of a multiplatform environment, and a medium-sized project like this was an multiplatform environment, and a medium-sized project like this was an ideal testing ground for that technology.
ideal testing ground for that technology.
Secondly, I felt that the Android ecosystem of Lemmy apps was a little "poor" with few Secondly, I felt that the Android ecosystem of Lemmy apps was a little "poor" with few native apps (fewer open source),
native apps (fewer open source), while the "market" is dominated by iOS and cross platform clients. while the "market" is dominated by iOS and cross-platform clients. I ❤️ Kotlin, I ❤️ Free and Open Source Software and
I ❤️ Kotlin, I ❤️ Free and Open Source Software and I ❤️ native app development, so there was a I ❤️ native app development, so there was a niche that could be filled.
niche that needed to be filled.
Developing a new client was an opportunity to add all the good features that were "scattered" across Developing a new client was an opportunity to add all the good features that were "scattered" across different apps,
different apps, e.g. the feature richness of [Liftoff](https://github.com/liftoff-app/liftoff), the e.g. the feature richness of [Liftoff](https://github.com/liftoff-app/liftoff), the
multi-community feature of multi-community feature of
[Summit](https://github.com/idunnololz/summit-for-lemmy) and the polished UI of the really great [Summit](https://github.com/idunnololz/summit-for-lemmy) and the polished UI of the really great
[Thunder](https://github.com/thunder-app/thunder) and so on. This app tries to be configurable [Thunder](https://github.com/thunder-app/thunder) and so on. This app tries to be configurable
enough to make users feel "at home" and choose what they want, while at the same time having a not enough to make users feel "at home" and choose what they want, while at the same time having a not
too cluttered interface (except for the Settings screen - I know!) too cluttered interface (except for the Settings screen - I know!)
In the third place, this app has been a means to dig deeper inside Lemmy's internals and become more In the third place, this app has been a means to dig deeper inside Lemmy's internals and become more humble and patient
humble and patient towards other apps because there are technical difficulties in having to deal towards other apps because there are technical difficulties in having to deal with a platform like Lemmy.
with a platform like Lemmy.
This involves a high level of discretion and personal taste, I know, but this project _is_ all This involves a high level of discretion and personal taste, I know, but this project _is_ all about experimenting and
about experimenting and learning. learning.
## Technical notes: ## Technical notes:
@ -165,8 +160,7 @@ the [CONTRIBUTING.md](https://github.com/diegoberaldin/RaccoonForLemmy/blob/mast
feel confident with repository forks, pull requests, managing resource files, etc. feel free to feel confident with repository forks, pull requests, managing resource files, etc. feel free to
drop an email or contact me in any way. drop an email or contact me in any way.
Please remember: every contribution is welcome and everyone's opinion matters here. This is a Please remember: every contribution is welcome and everyone's opinion matters here. This is a community project, open
community project, open source, ad-free and free of charge, and it belongs to us all so don't be source, ad-free and free of charge, and it belongs to us all so don't be afraid to get involved.
afraid to get involved.
And don't forget every 🦝's motto: «Live Fast, Eat Trash» (abbreviated L.F.E.T.). And don't forget every 🦝's motto: «Live Fast, Eat Trash» (abbreviated L.F.E.T.).

View File

@ -231,7 +231,7 @@
<string name="report_list_type_unresolved">Unresolved</string> <string name="report_list_type_unresolved">Unresolved</string>
<string name="report_action_resolve">Resolve</string> <string name="report_action_resolve">Resolve</string>
<string name="report_action_unresolve">Unresolve</string> <string name="report_action_unresolve">Unresolve</string>
<string name="sidebar_not_logged_message">Welcome to Raccoon for Lemmy!\n\nIn anonymous mode, use the drop down button (▼) above to change instance.\n\nYou can log in to your intance at any time from the Profile screen.\n\nEnjoy Lemmy!</string> <string name="sidebar_not_logged_message">Welcome to Raccoon for Lemmy!\n\nIn anonymous mode, use the drop down button (▼) above to change instance.\n\nYou can log in to your instance at any time from the Profile screen.\n\nEnjoy Lemmy!</string>
<string name="settings_default_inbox_type">Default inbox type</string> <string name="settings_default_inbox_type">Default inbox type</string>
<string name="mod_action_add_mod">Add moderator</string> <string name="mod_action_add_mod">Add moderator</string>
<string name="mod_action_remove_mod">Remove moderator</string> <string name="mod_action_remove_mod">Remove moderator</string>

View File

@ -1,55 +1,48 @@
## Module structure ## Module structure
The project has different kinds of modules and, depending on the group a module belongs to, there The project has different kinds of modules and, depending on the group a module belongs to, there are some rules about
are some rules about which other modules it can depend on. which other modules it can depend on.
Here is a description of the dependency flow: Here is a description of the dependency flow:
- `:androidApp` which is the KMP equivalent of `:app` module in Android-only projects) - `:androidApp` which is the KMP equivalent of `:app` module in Android-only projects) include `:shared` and can
include `:shared` and can include `:core` modules (e.g. for navigation); include `:core` modules (e.g. for navigation);
- `:shared` is the heart of the KMP application and it virtually includes every other Gradle module - `:shared` is the heart of the KMP application and it virtually includes every other Gradle module as a dependency (it
as a dependency (it contains in the `DiHelper.kt` files the setup of the DI so it basically needs contains in the `DiHelper.kt` files the setup of the DI, so it basically needs to see all Koin modules);
to see all Koin modules); - `:feature` modules are included by :shared and include :domain, :core and :unit modules, but they DO not include other
- `:feature` modules are included by :shared and include :domain, :core and :unit modules but they each other nor any top level module; some unit modules are used just by one feature (e.g. `:unit:postlist` is used
DO not include other each other nor any top level module; some unit modules are used just by one only by `:feature:home`) in some other cases multiple features use the same unit (e.g. `:unit:zoomableimage` is used
feature (e.g. `:unit:postlist` is used only by `:feature:home`) in some other cases multiple by both `:feature:home`, `:feature:search`, `:feature:profile` and `:feature:inbox`);
features use the same unit (e.g. `:unit:zoomableimage` is used by - `:domain` modules can be used by feature and unit modules and can only include core modules; only exception
both `:feature:home`, `:feature:search`, `:feature:profile` and `:feature:inbox`): is `:domain:inbox` which is a thin layer on top of `:domain:lemmy` so it depends on it (for inbox related functions);
- `:domain` modules can be used by feature and unit modules and can only include core modules; only - `:unit` modules are included by feature modules (and `:shared`) and sometimes by other unit modules in case of highly
exception is `:domain:inbox` which is a thin layer on top of `:domain:lemmy` so it depends on it ( reusable parts of the app; the only notable violation to this rule is `:core:commonui:detailopener-impl` which is a
for inbox related functions); special module because it is only included by `:shared` (which does the binding between `:detailopener-api`
- `:unit` modules are included by feature modules (and `:shared`) and sometimes by other unit and `:detailopener-impl`) and it includes some unit modules but the fact of a unit module included by a core module in
modules in case of highly reusable parts of the app; the only notable violation to this rule general should never happen (instead, the reverse is perfectly ok);
is `:core:commonui:detailopener-impl` which is a special module because it is only included
by `:shared` (which does the binding between `:detailopener-api` and `:detailopener-impl`) and it
includes some unit modules but the fact of a unit module included by a core module in general
should never happen (instead, the reverse is perfectly ok);
- `:core` modules can sometimes include each other (but without cycles, e.g. `:core:markdown` - `:core` modules can sometimes include each other (but without cycles, e.g. `:core:markdown`
includes `:core:commonui:components` / `:core:utils` because it is a mid-level module and includes `:core:commonui:components` / `:core:utils` because it is a mid-level module and something similar happens
something similar happens with `:core:persistecnce` which with `:core:persistecnce` which uses `:core:preferences` / `:core:appearance`) and nothing else; they are in turn
uses `:core:preferences` / `:core:appearance`) and nothing else; they are in turn used by all the used by all the other types of modules.
other types of modules.
### Top-level modules ### Top-level modules
The main module (Android-specific) is `:androidApp`, which contains the Application The main module (Android-specific) is `:androidApp`, which contains the Application subclass (`MainApplication`) and the
subclass (`MainApplication`) and the main activity (`MainActivity`). The latter in main activity (`MainActivity`). The latter in its `onCreate(Bundle?)` invokes the `MainView` Composable function which
its `onCreate(Bundle?)` invokes the `MainView` Composable function which in turns calls `App`, the in turns calls `App`, the main entry point of the multiplatform application which is defined in the `:shared` module.
main entry point of the multiplatform application which is defined in the `:shared` module.
`:shared` is the top module of the multiplatform application, which includes all the other modules `:shared` is the top module of the multiplatform application, which includes all the other modules and is not included
and is not included by anything (except `:androidApp`). In its `commonMain` source set, this module by anything (except `:androidApp`). In its `commonMain` source set, this module contains `App`, the application entry
contains `App`, the application entry point, the definition on the `MainScreen` (and its ViewModel) point, the definition on the `MainScreen` (and its ViewModel) hosting the main navigation with the bottom tab bar.
hosting the main navigation with the bottom tab bar. Another important part of this module resides Another important part of this module resides in the platform specific source sets (`androidMain` and `iosMain`
in the platform specific source sets (`androidMain` and `iosMain` respectively) where respectively) where two `DiHelper.kt` files (one for each platform) can be found, which contain the setup of the root of
two `DiHelper.kt` files (one for each platform) can be found, which contain the setup of the root of the project's dependency injection in a platform specific way, an initialization function on iOS and a Koin module for
the project's dependency injection in a platform specific way, an initialization function on iOS and Android (which is included in `MainApplication`).
a Koin module for Android (which is included in `MainApplication`).
### Feature modules ### Feature modules
These modules correspond to the main functions of the application, i.e. the sections of the main These modules correspond to the main functions of the application, i.e. the sections of the main bottom navigation. In
bottom navigation. In particular: particular:
- `:feature:home` contains the post list tab; - `:feature:home` contains the post list tab;
- `:feature:search` contains the Explore tab; - `:feature:search` contains the Explore tab;
@ -61,25 +54,23 @@ bottom navigation. In particular:
These are purely business logic modules that can be reused to provide application main parts: These are purely business logic modules that can be reused to provide application main parts:
- `:domain:identity` contains the repositories and use cases that are related to user identity, - `:domain:identity` contains the repositories and use cases that are related to user identity, authorization and API
authorization and API configuration; configuration;
- `:domain:lemmy` contains all the Lemmy API interaction logic and is divided into two submodules: - `:domain:lemmy` contains all the Lemmy API interaction logic and is divided into two submodules:
- `:data` contains all the domain models for Lemmy entities (posts, comments, communities, - `:data` contains all the domain models for Lemmy entities (posts, comments, communities, users, etc);
users, etc); - `:repository` contains the repositories that access Lemmy APIs (through the :core:api module) and are used
- `:repository` contains the repositories that access Lemmy APIs (through the :core:api module) to manage the entities contained in the :data module;
and are used to manage the entities contained in the :data module; - `:domain:inbox` contains some uses cases needed to interact with the replies, mentions and private messages
- `:domain:inbox` contains some uses cases needed to interact with the replies, mentions and private repositories and coordinate the interaction between inbox-related app components.
messages repositories and coordinate the interaction between inbox-related app components.
### Unit modules ### Unit modules
These modules are the building blocks that are used to create user-visible parts of the application, These modules are the building blocks that are used to create user-visible parts of the application, i.e. the various
i.e. the various screens, some of which are reusable in multiple points (e.g. the user detail, screens, some of which are reusable in multiple points (e.g. the user detail, community detail or post detail, but also
community detail or post detail, but also report/post/comment creation forms, etc.). In some cases report/post/comment creation forms, etc.). In some cases even a dialog or a bottom-sheet can become a "unit", especially
even a dialog or a bottom-sheet can become a "unit", especially if it is used in multiple points or if it is used in multiple points or contains a little more than pure UI (e.g. some presentation logic); simple pure-UI
contains a little more than pure UI (e.g. some presentation logic); simple pure-UI dialogs and dialogs and sheets are located in the `:core:commonui:modals` module instead (but are being progressively converted to
sheets are located in the `:core:commonui:modals` module instead (but are being progressively separate units).
converted to separate units).
Here is a list of the main unit modules and their purpose: Here is a list of the main unit modules and their purpose:
@ -97,6 +88,7 @@ Here is a list of the main unit modules and their purpose:
- `:unit:createpost` contains the create post form - `:unit:createpost` contains the create post form
- `:unit:drafts` contains the screen uses to display post and comment drafts - `:unit:drafts` contains the screen uses to display post and comment drafts
- `:unit:drawer` contains the navigation drawer - `:unit:drawer` contains the navigation drawer
- `:unit:filteredcontents` contains the screen to access moderated contents or liked/disliked contents
- `:unit:instanceinfo` contains the instance info bottom sheet with the list of communities - `:unit:instanceinfo` contains the instance info bottom sheet with the list of communities
- `:unit:login` contains the login modal bottom sheet - `:unit:login` contains the login modal bottom sheet
- `:unit:manageaccounts` contains the modal bottom sheet used to change account - `:unit:manageaccounts` contains the modal bottom sheet used to change account
@ -123,49 +115,48 @@ Here is a list of the main unit modules and their purpose:
### Core modules ### Core modules
These are the foundational blocks containing the design system and various reusable utilities that These are the foundational blocks containing the design system and various reusable utilities that are called throughout
are called throughout the whole project. Here is a short description of them: the whole project. Here is a short description of them:
- `:core:api` contains the Ktorfit services used to interact with Lemmy APIs and all the data - `:core:api` contains the Ktorfit services used to interact with Lemmy APIs and all the data transfer objects (DTOs)
transfer objects (DTOs) used to send and receive data from the APIs; used to send and receive data from the APIs;
- `:core:appearance` contains the look and feel repository which exposes the information about the - `:core:appearance` contains the look and feel repository which exposes the information about the current theme as
current theme as observable states and allows to change them; observable states and allows to change them;
- `:core:architecture` contains the building blocks for the Model-View-Intent architecture used in - `:core:architecture` contains the building blocks for the Model-View-Intent architecture used in all the screens of
all the screens of the application; the application;
- `:core:commonui` contains a series of sub-modules that are used to define UI components used in - `:core:commonui` contains a series of submodules that are used to define UI components used in the app and reusable
the app and reusable UI blocks: UI blocks:
- `:components`: a collection of components that represent graphical widgets - `:components`: a collection of components that represent graphical widgets
- `:detailopener-api` : a utility module used to expose an API to centralize content opening ( - `:detailopener-api` : a utility module used to expose an API to centralize content opening (post detail,
post detail, community, detail, user detail, comment creation and post creation) community, detail, user detail, comment creation and post creation)
- `:detailopener-impl`: implementation of the detail opener, this is an exception to the module - `:detailopener-impl`: implementation of the detail opener, this is an exception to the module architecture because
architecture because it is a core module which includes unit modules so the important thing is it is a core module which includes unit modules so the important thing is
that no one **ever** include this module except for `:shared`; that no one **ever** include this module except for `:shared`;
- `:lemmyui`: graphical components used to represent Lemmy UI (posts, comments, inbox items, - `:lemmyui`: graphical components used to represent Lemmy UI (posts, comments, inbox items, etc.) and reusable
etc.) and reusable sub-components such as different types of headers, footers, cards, etc. subcomponents such as different types of headers, footers, cards, etc.
- `:modals`: definition of modal bottom sheets and dialogs that have no presentation logic. This - `:modals`: definition of modal bottom sheets and dialogs that have no presentation logic. This module was
module was historically much bigger and over time components were migrated to separate units historically much bigger and over time components were migrated to separate units
modules; modules;
- `:core:markdown` contains Markdown rendering logic; - `:core:markdown` contains Markdown rendering logic;
- `core:l10n` contains all the localization messages and the `L10nManager` interface which acts - `core:l10n` contains all the localization messages and the `L10nManager` interface which acts as a wrapper around
as a wrapper around Lyricist to load the internationalized messages; Lyricist to load the internationalized messages;
- `:core:navigation` contains the navigation manager used for stack navigation, bottom sheet - `:core:navigation` contains the navigation manager used for stack navigation, bottom sheet navigation and a
navigation and a coordinator for the events originated by the navigation drawer; coordinator for the events originated by the navigation drawer;
- `:core:notifications` contains the `NotificationCenter` contract and implementation as well as the - `:core:notifications` contains the `NotificationCenter` contract and implementation as well as the event definition,
event definition, this is used as an event bus throughout the whole project; this is used as an event bus throughout the whole project;
- `:core:persistence` contains the local database (primary storage) management logic as well as - `:core:persistence` contains the local database (primary storage) management logic as well as SQLDelight definitions
SQLDelight definitions of entities and migrations, plus all the local data sources that are used of entities and migrations, plus all the local data sources that are used
to access the database; to access the database;
- `:core:preferences` contains the shared preferences/user defaults (secondary storage) and relies - `:core:preferences` contains the shared preferences/user defaults (secondary storage) and relies on the
on the multiplatform-settings library to offer a temporary key-value store; multiplatform-settings library to offer a temporary key-value store;
- `:core:resources` is a wrapper around the resource loading (fonts and images mainly) which used to - `:core:resources` is a wrapper around the resource loading (fonts and images mainly) which used to rely on an external
rely on an external library and now used the built-in resource management of Compose; library and now used the built-in resource management of Compose;
- `:core:utils`: contains a series of helper and utility functions/classes that are used in the - `:core:utils`: contains a series of helper and utility functions/classes that are used in the project but were not big
project but were not big enough to be converted to separate domain/core modules on their own. enough to be converted to separate domain/core modules on their own.
On second thoughts: On second thoughts:
- `:core:commonui` has still too much in it, especially `:modals` packages should become unit - `:core:commonui` has still too much in it, especially `:modals` packages should become unit modules;
modules; - `:core:persistence` belongs more to domain modules, e.g. `:domain:accounts`/`:domain:settings` but it is implemented
- `:core:persistence` belongs more to domain modules, e.g. `:domain:accounts`/`:domain:settings` but as a core module because is is strongly tied to SQLDelight and its generated code which provides the named queries to
it is implemented as a core module because is is strongly tied to SQLDelight and its generated fetch/save data to the local DB.
code which provides the named queries to fetch/save data to the local DB.

View File

@ -41,7 +41,7 @@ class FilteredContentsViewModel(
initialState = FilteredContentsMviModel.State(), initialState = FilteredContentsMviModel.State(),
) { ) {
private var currentPage = 1 private val currentPage = mutableMapOf<FilteredContentsSection, Int>()
private var pageCursor: String? = null private var pageCursor: String? = null
init { init {
@ -157,7 +157,8 @@ class FilteredContentsViewModel(
} }
private fun refresh(initial: Boolean = false) { private fun refresh(initial: Boolean = false) {
currentPage = 1 currentPage[FilteredContentsSection.Posts] = 1
currentPage[FilteredContentsSection.Comments] = 1
pageCursor = null pageCursor = null
updateState { updateState {
it.copy( it.copy(
@ -198,11 +199,12 @@ class FilteredContentsViewModel(
val refreshing = currentState.refreshing val refreshing = currentState.refreshing
if (currentState.section == FilteredContentsSection.Posts) { if (currentState.section == FilteredContentsSection.Posts) {
val page = currentPage[FilteredContentsSection.Posts] ?: 1
coroutineScope { coroutineScope {
val itemList = async { val itemList = async {
postRepository.getAll( postRepository.getAll(
auth = auth, auth = auth,
page = currentPage, page = page,
pageCursor = pageCursor, pageCursor = pageCursor,
type = ListingType.ModeratorView, type = ListingType.ModeratorView,
sort = SortType.New, sort = SortType.New,
@ -227,21 +229,18 @@ class FilteredContentsViewModel(
} }
}.await() }.await()
val comments = async { val comments = async {
if (currentPage == 1 && (currentState.comments.isEmpty() || refreshing)) { if (page == 1 && (currentState.comments.isEmpty() || refreshing)) {
// this is needed because otherwise on first selector change // this is needed because otherwise on first selector change
// the lazy column scrolls back to top (it must have an empty data set) // the lazy column scrolls back to top (it must have an empty data set)
commentRepository.getAll( commentRepository.getAll(
auth = auth, auth = auth,
page = currentPage, page = 1,
type = ListingType.ModeratorView, type = ListingType.ModeratorView,
).orEmpty() ).orEmpty()
} else { } else {
currentState.comments currentState.comments
} }
}.await() }.await()
if (!itemList.isNullOrEmpty()) {
currentPage++
}
val itemsToAdd = itemList.orEmpty().filter { post -> val itemsToAdd = itemList.orEmpty().filter { post ->
!post.deleted !post.deleted
} }
@ -268,13 +267,14 @@ class FilteredContentsViewModel(
) )
} }
if (!itemList.isNullOrEmpty()) { if (!itemList.isNullOrEmpty()) {
currentPage++ currentPage[FilteredContentsSection.Posts] = page + 1
} }
} }
} else { } else {
val page = currentPage[FilteredContentsSection.Comments] ?: 1
val itemList = commentRepository.getAll( val itemList = commentRepository.getAll(
auth = auth, auth = auth,
page = currentPage, page = page,
type = ListingType.ModeratorView, type = ListingType.ModeratorView,
)?.let { list -> )?.let { list ->
if (refreshing) { if (refreshing) {
@ -305,7 +305,7 @@ class FilteredContentsViewModel(
) )
} }
if (!itemList.isNullOrEmpty()) { if (!itemList.isNullOrEmpty()) {
currentPage++ currentPage[FilteredContentsSection.Comments] = page + 1
} }
} }
} }
@ -317,11 +317,12 @@ class FilteredContentsViewModel(
val refreshing = currentState.refreshing val refreshing = currentState.refreshing
if (currentState.section == FilteredContentsSection.Posts) { if (currentState.section == FilteredContentsSection.Posts) {
val page = currentPage[FilteredContentsSection.Posts] ?: 1
coroutineScope { coroutineScope {
val itemList = async { val itemList = async {
userRepository.getLikedPosts( userRepository.getLikedPosts(
auth = auth, auth = auth,
page = currentPage, page = page,
pageCursor = pageCursor, pageCursor = pageCursor,
liked = currentState.liked, liked = currentState.liked,
sort = SortType.New, sort = SortType.New,
@ -346,12 +347,12 @@ class FilteredContentsViewModel(
} }
}.await() }.await()
val comments = async { val comments = async {
if (currentPage == 1 && (currentState.comments.isEmpty() || refreshing)) { if (page == 1 && (currentState.comments.isEmpty() || refreshing)) {
// this is needed because otherwise on first selector change // this is needed because otherwise on first selector change
// the lazy column scrolls back to top (it must have an empty data set) // the lazy column scrolls back to top (it must have an empty data set)
userRepository.getLikedComments( userRepository.getLikedComments(
auth = auth, auth = auth,
page = currentPage, page = 1,
liked = currentState.liked, liked = currentState.liked,
sort = SortType.New, sort = SortType.New,
).orEmpty() ).orEmpty()
@ -359,9 +360,6 @@ class FilteredContentsViewModel(
currentState.comments currentState.comments
} }
}.await() }.await()
if (!itemList.isNullOrEmpty()) {
currentPage++
}
val itemsToAdd = itemList.orEmpty().filter { post -> val itemsToAdd = itemList.orEmpty().filter { post ->
!post.deleted !post.deleted
} }
@ -388,13 +386,14 @@ class FilteredContentsViewModel(
) )
} }
if (!itemList.isNullOrEmpty()) { if (!itemList.isNullOrEmpty()) {
currentPage++ currentPage[FilteredContentsSection.Posts] = page + 1
} }
} }
} else { } else {
val page = currentPage[FilteredContentsSection.Comments] ?: 1
val itemList = userRepository.getLikedComments( val itemList = userRepository.getLikedComments(
auth = auth, auth = auth,
page = currentPage, page = page,
liked = currentState.liked, liked = currentState.liked,
sort = SortType.New, sort = SortType.New,
)?.let { list -> )?.let { list ->
@ -426,7 +425,7 @@ class FilteredContentsViewModel(
) )
} }
if (!itemList.isNullOrEmpty()) { if (!itemList.isNullOrEmpty()) {
currentPage++ currentPage[FilteredContentsSection.Comments] = page + 1
} }
} }
} }

View File

@ -48,7 +48,7 @@ class ProfileLoggedViewModel(
initialState = ProfileLoggedMviModel.UiState() initialState = ProfileLoggedMviModel.UiState()
) { ) {
private var currentPage = 1 private val currentPage = mutableMapOf<ProfileLoggedSection, Int>()
init { init {
updateState { it.copy(instance = apiConfigurationRepository.instance.value) } updateState { it.copy(instance = apiConfigurationRepository.instance.value) }
@ -209,7 +209,8 @@ class ProfileLoggedViewModel(
} }
private suspend fun refresh(initial: Boolean = false) { private suspend fun refresh(initial: Boolean = false) {
currentPage = 1 currentPage[ProfileLoggedSection.Posts] = 1
currentPage[ProfileLoggedSection.Comments] = 1
updateState { updateState {
it.copy( it.copy(
canFetchMore = true, canFetchMore = true,
@ -245,23 +246,24 @@ class ProfileLoggedViewModel(
val section = currentState.section val section = currentState.section
val includeNsfw = settingsRepository.currentSettings.value.includeNsfw val includeNsfw = settingsRepository.currentSettings.value.includeNsfw
if (section == ProfileLoggedSection.Posts) { if (section == ProfileLoggedSection.Posts) {
val page = currentPage[ProfileLoggedSection.Posts] ?: 1
coroutineScope { coroutineScope {
val itemList = async { val itemList = async {
userRepository.getPosts( userRepository.getPosts(
auth = auth, auth = auth,
id = userId, id = userId,
page = currentPage, page = page,
sort = SortType.New, sort = SortType.New,
) )
}.await() }.await()
val comments = async { val comments = async {
if (currentPage == 1 && (currentState.comments.isEmpty() || refreshing)) { if (page == 1 && (currentState.comments.isEmpty() || refreshing)) {
// this is needed because otherwise on first selector change // this is needed because otherwise on first selector change
// the lazy column scrolls back to top (it must have an empty data set) // the lazy column scrolls back to top (it must have an empty data set)
userRepository.getComments( userRepository.getComments(
auth = auth, auth = auth,
id = userId, id = userId,
page = currentPage, page = 1,
sort = SortType.New, sort = SortType.New,
).orEmpty() ).orEmpty()
} else { } else {
@ -299,14 +301,15 @@ class ProfileLoggedViewModel(
) )
} }
if (!itemList.isNullOrEmpty()) { if (!itemList.isNullOrEmpty()) {
currentPage++ currentPage[ProfileLoggedSection.Posts] = page + 1
} }
} }
} else { } else {
val page = currentPage[ProfileLoggedSection.Comments] ?: 1
val itemList = userRepository.getComments( val itemList = userRepository.getComments(
auth = auth, auth = auth,
id = userId, id = userId,
page = currentPage, page = page,
sort = SortType.New, sort = SortType.New,
) )
val commentsToAdd = itemList.orEmpty() val commentsToAdd = itemList.orEmpty()
@ -328,7 +331,7 @@ class ProfileLoggedViewModel(
) )
} }
if (!itemList.isNullOrEmpty()) { if (!itemList.isNullOrEmpty()) {
currentPage++ currentPage[ProfileLoggedSection.Comments] = page + 1
} }
} }
} }

View File

@ -32,7 +32,7 @@ class ReportListViewModel(
initialState = ReportListMviModel.UiState(), initialState = ReportListMviModel.UiState(),
) { ) {
private var currentPage = 1 private val currentPage = mutableMapOf<ReportListSection, Int>()
init { init {
screenModelScope.launch { screenModelScope.launch {
@ -98,7 +98,8 @@ class ReportListViewModel(
} }
private fun refresh(initial: Boolean = false) { private fun refresh(initial: Boolean = false) {
currentPage = 1 currentPage[ReportListSection.Posts] = 1
currentPage[ReportListSection.Comments] = 1
updateState { updateState {
it.copy( it.copy(
canFetchMore = true, canFetchMore = true,
@ -124,23 +125,24 @@ class ReportListViewModel(
val section = currentState.section val section = currentState.section
val unresolvedOnly = currentState.unresolvedOnly val unresolvedOnly = currentState.unresolvedOnly
if (section == ReportListSection.Posts) { if (section == ReportListSection.Posts) {
val page = currentPage[ReportListSection.Posts] ?: 1
coroutineScope { coroutineScope {
val itemList = async { val itemList = async {
postRepository.getReports( postRepository.getReports(
auth = auth, auth = auth,
communityId = communityId, communityId = communityId,
page = currentPage, page = page,
unresolvedOnly = unresolvedOnly, unresolvedOnly = unresolvedOnly,
) )
}.await() }.await()
val commentReports = async { val commentReports = async {
if (currentPage == 1 && (currentState.commentReports.isEmpty() || refreshing)) { if (page == 1 && (currentState.commentReports.isEmpty() || refreshing)) {
// this is needed because otherwise on first selector change // this is needed because otherwise on first selector change
// the lazy column scrolls back to top (it must have an empty data set) // the lazy column scrolls back to top (it must have an empty data set)
commentRepository.getReports( commentRepository.getReports(
auth = auth, auth = auth,
communityId = communityId, communityId = communityId,
page = currentPage, page = 1,
unresolvedOnly = unresolvedOnly, unresolvedOnly = unresolvedOnly,
).orEmpty() ).orEmpty()
} else { } else {
@ -163,14 +165,15 @@ class ReportListViewModel(
) )
} }
if (!itemList.isNullOrEmpty()) { if (!itemList.isNullOrEmpty()) {
currentPage++ currentPage[ReportListSection.Posts] = page + 1
} }
} }
} else { } else {
val page = currentPage[ReportListSection.Comments] ?: 1
val itemList = commentRepository.getReports( val itemList = commentRepository.getReports(
auth = auth, auth = auth,
communityId = communityId, communityId = communityId,
page = currentPage, page = page,
unresolvedOnly = unresolvedOnly, unresolvedOnly = unresolvedOnly,
) )
@ -189,7 +192,7 @@ class ReportListViewModel(
) )
} }
if (!itemList.isNullOrEmpty()) { if (!itemList.isNullOrEmpty()) {
currentPage++ currentPage[ReportListSection.Comments] = page + 1
} }
} }
} }

View File

@ -56,7 +56,7 @@ class UserDetailViewModel(
initialState = UserDetailMviModel.UiState(), initialState = UserDetailMviModel.UiState(),
) { ) {
private var currentPage = 1 private val currentPage = mutableMapOf<UserDetailSection, Int>()
init { init {
updateState { updateState {
@ -247,7 +247,8 @@ class UserDetailViewModel(
} }
private suspend fun refresh(initial: Boolean = false) { private suspend fun refresh(initial: Boolean = false) {
currentPage = 1 currentPage[UserDetailSection.Posts] = 1
currentPage[UserDetailSection.Comments] = 1
updateState { updateState {
it.copy( it.copy(
canFetchMore = true, canFetchMore = true,
@ -281,25 +282,26 @@ class UserDetailViewModel(
val userId = currentState.user.id val userId = currentState.user.id
val includeNsfw = settingsRepository.currentSettings.value.includeNsfw val includeNsfw = settingsRepository.currentSettings.value.includeNsfw
if (section == UserDetailSection.Posts) { if (section == UserDetailSection.Posts) {
val page = currentPage[UserDetailSection.Posts] ?: 1
coroutineScope { coroutineScope {
val itemList = async { val itemList = async {
userRepository.getPosts( userRepository.getPosts(
auth = auth, auth = auth,
id = userId, id = userId,
page = currentPage, page = page,
sort = currentState.sortType, sort = currentState.sortType,
username = uiState.value.user.name, username = uiState.value.user.name,
otherInstance = otherInstance, otherInstance = otherInstance,
) )
}.await() }.await()
val comments = async { val comments = async {
if (currentPage == 1 && (currentState.comments.isEmpty() || refreshing)) { if (page == 1 && (currentState.comments.isEmpty() || refreshing)) {
// this is needed because otherwise on first selector change // this is needed because otherwise on first selector change
// the lazy column scrolls back to top (it must have an empty data set) // the lazy column scrolls back to top (it must have an empty data set)
userRepository.getComments( userRepository.getComments(
auth = auth, auth = auth,
id = userId, id = userId,
page = currentPage, page = 1,
sort = currentState.sortType, sort = currentState.sortType,
username = uiState.value.user.name, username = uiState.value.user.name,
otherInstance = otherInstance, otherInstance = otherInstance,
@ -346,14 +348,15 @@ class UserDetailViewModel(
) )
} }
if (!itemList.isNullOrEmpty()) { if (!itemList.isNullOrEmpty()) {
currentPage++ currentPage[UserDetailSection.Posts] = page + 1
} }
} }
} else { } else {
val page = currentPage[UserDetailSection.Comments] ?: 1
val itemList = userRepository.getComments( val itemList = userRepository.getComments(
auth = auth, auth = auth,
id = userId, id = userId,
page = currentPage, page = page,
sort = currentState.sortType, sort = currentState.sortType,
otherInstance = otherInstance, otherInstance = otherInstance,
) )
@ -376,7 +379,7 @@ class UserDetailViewModel(
) )
} }
if (!itemList.isNullOrEmpty()) { if (!itemList.isNullOrEmpty()) {
currentPage++ currentPage[UserDetailSection.Comments] = page + 1
} }
} }
} }