Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ import androidx.compose.ui.unit.dp
import compose.icons.FontAwesomeIcons
import compose.icons.fontawesomeicons.Solid
import compose.icons.fontawesomeicons.solid.AnglesUp
import compose.icons.fontawesomeicons.solid.Sliders
import compose.icons.fontawesomeicons.solid.Plus
import dev.chrisbanes.haze.hazeSource
import dev.chrisbanes.haze.rememberHazeState
import dev.dimension.flare.R
Expand All @@ -75,6 +75,7 @@ import dev.dimension.flare.ui.component.platform.isBigScreen
import dev.dimension.flare.ui.component.status.AdaptiveCard
import dev.dimension.flare.ui.component.status.LazyStatusVerticalStaggeredGrid
import dev.dimension.flare.ui.component.status.status
import dev.dimension.flare.ui.model.UiRssSource
import dev.dimension.flare.ui.model.UiTimeline
import dev.dimension.flare.ui.model.collectAsUiState
import dev.dimension.flare.ui.model.map
Expand All @@ -84,6 +85,7 @@ import dev.dimension.flare.ui.presenter.home.NotificationBadgePresenter
import dev.dimension.flare.ui.presenter.home.UserPresenter
import dev.dimension.flare.ui.presenter.home.UserState
import dev.dimension.flare.ui.presenter.invoke
import dev.dimension.flare.ui.presenter.settings.AccountEventPresenter
import dev.dimension.flare.ui.screen.settings.TabIcon
import dev.dimension.flare.ui.screen.settings.TabTitle
import dev.dimension.flare.ui.theme.screenHorizontalPadding
Expand Down Expand Up @@ -138,20 +140,29 @@ internal fun HomeTimelineScreen(
title = {
state.pagerState.onSuccess { pagerState ->
state.tabState.onSuccess { tabs ->
if (tabs.size > 1) {
if (tabs.any()) {
SecondaryScrollableTabRow(
containerColor = Color.Transparent,
modifier =
Modifier
.fillMaxWidth(),
selectedTabIndex = minOf(pagerState.currentPage, tabs.lastIndex),
selectedTabIndex =
minOf(
pagerState.currentPage,
tabs.lastIndex,
),
edgePadding = 0.dp,
divider = {},
indicator = {
TabRowIndicator(
selectedIndex = minOf(pagerState.currentPage, tabs.lastIndex),
selectedIndex =
minOf(
pagerState.currentPage,
tabs.lastIndex,
),
)
},
minTabWidth = 48.dp,
) {
state.tabState.onSuccess { tabs ->
tabs.forEachIndexed { index, tab ->
Expand Down Expand Up @@ -186,9 +197,17 @@ internal fun HomeTimelineScreen(
)
}
}
IconButton(
onClick = {
toTabSettings.invoke()
},
) {
FAIcon(
imageVector = FontAwesomeIcons.Solid.Plus,
contentDescription = null,
)
}
}
} else {
TabTitle(title = tabs[0].timelineTabItem.metaData.title)
}
}
}
Expand Down Expand Up @@ -217,14 +236,6 @@ internal fun HomeTimelineScreen(
Text(text = stringResource(id = R.string.login_button))
}
}.onSuccess {
IconButton(
onClick = toTabSettings,
) {
FAIcon(
FontAwesomeIcons.Solid.Sliders,
contentDescription = null,
)
}
}
},
)
Expand Down Expand Up @@ -373,6 +384,43 @@ private fun timelinePresenter(
)
}.invoke()

val accountEvent =
remember {
AccountEventPresenter()
}.invoke()

LaunchedEffect(accountEvent.onAdded) {
accountEvent.onAdded.collect { account ->
val tab =
HomeTimelineTabItem(
accountKey = account.accountKey,
icon = UiRssSource.favIconUrl(account.accountKey.host),
Copy link

Copilot AI Aug 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using UiRssSource.favIconUrl for account icons seems semantically incorrect. Consider creating a dedicated method for account favicon URLs or using a more appropriate API.

Suggested change
icon = UiRssSource.favIconUrl(account.accountKey.host),
icon = accountFaviconUrl(account.accountKey.host),

Copilot uses AI. Check for mistakes.
title =
account.accountKey.host
.substringBeforeLast('.')
.substringAfter('.'),
)
settingsRepository.updateTabSettings {
copy(
mainTabs =
(mainTabs + tab).distinctBy {
it.key
},
)
}
}
}

LaunchedEffect(accountEvent.onRemoved) {
accountEvent.onRemoved.collect { accountKey ->
settingsRepository.updateTabSettings {
copy(
mainTabs = mainTabs.filterNot { it.account == AccountType.Specific(accountKey) },
)
}
}
}

