feat: multi-communities (#36)

* chore: add persistence

* chore: move serializable to core-utils

* fix: leftover color in user detail

* chore: new messages

* feat: add multi-communities in subscription list

* feat: add multi-community editor

* feat: add multi-community detail
This commit is contained in:
Diego Beraldin 2023-10-05 21:18:32 +02:00 committed by GitHub
parent 8414cf5019
commit abc9b3632a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
49 changed files with 1846 additions and 50 deletions

View File

@ -0,0 +1,72 @@
package com.github.diegoberaldin.raccoonforlemmy.core.commonui.components
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.unit.dp
import com.github.diegoberaldin.raccoonforlemmy.core.appearance.theme.Spacing
import com.github.diegoberaldin.raccoonforlemmy.core.persistence.data.MultiCommunityModel
@Composable
fun MultiCommunityItem(
community: MultiCommunityModel,
modifier: Modifier = Modifier,
) {
val title = community.name
val communityIcon = community.icon.orEmpty()
val iconSize = 30.dp
Row(
modifier = modifier.padding(
vertical = Spacing.xs,
horizontal = Spacing.s,
),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(Spacing.xs),
) {
if (communityIcon.isNotEmpty()) {
CustomImage(
modifier = Modifier.padding(Spacing.xxxs).size(iconSize)
.clip(RoundedCornerShape(iconSize / 2)),
url = communityIcon,
contentDescription = null,
contentScale = ContentScale.FillBounds,
)
} else {
Box(
modifier = Modifier.padding(Spacing.xxxs).size(iconSize)
.background(
color = MaterialTheme.colorScheme.primary,
shape = RoundedCornerShape(iconSize / 2),
),
contentAlignment = Alignment.Center,
) {
Text(
text = title.firstOrNull()?.toString().orEmpty().uppercase(),
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onPrimary,
)
}
}
Column {
Text(
modifier = Modifier.padding(vertical = Spacing.s),
text = buildString {
append(title)
},
style = MaterialTheme.typography.bodyLarge,
)
}
}
}

View File

@ -438,7 +438,7 @@ class UserDetailScreen(
},
backgroundColor = {
when (it) {
DismissValue.DismissedToStart -> MaterialTheme.colorScheme.secondary
DismissValue.DismissedToStart -> MaterialTheme.colorScheme.surfaceTint
DismissValue.DismissedToEnd -> MaterialTheme.colorScheme.tertiary
else -> Color.Transparent
}

View File

@ -15,4 +15,5 @@ object NotificationCenterContractKeys {
const val PostUpdated = "postUpdated"
const val PostDeleted = "postDeleted"
const val ChangeColor = "changeColor"
const val MultiCommunityCreated = "multiCommunityCreated"
}

View File

@ -48,6 +48,7 @@ kotlin {
implementation(libs.koin.core)
implementation(projects.corePreferences)
implementation(projects.coreUtils)
}
}
val commonTest by getting {

View File

@ -1,5 +1,7 @@
package com.github.diegoberaldin.raccoonforlemmy.core.persistence.data
import com.github.diegoberaldin.raccoonforlemmy.core.utils.JavaSerializable
data class AccountModel(
val id: Long? = null,
val username: String,
@ -7,4 +9,4 @@ data class AccountModel(
val instance: String,
val jwt: String,
val active: Boolean = false,
)
) : JavaSerializable

View File

@ -0,0 +1,10 @@
package com.github.diegoberaldin.raccoonforlemmy.core.persistence.data
import com.github.diegoberaldin.raccoonforlemmy.core.utils.JavaSerializable
data class MultiCommunityModel(
val id: Long? = null,
val name: String = "",
val communityIds: List<Int> = emptyList(),
val icon: String? = null,
) : JavaSerializable

View File

@ -1,5 +1,7 @@
package com.github.diegoberaldin.raccoonforlemmy.core.persistence.data
import com.github.diegoberaldin.raccoonforlemmy.core.utils.JavaSerializable
data class SettingsModel(
val id: Long? = null,
val theme: Int? = null,
@ -16,4 +18,4 @@ data class SettingsModel(
val enableSwipeActions: Boolean = true,
val customSeedColor: Int? = null,
val postLayout: Int = 0,
)
) : JavaSerializable

View File

@ -4,7 +4,9 @@ import com.github.diegoberaldin.raccoonforlemmy.core.persistence.DatabaseProvide
import com.github.diegoberaldin.raccoonforlemmy.core.persistence.DefaultDatabaseProvider
import com.github.diegoberaldin.raccoonforlemmy.core.persistence.repository.AccountRepository
import com.github.diegoberaldin.raccoonforlemmy.core.persistence.repository.DefaultAccountRepository
import com.github.diegoberaldin.raccoonforlemmy.core.persistence.repository.DefaultMultiCommunityRepository
import com.github.diegoberaldin.raccoonforlemmy.core.persistence.repository.DefaultSettingsRepository
import com.github.diegoberaldin.raccoonforlemmy.core.persistence.repository.MultiCommunityRepository
import com.github.diegoberaldin.raccoonforlemmy.core.persistence.repository.SettingsRepository
import org.koin.dsl.module
@ -26,4 +28,9 @@ val corePersistenceModule = module {
keyStore = get(),
)
}
single<MultiCommunityRepository> {
DefaultMultiCommunityRepository(
provider = get()
)
}
}

View File

@ -0,0 +1,60 @@
package com.github.diegoberaldin.raccoonforlemmy.core.persistence.repository
import com.github.diegoberaldin.raccoonforlemmy.core.persistence.DatabaseProvider
import com.github.diegoberaldin.raccoonforlemmy.core.persistence.MultiCommunityEntity
import com.github.diegoberaldin.raccoonforlemmy.core.persistence.data.MultiCommunityModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.IO
import kotlinx.coroutines.withContext
internal class DefaultMultiCommunityRepository(
val provider: DatabaseProvider,
) : MultiCommunityRepository {
private val db = provider.getDatabase()
override suspend fun getAll(accountId: Long?): List<MultiCommunityModel> =
withContext(Dispatchers.IO) {
db.multicommunitiesQueries.getAll(accountId)
.executeAsList().map { it.toModel() }
}
override suspend fun create(model: MultiCommunityModel, accountId: Long): Long =
withContext(Dispatchers.IO) {
db.multicommunitiesQueries.create(
name = model.name,
icon = model.icon,
communityIds = model.communityIds.joinToString(","),
account_id = accountId,
)
val id = db.multicommunitiesQueries.getBy(name = model.name, account_id = accountId)
.executeAsOneOrNull()?.id
id ?: 0L
}
override suspend fun update(model: MultiCommunityModel) =
withContext(Dispatchers.IO) {
db.multicommunitiesQueries.update(
name = model.name,
icon = model.icon,
communityIds = model.communityIds.joinToString(","),
id = model.id ?: 0,
)
}
override suspend fun delete(model: MultiCommunityModel) =
withContext(Dispatchers.IO) {
db.multicommunitiesQueries.delete(model.id ?: 0)
}
}
private fun MultiCommunityEntity.toModel() = MultiCommunityModel(
id = id,
name = name,
icon = icon,
communityIds = communityIds
.split(",")
.map { it.trim() }
.filter { it.isNotEmpty() }
.map { it.toInt() }
)

View File

