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;
- possibility to upvote and downvote (with configurable swipe actions);
- 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;
- global search with different result types (all, posts, comments, user, communities);
- create and edit new posts (with optional images);
@ -85,56 +85,51 @@ for some technical notes.
- multi-community (community aggregation);
- view the moderation log;
- community moderation tool (examine and resolve reports, ban users, feature posts, block
further comments from posts, mark comments as distinguished, remove posts/comments);
- save posts and comments you are creating as draft to edit them later.
further comments from posts, mark comments as distinguished, remove posts/comments, examine all posts/comments created
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
there is nothing special about Raccoon for Lemmy, whereas the last ones are less common and are
directed to more demanding users.
Concerning customization, the ability to change some aspects like font face or size and app
colors, vote format, bar transparency and so on was of paramount importance from the very beginning.
Similarly, users should be able to use the app in their native language and change the UI language
independently from the system language.
Concerning customization, the ability to change some aspects like font face or size and app colors, vote format, bar
transparency and so on was of paramount importance from the very beginning. Similarly, users should be able to use the
app in their native language and change the UI language independently of the system language.
This app is also intended for moderators who want to use their mobile device, offering moderation
tools (feature post, lock post, distinguish comment, remove post/comment, ban users) and the ability
to revert any of these actions.
This app is also intended for moderators who want to use their mobile device, offering moderation tools (feature post,
lock post, distinguish comment, remove post/comment, ban users) and the ability to revert any of these actions.
The project is under active development, so expect new features to be added over time. Have a
look on the issues labeled with "feature" in the issue tracker to get an idea of what's going to
come next.
The project is under active development, so expect new features to be added over time. Have a look on the issues labeled
with "feature" in the issue tracker to get an idea of what's going to come next.
If you have ideas, feedback, suggestions or comments remember to speak up and use your
voice. You can add reports or request features and they will be considered.
If you have ideas, feedback, suggestions or comments remember to speak up and use your voice. You can add reports or
request features and they will be considered.
## Why was the project started?
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
with the challenges of a multiplatform environment, and a medium-sized project like this was an
ideal testing ground for that technology.
Joking apart, one of the main goals was to experiment with KMP and learn how to properly deal with the challenges of a
multiplatform environment, and a medium-sized project like this was an ideal testing ground for that technology.
Secondly, I felt that the Android ecosystem of Lemmy apps was a little "poor" with few
native apps (fewer open source), while the "market" is dominated by iOS and cross platform clients.
I ❤️ Kotlin, I ❤️ Free and Open Source Software and I ❤️ native app development, so there was a
niche that needed to be filled.
Secondly, I felt that the Android ecosystem of Lemmy apps was a little "poor" with few native apps (fewer open source),
while the "market" is dominated by iOS and cross-platform clients. I ❤️ Kotlin, I ❤️ Free and Open Source Software and
I ❤️ native app development, so there was a niche that could be filled.
Developing a new client was an opportunity to add all the good features that were "scattered" across
different apps, e.g. the feature richness of [Liftoff](https://github.com/liftoff-app/liftoff), the
Developing a new client was an opportunity to add all the good features that were "scattered" across different apps,
e.g. the feature richness of [Liftoff](https://github.com/liftoff-app/liftoff), the
multi-community feature of
[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
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!)
In the third place, this app has been a means to dig deeper inside Lemmy's internals and become more
humble and patient towards other apps because there are technical difficulties in having to deal
with a platform like Lemmy.
In the third place, this app has been a means to dig deeper inside Lemmy's internals and become more humble and patient
towards other apps because there are technical difficulties in having to deal with a platform like Lemmy.
This involves a high level of discretion and personal taste, I know, but this project _is_ all
about experimenting and learning.
This involves a high level of discretion and personal taste, I know, but this project _is_ all about experimenting and
learning.
## 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
drop an email or contact me in any way.
Please remember: every contribution is welcome and everyone's opinion matters here. This is a
community project, open source, ad-free and free of charge, and it belongs to us all so don't be
afraid to get involved.
Please remember: every contribution is welcome and everyone's opinion matters here. This is a community project, open
source, ad-free and free of charge, and it belongs to us all so don't be afraid to get involved.
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_action_resolve">Resolve</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="mod_action_add_mod">Add moderator</string>
<string name="mod_action_remove_mod">Remove moderator</string>

View File

@ -1,55 +1,48 @@
## Module structure
The project has different kinds of modules and, depending on the group a module belongs to, there
are some rules about which other modules it can depend on.
The project has different kinds of modules and, depending on the group a module belongs to, there are some rules about
which other modules it can depend on.
Here is a description of the dependency flow:
- `:androidApp` which is the KMP equivalent of `:app` module in Android-only projects)
include `:shared` and can include `:core` modules (e.g. for navigation);
- `:shared` is the heart of the KMP application and it virtually includes every other Gradle module
as a dependency (it contains in the `DiHelper.kt` files the setup of the DI so it basically needs
to see all Koin modules);
- `:feature` modules are included by :shared and include :domain, :core and :unit modules but they
DO not include other each other nor any top level module; some unit modules are used just by one
feature (e.g. `:unit:postlist` is used only by `:feature:home`) in some other cases multiple
features use the same unit (e.g. `:unit:zoomableimage` is used by
both `:feature:home`, `:feature:search`, `:feature:profile` and `:feature:inbox`):
- `:domain` modules can be used by feature and unit modules and can only include core modules; only
exception is `:domain:inbox` which is a thin layer on top of `:domain:lemmy` so it depends on it (
for inbox related functions);
- `:unit` modules are included by feature modules (and `:shared`) and sometimes by other unit
modules in case of highly reusable parts of the app; the only notable violation to this rule
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);
- `:androidApp` which is the KMP equivalent of `:app` module in Android-only projects) include `:shared` and can
include `:core` modules (e.g. for navigation);
- `:shared` is the heart of the KMP application and it virtually includes every other Gradle module as a dependency (it
contains in the `DiHelper.kt` files the setup of the DI, so it basically needs to see all Koin modules);
- `:feature` modules are included by :shared and include :domain, :core and :unit modules, but they DO not include other
each other nor any top level module; some unit modules are used just by one feature (e.g. `:unit:postlist` is used
only by `:feature:home`) in some other cases multiple features use the same unit (e.g. `:unit:zoomableimage` is used
by both `:feature:home`, `:feature:search`, `:feature:profile` and `:feature:inbox`);
- `:domain` modules can be used by feature and unit modules and can only include core modules; only exception
is `:domain:inbox` which is a thin layer on top of `:domain:lemmy` so it depends on it (for inbox related functions);
- `:unit` modules are included by feature modules (and `:shared`) and sometimes by other unit modules in case of highly
reusable parts of the app; the only notable violation to this rule 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`
includes `:core:commonui:components` / `:core:utils` because it is a mid-level module and
something similar happens with `:core:persistecnce` which
uses `:core:preferences` / `:core:appearance`) and nothing else; they are in turn used by all the
other types of modules.
includes `:core:commonui:components` / `:core:utils` because it is a mid-level module and something similar happens
with `:core:persistecnce` which uses `:core:preferences` / `:core:appearance`) and nothing else; they are in turn
used by all the other types of modules.
### Top-level modules
The main module (Android-specific) is `:androidApp`, which contains the Application
subclass (`MainApplication`) and the main activity (`MainActivity`). The latter in
its `onCreate(Bundle?)` invokes the `MainView` Composable function which in turns calls `App`, the
main entry point of the multiplatform application which is defined in the `:shared` module.
The main module (Android-specific) is `:androidApp`, which contains the Application subclass (`MainApplication`) and the
main activity (`MainActivity`). The latter in its `onCreate(Bundle?)` invokes the `MainView` Composable function which
in turns calls `App`, the 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
and is not included by anything (except `:androidApp`). In its `commonMain` source set, this module
contains `App`, the application entry point, the definition on the `MainScreen` (and its ViewModel)
hosting the main navigation with the bottom tab bar. Another important part of this module resides
in the platform specific source sets (`androidMain` and `iosMain` respectively) where
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 Android (which is included in `MainApplication`).
`:shared` is the top module of the multiplatform application, which includes all the other modules and is not included
by anything (except `:androidApp`). In its `commonMain` source set, this module contains `App`, the application entry
point, the definition on the `MainScreen` (and its ViewModel) hosting the main navigation with the bottom tab bar.
Another important part of this module resides in the platform specific source sets (`androidMain` and `iosMain`
respectively) where 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
Android (which is included in `MainApplication`).
### Feature modules
These modules correspond to the main functions of the application, i.e. the sections of the main
bottom navigation. In particular:
These modules correspond to the main functions of the application, i.e. the sections of the main bottom navigation. In
particular:
- `:feature:home` contains the post list 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:
- `:domain:identity` contains the repositories and use cases that are related to user identity,
authorization and API configuration;
- `:domain:identity` contains the repositories and use cases that are related to user identity, authorization and API
configuration;
- `: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,
users, etc);
- `:repository` contains the repositories that access Lemmy APIs (through the :core:api 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 repositories and coordinate the interaction between inbox-related app components.
- `:data` contains all the domain models for Lemmy entities (posts, comments, communities, users, etc);
- `:repository` contains the repositories that access Lemmy APIs (through the :core:api 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
repositories and coordinate the interaction between inbox-related app components.
### Unit modules
These modules are the building blocks that are used to create user-visible parts of the application,
i.e. the various screens, some of which are reusable in multiple points (e.g. the user detail,
community detail or post detail, but also report/post/comment creation forms, etc.). In some cases
even a dialog or a bottom-sheet can become a "unit", especially if it is used in multiple points or
contains a little more than pure UI (e.g. some presentation logic); simple pure-UI dialogs and
sheets are located in the `:core:commonui:modals` module instead (but are being progressively
converted to separate units).
These modules are the building blocks that are used to create user-visible parts of the application, i.e. the various
screens, some of which are reusable in multiple points (e.g. the user detail, community detail or post detail, but also
report/post/comment creation forms, etc.). In some cases even a dialog or a bottom-sheet can become a "unit", especially
if it is used in multiple points or contains a little more than pure UI (e.g. some presentation logic); simple pure-UI
dialogs and sheets are located in the `:core:commonui:modals` module instead (but are being progressively converted to
separate units).
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:drafts` contains the screen uses to display post and comment drafts
- `: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:login` contains the login modal bottom sheet
- `: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
These are the foundational blocks containing the design system and various reusable utilities that
are called throughout the whole project. Here is a short description of them:
These are the foundational blocks containing the design system and various reusable utilities that are called throughout
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
transfer objects (DTOs) used to send and receive data from the APIs;
- `:core:appearance` contains the look and feel repository which exposes the information about the
current theme as observable states and allows to change them;
- `:core:architecture` contains the building blocks for the Model-View-Intent architecture used in
all the screens of the application;
- `:core:commonui` contains a series of sub-modules that are used to define UI components used in
the app and reusable UI blocks:
- `:core:api` contains the Ktorfit services used to interact with Lemmy APIs and all the data transfer objects (DTOs)
used to send and receive data from the APIs;
- `:core:appearance` contains the look and feel repository which exposes the information about the current theme as
observable states and allows to change them;
- `:core:architecture` contains the building blocks for the Model-View-Intent architecture used in all the screens of
the application;
- `:core:commonui` contains a series of submodules that are used to define UI components used in the app and reusable
UI blocks:
- `:components`: a collection of components that represent graphical widgets
- `:detailopener-api` : a utility module used to expose an API to centralize content opening (
post detail, community, detail, user detail, comment creation and post creation)
- `:detailopener-impl`: implementation of the detail opener, this is an exception to the module
architecture because it is a core module which includes unit modules so the important thing is
- `:detailopener-api` : a utility module used to expose an API to centralize content opening (post detail,
community, detail, user detail, comment creation and post creation)
- `:detailopener-impl`: implementation of the detail opener, this is an exception to the module architecture because
it is a core module which includes unit modules so the important thing is
that no one **ever** include this module except for `:shared`;
- `:lemmyui`: graphical components used to represent Lemmy UI (posts, comments, inbox items,
etc.) and reusable sub-components such as different types of headers, footers, cards, etc.
- `:modals`: definition of modal bottom sheets and dialogs that have no presentation logic. This
module was historically much bigger and over time components were migrated to separate units
- `:lemmyui`: graphical components used to represent Lemmy UI (posts, comments, inbox items, etc.) and reusable
subcomponents such as different types of headers, footers, cards, etc.
- `:modals`: definition of modal bottom sheets and dialogs that have no presentation logic. This module was
historically much bigger and over time components were migrated to separate units
modules;
- `:core:markdown` contains Markdown rendering logic;
- `core:l10n` contains all the localization messages and the `L10nManager` interface which acts
as a wrapper around Lyricist to load the internationalized messages;
- `:core:navigation` contains the navigation manager used for stack navigation, bottom sheet
navigation and a coordinator for the events originated by the navigation drawer;
- `:core:notifications` contains the `NotificationCenter` contract and implementation as well as the
event definition, this is used as an event bus throughout the whole project;
- `:core:persistence` contains the local database (primary storage) management logic as well as
SQLDelight definitions of entities and migrations, plus all the local data sources that are used
- `core:l10n` contains all the localization messages and the `L10nManager` interface which acts as a wrapper around
Lyricist to load the internationalized messages;
- `:core:navigation` contains the navigation manager used for stack navigation, bottom sheet navigation and a
coordinator for the events originated by the navigation drawer;
- `:core:notifications` contains the `NotificationCenter` contract and implementation as well as the event definition,
this is used as an event bus throughout the whole project;
- `:core:persistence` contains the local database (primary storage) management logic as well as SQLDelight definitions
of entities and migrations, plus all the local data sources that are used
to access the database;
- `:core:preferences` contains the shared preferences/user defaults (secondary storage) and relies
on the 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
rely on an external 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
project but were not big enough to be converted to separate domain/core modules on their own.
- `:core:preferences` contains the shared preferences/user defaults (secondary storage) and relies on the
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 rely on an external
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 project but were not big
enough to be converted to separate domain/core modules on their own.
On second thoughts:
- `:core:commonui` has still too much in it, especially `:modals` packages should become unit
modules;
- `:core:persistence` belongs more to domain modules, e.g. `:domain:accounts`/`:domain:settings` but
it is implemented as a core module because is is strongly tied to SQLDelight and its generated
code which provides the named queries to fetch/save data to the local DB.
- `:core:commonui` has still too much in it, especially `:modals` packages should become unit modules;
- `:core:persistence` belongs more to domain modules, e.g. `:domain:accounts`/`:domain:settings` but it is implemented
as a core module because is is strongly tied to SQLDelight and its generated 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(),
) {
private var currentPage = 1
private val currentPage = mutableMapOf<FilteredContentsSection, Int>()
private var pageCursor: String? = null
init {
@ -157,7 +157,8 @@ class FilteredContentsViewModel(
}
private fun refresh(initial: Boolean = false) {
currentPage = 1
currentPage[FilteredContentsSection.Posts] = 1
currentPage[FilteredContentsSection.Comments] = 1
pageCursor = null
updateState {
it.copy(
@ -198,11 +199,12 @@ class FilteredContentsViewModel(
val refreshing = currentState.refreshing
if (currentState.section == FilteredContentsSection.Posts) {
val page = currentPage[FilteredContentsSection.Posts] ?: 1
coroutineScope {
val itemList = async {
postRepository.getAll(
auth = auth,
page = currentPage,
page = page,
pageCursor = pageCursor,
type = ListingType.ModeratorView,
sort = SortType.New,
@ -227,21 +229,18 @@ class FilteredContentsViewModel(
}
}.await()
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
// the lazy column scrolls back to top (it must have an empty data set)
commentRepository.getAll(
auth = auth,
page = currentPage,
page = 1,
type = ListingType.ModeratorView,
).orEmpty()
} else {
currentState.comments
}
}.await()
if (!itemList.isNullOrEmpty()) {
currentPage++
}
val itemsToAdd = itemList.orEmpty().filter { post ->
!post.deleted
}
@ -268,13 +267,14 @@ class FilteredContentsViewModel(
)
}
if (!itemList.isNullOrEmpty()) {
currentPage++
currentPage[FilteredContentsSection.Posts] = page + 1
}
}
} else {
val page = currentPage[FilteredContentsSection.Comments] ?: 1
val itemList = commentRepository.getAll(
auth = auth,
page = currentPage,
page = page,
type = ListingType.ModeratorView,
)?.let { list ->
if (refreshing) {
@ -305,7 +305,7 @@ class FilteredContentsViewModel(
)
}
if (!itemList.isNullOrEmpty()) {
currentPage++
currentPage[FilteredContentsSection.Comments] = page + 1
}
}
}
@ -317,11 +317,12 @@ class FilteredContentsViewModel(
val refreshing = currentState.refreshing
if (currentState.section == FilteredContentsSection.Posts) {
val page = currentPage[FilteredContentsSection.Posts] ?: 1
coroutineScope {
val itemList = async {
userRepository.getLikedPosts(
auth = auth,
page = currentPage,
page = page,
pageCursor = pageCursor,
liked = currentState.liked,
sort = SortType.New,
@ -346,12 +347,12 @@ class FilteredContentsViewModel(
}
}.await()
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
// the lazy column scrolls back to top (it must have an empty data set)
userRepository.getLikedComments(
auth = auth,
page = currentPage,
page = 1,
liked = currentState.liked,
sort = SortType.New,
).orEmpty()
@ -359,9 +360,6 @@ class FilteredContentsViewModel(
currentState.comments
}
}.await()
if (!itemList.isNullOrEmpty()) {
currentPage++
}
val itemsToAdd = itemList.orEmpty().filter { post ->
!post.deleted
}
@ -388,13 +386,14 @@ class FilteredContentsViewModel(
)
}
if (!itemList.isNullOrEmpty()) {
currentPage++
currentPage[FilteredContentsSection.Posts] = page + 1
}
}
} else {
val page = currentPage[FilteredContentsSection.Comments] ?: 1
val itemList = userRepository.getLikedComments(
auth = auth,
page = currentPage,
page = page,
liked = currentState.liked,
sort = SortType.New,
)?.let { list ->
@ -426,7 +425,7 @@ class FilteredContentsViewModel(
)
}
if (!itemList.isNullOrEmpty()) {
currentPage++
currentPage[FilteredContentsSection.Comments] = page + 1
}
}
}

View File

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

View File

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

View File

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