val tabs by remember {
settingsRepository.tabSettings
.map { settings ->
Expand All @@ -381,15 +429,23 @@ private fun timelinePresenter(
HomeTimelineTabItem(AccountType.Guest),
)
} else {
listOfNotNull(
if (settings.enableMixedTimeline && settings.mainTabs.size > 1) {
MixedTimelineTabItem(
subTimelineTabItem = settings.mainTabs,
)
} else {
null
},
) + settings.mainTabs
(
listOfNotNull(
if (settings.enableMixedTimeline && settings.mainTabs.size > 1) {
MixedTimelineTabItem(
subTimelineTabItem = settings.mainTabs,
)
} else {
null
},
) + settings.mainTabs
).ifEmpty {
listOf(
HomeTimelineTabItem(
accountType = AccountType.Active,
),
)
}
}
}.map {
it.toImmutableList()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,8 @@ import java.io.OutputStream
@Serializable
public data class TabSettings(
val secondaryItems: List<TabItem>? = null,
val enableMixedTimeline: Boolean = false,
val mainTabs: List<TimelineTabItem> = listOf(HomeTimelineTabItem(AccountType.Active)),
val enableMixedTimeline: Boolean = true,
val mainTabs: List<TimelineTabItem> = listOf(),
)

@Serializable
Expand Down Expand Up @@ -630,6 +630,16 @@ public data class HomeTimelineTabItem(
icon = IconType.Material(IconType.Material.MaterialIcon.Home),
),
)

public constructor(accountKey: MicroBlogKey, icon: String, title: String) :
this(
account = AccountType.Specific(accountKey),
metaData =
TabMetaData(
title = TitleType.Text(title),
icon = IconType.Url(icon),
),
)
}

@Serializable
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.distinctUntilChangedBy
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map
Expand Down Expand Up @@ -59,6 +60,23 @@ internal class AccountRepository(
}
}

private val _onAdded by lazy {
MutableStateFlow<UiAccount?>(null)
}
val onAdded: Flow<UiAccount> by lazy {
_onAdded
.mapNotNull { it }
.distinctUntilChangedBy { it.accountKey }
}
private val _onRemoved by lazy {
MutableStateFlow<MicroBlogKey?>(null)
}
val onRemoved: Flow<MicroBlogKey> by lazy {
_onRemoved
.mapNotNull { it }
Copy link

Copilot AI Aug 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using MutableStateFlow for events can cause issues as new subscribers will not receive previously emitted events. Consider using SharedFlow or Channel for proper event emission semantics.

Suggested change
.mapNotNull { it }
MutableSharedFlow<UiAccount>(extraBufferCapacity = 1)
}
val onAdded: Flow<UiAccount> by lazy {
_onAdded
.distinctUntilChangedBy { it.accountKey }
}
private val _onRemoved by lazy {
MutableSharedFlow<MicroBlogKey>(extraBufferCapacity = 1)
}
val onRemoved: Flow<MicroBlogKey> by lazy {
_onRemoved

Copilot uses AI. Check for mistakes.
Copy link

Copilot AI Aug 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using MutableStateFlow for events can cause issues as new subscribers will not receive previously emitted events. Consider using SharedFlow or Channel for proper event emission semantics.

Suggested change
.mapNotNull { it }
MutableSharedFlow<UiAccount>(replay = 0)
}
val onAdded: Flow<UiAccount> by lazy {
_onAdded
.distinctUntilChangedBy { it.accountKey }
}
private val _onRemoved by lazy {
MutableSharedFlow<MicroBlogKey>(replay = 0)
}
val onRemoved: Flow<MicroBlogKey> by lazy {
_onRemoved

Copilot uses AI. Check for mistakes.
.distinctUntilChangedBy { it }
}

fun addAccount(
account: UiAccount,
credential: UiAccount.Credential,
Expand All @@ -71,6 +89,7 @@ internal class AccountRepository(
credential_json = credential.encodeJson(),
),
)
_onAdded.value = account
}

fun setActiveAccount(accountKey: MicroBlogKey) =
Expand All @@ -83,6 +102,7 @@ internal class AccountRepository(

fun delete(accountKey: MicroBlogKey) =
coroutineScope.launch {
_onRemoved.value = accountKey
cacheDatabase.pagingTimelineDao().deleteByAccountType(
AccountType.Specific(accountKey),
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ public abstract class TimelinePresenter :
when (data.timeline.accountType) {
AccountType.Guest -> null
is AccountType.Specific -> {
accounts.first {
accounts.firstOrNull {
it.accountKey == data.timeline.accountType.accountKey
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package dev.dimension.flare.ui.presenter.settings

import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
import dev.dimension.flare.data.repository.AccountRepository
import dev.dimension.flare.model.MicroBlogKey
import dev.dimension.flare.ui.model.UiAccount
import dev.dimension.flare.ui.presenter.PresenterBase
import kotlinx.coroutines.flow.Flow
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject

public class AccountEventPresenter :
PresenterBase<AccountEventPresenter.State>(),
KoinComponent {
@Immutable
public interface State {
public val onAdded: Flow<UiAccount>
public val onRemoved: Flow<MicroBlogKey>
}

private val accountRepository: AccountRepository by inject()

@Composable
override fun body(): State =
object : State {
override val onAdded = accountRepository.onAdded
override val onRemoved = accountRepository.onRemoved
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ public fun AdaptiveCard(
horizontal = 2.dp,
vertical = 6.dp,
),
elevated = true,
elevated = false,
containerColor = PlatformTheme.colorScheme.card,
) {
content.invoke()
Expand Down