@ -26,7 +26,7 @@ private object KeyStoreKeys {
const val PostLayout = "postLayout"
}
class DefaultSettingsRepository(
internal class DefaultSettingsRepository(
val provider: DatabaseProvider,
private val keyStore: TemporaryKeyStore,
) : SettingsRepository {
@ -37,7 +37,7 @@ class DefaultSettingsRepository(
override suspend fun createSettings(settings: SettingsModel, accountId: Long) =
withContext(Dispatchers.IO) {
db.settingsQueries.createSettings(
db.settingsQueries.create(
theme = settings.theme?.toLong(),
contentFontScale = settings.contentFontScale.toDouble(),
locale = settings.locale,
@ -77,7 +77,7 @@ class DefaultSettingsRepository(
postLayout = keyStore[KeyStoreKeys.PostLayout, 0],
)
} else {
db.settingsQueries.getSettings(accountId)
db.settingsQueries.getBy(accountId)
.executeAsOneOrNull()?.toModel() ?: SettingsModel()
}
}
@ -110,7 +110,7 @@ class DefaultSettingsRepository(
}
keyStore.save(KeyStoreKeys.PostLayout, settings.postLayout)
} else {
db.settingsQueries.updateSettings(
db.settingsQueries.update(
theme = settings.theme?.toLong(),
contentFontScale = settings.contentFontScale.toDouble(),
locale = settings.locale,

View File

@ -0,0 +1,13 @@
package com.github.diegoberaldin.raccoonforlemmy.core.persistence.repository
import com.github.diegoberaldin.raccoonforlemmy.core.persistence.data.MultiCommunityModel
interface MultiCommunityRepository {
suspend fun getAll(accountId: Long?): List<MultiCommunityModel>
suspend fun create(model: MultiCommunityModel, accountId: Long): Long
suspend fun update(model: MultiCommunityModel)
suspend fun delete(model: MultiCommunityModel)
}

View File

@ -0,0 +1,8 @@
CREATE TABLE MultiCommunityEntity (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL DEFAULT "",
icon TEXT DEFAULT NULL,
communityIds TEXT NOT NULL DEFAULT "",
account_id INTEGER,
FOREIGN KEY (account_id) REFERENCES AccountEntity(id) ON DELETE CASCADE
);

View File

@ -0,0 +1,44 @@
CREATE TABLE MultiCommunityEntity (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL DEFAULT "",
icon TEXT DEFAULT NULL,
communityIds TEXT NOT NULL DEFAULT "",
account_id INTEGER,
FOREIGN KEY (account_id) REFERENCES AccountEntity(id) ON DELETE CASCADE,
UNIQUE(name, account_id)
);
getAll:
SELECT *
FROM MultiCommunityEntity
WHERE account_id = ?;
getBy:
SELECT *
FROM MultiCommunityEntity
WHERE name = ? AND account_id = ?;
create:
INSERT OR IGNORE INTO MultiCommunityEntity (
name,
icon,
communityIds,
account_id
) VALUES (
?,
?,
?,
?
);
update:
UPDATE OR IGNORE MultiCommunityEntity
SET
name = ?,
icon = ?,
communityIds = ?
WHERE id = ?;
delete:
DELETE FROM MultiCommunityEntity
WHERE id = ?;

View File

@ -19,7 +19,7 @@ CREATE TABLE SettingsEntity (
UNIQUE(account_id)
);
createSettings:
create:
INSERT OR IGNORE INTO SettingsEntity (
theme,
contentFontScale,
@ -54,7 +54,7 @@ INSERT OR IGNORE INTO SettingsEntity (
?
);
updateSettings:
update:
UPDATE SettingsEntity
SET theme = ?,
contentFontScale = ?,
@ -72,7 +72,7 @@ SET theme = ?,
postLayout = ?
WHERE account_id = ?;
getSettings:
getBy:
SELECT *
FROM SettingsEntity
WHERE account_id = ?;

View File

@ -0,0 +1,3 @@
package com.github.diegoberaldin.raccoonforlemmy.core.utils
actual typealias JavaSerializable = java.io.Serializable

View File

@ -0,0 +1,3 @@
package com.github.diegoberaldin.raccoonforlemmy.core.utils
expect interface JavaSerializable

View File

@ -0,0 +1,3 @@
package com.github.diegoberaldin.raccoonforlemmy.core.utils
actual interface JavaSerializable

View File

@ -31,7 +31,9 @@ kotlin {
implementation(compose.runtime)
implementation(compose.foundation)
implementation(compose.materialIconsExtended)
implementation(projects.resources)
implementation(projects.coreUtils)
}
}
val commonTest by getting {

View File

@ -1,3 +0,0 @@
package com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data
actual typealias JavaSerializable = java.io.Serializable

View File

@ -1,5 +1,7 @@
package com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data
import com.github.diegoberaldin.raccoonforlemmy.core.utils.JavaSerializable
data class CommentModel(
val id: Int = 0,
val postId: Int = 0,

View File

@ -1,5 +1,7 @@
package com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data
import com.github.diegoberaldin.raccoonforlemmy.core.utils.JavaSerializable
data class CommunityModel(
val id: Int = 0,
val name: String = "",

View File

@ -1,6 +1,8 @@
package com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data
import com.github.diegoberaldin.raccoonforlemmy.core.utils.JavaSerializable
data class MetadataModel(
val title: String = "",
val description: String = "",
): JavaSerializable
) : JavaSerializable

View File

@ -1,5 +1,7 @@
package com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data
import com.github.diegoberaldin.raccoonforlemmy.core.utils.JavaSerializable
data class PersonMentionModel(
val id: Int = 0,
val post: PostModel,

View File

@ -1,5 +1,7 @@
package com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data
import com.github.diegoberaldin.raccoonforlemmy.core.utils.JavaSerializable
data class PostModel(
val id: Int = 0,
val title: String = "",

View File

@ -1,5 +1,7 @@
package com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data
import com.github.diegoberaldin.raccoonforlemmy.core.utils.JavaSerializable
data class PrivateMessageModel(
val id: Int = 0,
val content: String? = null,
@ -8,4 +10,4 @@ data class PrivateMessageModel(
val publishDate: String? = null,
val updateDate: String? = null,
val read: Boolean = false,
)
) : JavaSerializable

View File

@ -1,5 +1,7 @@
package com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data
import com.github.diegoberaldin.raccoonforlemmy.core.utils.JavaSerializable
data class UserModel(
val id: Int = 0,
val name: String = "",

View File

@ -1,5 +1,7 @@
package com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data
import com.github.diegoberaldin.raccoonforlemmy.core.utils.JavaSerializable
data class UserScoreModel(
val postScore: Int = 0,
val commentScore: Int = 0,

View File

@ -1,3 +0,0 @@
package com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data
expect interface JavaSerializable

View File

@ -1,3 +0,0 @@
package com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data
actual interface JavaSerializable

View File

@ -1,3 +0,0 @@
package com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data
actual interface JavaSerializable

View File

@ -1,7 +1,11 @@
package com.github.diegoberaldin.raccoonforlemmy.feature.search.di
import com.github.diegoberaldin.raccoonforlemmy.core.persistence.data.MultiCommunityModel
import com.github.diegoberaldin.raccoonforlemmy.feature.search.main.ExploreViewModel
import com.github.diegoberaldin.raccoonforlemmy.feature.search.managesubscriptions.ManageSubscriptionsViewModel
import com.github.diegoberaldin.raccoonforlemmy.feature.search.multicommunity.detail.MultiCommunityViewModel
import com.github.diegoberaldin.raccoonforlemmy.feature.search.multicommunity.editor.MultiCommunityEditorViewModel
import org.koin.core.parameter.parametersOf
import org.koin.java.KoinJavaComponent.inject
actual fun getExploreViewModel(): ExploreViewModel {
@ -14,3 +18,18 @@ actual fun getManageSubscriptionsViewModel(): ManageSubscriptionsViewModel {
return res
}
actual fun getMultiCommunityViewModel(community: MultiCommunityModel): MultiCommunityViewModel {
val res: MultiCommunityViewModel by inject(
MultiCommunityViewModel::class.java,
parameters = { parametersOf(community) }
)
return res
}
actual fun getMultiCommunityEditorViewModel(editedCommunity: MultiCommunityModel?): MultiCommunityEditorViewModel {
val res: MultiCommunityEditorViewModel by inject(
MultiCommunityEditorViewModel::class.java,
parameters = { parametersOf(editedCommunity) }
)
return res
}

View File

@ -5,6 +5,12 @@ import com.github.diegoberaldin.raccoonforlemmy.feature.search.main.ExploreMviMo
import com.github.diegoberaldin.raccoonforlemmy.feature.search.main.ExploreViewModel
import com.github.diegoberaldin.raccoonforlemmy.feature.search.managesubscriptions.ManageSubscriptionsMviModel
import com.github.diegoberaldin.raccoonforlemmy.feature.search.managesubscriptions.ManageSubscriptionsViewModel
import com.github.diegoberaldin.raccoonforlemmy.feature.search.multicommunity.detail.MultiCommunityMviModel
import com.github.diegoberaldin.raccoonforlemmy.feature.search.multicommunity.detail.MultiCommunityViewModel
import com.github.diegoberaldin.raccoonforlemmy.feature.search.multicommunity.editor.MultiCommunityEditorMviModel
import com.github.diegoberaldin.raccoonforlemmy.feature.search.multicommunity.editor.MultiCommunityEditorViewModel
import com.github.diegoberaldin.raccoonforlemmy.feature.search.multicommunity.utils.DefaultMultiCommunityPaginator
import com.github.diegoberaldin.raccoonforlemmy.feature.search.multicommunity.utils.MultiCommunityPaginator
import org.koin.dsl.module
val searchTabModule = module {
@ -27,7 +33,40 @@ val searchTabModule = module {
mvi = DefaultMviModel(ManageSubscriptionsMviModel.UiState()),
identityRepository = get(),
communityRepository = get(),
accountRepository = get(),
multiCommunityRepository = get(),
hapticFeedback = get(),
notificationCenter = get(),
)
}
factory { params ->
MultiCommunityViewModel(
mvi = DefaultMviModel(MultiCommunityMviModel.UiState()),
community = params[0],
postRepository = get(),
identityRepository = get(),
themeRepository = get(),
shareHelper = get(),
settingsRepository = get(),
notificationCenter = get(),
hapticFeedback = get(),
paginator = get(),
)
}
factory<MultiCommunityPaginator> {
DefaultMultiCommunityPaginator(
postRepository = get(),
)
}
factory { params ->
MultiCommunityEditorViewModel(
mvi = DefaultMviModel(MultiCommunityEditorMviModel.UiState()),
editedCommunity = params[0],
identityRepository = get(),
communityRepository = get(),
accountRepository = get(),
multiCommunityRepository = get(),
notificationCenter = get(),
)
}
}

View File

@ -1,8 +1,15 @@
package com.github.diegoberaldin.raccoonforlemmy.feature.search.di
import com.github.diegoberaldin.raccoonforlemmy.core.persistence.data.MultiCommunityModel
import com.github.diegoberaldin.raccoonforlemmy.feature.search.main.ExploreViewModel
import com.github.diegoberaldin.raccoonforlemmy.feature.search.managesubscriptions.ManageSubscriptionsViewModel
import com.github.diegoberaldin.raccoonforlemmy.feature.search.multicommunity.detail.MultiCommunityViewModel
import com.github.diegoberaldin.raccoonforlemmy.feature.search.multicommunity.editor.MultiCommunityEditorViewModel
expect fun getExploreViewModel(): ExploreViewModel
expect fun getManageSubscriptionsViewModel(): ManageSubscriptionsViewModel
expect fun getMultiCommunityViewModel(community: MultiCommunityModel): MultiCommunityViewModel
expect fun getMultiCommunityEditorViewModel(editedCommunity: MultiCommunityModel?): MultiCommunityEditorViewModel

View File

@ -1,6 +1,7 @@
package com.github.diegoberaldin.raccoonforlemmy.feature.search.managesubscriptions
import com.github.diegoberaldin.raccoonforlemmy.core.architecture.MviModel
import com.github.diegoberaldin.raccoonforlemmy.core.persistence.data.MultiCommunityModel
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.CommunityModel
interface ManageSubscriptionsMviModel :
@ -8,12 +9,13 @@ interface ManageSubscriptionsMviModel :
sealed interface Intent {
data object Refresh : Intent
data object HapticIndication : Intent
data class Unsubscribe(val index: Int) : Intent
data class DeleteMultiCommunity(val index: Int) : Intent
}
data class UiState(
val refreshing: Boolean = false,
val multiCommunities: List<MultiCommunityModel> = emptyList(),
val communities: List<CommunityModel> = emptyList(),
)

View File

@ -4,18 +4,22 @@ import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.DismissDirection
import androidx.compose.material.DismissValue
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.Icon
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.AddCircle
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material.icons.filled.Edit
import androidx.compose.material.icons.filled.Unsubscribe
import androidx.compose.material.pullrefresh.PullRefreshIndicator
import androidx.compose.material.pullrefresh.pullRefresh
@ -41,10 +45,13 @@ import com.github.diegoberaldin.raccoonforlemmy.core.appearance.theme.Spacing
import com.github.diegoberaldin.raccoonforlemmy.core.architecture.bindToLifecycle
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.communitydetail.CommunityDetailScreen
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.components.CommunityItem
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.components.MultiCommunityItem
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.components.SwipeableCard
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.di.getNavigationCoordinator
import com.github.diegoberaldin.raccoonforlemmy.core.utils.onClick
import com.github.diegoberaldin.raccoonforlemmy.feature.search.di.getManageSubscriptionsViewModel
import com.github.diegoberaldin.raccoonforlemmy.feature.search.multicommunity.detail.MultiCommunityScreen
import com.github.diegoberaldin.raccoonforlemmy.feature.search.multicommunity.editor.MultiCommunityEditorScreen
import com.github.diegoberaldin.raccoonforlemmy.resources.MR
import dev.icerock.moko.resources.compose.stringResource
@ -81,15 +88,11 @@ class ManageSubscriptionsScreen : Screen {
)
},
) { paddingValues ->
val pullRefreshState = rememberPullRefreshState(
uiState.refreshing,
{
model.reduce(ManageSubscriptionsMviModel.Intent.Refresh)
}
)
val pullRefreshState = rememberPullRefreshState(uiState.refreshing, {
model.reduce(ManageSubscriptionsMviModel.Intent.Refresh)
})
Box(
modifier = Modifier
.padding(paddingValues)
modifier = Modifier.padding(paddingValues)
.nestedScroll(scrollBehavior.nestedScrollConnection)
.pullRefresh(pullRefreshState),
) {
@ -97,6 +100,87 @@ class ManageSubscriptionsScreen : Screen {
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.spacedBy(Spacing.xxs),
) {
item {
Row(
modifier = Modifier.fillMaxWidth().padding(horizontal = Spacing.s),
verticalAlignment = Alignment.CenterVertically,
) {
Text(
text = stringResource(MR.strings.manage_subscriptions_header_multicommunities),
style = MaterialTheme.typography.titleLarge,
color = MaterialTheme.colorScheme.onBackground,
)
Spacer(modifier = Modifier.weight(1f))
Icon(
modifier = Modifier.onClick {
navigator?.push(MultiCommunityEditorScreen())
},
imageVector = Icons.Default.AddCircle,
contentDescription = null,
tint = MaterialTheme.colorScheme.onBackground,
)
}
}
itemsIndexed(uiState.multiCommunities) { idx, community ->
SwipeableCard(
modifier = Modifier.fillMaxWidth(),
backgroundColor = {
when (it) {
DismissValue.DismissedToStart -> MaterialTheme.colorScheme.surfaceTint
DismissValue.DismissedToEnd -> MaterialTheme.colorScheme.tertiary
else -> Color.Transparent
}
},
onGestureBegin = {
model.reduce(ManageSubscriptionsMviModel.Intent.HapticIndication)
},
onDismissToStart = {
navigator?.push(
MultiCommunityEditorScreen(community),
)
},
onDismissToEnd = {
model.reduce(
ManageSubscriptionsMviModel.Intent.DeleteMultiCommunity(idx),
)
},
swipeContent = { direction ->
val icon = when (direction) {
DismissDirection.StartToEnd -> Icons.Default.Delete
DismissDirection.EndToStart -> Icons.Default.Edit
}
Icon(
modifier = Modifier.padding(Spacing.xs),
imageVector = icon,
contentDescription = null,
tint = Color.White,
)
},
content = {
MultiCommunityItem(
modifier = Modifier.fillMaxWidth()
.background(MaterialTheme.colorScheme.background).onClick {
navigator?.push(
MultiCommunityScreen(community),
)
},
community = community,
)
},
)
}
item {
Row(
modifier = Modifier.fillMaxWidth().padding(horizontal = Spacing.s),
verticalAlignment = Alignment.CenterVertically,
) {
Text(
text = stringResource(MR.strings.manage_subscriptions_header_subscriptions),
style = MaterialTheme.typography.titleLarge,
color = MaterialTheme.colorScheme.onBackground,
)
}
}
itemsIndexed(uiState.communities) { idx, community ->
SwipeableCard(
modifier = Modifier.fillMaxWidth(),
@ -117,21 +201,16 @@ class ManageSubscriptionsScreen : Screen {
},
swipeContent = { _ ->
Icon(
modifier = Modifier.background(
color = MaterialTheme.colorScheme.onSecondary,
shape = CircleShape,
).padding(Spacing.xs),
modifier = Modifier.padding(Spacing.xs),
imageVector = Icons.Default.Unsubscribe,
contentDescription = null,
tint = MaterialTheme.colorScheme.secondary,
tint = Color.White,
)
},
content = {
CommunityItem(
modifier = Modifier
.fillMaxWidth()
.background(MaterialTheme.colorScheme.background)
.onClick {
modifier = Modifier.fillMaxWidth()
.background(MaterialTheme.colorScheme.background).onClick {
navigator?.push(
CommunityDetailScreen(community),
)

View File

@ -3,6 +3,11 @@ package com.github.diegoberaldin.raccoonforlemmy.feature.search.managesubscripti
import cafe.adriel.voyager.core.model.ScreenModel
import com.github.diegoberaldin.raccoonforlemmy.core.architecture.DefaultMviModel
import com.github.diegoberaldin.raccoonforlemmy.core.architecture.MviModel
import com.github.diegoberaldin.raccoonforlemmy.core.notifications.NotificationCenter
import com.github.diegoberaldin.raccoonforlemmy.core.notifications.NotificationCenterContractKeys
import com.github.diegoberaldin.raccoonforlemmy.core.persistence.data.MultiCommunityModel
import com.github.diegoberaldin.raccoonforlemmy.core.persistence.repository.AccountRepository
import com.github.diegoberaldin.raccoonforlemmy.core.persistence.repository.MultiCommunityRepository
import com.github.diegoberaldin.raccoonforlemmy.core.utils.HapticFeedback
import com.github.diegoberaldin.raccoonforlemmy.domain.identity.repository.IdentityRepository
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.CommunityModel
@ -15,10 +20,29 @@ class ManageSubscriptionsViewModel(
private val mvi: DefaultMviModel<ManageSubscriptionsMviModel.Intent, ManageSubscriptionsMviModel.UiState, ManageSubscriptionsMviModel.Effect>,
private val identityRepository: IdentityRepository,
private val communityRepository: CommunityRepository,
private val accountRepository: AccountRepository,
private val multiCommunityRepository: MultiCommunityRepository,
private val hapticFeedback: HapticFeedback,
private val notificationCenter: NotificationCenter,
) : ScreenModel,
MviModel<ManageSubscriptionsMviModel.Intent, ManageSubscriptionsMviModel.UiState, ManageSubscriptionsMviModel.Effect> by mvi {
init {
notificationCenter.addObserver(
{ evt ->
(evt as? MultiCommunityModel)?.also {
handleMultiCommunityCreated(it)
}
},
this::class.simpleName.orEmpty(),
NotificationCenterContractKeys.MultiCommunityCreated
)
}
fun finalize() {
notificationCenter.removeObserver(this::class.simpleName.orEmpty())
}
override fun onStarted() {
mvi.onStarted()
if (uiState.value.communities.isEmpty()) {
@ -31,7 +55,11 @@ class ManageSubscriptionsViewModel(
ManageSubscriptionsMviModel.Intent.HapticIndication -> hapticFeedback.vibrate()
ManageSubscriptionsMviModel.Intent.Refresh -> refresh()
is ManageSubscriptionsMviModel.Intent.Unsubscribe -> handleUnsubscription(
community = uiState.value.communities[intent.index]
community = uiState.value.communities[intent.index],
)
is ManageSubscriptionsMviModel.Intent.DeleteMultiCommunity -> deleteMultiCommunity(
community = uiState.value.multiCommunities[intent.index],
)
}
}
@ -43,11 +71,15 @@ class ManageSubscriptionsViewModel(
mvi.updateState { it.copy(refreshing = true) }
mvi.scope?.launch(Dispatchers.IO) {
val auth = identityRepository.authToken.value
val items = communityRepository.getSubscribed(auth).sortedBy { it.name }
val communities = communityRepository.getSubscribed(auth).sortedBy { it.name }
val accountId = accountRepository.getActive()?.id ?: 0L
val multiCommunitites = multiCommunityRepository.getAll(accountId).sortedBy { it.name }
mvi.updateState {
it.copy(
refreshing = false,
communities = items,
communities = communities,
multiCommunities = multiCommunitites,
)
}
}
@ -64,4 +96,30 @@ class ManageSubscriptionsViewModel(
}
}
}
private fun deleteMultiCommunity(community: MultiCommunityModel) {
mvi.scope?.launch(Dispatchers.IO) {
multiCommunityRepository.delete(community)
mvi.updateState {
val newCommunities = it.multiCommunities.filter { c -> c.id != community.id }
it.copy(multiCommunities = newCommunities)
}
}
}
private fun handleMultiCommunityCreated(community: MultiCommunityModel) {
val oldCommunities = uiState.value.multiCommunities
val newCommunities = if (oldCommunities.any { it.id == community.id }) {
oldCommunities.map {
if (it.id == community.id) {
community
} else {
it
}
}
} else {
oldCommunities + community
}.sortedBy { it.name }
mvi.updateState { it.copy(multiCommunities = newCommunities) }
}
}

View File

@ -0,0 +1,35 @@
package com.github.diegoberaldin.raccoonforlemmy.feature.search.multicommunity.detail
import com.github.diegoberaldin.raccoonforlemmy.core.appearance.data.PostLayout
import com.github.diegoberaldin.raccoonforlemmy.core.architecture.MviModel
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.PostModel
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.SortType
interface MultiCommunityMviModel :
MviModel<MultiCommunityMviModel.Intent, MultiCommunityMviModel.UiState, MultiCommunityMviModel.Effect> {
sealed interface Intent {
data object Refresh : Intent
data object LoadNextPage : Intent
data class ChangeSort(val value: SortType) : Intent
data object HapticIndication : Intent
data class UpVotePost(val index: Int, val feedback: Boolean = false) : Intent
data class DownVotePost(val index: Int, val feedback: Boolean = false) : Intent
data class SavePost(val index: Int, val feedback: Boolean = false) : Intent
data class SharePost(val index: Int) : Intent
}
data class UiState(
val refreshing: Boolean = false,
val loading: Boolean = false,
val canFetchMore: Boolean = true,
val instance: String = "",
val isLogged: Boolean = false,
val sortType: SortType? = null,
val posts: List<PostModel> = emptyList(),
val blurNsfw: Boolean = true,
val swipeActionsEnabled: Boolean = true,
val postLayout: PostLayout = PostLayout.Card,
)
sealed interface Effect
}

View File

@ -0,0 +1,346 @@
package com.github.diegoberaldin.raccoonforlemmy.feature.search.multicommunity.detail
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.material.DismissDirection
import androidx.compose.material.DismissValue
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.Icon
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material.icons.filled.ArrowCircleDown
import androidx.compose.material.icons.filled.ArrowCircleUp
import androidx.compose.material.pullrefresh.PullRefreshIndicator
import androidx.compose.material.pullrefresh.pullRefresh
import androidx.compose.material.pullrefresh.rememberPullRefreshState
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.Divider
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.dp
import cafe.adriel.voyager.core.model.rememberScreenModel
import cafe.adriel.voyager.core.screen.Screen
import cafe.adriel.voyager.navigator.bottomSheet.LocalBottomSheetNavigator
import com.github.diegoberaldin.raccoonforlemmy.core.appearance.data.PostLayout
import com.github.diegoberaldin.raccoonforlemmy.core.appearance.di.getThemeRepository
import com.github.diegoberaldin.raccoonforlemmy.core.appearance.theme.Spacing
import com.github.diegoberaldin.raccoonforlemmy.core.architecture.bindToLifecycle
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.communitydetail.CommunityDetailScreen
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.components.PostCard
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.components.PostCardPlaceholder
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.components.SwipeableCard
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.createcomment.CreateCommentScreen
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.di.getNavigationCoordinator
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.image.ZoomableImageScreen
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.modals.SortBottomSheet
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.postdetail.PostDetailScreen
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.userdetail.UserDetailScreen
import com.github.diegoberaldin.raccoonforlemmy.core.notifications.NotificationCenterContractKeys
import com.github.diegoberaldin.raccoonforlemmy.core.notifications.di.getNotificationCenter
import com.github.diegoberaldin.raccoonforlemmy.core.persistence.data.MultiCommunityModel
import com.github.diegoberaldin.raccoonforlemmy.core.utils.onClick
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.SortType
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.toIcon
import com.github.diegoberaldin.raccoonforlemmy.feature.search.di.getMultiCommunityViewModel
import com.github.diegoberaldin.raccoonforlemmy.resources.MR
import dev.icerock.moko.resources.compose.stringResource
class MultiCommunityScreen(
private val community: MultiCommunityModel,
) : Screen {
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterialApi::class)
@Composable
override fun Content() {
val model = rememberScreenModel { getMultiCommunityViewModel(community) }
model.bindToLifecycle(key)
val uiState by model.uiState.collectAsState()
val bottomSheetNavigator = LocalBottomSheetNavigator.current
val scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior()
val bottomNavCoordinator = remember { getNavigationCoordinator() }
val navigator = remember { bottomNavCoordinator.getRootNavigator() }
val notificationCenter = remember { getNotificationCenter() }
DisposableEffect(key) {
onDispose {
notificationCenter.removeObserver(key)
}
}
Scaffold(
topBar = {
val sortType = uiState.sortType
TopAppBar(
title = {
Text(
text = community.name,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
},
scrollBehavior = scrollBehavior,
navigationIcon = {
Image(
modifier = Modifier.onClick {
navigator?.pop()
},
imageVector = Icons.Default.ArrowBack,
contentDescription = null,
colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.onBackground),
)
},
actions = {
Row {
val additionalLabel = when (sortType) {
SortType.Top.Day -> stringResource(MR.strings.home_sort_type_top_day_short)
SortType.Top.Month -> stringResource(MR.strings.home_sort_type_top_month_short)
SortType.Top.Past12Hours -> stringResource(MR.strings.home_sort_type_top_12_hours_short)
SortType.Top.Past6Hours -> stringResource(MR.strings.home_sort_type_top_6_hours_short)
SortType.Top.PastHour -> stringResource(MR.strings.home_sort_type_top_hour_short)
SortType.Top.Week -> stringResource(MR.strings.home_sort_type_top_week_short)
SortType.Top.Year -> stringResource(MR.strings.home_sort_type_top_year_short)
else -> ""
}
if (additionalLabel.isNotEmpty()) {
Text(
text = buildString {
append("(")
append(additionalLabel)
append(")")
}
)
}
if (sortType != null) {
Image(
modifier = Modifier.onClick {
val sheet = SortBottomSheet(
expandTop = true,
)
notificationCenter.addObserver({
(it as? SortType)?.also { sortType ->
model.reduce(
MultiCommunityMviModel.Intent.ChangeSort(
sortType
)
)
}
}, key, NotificationCenterContractKeys.ChangeSortType)
bottomSheetNavigator.show(sheet)
},
imageVector = sortType.toIcon(),
contentDescription = null,
colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.onBackground),
)
}
}
}
)
},
) { padding ->
val pullRefreshState = rememberPullRefreshState(uiState.refreshing, {
model.reduce(MultiCommunityMviModel.Intent.Refresh)
})
Box(
modifier = Modifier
.padding(padding)
.fillMaxWidth()
.nestedScroll(scrollBehavior.nestedScrollConnection).let {
val connection = bottomNavCoordinator.getBottomBarScrollConnection()
if (connection != null) {
it.nestedScroll(connection)
} else it
}
.pullRefresh(pullRefreshState),
) {
LazyColumn {
if (uiState.posts.isEmpty() && uiState.loading) {
items(5) {
PostCardPlaceholder(
postLayout = uiState.postLayout,
)
if (uiState.postLayout != PostLayout.Card) {
Divider(modifier = Modifier.padding(vertical = Spacing.s))
} else {
Spacer(modifier = Modifier.height(Spacing.s))
}
}
}
itemsIndexed(uiState.posts) { idx, post ->
val themeRepository = remember { getThemeRepository() }
val fontScale by themeRepository.contentFontScale.collectAsState()
CompositionLocalProvider(
LocalDensity provides Density(
density = LocalDensity.current.density,
fontScale = fontScale,
),
) {
SwipeableCard(
modifier = Modifier.fillMaxWidth(),
enabled = uiState.swipeActionsEnabled,
backgroundColor = {
when (it) {
DismissValue.DismissedToStart -> MaterialTheme.colorScheme.surfaceTint
DismissValue.DismissedToEnd -> MaterialTheme.colorScheme.tertiary
DismissValue.Default -> Color.Transparent
}
},
onGestureBegin = {
model.reduce(MultiCommunityMviModel.Intent.HapticIndication)
},
onDismissToStart = {
model.reduce(MultiCommunityMviModel.Intent.UpVotePost(idx))
},
onDismissToEnd = {
model.reduce(MultiCommunityMviModel.Intent.DownVotePost(idx))
},
swipeContent = { direction ->
val icon = when (direction) {
DismissDirection.StartToEnd -> Icons.Default.ArrowCircleDown
DismissDirection.EndToStart -> Icons.Default.ArrowCircleUp
}
Icon(
imageVector = icon,
contentDescription = null,
tint = Color.White,
)
},
content = {
PostCard(
modifier = Modifier.onClick {
navigator?.push(
PostDetailScreen(post),
)
},
post = post,
postLayout = uiState.postLayout,
options = buildList {
add(stringResource(MR.strings.post_action_share))
},
blurNsfw = uiState.blurNsfw,
onOpenCommunity = { community ->
navigator?.push(
CommunityDetailScreen(community),
)
},
onOpenCreator = { user ->
navigator?.push(
UserDetailScreen(user),
)
},
onUpVote = {
model.reduce(
MultiCommunityMviModel.Intent.UpVotePost(
index = idx,
feedback = true,
),
)
},
onDownVote = {
model.reduce(
MultiCommunityMviModel.Intent.DownVotePost(
index = idx,
feedback = true,
),
)
},
onSave = {
model.reduce(
MultiCommunityMviModel.Intent.SavePost(
index = idx,
feedback = true,
),
)
},
onReply = {
val screen = CreateCommentScreen(
originalPost = post,
)
notificationCenter.addObserver(
{
model.reduce(MultiCommunityMviModel.Intent.Refresh)
},
key,
NotificationCenterContractKeys.CommentCreated
)
bottomSheetNavigator.show(screen)
},
onImageClick = { url ->
navigator?.push(
ZoomableImageScreen(url),
)
},
onOptionSelected = { optionIdx ->
when (optionIdx) {
else -> model.reduce(
MultiCommunityMviModel.Intent.SharePost(idx)
)
}
}
)
},
)
if (uiState.postLayout != PostLayout.Card) {
Divider(modifier = Modifier.padding(vertical = Spacing.s))
} else {
Spacer(modifier = Modifier.height(Spacing.s))
}
}
}
item {
if (!uiState.loading && !uiState.refreshing && uiState.canFetchMore) {
model.reduce(MultiCommunityMviModel.Intent.LoadNextPage)
}
if (uiState.loading && !uiState.refreshing) {
Box(
modifier = Modifier.fillMaxWidth().padding(Spacing.xs),
contentAlignment = Alignment.Center,
) {
CircularProgressIndicator(
modifier = Modifier.size(25.dp),
color = MaterialTheme.colorScheme.primary,
)
}
}
}
item {
Spacer(modifier = Modifier.height(Spacing.xxxl))
}
}
PullRefreshIndicator(
refreshing = uiState.refreshing,
state = pullRefreshState,
modifier = Modifier.align(Alignment.TopCenter),
backgroundColor = MaterialTheme.colorScheme.background,
contentColor = MaterialTheme.colorScheme.onBackground,
)
}
}
}
}

View File

@ -0,0 +1,316 @@
package com.github.diegoberaldin.raccoonforlemmy.feature.search.multicommunity.detail
import cafe.adriel.voyager.core.model.ScreenModel
import com.github.diegoberaldin.raccoonforlemmy.core.appearance.repository.ThemeRepository
import com.github.diegoberaldin.raccoonforlemmy.core.architecture.DefaultMviModel
import com.github.diegoberaldin.raccoonforlemmy.core.architecture.MviModel
import com.github.diegoberaldin.raccoonforlemmy.core.notifications.NotificationCenter
import com.github.diegoberaldin.raccoonforlemmy.core.notifications.NotificationCenterContractKeys
import com.github.diegoberaldin.raccoonforlemmy.core.persistence.data.MultiCommunityModel
import com.github.diegoberaldin.raccoonforlemmy.core.persistence.repository.SettingsRepository
import com.github.diegoberaldin.raccoonforlemmy.core.utils.HapticFeedback
import com.github.diegoberaldin.raccoonforlemmy.core.utils.ShareHelper
import com.github.diegoberaldin.raccoonforlemmy.domain.identity.repository.IdentityRepository
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.PostModel
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.SortType
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.shareUrl
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.toSortType
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.repository.PostRepository
import com.github.diegoberaldin.raccoonforlemmy.feature.search.multicommunity.utils.MultiCommunityPaginator
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.IO
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
class MultiCommunityViewModel(
private val mvi: DefaultMviModel<MultiCommunityMviModel.Intent, MultiCommunityMviModel.UiState, MultiCommunityMviModel.Effect>,
private val community: MultiCommunityModel,
private val postRepository: PostRepository,
private val identityRepository: IdentityRepository,
private val themeRepository: ThemeRepository,
private val shareHelper: ShareHelper,
private val settingsRepository: SettingsRepository,
private val notificationCenter: NotificationCenter,
private val hapticFeedback: HapticFeedback,
private val paginator: MultiCommunityPaginator,
) : ScreenModel,
MviModel<MultiCommunityMviModel.Intent, MultiCommunityMviModel.UiState, MultiCommunityMviModel.Effect> by mvi {
init {
notificationCenter.addObserver({
(it as? PostModel)?.also { post ->
handlePostUpdate(post)
}
}, this::class.simpleName.orEmpty(), NotificationCenterContractKeys.PostUpdated)
}
fun finalize() {
notificationCenter.removeObserver(this::class.simpleName.orEmpty())
}
override fun onStarted() {
mvi.onStarted()
mvi.scope?.launch {
themeRepository.postLayout.onEach { layout ->
mvi.updateState { it.copy(postLayout = layout) }
}.launchIn(this)
settingsRepository.currentSettings.onEach { settings ->
mvi.updateState {
it.copy(
blurNsfw = settings.blurNsfw,
swipeActionsEnabled = settings.enableSwipeActions,
)
}
}.launchIn(this)
}
mvi.scope?.launch(Dispatchers.IO) {
if (uiState.value.posts.isEmpty()) {
val settings = settingsRepository.currentSettings.value
mvi.updateState {
it.copy(
sortType = settings.defaultPostSortType.toSortType(),
)
}
paginator.setCommunities(community.communityIds)
refresh()
}
}
}
override fun reduce(intent: MultiCommunityMviModel.Intent) {
when (intent) {
is MultiCommunityMviModel.Intent.ChangeSort -> applySortType(intent.value)
is MultiCommunityMviModel.Intent.DownVotePost -> toggleDownVote(
post = uiState.value.posts[intent.index],
feedback = intent.feedback,
)
MultiCommunityMviModel.Intent.HapticIndication -> hapticFeedback.vibrate()
MultiCommunityMviModel.Intent.LoadNextPage -> loadNextPage()
MultiCommunityMviModel.Intent.Refresh -> refresh()
is MultiCommunityMviModel.Intent.SavePost -> toggleSave(
post = uiState.value.posts[intent.index],
feedback = intent.feedback,
)
is MultiCommunityMviModel.Intent.SharePost -> share(post = uiState.value.posts[intent.index])
is MultiCommunityMviModel.Intent.UpVotePost -> toggleUpVote(
post = uiState.value.posts[intent.index],
feedback = intent.feedback,
)
}
}
private fun refresh() {
paginator.reset()
mvi.updateState { it.copy(canFetchMore = true, refreshing = true) }
loadNextPage()
}
private fun loadNextPage() {
val currentState = mvi.uiState.value
if (!currentState.canFetchMore || currentState.loading) {
mvi.updateState { it.copy(refreshing = false) }
return
}
mvi.scope?.launch(Dispatchers.IO) {
mvi.updateState { it.copy(loading = true) }
val auth = identityRepository.authToken.value
val sort = currentState.sortType ?: SortType.Active
val refreshing = currentState.refreshing
val includeNsfw = settingsRepository.currentSettings.value.includeNsfw
val postList = paginator.loadNextPage(
auth = auth,
sort = sort,
)
val canFetchMore = paginator.canFetchMore
mvi.updateState {
val newPosts = if (refreshing) {
postList
} else {
// prevents accidental duplication
it.posts + postList.filter { p -> it.posts.none { e -> e.id == p.id } }
}.filter { post ->
if (includeNsfw) {
true
} else {
!post.nsfw
}
}
it.copy(
posts = newPosts,
loading = false,
canFetchMore = canFetchMore,
refreshing = false,
)
}
}
}
private fun applySortType(value: SortType) {
mvi.updateState { it.copy(sortType = value) }
refresh()
}
private fun toggleUpVote(post: PostModel, feedback: Boolean) {
val newVote = post.myVote <= 0
val newPost = postRepository.asUpVoted(
post = post,
voted = newVote,
)
if (feedback) {
hapticFeedback.vibrate()
}
mvi.updateState {
it.copy(
posts = it.posts.map { p ->
if (p.id == post.id) {
newPost
} else {
p
}
},
)
}
mvi.scope?.launch(Dispatchers.IO) {
try {
val auth = identityRepository.authToken.value.orEmpty()
postRepository.upVote(
post = post,
auth = auth,
voted = newVote,
)
} catch (e: Throwable) {
e.printStackTrace()
mvi.updateState {
it.copy(
posts = it.posts.map { p ->
if (p.id == post.id) {
post
} else {
p
}
},
)
}
}
}
}
private fun toggleDownVote(post: PostModel, feedback: Boolean) {
val newValue = post.myVote >= 0
val newPost = postRepository.asDownVoted(
post = post,
downVoted = newValue,
)
if (feedback) {
hapticFeedback.vibrate()
}
mvi.updateState {
it.copy(
posts = it.posts.map { p ->
if (p.id == post.id) {
newPost
} else {
p
}
},
)
}
mvi.scope?.launch(Dispatchers.IO) {
try {
val auth = identityRepository.authToken.value.orEmpty()
postRepository.downVote(
post = post,
auth = auth,
downVoted = newValue,
)
} catch (e: Throwable) {
e.printStackTrace()
mvi.updateState {
it.copy(
posts = it.posts.map { p ->
if (p.id == post.id) {
post
} else {
p
}
},
)
}
}
}
}
private fun toggleSave(post: PostModel, feedback: Boolean) {
val newValue = !post.saved
val newPost = postRepository.asSaved(
post = post,
saved = newValue,
)
if (feedback) {
hapticFeedback.vibrate()
}
mvi.updateState {
it.copy(
posts = it.posts.map { p ->
if (p.id == post.id) {
newPost
} else {
p
}
},
)
}
mvi.scope?.launch(Dispatchers.IO) {
try {
val auth = identityRepository.authToken.value.orEmpty()
postRepository.save(
post = post,
auth = auth,
saved = newValue,
)
} catch (e: Throwable) {
e.printStackTrace()
mvi.updateState {
it.copy(
posts = it.posts.map { p ->
if (p.id == post.id) {
post
} else {
p
}
},
)
}
}
}
}
private fun handlePostUpdate(post: PostModel) {
mvi.updateState {
it.copy(
posts = it.posts.map { p ->
if (p.id == post.id) {
post
} else {
p
}
},
)
}
}
private fun share(post: PostModel) {
val url = post.shareUrl
if (url.isNotEmpty()) {
shareHelper.share(url, "text/plain")
}
}
}

View File

@ -0,0 +1,29 @@
package com.github.diegoberaldin.raccoonforlemmy.feature.search.multicommunity.editor
import com.github.diegoberaldin.raccoonforlemmy.core.architecture.MviModel
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.CommunityModel
import dev.icerock.moko.resources.desc.StringDesc
interface MultiCommunityEditorMviModel :
MviModel<MultiCommunityEditorMviModel.Intent, MultiCommunityEditorMviModel.UiState, MultiCommunityEditorMviModel.Effect> {
sealed interface Intent {
data class SetName(val value: String) : Intent
data class SetSearch(val value: String) : Intent
data class SelectImage(val index: Int?) : Intent
data class ToggleCommunity(val index: Int) : Intent
data object Submit : Intent
}
data class UiState(
val name: String = "",
val nameError: StringDesc? = null,
val icon: String? = null,
val availableIcons: List<String> = emptyList(),
val communities: List<Pair<CommunityModel, Boolean>> = emptyList(),
val searchText: String = "",
)
sealed interface Effect {
data object Close : Effect
}
}

View File

@ -0,0 +1,336 @@
package com.github.diegoberaldin.raccoonforlemmy.feature.search.multicommunity.editor
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material.icons.filled.CheckCircle
import androidx.compose.material.icons.filled.Clear
import androidx.compose.material.icons.filled.Search
import androidx.compose.material3.Checkbox
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TextField
import androidx.compose.material3.TextFieldDefaults
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.unit.dp
import cafe.adriel.voyager.core.model.rememberScreenModel
import cafe.adriel.voyager.core.screen.Screen
import com.github.diegoberaldin.raccoonforlemmy.core.appearance.theme.Spacing
import com.github.diegoberaldin.raccoonforlemmy.core.architecture.bindToLifecycle
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.components.CommunityItem
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.components.CustomImage
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.di.getNavigationCoordinator
import com.github.diegoberaldin.raccoonforlemmy.core.persistence.data.MultiCommunityModel
import com.github.diegoberaldin.raccoonforlemmy.core.utils.onClick
import com.github.diegoberaldin.raccoonforlemmy.feature.search.di.getMultiCommunityEditorViewModel
import com.github.diegoberaldin.raccoonforlemmy.resources.MR
import dev.icerock.moko.resources.compose.localized
import dev.icerock.moko.resources.compose.stringResource
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
class MultiCommunityEditorScreen(
private val editedCommunity: MultiCommunityModel? = null,
) : Screen {
@OptIn(ExperimentalMaterial3Api::class)
@Composable
override fun Content() {
val model = rememberScreenModel { getMultiCommunityEditorViewModel(editedCommunity) }
model.bindToLifecycle(key)
val uiState by model.uiState.collectAsState()
val navigator = remember { getNavigationCoordinator().getRootNavigator() }
LaunchedEffect(model) {
model.effects.onEach {
when (it) {
MultiCommunityEditorMviModel.Effect.Close -> {
navigator?.pop()
}
}
}.launchIn(this)
}
Scaffold(
topBar = {
TopAppBar(
title = {
Text(
text = stringResource(MR.strings.multi_community_editor_title),
style = MaterialTheme.typography.titleLarge,
color = MaterialTheme.colorScheme.onBackground,
)
},
navigationIcon = {
Image(
modifier = Modifier.onClick {
navigator?.pop()
},
imageVector = Icons.Default.ArrowBack,
contentDescription = null,
colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.onBackground),
)
},
actions = {
IconButton(onClick = {
model.reduce(MultiCommunityEditorMviModel.Intent.Submit)
}) {
Icon(
imageVector = Icons.Default.CheckCircle,
contentDescription = null,
)
}
},
)
},
) { paddingValues ->
val focusManager = LocalFocusManager.current
val keyboardScrollConnection = remember {
object : NestedScrollConnection {
override fun onPreScroll(
available: Offset,
source: NestedScrollSource,
): Offset {
focusManager.clearFocus()
return Offset.Zero
}
}
}
Column(
modifier = Modifier.padding(paddingValues).nestedScroll(keyboardScrollConnection),
verticalArrangement = Arrangement.spacedBy(Spacing.s),
) {
TextField(
modifier = Modifier.fillMaxWidth(),
colors = TextFieldDefaults.colors(
focusedContainerColor = Color.Transparent,
unfocusedContainerColor = Color.Transparent,
disabledContainerColor = Color.Transparent,
),
maxLines = 1,
label = {
Text(text = stringResource(MR.strings.multi_community_editor_name))
},
textStyle = MaterialTheme.typography.bodyMedium,
value = uiState.name,
keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Ascii,
autoCorrect = false,
imeAction = ImeAction.Next,
),
onValueChange = { value ->
model.reduce(MultiCommunityEditorMviModel.Intent.SetName(value))
},
isError = uiState.nameError != null,
supportingText = {
if (uiState.nameError != null) {
Text(
text = uiState.nameError?.localized().orEmpty(),
color = MaterialTheme.colorScheme.error,
)
}
},
)
Spacer(modifier = Modifier.height(Spacing.s))
Column(
modifier = Modifier.padding(horizontal = Spacing.m)
) {
Text(text = stringResource(MR.strings.multi_community_editor_icon))
LazyRow(
horizontalArrangement = Arrangement.spacedBy(Spacing.s),
) {
val iconSize = 40.dp
itemsIndexed(uiState.availableIcons) { idx, url ->
val selected = url == uiState.icon
CustomImage(
modifier = Modifier
.size(iconSize)
.clip(RoundedCornerShape(iconSize / 2))
.let {
if (selected) {
it.border(
width = 1.dp,
color = MaterialTheme.colorScheme.primary,
shape = CircleShape,
).padding(1.dp).border(
width = 1.dp,
color = MaterialTheme.colorScheme.onPrimary,
shape = CircleShape,
).padding(1.dp).border(
width = 1.dp,
color = MaterialTheme.colorScheme.primary,
shape = CircleShape,
)
} else {
it
}
}.onClick {
model.reduce(
MultiCommunityEditorMviModel.Intent.SelectImage(
idx,
)
)
},
url = url,
contentScale = ContentScale.FillBounds,
)
}
item {
val selected = uiState.icon == null
Box(
modifier = Modifier
.padding(Spacing.xxxs)
.size(iconSize)
.background(
color = MaterialTheme.colorScheme.primary,
shape = RoundedCornerShape(iconSize / 2),
).let {
if (selected) {
it.border(
width = 1.dp,
color = MaterialTheme.colorScheme.primary,
shape = CircleShape,
).padding(1.dp).border(
width = 1.dp,
color = MaterialTheme.colorScheme.onPrimary,
shape = CircleShape,
).padding(1.dp).border(
width = 1.dp,
color = MaterialTheme.colorScheme.primary,
shape = CircleShape,
)
} else {
it
}
}.onClick {
model.reduce(
MultiCommunityEditorMviModel.Intent.SelectImage(
null,
)
)
},
contentAlignment = Alignment.Center,
) {
Text(
text = uiState.name.firstOrNull()?.toString().orEmpty()
.uppercase(),
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onPrimary,
)
}
}
}
}
Spacer(modifier = Modifier.height(Spacing.s))
Column {
Text(text = stringResource(MR.strings.multi_community_editor_communities))
// search field
TextField(
modifier = Modifier.fillMaxWidth(),
colors = TextFieldDefaults.colors(
focusedContainerColor = Color.Transparent,
unfocusedContainerColor = Color.Transparent,
disabledContainerColor = Color.Transparent,
),
label = {
Text(text = stringResource(MR.strings.explore_search_placeholder))
},
singleLine = true,
value = uiState.searchText,
keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Text,
),
onValueChange = { value ->
model.reduce(MultiCommunityEditorMviModel.Intent.SetSearch(value))
},
trailingIcon = {
Icon(
modifier = Modifier.onClick {
if (uiState.searchText.isNotEmpty()) {
model.reduce(
MultiCommunityEditorMviModel.Intent.SetSearch("")
)
}
},
imageVector = if (uiState.searchText.isEmpty()) Icons.Default.Search else Icons.Default.Clear,
contentDescription = null,
)
},
)
}
LazyColumn(
modifier = Modifier.fillMaxWidth()
.padding(horizontal = Spacing.m)
.weight(1f),
verticalArrangement = Arrangement.spacedBy(Spacing.xxs),
) {
itemsIndexed(uiState.communities) { idx, communityItem ->
val community = communityItem.first
val selected = communityItem.second
Row(
verticalAlignment = Alignment.CenterVertically,
) {
CommunityItem(
modifier = Modifier.fillMaxWidth()
.background(MaterialTheme.colorScheme.background),
community = community,
)
Checkbox(
checked = selected,
onCheckedChange = {
model.reduce(
MultiCommunityEditorMviModel.Intent.ToggleCommunity(
idx
)
)
},
)
}
}
}
}
}
}
}

View File

@ -0,0 +1,158 @@
package com.github.diegoberaldin.raccoonforlemmy.feature.search.multicommunity.editor
import cafe.adriel.voyager.core.model.ScreenModel
import com.github.diegoberaldin.raccoonforlemmy.core.architecture.DefaultMviModel
import com.github.diegoberaldin.raccoonforlemmy.core.architecture.MviModel
import com.github.diegoberaldin.raccoonforlemmy.core.notifications.NotificationCenter
import com.github.diegoberaldin.raccoonforlemmy.core.notifications.NotificationCenterContractKeys
import com.github.diegoberaldin.raccoonforlemmy.core.persistence.data.MultiCommunityModel
import com.github.diegoberaldin.raccoonforlemmy.core.persistence.repository.AccountRepository
import com.github.diegoberaldin.raccoonforlemmy.core.persistence.repository.MultiCommunityRepository
import com.github.diegoberaldin.raccoonforlemmy.domain.identity.repository.IdentityRepository
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.CommunityModel
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.repository.CommunityRepository
import com.github.diegoberaldin.raccoonforlemmy.resources.MR
import dev.icerock.moko.resources.desc.desc
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.IO
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
class MultiCommunityEditorViewModel(
private val mvi: DefaultMviModel<MultiCommunityEditorMviModel.Intent, MultiCommunityEditorMviModel.UiState, MultiCommunityEditorMviModel.Effect>,
private val editedCommunity: MultiCommunityModel? = null,
private val identityRepository: IdentityRepository,
private val communityRepository: CommunityRepository,
private val multiCommunityRepository: MultiCommunityRepository,
private val accountRepository: AccountRepository,
private val notificationCenter: NotificationCenter,
) : ScreenModel,
MviModel<MultiCommunityEditorMviModel.Intent, MultiCommunityEditorMviModel.UiState, MultiCommunityEditorMviModel.Effect> by mvi {
private var communities: List<Pair<CommunityModel, Boolean>> = emptyList()
private var debounceJob: Job? = null
override fun onStarted() {
mvi.onStarted()
if (communities.isEmpty()) {
populate()
}
}
override fun reduce(intent: MultiCommunityEditorMviModel.Intent) {
when (intent) {
is MultiCommunityEditorMviModel.Intent.SelectImage -> selectImage(intent.index)
is MultiCommunityEditorMviModel.Intent.SetName -> mvi.updateState { it.copy(name = intent.value) }
is MultiCommunityEditorMviModel.Intent.ToggleCommunity -> toggleCommunity(intent.index)
is MultiCommunityEditorMviModel.Intent.SetSearch -> setSearch(intent.value)
MultiCommunityEditorMviModel.Intent.Submit -> submit()
}
}
private fun populate() {
mvi.scope?.launch(Dispatchers.IO) {
val auth = identityRepository.authToken.value
communities = communityRepository.getSubscribed(auth).sortedBy { it.name }.map { c ->
c to (editedCommunity?.communityIds?.contains(c.id) == true)
}
mvi.updateState {
val newCommunities = communities
val availableIcons =
newCommunities.filter { i -> i.second }.mapNotNull { i -> i.first.icon }
it.copy(
communities = newCommunities,
name = editedCommunity?.name.orEmpty(),
icon = editedCommunity?.icon,
availableIcons = availableIcons,
)
}
}
}
private fun setSearch(value: String) {
debounceJob?.cancel()
mvi.updateState { it.copy(searchText = value) }
debounceJob = mvi.scope?.launch(Dispatchers.IO) {
delay(1_000)
filterCommunities()
}
}
private fun filterCommunities() {
val searchText = uiState.value.searchText
val filtered = if (searchText.isNotEmpty()) {
communities.filter { it.first.name.contains(searchText) }
} else {
communities
}
mvi.updateState { it.copy(communities = filtered) }
}
private fun selectImage(index: Int?) {
val image = if (index == null) {
null
} else {
uiState.value.availableIcons[index]
}
mvi.updateState { it.copy(icon = image) }
}
private fun toggleCommunity(index: Int) {
mvi.updateState { state ->
val newCommunities = state.communities.mapIndexed { idx, item ->
if (idx == index) {
item.first to !item.second
} else {
item
}
}
val availableIcons =
newCommunities.filter { i -> i.second }.mapNotNull { i -> i.first.icon }
state.copy(
communities = newCommunities,
availableIcons = availableIcons,
)
}
}
private fun submit() {
mvi.updateState { it.copy(nameError = null) }
val currentState = uiState.value
var valid = true
val name = currentState.name
if (name.isEmpty()) {
mvi.updateState { it.copy(nameError = MR.strings.message_missing_field.desc()) }
valid = false
}
if (!valid) {
return
}
val icon = currentState.icon
val communityIds = currentState.communities.filter { it.second }.map { it.first.id }
val multiCommunity = editedCommunity?.copy(
name = name,
icon = icon,
communityIds = communityIds,
) ?: MultiCommunityModel(
name = name,
icon = icon,
communityIds = communityIds,
)
mvi.scope?.launch(Dispatchers.IO) {
val accountId = accountRepository.getActive()?.id ?: return@launch
if (multiCommunity.id == null) {
val id = multiCommunityRepository.create(multiCommunity, accountId)
notificationCenter.getAllObservers(NotificationCenterContractKeys.MultiCommunityCreated)
.forEach { it.invoke(multiCommunity.copy(id = id)) }
} else {
multiCommunityRepository.update(multiCommunity)
notificationCenter.getAllObservers(NotificationCenterContractKeys.MultiCommunityCreated)
.forEach { it.invoke(multiCommunity) }
}
mvi.emitEffect(MultiCommunityEditorMviModel.Effect.Close)
}
}
}

View File

@ -0,0 +1,36 @@
package com.github.diegoberaldin.raccoonforlemmy.feature.search.multicommunity.utils
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.ListingType
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.PostModel
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.SortType
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.repository.PostRepository
internal class CommunityPaginator(
private val communityId: Int,
private val postRepository: PostRepository,
) {
private var currentPage: Int = 1
var canFetchMore: Boolean = true
private set
fun reset() {
currentPage = 1
canFetchMore = true
}
suspend fun loadNextPage(
auth: String?,
sort: SortType,
): List<PostModel> {
val result = postRepository.getAll(
auth = auth,
page = currentPage,
limit = PostRepository.DEFAULT_PAGE_SIZE,
type = ListingType.All,
sort = sort,
communityId = communityId,
)
canFetchMore = result.size >= PostRepository.DEFAULT_PAGE_SIZE
return result
}
}

View File

@ -0,0 +1,42 @@
package com.github.diegoberaldin.raccoonforlemmy.feature.search.multicommunity.utils
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.PostModel
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.SortType
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.repository.PostRepository
class DefaultMultiCommunityPaginator(
private val postRepository: PostRepository,
) : MultiCommunityPaginator {
private var paginators = emptyList<CommunityPaginator>()
override val canFetchMore: Boolean
get() = paginators.any { it.canFetchMore }
override fun setCommunities(ids: List<Int>) {
paginators = ids.map {
CommunityPaginator(
communityId = it,
postRepository = postRepository,
)
}
}
override fun reset() {
paginators.forEach { it.reset() }
}
override suspend fun loadNextPage(
auth: String?,
sort: SortType,
): List<PostModel> = buildList {
for (paginator in paginators) {
if (paginator.canFetchMore) {
val elements = paginator.loadNextPage(
auth = auth,
sort = sort,
)
addAll(elements)
}
}
}.sortedBy { it.publishDate }
}

View File

@ -0,0 +1,15 @@
package com.github.diegoberaldin.raccoonforlemmy.feature.search.multicommunity.utils
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.PostModel
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.SortType
interface MultiCommunityPaginator {
val canFetchMore: Boolean
fun setCommunities(ids: List<Int>)
fun reset()
suspend fun loadNextPage(
auth: String? = null,
sort: SortType,
): List<PostModel>
}

View File

@ -1,15 +1,35 @@
package com.github.diegoberaldin.raccoonforlemmy.feature.search.di
import com.github.diegoberaldin.raccoonforlemmy.core.persistence.data.MultiCommunityModel
import com.github.diegoberaldin.raccoonforlemmy.feature.search.main.ExploreViewModel
import com.github.diegoberaldin.raccoonforlemmy.feature.search.managesubscriptions.ManageSubscriptionsViewModel
import com.github.diegoberaldin.raccoonforlemmy.feature.search.multicommunity.detail.MultiCommunityViewModel
import com.github.diegoberaldin.raccoonforlemmy.feature.search.multicommunity.editor.MultiCommunityEditorViewModel
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
import org.koin.core.parameter.parametersOf
actual fun getExploreViewModel() = SearchScreenModelHelper.model
actual fun getManageSubscriptionsViewModel() = SearchScreenModelHelper.manageSuscriptionsViewModel
actual fun getMultiCommunityViewModel(community: MultiCommunityModel) =
SearchScreenModelHelper.getMultiCommunityViewModel(community)
actual fun getMultiCommunityEditorViewModel(editedCommunity: MultiCommunityModel?) =
SearchScreenModelHelper.getMultiCommunityEditorViewModel(editedCommunity)
object SearchScreenModelHelper : KoinComponent {
val model: ExploreViewModel by inject()
val manageSuscriptionsViewModel: ManageSubscriptionsViewModel by inject()
internal fun getMultiCommunityViewModel(community: MultiCommunityModel): MultiCommunityViewModel {
val res: MultiCommunityViewModel by inject(parameters = { parametersOf(community) })
return res
}
internal fun getMultiCommunityEditorViewModel(editedCommunity: MultiCommunityModel?): MultiCommunityEditorViewModel {
val res: MultiCommunityEditorViewModel by inject(parameters = { parametersOf(editedCommunity) })
return res
}
}

View File

@ -65,7 +65,7 @@
<string name="explore_result_type_comments">Comments</string>
<string name="explore_result_type_communities">Communities</string>
<string name="explore_result_type_users">Users</string>
<string name="explore_search_placeholder">Search text</string>
<string name="explore_search_placeholder">Search</string>
<string name="explore_title_manage_subscriptions">Manage subscriptions</string>
<string name="profile_not_logged_message">You are currently not logged in.\nPlease add an
@ -162,4 +162,12 @@
<string name="manage_accounts_title">Manage accounts</string>
<string name="manage_accounts_button_add">Add account</string>
<string name="manage_subscriptions_header_multicommunities">Multi-communities</string>
<string name="manage_subscriptions_header_subscriptions">Subscriptions</string>
<string name="multi_community_editor_title">Multi-community editor</string>
<string name="multi_community_editor_name">Name</string>
<string name="multi_community_editor_icon">Icon</string>
<string name="multi_community_editor_communities">Communities</string>
</resources>

View File

@ -61,7 +61,7 @@
<string name="explore_result_type_comments">Comentarios</string>
<string name="explore_result_type_communities">Comunidades</string>
<string name="explore_result_type_users">Usuarios</string>
<string name="explore_search_placeholder">Texto de búsqueda</string>
<string name="explore_search_placeholder">Búsqueda</string>
<string name="explore_title_manage_subscriptions">Gestionar subscripciones</string>
<string name="profile_not_logged_message">Acceso no efectuado.\nAñadir una cuenta para
@ -159,4 +159,12 @@
<string name="manage_accounts_title">Gestionar cuentas</string>
<string name="manage_accounts_button_add">Nueva cuenta</string>
<string name="manage_subscriptions_header_multicommunities">Multi-comunidades</string>
<string name="manage_subscriptions_header_subscriptions">Subscripciones</string>
<string name="multi_community_editor_title">Editor multi-comunidad</string>
<string name="multi_community_editor_name">Nombre</string>
<string name="multi_community_editor_icon">Icono</string>
<string name="multi_community_editor_communities">Comunidades</string>
</resources>

View File

@ -61,7 +61,7 @@
<string name="explore_result_type_comments">Commenti</string>
<string name="explore_result_type_communities">Comunità</string>
<string name="explore_result_type_users">Utenti</string>
<string name="explore_search_placeholder">Testo di ricerca</string>
<string name="explore_search_placeholder">Ricerca</string>
<string name="explore_title_manage_subscriptions">Gestione iscrizioni</string>
<string name="profile_not_logged_message">Login non effettuato.\nAggiungi un account per
@ -159,4 +159,12 @@
<string name="manage_accounts_title">Gestisci account</string>
<string name="manage_accounts_button_add">Aggiungi account</string>
<string name="manage_subscriptions_header_multicommunities">Multi-comunità</string>
<string name="manage_subscriptions_header_subscriptions">Iscrizioni</string>
<string name="multi_community_editor_title">Editor Multi-comunità</string>
<string name="multi_community_editor_name">Nome</string>
<string name="multi_community_editor_icon">Icona</string>
<string name="multi_community_editor_communities">Comunità</string>
</resources>