From cb908b0b7b3aa3828c9e8465d9a7bad387dca440 Mon Sep 17 00:00:00 2001 From: Orual Date: Sat, 7 Sep 2024 22:51:58 -0400 Subject: [PATCH 01/42] Settings Service Reworking the approach to preferences to accommodate user-configurable settings. Started to build out the settings UI. Misc bug fixes User list embeds --- .../com/morpho/app/ui/common/BackHandler.kt | 4 +- .../morpho/app/data/PreferencesRepository.kt | 9 + .../kotlin/com/morpho/app/di/AppModule.kt | 2 + .../app/model/bluesky/BskyPreferences.kt | 12 +- .../morpho/app/model/bluesky/FeedGenerator.kt | 12 + .../morpho/app/model/bluesky/UISavedFeed.kt | 79 ++++ .../app/model/uidata/BskyDataService.kt | 19 +- .../app/model/uidata/ContentLabelService.kt | 60 +-- .../com/morpho/app/model/uidata/FeedInfo.kt | 2 + .../app/model/uidata/SettingsService.kt | 356 ++++++++++++++++++ .../app/screens/main/MainScreenModel.kt | 125 +++--- .../app/screens/main/tabbed/TabbedHomeView.kt | 1 + .../main/tabbed/TabbedMainScreenModel.kt | 69 ++-- .../morpho/app/screens/thread/ThreadView.kt | 2 +- .../app/ui/common/TabbedSkylineFragment.kt | 5 +- .../app/ui/elements/HighlightIndication.kt | 41 ++ .../morpho/app/ui/elements/OutlinedAvatar.kt | 17 +- .../com/morpho/app/ui/elements/RichText.kt | 39 +- .../morpho/app/ui/elements/SettingsItems.kt | 106 ++++++ .../app/ui/lists/UserListEntryFragment.kt | 210 +++++++++++ .../morpho/app/ui/post/EmbedPostFragment.kt | 48 ++- .../com/morpho/app/ui/post/PostFragment.kt | 341 +++++++++-------- .../com/morpho/app/ui/post/PostImage.kt | 18 +- .../app/ui/settings/AccessibilitySettings.kt | 119 ++++++ .../composeApp/src/desktopMain/kotlin/main.kt | 1 - 25 files changed, 1353 insertions(+), 344 deletions(-) create mode 100644 Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/UISavedFeed.kt create mode 100644 Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/SettingsService.kt create mode 100644 Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/elements/HighlightIndication.kt create mode 100644 Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/elements/SettingsItems.kt create mode 100644 Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/lists/UserListEntryFragment.kt create mode 100644 Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/settings/AccessibilitySettings.kt diff --git a/Morpho/composeApp/src/androidMain/kotlin/com/morpho/app/ui/common/BackHandler.kt b/Morpho/composeApp/src/androidMain/kotlin/com/morpho/app/ui/common/BackHandler.kt index fc71628..ddebe6e 100644 --- a/Morpho/composeApp/src/androidMain/kotlin/com/morpho/app/ui/common/BackHandler.kt +++ b/Morpho/composeApp/src/androidMain/kotlin/com/morpho/app/ui/common/BackHandler.kt @@ -4,7 +4,5 @@ import androidx.compose.runtime.Composable @Composable actual fun BackHandler(content: () -> Unit) { - BackHandler { - content() - } + } \ No newline at end of file diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/data/PreferencesRepository.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/data/PreferencesRepository.kt index f995c6d..46f27aa 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/data/PreferencesRepository.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/data/PreferencesRepository.kt @@ -27,12 +27,21 @@ data class BskyUserPreferences( val morphoPrefs: MorphoPreferences, ) +@Serializable +data class AccessibilityPreferences( + val requireAltText: Boolean = false, + val displayLargerAltBadge: Boolean = false, + val reduceMotion: Boolean = false, + val disableAutoplay: Boolean = false, + val disableHaptics: Boolean = false, +) @Serializable data class MorphoPreferences( val tabbed: Boolean = true, val undecorated: Boolean = true, val notificationsFilter: NotificationsFilterState = NotificationsFilterState(), + val accessibility: AccessibilityPreferences = AccessibilityPreferences(), ) class PreferencesRepository(storageDir: String): KoinComponent { diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/di/AppModule.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/di/AppModule.kt index 1869ba1..338fdca 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/di/AppModule.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/di/AppModule.kt @@ -5,6 +5,7 @@ import com.morpho.app.data.PreferencesRepository import com.morpho.app.model.uidata.BskyDataService import com.morpho.app.model.uidata.BskyNotificationService import com.morpho.app.model.uidata.ContentLabelService +import com.morpho.app.model.uidata.SettingsService import com.morpho.app.screens.base.BaseScreenModel import com.morpho.app.screens.login.LoginScreenModel import com.morpho.app.screens.main.MainScreenModel @@ -47,6 +48,7 @@ val dataModule = module { single { BskyNotificationService() } single { ContentLabelService() } single { PollBlueService() } + single { SettingsService() } } @Suppress("MemberVisibilityCanBePrivate") diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/BskyPreferences.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/BskyPreferences.kt index f2b7f1f..27d0376 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/BskyPreferences.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/BskyPreferences.kt @@ -20,13 +20,13 @@ public data class BskyPreferences( public var adultContent: AdultContentPref? = null, public val feedViewPrefs: MutableMap = mutableMapOf(), public var skyFeedBuilderFeeds: SkyFeedBuilderFeedsPref? = null, - public var savedFeeds: SavedFeedsPref? = null, + public var savedFeeds: SavedFeedsPrefV2? = null, public val contentLabelPrefs: MutableList = mutableListOf(), public var threadViewPrefs: ThreadViewPref? = null, // Get system languages and allow customization of this public var languages: List = persistentListOf(), public var mergeFeeds: Boolean = false, - public val mutes: MutableList = mutableListOf(), + public val mutes: List = persistentListOf(), public val listsMuted: MutableMap = mutableMapOf(), public var mutedWords: List = persistentListOf(), public var hiddenPosts: List = persistentListOf(), @@ -36,7 +36,7 @@ public data class BskyPreferences( val prefs = persistentListOf() if (this.adultContent != null) prefs.add(PreferencesUnion.AdultContentPref(this.adultContent!!)) if (this.personalDetails != null) prefs.add(PreferencesUnion.PersonalDetailsPref(this.personalDetails!!)) - if (this.savedFeeds != null) prefs.add(PreferencesUnion.SavedFeedsPref(this.savedFeeds!!)) + if (this.savedFeeds != null) prefs.add(PreferencesUnion.SavedFeedsPrefV2(this.savedFeeds!!)) if (this.skyFeedBuilderFeeds != null) prefs.add( PreferencesUnion.SkyFeedBuilderFeedsPref(this.skyFeedBuilderFeeds!!)) if (this.threadViewPrefs != null) prefs.add(PreferencesUnion.ThreadViewPref(this.threadViewPrefs!!)) @@ -59,10 +59,6 @@ public data class BskyPreferences( LabelersPref(this.labelers.toImmutableList().mapImmutable { LabelerPrefItem(it) }))) return prefs.toImmutableList() } - - fun labelsToHide(feed: String): List { - return feedViewPrefs[feed]?.labelsToHide ?: contentLabelPrefs.filter { it.visibility == Visibility.HIDE } - } } @@ -142,7 +138,7 @@ fun GetPreferencesResponse.toPreferences(prefs: BskyPreferences) : BskyPreferenc if(!languages.isNullOrEmpty()) prefs.feedViewPrefs[pref.value.feed]?.languages = languages else persistentListOf() } is PreferencesUnion.PersonalDetailsPref -> prefs.personalDetails = pref.value - is PreferencesUnion.SavedFeedsPref -> prefs.savedFeeds = pref.value + is PreferencesUnion.SavedFeedsPrefV2 -> prefs.savedFeeds = pref.value is PreferencesUnion.SkyFeedBuilderFeedsPref -> prefs.skyFeedBuilderFeeds = pref.value is PreferencesUnion.ThreadViewPref -> prefs.threadViewPrefs = pref.value is PreferencesUnion.HiddenPostsPref -> prefs.hiddenPosts = pref.value.items.toPersistentList() diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/FeedGenerator.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/FeedGenerator.kt index a82c2fa..4e191a9 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/FeedGenerator.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/FeedGenerator.kt @@ -2,12 +2,15 @@ package com.morpho.app.model.bluesky import androidx.compose.runtime.Immutable import androidx.compose.ui.util.fastMap +import app.bsky.actor.FeedType +import app.bsky.actor.SavedFeed import app.bsky.feed.GeneratorView import com.morpho.app.model.uidata.Moment import com.morpho.app.util.mapImmutable import com.morpho.butterfly.AtUri import com.morpho.butterfly.Cid import com.morpho.butterfly.Did +import com.morpho.butterfly.model.TID import kotlinx.serialization.Serializable @Serializable @@ -47,4 +50,13 @@ fun GeneratorView.toFeedGenerator() : FeedGenerator { fun List.toFeedGenList(): List { return this.fastMap { it.toFeedGenerator() } +} + +fun FeedGenerator.toSavedFeed(pinned: Boolean = false): SavedFeed { + return SavedFeed( + id = TID.next().toString(), + type = FeedType.FEED, + value = this.uri.atUri, + pinned = pinned, + ) } \ No newline at end of file diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/UISavedFeed.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/UISavedFeed.kt new file mode 100644 index 0000000..8d7c5a1 --- /dev/null +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/UISavedFeed.kt @@ -0,0 +1,79 @@ +package com.morpho.app.model.bluesky + +import androidx.compose.runtime.Immutable +import app.bsky.actor.FeedType +import app.bsky.feed.GetFeedGeneratorQuery +import app.bsky.graph.GetListQuery +import com.morpho.butterfly.AtUri +import com.morpho.butterfly.Butterfly +import kotlinx.serialization.Serializable + +@Immutable +@Serializable +data class UISavedFeed( + public val avatar: String? = null, + public val title: String, + val description: String? = null, + public val type: UIFeedType, + public val pinned: Boolean, + val feed: FeedGenerator? = null, + val list: UserList? = null, +) + +sealed interface UIFeedType { + val type: FeedType + val value: String + + data class Feed( + val uri: AtUri + ): UIFeedType { + override val type: FeedType = FeedType.FEED + override val value: String = uri.atUri + } + + data class List( + val uri: AtUri + ): UIFeedType { + override val type: FeedType = FeedType.LIST + override val value: String = uri.atUri + } + + data object Timeline: UIFeedType { + override val type: FeedType = FeedType.TIMELINE + override val value: String = "following" + } +} + +suspend fun app.bsky.actor.SavedFeed.toUISavedFeed(api: Butterfly): UISavedFeed { + return when(this.type) { + FeedType.FEED -> { + val feed = api.api.getFeedGenerator(GetFeedGeneratorQuery(AtUri(this.value))) + .getOrNull() + UISavedFeed( + avatar = feed?.view?.avatar, + description = feed?.view?.description, + title = feed?.view?.displayName ?: this.value, + type = UIFeedType.Feed(AtUri(this.value)), + pinned = this.pinned, + feed = feed?.view?.toFeedGenerator() + ) + } + FeedType.LIST -> { + val list = api.api.getList(GetListQuery(AtUri(this.value))).getOrNull()?.list + UISavedFeed( + avatar = list?.avatar, + title = list?.name ?: this.value, + description = list?.description, + type = UIFeedType.List(AtUri(this.value)), + pinned = this.pinned, + list = list?.toList() + ) + } + FeedType.TIMELINE -> UISavedFeed( + avatar = null, + title = "Home", + type = UIFeedType.Timeline, + pinned = this.pinned + ) + } +} \ No newline at end of file diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/BskyDataService.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/BskyDataService.kt index dec70fe..ff07320 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/BskyDataService.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/BskyDataService.kt @@ -1,7 +1,6 @@ package com.morpho.app.model.uidata //import com.rickclephas.kmp.nativecoroutines.NativeCoroutines -import androidx.compose.ui.util.fastFirstOrNull import app.bsky.actor.GetProfilesQuery import app.bsky.actor.ProfileViewBasic import app.bsky.feed.* @@ -136,15 +135,9 @@ class BskyDataService: KoinComponent { { posts -> filterbyLanguage(posts, languages.value) }, ) private val contentLabelService by inject() - private val languages: StateFlow> = contentLabelService.preferences.prefs - .distinctUntilChanged().map { preferencesList -> - preferencesList?.fastFirstOrNull { - it.user.userDid == api.atpUser?.id?.toString() - }?.preferences?.languages?: persistentListOf(Language("en")) } .stateIn( - serviceScope, - SharingStarted.WhileSubscribed(100), - persistentListOf(Language("en")) - ) + private val settings: SettingsService by inject() + private val languages: StateFlow> = settings.languages + .stateIn(serviceScope, SharingStarted.Lazily, persistentListOf()) // Secondary way to make sure you have the most recent stuff, in case you lose the original reference @@ -156,8 +149,6 @@ class BskyDataService: KoinComponent { val serviceScope = CoroutineScope(SupervisorJob() + Dispatchers.Default) } - - suspend fun refresh( uri: AtUri, cursor: AtCursor = null, @@ -592,6 +583,7 @@ class BskyDataService: KoinComponent { } } }.distinctUntilChanged().flowOn(dispatcher + CoroutineName("Followers of $id")) + suspend fun authorFeed( id: AtIdentifier, type: FeedType, @@ -692,6 +684,7 @@ class BskyDataService: KoinComponent { } } }.distinctUntilChanged().flowOn(dispatcher + CoroutineName("${type.name} feed for $id")) + suspend fun profileTabContent( id: AtIdentifier, type: FeedType, @@ -756,6 +749,7 @@ class BskyDataService: KoinComponent { } } }.distinctUntilChanged().flowOn(dispatcher + CoroutineName("Lists made by $id")) + suspend fun profileFeedsList( id: AtIdentifier, cursor: SharedFlow, @@ -788,6 +782,7 @@ class BskyDataService: KoinComponent { } } }.distinctUntilChanged().flowOn(dispatcher + CoroutineName("Feeds made by $id")) + suspend fun profileServiceView( did: Did, update: SharedFlow, diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/ContentLabelService.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/ContentLabelService.kt index bb61dfc..42b0964 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/ContentLabelService.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/ContentLabelService.kt @@ -4,19 +4,13 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Info import androidx.compose.material.icons.filled.Warning import androidx.compose.runtime.Immutable -import androidx.compose.runtime.mutableStateMapOf import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.util.fastAny import androidx.compose.ui.util.fastFilter import androidx.compose.ui.util.fastForEach -import app.bsky.actor.ContentLabelPref -import app.bsky.actor.MutedWord import app.bsky.actor.Visibility -import app.bsky.labeler.GetServicesQuery -import app.bsky.labeler.GetServicesResponseViewUnion import com.atproto.label.LabelValue import com.atproto.label.Severity -import com.morpho.app.data.PreferencesRepository import com.morpho.app.model.bluesky.* import com.morpho.butterfly.AtUri import com.morpho.butterfly.Butterfly @@ -24,7 +18,9 @@ import com.morpho.butterfly.Language import com.morpho.butterfly.model.ReadOnlyList import kotlinx.collections.immutable.* import kotlinx.coroutines.* -import kotlinx.coroutines.flow.* +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn import kotlinx.serialization.Contextual import kotlinx.serialization.Serializable import org.koin.core.component.KoinComponent @@ -421,27 +417,21 @@ data object GraphicMedia: InterpretedLabelDefinition( class ContentLabelService: KoinComponent { val api:Butterfly by inject() - val preferences: PreferencesRepository by inject() - - private val _labelPrefs: MutableStateFlow> = MutableStateFlow(listOf()) - private val _labelers: MutableStateFlow> = MutableStateFlow(listOf()) - private val _mutedWords: MutableStateFlow> = MutableStateFlow(listOf()) - private val _hiddenPosts: MutableStateFlow> = MutableStateFlow(listOf()) - private val _showAdultContent: MutableStateFlow = MutableStateFlow(false) - private val _feedPrefs: MutableStateFlow> = MutableStateFlow(mapOf()) - - val labelers = _labelers.asStateFlow() - val labelPrefs = _labelPrefs.asStateFlow() - val mutedWords = _mutedWords.asStateFlow() - val hiddenPosts = _hiddenPosts.asStateFlow() - val showAdultContent = _showAdultContent.asStateFlow() - val feedPrefs = _feedPrefs.asStateFlow() + val settings: SettingsService by inject() + + val labelers = settings.labelers.stateIn(serviceScope, SharingStarted.Lazily, persistentListOf()) + val labelPrefs = settings.contentLabelPrefs.stateIn(serviceScope, SharingStarted.Lazily, persistentListOf()) + val mutedUsers = settings.mutedUsers.stateIn(serviceScope, SharingStarted.Lazily, persistentListOf()) + val mutedWords = settings.mutedWords.stateIn(serviceScope, SharingStarted.Lazily, persistentListOf()) + val hiddenPosts = settings.hiddenPosts.stateIn(serviceScope, SharingStarted.Lazily, persistentListOf()) + val showAdultContent = settings.showAdultContent.stateIn(serviceScope, SharingStarted.Lazily, false) + val feedPrefs = settings.feedViewPrefs.stateIn(serviceScope, SharingStarted.Lazily, mapOf()) val labelsToHide = labelPrefs.map { contentLabelPrefs -> contentLabelPrefs.fastFilter { it.visibility == Visibility.HIDE } }.stateIn(serviceScope, SharingStarted.Eagerly, persistentListOf()) - private val handlingCache = mutableStateMapOf>() - private val definitionCache = mutableStateMapOf() + private val handlingCache = mutableMapOf>() + private val definitionCache = mutableMapOf() companion object { val log = logging() @@ -454,28 +444,6 @@ class ContentLabelService: KoinComponent { delay(100) } if (api.isLoggedIn()) { - preferences.userPrefs(api.atpUser!!.id).map { prefs -> - _labelPrefs.update { prefs?.preferences?.contentLabelPrefs ?: emptyList() } - _mutedWords.update { prefs?.preferences?.mutedWords ?: emptyList() } - _hiddenPosts.update { prefs?.preferences?.hiddenPosts ?: emptyList() } - _showAdultContent.update { prefs?.preferences?.adultContent?.enabled ?: false } - _feedPrefs.update { prefs?.preferences?.feedViewPrefs ?: emptyMap() } - val labelerProfiles = prefs?.preferences?.labelers?.toImmutableList() - ?.let { GetServicesQuery(it) }?.let { - api.api.getServices(it) - .map { resp -> - resp.views.map { service -> - when(service) { - is GetServicesResponseViewUnion.LabelerView -> - service.value.toLabelService() - is GetServicesResponseViewUnion.LabelerViewDetailed -> - service.value.toLabelService() - } - } - }.getOrNull() - } ?: emptyList() - _labelers.update { labelerProfiles } - } initDefinitionCache() } diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/FeedInfo.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/FeedInfo.kt index 125a716..376e7f6 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/FeedInfo.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/FeedInfo.kt @@ -5,6 +5,7 @@ import androidx.compose.material.icons.filled.RssFeed import androidx.compose.runtime.Immutable import androidx.compose.ui.graphics.vector.ImageVector import com.morpho.app.model.bluesky.FeedGenerator +import com.morpho.app.model.bluesky.UserList import com.morpho.butterfly.AtUri @Immutable @@ -15,4 +16,5 @@ data class FeedInfo( val avatar: String? = null, val icon: ImageVector = Icons.Default.RssFeed, val feed: FeedGenerator? = null, + val list: UserList? = null, ) \ No newline at end of file diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/SettingsService.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/SettingsService.kt new file mode 100644 index 0000000..74ede82 --- /dev/null +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/SettingsService.kt @@ -0,0 +1,356 @@ +package com.morpho.app.model.uidata + +import androidx.compose.ui.util.fastAny +import androidx.compose.ui.util.fastMap +import app.bsky.actor.* +import app.bsky.graph.* +import app.bsky.labeler.GetServicesQuery +import app.bsky.labeler.GetServicesResponseViewUnion +import com.morpho.app.data.AccessibilityPreferences +import com.morpho.app.data.BskyUserPreferences +import com.morpho.app.data.PreferencesRepository +import com.morpho.app.model.bluesky.* +import com.morpho.app.model.uistate.NotificationsFilterState +import com.morpho.butterfly.* +import com.morpho.butterfly.model.RecordType +import com.morpho.butterfly.model.RecordUnion +import kotlinx.collections.immutable.toImmutableList +import kotlinx.collections.immutable.toPersistentList +import kotlinx.coroutines.* +import kotlinx.coroutines.flow.* +import org.koin.core.component.KoinComponent +import org.koin.core.component.inject +import org.lighthousegames.logging.logging +import kotlin.collections.List + + +@OptIn(ExperimentalCoroutinesApi::class) +class SettingsService: KoinComponent { + companion object { + val log = logging() + val serviceScope = CoroutineScope(SupervisorJob() + Dispatchers.Default) + } + + val api: Butterfly by inject() + val prefs: PreferencesRepository by inject() + + + private var _currentUser: MutableStateFlow = MutableStateFlow(null) + private var _currentUserPrefs: MutableStateFlow = MutableStateFlow(null) + + val currentUser = _currentUser.asStateFlow() + var currentUserPrefs = _currentUserPrefs.asStateFlow() + + + val languages: Flow> = currentUserPrefs.transform { + if(it != null) emit(it.preferences.languages) + } + + val notificationsFilter: Flow = currentUserPrefs.transform { + if(it?.morphoPrefs?.notificationsFilter != null) emit(it.morphoPrefs.notificationsFilter) + } + + val threadViewPrefs: Flow = currentUserPrefs.transform { + if(it?.preferences?.threadViewPrefs != null) emit(it.preferences.threadViewPrefs!!) + } + + val feedViewPrefs: Flow> = currentUserPrefs.transform { + if(it?.preferences?.feedViewPrefs != null) emit(it.preferences.feedViewPrefs) + } + + val mergeFeeds: Flow = currentUserPrefs.transform { + if(it?.preferences?.mergeFeeds != null) emit(it.preferences.mergeFeeds) + } + + val contentLabelPrefs: Flow> = currentUserPrefs.transform { + if(it?.preferences?.contentLabelPrefs != null) emit(it.preferences.contentLabelPrefs) + } + + val mutedWords: Flow> = currentUserPrefs.transform { + if(it?.preferences?.mutedWords != null) emit(it.preferences.mutedWords) + } + + val mutedUsers: Flow> = currentUserPrefs.transform { + if(it?.preferences?.mutes != null) emit(it.preferences.mutes) + } + + val hiddenPosts: Flow> = currentUserPrefs.transform { + if(it?.preferences?.hiddenPosts != null) emit(it.preferences.hiddenPosts) + } + + val showAdultContent: Flow = currentUserPrefs.transform { + if(it?.preferences?.adultContent?.enabled != null) emit(it.preferences.adultContent?.enabled ?: false) + } + + val savedFeeds: Flow> = currentUserPrefs.transform { preferences -> + if(preferences?.preferences?.savedFeeds != null) emit(preferences.preferences.savedFeeds!!.items.map { it.toUISavedFeed(api) }) + } + val pinnedFeeds: Flow> = currentUserPrefs.transform { preferences -> + if(preferences?.preferences?.savedFeeds != null) + emit(preferences.preferences.savedFeeds!!.items.filter { it.pinned }.map { + it.toUISavedFeed(api) + }) + } + + val labelers: Flow> = currentUserPrefs.transformLatest { preferences -> + if (preferences?.preferences?.labelers?.isNotEmpty() == true) + emit(preferences.preferences.labelers.toImmutableList() + .let { labelerList -> GetServicesQuery(labelerList) }.let { query -> + api.api.getServices(query) + .map { resp -> + resp.views.map { service -> + when(service) { + is GetServicesResponseViewUnion.LabelerView -> + service.value.toLabelService() + is GetServicesResponseViewUnion.LabelerViewDetailed -> + service.value.toLabelService() + } + } + }.getOrNull() + } ?: emptyList()) + } + + + init { + serviceScope.launch { + while(!api.isLoggedIn()) { + delay(100) + } + _currentUser.value = api.atpUser?.let { prefs.getUser(it.id).getOrNull() } + if(_currentUser.value != null && _currentUserPrefs.value == null) { + _currentUserPrefs.value = prefs.getFullPrefsLocal(api.atpUser!!.id).getOrNull() ?: + prefs.getFullPrefsRemote(api.atpUser!!.id).getOrNull() +// currentUserPrefs = prefs.userPrefs(api.atpUser!!.id).stateIn( +// serviceScope, +// SharingStarted.Eagerly, +// prefs.getFullPrefsLocal(api.atpUser!!.id).getOrNull() +// ) + } + } + } + + fun setUser(id: AtIdentifier) = serviceScope.launch { + _currentUser.value = prefs.getUser(id).getOrNull() + api.switchUser(id) + if(_currentUser.value != null) { + currentUserPrefs = prefs.userPrefs(id).stateIn( + serviceScope, + SharingStarted.Eagerly, + prefs.getFullPrefsRemote(id).getOrNull() + ) + } + delay(10000) + if(_currentUserPrefs.value != null) { + currentUserPrefs = _currentUserPrefs.asStateFlow() + } + } + + fun setAccessibilityPrefs(newPrefs: AccessibilityPreferences) = serviceScope.launch { + val updatedPrefs = _currentUserPrefs.updateAndGet { + it?.copy(morphoPrefs = it.morphoPrefs.copy(accessibility = newPrefs)) + } + if (updatedPrefs != null) { + prefs.setPreferencesRemote(currentUser.value!!, updatedPrefs.preferences, updatedPrefs.morphoPrefs) + } + } + + fun setNotificationsPrefs(newPrefs: NotificationsFilterState) = serviceScope.launch { + val updatedPrefs = _currentUserPrefs.updateAndGet { + it?.copy(morphoPrefs = it.morphoPrefs.copy(notificationsFilter = newPrefs)) + } + if (updatedPrefs != null) { + prefs.setPreferencesRemote(currentUser.value!!, updatedPrefs.preferences, updatedPrefs.morphoPrefs) + } + } + + fun setThreadViewPrefs(newPrefs: ThreadViewPref) = serviceScope.launch { + val updatedPrefs = _currentUserPrefs.updateAndGet { + it?.copy(preferences = it.preferences.copy(threadViewPrefs = newPrefs)) + } + if (updatedPrefs != null) { + prefs.setPreferencesRemote(currentUser.value!!, updatedPrefs.preferences, updatedPrefs.morphoPrefs) + } + } + + fun toggleMergeFeeds() = serviceScope.launch { + val updatedPrefs = _currentUserPrefs.updateAndGet { + it?.copy(preferences = it.preferences.copy(mergeFeeds = !it.preferences.mergeFeeds)) + } + if (updatedPrefs != null) { + prefs.setPreferencesRemote(currentUser.value!!, updatedPrefs.preferences, updatedPrefs.morphoPrefs) + } + } + + fun addMutedWord(newWord: MutedWord) = serviceScope.launch { + val sanitizedMuteWord = newWord.copy(value = newWord.value.trim().replace( + "/^#(?!\\ufe0f)/", "" + ).replace("/[\\r\\n\\u00AD\\u2060\\u200D\\u200C\\u200B]+/", "")) + val updatedPrefs = _currentUserPrefs + .updateAndGet { + it?.copy(preferences = it.preferences.copy( + mutedWords = it.preferences.mutedWords + sanitizedMuteWord + )) + } + if (updatedPrefs != null) { + prefs.setPreferencesRemote(currentUser.value!!, updatedPrefs.preferences, updatedPrefs.morphoPrefs) + } + } + + fun removeMutedWord(word: MutedWord) = serviceScope.launch { + val updatedPrefs = _currentUserPrefs + .updateAndGet { preferences -> + preferences?.copy(preferences = preferences.preferences.copy( + mutedWords = preferences.preferences.mutedWords.filterNot { it == word } + )) + } + if (updatedPrefs != null) { + prefs.setPreferencesRemote(currentUser.value!!, updatedPrefs.preferences, updatedPrefs.morphoPrefs) + } + } + + fun addMutedUser(newMute: BasicProfile) = serviceScope.launch { + _currentUserPrefs.update { + it?.copy(preferences = it.preferences.copy(mutes = it.preferences.mutes + newMute)) + } + api.api.muteActor(MuteActorRequest(newMute.did)) + } + + fun muteUserList(newMute: AtUri) = serviceScope.launch { + val list = api.api.getList(GetListQuery(newMute)).getOrNull() ?: return@launch + _currentUserPrefs.update { preferences -> + preferences?.copy(preferences = preferences.preferences.copy( + mutes = preferences.preferences.mutes + list.list.items.map { it.toProfile() as BasicProfile})) + } + api.api.muteActorList(MuteActorListRequest(newMute)) + } + + fun removeMutedUser(oldMute: BasicProfile) = serviceScope.launch { + _currentUserPrefs.update { preferences -> + preferences?.copy(preferences = preferences.preferences.copy( + mutes = preferences.preferences.mutes.filterNot { it.did == oldMute.did } + )) + } + api.api.unmuteActor(UnmuteActorRequest(oldMute.did)) + } + + fun unmuteUserList(oldMute: AtUri) = serviceScope.launch { + val list = api.api.getList(GetListQuery(oldMute)).getOrNull() ?: return@launch + _currentUserPrefs.update { preferences -> + preferences?.copy(preferences = preferences.preferences.copy( + mutes = preferences.preferences.mutes.filterNot { prefMut -> + list.list.items.fastAny { + it.did == prefMut.did + } } + )) + } + api.api.unmuteActorList(UnmuteActorListRequest(oldMute)) + } + + fun hidePost(post: AtUri) = serviceScope.launch { + val updatedPrefs = _currentUserPrefs + .updateAndGet { preferences -> + preferences?.copy(preferences = preferences.preferences.copy( + hiddenPosts = preferences.preferences.hiddenPosts + post + )) + } + if (updatedPrefs != null) { + prefs.setPreferencesRemote(currentUser.value!!, updatedPrefs.preferences, updatedPrefs.morphoPrefs) + } + } + + fun unhidePost(post: AtUri) = serviceScope.launch { + val updatedPrefs = _currentUserPrefs + .updateAndGet { preferences -> + preferences?.copy(preferences = preferences.preferences.copy( + hiddenPosts = preferences.preferences.hiddenPosts.filterNot { it == post } + )) + } + if (updatedPrefs != null) { + prefs.setPreferencesRemote(currentUser.value!!, updatedPrefs.preferences, updatedPrefs.morphoPrefs) + } + } + + fun setAdultContentPref(showAdultContent: Boolean) = serviceScope.launch { + val updatedPrefs = _currentUserPrefs.updateAndGet { prefs -> + prefs?.copy(preferences = prefs.preferences.copy(adultContent = AdultContentPref(showAdultContent))) + } + if (updatedPrefs != null) { + prefs.setPreferencesRemote(currentUser.value!!, updatedPrefs.preferences, updatedPrefs.morphoPrefs) + } + } + + fun addSavedFeed(newFeed: SavedFeed) = serviceScope.launch { + val updatedPrefs = _currentUserPrefs + .updateAndGet { + it?.copy(preferences = it.preferences.copy( + savedFeeds = it.preferences.savedFeeds?.copy( + items = (it.preferences.savedFeeds!!.items + newFeed).toPersistentList()))) + } + + if (updatedPrefs != null) { + prefs.setPreferencesRemote(currentUser.value!!, updatedPrefs.preferences, updatedPrefs.morphoPrefs) + } + } + + fun addSavedFeeds(newFeeds: List) = serviceScope.launch { + val updatedPrefs = _currentUserPrefs + .updateAndGet { + it?.copy(preferences = it.preferences.copy( + savedFeeds = it.preferences.savedFeeds?.copy( + items = (it.preferences.savedFeeds!!.items + newFeeds).toPersistentList()))) + } + + if (updatedPrefs != null) { + prefs.setPreferencesRemote(currentUser.value!!, updatedPrefs.preferences, updatedPrefs.morphoPrefs) + } + } + + + fun updateSavedFeed(newFeed: SavedFeed) = serviceScope.launch { + val updatedPrefs = _currentUserPrefs + .updateAndGet { preferences -> + preferences?.copy(preferences = preferences.preferences.copy( + savedFeeds = preferences.preferences.savedFeeds?.copy( + items = (preferences.preferences.savedFeeds!!.items.fastMap { + if (it.value == newFeed.value) newFeed else it + }).toPersistentList()))) + } + if (updatedPrefs != null) { + prefs.setPreferencesRemote(currentUser.value!!, updatedPrefs.preferences, updatedPrefs.morphoPrefs) + } + } + + fun removeSavedFeed(uri: AtUri) = serviceScope.launch { + val updatedPrefs = _currentUserPrefs + .updateAndGet { preferences -> + preferences?.copy(preferences = preferences.preferences.copy( + savedFeeds = preferences.preferences.savedFeeds?.copy( + items = (preferences.preferences.savedFeeds!!.items.filter { + it.value != uri.toString() + }).toPersistentList()))) + } + if (updatedPrefs != null) { + prefs.setPreferencesRemote(currentUser.value!!, updatedPrefs.preferences, updatedPrefs.morphoPrefs) + } + } + + fun blockUser(user: BasicProfile) = serviceScope.launch { + api.createRecord(RecordUnion.Block(user.did)) + } + + fun unblockUser(user: Did) = serviceScope.launch { + val profile = api.api.getProfile(GetProfileQuery(user)).getOrNull() ?: return@launch + api.deleteRecord(RecordType.Block, profile.viewer?.blocking) + } + + fun followUser(user: Did) = serviceScope.launch { + api.createRecord(RecordUnion.Follow(user)) + } + + fun unfollowUser(user: Did) = serviceScope.launch { + val profile = api.api.getProfile(GetProfileQuery(user)).getOrNull() ?: return@launch + api.deleteRecord(RecordType.Follow, profile.viewer?.following) + } + + +} \ No newline at end of file diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/main/MainScreenModel.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/main/MainScreenModel.kt index ebad341..96dc8c2 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/main/MainScreenModel.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/main/MainScreenModel.kt @@ -6,13 +6,13 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import app.bsky.actor.GetProfileQuery +import app.bsky.actor.SavedFeed import app.bsky.feed.GetFeedGeneratorsQuery import app.bsky.feed.GetPostThreadQuery import app.bsky.feed.GetPostThreadResponseThreadUnion import app.bsky.feed.GetPostsQuery import cafe.adriel.voyager.core.model.screenModelScope import cafe.adriel.voyager.core.stack.mutableStateStackOf -import com.morpho.app.data.BskyUserPreferences import com.morpho.app.model.bluesky.* import com.morpho.app.model.uidata.* import com.morpho.app.model.uistate.ContentCardState @@ -24,8 +24,10 @@ import com.morpho.butterfly.AtUri import kotlinx.collections.immutable.ImmutableMap import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableMap +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.* +import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking import org.koin.core.component.inject import org.lighthousegames.logging.logging @@ -36,8 +38,7 @@ import org.lighthousegames.logging.logging open class MainScreenModel: BaseScreenModel() { protected val dataService: BskyDataService by inject() - protected val _pinnedFeeds = mutableListOf() - protected val _savedFeeds = mutableListOf() + protected val _feedStates = mutableListOf>>() val feedStates: List>> @@ -48,7 +49,10 @@ open class MainScreenModel: BaseScreenModel() { val history = mutableStateStackOf() - val userPrefs = MutableStateFlow(null) + val settings: SettingsService by inject() + + protected val pinnedFeeds = MutableStateFlow(emptyList()) + protected val savedFeeds = MutableStateFlow(emptyList()) protected val _cursors = mutableMapOf>() public val cursors: ImmutableMap> @@ -69,6 +73,16 @@ open class MainScreenModel: BaseScreenModel() { if(initialized) return@runBlocking initialized = true userId = api.atpUser?.id + screenModelScope.launch(Dispatchers.Default) { + settings.pinnedFeeds.collect { feeds -> + log.d { "Pinned Feeds: $feeds" } + pinnedFeeds.value = feeds + } + settings.savedFeeds.collect { feeds -> + log.d { "Saved Feeds: $feeds" } + savedFeeds.value = feeds + } + } if(userId != null){ if(preferences.prefs.firstOrNull().isNullOrEmpty()){ val prefs = userId?.let { @@ -76,8 +90,7 @@ open class MainScreenModel: BaseScreenModel() { }?.getOrNull() log.d { "Preferences: $prefs" } if(prefs != null) { - userPrefs.value = preferences.getFullPrefsLocal(userId!!).getOrNull() - currentUser = userPrefs.value?.user?.getProfile() + currentUser = settings.currentUser.value?.getProfile() } else { log.e { "Failed to get preferences" } } @@ -96,34 +109,55 @@ open class MainScreenModel: BaseScreenModel() { } else { log.d { "Preferences already set maybe?" } } - currentUser = userPrefs.value?.user?.getProfile() - if(userPrefs.value == null) { - userPrefs.value = preferences.getFullPrefs(userId!!).getOrNull() - } + currentUser = settings.currentUser.value?.getProfile() + if(currentUser == null) { currentUser = userId?.let { GetProfileQuery(it) }?.let { api.api.getProfile(it).getOrNull()?.toProfile() } + } - preferences.userPrefs(userId!!).collect { userPrefs.value = it } } if(populateFeeds) initFeeds() } fun getFeedInfo(uri: AtUri) : FeedInfo? { when { - uri == AtUri.HOME_URI -> return FeedInfo(uri, "Home", "Your home feed", icon = Icons.Default.Home) + uri == AtUri.HOME_URI -> return FeedInfo( + uri, + "Home", + "Your home feed", + icon = Icons.Default.Home + ) + else -> { - _pinnedFeeds.firstOrNull { it.uri == uri }?.let { - return FeedInfo(uri, it.displayName, it.description, it.avatar, feed = it) - } - _savedFeeds.firstOrNull { it.uri == uri }?.let { - return FeedInfo(uri, it.displayName, it.description, it.avatar, feed = it) + if(savedFeeds.value.isNotEmpty()) { + savedFeeds.value.firstOrNull { + when (it.type) { + is UIFeedType.Feed -> it.type.uri == uri + is UIFeedType.List -> it.type.uri == uri + is UIFeedType.Timeline -> return FeedInfo( + uri, + "Home", + "Your home feed", + icon = Icons.Default.Home + ) + } + }?.let { + if (it.type is UIFeedType.Feed) { + return FeedInfo(uri, it.title, it.description, it.avatar, feed = it.feed) + } else if (it.type is UIFeedType.List) { + return FeedInfo(uri, it.title, it.description, it.avatar, list = it.list) + } + // TODO: Get the feed info from the data service + return null + } } - // TODO: Get the feed info from the data service - return null + } } + log.e { "Failed to get feed info for $uri" } + return null } protected open suspend fun initFeeds() { @@ -136,22 +170,14 @@ open class MainScreenModel: BaseScreenModel() { // Init some default feeds } - val savedFeedsPref = userPrefs.value?.preferences?.savedFeeds - if (savedFeedsPref != null) { - api.api.getFeedGenerators(GetFeedGeneratorsQuery(savedFeedsPref.pinned)).onSuccess { resp -> - _pinnedFeeds.addAll(resp.feeds.map{ it.toFeedGenerator() }) - _pinnedFeeds.forEach { feedGen -> - val flow = - initFeed(feedGen, initAtCursor(), force = true, start = true).first() - if (flow == null) { log.e { "Failed to initialize feed: ${feedGen.displayName}" } } - } - } - api.api.getFeedGenerators(GetFeedGeneratorsQuery(savedFeedsPref.saved)).onSuccess { resp -> - _savedFeeds.addAll(resp.feeds.map{ it.toFeedGenerator() }) - _savedFeeds.forEach { feedGen -> + if (settings.pinnedFeeds.last().isNotEmpty()) { + settings.pinnedFeeds.collectLatest { feeds -> + feeds.forEach { feed -> val flow = - initFeed(feedGen, initAtCursor(), force = true, start = false).first() - if (flow == null) { log.e { "Failed to initialize feed: ${feedGen.displayName}" } } + initFeed(feed.feed!!, initAtCursor(), force = true, start = true).first() + if (flow == null) { + log.e { "Failed to initialize feed: ${feed.title}" } + } } } } else { @@ -164,12 +190,24 @@ open class MainScreenModel: BaseScreenModel() { AtUri("at://did:plc:tenurhgjptubkk5zf5qhi3og/app.bsky.feed.generator/feed-of-feeds"), ) )).onSuccess { resp -> - _pinnedFeeds.addAll(resp.feeds.map{ it.toFeedGenerator() }) - _pinnedFeeds.forEach { feedGen -> - val flow = - initFeed(feedGen, initAtCursor(), force = true, start = true).first() - if (flow == null) { log.e { "Failed to initialize feed: ${feedGen.displayName}" } } + settings.addSavedFeeds(resp.feeds.map { feed -> + SavedFeed( + feed.uri.atUri, + pinned = true, + type = app.bsky.actor.FeedType.FEED, + value = feed.uri.atUri, + ) + }) + settings.savedFeeds.collectLatest { feeds -> + feeds.forEach { feed -> + val flow = + initFeed(feed.feed!!, initAtCursor(), force = true, start = false).first() + if (flow == null) { + log.e { "Failed to initialize feed: ${feed.title}" } + } + } } + } } } @@ -206,6 +244,7 @@ open class MainScreenModel: BaseScreenModel() { ?: return null return loadThread(ContentCardState.PostThread(post, MutableStateFlow(null).asStateFlow(), ContentLoadingState.Loading)) } + @OptIn(ExperimentalCoroutinesApi::class) suspend fun loadThread(state: ContentCardState.PostThread): StateFlow = flow { val r = api.api.getPostThread(GetPostThreadQuery(state.uri, 15, 200)).map { response -> @@ -324,8 +363,8 @@ open class MainScreenModel: BaseScreenModel() { MutableStateFlow(BskyFeedPref()) } else { log.d { "Preferences found"} - userPrefs.map { - it?.preferences?.feedViewPrefs?.get("home") ?: BskyFeedPref() + settings.feedViewPrefs.map { + it["home"] ?: BskyFeedPref() }.stateIn(screenModelScope, SharingStarted.Lazily, BskyFeedPref()) } val feedService = dataService.dataFlows[timeline.uri] @@ -356,8 +395,8 @@ open class MainScreenModel: BaseScreenModel() { force: Boolean = false, ): Flow?> = flow { val uri = AtUri.HOME_URI - val prefs = userPrefs.map { - it?.preferences?.feedViewPrefs?.get("home") ?: BskyFeedPref() + val prefs = settings.feedViewPrefs.map { + it["home"] ?: BskyFeedPref() }.filterNotNull().stateIn(screenModelScope) val feedService = dataService.dataFlows[uri] diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/main/tabbed/TabbedHomeView.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/main/tabbed/TabbedHomeView.kt index cbe6466..2cce811 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/main/tabbed/TabbedHomeView.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/main/tabbed/TabbedHomeView.kt @@ -94,6 +94,7 @@ fun TabScreen.TabbedHomeView() { topContent = { HomeTabRow( tabs = tabs, + modifier = Modifier.statusBarsPadding(), tabIndex = selectedTabIndex, onChanged = { index -> if (index == selectedTabIndex) return@HomeTabRow diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/main/tabbed/TabbedMainScreenModel.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/main/tabbed/TabbedMainScreenModel.kt index de2feb4..2edef22 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/main/tabbed/TabbedMainScreenModel.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/main/tabbed/TabbedMainScreenModel.kt @@ -3,13 +3,12 @@ package com.morpho.app.screens.main.tabbed import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue -import androidx.compose.ui.util.fastForEach +import app.bsky.actor.SavedFeed import app.bsky.feed.GetFeedGeneratorsQuery import cafe.adriel.voyager.core.model.screenModelScope import com.morpho.app.model.bluesky.FeedGenerator import com.morpho.app.model.bluesky.MorphoDataItem import com.morpho.app.model.bluesky.Profile -import com.morpho.app.model.bluesky.toFeedGenerator import com.morpho.app.model.uidata.AtCursor import com.morpho.app.model.uidata.ContentCardMapEntry import com.morpho.app.model.uidata.MorphoData @@ -20,6 +19,7 @@ import com.morpho.app.screens.main.MainScreenModel import com.morpho.butterfly.AtUri import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch import kotlinx.serialization.Serializable @@ -51,7 +51,7 @@ class TabbedMainScreenModel : MainScreenModel() { init(false) initialized = true val home = initHomeTab() - val savedFeedsPref = userPrefs.value?.preferences?.savedFeeds + tabs.clear() val newFeeds = mutableListOf>>() if(home.isSuccess) { @@ -71,27 +71,26 @@ class TabbedMainScreenModel : MainScreenModel() { } } } - if (savedFeedsPref != null) { - log.d { "Pinned feeds: ${savedFeedsPref.pinned}" } - api.api.getFeedGenerators(GetFeedGeneratorsQuery(savedFeedsPref.pinned)) - .map { resp -> - _pinnedFeeds.addAll(resp.feeds.map { it.toFeedGenerator() }) - _pinnedFeeds.associateBy { _pinnedFeeds.indexOf(it) }.mapValues { feedGen -> - initFeedTab(feedGen.value) - } - }.getOrNull()?.forEach { (index, pair) -> - val feed = pair.getOrNull() - if (feed != null) { - feedStates.firstOrNull { - it.value.uri == feed.first.uri - }?.let { state -> - tabs.add(feed.first) - newFeeds.add(state as StateFlow>) - } - } else { - log.e { "Failed to initialize feed tab at index $index" } + while(pinnedFeeds.value.isEmpty()) { + delay(10) + } + + if (pinnedFeeds.value.isNotEmpty()) { + log.d { "Pinned feeds: ${pinnedFeeds.value}" } + pinnedFeeds.value.forEach { feed -> + if(feed.feed == null) return@forEach + val result = initFeedTab(feed.feed) + if (result.isFailure) { + MainScreenModel.log.e { "Failed to initialize feed: ${feed.title}" } + } else { + feedStates.firstOrNull { + it.value.uri == result.getOrNull()?.first?.uri + }?.let { state -> + tabs.add(result.getOrNull()?.first!!) + newFeeds.add(state as StateFlow>) } } + } } else if(false) { // Temporarily disabled // Init some default feeds api.api.getFeedGenerators(GetFeedGeneratorsQuery( @@ -102,11 +101,21 @@ class TabbedMainScreenModel : MainScreenModel() { AtUri("at://did:plc:tenurhgjptubkk5zf5qhi3og/app.bsky.feed.generator/feed-of-feeds"), ) )).onSuccess { resp -> - _pinnedFeeds.addAll(resp.feeds.map{ it.toFeedGenerator() }) - _pinnedFeeds.associateBy { _pinnedFeeds.indexOf(it) }.mapValues { feedGen -> - val result = initFeedTab(feedGen.value) - if (result.isFailure) { MainScreenModel.log.e { "Failed to initialize feed: ${feedGen.value.displayName}" } } - else { + resp.feeds.forEach { feed -> + settings.addSavedFeed( + SavedFeed( + feed.uri.atUri, + pinned = true, + type = app.bsky.actor.FeedType.FEED, + value = feed.uri.atUri + )) + + } + pinnedFeeds.value.associateBy { pinnedFeeds.value.indexOf(it) }.mapValues { feedGen -> + val result = initFeedTab(feedGen.value.feed!!) + if (result.isFailure) { + MainScreenModel.log.e { "Failed to initialize feed: ${feedGen.value.title}" } + } else { feedStates.firstOrNull { it.value.uri == result.getOrNull()?.first?.uri }?.let { state -> @@ -115,19 +124,15 @@ class TabbedMainScreenModel : MainScreenModel() { } } } - } } else { - log.d { "Saved Feeds: $savedFeedsPref" } + log.d { "Saved Feeds: ${savedFeeds.value}" } log.d { "Prefs ${preferences.prefs.firstOrNull()}" } } _tabFlow.value = tabs.toImmutableList() uiState = uiState.copy(loadingState = UiLoadingState.Idle, tabs = tabFlow, tabStates = newFeeds.toImmutableList()) - uiState.tabStates.fastForEach { - - } } fun refreshTab(index: Int, cursor: AtCursor = null) :Boolean { diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/thread/ThreadView.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/thread/ThreadView.kt index fdb4b27..3dfedd3 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/thread/ThreadView.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/thread/ThreadView.kt @@ -121,7 +121,6 @@ fun ThreadView( }, onRepost = { repostClicked = false - composerRole = ComposerRole.QuotePost initialContent?.let { post -> RecordUnion.Repost( StrongRef(post.uri,post.cid) @@ -129,6 +128,7 @@ fun ThreadView( }?.let { createRecord(it) } }, onQuotePost = { + composerRole = ComposerRole.QuotePost showComposer = true repostClicked = false } diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/common/TabbedSkylineFragment.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/common/TabbedSkylineFragment.kt index 410b87f..2afb0ce 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/common/TabbedSkylineFragment.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/common/TabbedSkylineFragment.kt @@ -70,8 +70,9 @@ fun > TabbedSkylin } } val content = state?.collectAsState() + val clipboard = getKoin().get() if(content?.value != null) { - val clipboard = getKoin().get() + SkylineFragment( content = state, onProfileClicked = { @@ -98,7 +99,6 @@ fun > TabbedSkylin }, onRepost = { repostClicked = false - composerRole = ComposerRole.QuotePost initialContent?.let { post -> RecordUnion.Repost( StrongRef(post.uri, post.cid) @@ -106,6 +106,7 @@ fun > TabbedSkylin }?.let { sm.api.createRecord(it) } }, onQuotePost = { + composerRole = ComposerRole.QuotePost showComposer = true repostClicked = false } diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/elements/HighlightIndication.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/elements/HighlightIndication.kt new file mode 100644 index 0000000..a76c6a0 --- /dev/null +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/elements/HighlightIndication.kt @@ -0,0 +1,41 @@ +package com.morpho.app.ui.elements + +import androidx.compose.foundation.Indication +import androidx.compose.foundation.IndicationInstance +import androidx.compose.foundation.interaction.InteractionSource +import androidx.compose.foundation.interaction.collectIsFocusedAsState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.State +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.geometry.CornerRadius +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.drawscope.ContentDrawScope +import androidx.compose.ui.graphics.drawscope.Stroke +import androidx.compose.ui.unit.dp + +class MorphoHighlightIndicationInstance(isEnabledState: State) : + IndicationInstance { + private val isEnabled by isEnabledState + override fun ContentDrawScope.drawIndication() { + drawContent() + if (isEnabled) { + drawRoundRect(cornerRadius = CornerRadius(4.dp.toPx()), size = size, color = Color.Gray, alpha = 0.2f) + drawRoundRect(cornerRadius = CornerRadius(4.dp.toPx()), + style = Stroke(width = Stroke.HairlineWidth), + size = size, color = Color.White, alpha = 0.9f) + } + } + +} + +class MorphoHighlightIndication : Indication { + @Composable + override fun rememberUpdatedInstance(interactionSource: InteractionSource): + IndicationInstance { + val isFocusedState = interactionSource.collectIsFocusedAsState() + return remember(interactionSource) { + MorphoHighlightIndicationInstance(isEnabledState = isFocusedState) + } + } +} \ No newline at end of file diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/elements/OutlinedAvatar.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/elements/OutlinedAvatar.kt index 76d65f3..8c13037 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/elements/OutlinedAvatar.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/elements/OutlinedAvatar.kt @@ -17,10 +17,12 @@ package com.morpho.app.ui.elements */ import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.CircleShape import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.drawBehind @@ -28,6 +30,7 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.FilterQuality import androidx.compose.ui.graphics.drawOutline import androidx.compose.ui.graphics.drawscope.Fill +import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.unit.Dp @@ -62,6 +65,7 @@ fun OutlinedAvatar( contentDescription: String = "", avatarShape: AvatarShape = AvatarShape.Corner, onClicked: (() -> Unit)? = null, + placeholder: Painter = painterResource(Res.drawable.placeholder_pfp), size: Dp = 30.dp, ) { @@ -70,6 +74,8 @@ fun OutlinedAvatar( AvatarShape.Rounded -> MaterialTheme.shapes.small AvatarShape.Corner -> roundedTopLBotR.small } + val interactionSource = remember { MutableInteractionSource() } + val indication = remember { MorphoHighlightIndication() } val pxSize = LocalDensity.current.run { (size-outlineSize).toPx()*2 }.toInt() val sB = when(avatarShape) { AvatarShape.Circle -> CircleShape.createOutline( @@ -83,7 +89,12 @@ fun OutlinedAvatar( LocalDensity.current) } val modClicked = if(onClicked != null) { - modifier.clickable { onClicked() } + modifier.clickable( + interactionSource = interactionSource, + indication = indication, + enabled = true, + onClick = { onClicked() } + ) } else modifier val mod = if(outlineSize > 0.dp) { modClicked.size(size).clip(s) @@ -109,8 +120,8 @@ fun OutlinedAvatar( .build(), contentDescription = contentDescription, contentScale = ContentScale.Crop, - fallback = painterResource(Res.drawable.placeholder_pfp), - placeholder = painterResource(Res.drawable.placeholder_pfp), + fallback = placeholder, + placeholder = placeholder, filterQuality = FilterQuality.High, modifier = mod ) diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/elements/RichText.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/elements/RichText.kt index eb6ce34..06a4fc0 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/elements/RichText.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/elements/RichText.kt @@ -5,7 +5,6 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.text.BasicText import androidx.compose.foundation.text.InlineTextContent import androidx.compose.foundation.text.appendInlineContent -import androidx.compose.foundation.text.selection.SelectionContainer import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.mutableStateOf @@ -160,30 +159,28 @@ fun RichTextElement( }.flatten().filter { it.first.isNotEmpty() }.toMap() } - SelectionContainer( - modifier = Modifier.padding(vertical = 6.dp, horizontal = 2.dp) - ) { - val layoutResult = remember { mutableStateOf(null) } - val pressIndicator = Modifier.pointerInput(onClick) { - detectTapGestures { pos -> - layoutResult.value?.let { layoutResult -> - val offset = layoutResult.getOffsetForPosition(pos) - facets.forEach { - if (it.start <= offset && offset <= it.end) { - return@detectTapGestures onClick(it.facetType) - } + + val layoutResult = remember { mutableStateOf(null) } + val pressIndicator = Modifier.pointerInput(onClick) { + detectTapGestures { pos -> + layoutResult.value?.let { layoutResult -> + val offset = layoutResult.getOffsetForPosition(pos) + facets.forEach { + if (it.start <= offset && offset <= it.end) { + return@detectTapGestures onClick(it.facetType) } - onClick(listOf()) } + onClick(listOf()) } } - BasicText( - text = formattedText, - inlineContent = inlineContentMap, - maxLines = maxLines, // Sorry @retr0.id, no more 200 line posts. - overflow = TextOverflow.Ellipsis, - modifier = modifier.then(pressIndicator), - ) } + BasicText( + text = formattedText, + inlineContent = inlineContentMap, + maxLines = maxLines, // Sorry @retr0.id, no more 200 line posts. + overflow = TextOverflow.Ellipsis, + modifier = modifier.padding(vertical = 6.dp, horizontal = 2.dp).then(pressIndicator), + ) + } \ No newline at end of file diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/elements/SettingsItems.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/elements/SettingsItems.kt new file mode 100644 index 0000000..8305999 --- /dev/null +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/elements/SettingsItems.kt @@ -0,0 +1,106 @@ +package com.morpho.app.ui.elements + +import androidx.compose.foundation.layout.* +import androidx.compose.material3.* +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.unit.dp + + +@Composable +fun SettingsGroup( + title: String, + modifier: Modifier = Modifier, + distinguish: Boolean = true, + content: @Composable ColumnScope.() -> Unit +) { + ElevatedCard( + colors = if (distinguish) { + CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.surfaceColorAtElevation(2.dp) + ) + } else { + CardDefaults.cardColors(containerColor = Color.Transparent) + }, + elevation = if (distinguish) CardDefaults.elevatedCardElevation(4.dp) + else CardDefaults.elevatedCardElevation(0.dp) , + modifier = modifier, + shape = MaterialTheme.shapes.small, + ) { + Text( + text = title, + style = MaterialTheme.typography.titleMedium, + modifier = Modifier + .padding(12.dp) + .align(Alignment.Start) + ) + content() + } +} + + + +@Composable +fun ColumnScope.SettingsItem( + text: AnnotatedString? = null, + description: AnnotatedString? = null, + modifier: Modifier = Modifier, + content: @Composable (Modifier) -> Unit, +){ + if(text != null && description == null) { + Column( + modifier = modifier, + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.Start + ) { + Text( + text = text, + modifier = Modifier + .padding(12.dp) + .align(Alignment.Start) + ) + content(Modifier.padding(start = 12.dp, end = 12.dp)) + } + } else { + Row( + modifier = modifier, + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween + ) { + if(description != null && text != null) { + Column( + modifier = Modifier + .padding(horizontal = 12.dp) + ) { + Text( + text = text, + style = MaterialTheme.typography.labelSmall, + modifier = Modifier + .padding(12.dp) + .align(Alignment.Start) + ) + Text( + text = description, + style = MaterialTheme.typography.bodySmall, + modifier = Modifier + .padding(12.dp) + .align(Alignment.Start) + ) + } + content(Modifier.padding(horizontal = 12.dp)) + } else { + content(Modifier.padding(horizontal = 12.dp)) + Text( + text = description?: AnnotatedString(""), + modifier = Modifier + .padding(12.dp) + ) + } + + } + } + +} \ No newline at end of file diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/lists/UserListEntryFragment.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/lists/UserListEntryFragment.kt new file mode 100644 index 0000000..e02af15 --- /dev/null +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/lists/UserListEntryFragment.kt @@ -0,0 +1,210 @@ +package com.morpho.app.ui.lists + + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.text.selection.SelectionContainer +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.DeleteOutline +import androidx.compose.material.icons.filled.PushPin +import androidx.compose.material.icons.filled.RssFeed +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.text.withStyle +import androidx.compose.ui.unit.dp +import androidx.compose.ui.util.fastForEach +import app.bsky.graph.ListType +import com.atproto.repo.StrongRef +import com.morpho.app.model.bluesky.BskyList +import com.morpho.app.model.bluesky.FacetType +import com.morpho.app.model.bluesky.UserList +import com.morpho.app.model.bluesky.UserListBasic +import com.morpho.app.ui.elements.OutlinedAvatar +import com.morpho.app.ui.elements.RichTextElement +import com.morpho.app.util.openBrowser +import kotlinx.collections.immutable.persistentListOf + +@Composable +fun UserListEntryFragment( + list: BskyList, + modifier: Modifier = Modifier, + hasListPinned: Boolean = false, + muteListClicked: (StrongRef, Boolean) -> Unit = {_,_->}, + blockListClicked: (StrongRef, Boolean) -> Unit = {_,_->}, + pinListClicked: (StrongRef, Boolean) -> Unit = {_,_->}, + onListClicked: (BskyList) -> Unit = {}, +) { + var pinned by remember { mutableStateOf(hasListPinned) } + var muted by remember { mutableStateOf(list.viewerMuted) } + var blocked by remember { mutableStateOf(list.viewerBlocked != null)} + Surface ( + shadowElevation = 1.dp, + tonalElevation = 4.dp, + shape = MaterialTheme.shapes.small, + modifier = modifier + .fillMaxWidth() + + ) { + Column( + Modifier + .fillMaxWidth() + .clickable { onListClicked(list) } + .padding(bottom = 4.dp) + .padding(start = 0.dp, end = 6.dp) + ) { + Row( + modifier = Modifier + .padding(end = 4.dp), + horizontalArrangement = Arrangement.End + + ) { + if(list.avatar != null) { + OutlinedAvatar( + url = list.avatar.orEmpty(), + contentDescription = "Avatar for ${list.name}", + modifier = Modifier + .size(55.dp) + .align(Alignment.CenterVertically), + outlineColor = MaterialTheme.colorScheme.tertiary, + onClicked = { onListClicked(list) } + ) + } else { + Icon( + imageVector = Icons.Default.RssFeed, + contentDescription = "Avatar for ${list.name}", + modifier = Modifier + .size(55.dp) + .align(Alignment.CenterVertically), + tint = MaterialTheme.colorScheme.tertiary, + ) + } + SelectionContainer( + modifier = Modifier + //.padding(bottom = 12.dp + .align(Alignment.CenterVertically) + .padding(start = 16.dp, top = 4.dp) + .clickable { onListClicked(list) }, + ) { + Text( + text = buildAnnotatedString { + withStyle( + style = SpanStyle( + color = MaterialTheme.colorScheme.onSurface, + fontSize = MaterialTheme.typography.labelLarge.fontSize + .times(1.2f), + fontWeight = FontWeight.Medium + ) + ) { + append(when(list) { + is UserList -> list.creator.displayName.orEmpty() + is UserListBasic -> "" + }) + } + withStyle( + style = SpanStyle( + color = MaterialTheme.colorScheme.onSurfaceVariant, + fontSize = MaterialTheme.typography.labelLarge.fontSize + .times(1.0f) + ) + ) { + append(when(list) { + is UserList -> list.creator.handle.handle + is UserListBasic -> "" + }) + } + + }, + maxLines = 2, + style = MaterialTheme.typography.labelLarge, + overflow = TextOverflow.Ellipsis, + modifier = Modifier + .wrapContentWidth(Alignment.Start) + //.padding(bottom = 12.dp) + .alignByBaseline() + .align(Alignment.CenterVertically) + //.padding(start = 16.dp), + + ) + } + Spacer( + modifier = Modifier + .width(1.dp) + .weight(0.1F), + ) + if(list.purpose == ListType.CURATELIST) { + IconButton( + onClick = { + pinned = !pinned + pinListClicked(StrongRef(list.uri, list.cid), pinned) + }, + ) { + Icon( + imageVector = if (pinned) Icons.Default.DeleteOutline else Icons.Default.PushPin, + contentDescription = if(pinned) "Unpin from my feeds" else "Pin as a feed", + tint = MaterialTheme.colorScheme.onSurface + ) + } + } else { + TextButton( + onClick = { + muted = !muted + muteListClicked(StrongRef(list.uri, list.cid), muted) + }, + ) { + Text( + text = if(muted) "Unmute" else "Mute", + color = MaterialTheme.colorScheme.onSurface, + style = MaterialTheme.typography.labelLarge, + ) + } + TextButton( + onClick = { + blocked = !blocked + blockListClicked(StrongRef(list.uri, list.cid), blocked) + }, + ) { + Text( + text = if(blocked) "Unblock" else "Block", + color = MaterialTheme.colorScheme.onSurface, + style = MaterialTheme.typography.labelLarge, + ) + } + } + } + RichTextElement( + text = when(list) { + is UserList -> list.description.orEmpty() + is UserListBasic -> "" + }, + facets = when(list) { + is UserList -> list.descriptionFacets + is UserListBasic -> persistentListOf() + }, + onClick = { facetTypes -> + if (facetTypes.isEmpty()) { + onListClicked(list) + return@RichTextElement + } + facetTypes.fastForEach { facetType -> + when (facetType) { + is FacetType.ExternalLink -> { openBrowser(facetType.uri.uri) } + is FacetType.Format -> { } + is FacetType.PollBlueOption -> {} + is FacetType.Tag -> { } + is FacetType.UserDidMention -> { } + is FacetType.UserHandleMention -> { } + else -> {} + } + } + }, + modifier = Modifier.padding(horizontal = 6.dp) + ) + } + } +} \ No newline at end of file diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/post/EmbedPostFragment.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/post/EmbedPostFragment.kt index 16ecf62..f2b49a2 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/post/EmbedPostFragment.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/post/EmbedPostFragment.kt @@ -3,6 +3,7 @@ package com.morpho.app.ui.post import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.* import androidx.compose.foundation.text.selection.SelectionContainer import androidx.compose.material3.MaterialTheme @@ -20,10 +21,12 @@ import androidx.compose.ui.text.withStyle import androidx.compose.ui.unit.dp import androidx.compose.ui.util.fastForEach import com.morpho.app.model.bluesky.* +import com.morpho.app.ui.elements.MorphoHighlightIndication import com.morpho.app.ui.elements.OutlinedAvatar import com.morpho.app.ui.elements.RichTextElement import com.morpho.app.ui.elements.WrappedColumn import com.morpho.app.ui.lists.FeedListEntryFragment +import com.morpho.app.ui.lists.UserListEntryFragment import com.morpho.app.util.getFormattedDateTimeSince import com.morpho.app.util.openBrowser import com.morpho.app.util.parseImageFullRef @@ -42,10 +45,12 @@ fun EmbedPostFragment( val delta = remember { getFormattedDateTimeSince(post.litePost.createdAt) } var hidePost by rememberSaveable { mutableStateOf(post.author.mutedByMe) } val muted = rememberSaveable { post.author.mutedByMe } + val interactionSource = remember { MutableInteractionSource() } + val indication = remember { MorphoHighlightIndication() } WrappedColumn( modifier .fillMaxWidth() - .padding(2.dp) + .padding(top = 6.dp) ) { Surface ( tonalElevation = 4.dp, @@ -55,23 +60,27 @@ fun EmbedPostFragment( modifier = Modifier .fillMaxWidth() .align(Alignment.End) + .clickable( + interactionSource = interactionSource, + indication = indication, + enabled = true, + onClick = { onItemClicked(post.uri) } + ) ) { Column( - Modifier.clickable { onItemClicked(post.uri) } - .padding(bottom = 6.dp, end = 2.dp) - .fillMaxWidth(), + Modifier.fillMaxWidth(), ) { Row( modifier = Modifier - .padding(end = 4.dp), + .padding(end = 6.dp), horizontalArrangement = Arrangement.End ) { OutlinedAvatar( url = post.author.avatar.orEmpty(), contentDescription = "Avatar for ${post.author.handle}", - size = 20.dp, + size = 25.dp, //outlineColor = MaterialTheme.colorScheme.background, onClicked = { onProfileClicked(post.author.did) @@ -107,7 +116,12 @@ fun EmbedPostFragment( .padding(top = 4.dp, start = 4.dp) .weight(10.0F) .alignByBaseline() - .clickable { onProfileClicked(post.author.did) }, + .clickable( + interactionSource = interactionSource, + indication = indication, + enabled = true, + onClick = { onProfileClicked(post.author.did) } + ), ) Spacer( modifier = Modifier @@ -160,7 +174,7 @@ fun EmbedPostFragment( } } }, - modifier = Modifier.padding(horizontal = 4.dp) + modifier = Modifier.padding(horizontal = 6.dp) ) EmbedPostFeature(embed = post, onItemClicked, onLinkClicked = { openBrowser(it) @@ -195,10 +209,15 @@ fun ColumnScope.EmbedPostFeature( ) } is EmbedRecord.EmbedLabelService -> { - + Text(text = "Label Service") } is EmbedRecord.EmbedList -> { + UserListEntryFragment( + list = embed.list, + onListClicked = { + } + ) } is EmbedRecord.InvisibleEmbedPost -> { EmbedNotFoundPostFragment(uri = embed.uri) @@ -320,7 +339,12 @@ fun ColumnScope.EmbedPostFeature( onItemClicked = onItemClicked, modifier = Modifier.align(Alignment.CenterHorizontally) ) - is EmbedRecord.EmbedList -> {} + is EmbedRecord.EmbedList -> { + UserListEntryFragment( + list = embed.litePost.feature.record.list, + onListClicked = { } + ) + } is EmbedRecord.EmbedFeed -> { FeedListEntryFragment( embed.litePost.feature.record.feed, @@ -342,8 +366,8 @@ fun ColumnScope.EmbedPostFeature( alt = embed.alt, aspectRatio = embed.aspectRatio, modifier = Modifier - .padding(vertical = 6.dp) - .heightIn(10.dp, 700.dp) + .padding(top = 6.dp) + .heightIn(100.dp, 600.dp) .fillMaxWidth(), ) } diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/post/PostFragment.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/post/PostFragment.kt index 7e2253f..1ae3f4d 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/post/PostFragment.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/post/PostFragment.kt @@ -4,9 +4,9 @@ package com.morpho.app.ui.post import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.CornerSize -import androidx.compose.foundation.text.selection.SelectionContainer import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.Reply import androidx.compose.material.icons.filled.MoreHoriz @@ -31,6 +31,7 @@ import com.morpho.app.model.uidata.LabelDescription import com.morpho.app.ui.common.OnPostClicked import com.morpho.app.ui.elements.* import com.morpho.app.ui.lists.FeedListEntryFragment +import com.morpho.app.ui.lists.UserListEntryFragment import com.morpho.app.util.getFormattedDateTimeSince import com.morpho.app.util.openBrowser import com.morpho.butterfly.AtIdentifier @@ -109,6 +110,8 @@ fun PostFragment( topStart = CornerSize(0.dp), ) } } + val interactionSource = remember { MutableInteractionSource() } + val indication = remember { MorphoHighlightIndication() } val bgColor = if (role == PostFragmentRole.ThreadEnd) { MaterialTheme.colorScheme.background } else { @@ -137,179 +140,190 @@ fun PostFragment( .fillMaxWidth(indentLevel(indent)) .align(Alignment.End) .background(bgColor, shape) + .clickable( + interactionSource = interactionSource, + indication = indication, + enabled = true, + onClick = { onItemClicked(post.uri) } + ) ) { ContentHider( reasons = contentHandling, scope = LabelScope.Content, ) { - SelectionContainer( - Modifier.clickable { onItemClicked(post.uri) } - ) { - Row( - modifier = Modifier.padding(bottom = 2.dp).padding(start = 0.dp, end = 6.dp) - .fillMaxWidth(indentLevel(indent)) - ) { - if (indent < 2) { - OutlinedAvatar( - url = post.author.avatar.orEmpty(), - contentDescription = "Avatar for ${post.author.handle}", - size = 45.dp, - outlineColor = MaterialTheme.colorScheme.background, - onClicked = { onProfileClicked(post.author.did) }, - avatarShape = AvatarShape.Corner - ) - } + Row( + modifier = Modifier.padding(bottom = 2.dp).padding(start = 0.dp, end = 6.dp) + .fillMaxWidth(indentLevel(indent)) + ) { - Column( - Modifier - .padding(vertical = 2.dp, horizontal = 6.dp) - .fillMaxWidth(indentLevel(indent)), - ) { - if (post.reason is BskyPostReason.BskyPostRepost) { - Row( - modifier = Modifier, - verticalAlignment = Alignment.CenterVertically - ) { - Icon( - imageVector = Icons.Default.Repeat, - contentDescription = "", - tint = MaterialTheme.colorScheme.secondary, - modifier = Modifier.height(15.dp) - ) - Text( - text = "Reposted by ${post.reason.repostAuthor.displayName}", - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.secondary, - modifier = Modifier.padding(start = 5.dp) - ) - } - } + if (indent < 2) { + OutlinedAvatar( + url = post.author.avatar.orEmpty(), + contentDescription = "Avatar for ${post.author.handle}", + size = 45.dp, + outlineColor = MaterialTheme.colorScheme.background, + onClicked = { onProfileClicked(post.author.did) }, + avatarShape = AvatarShape.Corner + ) + } + Column( + Modifier + .padding(top = 2.dp) + .padding(horizontal = 6.dp) + .fillMaxWidth(indentLevel(indent)), + ) { + if (post.reason is BskyPostReason.BskyPostRepost) { Row( - modifier = Modifier.padding(top = 4.dp).padding(horizontal = 4.dp), - horizontalArrangement = Arrangement.End + modifier = Modifier, + verticalAlignment = Alignment.CenterVertically ) { - if (indent >= 2) { - OutlinedAvatar( - url = post.author.avatar.orEmpty(), - contentDescription = "Avatar for ${post.author.handle}", - size = 30.dp, - avatarShape = AvatarShape.Rounded, - outlineColor = MaterialTheme.colorScheme.background, - onClicked = { onProfileClicked(post.author.did) } - ) - } - Text( - text = buildAnnotatedString { - withStyle( - style = SpanStyle( - color = MaterialTheme.colorScheme.onSurface, - fontSize = MaterialTheme.typography.labelLarge.fontSize.times( - 1.2f - ), - fontWeight = FontWeight.Medium - ) - ) { - if (post.author.displayName != null) append("${post.author.displayName} ") - } - withStyle( - style = SpanStyle( - color = MaterialTheme.colorScheme.onSurfaceVariant, - fontSize = MaterialTheme.typography.labelLarge.fontSize.times( - 1.0f - ) - ) - ) { - append("@${post.author.handle}") - } - - }, - maxLines = 1, - style = MaterialTheme.typography.labelLarge, - overflow = TextOverflow.Ellipsis, - modifier = Modifier - .wrapContentWidth(Alignment.Start) - .weight(10.0F) - .alignByBaseline() - .clickable { onProfileClicked(post.author.did) }, + Icon( + imageVector = Icons.Default.Repeat, + contentDescription = "", + tint = MaterialTheme.colorScheme.secondary, + modifier = Modifier.height(15.dp) ) - - Spacer(modifier = Modifier.width(1.dp).weight(0.1F)) Text( - text = delta, - color = MaterialTheme.colorScheme.onSurfaceVariant, - style = MaterialTheme.typography.labelLarge, - fontSize = MaterialTheme.typography.labelLarge.fontSize.div(1.2F), - modifier = Modifier - .wrapContentWidth(Alignment.End) - //.weight(3.0F) - .alignByBaseline(), - maxLines = 1, - overflow = TextOverflow.Visible, - softWrap = false, + text = "Reposted by ${post.reason.repostAuthor.displayName}", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.secondary, + modifier = Modifier.padding(start = 5.dp) ) } + } - if (post.reply?.parent != null) { - ReplyIndicator(post.reply.parent) + Row( + modifier = Modifier.padding(top = 4.dp).padding(horizontal = 4.dp), + horizontalArrangement = Arrangement.End + ) { + if (indent >= 2) { + OutlinedAvatar( + url = post.author.avatar.orEmpty(), + contentDescription = "Avatar for ${post.author.handle}", + size = 30.dp, + avatarShape = AvatarShape.Rounded, + outlineColor = MaterialTheme.colorScheme.background, + onClicked = { onProfileClicked(post.author.did) } + ) } + Text( + text = buildAnnotatedString { + withStyle( + style = SpanStyle( + color = MaterialTheme.colorScheme.onSurface, + fontSize = MaterialTheme.typography.labelLarge.fontSize.times( + 1.2f + ), + fontWeight = FontWeight.Medium + ) + ) { + if (post.author.displayName != null) append("${post.author.displayName} ") + } + withStyle( + style = SpanStyle( + color = MaterialTheme.colorScheme.onSurfaceVariant, + fontSize = MaterialTheme.typography.labelLarge.fontSize.times( + 1.0f + ) + ) + ) { + append("@${post.author.handle}") + } - if (post.facets.fastAny { - it.facetType.first() is FacetType.PollBlueOption - }) { - PollBluePost( - text = post.text, - facets = post.facets, - //modifier = Modifier.padding(bottom = 2.dp).padding(start = 0.dp, end = 6.dp), - onItemClicked = { onItemClicked(post.uri) }, - onProfileClicked = onProfileClicked, - getContentHandling = getContentHandling - ) - } else { - RichTextElement( - text = post.text, - facets = post.facets, - onClick = { facetTypes -> - if (facetTypes.isEmpty()) { - onItemClicked(post.uri) - return@RichTextElement - } - facetTypes.fastForEach { - when(it) { - is FacetType.ExternalLink -> { - openBrowser(it.uri.uri) - } - is FacetType.Tag -> {onItemClicked(post.uri)} - is FacetType.UserDidMention -> { - onProfileClicked(it.did) - } - is FacetType.UserHandleMention -> { - onProfileClicked(it.handle) - } + }, + maxLines = 1, + style = MaterialTheme.typography.labelLarge, + overflow = TextOverflow.Ellipsis, + modifier = Modifier + .wrapContentWidth(Alignment.Start) + .weight(10.0F) + .alignByBaseline() + .clickable( + interactionSource = interactionSource, + indication = indication, + enabled = true, + onClick = { onProfileClicked(post.author.did) } + ) + ) - else -> {} - } - } - }, - ) - } - PostFeatureElement( - post.feature, onItemClicked, contentHandling = contentHandling + Spacer(modifier = Modifier.width(1.dp).weight(0.1F)) + Text( + text = delta, + color = MaterialTheme.colorScheme.onSurfaceVariant, + style = MaterialTheme.typography.labelLarge, + fontSize = MaterialTheme.typography.labelLarge.fontSize.div(1.2F), + modifier = Modifier + .wrapContentWidth(Alignment.End) + //.weight(3.0F) + .alignByBaseline(), + maxLines = 1, + overflow = TextOverflow.Visible, + softWrap = false, ) + } - PostActions( - post = post, - onLikeClicked = { onLikeClicked(StrongRef(post.uri, post.cid)) }, - onMenuClicked = { onMenuClicked(it, post) }, - onReplyClicked = { onReplyClicked(post) }, - onRepostClicked = { onRepostClicked(post) }, - onUnClicked = onUnClicked, + if (post.reply?.parent != null) { + ReplyIndicator(post.reply.parent) + } + + if (post.facets.fastAny { + it.facetType.first() is FacetType.PollBlueOption + }) { + PollBluePost( + text = post.text, + facets = post.facets, + //modifier = Modifier.padding(bottom = 2.dp).padding(start = 0.dp, end = 6.dp), + onItemClicked = { onItemClicked(post.uri) }, + onProfileClicked = onProfileClicked, + getContentHandling = getContentHandling + ) + } else { + RichTextElement( + text = post.text, + facets = post.facets, + modifier = Modifier.padding(end = 2.dp), + onClick = { facetTypes -> + if (facetTypes.isEmpty()) { + onItemClicked(post.uri) + return@RichTextElement + } + facetTypes.fastForEach { + when(it) { + is FacetType.ExternalLink -> { + openBrowser(it.uri.uri) + } + is FacetType.Tag -> {onItemClicked(post.uri)} + is FacetType.UserDidMention -> { + onProfileClicked(it.did) + } + is FacetType.UserHandleMention -> { + onProfileClicked(it.handle) + } + + else -> {} + } + } + }, ) } + PostFeatureElement( + post.feature, onItemClicked, contentHandling = contentHandling + ) + + PostActions( + post = post, + onLikeClicked = { onLikeClicked(StrongRef(post.uri, post.cid)) }, + onMenuClicked = { onMenuClicked(it, post) }, + onReplyClicked = { onReplyClicked(post) }, + onRepostClicked = { onRepostClicked(post) }, + onUnClicked = onUnClicked, + ) } } + } @@ -356,14 +370,17 @@ inline fun ColumnScope.PostFeatureElement( when (feature) { is BskyPostFeature.ExternalFeature -> PostLinkEmbed(linkData = feature, linkPress = { openBrowser(it) }, - modifier = Modifier.align(Alignment.CenterHorizontally)) + modifier = Modifier.padding(end = 2.dp) + .align(Alignment.CenterHorizontally)) is BskyPostFeature.ImagesFeature -> { ContentHider( reasons = contentHandling, scope = LabelScope.Media, + modifier = Modifier.padding(horizontal = 2.dp) ) { PostImages(imagesFeature = feature, - modifier = Modifier.align(Alignment.CenterHorizontally)) + modifier = Modifier.padding(end = 2.dp) + .align(Alignment.CenterHorizontally)) } } is BskyPostFeature.MediaRecordFeature -> { @@ -409,6 +426,9 @@ inline fun ColumnScope.PostFeatureElement( } } + is BskyPostFeature.UnknownEmbed -> { + Text(text = "Unknown Embed ${feature.value}") + } null -> {} else -> {Text(text = "Feature type not supported")} @@ -429,20 +449,24 @@ inline fun ColumnScope.RecordFeature( ContentHider( reasons = contentHandling, scope = LabelScope.Media, - modifier = Modifier.align(Alignment.CenterHorizontally) + modifier = Modifier + .padding(horizontal = 2.dp) + .align(Alignment.CenterHorizontally) ) { when(media) { is BskyPostFeature.ExternalFeature -> { PostLinkEmbed( linkData = media, linkPress = { openBrowser(it) }, - modifier = Modifier.align(Alignment.CenterHorizontally) + modifier = Modifier.padding(end = 2.dp) + .align(Alignment.CenterHorizontally) ) } is BskyPostFeature.ImagesFeature -> { PostImages( imagesFeature = media, - modifier = Modifier.align(Alignment.CenterHorizontally) + modifier = Modifier.padding(end = 2.dp) + .align(Alignment.CenterHorizontally) ) } is BskyPostFeature.VideoFeature -> { @@ -450,7 +474,8 @@ inline fun ColumnScope.RecordFeature( video = media.video, alt = media.alt, aspectRatio = media.aspectRatio, - modifier = Modifier.align(Alignment.CenterHorizontally) + modifier = Modifier.padding(end = 2.dp) + .align(Alignment.CenterHorizontally) ) } else -> {Text(text = "Record Feature not supported")} @@ -482,6 +507,12 @@ inline fun ColumnScope.RecordFeature( onFeedClicked = { } ) } + is EmbedRecord.EmbedList -> { + UserListEntryFragment( + list = record.list, + onListClicked = { } + ) + } else -> { Text(text = "Record Media Feature not supported") diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/post/PostImage.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/post/PostImage.kt index 2178a76..f029165 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/post/PostImage.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/post/PostImage.kt @@ -18,6 +18,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.FilterQuality import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.text.style.TextAlign @@ -28,6 +29,7 @@ import coil3.request.ImageRequest import coil3.size.Size import com.morpho.app.model.bluesky.BskyPostFeature import com.morpho.app.model.bluesky.EmbedImage +import kotlin.math.roundToInt @OptIn(ExperimentalLayoutApi::class) @Composable @@ -39,9 +41,9 @@ fun PostImages( if(numImages > 1) { LazyVerticalStaggeredGrid( columns = StaggeredGridCells.Adaptive(120.dp), - contentPadding = PaddingValues(0.dp), + contentPadding = PaddingValues(2.dp), modifier = modifier - .padding(vertical = 6.dp) + .padding(top = 6.dp) .heightIn(10.dp, 700.dp) ) { items(imagesFeature.images) {image -> @@ -53,7 +55,7 @@ fun PostImages( } } else if (numImages == 1 && imagesFeature.images.isNotEmpty()) { PostImageThumb(image = imagesFeature.images.first(), modifier = Modifier - .padding(vertical = 6.dp) + .padding(top = 6.dp) .heightIn(10.dp, 700.dp) .fillMaxWidth() ) @@ -74,7 +76,7 @@ fun PostImageThumb( } val showAltText = remember { mutableStateOf(false) } BoxWithConstraints( - modifier = modifier.padding(2.dp) + modifier = modifier ) { if (image.aspectRatio == null) { AsyncImage( @@ -83,6 +85,7 @@ fun PostImageThumb( .build(), contentDescription = image.alt, contentScale = ContentScale.Inside, + filterQuality = FilterQuality.High, modifier = Modifier .clip(MaterialTheme.shapes.small) .clickable { @@ -96,15 +99,20 @@ fun PostImageThumb( val ratio = image.aspectRatio.width.toFloat() / image.aspectRatio.height.toFloat() if (ratio > 1) { height /= ratio + height = height.roundToInt().toFloat() + width = (height / ratio).roundToInt().toFloat() } else { width /= ratio + width = width.roundToInt().toFloat() + height = (width / ratio).roundToInt().toFloat() } AsyncImage( model = ImageRequest.Builder(LocalPlatformContext.current) - .data(image.thumb) .size(Size(width.toInt(), height.toInt())) + .data(image.thumb) .build(), contentDescription = image.alt, + filterQuality = FilterQuality.High, contentScale = ContentScale.Inside, modifier = Modifier .clip(MaterialTheme.shapes.small) diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/settings/AccessibilitySettings.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/settings/AccessibilitySettings.kt new file mode 100644 index 0000000..0521da4 --- /dev/null +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/settings/AccessibilitySettings.kt @@ -0,0 +1,119 @@ +package com.morpho.app.ui.settings + +import androidx.compose.material3.Switch +import androidx.compose.runtime.* +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.AnnotatedString +import com.morpho.app.ui.elements.SettingsGroup +import com.morpho.app.ui.elements.SettingsItem + +@Composable +fun AccessibilitySettings( + distinguish: Boolean = true, + modifier: Modifier = Modifier, +) { + SettingsGroup( + title = "Accessibility", + modifier = modifier, + distinguish = distinguish, + ) { + SettingsGroup( + title = "Alt Text", + distinguish = true, + ) { + SettingsItem( text = AnnotatedString("Require Alt Text")) { + var requireAltText by remember { + mutableStateOf(false) + /// TODO: Get preferences + } + + Switch( + checked = requireAltText, + onCheckedChange = { + requireAltText = it + /// TODO: Update preferences + } + ) + } + + SettingsItem( text = AnnotatedString("Display larger alt text")) { + var showLargerAltText by remember { + mutableStateOf(false) + /// TODO: Get preferences + } + + Switch( + checked = showLargerAltText, + onCheckedChange = { + showLargerAltText = it + /// TODO: Update preferences + } + ) + } + } + + SettingsGroup( + title = "Sensory", + distinguish = true, + ) { + SettingsItem( text = AnnotatedString("Disable autoplay for media")) { + var disableAutoplay by remember { + mutableStateOf(false) + /// TODO: Get preferences + } + + Switch( + checked = disableAutoplay, + onCheckedChange = { + disableAutoplay = it + /// TODO: Update preferences + } + ) + } + + SettingsItem( text = AnnotatedString("Reduce/remove animations")) { + var reduceMotion by remember { + mutableStateOf(false) + /// TODO: Get preferences + } + + Switch( + checked = reduceMotion, + onCheckedChange = { + reduceMotion = it + /// TODO: Update preferences + } + ) + } + SettingsItem( text = AnnotatedString("Disable haptic feedback")) { + var disableHaptics by remember { + mutableStateOf(false) + /// TODO: Get preferences + } + + Switch( + checked = disableHaptics, + onCheckedChange = { + disableHaptics = it + /// TODO: Update preferences + } + ) + } + SettingsItem( text = AnnotatedString("Simplify UI")) { + var simpleUI by remember { + mutableStateOf(false) + /// TODO: Get preferences + } + + Switch( + enabled = false, + checked = simpleUI, + onCheckedChange = { + simpleUI = it + /// TODO: Update preferences + } + ) + } + } + } +} \ No newline at end of file diff --git a/Morpho/composeApp/src/desktopMain/kotlin/main.kt b/Morpho/composeApp/src/desktopMain/kotlin/main.kt index 00ec607..9bbf1e7 100644 --- a/Morpho/composeApp/src/desktopMain/kotlin/main.kt +++ b/Morpho/composeApp/src/desktopMain/kotlin/main.kt @@ -70,7 +70,6 @@ fun main() = application { val api = koin.get() val prefs = koin.get { parametersOf(storageDir) } - val morphoPrefs = runBlocking { prefs.prefs.firstOrNull()?.firstOrNull()?.morphoPrefs } From 10a49cff1365272bf3d7b906aa350259428b5e15 Mon Sep 17 00:00:00 2001 From: Orual Date: Sun, 8 Sep 2024 22:52:32 -0400 Subject: [PATCH 02/42] Bunch of state saving fixes and other stuff. --- Morpho/composeApp/build.gradle.kts | 13 + .../androidMain/kotlin/Platform.android.kt | 31 ++- .../ui/common/TabbedScreenScaffold.android.kt | 19 +- .../morpho/app/util/LinkParsing.android.kt | 8 +- .../com/morpho/app/util/Savers.android.kt | 5 + .../src/appleMain/kotlin/Platform.apple.kt | 10 + .../kotlin/com/morpho/app/Platform.apple.kt | 6 + .../ui/common/TabbedScreenScaffold.apple.kt | 15 +- .../com/morpho/app/util/Savers.apple.kt | 5 + .../src/commonMain/kotlin/Platform.kt | 5 - .../kotlin/com/morpho/app/Platform.kt | 42 +++ .../com/morpho/app/model/bluesky/BskyFacet.kt | 23 +- .../com/morpho/app/model/bluesky/BskyLabel.kt | 26 +- .../com/morpho/app/model/bluesky/BskyPost.kt | 1 + .../app/model/bluesky/BskyPostFeature.kt | 25 ++ .../app/model/bluesky/BskyPostReason.kt | 5 + .../app/model/bluesky/BskyPreferences.kt | 10 +- .../com/morpho/app/model/bluesky/LitePost.kt | 2 + .../app/model/bluesky/MorphoDataFeed.kt | 1 + .../app/model/bluesky/MorphoDataItem.kt | 35 ++- .../com/morpho/app/model/bluesky/Profile.kt | 6 +- .../com/morpho/app/model/bluesky/Reference.kt | 4 +- .../morpho/app/model/bluesky/UISavedFeed.kt | 8 + .../app/model/uidata/BskyDataService.kt | 2 + .../model/uidata/BskyNotificationService.kt | 2 + .../app/model/uidata/ContentCardMapEntry.kt | 12 +- .../app/model/uidata/ContentLabelService.kt | 79 +++++- .../com/morpho/app/model/uidata/Delta.kt | 5 +- .../com/morpho/app/model/uidata/FeedInfo.kt | 8 +- .../com/morpho/app/model/uidata/MorphoData.kt | 18 +- .../app/model/uidata/SettingsService.kt | 26 +- .../app/model/uistate/ContentCardState.kt | 3 +- .../morpho/app/model/uistate/LoadingState.kt | 16 +- .../morpho/app/model/uistate/LoginState.kt | 2 + .../app/model/uistate/NotificationsState.kt | 1 + .../model/uistate/PostThreadContentState.kt | 5 +- .../morpho/app/model/uistate/SkylineState.kt | 5 + .../app/model/uistate/TabbedScreenState.kt | 19 +- .../app/screens/base/BaseScreenModel.kt | 10 +- .../app/screens/base/tabbed/NavigationTabs.kt | 136 ++++++---- .../screens/base/tabbed/TabbedBaseScreen.kt | 29 +- .../morpho/app/screens/login/LoginScreen.kt | 22 +- .../app/screens/login/LoginScreenModel.kt | 4 +- .../app/screens/main/MainScreenModel.kt | 65 ++--- .../app/screens/main/tabbed/TabbedHomeView.kt | 243 ++++++++++------- .../main/tabbed/TabbedMainScreenModel.kt | 32 ++- .../notifications/NotificationsView.kt | 92 ++++--- .../TabbedNotificationScreenModel.kt | 4 +- .../app/screens/profile/TabbedProfileView.kt | 250 ++++++++++-------- .../screens/profile/TabbedProfileViewModel.kt | 17 +- .../morpho/app/screens/thread/ThreadView.kt | 50 ++-- .../morpho/app/ui/common/SkylineFragment.kt | 48 ++-- .../app/ui/common/TabbedScreenScaffold.kt | 61 ++++- .../app/ui/common/TabbedSkylineFragment.kt | 17 +- .../morpho/app/ui/elements/OverFlowMenu.kt | 12 +- .../com/morpho/app/ui/elements/RichText.kt | 17 +- .../app/ui/lists/FeedListEntryFragment.kt | 6 +- .../app/ui/lists/UserListEntryFragment.kt | 6 +- .../morpho/app/ui/post/EmbedPostFragment.kt | 6 +- .../morpho/app/ui/post/FullPostFragment.kt | 4 +- .../com/morpho/app/ui/post/PollBlueEmbed.kt | 7 +- .../com/morpho/app/ui/post/PostFragment.kt | 37 ++- .../kotlin/com/morpho/app/util/BlueskyText.kt | 2 +- .../kotlin/com/morpho/app/util/LinkParsing.kt | 9 +- .../kotlin/com/morpho/app/util/Savers.kt | 43 ++- .../kotlin/com/morpho/app/Platform.desktop.kt | 15 ++ .../{ => com/morpho/app}/Platform.jvm.kt | 1 + .../ui/common/TabbedScreenScaffold.desktop.kt | 15 +- .../morpho/app/util/LinkParsing.desktop.kt | 3 +- .../com/morpho/app/util/Savers.desktop.kt | 4 + .../{ => com/morpho/app}/Platform.ios.kt | 1 + .../com/morpho/app/util/LinkParsing.ios.kt | 4 +- Morpho/gradle/libs.versions.toml | 2 +- gradle/libs.versions.toml | 2 +- 74 files changed, 1244 insertions(+), 540 deletions(-) create mode 100644 Morpho/composeApp/src/androidMain/kotlin/com/morpho/app/util/Savers.android.kt create mode 100644 Morpho/composeApp/src/appleMain/kotlin/Platform.apple.kt create mode 100644 Morpho/composeApp/src/appleMain/kotlin/com/morpho/app/Platform.apple.kt create mode 100644 Morpho/composeApp/src/appleMain/kotlin/com/morpho/app/util/Savers.apple.kt delete mode 100644 Morpho/composeApp/src/commonMain/kotlin/Platform.kt create mode 100644 Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/Platform.kt create mode 100644 Morpho/composeApp/src/desktopMain/kotlin/com/morpho/app/Platform.desktop.kt rename Morpho/composeApp/src/desktopMain/kotlin/{ => com/morpho/app}/Platform.jvm.kt (87%) create mode 100644 Morpho/composeApp/src/desktopMain/kotlin/com/morpho/app/util/Savers.desktop.kt rename Morpho/composeApp/src/iosMain/kotlin/{ => com/morpho/app}/Platform.ios.kt (90%) diff --git a/Morpho/composeApp/build.gradle.kts b/Morpho/composeApp/build.gradle.kts index 334d8c4..08af764 100644 --- a/Morpho/composeApp/build.gradle.kts +++ b/Morpho/composeApp/build.gradle.kts @@ -1,4 +1,5 @@ import org.jetbrains.compose.desktop.application.dsl.TargetFormat +import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi plugins { alias(libs.plugins.kotlinMultiplatform) @@ -10,12 +11,20 @@ plugins { alias(libs.plugins.androidApplication) id("kotlin-parcelize") + id("kotlin-kapt") //id("com.rickclephas.kmp.nativecoroutines") version "1.0.0-ALPHA-27" } kotlin { androidTarget { + @OptIn(ExperimentalKotlinGradlePluginApi::class) + compilerOptions { + freeCompilerArgs.addAll( + "-P", + "plugin:org.jetbrains.kotlin.parcelize:additionalAnnotation=com.morpho.app.CommonParcelize", + ) + } compilations.all { kotlinOptions { jvmTarget = "11" @@ -155,6 +164,8 @@ kotlin { implementation(libs.voyager.navigator) // Screen Model implementation(libs.voyager.screenmodel) + implementation("org.jetbrains.androidx.lifecycle:lifecycle-viewmodel-compose:2.8.0") + implementation("cafe.adriel.voyager:voyager-lifecycle-kmp:1.1.0-beta02") // BottomSheetNavigator implementation(libs.voyager.bottom.sheet.navigator) // TabNavigator @@ -231,7 +242,9 @@ android { applicationIdSuffix = ".debug" } } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 targetCompatibility = JavaVersion.VERSION_11 } diff --git a/Morpho/composeApp/src/androidMain/kotlin/Platform.android.kt b/Morpho/composeApp/src/androidMain/kotlin/Platform.android.kt index 4f3ea05..73f97d9 100644 --- a/Morpho/composeApp/src/androidMain/kotlin/Platform.android.kt +++ b/Morpho/composeApp/src/androidMain/kotlin/Platform.android.kt @@ -1,7 +1,36 @@ +@file:Suppress("EXPECT_ACTUAL_CLASSIFIERS_ARE_IN_BETA_WARNING") +package com.morpho.app + import android.os.Build +import android.os.Parcel +import android.os.Parcelable +import kotlinx.datetime.LocalDateTime +import kotlinx.parcelize.Parceler +import kotlinx.parcelize.Parcelize +import kotlinx.parcelize.RawValue +import kotlinx.parcelize.TypeParceler + +actual typealias CommonParcelize = Parcelize +actual typealias CommonParcelable = Parcelable + +actual typealias CommonRawValue = RawValue +actual typealias CommonParceler = Parceler +actual typealias CommonTypeParceler = TypeParceler +actual object LocalDateTimeParceler : Parceler { + override fun create(parcel: Parcel): LocalDateTime { + val date = parcel.readString() + return date?.let { LocalDateTime.parse(it) } + ?: LocalDateTime(0, 0, 0, 0, 0) + } + + override fun LocalDateTime.write(parcel: Parcel, flags: Int) { + parcel.writeString(this.toString()) + } +} class AndroidPlatform : Platform { override val name: String = "Android ${Build.VERSION.SDK_INT}" } -actual fun getPlatform(): Platform = AndroidPlatform() \ No newline at end of file +actual fun getPlatform(): Platform = AndroidPlatform() + diff --git a/Morpho/composeApp/src/androidMain/kotlin/com/morpho/app/ui/common/TabbedScreenScaffold.android.kt b/Morpho/composeApp/src/androidMain/kotlin/com/morpho/app/ui/common/TabbedScreenScaffold.android.kt index 0ac8cc1..a616d2b 100644 --- a/Morpho/composeApp/src/androidMain/kotlin/com/morpho/app/ui/common/TabbedScreenScaffold.android.kt +++ b/Morpho/composeApp/src/androidMain/kotlin/com/morpho/app/ui/common/TabbedScreenScaffold.android.kt @@ -9,12 +9,16 @@ import androidx.compose.material3.TopAppBarScrollBehavior import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.input.nestedscroll.NestedScrollConnection +import com.morpho.app.model.bluesky.MorphoDataItem +import com.morpho.app.model.uistate.ContentCardState +import kotlinx.coroutines.flow.StateFlow @Composable -actual fun TabbedScreenScaffold( +actual fun TabbedScreenScaffold( navBar: @Composable () -> Unit, - content: @Composable (PaddingValues) -> Unit, + content: @Composable (PaddingValues, StateFlow?) -> Unit, topContent: @Composable () -> Unit, + state: StateFlow?, modifier: Modifier, ) { Scaffold( @@ -22,7 +26,9 @@ actual fun TabbedScreenScaffold( modifier = modifier, topBar = { topContent() }, bottomBar = { navBar() }, - content = content + content = { insets -> + content(insets, state) + } ) } @@ -30,8 +36,9 @@ actual fun TabbedScreenScaffold( @Composable actual fun TabbedProfileScreenScaffold( navBar: @Composable () -> Unit, - content: @Composable (PaddingValues) -> Unit, + content: @Composable (PaddingValues, StateFlow>?) -> Unit, topContent: @Composable (TopAppBarScrollBehavior) -> Unit, + state: StateFlow>?, modifier: Modifier, scrollBehavior: TopAppBarScrollBehavior, nestedScrollConnection: NestedScrollConnection, @@ -41,6 +48,8 @@ actual fun TabbedProfileScreenScaffold( modifier = modifier, topBar = { topContent(scrollBehavior) }, bottomBar = { navBar() }, - content = content + content = { insets -> + content(insets, state) + } ) } \ No newline at end of file diff --git a/Morpho/composeApp/src/androidMain/kotlin/com/morpho/app/util/LinkParsing.android.kt b/Morpho/composeApp/src/androidMain/kotlin/com/morpho/app/util/LinkParsing.android.kt index c711706..8a6db35 100644 --- a/Morpho/composeApp/src/androidMain/kotlin/com/morpho/app/util/LinkParsing.android.kt +++ b/Morpho/composeApp/src/androidMain/kotlin/com/morpho/app/util/LinkParsing.android.kt @@ -3,14 +3,16 @@ package com.morpho.app.util import android.content.Intent import android.net.Uri -import com.morpho.app.MorphoApplication +import androidx.compose.ui.platform.UriHandler -actual fun openBrowser(url: String) { + +actual fun openBrowser(url: String, uriHandler: UriHandler) { val urlIntent = Intent( Intent.ACTION_VIEW, safeUrlParse(url) ) - MorphoApplication().applicationContext.startActivity(urlIntent) + + uriHandler.openUri(url) } fun safeUrlParse(uri: String): Uri? { diff --git a/Morpho/composeApp/src/androidMain/kotlin/com/morpho/app/util/Savers.android.kt b/Morpho/composeApp/src/androidMain/kotlin/com/morpho/app/util/Savers.android.kt new file mode 100644 index 0000000..ccea826 --- /dev/null +++ b/Morpho/composeApp/src/androidMain/kotlin/com/morpho/app/util/Savers.android.kt @@ -0,0 +1,5 @@ +package com.morpho.app.util + +// commonMain - module core +@Suppress("EXPECT_ACTUAL_CLASSIFIERS_ARE_IN_BETA_WARNING") +actual typealias JavaSerializable = java.io.Serializable \ No newline at end of file diff --git a/Morpho/composeApp/src/appleMain/kotlin/Platform.apple.kt b/Morpho/composeApp/src/appleMain/kotlin/Platform.apple.kt new file mode 100644 index 0000000..c42198b --- /dev/null +++ b/Morpho/composeApp/src/appleMain/kotlin/Platform.apple.kt @@ -0,0 +1,10 @@ +@file:Suppress("EXPECT_ACTUAL_CLASSIFIERS_ARE_IN_BETA_WARNING") +package com.morpho.app +import kotlinx.datetime.LocalDateTime + +// Note: no need to define CommonParcelize here (bc its @OptionalExpectation) +actual interface CommonParcelable // not used on iOS + +// Note: no need to define CommonTypeParceler> here (bc its @OptionalExpectation) +actual interface CommonParceler // not used on iOS +actual object LocalDateTimeParceler : CommonParceler // not used on iOS diff --git a/Morpho/composeApp/src/appleMain/kotlin/com/morpho/app/Platform.apple.kt b/Morpho/composeApp/src/appleMain/kotlin/com/morpho/app/Platform.apple.kt new file mode 100644 index 0000000..ab7f6d4 --- /dev/null +++ b/Morpho/composeApp/src/appleMain/kotlin/com/morpho/app/Platform.apple.kt @@ -0,0 +1,6 @@ +package com.morpho.app + +// For Android @Parcelize +@Target(AnnotationTarget.PROPERTY) +@Retention(AnnotationRetention.SOURCE) +actual annotation class CommonRawValue \ No newline at end of file diff --git a/Morpho/composeApp/src/appleMain/kotlin/com/morpho/app/ui/common/TabbedScreenScaffold.apple.kt b/Morpho/composeApp/src/appleMain/kotlin/com/morpho/app/ui/common/TabbedScreenScaffold.apple.kt index 0a0f832..fccc58d 100644 --- a/Morpho/composeApp/src/appleMain/kotlin/com/morpho/app/ui/common/TabbedScreenScaffold.apple.kt +++ b/Morpho/composeApp/src/appleMain/kotlin/com/morpho/app/ui/common/TabbedScreenScaffold.apple.kt @@ -9,12 +9,16 @@ import androidx.compose.material3.TopAppBarScrollBehavior import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.input.nestedscroll.NestedScrollConnection +import com.morpho.app.model.bluesky.MorphoDataItem +import com.morpho.app.model.uistate.ContentCardState +import kotlinx.coroutines.flow.StateFlow @Composable -actual fun TabbedScreenScaffold( +actual fun TabbedScreenScaffold( navBar: @Composable () -> Unit, - content: @Composable (PaddingValues) -> Unit, + content: @Composable (PaddingValues, StateFlow?) -> Unit, topContent: @Composable () -> Unit, + state: StateFlow?, modifier: Modifier, ) { Scaffold( @@ -22,7 +26,9 @@ actual fun TabbedScreenScaffold( modifier = modifier, topBar = { topContent() }, bottomBar = { navBar() }, - content = content + content = { insets -> + content(insets, state) + } ) } @@ -30,8 +36,9 @@ actual fun TabbedScreenScaffold( @Composable actual fun TabbedProfileScreenScaffold( navBar: @Composable () -> Unit, - content: @Composable (PaddingValues) -> Unit, + content: @Composable (PaddingValues, StateFlow>?) -> Unit, topContent: @Composable (TopAppBarScrollBehavior) -> Unit, + state: StateFlow>?, modifier: Modifier, scrollBehavior: TopAppBarScrollBehavior, nestedScrollConnection: NestedScrollConnection, diff --git a/Morpho/composeApp/src/appleMain/kotlin/com/morpho/app/util/Savers.apple.kt b/Morpho/composeApp/src/appleMain/kotlin/com/morpho/app/util/Savers.apple.kt new file mode 100644 index 0000000..598c2bc --- /dev/null +++ b/Morpho/composeApp/src/appleMain/kotlin/com/morpho/app/util/Savers.apple.kt @@ -0,0 +1,5 @@ +package com.morpho.app.util + +// commonMain - module core +@Suppress("EXPECT_ACTUAL_CLASSIFIERS_ARE_IN_BETA_WARNING") +actual interface JavaSerializable \ No newline at end of file diff --git a/Morpho/composeApp/src/commonMain/kotlin/Platform.kt b/Morpho/composeApp/src/commonMain/kotlin/Platform.kt deleted file mode 100644 index 87ca3ff..0000000 --- a/Morpho/composeApp/src/commonMain/kotlin/Platform.kt +++ /dev/null @@ -1,5 +0,0 @@ -interface Platform { - val name: String -} - -expect fun getPlatform(): Platform \ No newline at end of file diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/Platform.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/Platform.kt new file mode 100644 index 0000000..aac891b --- /dev/null +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/Platform.kt @@ -0,0 +1,42 @@ +@file:Suppress("EXPECT_ACTUAL_CLASSIFIERS_ARE_IN_BETA_WARNING") +package com.morpho.app + +import kotlinx.datetime.LocalDateTime + +interface Platform { + val name: String +} + +expect fun getPlatform(): Platform + + + +// For Android @Parcelize +@OptIn(ExperimentalMultiplatform::class) +@OptionalExpectation +@Target(AnnotationTarget.CLASS) +@Retention(AnnotationRetention.BINARY) +expect annotation class CommonParcelize() + +// For Android @Parcelize +@OptIn(ExperimentalMultiplatform::class) +@OptionalExpectation +@Target(AnnotationTarget.TYPE) +expect annotation class CommonRawValue() + +// For Android Parcelable +expect interface CommonParcelable + +// For Android @TypeParceler +@OptIn(ExperimentalMultiplatform::class) +@OptionalExpectation +@Retention(AnnotationRetention.SOURCE) +@Repeatable +@Target(AnnotationTarget.CLASS, AnnotationTarget.PROPERTY) +expect annotation class CommonTypeParceler>() + +// For Android Parceler +expect interface CommonParceler + +// For Android @TypeParceler to convert LocalDateTime to Parcel +expect object LocalDateTimeParceler: CommonParceler \ No newline at end of file diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/BskyFacet.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/BskyFacet.kt index c01343a..54a26be 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/BskyFacet.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/BskyFacet.kt @@ -1,5 +1,6 @@ package com.morpho.app.model.bluesky +import androidx.compose.runtime.Immutable import app.bsky.richtext.* import com.atproto.label.SelfLabels import com.morpho.app.util.didCidToImageLink @@ -12,6 +13,7 @@ import kotlinx.collections.immutable.toImmutableList import kotlinx.serialization.Serializable +@Immutable @Serializable data class BskyFacet( val start: Int, @@ -19,42 +21,50 @@ data class BskyFacet( val facetType: List, ) +@Immutable @Serializable sealed interface FacetType { + @Immutable @Serializable data class UserHandleMention( val handle: Handle, ) : FacetType + @Immutable @Serializable data class UserDidMention( val did: Did, ) : FacetType + @Immutable @Serializable data class ExternalLink( val uri: Uri, ) : FacetType - + @Immutable @Serializable data class Tag( val tag: String, ) : FacetType + @Immutable @Serializable data class PollBlueOption( val number: Int, ) : FacetType + @Immutable @Serializable data object PollBlueQuestion : FacetType + @Immutable @Serializable data class Format( val format: RichTextFormat ) : FacetType + @Immutable @Serializable data class BlueMoji( val did: Did, @@ -65,6 +75,7 @@ sealed interface FacetType { val labels: List? = null, ) : FacetType + @Immutable @Serializable data class UnknownFacet( val value: String, @@ -72,22 +83,31 @@ sealed interface FacetType { } +@Immutable @Serializable sealed interface BlueMojiImageLink { val url: String val apng: Boolean val lottie: Boolean + @Immutable + @Serializable data class Png( override val url: String, override val apng: Boolean = false, override val lottie: Boolean = false ) : BlueMojiImageLink + + @Immutable + @Serializable data class Webp( override val url: String, override val apng: Boolean = false, override val lottie: Boolean = false ) : BlueMojiImageLink + + @Immutable + @Serializable data class Gif( override val url: String, override val apng: Boolean = false, @@ -96,6 +116,7 @@ sealed interface BlueMojiImageLink { } +@Immutable @Serializable enum class RichTextFormat { BOLD, diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/BskyLabel.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/BskyLabel.kt index 7745bc6..7f07d14 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/BskyLabel.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/BskyLabel.kt @@ -104,6 +104,7 @@ fun Blurs.toScope(): LabelScope { } } +@Immutable @Serializable enum class LabelAction { Blur, @@ -112,6 +113,7 @@ enum class LabelAction { None } +@Immutable @Serializable enum class LabelTarget { Account, @@ -119,6 +121,7 @@ enum class LabelTarget { Content } +@Immutable @Serializable open class ModBehaviour( val profileList: LabelAction = LabelAction.None, @@ -169,6 +172,7 @@ open class ModBehaviour( } } +@Immutable @Serializable data class ModBehaviours( val account: ModBehaviour = ModBehaviour(), @@ -216,6 +220,7 @@ open class DescribedBehaviours( } +@Immutable @Serializable data object BlockBehaviour: ModBehaviour( profileList = LabelAction.Blur, @@ -226,6 +231,8 @@ data object BlockBehaviour: ModBehaviour( contentView = LabelAction.Blur, ) +@Immutable +@Serializable data object MuteBehaviour: ModBehaviour( profileList = LabelAction.Inform, profileView = LabelAction.Alert, @@ -233,32 +240,46 @@ data object MuteBehaviour: ModBehaviour( contentView = LabelAction.Inform, ) +@Immutable +@Serializable data object MuteWordBehaviour: ModBehaviour( contentList = LabelAction.Blur, contentView = LabelAction.Blur, ) +@Immutable +@Serializable data object HideBehaviour: ModBehaviour( contentList = LabelAction.Blur, contentView = LabelAction.Blur, ) +@Immutable +@Serializable data object InappropriateMediaBehaviour: ModBehaviour( contentMedia = LabelAction.Blur, ) +@Immutable +@Serializable data object InappropriateAvatarBehaviour: ModBehaviour( avatar = LabelAction.Blur, ) +@Immutable +@Serializable data object InappropriateBannerBehaviour: ModBehaviour( banner = LabelAction.Blur, ) +@Immutable +@Serializable data object InappropriateDisplayNameBehaviour: ModBehaviour( displayName = LabelAction.Blur, ) + +@Serializable val BlurAllMedia = ModBehaviours( content = InappropriateMediaBehaviour, profile = ModBehaviour( @@ -274,9 +295,11 @@ val BlurAllMedia = ModBehaviours( ) - +@Immutable +@Serializable data object NoopBehaviour: ModBehaviour() +@Immutable @Serializable enum class LabelValueDefFlag { NoOverride, @@ -285,6 +308,7 @@ enum class LabelValueDefFlag { NoSelf, } +@Immutable @Serializable enum class LabelSetting { @SerialName("ignore") diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/BskyPost.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/BskyPost.kt index eebf817..9ca6184 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/BskyPost.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/BskyPost.kt @@ -12,6 +12,7 @@ import kotlinx.collections.immutable.persistentListOf import kotlinx.datetime.Clock import kotlinx.serialization.Serializable +@Immutable @Serializable enum class PostType { BlockedThread, diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/BskyPostFeature.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/BskyPostFeature.kt index 362fb8c..992a4c3 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/BskyPostFeature.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/BskyPostFeature.kt @@ -1,5 +1,6 @@ package com.morpho.app.model.bluesky +import androidx.compose.runtime.Immutable import app.bsky.embed.* import app.bsky.feed.Post import app.bsky.feed.PostEmbedUnion @@ -11,13 +12,16 @@ import com.morpho.butterfly.model.Blob import kotlinx.serialization.Serializable import kotlinx.serialization.json.JsonElement +@Immutable @Serializable sealed interface BskyPostFeature { + @Immutable @Serializable data class ImagesFeature( val images: List, ) : BskyPostFeature, TimelinePostMedia + @Immutable @Serializable data class VideoFeature( val video: VideoEmbed, @@ -25,6 +29,7 @@ sealed interface BskyPostFeature { val aspectRatio: AspectRatio?, ) : BskyPostFeature, TimelinePostMedia + @Immutable @Serializable data class ExternalFeature( val uri: Uri, @@ -33,29 +38,35 @@ sealed interface BskyPostFeature { val thumb: String?, ) : BskyPostFeature, TimelinePostMedia + @Immutable @Serializable data class RecordFeature( val record: EmbedRecord, ) : BskyPostFeature + @Immutable @Serializable data class MediaRecordFeature( val record: EmbedRecord, val media: TimelinePostMedia, ) : BskyPostFeature + @Immutable @Serializable data class UnknownEmbed( val value: String, ) : BskyPostFeature, TimelinePostMedia } +@Immutable @Serializable sealed interface TimelinePostMedia +@Immutable @Serializable sealed interface VideoEmbed +@Immutable @Serializable data class EmbedVideoView( val cid: Cid, @@ -63,12 +74,14 @@ data class EmbedVideoView( val thumbnail: AtUri, ): VideoEmbed +@Immutable @Serializable data class EmbedVideo( val blob: Blob, val captions: List?, ): VideoEmbed +@Immutable @Serializable data class EmbedImage( val thumb: String, @@ -79,9 +92,11 @@ data class EmbedImage( +@Immutable @Serializable sealed interface EmbedRecord { + @Immutable @Serializable data class VisibleEmbedPost( val uri: AtUri, @@ -92,6 +107,7 @@ sealed interface EmbedRecord { val reference: Reference = Reference(uri, cid) } + @Immutable @Serializable data class EmbedFeed( val uri: AtUri, @@ -101,6 +117,7 @@ sealed interface EmbedRecord { val feed: FeedGenerator, ) : EmbedRecord + @Immutable @Serializable data class EmbedList( val uri: AtUri, @@ -109,6 +126,7 @@ sealed interface EmbedRecord { val list: BskyList, ) : EmbedRecord + @Immutable @Serializable data class EmbedLabelService( val uri: AtUri, @@ -118,21 +136,25 @@ sealed interface EmbedRecord { ) : EmbedRecord + @Immutable @Serializable data class InvisibleEmbedPost( val uri: AtUri, ) : EmbedRecord + @Immutable @Serializable data class BlockedEmbedPost( val uri: AtUri, ) : EmbedRecord + @Immutable @Serializable data class DetachedQuotePost( val uri: AtUri, ) : EmbedRecord + @Immutable @Serializable data class EmbedVideo( val video: VideoEmbed, @@ -140,11 +162,14 @@ sealed interface EmbedRecord { val aspectRatio: AspectRatio?, ) : EmbedRecord + @Immutable @Serializable data class UnknownEmbed( val value: String, ) : EmbedRecord + @Immutable + @Serializable data class StarterPack( val uri: AtUri, val cid: Cid, diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/BskyPostReason.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/BskyPostReason.kt index f442a4c..0a34953 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/BskyPostReason.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/BskyPostReason.kt @@ -1,24 +1,29 @@ package com.morpho.app.model.bluesky +import androidx.compose.runtime.Immutable import app.bsky.feed.FeedViewPostReasonUnion import app.bsky.feed.SkeletonFeedPostReasonUnion import com.morpho.app.model.uidata.Moment import com.morpho.butterfly.AtUri import kotlinx.serialization.Serializable +@Immutable @Serializable sealed interface BskyPostReason { + @Immutable @Serializable data class BskyPostRepost( val repostAuthor: Profile, val indexedAt: Moment, ) : BskyPostReason + @Immutable @Serializable data class BskyPostFeedPost( val repost: AtUri ) : BskyPostReason + @Immutable @Serializable data class SourceFeed( val feed: FeedGenerator diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/BskyPreferences.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/BskyPreferences.kt index 27d0376..f07e3e4 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/BskyPreferences.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/BskyPreferences.kt @@ -24,13 +24,13 @@ public data class BskyPreferences( public val contentLabelPrefs: MutableList = mutableListOf(), public var threadViewPrefs: ThreadViewPref? = null, // Get system languages and allow customization of this - public var languages: List = persistentListOf(), + public var languages: List = listOf(), public var mergeFeeds: Boolean = false, - public val mutes: List = persistentListOf(), + public val mutes: List = listOf(), public val listsMuted: MutableMap = mutableMapOf(), - public var mutedWords: List = persistentListOf(), - public var hiddenPosts: List = persistentListOf(), - public var labelers: List = persistentListOf(), + public var mutedWords: List = listOf(), + public var hiddenPosts: List = listOf(), + public var labelers: List = listOf(), ) { fun toRemotePrefs(): ReadOnlyList { val prefs = persistentListOf() diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/LitePost.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/LitePost.kt index 4cf3216..83eb2db 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/LitePost.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/LitePost.kt @@ -1,11 +1,13 @@ package com.morpho.app.model.bluesky +import androidx.compose.runtime.Immutable import app.bsky.feed.Post import com.morpho.app.model.uidata.Moment import com.morpho.app.util.mapImmutable import com.morpho.butterfly.Language import kotlinx.serialization.Serializable +@Immutable @Serializable data class LitePost( val text: String, diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/MorphoDataFeed.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/MorphoDataFeed.kt index f3b97f2..d0577d9 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/MorphoDataFeed.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/MorphoDataFeed.kt @@ -19,6 +19,7 @@ import kotlinx.serialization.Serializable import kotlin.time.Duration + typealias TunerFunction = (List) -> List diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/MorphoDataItem.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/MorphoDataItem.kt index b8db7a5..43177b5 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/MorphoDataItem.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/MorphoDataItem.kt @@ -1,6 +1,10 @@ package com.morpho.app.model.bluesky import androidx.compose.runtime.Immutable +import com.morpho.app.CommonParcelable +import com.morpho.app.CommonParcelize +import com.morpho.app.CommonRawValue +import com.morpho.app.util.JavaSerializable import kotlinx.serialization.Serializable /** @@ -12,53 +16,64 @@ import kotlinx.serialization.Serializable */ @Immutable @Serializable -sealed interface MorphoDataItem { +@CommonParcelize +sealed interface MorphoDataItem: CommonParcelable, JavaSerializable { + @Immutable + @Serializable + @CommonParcelize sealed interface FeedItem: MorphoDataItem @Immutable @Serializable + @CommonParcelize data class Post( - val post: BskyPost, - val reason: BskyPostReason? = null, + val post: @CommonRawValue BskyPost, + val reason: @CommonRawValue BskyPostReason? = null, ): FeedItem @Immutable @Serializable + @CommonParcelize data class Thread( - val thread: BskyPostThread, - val reason: BskyPostReason? = null, + val thread: @CommonRawValue BskyPostThread, + val reason: @CommonRawValue BskyPostReason? = null, ): FeedItem @Immutable @Serializable + @CommonParcelize data class FeedInfo( - val feed: FeedGenerator, + val feed: @CommonRawValue FeedGenerator, ): MorphoDataItem @Immutable @Serializable + @CommonParcelize data class ProfileItem( - val profile: Profile, + val profile: @CommonRawValue Profile, ): MorphoDataItem @Immutable @Serializable + @CommonParcelize data class ListInfo( - val list: BskyList, + val list: @CommonRawValue BskyList, ): MorphoDataItem @Immutable @Serializable + @CommonParcelize data class ModLabel( - val label: BskyLabelDefinition, + val label: @CommonRawValue BskyLabelDefinition, ): MorphoDataItem @Immutable @Serializable + @CommonParcelize data class LabelService( - val service: BskyLabelService, + val service: @CommonRawValue BskyLabelService, ): MorphoDataItem } diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/Profile.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/Profile.kt index ebc78a5..9bd67e1 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/Profile.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/Profile.kt @@ -4,6 +4,7 @@ package com.morpho.app.model.bluesky import androidx.compose.runtime.Immutable import app.bsky.actor.* import com.morpho.app.model.uidata.Moment +import com.morpho.app.util.JavaSerializable import com.morpho.app.util.mapImmutable import com.morpho.butterfly.AtUri import com.morpho.butterfly.Did @@ -11,7 +12,8 @@ import com.morpho.butterfly.Handle import kotlinx.collections.immutable.toImmutableList import kotlinx.serialization.Serializable - +@Immutable +@Serializable enum class ProfileType { Basic, Detailed, @@ -20,7 +22,7 @@ enum class ProfileType { @Immutable @Serializable -sealed interface Profile { +sealed interface Profile: JavaSerializable { val did: Did val handle: Handle val displayName: String? diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/Reference.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/Reference.kt index e3b6fec..b11cac8 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/Reference.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/Reference.kt @@ -1,9 +1,11 @@ package com.morpho.app.model.bluesky -import kotlinx.serialization.Serializable +import androidx.compose.runtime.Immutable import com.morpho.butterfly.AtUri import com.morpho.butterfly.Cid +import kotlinx.serialization.Serializable +@Immutable @Serializable data class Reference( val uri: AtUri, diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/UISavedFeed.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/UISavedFeed.kt index 8d7c5a1..b128c9f 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/UISavedFeed.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/UISavedFeed.kt @@ -20,10 +20,14 @@ data class UISavedFeed( val list: UserList? = null, ) +@Immutable +@Serializable sealed interface UIFeedType { val type: FeedType val value: String + @Immutable + @Serializable data class Feed( val uri: AtUri ): UIFeedType { @@ -31,6 +35,8 @@ sealed interface UIFeedType { override val value: String = uri.atUri } + @Immutable + @Serializable data class List( val uri: AtUri ): UIFeedType { @@ -38,6 +44,8 @@ sealed interface UIFeedType { override val value: String = uri.atUri } + @Immutable + @Serializable data object Timeline: UIFeedType { override val type: FeedType = FeedType.TIMELINE override val value: String = "following" diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/BskyDataService.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/BskyDataService.kt index ff07320..fdc9fab 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/BskyDataService.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/BskyDataService.kt @@ -28,6 +28,7 @@ import kotlinx.coroutines.channels.BufferOverflow import kotlinx.coroutines.flow.* import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock +import kotlinx.serialization.Serializable import kotlinx.serialization.json.Json import kotlinx.serialization.json.decodeFromJsonElement import kotlinx.serialization.json.encodeToJsonElement @@ -124,6 +125,7 @@ suspend fun getPosts(posts: List, api: Butterfly = getKoin().get val avatar: String? @@ -20,6 +23,7 @@ sealed interface ContentCardMapEntry { data object Home: ContentCardMapEntry, Skyline { override val uri: AtUri = AtUri.HOME_URI override val title: String = "Home" + @Serializable(with = MutableSharedFlowSerializer::class) override val cursorFlow: MutableSharedFlow = initAtCursor() override val avatar: String? = null } @@ -33,6 +37,7 @@ sealed interface ContentCardMapEntry { data class Feed( override val uri: AtUri, override val title: String = uri.atUri, + @Serializable(with = MutableSharedFlowSerializer::class) override val cursorFlow: MutableSharedFlow = initAtCursor(), override val avatar: String? = null, ) : ContentCardMapEntry, Skyline @@ -42,6 +47,7 @@ sealed interface ContentCardMapEntry { data class PostThread( override val uri: AtUri, override val title: String = uri.atUri, + @Serializable(with = MutableSharedFlowSerializer::class) override val cursorFlow: MutableSharedFlow = initAtCursor(), override val avatar: String? = null, ) : ContentCardMapEntry @@ -51,6 +57,7 @@ sealed interface ContentCardMapEntry { data class UserList( override val uri: AtUri, override val title: String = uri.atUri, + @Serializable(with = MutableSharedFlowSerializer::class) override val cursorFlow: MutableSharedFlow = initAtCursor(), override val avatar: String? = null, ) : ContentCardMapEntry @@ -60,6 +67,7 @@ sealed interface ContentCardMapEntry { data class FeedList( override val uri: AtUri, override val title: String = uri.atUri, + @Serializable(with = MutableSharedFlowSerializer::class) override val cursorFlow: MutableSharedFlow = initAtCursor(), override val avatar: String? = null, ) : ContentCardMapEntry @@ -69,6 +77,7 @@ sealed interface ContentCardMapEntry { data class ServiceList( override val uri: AtUri, override val title: String = uri.atUri, + @Serializable(with = MutableSharedFlowSerializer::class) override val cursorFlow: MutableSharedFlow = initAtCursor(), override val avatar: String? = null, ) : ContentCardMapEntry @@ -79,6 +88,7 @@ sealed interface ContentCardMapEntry { val id: AtIdentifier, override val uri: AtUri = AtUri.profileUri(id), override val title: String = uri.atUri, + @Serializable(with = MutableSharedFlowSerializer::class) override val cursorFlow: MutableSharedFlow = initAtCursor(), override val avatar: String? = null, ) : ContentCardMapEntry diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/ContentLabelService.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/ContentLabelService.kt index 42b0964..5f99bc6 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/ContentLabelService.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/ContentLabelService.kt @@ -12,6 +12,7 @@ import app.bsky.actor.Visibility import com.atproto.label.LabelValue import com.atproto.label.Severity import com.morpho.app.model.bluesky.* +import com.morpho.app.util.JavaSerializable import com.morpho.butterfly.AtUri import com.morpho.butterfly.Butterfly import com.morpho.butterfly.Language @@ -27,31 +28,41 @@ import org.koin.core.component.KoinComponent import org.koin.core.component.inject import org.lighthousegames.logging.logging -@Immutable -@Serializable + data class ContentHandling( val scope: LabelScope, val action: LabelAction, val source: LabelDescription, val id: String, - @Contextual + val icon: ImageVector, -) +): JavaSerializable + -sealed interface LabelDescription { +@Immutable +@Serializable +sealed interface LabelDescription: JavaSerializable { val name: String val description: String + @Immutable + @Serializable sealed interface Block: LabelDescription + @Immutable + @Serializable data object Blocking: Block { override val name: String = "User Blocked" override val description: String = "You have blocked this user. You cannot view their content" } + @Immutable + @Serializable data object BlockedBy: Block { override val name: String = "User Blocking You" override val description: String = "This user has blocked you. You cannot view their content." } + @Immutable + @Serializable data class BlockList( val listName: String, val listUri: AtUri, @@ -59,12 +70,18 @@ sealed interface LabelDescription { override val name: String = "User Blocked by $listName" override val description: String = "This user is on a block list you subscribe to. You cannot view their content." } + @Immutable + @Serializable data object OtherBlocked: Block { override val name: String = "Content Not Available" override val description: String = "This content is not available because one of the users involved has blocked the other." } + @Immutable + @Serializable sealed interface Muted: LabelDescription + @Immutable + @Serializable data class MuteList( val listName: String, val listUri: AtUri, @@ -72,20 +89,28 @@ sealed interface LabelDescription { override val name: String = "User Muted by $listName" override val description: String = "This user is on a mute list you subscribe to." } + @Immutable + @Serializable data object YouMuted: Muted { override val name: String = "Account Muted" override val description: String = "You have muted this user." } + @Immutable + @Serializable data class MutedWord(val word: String): Muted { override val name: String = "Post Hidden by Muted Word" override val description: String = "This post contains the word or tag \"$word\". You've chosen to hide it." } + @Immutable + @Serializable data class HiddenPost(val uri: AtUri): LabelDescription { override val name: String = "Post Hidden by You" override val description: String = "You have hidden this post." } + @Immutable + @Serializable data class Label( override val name: String, override val description: String, @@ -93,26 +118,40 @@ sealed interface LabelDescription { ): LabelDescription } -sealed interface LabelSource { +@Immutable +@Serializable +sealed interface LabelSource: JavaSerializable { + @Immutable + @Serializable data object User: LabelSource + @Immutable + @Serializable data class List( val list: BskyList, ): LabelSource + @Immutable + @Serializable data class Labeler( val labeler: BskyLabelService, ): LabelSource } -sealed interface LabelCause { +@Immutable +@Serializable +sealed interface LabelCause: JavaSerializable { val downgraded: Boolean val priority: Int val source: LabelSource + @Immutable + @Serializable data class Blocking( override val source: LabelSource, override val downgraded: Boolean, ): LabelCause { override val priority: Int = 3 } + @Immutable + @Serializable data class BlockedBy( override val source: LabelSource, override val downgraded: Boolean, @@ -120,6 +159,8 @@ sealed interface LabelCause { override val priority: Int = 4 } + @Immutable + @Serializable data class BlockOther( override val source: LabelSource, override val downgraded: Boolean, @@ -127,6 +168,8 @@ sealed interface LabelCause { override val priority: Int = 4 } + @Immutable + @Serializable data class Label( override val source: LabelSource, val label: BskyLabel, @@ -146,6 +189,8 @@ sealed interface LabelCause { } } + @Immutable + @Serializable data class Muted( override val source: LabelSource, override val downgraded: Boolean, @@ -153,6 +198,8 @@ sealed interface LabelCause { override val priority: Int = 6 } + @Immutable + @Serializable data class MutedWord( override val source: LabelSource, override val downgraded: Boolean, @@ -160,6 +207,8 @@ sealed interface LabelCause { override val priority: Int = 6 } + @Immutable + @Serializable data class Hidden( override val source: LabelSource, override val downgraded: Boolean, @@ -186,7 +235,7 @@ open class InterpretedLabelDefinition( val localizedDescription: String = "", @Contextual val allDescriptions: ImmutableMap = persistentMapOf(), -) { +): JavaSerializable { companion object { } @@ -225,6 +274,8 @@ val LABELS: PersistentMap = persistentMa LabelValue.NUDITY to Nudity, LabelValue.GRAPHIC_MEDIA to GraphicMedia, ) +@Immutable +@Serializable data object Hide: InterpretedLabelDefinition( "!hide", false, @@ -256,6 +307,8 @@ data object Hide: InterpretedLabelDefinition( localizedDescription = "Hide", ) +@Immutable +@Serializable data object Warn: InterpretedLabelDefinition( "!warn", false, @@ -287,6 +340,8 @@ data object Warn: InterpretedLabelDefinition( localizedDescription = "Warn", ) +@Immutable +@Serializable data object NoUnauthed: InterpretedLabelDefinition( "!no-unauthenticated", false, @@ -318,6 +373,8 @@ data object NoUnauthed: InterpretedLabelDefinition( localizedDescription = "Do not show to unauthenticated users", ) +@Immutable +@Serializable data object Porn: InterpretedLabelDefinition( "porn", true, @@ -342,6 +399,8 @@ data object Porn: InterpretedLabelDefinition( localizedDescription = "This content is sexually explicit", ) +@Immutable +@Serializable data object Sexual: InterpretedLabelDefinition( "sexual", true, @@ -366,6 +425,8 @@ data object Sexual: InterpretedLabelDefinition( localizedDescription = "This content may be suggestive or sexual in nature", ) +@Immutable +@Serializable data object Nudity: InterpretedLabelDefinition( "nudity", true, @@ -390,6 +451,8 @@ data object Nudity: InterpretedLabelDefinition( localizedDescription = "This content contains nudity, artistic or otherwise", ) +@Immutable +@Serializable data object GraphicMedia: InterpretedLabelDefinition( "graphic-media", true, diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/Delta.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/Delta.kt index e95abff..e81ecd4 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/Delta.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/Delta.kt @@ -1,9 +1,12 @@ package com.morpho.app.model.uidata +import androidx.compose.runtime.Immutable +import com.morpho.app.util.JavaSerializable import kotlinx.serialization.Serializable import kotlin.time.Duration +@Immutable @Serializable data class Delta( val duration: Duration, -) \ No newline at end of file +): JavaSerializable \ No newline at end of file diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/FeedInfo.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/FeedInfo.kt index 376e7f6..3e2e321 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/FeedInfo.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/FeedInfo.kt @@ -6,15 +6,19 @@ import androidx.compose.runtime.Immutable import androidx.compose.ui.graphics.vector.ImageVector import com.morpho.app.model.bluesky.FeedGenerator import com.morpho.app.model.bluesky.UserList +import com.morpho.app.util.JavaSerializable import com.morpho.butterfly.AtUri +import kotlinx.serialization.Serializable +import kotlinx.serialization.Transient @Immutable +@Serializable data class FeedInfo( val uri: AtUri, val name: String, val description: String? = null, val avatar: String? = null, - val icon: ImageVector = Icons.Default.RssFeed, + @Transient val icon: ImageVector = Icons.Default.RssFeed, val feed: FeedGenerator? = null, val list: UserList? = null, -) \ No newline at end of file +): JavaSerializable \ No newline at end of file diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/MorphoData.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/MorphoData.kt index 8ef8aa1..0321604 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/MorphoData.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/MorphoData.kt @@ -1,12 +1,13 @@ package com.morpho.app.model.uidata //import com.rickclephas.kmp.nativecoroutines.NativeCoroutines +import androidx.compose.runtime.Immutable import androidx.compose.ui.util.fastAny import com.morpho.app.model.bluesky.MorphoDataFeed import com.morpho.app.model.bluesky.MorphoDataItem import com.morpho.app.model.uistate.FeedType +import com.morpho.app.util.JavaSerializable import com.morpho.butterfly.* -import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toPersistentList import kotlinx.datetime.Instant import kotlinx.serialization.Serializable @@ -15,16 +16,27 @@ import kotlinx.serialization.json.JsonObject typealias AtCursor = String? +@Immutable @Serializable data class MorphoData( val title: String = "Home", val uri: AtUri = AtUri.HOME_URI, val cursor: AtCursor = null, - val items: List = persistentListOf(), + val items: List = listOf(), val query: JsonElement = JsonObject(emptyMap()), -) { +): JavaSerializable { companion object { + fun EMPTY(): MorphoData { + return MorphoData( + title = "Home", + uri = AtUri.HOME_URI, + cursor = null, + items = listOf(), + query = JsonObject(emptyMap()), + ) + } + fun concat( first: MorphoData, diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/SettingsService.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/SettingsService.kt index 74ede82..f3177bd 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/SettingsService.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/SettingsService.kt @@ -44,53 +44,53 @@ class SettingsService: KoinComponent { val languages: Flow> = currentUserPrefs.transform { if(it != null) emit(it.preferences.languages) - } + }.distinctUntilChanged() val notificationsFilter: Flow = currentUserPrefs.transform { if(it?.morphoPrefs?.notificationsFilter != null) emit(it.morphoPrefs.notificationsFilter) - } + }.distinctUntilChanged() val threadViewPrefs: Flow = currentUserPrefs.transform { if(it?.preferences?.threadViewPrefs != null) emit(it.preferences.threadViewPrefs!!) - } + }.distinctUntilChanged() val feedViewPrefs: Flow> = currentUserPrefs.transform { if(it?.preferences?.feedViewPrefs != null) emit(it.preferences.feedViewPrefs) - } + }.distinctUntilChanged() val mergeFeeds: Flow = currentUserPrefs.transform { if(it?.preferences?.mergeFeeds != null) emit(it.preferences.mergeFeeds) - } + }.distinctUntilChanged() val contentLabelPrefs: Flow> = currentUserPrefs.transform { if(it?.preferences?.contentLabelPrefs != null) emit(it.preferences.contentLabelPrefs) - } + }.distinctUntilChanged() val mutedWords: Flow> = currentUserPrefs.transform { if(it?.preferences?.mutedWords != null) emit(it.preferences.mutedWords) - } + }.distinctUntilChanged() val mutedUsers: Flow> = currentUserPrefs.transform { if(it?.preferences?.mutes != null) emit(it.preferences.mutes) - } + }.distinctUntilChanged() val hiddenPosts: Flow> = currentUserPrefs.transform { if(it?.preferences?.hiddenPosts != null) emit(it.preferences.hiddenPosts) - } + }.distinctUntilChanged() val showAdultContent: Flow = currentUserPrefs.transform { if(it?.preferences?.adultContent?.enabled != null) emit(it.preferences.adultContent?.enabled ?: false) - } + }.distinctUntilChanged() val savedFeeds: Flow> = currentUserPrefs.transform { preferences -> if(preferences?.preferences?.savedFeeds != null) emit(preferences.preferences.savedFeeds!!.items.map { it.toUISavedFeed(api) }) - } + }.distinctUntilChanged() val pinnedFeeds: Flow> = currentUserPrefs.transform { preferences -> if(preferences?.preferences?.savedFeeds != null) emit(preferences.preferences.savedFeeds!!.items.filter { it.pinned }.map { it.toUISavedFeed(api) }) - } + }.distinctUntilChanged() val labelers: Flow> = currentUserPrefs.transformLatest { preferences -> if (preferences?.preferences?.labelers?.isNotEmpty() == true) @@ -108,7 +108,7 @@ class SettingsService: KoinComponent { } }.getOrNull() } ?: emptyList()) - } + }.distinctUntilChanged() init { diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uistate/ContentCardState.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uistate/ContentCardState.kt index c8ce42c..2670414 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uistate/ContentCardState.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uistate/ContentCardState.kt @@ -2,6 +2,7 @@ package com.morpho.app.model.uistate import com.morpho.app.model.bluesky.* import com.morpho.app.model.uidata.MorphoData +import com.morpho.app.util.JavaSerializable import com.morpho.butterfly.AtUri import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -10,7 +11,7 @@ import kotlinx.serialization.Serializable @Suppress("unused") @Serializable -sealed interface ContentCardState { +sealed interface ContentCardState: JavaSerializable { val uri: AtUri val feed: MorphoData val hasNewPosts: Boolean diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uistate/LoadingState.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uistate/LoadingState.kt index 5a65c97..b099704 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uistate/LoadingState.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uistate/LoadingState.kt @@ -1,12 +1,24 @@ package com.morpho.app.model.uistate -sealed interface UiLoadingState { +import androidx.compose.runtime.Immutable +import com.morpho.app.CommonParcelable +import com.morpho.app.CommonParcelize +import com.morpho.app.util.JavaSerializable +import kotlinx.serialization.Serializable + +@CommonParcelize +@Immutable +@Serializable +sealed interface UiLoadingState: CommonParcelable, JavaSerializable { data object Loading : UiLoadingState data object Idle : UiLoadingState data class Error(val errorMessage: String) : UiLoadingState } -sealed interface ContentLoadingState { +@CommonParcelize +@Immutable +@Serializable +sealed interface ContentLoadingState: CommonParcelable, JavaSerializable { data object Loading : ContentLoadingState data object Idle : ContentLoadingState data class Error(val errorMessage: String) : ContentLoadingState diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uistate/LoginState.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uistate/LoginState.kt index b862fae..16b2b3f 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uistate/LoginState.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uistate/LoginState.kt @@ -5,12 +5,14 @@ import com.morpho.butterfly.auth.AuthInfo import com.morpho.butterfly.auth.Credentials import kotlinx.serialization.Serializable +@Immutable @Serializable enum class LoginScreenMode { SIGN_UP, SIGN_IN } +@Immutable @Serializable sealed interface AuthState { data object NoAuth : AuthState diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uistate/NotificationsState.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uistate/NotificationsState.kt index c865dd9..2d64fc2 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uistate/NotificationsState.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uistate/NotificationsState.kt @@ -13,6 +13,7 @@ import kotlinx.serialization.Serializable import org.koin.core.component.KoinComponent + @Serializable data class NotificationsUIState( private val notificationsList: StateFlow = MutableStateFlow(NotificationsList()), diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uistate/PostThreadContentState.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uistate/PostThreadContentState.kt index 614a128..a2fc50e 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uistate/PostThreadContentState.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uistate/PostThreadContentState.kt @@ -1,6 +1,9 @@ package com.morpho.app.model.uistate -interface PostThreadContentState { +import com.morpho.app.util.JavaSerializable + + +interface PostThreadContentState: JavaSerializable { val hasNewPosts: Boolean val loadingState: ContentLoadingState val isLoading: Boolean diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uistate/SkylineState.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uistate/SkylineState.kt index 7326907..10ff828 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uistate/SkylineState.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uistate/SkylineState.kt @@ -1,9 +1,11 @@ package com.morpho.app.model.uistate +import androidx.compose.runtime.Immutable import com.morpho.app.model.bluesky.MorphoDataItem import com.morpho.app.model.uidata.MorphoData import kotlinx.serialization.Serializable + interface SkylineContentState { val hasNewPosts: Boolean val feed: MorphoData @@ -12,6 +14,7 @@ interface SkylineContentState { get() = loadingState == ContentLoadingState.Loading } +@Immutable @Serializable data class SkylineState( override val feed: MorphoData, @@ -19,6 +22,8 @@ data class SkylineState( override val hasNewPosts: Boolean = false, ): SkylineContentState +@Immutable +@Serializable enum class FeedType { HOME, PROFILE_POSTS, diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uistate/TabbedScreenState.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uistate/TabbedScreenState.kt index 5b431ee..b3f6061 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uistate/TabbedScreenState.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uistate/TabbedScreenState.kt @@ -3,30 +3,31 @@ package com.morpho.app.model.uistate //import com.rickclephas.kmp.nativecoroutines.NativeCoroutines +import androidx.compose.runtime.Immutable import com.morpho.app.model.bluesky.MorphoDataItem import com.morpho.app.model.uidata.ContentCardMapEntry +import com.morpho.app.util.StateFlowSerializer import com.morpho.butterfly.AtUri -import kotlinx.collections.immutable.ImmutableMap -import kotlinx.collections.immutable.toImmutableMap import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.serialization.Serializable +@Immutable @Serializable data class TabbedScreenState( override val loadingState: UiLoadingState = UiLoadingState.Idle, + @Serializable(with = StateFlowSerializer::class) val tabs: StateFlow> = MutableStateFlow>(listOf()).asStateFlow(), val tabStates: List>> = listOf(), ): UiState { - val tabMap: ImmutableMap> + val tabMap: Map> get() = tabStates.associateBy { it.value.uri } .filter { entry -> entry.value.value.uri in tabs.value.map { it.uri } } - .mapValues { it.value.value } - .toImmutableMap() + .mapValues { it.value.value }.toMap() val tabsWithNewPosts: List get() = tabMap.filterValues { it.hasNewPosts }.keys.toList() @@ -34,18 +35,20 @@ data class TabbedScreenState( +@Immutable +@Serializable data class TabbedProfileScreenState( override val loadingState: UiLoadingState = UiLoadingState.Idle, + @Serializable(with = StateFlowSerializer::class) val tabs: StateFlow> = MutableStateFlow>(listOf()).asStateFlow(), val tabStates: List>> = listOf(), ): UiState { - val tabMap: ImmutableMap> + val tabMap: Map> get() = tabStates.associateBy { it.value.uri } .filter { entry -> entry.value.value.uri in tabs.value.map { it.uri } } - .mapValues { it.value.value } - .toImmutableMap() + .mapValues { it.value.value }.toMap() val tabsWithNewPosts: List get() = tabMap.filterValues { it.hasNewPosts }.keys.toList() diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/base/BaseScreenModel.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/base/BaseScreenModel.kt index 0271a09..bba0016 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/base/BaseScreenModel.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/base/BaseScreenModel.kt @@ -1,7 +1,7 @@ package com.morpho.app.screens.base -import cafe.adriel.voyager.core.model.ScreenModel -import cafe.adriel.voyager.core.model.screenModelScope +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope import com.morpho.app.data.PreferencesRepository import com.morpho.app.model.bluesky.BskyPost import com.morpho.app.model.uidata.BskyNotificationService @@ -18,7 +18,7 @@ import org.koin.core.component.KoinComponent import org.koin.core.component.inject import org.lighthousegames.logging.logging -open class BaseScreenModel : ScreenModel, KoinComponent { +open class BaseScreenModel : ViewModel(), KoinComponent { val api: Butterfly by inject() val preferences: PreferencesRepository by inject() val notifService: BskyNotificationService by inject() @@ -31,11 +31,11 @@ open class BaseScreenModel : ScreenModel, KoinComponent { val log = logging() } - fun createRecord(record: RecordUnion) = screenModelScope.launch(Dispatchers.IO) { + fun createRecord(record: RecordUnion) = viewModelScope.launch(Dispatchers.IO) { api.createRecord(record) } - fun deleteRecord(type: RecordType, rkey: AtUri) = screenModelScope.launch(Dispatchers.IO) { + fun deleteRecord(type: RecordType, rkey: AtUri) = viewModelScope.launch(Dispatchers.IO) { api.deleteRecord(type, rkey) } diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/base/tabbed/NavigationTabs.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/base/tabbed/NavigationTabs.kt index 43344e0..3f6749b 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/base/tabbed/NavigationTabs.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/base/tabbed/NavigationTabs.kt @@ -6,12 +6,14 @@ import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.* -import cafe.adriel.voyager.core.lifecycle.LifecycleEffect -import cafe.adriel.voyager.core.model.rememberScreenModel -import cafe.adriel.voyager.core.model.screenModelScope +import androidx.compose.ui.Modifier +import androidx.lifecycle.viewModelScope +import cafe.adriel.voyager.core.annotation.ExperimentalVoyagerApi +import cafe.adriel.voyager.core.lifecycle.LifecycleEffectOnce import cafe.adriel.voyager.core.screen.Screen import cafe.adriel.voyager.core.screen.ScreenKey -import cafe.adriel.voyager.koin.getNavigatorScreenModel +import cafe.adriel.voyager.core.screen.uniqueScreenKey +import cafe.adriel.voyager.jetpack.navigatorViewModel import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.Navigator import cafe.adriel.voyager.navigator.currentOrThrow @@ -19,26 +21,31 @@ import com.morpho.app.model.uistate.ContentCardState import com.morpho.app.screens.main.tabbed.TabbedHomeView import com.morpho.app.screens.main.tabbed.TabbedMainScreenModel import com.morpho.app.screens.notifications.NotificationViewContent -import com.morpho.app.screens.notifications.TabbedNotificationScreenModel import com.morpho.app.screens.profile.TabbedProfileContent -import com.morpho.app.screens.profile.TabbedProfileViewModel import com.morpho.app.screens.thread.ThreadTopBar import com.morpho.app.screens.thread.ThreadViewContent import com.morpho.app.ui.common.LoadingCircle import com.morpho.app.ui.common.TabbedScreenScaffold +import com.morpho.app.util.JavaSerializable import com.morpho.butterfly.AtIdentifier import com.morpho.butterfly.AtUri import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.launch +import kotlinx.serialization.Contextual +import kotlinx.serialization.Serializable +import kotlinx.serialization.Transient +@Immutable +@Serializable data class TabScreenOptions( val index: Int, val icon: @Composable () -> Unit, val title: String, ) -interface TabScreen: Screen { + +interface TabScreen: Screen, JavaSerializable { val navBar: @Composable (Navigator) -> Unit @@ -50,18 +57,23 @@ interface TabScreen: Screen { } +@Immutable +@Serializable data class HomeTab( val k: ScreenKey = "HomeTab" ): TabScreen { - override val navBar: @Composable (Navigator) -> Unit = { n -> + @kotlin.jvm.Transient @Transient override val navBar: @Composable (@Contextual Navigator) -> Unit = { n -> TabbedNavBar(options.index, n) } - override val key: ScreenKey = "${k}_${hashCode()}" + override val key: ScreenKey + get() = "${k}_${hashCode()}${uniqueScreenKey}" + @OptIn(ExperimentalVoyagerApi::class) @Composable override fun Content() { - TabbedHomeView() + val sm = navigatorViewModel { TabbedMainScreenModel() } + TabbedHomeView(sm) } override val options: TabScreenOptions @@ -77,11 +89,14 @@ data class HomeTab( } +@Immutable +@Serializable data object SearchTab: TabScreen { - override val key: ScreenKey = "searchTab" + override val key: ScreenKey + get() = "searchTab${uniqueScreenKey}" - override val navBar: @Composable (Navigator) -> Unit = { n -> + @kotlin.jvm.Transient @Transient override val navBar: @Composable (@Contextual Navigator) -> Unit = { n -> TabbedNavBar(options.index, n) } @Composable @@ -102,11 +117,14 @@ data object SearchTab: TabScreen { } +@Immutable +@Serializable data object FeedsTab: TabScreen { - override val key: ScreenKey = "feedsTab" + override val key: ScreenKey + get() = "feedsTab${uniqueScreenKey}" - override val navBar: @Composable (Navigator) -> Unit = { n -> + @kotlin.jvm.Transient @Transient override val navBar: @Composable (@Contextual Navigator) -> Unit = { n -> TabbedNavBar(options.index, n) } @Composable @@ -126,20 +144,23 @@ data object FeedsTab: TabScreen { } +@Immutable +@Serializable data object NotificationsTab: TabScreen { - override val key: ScreenKey = "notificationsTab" + override val key: ScreenKey + get() = "notificationsTab${uniqueScreenKey}" - override val navBar: @Composable (Navigator) -> Unit = { n -> + @kotlin.jvm.Transient @Transient override val navBar: @Composable (@Contextual Navigator) -> Unit = { n -> TabbedNavBar(options.index, n) } + @OptIn(ExperimentalVoyagerApi::class) @Composable override fun Content() { val navigator = LocalNavigator.currentOrThrow NotificationViewContent( navigator, - navigator.getNavigatorScreenModel() ) } @@ -158,20 +179,17 @@ data object NotificationsTab: TabScreen { data class ProfileTab( val id: AtIdentifier, - ): TabScreen { +): TabScreen { override val key: ScreenKey = "profileTab_${id}_${hashCode()}" - override val navBar: @Composable (Navigator) -> Unit = { n -> + @kotlin.jvm.Transient @Transient override val navBar: @Composable (@Contextual Navigator) -> Unit = { n -> TabbedNavBar(options.index, n) } @OptIn(ExperimentalMaterial3Api::class) @Composable override fun Content() { - val screenModel = rememberScreenModel { TabbedProfileViewModel(id) } - val ownProfile = remember { screenModel.api.atpUser?.id == id } - TabbedProfileContent(ownProfile, screenModel) - + TabbedProfileContent(id) } @@ -188,60 +206,66 @@ data class ProfileTab( } +@Immutable +@Serializable data class ThreadTab( val uri: AtUri, ): TabScreen { - override val key: ScreenKey = "threadTab_${uri}_${hashCode()}" + override val key: ScreenKey + get() = "threadTab_${uri}_$uniqueScreenKey}" - override val navBar: @Composable (Navigator) -> Unit = { n -> - TabbedNavBar(options.index, n) + @kotlin.jvm.Transient @Transient override val navBar: @Composable (@Contextual Navigator) -> Unit = { n -> + TabbedNavBar(options.index, n) + } + @OptIn(ExperimentalVoyagerApi::class) + @Composable + override fun Content() { + val navigator = LocalNavigator.currentOrThrow + val sm = navigatorViewModel { TabbedMainScreenModel() } + var threadState: StateFlow? by remember { mutableStateOf(null)} + LifecycleEffectOnce { + sm.viewModelScope.launch { threadState = sm.loadThread(uri) } } - @Composable - override fun Content() { - val navigator = LocalNavigator.currentOrThrow - val sm = navigator.getNavigatorScreenModel() - var threadState: StateFlow? by remember { mutableStateOf(null)} - LifecycleEffect( - onStarted = { - sm.screenModelScope.launch { threadState = sm.loadThread(uri) } - } - ) - if(threadState != null) { - ThreadViewContent(threadState!!, navigator, sm) - } else { - TabbedScreenScaffold( + if(threadState != null) { + ThreadViewContent(threadState!!, navigator) + } else { + TabbedScreenScaffold( navBar = { navBar(navigator) }, topContent = { ThreadTopBar(navigator = navigator) }, - content = { _ -> LoadingCircle() } - ) - } + content = { _, _ -> LoadingCircle() }, + state = threadState, + modifier = Modifier + ) } + } - override val options: TabScreenOptions - @Composable get() { - return TabScreenOptions( - index = 6, - icon = { Icon(Icons.Default.NotificationsNone, contentDescription = "Thread", - tint = MaterialTheme.colorScheme.onBackground) }, - title = "Thread" - ) - } + override val options: TabScreenOptions + @Composable get() { + return TabScreenOptions( + index = 6, + icon = { Icon(Icons.Default.NotificationsNone, contentDescription = "Thread", + tint = MaterialTheme.colorScheme.onBackground) }, + title = "Thread" + ) + } } - +@Immutable +@Serializable data object MyProfileTab: TabScreen { - override val key: ScreenKey = "myProfileTab" + override val key: ScreenKey + get() = "myProfileTab${uniqueScreenKey}" - override val navBar: @Composable (Navigator) -> Unit = { n -> + override val navBar: @Composable (@Contextual Navigator) -> Unit = { n -> TabbedNavBar(options.index, n) } @OptIn(ExperimentalMaterial3Api::class) @Composable override fun Content() { - TabbedProfileContent(true) + TabbedProfileContent() } diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/base/tabbed/TabbedBaseScreen.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/base/tabbed/TabbedBaseScreen.kt index 25dd5df..ef6b388 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/base/tabbed/TabbedBaseScreen.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/base/tabbed/TabbedBaseScreen.kt @@ -1,6 +1,7 @@ package com.morpho.app.screens.base.tabbed import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.systemBarsPadding import androidx.compose.material3.* import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState @@ -12,34 +13,43 @@ import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.semantics import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp +import cafe.adriel.voyager.core.annotation.ExperimentalVoyagerApi import cafe.adriel.voyager.core.screen.ScreenKey +import cafe.adriel.voyager.jetpack.ProvideNavigatorLifecycleKMPSupport import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.Navigator +import cafe.adriel.voyager.navigator.NavigatorDisposeBehavior import cafe.adriel.voyager.navigator.currentOrThrow import cafe.adriel.voyager.navigator.tab.Tab import cafe.adriel.voyager.navigator.tab.TabNavigator import cafe.adriel.voyager.navigator.tab.TabOptions import com.morpho.app.model.uidata.BskyDataService import com.morpho.app.model.uidata.BskyNotificationService -import com.morpho.app.screens.main.tabbed.SlideTabTransition +import com.morpho.app.ui.common.SlideTabTransition import com.morpho.app.ui.theme.roundedTopR import io.ktor.util.reflect.instanceOf +import kotlinx.serialization.Serializable import org.koin.compose.koinInject import kotlin.math.min - +@Serializable data object TabbedBaseScreen: Tab { override val key: ScreenKey = "TabbedBaseScreen_${hashCode()}" - @OptIn(ExperimentalMaterial3Api::class) + @OptIn(ExperimentalMaterial3Api::class, ExperimentalVoyagerApi::class) @Composable override fun Content() { - Navigator( - HomeTab("startHome"), - ) { navigator -> - /*LaunchedEffect(Unit) { navigator.replaceAll(HomeTab("startHome2")) }*/ - SlideTabTransition(navigator) + ProvideNavigatorLifecycleKMPSupport { + Navigator( + HomeTab("startHome"), + disposeBehavior = NavigatorDisposeBehavior( + disposeNestedNavigators = false, + ) + ) { navigator -> + /*LaunchedEffect(Unit) { navigator.replaceAll(HomeTab("startHome2")) }*/ + SlideTabTransition(navigator) + } } } @@ -72,6 +82,7 @@ fun TabNavigationItem( when { nav.lastItem.key == tab.key -> return@Tab newIndex == 0 -> nav.replaceAll(tab) + nav.items.contains(tab) -> nav.popUntil { it == tab } else -> nav.push(tab) } }, @@ -133,7 +144,7 @@ fun TabbedNavBar( selectedTabIndex = min(selectedTab, 4), modifier = Modifier.clip( roundedTopR.medium - ), + ).systemBarsPadding(), indicator = { if (selectedTab <= 4) { TabRowDefaults.PrimaryIndicator( diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/login/LoginScreen.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/login/LoginScreen.kt index d7ceb51..7f9814e 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/login/LoginScreen.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/login/LoginScreen.kt @@ -13,18 +13,24 @@ import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.PasswordVisualTransformation import androidx.compose.ui.unit.dp -import cafe.adriel.voyager.core.model.screenModelScope +import androidx.lifecycle.viewModelScope +import androidx.lifecycle.viewmodel.compose.viewModel import cafe.adriel.voyager.core.screen.ScreenKey -import cafe.adriel.voyager.koin.getScreenModel import cafe.adriel.voyager.navigator.tab.LocalTabNavigator import cafe.adriel.voyager.navigator.tab.Tab import cafe.adriel.voyager.navigator.tab.TabOptions +import com.morpho.app.CommonParcelable +import com.morpho.app.CommonParcelize import com.morpho.app.model.uistate.AuthState import com.morpho.app.screens.base.tabbed.TabbedBaseScreen import com.morpho.app.ui.common.LoadingCircle import kotlinx.coroutines.launch +import kotlinx.serialization.Serializable + +@CommonParcelize +@Serializable +data object LoginScreen: Tab, CommonParcelable { -data object LoginScreen: Tab { override val key: ScreenKey = hashCode().toString() + "TabbedLoginScreen" @Composable @@ -34,7 +40,7 @@ data object LoginScreen: Tab { val focusManager = LocalFocusManager.current val snackbarHostState = remember { SnackbarHostState() } val tabNavigator = LocalTabNavigator.current - val screenModel = getScreenModel() + val screenModel = viewModel { LoginScreenModel() } if(screenModel.isLoggedIn) { tabNavigator.current = TabbedBaseScreen @@ -142,7 +148,7 @@ fun SignupView( Button(onClick = { if(screenModel.handle.isNotBlank() && screenModel.password.isNotBlank() && !isAppPassword(screenModel.password) && !appPWOverride) { - screenModel.screenModelScope.launch { + screenModel.viewModelScope.launch { val result = snackbarHostState.showSnackbar( message = "Please Use an App Password", actionLabel = "Security Sucks", @@ -164,7 +170,7 @@ fun SignupView( screenModel.onLoginClicked(screenModel.handle) focusManager.clearFocus() } else { - screenModel.screenModelScope.launch { + screenModel.viewModelScope.launch { snackbarHostState.showSnackbar( message = "Handle/Email or Password missing", withDismissAction = true @@ -226,7 +232,7 @@ fun LoginView( Button(onClick = { if(screenModel.handle.isNotBlank() && screenModel.password.isNotBlank() && !isAppPassword(screenModel.password) && !appPWOverride) { - screenModel.screenModelScope.launch { + screenModel.viewModelScope.launch { val result = snackbarHostState.showSnackbar( message = "Please Use an App Password", actionLabel = "Security Sucks", @@ -248,7 +254,7 @@ fun LoginView( screenModel.onLoginClicked(screenModel.handle) focusManager.clearFocus() } else { - screenModel.screenModelScope.launch { + screenModel.viewModelScope.launch { snackbarHostState.showSnackbar( message = "Handle/Email or Password missing", withDismissAction = true diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/login/LoginScreenModel.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/login/LoginScreenModel.kt index 32f6b35..3ecc613 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/login/LoginScreenModel.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/login/LoginScreenModel.kt @@ -3,7 +3,7 @@ package com.morpho.app.screens.login import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue -import cafe.adriel.voyager.core.model.screenModelScope +import androidx.lifecycle.viewModelScope import com.morpho.app.model.uistate.AuthState import com.morpho.app.model.uistate.LoginState import com.morpho.app.model.uistate.UiLoadingState @@ -38,7 +38,7 @@ class LoginScreenModel: BaseScreenModel() { // If the url is fucked up, just try Bluesky if(checkValidUrl(service) != null) Server.CustomServer(service) else Server.BlueskySocial } - screenModelScope.launch { + viewModelScope.launch { api.makeLoginRequest(credentials, server).onSuccess { loginState = loginState.copy( loadingState = UiLoadingState.Idle, diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/main/MainScreenModel.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/main/MainScreenModel.kt index 96dc8c2..1092de8 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/main/MainScreenModel.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/main/MainScreenModel.kt @@ -5,13 +5,13 @@ import androidx.compose.material.icons.filled.Home import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue +import androidx.lifecycle.viewModelScope import app.bsky.actor.GetProfileQuery import app.bsky.actor.SavedFeed import app.bsky.feed.GetFeedGeneratorsQuery import app.bsky.feed.GetPostThreadQuery import app.bsky.feed.GetPostThreadResponseThreadUnion import app.bsky.feed.GetPostsQuery -import cafe.adriel.voyager.core.model.screenModelScope import cafe.adriel.voyager.core.stack.mutableStateStackOf import com.morpho.app.model.bluesky.* import com.morpho.app.model.uidata.* @@ -73,16 +73,7 @@ open class MainScreenModel: BaseScreenModel() { if(initialized) return@runBlocking initialized = true userId = api.atpUser?.id - screenModelScope.launch(Dispatchers.Default) { - settings.pinnedFeeds.collect { feeds -> - log.d { "Pinned Feeds: $feeds" } - pinnedFeeds.value = feeds - } - settings.savedFeeds.collect { feeds -> - log.d { "Saved Feeds: $feeds" } - savedFeeds.value = feeds - } - } + if(userId != null){ if(preferences.prefs.firstOrNull().isNullOrEmpty()){ val prefs = userId?.let { @@ -118,7 +109,19 @@ open class MainScreenModel: BaseScreenModel() { } } - if(populateFeeds) initFeeds() + if(populateFeeds) { + viewModelScope.launch(Dispatchers.Default) { + settings.pinnedFeeds.collect { feeds -> + log.d { "Pinned Feeds: $feeds" } + pinnedFeeds.value = feeds + } + settings.savedFeeds.collect { feeds -> + log.d { "Saved Feeds: $feeds" } + savedFeeds.value = feeds + } + } + initFeeds() + } } fun getFeedInfo(uri: AtUri) : FeedInfo? { @@ -234,7 +237,7 @@ open class MainScreenModel: BaseScreenModel() { if(feed == null) { emit(null); return@flow } if(update == null) dataService.peekLatest(feed.value.feed).onEach { emit(it) } else dataService.peekLatest(feed.value.feed, update).onEach { emit(it) } - }.stateIn(screenModelScope) + }.stateIn(viewModelScope) suspend fun loadThread(uri: AtUri): StateFlow? { val state = _threadStates.firstOrNull { it.value.uri == uri } @@ -277,7 +280,7 @@ open class MainScreenModel: BaseScreenModel() { } } emit(r.getOrDefault(state.copy(loadingState = ContentLoadingState.Error("Failed to load thread")))) - }.stateIn(screenModelScope) + }.stateIn(viewModelScope) private fun indexOf(state: ContentCardState): Int { return when(state) { @@ -316,7 +319,7 @@ open class MainScreenModel: BaseScreenModel() { } else { val i = _feedStates.indexOf(feedState) - _feedStates[i] = newFeed.filterNotNull().stateIn(screenModelScope) + _feedStates[i] = newFeed.filterNotNull().stateIn(viewModelScope) emit(newFeed.value) } } @@ -365,7 +368,7 @@ open class MainScreenModel: BaseScreenModel() { log.d { "Preferences found"} settings.feedViewPrefs.map { it["home"] ?: BskyFeedPref() - }.stateIn(screenModelScope, SharingStarted.Lazily, BskyFeedPref()) + }.stateIn(viewModelScope, SharingStarted.Lazily, BskyFeedPref()) } val feedService = dataService.dataFlows[timeline.uri] log.d { "Timeline service: $feedService"} @@ -397,7 +400,7 @@ open class MainScreenModel: BaseScreenModel() { val uri = AtUri.HOME_URI val prefs = settings.feedViewPrefs.map { it["home"] ?: BskyFeedPref() - }.filterNotNull().stateIn(screenModelScope) + }.filterNotNull().stateIn(viewModelScope) val feedService = dataService.dataFlows[uri] // Delete the feed if it's already there, initializing from scratch @@ -512,30 +515,30 @@ open class MainScreenModel: BaseScreenModel() { _cursors[AtUri.profileModServiceUri(p.did)] = servicesCursor val services = dataService .profileServiceView(p.did, servicesCursor.map { Unit } - .shareIn(screenModelScope, SharingStarted.Lazily) + .shareIn(viewModelScope, SharingStarted.Lazily) ).handleToState(p, MorphoData("Labels", AtUri.profileModServiceUri(p.did), servicesCursor.replayCache.lastOrNull())) servicesCursor.emit(null) ContentCardState.FullProfile( p, - posts.stateIn(screenModelScope), - replies.stateIn(screenModelScope), - media.stateIn(screenModelScope), - likes.stateIn(screenModelScope), - lists.stateIn(screenModelScope), - feeds.stateIn(screenModelScope), - services.stateIn(screenModelScope), + posts.stateIn(viewModelScope), + replies.stateIn(viewModelScope), + media.stateIn(viewModelScope), + likes.stateIn(viewModelScope), + lists.stateIn(viewModelScope), + feeds.stateIn(viewModelScope), + services.stateIn(viewModelScope), ContentLoadingState.Idle ) } else { postsCursor.emit(null) ContentCardState.FullProfile( p, - posts.stateIn(screenModelScope), - replies.stateIn(screenModelScope), - media.stateIn(screenModelScope), - likes.stateIn(screenModelScope), - lists.stateIn(screenModelScope), - feeds.stateIn(screenModelScope), + posts.stateIn(viewModelScope), + replies.stateIn(viewModelScope), + media.stateIn(viewModelScope), + likes.stateIn(viewModelScope), + lists.stateIn(viewModelScope), + feeds.stateIn(viewModelScope), loadingState = ContentLoadingState.Idle ) } diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/main/tabbed/TabbedHomeView.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/main/tabbed/TabbedHomeView.kt index 2cce811..bdb8750 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/main/tabbed/TabbedHomeView.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/main/tabbed/TabbedHomeView.kt @@ -1,5 +1,7 @@ package com.morpho.app.screens.main.tabbed + + import androidx.compose.animation.ExperimentalAnimationApi import androidx.compose.animation.core.FiniteAnimationSpec import androidx.compose.animation.core.Spring @@ -17,12 +19,16 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.dp -import cafe.adriel.voyager.core.lifecycle.LifecycleEffect +import cafe.adriel.voyager.core.annotation.ExperimentalVoyagerApi +import cafe.adriel.voyager.core.lifecycle.LifecycleEffectOnce import cafe.adriel.voyager.core.screen.ScreenKey +import cafe.adriel.voyager.core.screen.uniqueScreenKey import cafe.adriel.voyager.core.stack.StackEvent -import cafe.adriel.voyager.koin.getNavigatorScreenModel +import cafe.adriel.voyager.jetpack.ProvideNavigatorLifecycleKMPSupport +import cafe.adriel.voyager.jetpack.navigatorViewModel import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.Navigator +import cafe.adriel.voyager.navigator.NavigatorDisposeBehavior import cafe.adriel.voyager.navigator.currentOrThrow import cafe.adriel.voyager.navigator.tab.TabOptions import cafe.adriel.voyager.transitions.ScreenTransition @@ -37,6 +43,7 @@ import com.morpho.app.ui.common.TabbedScreenScaffold import com.morpho.app.ui.common.TabbedSkylineFragment import com.morpho.app.ui.elements.AvatarShape import com.morpho.app.ui.elements.OutlinedAvatar +import com.morpho.app.util.JavaSerializable import kotlinx.coroutines.flow.StateFlow import kotlinx.serialization.Serializable import org.jetbrains.compose.resources.ExperimentalResourceApi @@ -44,96 +51,134 @@ import kotlin.math.max import kotlin.math.min import cafe.adriel.voyager.navigator.tab.Tab as NavTab -@Suppress("UNCHECKED_CAST") -@OptIn(ExperimentalAnimationApi::class, ExperimentalMaterial3Api::class) @Composable -fun TabScreen.TabbedHomeView() { - +public fun CurrentSkylineScreen( + sm: TabbedMainScreenModel, + paddingValues: PaddingValues, + state: StateFlow>?, + modifier: Modifier +) { val navigator = LocalNavigator.currentOrThrow - val sm = navigator.getNavigatorScreenModel() + val currentScreen = navigator.lastItem as SkylineTab - var selectedTabIndex by rememberSaveable { mutableIntStateOf(0) } - var insets = WindowInsets.navigationBars.asPaddingValues() + navigator.saveableState("currentScreen") { + currentScreen.Content( + sm = sm, + paddingValues = paddingValues, + state = state, + modifier = modifier + ) + } +} - LifecycleEffect( - onStarted = { - sm.initTabs() - }, - onDisposed = {}, + +abstract class SkylineTab: NavTab { + + @Composable + abstract fun Content( + sm: TabbedMainScreenModel, + paddingValues: PaddingValues, + state: StateFlow>?, + modifier: Modifier ) - val tabs = rememberSaveable( - sm.tabFlow.value, sm.uiState.loadingState, sm.uiState.tabs.value.size - ) { - List(sm.uiState.tabs.value.size) { index -> - HomeSkylineTab( - index = index.toUShort(), - screenModel = sm, - state = sm.uiState.tabStates[index] - as StateFlow>, - paddingValues = insets, - icon = { - if(sm.uiState.tabs.value[index].avatar != null) { - OutlinedAvatar( - url = sm.uiState.tabs.value[index].avatar!!, - size = 20.dp, - avatarShape = AvatarShape.Rounded, - modifier = Modifier.padding(end = 8.dp), - ) - } - } - ) - } - } - val tabsCreated = remember(tabs.size, sm.uiState.loadingState) { - tabs.isNotEmpty() && sm.uiState.loadingState == UiLoadingState.Idle - } - if (tabsCreated) { - Navigator(tabs.first()) { nav -> - TabbedScreenScaffold( - navBar = { navBar(navigator) }, - topContent = { - HomeTabRow( - tabs = tabs, - modifier = Modifier.statusBarsPadding(), - tabIndex = selectedTabIndex, - onChanged = { index -> - if (index == selectedTabIndex) return@HomeTabRow - if(index < selectedTabIndex) { - if (nav.items.contains(tabs[index])) { - nav.popUntil {it == tabs[index] } - } else nav.replace(tabs[index]) - } else if(index > selectedTabIndex) nav.push(tabs[index]) - selectedTabIndex = index - } - ) - }, - content = { - insets = it + @OptIn(ExperimentalVoyagerApi::class) + @Composable + final override fun Content() = Content(TabbedMainScreenModel(),PaddingValues(0.dp),null,Modifier) +} - SlideTabTransition(nav) - } - ) + +@Suppress("UNCHECKED_CAST") +@OptIn(ExperimentalAnimationApi::class, ExperimentalMaterial3Api::class, + ExperimentalVoyagerApi::class +) +@Composable +fun TabScreen.TabbedHomeView( + sm: TabbedMainScreenModel = navigatorViewModel { TabbedMainScreenModel() } +) { + ProvideNavigatorLifecycleKMPSupport { + val navigator = LocalNavigator.currentOrThrow + + + var selectedTabIndex by rememberSaveable { mutableIntStateOf(0) } + var insets = WindowInsets.navigationBars.asPaddingValues() + + LifecycleEffectOnce { + sm.initTabs() } - } else LoadingCircle() + val tabs = remember( + sm.tabFlow.value, sm.uiState.loadingState, sm.uiState.tabs.value.size + ) { + List(sm.uiState.tabs.value.size) { index -> + HomeSkylineTab( + index = index.toUShort(), + title = sm.uiState.tabs.value[index].title, + avatar = sm.uiState.tabs.value[index].avatar, + ) + } + } + val tabsCreated = remember(tabs.size, sm.uiState.loadingState) { + tabs.isNotEmpty() && sm.uiState.loadingState == UiLoadingState.Idle + } + if (tabsCreated) { + Navigator( + tabs.first(), + disposeBehavior = NavigatorDisposeBehavior( + disposeNestedNavigators = false, + ) + ) { nav -> + TabbedScreenScaffold( + navBar = { navBar(navigator) }, + topContent = { + HomeTabRow( + tabs = tabs, + modifier = Modifier.statusBarsPadding(), + tabIndex = selectedTabIndex, + onChanged = { index -> + if (index == selectedTabIndex) return@HomeTabRow + if(index < selectedTabIndex) { + if (nav.items.contains(tabs[index])) { + nav.popUntil {it == tabs[index] } + } else nav.replace(tabs[index]) + } else if(index > selectedTabIndex) nav.push(tabs[index]) + selectedTabIndex = index + } + ) + }, + content = { insets, state -> + SkylineTabTransition(nav, sm, insets, state) + }, + modifier = Modifier, + state = sm.uiState.tabStates.getOrNull(selectedTabIndex) as StateFlow>? + ) + } + + } else LoadingCircle() + } } +@OptIn(ExperimentalVoyagerApi::class) @Composable -fun SlideTabTransition( +fun SkylineTabTransition( navigator: Navigator, + sm: TabbedMainScreenModel, + insets: PaddingValues = PaddingValues(0.dp), + state: StateFlow>?, modifier: Modifier = Modifier, animationSpec: FiniteAnimationSpec = spring( stiffness = Spring.StiffnessMediumLow, visibilityThreshold = IntOffset.VisibilityThreshold ), - content: ScreenTransitionContent = { it.Content() } + content: ScreenTransitionContent = { + CurrentSkylineScreen(sm, insets, state, Modifier) + } ) { - ScreenTransition( navigator = navigator, modifier = modifier, content = content, + disposeScreenAfterTransitionEnd = true, transition = { val (initialOffset, targetOffset) = when (navigator.lastEvent) { StackEvent.Pop -> ({ size: Int -> -size }) to ({ size: Int -> size }) @@ -182,9 +227,16 @@ fun HomeTabRow( Row( verticalAlignment = Alignment.CenterVertically, ){ - tab.icon() + if(tab.avatar != null) { + OutlinedAvatar( + url = tab.avatar, + size = 20.dp, + avatarShape = AvatarShape.Rounded, + modifier = Modifier.padding(end = 8.dp), + ) + } Text( - text = tab.state.value.feed.title, + text = tab.title, //style = MaterialTheme.typography.titleSmall, ) } } @@ -194,52 +246,41 @@ fun HomeTabRow( } + @Serializable -data class HomeSkylineTab( +data class HomeSkylineTab @OptIn(ExperimentalVoyagerApi::class) constructor( val index: UShort, - val screenModel: TabbedMainScreenModel, - val state: StateFlow>, - val paddingValues: PaddingValues = PaddingValues(0.dp), - val icon: @Composable () -> Unit = {}, -): NavTab { - @OptIn(ExperimentalMaterial3Api::class) + val title: String, + val avatar: String? = null, +): SkylineTab(), JavaSerializable { + + @OptIn(ExperimentalMaterial3Api::class, ExperimentalVoyagerApi::class) @Composable - override fun Content() { + override fun Content( + sm: TabbedMainScreenModel, + paddingValues: PaddingValues, + state: StateFlow>?, + modifier: Modifier + ) { + TabbedSkylineFragment( - screenModel, state, paddingValues, + sm, state, paddingValues, refresh = { cursor -> - screenModel.refreshTab(index.toInt(), cursor) + sm.refreshTab(index.toInt(), cursor) }, ) } - override val key: ScreenKey = "${state.value.uri.atUri}${hashCode()}" + override val key: ScreenKey + get() = "${title}$uniqueScreenKey" @OptIn(ExperimentalResourceApi::class, ExperimentalCoilApi::class) override val options: TabOptions @Composable get() { - /*val (avatar, icon) = screenModel - .getFeedInfo(screenModel.uriForTab(index = index.toInt())) - ?.let { feedInfo -> - feedInfo.avatar to feedInfo.icon - } ?: (null to null) - val tabIcon = if(avatar != null) rememberAsyncImagePainter( - model = ImageRequest.Builder(LocalPlatformContext.current) - .data(avatar) - .crossfade(true) - .build(), - ) else if(icon != null) rememberAsyncImagePainter( - model = ImageRequest.Builder(LocalPlatformContext.current) - .data(icon) - .crossfade(true) - .build(), - ) - else null*/ - return TabOptions( index = index, - title = state.value.feed.title, + title = title, //icon = tabIcon, ) } diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/main/tabbed/TabbedMainScreenModel.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/main/tabbed/TabbedMainScreenModel.kt index 2edef22..a4ffbb7 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/main/tabbed/TabbedMainScreenModel.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/main/tabbed/TabbedMainScreenModel.kt @@ -3,9 +3,9 @@ package com.morpho.app.screens.main.tabbed import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue +import androidx.lifecycle.viewModelScope import app.bsky.actor.SavedFeed import app.bsky.feed.GetFeedGeneratorsQuery -import cafe.adriel.voyager.core.model.screenModelScope import com.morpho.app.model.bluesky.FeedGenerator import com.morpho.app.model.bluesky.MorphoDataItem import com.morpho.app.model.bluesky.Profile @@ -19,9 +19,11 @@ import com.morpho.app.screens.main.MainScreenModel import com.morpho.butterfly.AtUri import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch +import kotlinx.serialization.Contextual import kotlinx.serialization.Serializable import org.lighthousegames.logging.logging @@ -29,15 +31,19 @@ import org.lighthousegames.logging.logging @Serializable class TabbedMainScreenModel : MainScreenModel() { + @Contextual private val tabs = mutableListOf() - var uiState: TabbedScreenState by mutableStateOf(TabbedScreenState(loadingState = UiLoadingState.Loading)) - private set + private val _tabFlow = MutableStateFlow(tabs.toList()) + @Contextual val tabFlow: StateFlow> + get() = _tabFlow.asStateFlow() - private val tabs = mutableListOf() + var uiState: TabbedScreenState by mutableStateOf( + TabbedScreenState( + loadingState = UiLoadingState.Loading, + tabs = tabFlow + )) + private set - val _tabFlow = MutableStateFlow(tabs.toList()) - val tabFlow: StateFlow> - get() = _tabFlow.asStateFlow() companion object { val log = logging() } @@ -46,9 +52,19 @@ class TabbedMainScreenModel : MainScreenModel() { return tabs[index].uri } - fun initTabs() = screenModelScope.launch { + fun initTabs() = viewModelScope.launch { if (initialized) return@launch init(false) + viewModelScope.launch(Dispatchers.Default) { + settings.pinnedFeeds.collect { feeds -> + MainScreenModel.log.d { "Pinned Feeds: $feeds" } + pinnedFeeds.value = feeds + } + settings.savedFeeds.collect { feeds -> + MainScreenModel.log.d { "Saved Feeds: $feeds" } + savedFeeds.value = feeds + } + } initialized = true val home = initHomeTab() diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/notifications/NotificationsView.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/notifications/NotificationsView.kt index b70f759..a675b29 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/notifications/NotificationsView.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/notifications/NotificationsView.kt @@ -14,10 +14,12 @@ import androidx.compose.material.pullrefresh.rememberPullRefreshState import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.unit.Dp import androidx.constraintlayout.compose.ConstraintLayout -import cafe.adriel.voyager.core.model.screenModelScope -import cafe.adriel.voyager.koin.getNavigatorScreenModel +import androidx.lifecycle.viewModelScope +import cafe.adriel.voyager.core.annotation.ExperimentalVoyagerApi +import cafe.adriel.voyager.jetpack.navigatorViewModel import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.Navigator import cafe.adriel.voyager.navigator.currentOrThrow @@ -44,17 +46,21 @@ import kotlinx.coroutines.launch import org.koin.compose.getKoin -@OptIn(ExperimentalMaterialApi::class, ExperimentalMaterial3Api::class) +@OptIn(ExperimentalMaterialApi::class, ExperimentalMaterial3Api::class, + ExperimentalVoyagerApi::class +) @Composable fun TabScreen.NotificationViewContent( navigator: Navigator = LocalNavigator.currentOrThrow, - sm: TabbedNotificationScreenModel = navigator.getNavigatorScreenModel() + ) { + val sm = navigatorViewModel { TabbedNotificationScreenModel() } val numberUnread by sm.uiState.value.numberUnread.collectAsState(0) var showSettings by remember { mutableStateOf(false) } val hasUnread = remember(numberUnread) { numberUnread > 0 } val listState = rememberLazyListState() val scope = rememberCoroutineScope() + val uriHandler = LocalUriHandler.current TabbedScreenScaffold( navBar = { navBar(navigator) }, topContent = { @@ -71,7 +77,9 @@ fun TabScreen.NotificationViewContent( markAsRead = { sm.markAllRead() } ) }, - content = { insets -> + state = sm.uiState, + modifier = Modifier, + content = { insets, state -> val refreshing by remember { mutableStateOf(false)} val refreshState = rememberPullRefreshState( @@ -144,43 +152,49 @@ fun TabScreen.NotificationViewContent( } items( count = notifications.size, - key = { index -> notifications[index].hashCode() }, + //key = { index -> notifications[index].hashCode() }, contentType = { NotificationsListItem } ) { index -> - NotificationsElement( - item = notifications[index], - showPost = sm.uiState.value.showPosts, - getPost = { getPost(it, sm.api)}, - onUnClicked = { type, rkey -> - sm.api.deleteRecord(type, rkey) - }, - onAvatarClicked = { - navigator.push(ProfileTab(it)) - }, - onRepostClicked = { - initialContent = it - repostClicked = true - }, - onReplyClicked = { - initialContent = it - composerRole = ComposerRole.Reply - showComposer = true - }, - onMenuClicked = { option, post -> doMenuOperation(option, post, clipboardManager = clipboardManager ) }, - onLikeClicked = { - sm.api.createRecord(RecordUnion.Like(it)) - }, - onPostClicked = { - navigator.push(ThreadTab(it)) - }, - // If someone hides their read notifications, - // we don't want to just mark them as read unprompted. - // Might cause them to disappear unexpectedly. - readOnLoad = !sm.uiState.value.filterState.value.showAlreadyRead, - markRead = { sm.markAsRead(it) } - ) + if (state != null) { + NotificationsElement( + item = notifications[index], + showPost = state.value.showPosts, + getPost = { getPost(it, sm.api)}, + onUnClicked = { type, rkey -> + sm.api.deleteRecord(type, rkey) + }, + onAvatarClicked = { + navigator.push(ProfileTab(it)) + }, + onRepostClicked = { + initialContent = it + repostClicked = true + }, + onReplyClicked = { + initialContent = it + composerRole = ComposerRole.Reply + showComposer = true + }, + onMenuClicked = { option, post -> + doMenuOperation(option, post, + clipboardManager = clipboardManager, + uriHandler = uriHandler + ) }, + onLikeClicked = { + sm.api.createRecord(RecordUnion.Like(it)) + }, + onPostClicked = { + navigator.push(ThreadTab(it)) + }, + // If someone hides their read notifications, + // we don't want to just mark them as read unprompted. + // Might cause them to disappear unexpectedly. + readOnLoad = !state.value.filterState.value.showAlreadyRead, + markRead = { sm.markAsRead(it) } + ) + } } item { TextButton( @@ -205,7 +219,7 @@ fun TabScreen.NotificationViewContent( draft = DraftPost() }, onSend = { finishedDraft -> - sm.screenModelScope.launch(Dispatchers.IO) { + sm.viewModelScope.launch(Dispatchers.IO) { val post = finishedDraft.createPost(sm.api) sm.api.createRecord(RecordUnion.MakePost(post)) } diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/notifications/TabbedNotificationScreenModel.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/notifications/TabbedNotificationScreenModel.kt index 7b855ad..8811f2c 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/notifications/TabbedNotificationScreenModel.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/notifications/TabbedNotificationScreenModel.kt @@ -3,7 +3,7 @@ package com.morpho.app.screens.notifications import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue -import cafe.adriel.voyager.core.model.screenModelScope +import androidx.lifecycle.viewModelScope import com.morpho.app.model.uidata.AtCursor import com.morpho.app.model.uidata.initAtCursor import com.morpho.app.model.uistate.NotificationsUIState @@ -29,7 +29,7 @@ class TabbedNotificationScreenModel: BaseScreenModel() { ) init { - screenModelScope.launch { + viewModelScope.launch { val f = notifService.notifications(cursorFlow).map { it.getOrNull() } cursorFlow.emit(null) f.collect { diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/profile/TabbedProfileView.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/profile/TabbedProfileView.kt index a29c590..129fc84 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/profile/TabbedProfileView.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/profile/TabbedProfileView.kt @@ -1,22 +1,28 @@ package com.morpho.app.screens.profile import androidx.compose.animation.ExperimentalAnimationApi -import androidx.compose.foundation.layout.* +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding import androidx.compose.material3.* -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.* import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.unit.dp -import cafe.adriel.voyager.core.lifecycle.LifecycleEffect +import cafe.adriel.voyager.core.annotation.ExperimentalVoyagerApi +import cafe.adriel.voyager.core.lifecycle.LifecycleEffectOnce import cafe.adriel.voyager.core.screen.ScreenKey -import cafe.adriel.voyager.koin.getNavigatorScreenModel +import cafe.adriel.voyager.core.screen.uniqueScreenKey +import cafe.adriel.voyager.jetpack.ProvideNavigatorLifecycleKMPSupport +import cafe.adriel.voyager.jetpack.navigatorViewModel import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.currentOrThrow -import cafe.adriel.voyager.navigator.tab.* +import cafe.adriel.voyager.navigator.tab.LocalTabNavigator +import cafe.adriel.voyager.navigator.tab.TabDisposable +import cafe.adriel.voyager.navigator.tab.TabNavigator +import cafe.adriel.voyager.navigator.tab.TabOptions import coil3.annotation.ExperimentalCoilApi import com.morpho.app.model.bluesky.BskyLabelService import com.morpho.app.model.bluesky.DetailedProfile @@ -29,8 +35,11 @@ import com.morpho.app.ui.common.LoadingCircle import com.morpho.app.ui.common.TabbedProfileScreenScaffold import com.morpho.app.ui.common.TabbedSkylineFragment import com.morpho.app.ui.profile.DetailedProfileFragment +import com.morpho.app.util.JavaSerializable +import com.morpho.butterfly.AtIdentifier import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.flow.StateFlow +import kotlinx.serialization.Serializable import org.jetbrains.compose.resources.ExperimentalResourceApi import cafe.adriel.voyager.navigator.tab.Tab as NavTab @@ -42,6 +51,7 @@ fun TabbedProfileTopBar( scrollBehavior: TopAppBarScrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior( rememberTopAppBarState()), tabs: List, + onTabChanged: (Int) -> Unit = {}, onBackClicked: () -> Unit, tabIndex: Int = 0, ) { @@ -73,7 +83,10 @@ fun TabbedProfileTopBar( tabs.forEachIndexed { index, tab -> ProfileTabItem( tab, index.toUShort() - ) { selectedTabIndex = index } + ) { + selectedTabIndex = index + onTabChanged(selectedTabIndex) + } } } } @@ -92,9 +105,7 @@ fun ProfileTabItem( onClick: () -> Unit = {}, ) { val navigator = LocalTabNavigator.current - val title = rememberSaveable { - tab.state?.value?.feed?.title.orEmpty() - } + val tabModifier = Modifier .padding( bottom = 12.dp, @@ -110,28 +121,26 @@ fun ProfileTabItem( }, ) { Text( - text = title, + text = tab.title, modifier = tabModifier ) } } -@OptIn(ExperimentalAnimationApi::class, ExperimentalMaterial3Api::class) +@OptIn(ExperimentalAnimationApi::class, ExperimentalMaterial3Api::class, + ExperimentalVoyagerApi::class +) @Composable fun TabScreen.TabbedProfileContent( - ownProfile: Boolean, - sm: TabbedProfileViewModel = LocalNavigator.currentOrThrow.getNavigatorScreenModel(), + id: AtIdentifier? = null, + sm: TabbedProfileViewModel = navigatorViewModel { TabbedProfileViewModel(id) } ) { + ProvideNavigatorLifecycleKMPSupport { + val navigator = LocalNavigator.currentOrThrow - val navigator = LocalNavigator.currentOrThrow - LifecycleEffect( - onStarted = { - sm.initProfile() - }, - onDisposed = {}, - ) - /*val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior( + LifecycleEffectOnce { sm.initProfile() } + /*val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior( state = rememberTopAppBarState(), snapAnimationSpec = spring( stiffness = Spring.StiffnessMediumLow, @@ -139,119 +148,146 @@ fun TabScreen.TabbedProfileContent( ), //flingAnimationSpec = exponentialDecay() )*/ - val scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior(rememberTopAppBarState()) - var insets = WindowInsets.navigationBars.asPaddingValues() - val tabs = rememberSaveable( - sm.tabFlow.value, - sm.profileUiState.loadingState, - sm.profileUiState.tabs.value.size - ) { - List(sm.profileUiState.tabs.value.size) { index -> - ProfileSkylineTab( - index = index.toUShort(), - screenModel = sm, - state = sm.profileUiState.tabStates[index] as StateFlow>?, - paddingValues = insets, - ownProfile = ownProfile, - ) - } - } - val tabsCreated = rememberSaveable(tabs.size, sm.profileUiState.loadingState) { - tabs.isNotEmpty() && sm.profileUiState.loadingState == UiLoadingState.Idle - } - if (tabsCreated) { - TabNavigator( - tab = tabs.first(), - tabDisposable = { TabDisposable(navigator = it, tabs = tabs) } + var selectedTabIndex by rememberSaveable { mutableIntStateOf(0) } + val scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior(rememberTopAppBarState()) + val ownProfile = remember { sm.api.atpUser?.id == id } + val tabs = rememberSaveable( + sm.tabFlow, + sm.profileUiState.loadingState, ) { + List(sm.tabFlow.value.size) { index -> + ProfileSkylineTab( + index = index.toUShort(), + ownProfile = ownProfile, + title = sm.tabFlow.value[index].title, + ) + } + } + val tabsCreated = rememberSaveable(tabs.size, sm.profileUiState.loadingState) { + tabs.isNotEmpty() && sm.profileUiState.loadingState == UiLoadingState.Idle + } + if (tabsCreated) { + TabNavigator( + tab = tabs.first(), + disposeNestedNavigators = false, + tabDisposable = { TabDisposable(navigator = it, tabs = tabs) } + ) { + + TabbedProfileScreenScaffold( + navBar = { navBar(navigator) }, + modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection), + topContent = { + TabbedProfileTopBar( + sm.profileState?.profile, + ownProfile, scrollBehavior, tabs.toImmutableList(), + onBackClicked = { navigator.pop() }, + onTabChanged = { selectedTabIndex = it }, + tabIndex = selectedTabIndex, + ) + }, + content = { insets, state -> + CurrentProfileScreen(sm, insets, state, Modifier) + }, + state = sm.profileUiState.tabStates.getOrNull(selectedTabIndex), + scrollBehavior = scrollBehavior, + ) + } + } else { TabbedProfileScreenScaffold( navBar = { navBar(navigator) }, modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection), topContent = { - TabbedProfileTopBar( - sm.profileState?.profile, true, scrollBehavior, tabs.toImmutableList(), - onBackClicked = { navigator.pop() } - ) + if (sm.profileState?.profile != null) { + DetailedProfileFragment( + profile = sm.profileState?.profile!! as DetailedProfile, + myProfile = ownProfile, + isTopLevel = true, + scrollBehavior = scrollBehavior, + onBackClicked = { navigator.pop() } + ) + } else { + TopAppBar( + title = { Text("Loading...") } + ) + } }, - content = { - insets = it - CurrentTab() + content = { _, _ -> + LoadingCircle() }, scrollBehavior = scrollBehavior, + state = sm.profileUiState.tabStates.getOrNull(selectedTabIndex), ) } - } else { - TabbedProfileScreenScaffold( - navBar = { navBar(navigator) }, - modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection), - topContent = { - if (sm.profileState?.profile != null) { - DetailedProfileFragment( - profile = sm.profileState?.profile!! as DetailedProfile, - myProfile = ownProfile, - isTopLevel = true, - scrollBehavior = scrollBehavior, - onBackClicked = { navigator.pop() } - ) - } else { - TopAppBar( - title = { Text("Loading...") } - ) - } - }, - content = { - LoadingCircle() - }, - scrollBehavior = scrollBehavior, - ) } +} +@Composable +public fun CurrentProfileScreen( + sm: TabbedProfileViewModel, + paddingValues: PaddingValues, + state: StateFlow>?, + modifier: Modifier +) { + val navigator = LocalNavigator.currentOrThrow + val currentScreen = navigator.lastItem as ProfileTabScreen + + navigator.saveableState("currentScreen") { + currentScreen.Content( + sm = sm, + paddingValues = paddingValues, + state = state, + modifier = modifier + ) + } } +abstract class ProfileTabScreen: NavTab { + + @Composable + abstract fun Content( + sm: TabbedProfileViewModel, + paddingValues: PaddingValues, + state: StateFlow>?, + modifier: Modifier + ) + + @OptIn(ExperimentalVoyagerApi::class) + @Composable + final override fun Content() = Content(TabbedProfileViewModel(),PaddingValues(0.dp),null,Modifier) +} + +@Serializable data class ProfileSkylineTab( - val index: UShort, - val screenModel: TabbedProfileViewModel, - val state: StateFlow>?, - val paddingValues: PaddingValues = PaddingValues(0.dp), + val index: UShort, val ownProfile: Boolean = false, -): NavTab { - @OptIn(ExperimentalMaterial3Api::class) + val title: String, +): ProfileTabScreen(), JavaSerializable { + + @OptIn(ExperimentalMaterial3Api::class, ExperimentalVoyagerApi::class) @Composable - override fun Content() { - TabbedSkylineFragment(screenModel, state, paddingValues, refresh = { - state?.value?.uri?.let { it1 -> screenModel.updateFeed(it1, it) } + override fun Content( + sm: TabbedProfileViewModel, + paddingValues: PaddingValues, + state: StateFlow>?, + modifier: Modifier + ) { + TabbedSkylineFragment(sm, state, paddingValues, refresh = { cursor -> + sm.refreshTab(index.toInt(), cursor) }, isProfileFeed = true) } override val key: ScreenKey - get() = "${state?.value?.uri?.atUri.orEmpty()}${hashCode()}" + get() = "${title}$uniqueScreenKey" @OptIn(ExperimentalResourceApi::class, ExperimentalCoilApi::class) override val options: TabOptions @Composable get() { - /* Curious if this works for tab icons - val icon = rememberAsyncImagePainter( - model = ImageRequest.Builder(LocalPlatformContext.current) - .fallback(ImageRequest.Builder(LocalPlatformContext.current) - .data(imageResource(Res.drawable.placeholder_pfp).asSkiaBitmap()) - .build().fallbackFactory) - .data(state.profile.avatar) - .crossfade(true) - .build(), - ) - */ - - val name = rememberSaveable { - if (state?.value?.profile?.displayName != null && state.value.profile.displayName!!.isNotEmpty()) { - state.value.profile.displayName!! - } else { state?.value?.profile?.handle?.handle.orEmpty() } - } return TabOptions( index = index, - title = name, + title = title, //icon = icon, ) } diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/profile/TabbedProfileViewModel.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/profile/TabbedProfileViewModel.kt index a2d053c..b28039c 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/profile/TabbedProfileViewModel.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/profile/TabbedProfileViewModel.kt @@ -3,9 +3,10 @@ package com.morpho.app.screens.profile import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue +import androidx.lifecycle.viewModelScope import app.bsky.actor.GetProfileQuery -import cafe.adriel.voyager.core.model.screenModelScope import com.morpho.app.model.bluesky.* +import com.morpho.app.model.uidata.AtCursor import com.morpho.app.model.uidata.ContentCardMapEntry import com.morpho.app.model.uidata.MorphoData import com.morpho.app.model.uistate.ContentCardState @@ -19,8 +20,10 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch +import kotlinx.serialization.Serializable import org.lighthousegames.logging.logging +@Serializable @Suppress("UNCHECKED_CAST") // TODO: Revisit these casts if we can, but they should be safe class TabbedProfileViewModel( @@ -31,7 +34,10 @@ class TabbedProfileViewModel( val log = logging() } var profileUiState: TabbedProfileScreenState by mutableStateOf( - TabbedProfileScreenState(loadingState = UiLoadingState.Loading)) + TabbedProfileScreenState( + loadingState = UiLoadingState.Loading, + tabs = tabFlow + )) private set var profileState: ContentCardState.FullProfile? by mutableStateOf(null) @@ -52,7 +58,7 @@ class TabbedProfileViewModel( - fun initProfile() = screenModelScope.launch { + fun initProfile() = viewModelScope.launch { if(initialized) return@launch init(false) if(id != null) { @@ -226,6 +232,11 @@ class TabbedProfileViewModel( } + fun refreshTab(index: Int, cursor: AtCursor = null) :Boolean { + return if(index < 0 || index > tabs.lastIndex) false + else updateFeed(tabs[index], cursor) + } + suspend fun loadProfile(profile: DetailedProfile): ContentCardState.FullProfile? { val profileEntry = ContentCardMapEntry.Profile(profile.did) return initProfileContent(profileEntry, force = true, fill = true).first() diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/thread/ThreadView.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/thread/ThreadView.kt index 3dfedd3..a0a653d 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/thread/ThreadView.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/thread/ThreadView.kt @@ -6,8 +6,11 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.ArrowBackIosNew import androidx.compose.material3.* import androidx.compose.runtime.* +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.unit.dp -import cafe.adriel.voyager.koin.getNavigatorScreenModel +import cafe.adriel.voyager.core.annotation.ExperimentalVoyagerApi +import cafe.adriel.voyager.jetpack.navigatorViewModel import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.Navigator import cafe.adriel.voyager.navigator.currentOrThrow @@ -20,7 +23,10 @@ import com.morpho.app.screens.base.tabbed.ProfileTab import com.morpho.app.screens.base.tabbed.TabScreen import com.morpho.app.screens.base.tabbed.ThreadTab import com.morpho.app.screens.main.MainScreenModel -import com.morpho.app.ui.common.* +import com.morpho.app.ui.common.BottomSheetPostComposer +import com.morpho.app.ui.common.ComposerRole +import com.morpho.app.ui.common.RepostQueryDialog +import com.morpho.app.ui.common.TabbedScreenScaffold import com.morpho.app.ui.elements.doMenuOperation import com.morpho.app.ui.thread.ThreadFragment import com.morpho.app.util.ClipboardManager @@ -34,30 +40,35 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.launch import org.koin.compose.getKoin -@OptIn(ExperimentalAnimationApi::class, ExperimentalMaterial3Api::class) +@OptIn(ExperimentalAnimationApi::class, ExperimentalMaterial3Api::class, + ExperimentalVoyagerApi::class +) @Composable fun TabScreen.ThreadViewContent( threadState: StateFlow, navigator:Navigator = LocalNavigator.currentOrThrow, - sm:MainScreenModel = navigator.getNavigatorScreenModel() + ) { - val thread by threadState.value.thread.collectAsState() + val sm = navigatorViewModel { MainScreenModel() } + TabbedScreenScaffold( navBar = { navBar(navigator) }, topContent = { ThreadTopBar(navigator = navigator) }, - content = { insets -> - if(thread != null) { - ThreadView( - thread!!, - insets = insets, - navigator = navigator, - createRecord = { sm.createRecord(it) }, - deleteRecord = { type, uri -> sm.deleteRecord(type, uri) } - ) - } else { - LoadingCircle() + modifier = Modifier, + state = threadState, + content = { insets, state -> + if (state != null) { + state.value.thread.value?.let { thread -> + ThreadView( + thread, + insets = insets, + navigator = navigator, + createRecord = { sm.createRecord(it) }, + deleteRecord = { type, uri -> sm.deleteRecord(type, uri) } + ) + } } } @@ -96,6 +107,7 @@ fun ThreadView( var draft by remember{ mutableStateOf(DraftPost()) } val clipboard = getKoin().get() val scope = rememberCoroutineScope() + val uriHandler = LocalUriHandler.current ThreadFragment(thread = thread, contentPadding = insets, onItemClicked = { navigator.push(ThreadTab(it)) }, @@ -110,7 +122,11 @@ fun ThreadView( composerRole = ComposerRole.Reply showComposer = true }, - onMenuClicked = { option, post -> doMenuOperation(option, post, clipboardManager = clipboard) }, + onMenuClicked = { option, post -> + doMenuOperation(option, post, + clipboardManager = clipboard, + uriHandler = uriHandler + ) }, onLikeClicked = { createRecord(RecordUnion.Like(it)) }, ) if(repostClicked) { diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/common/SkylineFragment.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/common/SkylineFragment.kt index 2f5d3f8..6f868b7 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/common/SkylineFragment.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/common/SkylineFragment.kt @@ -22,14 +22,12 @@ import com.morpho.app.model.bluesky.MorphoDataItem import com.morpho.app.model.uidata.AtCursor import com.morpho.app.model.uidata.ContentHandling import com.morpho.app.model.uistate.ContentCardState -import com.morpho.app.model.uistate.ContentLoadingState import com.morpho.app.ui.elements.MenuOptions import com.morpho.app.ui.elements.WrappedLazyColumn import com.morpho.app.ui.post.PostFragment import com.morpho.butterfly.AtIdentifier import com.morpho.butterfly.AtUri import com.morpho.butterfly.model.RecordType -import io.ktor.util.encodeBase64 import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.launch @@ -88,15 +86,15 @@ fun SkylineFragment ( } - LaunchedEffect( - data.items.isNotEmpty() && - loading == ContentLoadingState.Idle && - !listState.canScrollForward && - !refreshing && - scrolledDownSome - ) { - currentRefresh(cursor) - } +// LaunchedEffect( +// data.items.isNotEmpty() && +// loading == ContentLoadingState.Idle && +// !listState.canScrollForward && +// !refreshing && +// scrolledDownSome +// ) { +// currentRefresh(cursor) +// } val refreshState = rememberPullRefreshState(refreshing, ::refreshPull) @@ -141,7 +139,7 @@ fun SkylineFragment ( verticalArrangement = Arrangement.Top, state = listState ) { - if(!isProfileFeed) { + if(false) { item { Row( modifier = Modifier.fillMaxWidth(), @@ -187,14 +185,16 @@ fun SkylineFragment ( } } } + items( - data.items, key = { - when(it) { - is MorphoDataItem.Post -> "post_${it.post.uri}_${it.post.hashCode()}_${it.post.cid}".encodeBase64() - is MorphoDataItem.Thread -> "thread_${it.thread.post.uri}_${it.thread.hashCode()}_${it.thread.post.cid}".encodeBase64() - else -> "${it.hashCode()}".encodeBase64() - } - }, + data.items, +// key = { +// when(it) { +// is MorphoDataItem.Post -> "post_${it.post.uri}_${it.post.hashCode()}_${it.post.cid}".encodeBase64() +// is MorphoDataItem.Thread -> "thread_${it.thread.post.uri}_${it.thread.hashCode()}_${it.thread.post.cid}".encodeBase64() +// else -> "${it.hashCode()}".encodeBase64() +// } +// }, contentType = { when(it) { is MorphoDataItem.Post -> MorphoDataItem.Post::class @@ -241,6 +241,16 @@ fun SkylineFragment ( else -> {} } } + item { + Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.Center) { + TextButton( + onClick = { currentRefresh(cursor) }, + modifier = Modifier.padding(6.dp) + ) { + Text("Load more...") + } + } + } } if (scrolledDownSome) { diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/common/TabbedScreenScaffold.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/common/TabbedScreenScaffold.kt index 90a7675..3a27264 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/common/TabbedScreenScaffold.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/common/TabbedScreenScaffold.kt @@ -1,27 +1,78 @@ package com.morpho.app.ui.common +import androidx.compose.animation.core.FiniteAnimationSpec +import androidx.compose.animation.core.Spring +import androidx.compose.animation.core.VisibilityThreshold +import androidx.compose.animation.core.spring +import androidx.compose.animation.slideInHorizontally +import androidx.compose.animation.slideOutHorizontally +import androidx.compose.animation.togetherWith import androidx.compose.foundation.layout.PaddingValues import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.TopAppBarScrollBehavior import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.input.nestedscroll.NestedScrollConnection +import androidx.compose.ui.unit.IntOffset +import cafe.adriel.voyager.core.annotation.ExperimentalVoyagerApi +import cafe.adriel.voyager.core.stack.StackEvent +import cafe.adriel.voyager.navigator.CurrentScreen +import cafe.adriel.voyager.navigator.Navigator +import cafe.adriel.voyager.transitions.ScreenTransition +import cafe.adriel.voyager.transitions.ScreenTransitionContent +import com.morpho.app.model.bluesky.MorphoDataItem +import com.morpho.app.model.uistate.ContentCardState +import kotlinx.coroutines.flow.StateFlow @Composable -expect fun TabbedScreenScaffold( +expect fun TabbedScreenScaffold( navBar: @Composable () -> Unit, - content: @Composable (PaddingValues) -> Unit, + content: @Composable (PaddingValues, StateFlow?) -> Unit, topContent: @Composable () -> Unit, - modifier: Modifier = Modifier, + state: StateFlow?, + modifier: Modifier, ) @ExperimentalMaterial3Api @Composable expect fun TabbedProfileScreenScaffold( navBar: @Composable () -> Unit, - content: @Composable (PaddingValues) -> Unit, + content: @Composable (PaddingValues,StateFlow>?) -> Unit, topContent: @Composable (TopAppBarScrollBehavior) -> Unit, + state: StateFlow>?, modifier: Modifier = Modifier, scrollBehavior: TopAppBarScrollBehavior, nestedScrollConnection: NestedScrollConnection = scrollBehavior.nestedScrollConnection, -) \ No newline at end of file +) + +@OptIn(ExperimentalVoyagerApi::class) +@Composable +fun SlideTabTransition( + navigator: Navigator, + modifier: Modifier = Modifier, + animationSpec: FiniteAnimationSpec = spring( + stiffness = Spring.StiffnessMediumLow, + visibilityThreshold = IntOffset.VisibilityThreshold + ), + content: ScreenTransitionContent = { + CurrentScreen() + } +) { + ScreenTransition( + navigator = navigator, + modifier = modifier, + content = content, + disposeScreenAfterTransitionEnd = true, + transition = { + val (initialOffset, targetOffset) = when (navigator.lastEvent) { + StackEvent.Pop -> ({ size: Int -> -size }) to ({ size: Int -> size }) + StackEvent.Replace -> ({ size: Int -> -size }) to ({ size: Int -> size }) + else -> ({ size: Int -> size }) to ({ size: Int -> -size }) + } + + slideInHorizontally(animationSpec, initialOffset) togetherWith + slideOutHorizontally(animationSpec, targetOffset) + + } + ) +} \ No newline at end of file diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/common/TabbedSkylineFragment.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/common/TabbedSkylineFragment.kt index 2afb0ce..03526b9 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/common/TabbedSkylineFragment.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/common/TabbedSkylineFragment.kt @@ -4,8 +4,9 @@ import androidx.compose.foundation.layout.PaddingValues import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.* +import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.unit.dp -import cafe.adriel.voyager.core.model.screenModelScope +import androidx.lifecycle.viewModelScope import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.currentOrThrow import cafe.adriel.voyager.navigator.tab.TabNavigator @@ -45,6 +46,7 @@ fun > TabbedSkylin var showComposer by remember { mutableStateOf(false) } var composerRole by remember { mutableStateOf(ComposerRole.StandalonePost) } val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) + val uriHandler = LocalUriHandler.current // Probably pull this farther up, // but this means if you don't explicitly cancel you don't lose the post var draft by remember { mutableStateOf(DraftPost()) } @@ -75,15 +77,16 @@ fun > TabbedSkylin SkylineFragment( content = state, - onProfileClicked = { - actor -> //if (isProfileFeed) navigator.popUntilRoot() - navigator.push(ProfileTab(actor)) - }, + onProfileClicked = { actor -> navigator.push(ProfileTab(actor)) }, onItemClicked = { uri -> navigator.push(ThreadTab(uri)) }, refresh = { cursor -> refresh(cursor)}, onUnClicked = { type, rkey -> sm.deleteRecord(type, rkey) }, onRepostClicked = { onRepostClicked(it) }, - onMenuClicked = { option, post -> doMenuOperation(option, post, clipboardManager = clipboard) }, + onMenuClicked = { option, post -> + doMenuOperation(option, post, + clipboardManager = clipboard, + uriHandler = uriHandler + ) }, onReplyClicked = { onReplyClicked(it) }, onLikeClicked = { uri -> sm.createRecord(RecordUnion.Like(uri)) }, onPostButtonClicked = { onPostButtonClicked() }, @@ -125,7 +128,7 @@ fun > TabbedSkylin draft = DraftPost() }, onSend = { finishedDraft -> - sm.screenModelScope.launch(Dispatchers.IO) { + sm.viewModelScope.launch(Dispatchers.IO) { val post = finishedDraft.createPost(sm.api) sm.api.createRecord(RecordUnion.MakePost(post)) } diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/elements/OverFlowMenu.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/elements/OverFlowMenu.kt index 8cc5e7b..7059ec5 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/elements/OverFlowMenu.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/elements/OverFlowMenu.kt @@ -5,6 +5,8 @@ import androidx.compose.material3.DropdownMenu import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.Immutable +import androidx.compose.ui.platform.UriHandler import com.morpho.app.model.bluesky.BskyPost import com.morpho.app.ui.common.sharePost import com.morpho.app.util.ClipboardManager @@ -12,8 +14,10 @@ import com.morpho.app.util.json import com.morpho.app.util.openBrowser import com.morpho.butterfly.AtUri import com.morpho.butterfly.Language +import kotlinx.serialization.Serializable - +@Immutable +@Serializable enum class MenuOptions(val text: String) { Translate("Translate"), Share("Share"), @@ -42,10 +46,14 @@ inline fun doMenuOperation( reportCallback: (AtUri) -> Unit = {}, muteCallback: (AtUri) -> Unit = {}, clipboardManager: ClipboardManager, + uriHandler: UriHandler, ) { when(options) { MenuOptions.Translate -> run { - openBrowser("https://translate.google.com/?sl=auto&tl=${language}&text=${post.text}&op=translate") + openBrowser( + "https://translate.google.com/?sl=auto&tl=${language}&text=${post.text}&op=translate", + uriHandler + ) } MenuOptions.Share -> { sharePost(post) } MenuOptions.MuteThread -> { diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/elements/RichText.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/elements/RichText.kt index 06a4fc0..2e0d775 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/elements/RichText.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/elements/RichText.kt @@ -39,8 +39,8 @@ fun RichTextElement( facets: List = persistentListOf(), onClick: (List) -> Unit = {}, maxLines: Int = 20, - - ) { +) { + val layoutResult = remember { mutableStateOf(null) } val utf8Text = text.encodeUtf8() val splitText = text.split("◌").listIterator() // special BlueMoji character val formattedText = buildAnnotatedString { @@ -160,17 +160,20 @@ fun RichTextElement( } - val layoutResult = remember { mutableStateOf(null) } + val pressIndicator = Modifier.pointerInput(onClick) { detectTapGestures { pos -> layoutResult.value?.let { layoutResult -> val offset = layoutResult.getOffsetForPosition(pos) facets.forEach { - if (it.start <= offset && offset <= it.end) { + val extents = formattedText.text.utf16FacetIndex(it.start, it.end) + val start = extents.first + val end = extents.second + if (offset in start..end) { return@detectTapGestures onClick(it.facetType) } } - onClick(listOf()) + //onClick(listOf()) } } } @@ -179,7 +182,9 @@ fun RichTextElement( inlineContent = inlineContentMap, maxLines = maxLines, // Sorry @retr0.id, no more 200 line posts. overflow = TextOverflow.Ellipsis, - modifier = modifier.padding(vertical = 6.dp, horizontal = 2.dp).then(pressIndicator), + onTextLayout = { layoutResult.value = it }, + modifier = modifier.then(pressIndicator) + .padding(vertical = 6.dp, horizontal = 2.dp), ) diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/lists/FeedListEntryFragment.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/lists/FeedListEntryFragment.kt index 69e98a3..a7669ed 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/lists/FeedListEntryFragment.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/lists/FeedListEntryFragment.kt @@ -12,6 +12,7 @@ import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.font.FontWeight @@ -38,6 +39,7 @@ fun FeedListEntryFragment( var saved by remember { mutableStateOf(hasFeedSaved) } var liked by remember { mutableStateOf(feed.likedByMe) } var numLikes by remember { mutableStateOf(feed.likeCount)} + val uriHandler = LocalUriHandler.current Surface ( shadowElevation = 1.dp, tonalElevation = 4.dp, @@ -138,7 +140,9 @@ fun FeedListEntryFragment( } facetTypes.fastForEach { facetType -> when (facetType) { - is FacetType.ExternalLink -> { openBrowser(facetType.uri.uri) } + is FacetType.ExternalLink -> { + openBrowser(facetType.uri.uri, uriHandler) + } is FacetType.Format -> { } is FacetType.PollBlueOption -> {} is FacetType.Tag -> { } diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/lists/UserListEntryFragment.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/lists/UserListEntryFragment.kt index e02af15..21f0adb 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/lists/UserListEntryFragment.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/lists/UserListEntryFragment.kt @@ -12,6 +12,7 @@ import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.font.FontWeight @@ -43,6 +44,7 @@ fun UserListEntryFragment( var pinned by remember { mutableStateOf(hasListPinned) } var muted by remember { mutableStateOf(list.viewerMuted) } var blocked by remember { mutableStateOf(list.viewerBlocked != null)} + val uriHandler = LocalUriHandler.current Surface ( shadowElevation = 1.dp, tonalElevation = 4.dp, @@ -193,7 +195,9 @@ fun UserListEntryFragment( } facetTypes.fastForEach { facetType -> when (facetType) { - is FacetType.ExternalLink -> { openBrowser(facetType.uri.uri) } + is FacetType.ExternalLink -> { + openBrowser(facetType.uri.uri, uriHandler) + } is FacetType.Format -> { } is FacetType.PollBlueOption -> {} is FacetType.Tag -> { } diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/post/EmbedPostFragment.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/post/EmbedPostFragment.kt index f2b49a2..7197dd5 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/post/EmbedPostFragment.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/post/EmbedPostFragment.kt @@ -13,6 +13,7 @@ import androidx.compose.runtime.* import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.font.FontWeight @@ -47,6 +48,7 @@ fun EmbedPostFragment( val muted = rememberSaveable { post.author.mutedByMe } val interactionSource = remember { MutableInteractionSource() } val indication = remember { MorphoHighlightIndication() } + val uriHandler = LocalUriHandler.current WrappedColumn( modifier .fillMaxWidth() @@ -156,7 +158,7 @@ fun EmbedPostFragment( facetTypes.fastForEach { when(it) { is FacetType.ExternalLink -> { - openBrowser(it.uri.uri) + openBrowser(it.uri.uri, uriHandler) } is FacetType.Format -> {} is FacetType.PollBlueOption -> { @@ -177,7 +179,7 @@ fun EmbedPostFragment( modifier = Modifier.padding(horizontal = 6.dp) ) EmbedPostFeature(embed = post, onItemClicked, onLinkClicked = { - openBrowser(it) + openBrowser(it, uriHandler) }) } diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/post/FullPostFragment.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/post/FullPostFragment.kt index cf8c688..695e321 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/post/FullPostFragment.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/post/FullPostFragment.kt @@ -15,6 +15,7 @@ import androidx.compose.material3.Text import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.font.FontWeight @@ -70,6 +71,7 @@ fun FullPostFragment( getContentHandling(post) }.toImmutableList() } + val uriHandler = LocalUriHandler.current WrappedColumn( @@ -177,7 +179,7 @@ fun FullPostFragment( facetTypes.fastForEach { when(it) { is FacetType.ExternalLink -> { - openBrowser(it.uri.uri) + openBrowser(it.uri.uri, uriHandler) } is FacetType.Tag -> {onItemClicked(post.uri)} is FacetType.UserDidMention -> { diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/post/PollBlueEmbed.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/post/PollBlueEmbed.kt index 7cee1bd..6a413af 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/post/PollBlueEmbed.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/post/PollBlueEmbed.kt @@ -13,6 +13,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.drawWithContent import androidx.compose.ui.geometry.CornerRadius import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.unit.dp import androidx.compose.ui.util.fastAny import androidx.compose.ui.util.fastFold @@ -51,6 +52,7 @@ fun ColumnScope.PollBlueOption( is FacetType.PollBlueOption -> type.number else -> throw IllegalArgumentException("Expected PollBlueOption, got $type") } } + Row( horizontalArrangement = Arrangement.Start, verticalAlignment = Alignment.CenterVertically, @@ -133,6 +135,7 @@ fun PollBluePost( ) } var optionChosen by remember { mutableStateOf(pollBlueService.lookupPollBlueVote(pollId)) } val scope = rememberCoroutineScope() + val uriHandler = LocalUriHandler.current DisableSelection { Column( horizontalAlignment = Alignment.Start, @@ -149,7 +152,7 @@ fun PollBluePost( facetTypes.fastForEach { when(it) { is FacetType.ExternalLink -> { - openBrowser(it.uri.uri) + openBrowser(it.uri.uri, uriHandler) } is FacetType.Tag -> {} is FacetType.UserDidMention -> { @@ -233,7 +236,7 @@ fun PollBluePost( color = MaterialTheme.colorScheme.tertiary, style = MaterialTheme.typography.labelSmall, modifier = Modifier.padding(top = 4.dp) - .clickable { openBrowser("https://poll.blue/post") } + .clickable { openBrowser("https://poll.blue/post", uriHandler) } ) } } diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/post/PostFragment.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/post/PostFragment.kt index 1ae3f4d..45251eb 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/post/PostFragment.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/post/PostFragment.kt @@ -16,6 +16,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.font.FontWeight @@ -23,7 +24,6 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.withStyle import androidx.compose.ui.unit.dp import androidx.compose.ui.util.fastAny -import androidx.compose.ui.util.fastForEach import com.atproto.repo.StrongRef import com.morpho.app.model.bluesky.* import com.morpho.app.model.uidata.ContentHandling @@ -71,6 +71,7 @@ fun PostFragment( PostFragmentRole.ThreadRootUnfocused -> Modifier.padding(2.dp) PostFragmentRole.ThreadEnd -> Modifier.padding(start = 2.dp, top = 0.dp, end = 2.dp, bottom = 0.dp) }} + val uriHandler = LocalUriHandler.current WrappedColumn(modifier = padding.fillMaxWidth()) { val delta = remember { getFormattedDateTimeSince(post.createdAt) } val indent = remember { when(role) { @@ -140,12 +141,12 @@ fun PostFragment( .fillMaxWidth(indentLevel(indent)) .align(Alignment.End) .background(bgColor, shape) - .clickable( - interactionSource = interactionSource, - indication = indication, - enabled = true, - onClick = { onItemClicked(post.uri) } - ) +// .clickable( +// interactionSource = interactionSource, +// indication = indication, +// enabled = true, +// onClick = { onItemClicked(post.uri) } +// ) ) { ContentHider( @@ -290,10 +291,10 @@ fun PostFragment( onItemClicked(post.uri) return@RichTextElement } - facetTypes.fastForEach { + facetTypes.forEach { when(it) { is FacetType.ExternalLink -> { - openBrowser(it.uri.uri) + openBrowser(it.uri.uri, uriHandler) } is FacetType.Tag -> {onItemClicked(post.uri)} is FacetType.UserDidMention -> { @@ -366,12 +367,17 @@ inline fun ColumnScope.PostFeatureElement( crossinline onUnClicked: (type: RecordType, uri: AtUri) -> Unit = { _, _ -> }, contentHandling: List = listOf() ) { + @Suppress("REDUNDANT_ELSE_IN_WHEN") when (feature) { - is BskyPostFeature.ExternalFeature -> PostLinkEmbed(linkData = feature, - linkPress = { openBrowser(it) }, - modifier = Modifier.padding(end = 2.dp) - .align(Alignment.CenterHorizontally)) + is BskyPostFeature.ExternalFeature -> { + val uriHandler = LocalUriHandler.current + PostLinkEmbed( + linkData = feature, + linkPress = { openBrowser(it, uriHandler) }, + modifier = Modifier.padding(end = 2.dp) + .align(Alignment.CenterHorizontally) + ) } is BskyPostFeature.ImagesFeature -> { ContentHider( reasons = contentHandling, @@ -445,7 +451,9 @@ inline fun ColumnScope.RecordFeature( contentHandling: List = listOf(), getContentHandling: (EmbedRecord) -> List = { listOf() } ) { + if(media != null) { + ContentHider( reasons = contentHandling, scope = LabelScope.Media, @@ -455,9 +463,10 @@ inline fun ColumnScope.RecordFeature( ) { when(media) { is BskyPostFeature.ExternalFeature -> { + val uriHandler = LocalUriHandler.current PostLinkEmbed( linkData = media, - linkPress = { openBrowser(it) }, + linkPress = { openBrowser(it, uriHandler) }, modifier = Modifier.padding(end = 2.dp) .align(Alignment.CenterHorizontally) ) diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/util/BlueskyText.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/util/BlueskyText.kt index ba529aa..f075a18 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/util/BlueskyText.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/util/BlueskyText.kt @@ -10,7 +10,7 @@ import com.morpho.app.model.bluesky.RichTextFormat import com.morpho.butterfly.Butterfly import kotlinx.serialization.Serializable - +@Serializable data class BlueskyText( val text: String, val facets: List diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/util/LinkParsing.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/util/LinkParsing.kt index 48dc2c8..c1749fa 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/util/LinkParsing.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/util/LinkParsing.kt @@ -1,6 +1,7 @@ package com.morpho.app.util +import androidx.compose.ui.platform.UriHandler import cafe.adriel.voyager.navigator.Navigator import com.morpho.app.screens.base.tabbed.ProfileTab import com.morpho.butterfly.AtUri @@ -8,7 +9,8 @@ import com.morpho.butterfly.Cid import com.morpho.butterfly.Did import com.morpho.butterfly.Handle -fun linkVisit(string: String, navigator: Navigator) { + +fun linkVisit(string: String, navigator: Navigator, uriHandler: UriHandler) { if(string.startsWith("@")) { if(string.startsWith("@did")) { navigator.push(ProfileTab(Did(string.removePrefix("@")))) @@ -22,11 +24,12 @@ fun linkVisit(string: String, navigator: Navigator) { string.replace("/post/", "/app.bsky.feed.post/") } } else if (string.startsWith("http")){ - checkValidUrl(string)?.let { openBrowser(it) } + checkValidUrl(string)?.let { openBrowser(it, uriHandler) } } } -expect fun openBrowser(url: String) + +expect fun openBrowser(url: String, uriHandler: UriHandler) fun didCidToImageLink(did: Did, cid: Cid, avatar: Boolean, type: String = "jpeg"): String { val collection = if (avatar) { diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/util/Savers.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/util/Savers.kt index 552c5ca..80331ac 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/util/Savers.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/util/Savers.kt @@ -3,10 +3,51 @@ package com.morpho.app.util import androidx.compose.runtime.saveable.Saver import androidx.compose.runtime.saveable.listSaver import com.morpho.butterfly.AtUri +import kotlinx.coroutines.channels.BufferOverflow +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.serialization.KSerializer +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder + +// commonMain - module core +@Suppress("EXPECT_ACTUAL_CLASSIFIERS_ARE_IN_BETA_WARNING") +expect interface JavaSerializable val atUriSaver: Saver = listSaver( save = { listOf(it.atUri)}, restore = { AtUri(it.first()) } -) \ No newline at end of file +) + + + +class StateFlowSerializer(private val dataSerializer: KSerializer) : KSerializer> { + override val descriptor: SerialDescriptor = dataSerializer.descriptor + override fun serialize(encoder: Encoder, value: StateFlow) = dataSerializer.serialize(encoder, value.value) + override fun deserialize(decoder: Decoder) = MutableStateFlow(dataSerializer.deserialize(decoder)).asStateFlow() +} + +class MutableStateFlowSerializer(private val dataSerializer: KSerializer) : KSerializer> { + override val descriptor: SerialDescriptor = dataSerializer.descriptor + override fun serialize(encoder: Encoder, value: MutableStateFlow) = dataSerializer.serialize(encoder, value.value) + override fun deserialize(decoder: Decoder) = MutableStateFlow(dataSerializer.deserialize(decoder)) +} + +class MutableSharedFlowSerializer(private val dataSerializer: KSerializer) : KSerializer> { + override val descriptor: SerialDescriptor = dataSerializer.descriptor + override fun serialize(encoder: Encoder, value: MutableSharedFlow) = dataSerializer.serialize(encoder, value.replayCache.last()) + override fun deserialize(decoder: Decoder): MutableSharedFlow { + val flow = MutableSharedFlow( + replay = 1, + extraBufferCapacity = 1, + onBufferOverflow = BufferOverflow.DROP_OLDEST + ) + flow.tryEmit(dataSerializer.deserialize(decoder)) + return flow + } +} \ No newline at end of file diff --git a/Morpho/composeApp/src/desktopMain/kotlin/com/morpho/app/Platform.desktop.kt b/Morpho/composeApp/src/desktopMain/kotlin/com/morpho/app/Platform.desktop.kt new file mode 100644 index 0000000..ce37ec9 --- /dev/null +++ b/Morpho/composeApp/src/desktopMain/kotlin/com/morpho/app/Platform.desktop.kt @@ -0,0 +1,15 @@ +@file:Suppress("EXPECT_ACTUAL_CLASSIFIERS_ARE_IN_BETA_WARNING") +package com.morpho.app +import kotlinx.datetime.LocalDateTime + +// Note: no need to define CommonParcelize here (bc its @OptionalExpectation) +actual interface CommonParcelable // not used on iOS + +// Note: no need to define CommonTypeParceler> here (bc its @OptionalExpectation) +actual interface CommonParceler // not used on iOS +actual object LocalDateTimeParceler : CommonParceler // not used on iOS + +// For Android @Parcelize +@Target(AnnotationTarget.TYPE) +@Retention(AnnotationRetention.SOURCE) +actual annotation class CommonRawValue \ No newline at end of file diff --git a/Morpho/composeApp/src/desktopMain/kotlin/Platform.jvm.kt b/Morpho/composeApp/src/desktopMain/kotlin/com/morpho/app/Platform.jvm.kt similarity index 87% rename from Morpho/composeApp/src/desktopMain/kotlin/Platform.jvm.kt rename to Morpho/composeApp/src/desktopMain/kotlin/com/morpho/app/Platform.jvm.kt index f5e7e49..25020e0 100644 --- a/Morpho/composeApp/src/desktopMain/kotlin/Platform.jvm.kt +++ b/Morpho/composeApp/src/desktopMain/kotlin/com/morpho/app/Platform.jvm.kt @@ -1,3 +1,4 @@ +package com.morpho.app class JVMPlatform: Platform { override val name: String = "Java ${System.getProperty("java.version")}" } diff --git a/Morpho/composeApp/src/desktopMain/kotlin/com/morpho/app/ui/common/TabbedScreenScaffold.desktop.kt b/Morpho/composeApp/src/desktopMain/kotlin/com/morpho/app/ui/common/TabbedScreenScaffold.desktop.kt index 0cf6e1f..437dcef 100644 --- a/Morpho/composeApp/src/desktopMain/kotlin/com/morpho/app/ui/common/TabbedScreenScaffold.desktop.kt +++ b/Morpho/composeApp/src/desktopMain/kotlin/com/morpho/app/ui/common/TabbedScreenScaffold.desktop.kt @@ -10,13 +10,17 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.input.nestedscroll.NestedScrollConnection import androidx.compose.ui.input.nestedscroll.nestedScroll +import com.morpho.app.model.bluesky.MorphoDataItem +import com.morpho.app.model.uistate.ContentCardState import com.morpho.app.ui.elements.WrappedColumn +import kotlinx.coroutines.flow.StateFlow @Composable -actual fun TabbedScreenScaffold( +actual fun TabbedScreenScaffold( navBar: @Composable () -> Unit, - content: @Composable (PaddingValues) -> Unit, + content: @Composable (PaddingValues, StateFlow?) -> Unit, topContent: @Composable () -> Unit, + state: StateFlow?, modifier: Modifier, ) { Scaffold( @@ -28,7 +32,7 @@ actual fun TabbedScreenScaffold( modifier = modifier ) { topContent() - content(it) + content(it, state) } } ) @@ -38,8 +42,9 @@ actual fun TabbedScreenScaffold( @Composable actual fun TabbedProfileScreenScaffold( navBar: @Composable () -> Unit, - content: @Composable (PaddingValues) -> Unit, + content: @Composable (PaddingValues, StateFlow>?) -> Unit, topContent: @Composable (TopAppBarScrollBehavior) -> Unit, + state: StateFlow>?, modifier: Modifier, scrollBehavior: TopAppBarScrollBehavior, nestedScrollConnection: NestedScrollConnection, @@ -53,7 +58,7 @@ actual fun TabbedProfileScreenScaffold( modifier = modifier.nestedScroll(scrollBehavior.nestedScrollConnection) ) { topContent(scrollBehavior) - content(it) + content(it, state) } } ) diff --git a/Morpho/composeApp/src/desktopMain/kotlin/com/morpho/app/util/LinkParsing.desktop.kt b/Morpho/composeApp/src/desktopMain/kotlin/com/morpho/app/util/LinkParsing.desktop.kt index f2ebd1c..89e15ca 100644 --- a/Morpho/composeApp/src/desktopMain/kotlin/com/morpho/app/util/LinkParsing.desktop.kt +++ b/Morpho/composeApp/src/desktopMain/kotlin/com/morpho/app/util/LinkParsing.desktop.kt @@ -1,11 +1,12 @@ package com.morpho.app.util +import androidx.compose.ui.platform.UriHandler import java.awt.Desktop import java.net.URI import java.util.Locale -actual fun openBrowser(url: String) { +actual fun openBrowser(url: String, uriHandler: UriHandler) { val osName by lazy(LazyThreadSafetyMode.NONE) { System.getProperty("os.name").lowercase(Locale.ROOT) } val desktop = Desktop.getDesktop() try { diff --git a/Morpho/composeApp/src/desktopMain/kotlin/com/morpho/app/util/Savers.desktop.kt b/Morpho/composeApp/src/desktopMain/kotlin/com/morpho/app/util/Savers.desktop.kt new file mode 100644 index 0000000..b3464f5 --- /dev/null +++ b/Morpho/composeApp/src/desktopMain/kotlin/com/morpho/app/util/Savers.desktop.kt @@ -0,0 +1,4 @@ +package com.morpho.app.util + +// commonMain - module core +actual interface JavaSerializable \ No newline at end of file diff --git a/Morpho/composeApp/src/iosMain/kotlin/Platform.ios.kt b/Morpho/composeApp/src/iosMain/kotlin/com/morpho/app/Platform.ios.kt similarity index 90% rename from Morpho/composeApp/src/iosMain/kotlin/Platform.ios.kt rename to Morpho/composeApp/src/iosMain/kotlin/com/morpho/app/Platform.ios.kt index 5cef987..f79a606 100644 --- a/Morpho/composeApp/src/iosMain/kotlin/Platform.ios.kt +++ b/Morpho/composeApp/src/iosMain/kotlin/com/morpho/app/Platform.ios.kt @@ -1,3 +1,4 @@ +package com.morpho.app import platform.UIKit.UIDevice class IOSPlatform: Platform { diff --git a/Morpho/composeApp/src/iosMain/kotlin/com/morpho/app/util/LinkParsing.ios.kt b/Morpho/composeApp/src/iosMain/kotlin/com/morpho/app/util/LinkParsing.ios.kt index 0dc5955..5ce6388 100644 --- a/Morpho/composeApp/src/iosMain/kotlin/com/morpho/app/util/LinkParsing.ios.kt +++ b/Morpho/composeApp/src/iosMain/kotlin/com/morpho/app/util/LinkParsing.ios.kt @@ -1,4 +1,6 @@ package com.morpho.app.util -actual fun openBrowser(url: String) { +import androidx.compose.ui.platform.UriHandler + +actual fun openBrowser(url: String, uriHandler: UriHandler) { } \ No newline at end of file diff --git a/Morpho/gradle/libs.versions.toml b/Morpho/gradle/libs.versions.toml index 4b61153..888aeee 100644 --- a/Morpho/gradle/libs.versions.toml +++ b/Morpho/gradle/libs.versions.toml @@ -49,7 +49,7 @@ window = "1.3.0" material3-android = "1.2.1" accompanist-permissions = "0.32.0" coil = "3.0.0-alpha06" -voyager = "1.0.0" +voyager = "1.1.0-beta02" kmpalette = "3.1.0" diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 9725199..855889e 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -48,7 +48,7 @@ window = "1.3.0" material3-android = "1.2.1" accompanist-permissions = "0.32.0" coil = "3.0.0-alpha06" -voyager = "1.0.0" +voyager = "1.1.0-beta02" kmpalette = "3.1.0" From f74a3a21d0ce1e6fd6c35516614a2bbf4370497c Mon Sep 17 00:00:00 2001 From: Orual Date: Mon, 9 Sep 2024 10:09:34 -0400 Subject: [PATCH 03/42] Finally think i fixed the reply bug for good. --- .../app/model/bluesky/BskyNotification.kt | 4 -- .../com/morpho/app/model/bluesky/BskyPost.kt | 25 +++-------- .../morpho/app/model/bluesky/BskyPostReply.kt | 17 ++++++-- .../app/model/bluesky/BskyPostThread.kt | 4 +- .../com/morpho/app/model/bluesky/DraftPost.kt | 17 ++------ .../app/screens/base/tabbed/NavigationTabs.kt | 3 +- .../com/morpho/app/ui/common/PostComposer.kt | 41 +------------------ .../com/morpho/app/ui/elements/RichText.kt | 17 ++++++-- .../com/morpho/app/ui/post/PostFragment.kt | 15 ++++--- 9 files changed, 49 insertions(+), 94 deletions(-) diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/BskyNotification.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/BskyNotification.kt index 22cc5ab..7296066 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/BskyNotification.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/BskyNotification.kt @@ -3,7 +3,6 @@ package com.morpho.app.model.bluesky import androidx.compose.runtime.Immutable import app.bsky.feed.Like import app.bsky.feed.Post -import app.bsky.feed.PostReplyRef import app.bsky.feed.Repost import app.bsky.graph.Follow import app.bsky.notification.ListNotificationsNotification @@ -207,6 +206,3 @@ fun ListNotificationsNotification.toBskyNotification() : BskyNotification { } } -fun PostReplyRef.toReply() { - -} \ No newline at end of file diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/BskyPost.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/BskyPost.kt index 9ca6184..8faf634 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/BskyPost.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/BskyPost.kt @@ -12,14 +12,6 @@ import kotlinx.collections.immutable.persistentListOf import kotlinx.datetime.Clock import kotlinx.serialization.Serializable -@Immutable -@Serializable -enum class PostType { - BlockedThread, - NotFoundThread, - VisibleThread, - BskyPost, -} @Serializable @Immutable @@ -117,17 +109,7 @@ fun PostView.toPost(): BskyPost { } fun ThreadViewPost.toPost() : BskyPost { - val replyRef = when (parent) { - is ThreadViewPostParentUnion.BlockedPost -> null - is ThreadViewPostParentUnion.NotFoundPost -> null - is ThreadViewPostParentUnion.ThreadViewPost -> { - val parentPost = (parent as ThreadViewPostParentUnion.ThreadViewPost).value.toPost() - val rootPost = findRootPost()?.toPost() ?: parentPost - BskyPostReply(root = rootPost, parent = parentPost, grandparentAuthor = parentPost.reply?.parent?.author) - } - null -> null - } - return post.toPost(reply = replyRef, reason = null) + return post.toPost() } fun ThreadViewPost.findRootPost(): ThreadViewPost? { @@ -157,6 +139,7 @@ fun PostView.toPost( Post.serializer().deserialize(record) } catch (e: Exception) { Post( + text = "Error deserializing post: $e\n" + "Record: $record", facets = persistentListOf(), @@ -165,6 +148,8 @@ fun PostView.toPost( langs = persistentListOf(), ) } + // copy in the replyRef if it's not already there + val replyRef = reply?.copy(replyRef = postRecord.reply) ?: postRecord.reply?.toReply() return BskyPost( uri = uri, @@ -185,7 +170,7 @@ fun PostView.toPost( likeUri = viewer?.like, labels = labels.mapImmutable { it.toLabel() }, langs = postRecord.langs.mapImmutable { it }, - reply = reply, + reply = replyRef, reason = reason, ) } diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/BskyPostReply.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/BskyPostReply.kt index a14849c..1f31604 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/BskyPostReply.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/BskyPostReply.kt @@ -2,6 +2,7 @@ package com.morpho.app.model.bluesky import androidx.compose.runtime.Immutable import app.bsky.actor.ProfileViewBasic +import app.bsky.feed.PostReplyRef import app.bsky.feed.ReplyRef import app.bsky.feed.ReplyRefParentUnion import app.bsky.feed.ReplyRefRootUnion @@ -10,9 +11,10 @@ import kotlinx.serialization.Serializable @Immutable @Serializable data class BskyPostReply( - val root: BskyPost?, - val parent: BskyPost?, - val grandparentAuthor: Profile? + val root: BskyPost? = null, + val parent: BskyPost? = null, + val grandParentAuthor: Profile? = null, + val replyRef: PostReplyRef? = null ) fun ReplyRef.toReply(): BskyPostReply { @@ -27,9 +29,16 @@ fun ReplyRef.toReply(): BskyPostReply { is ReplyRefParentUnion.NotFoundPost -> null is ReplyRefParentUnion.PostView -> parent.value.toPost() }, - grandparentAuthor = when (val grandparentAuthor = grandparentAuthor) { + grandParentAuthor = when (val grandparentAuthor = grandparentAuthor) { is ProfileViewBasic -> grandparentAuthor.toProfile() else -> null } ) } + +fun PostReplyRef.toReply(): BskyPostReply { + return BskyPostReply( + replyRef = this, + grandParentAuthor = this.grandParentAuthor?.toProfile() + ) +} \ No newline at end of file diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/BskyPostThread.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/BskyPostThread.kt index 0a56a50..f3fc492 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/BskyPostThread.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/BskyPostThread.kt @@ -197,7 +197,7 @@ fun ThreadViewPost.toThread(): BskyPostThread { } fun ThreadViewPost.toThreadPost(parent: BskyPost, root: BskyPost): ThreadPost { - val post = post.toPost(BskyPostReply(root, parent, root.reply?.parent?.author), null) + val post = post.toPost(null, null) return ViewablePost( post = post, replies = replies.mapImmutable { it.toThreadPost(post, root) } @@ -206,7 +206,7 @@ fun ThreadViewPost.toThreadPost(parent: BskyPost, root: BskyPost): ThreadPost { fun ThreadViewPostReplyUnion.toThreadPost(parent: BskyPost, root: BskyPost): ThreadPost = when (this) { is ThreadViewPostReplyUnion.ThreadViewPost -> { - val post = value.post.toPost(BskyPostReply(root, parent, root.reply?.parent?.author), null) + val post = value.post.toPost(null, null) ViewablePost( post = post, replies = value.replies.mapImmutable { it.toThreadPost(post, root) } diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/DraftPost.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/DraftPost.kt index 7b9534e..3ec1af8 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/DraftPost.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/DraftPost.kt @@ -40,21 +40,10 @@ data class DraftPost( suspend fun createPost(api: Butterfly): Post { val text = makeBlueskyText(text) val blueskyText = resolveBlueskyText(text, api).getOrDefault(text) - val replyRef = if (reply != null) { - val root = if (reply.reply?.root != null) { - StrongRef(reply.reply.root.uri, reply.reply.root.cid) - } else if (reply.reply?.parent != null) { - StrongRef(reply.reply.parent.uri, reply.reply.parent.cid) - } else { - StrongRef(reply.uri, reply.cid) - } + val replyRef = if (reply != null && reply.reply?.replyRef != null) { + val root = reply.reply.replyRef.root val parent = StrongRef(reply.uri, reply.cid) - val grandParentAuthor = (if (reply.reply?.parent != null) { - reply.reply.grandparentAuthor - } else { - reply.author - })?.toProfileViewBasic() - PostReplyRef(root, parent, grandParentAuthor) + PostReplyRef(root, parent) } else null val quoteRef = quote?.let { StrongRef(it.uri, it.cid) diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/base/tabbed/NavigationTabs.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/base/tabbed/NavigationTabs.kt index 3f6749b..7c47004 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/base/tabbed/NavigationTabs.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/base/tabbed/NavigationTabs.kt @@ -181,7 +181,8 @@ data class ProfileTab( val id: AtIdentifier, ): TabScreen { - override val key: ScreenKey = "profileTab_${id}_${hashCode()}" + override val key: ScreenKey + get() = "profileTab_${id}_${uniqueScreenKey}" @kotlin.jvm.Transient @Transient override val navBar: @Composable (@Contextual Navigator) -> Unit = { n -> TabbedNavBar(options.index, n) diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/common/PostComposer.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/common/PostComposer.kt index 172b26e..504795a 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/common/PostComposer.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/common/PostComposer.kt @@ -15,14 +15,10 @@ import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.text.input.KeyboardCapitalization import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.unit.dp -import app.bsky.feed.PostReplyRef -import com.atproto.repo.StrongRef import com.morpho.app.data.toSharedImage import com.morpho.app.model.bluesky.BskyPost import com.morpho.app.model.bluesky.DraftImage import com.morpho.app.model.bluesky.DraftPost -import com.morpho.app.model.bluesky.toProfileViewBasic -import com.morpho.app.model.uidata.getReplyRefs import com.morpho.app.ui.post.ComposerPostFragment import io.github.vinceglb.filekit.compose.rememberFilePickerLauncher import io.github.vinceglb.filekit.core.PickerMode @@ -30,7 +26,6 @@ import io.github.vinceglb.filekit.core.PickerType import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.IO -import kotlinx.coroutines.flow.singleOrNull import kotlinx.coroutines.launch @@ -108,38 +103,6 @@ fun PostComposer( ) { val focusManager = LocalFocusManager.current var postText by rememberSaveable { mutableStateOf(draft.text) } - val localReplyRef = remember { - if(role == ComposerRole.Reply) { - if (initialContent != null) { - val root: StrongRef = if (initialContent.reply?.root != null) { - StrongRef(initialContent.reply.root.uri,initialContent.reply.root.cid) - } else if (initialContent.reply?.parent != null) { - StrongRef(initialContent.reply.parent.uri, initialContent.reply.parent.cid) - } else { - StrongRef(initialContent.uri,initialContent.cid) - } - val parent: StrongRef = StrongRef(initialContent.uri, initialContent.cid) - val grandParentAuthor = (if (initialContent.reply?.parent != null) { - initialContent.reply.grandparentAuthor - } else { - initialContent.author - })?.toProfileViewBasic() - PostReplyRef(root, parent, grandParentAuthor) - } else if (draft.reply != null) { - StrongRef(draft.reply.uri, draft.reply.cid) - } else null - } else null - } - var replyRef by remember { mutableStateOf(localReplyRef) } - // TODO: Probably put this somewhere saner, but for now this works - LaunchedEffect(localReplyRef) { - val uri = initialContent?.uri ?: draft.reply?.uri - if (role == ComposerRole.Reply && localReplyRef == null && uri != null) { - getReplyRefs(uri).singleOrNull()?.getOrNull()?.let { - replyRef = it - } - } - } val submitText = rememberSaveable { when(role) { ComposerRole.StandalonePost -> "Post" @@ -179,7 +142,7 @@ fun PostComposer( onUpdate( DraftPost( text = postText, - reply = if (replyRef != null && role == ComposerRole.Reply) initialContent else null, + reply = if (role == ComposerRole.Reply) initialContent else null, quote = if (role == ComposerRole.QuotePost) initialContent else null, images = postImages ) @@ -224,7 +187,7 @@ fun PostComposer( .imePadding(), verticalArrangement = Arrangement.Top ) { - if (replyRef != null && initialContent != null) { + if (role == ComposerRole.Reply && initialContent != null) { ComposerPostFragment( post = initialContent, modifier = Modifier diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/elements/RichText.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/elements/RichText.kt index 2e0d775..015697b 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/elements/RichText.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/elements/RichText.kt @@ -10,6 +10,8 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Modifier +import androidx.compose.ui.input.pointer.PointerIcon +import androidx.compose.ui.input.pointer.pointerHoverIcon import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.text.* import androidx.compose.ui.text.font.FontStyle @@ -150,7 +152,7 @@ fun RichTextElement( else -> { Pair("", InlineTextContent( - Placeholder(1.sp, 1.sp, PlaceholderVerticalAlign.TextCenter) + Placeholder(0.sp, 0.sp, PlaceholderVerticalAlign.TextCenter) ){}) } } @@ -161,8 +163,15 @@ fun RichTextElement( } - val pressIndicator = Modifier.pointerInput(onClick) { - detectTapGestures { pos -> + val pressIndicator = Modifier + .pointerHoverIcon(PointerIcon.Hand) + .pointerInput(onClick) { + + detectTapGestures( + onLongPress ={ + + } + ) { pos -> layoutResult.value?.let { layoutResult -> val offset = layoutResult.getOffsetForPosition(pos) facets.forEach { @@ -173,7 +182,7 @@ fun RichTextElement( return@detectTapGestures onClick(it.facetType) } } - //onClick(listOf()) + onClick(listOf()) } } } diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/post/PostFragment.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/post/PostFragment.kt index 45251eb..02e201d 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/post/PostFragment.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/post/PostFragment.kt @@ -16,6 +16,8 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.input.pointer.PointerIcon +import androidx.compose.ui.input.pointer.pointerHoverIcon import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.buildAnnotatedString @@ -141,12 +143,12 @@ fun PostFragment( .fillMaxWidth(indentLevel(indent)) .align(Alignment.End) .background(bgColor, shape) -// .clickable( -// interactionSource = interactionSource, -// indication = indication, -// enabled = true, -// onClick = { onItemClicked(post.uri) } -// ) + .clickable( + interactionSource = interactionSource, + indication = indication, + enabled = true, + onClick = { onItemClicked(post.uri) } + ) ) { ContentHider( @@ -242,6 +244,7 @@ fun PostFragment( .wrapContentWidth(Alignment.Start) .weight(10.0F) .alignByBaseline() + .pointerHoverIcon(PointerIcon.Hand) .clickable( interactionSource = interactionSource, indication = indication, From 9e0812aeb5738db2cfaef95e83eb5d7c64e1828b Mon Sep 17 00:00:00 2001 From: Orual Date: Mon, 9 Sep 2024 22:31:48 -0400 Subject: [PATCH 04/42] Big feed backend refactor the second. Mostly eliminated the MorphoDataFeed type that had a bunch of hacky stuff, reduced boilerplate, made it more extensible, and improved thread/filter handling. Seem to have introduced another cursor bug, need to investigate. --- .../com/morpho/app/model/bluesky/BskyPost.kt | 8 +- .../morpho/app/model/bluesky/BskyPostReply.kt | 32 +- .../app/model/bluesky/BskyPostThread.kt | 129 +++- .../app/model/bluesky/MorphoDataFeed.kt | 233 ++++--- .../app/model/bluesky/MorphoDataItem.kt | 317 ++++++++- .../app/model/bluesky/NotificationsList.kt | 2 +- .../app/model/uidata/BskyDataService.kt | 495 +++++++------- .../model/uidata/BskyNotificationService.kt | 8 +- .../com/morpho/app/model/uidata/MorphoData.kt | 606 +++++++++++++++++- .../morpho/app/model/uistate/SkylineState.kt | 1 + .../app/screens/main/MainScreenModel.kt | 67 +- .../app/screens/main/tabbed/TabbedHomeView.kt | 31 +- .../main/tabbed/TabbedMainScreenModel.kt | 2 +- .../notifications/NotificationsView.kt | 3 +- .../TabbedNotificationScreenModel.kt | 2 +- .../app/screens/profile/TabbedProfileView.kt | 28 +- .../screens/profile/TabbedProfileViewModel.kt | 24 +- .../morpho/app/ui/common/SkylineFragment.kt | 49 +- .../app/ui/common/SkylineThreadFragment.kt | 22 +- .../app/ui/common/TabbedSkylineFragment.kt | 10 +- .../morpho/app/ui/elements/ContentHider.kt | 73 ++- .../com/morpho/app/ui/post/PostFragment.kt | 10 +- .../com/morpho/app/ui/post/PostImage.kt | 4 +- .../com/morpho/app/ui/thread/ThreadItem.kt | 8 +- .../morpho/app/model/bluesky/BskyPostTest.kt | 4 +- 25 files changed, 1712 insertions(+), 456 deletions(-) diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/BskyPost.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/BskyPost.kt index 8faf634..262da2e 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/BskyPost.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/BskyPost.kt @@ -73,7 +73,7 @@ data class BskyPost ( is Cid -> other == cid is AtUri -> other == uri is BskyPost -> other.cid == cid - else -> reply?.parent?.contains(other) == true + else -> reply?.parentPost?.contains(other) == true } } @@ -149,7 +149,11 @@ fun PostView.toPost( ) } // copy in the replyRef if it's not already there - val replyRef = reply?.copy(replyRef = postRecord.reply) ?: postRecord.reply?.toReply() + val replyRef = reply?.copy( + replyRef = postRecord.reply, + grandParentAuthor = reply.grandParentAuthor ?: + postRecord.reply?.grandParentAuthor?.toProfile() + ) ?: postRecord.reply?.toReply() return BskyPost( uri = uri, diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/BskyPostReply.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/BskyPostReply.kt index 1f31604..d388190 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/BskyPostReply.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/BskyPostReply.kt @@ -2,35 +2,34 @@ package com.morpho.app.model.bluesky import androidx.compose.runtime.Immutable import app.bsky.actor.ProfileViewBasic -import app.bsky.feed.PostReplyRef -import app.bsky.feed.ReplyRef -import app.bsky.feed.ReplyRefParentUnion -import app.bsky.feed.ReplyRefRootUnion +import app.bsky.feed.* +import com.morpho.butterfly.Butterfly +import kotlinx.collections.immutable.persistentListOf import kotlinx.serialization.Serializable @Immutable @Serializable data class BskyPostReply( - val root: BskyPost? = null, - val parent: BskyPost? = null, + val rootPost: BskyPost? = null, + val parentPost: BskyPost? = null, val grandParentAuthor: Profile? = null, val replyRef: PostReplyRef? = null ) fun ReplyRef.toReply(): BskyPostReply { return BskyPostReply( - root = when (val root = root) { + rootPost = when (val root = root) { is ReplyRefRootUnion.BlockedPost -> null is ReplyRefRootUnion.NotFoundPost -> null is ReplyRefRootUnion.PostView -> root.value.toPost() }, - parent = when (val parent = parent) { + parentPost = when (val parent = parent) { is ReplyRefParentUnion.BlockedPost -> null is ReplyRefParentUnion.NotFoundPost -> null is ReplyRefParentUnion.PostView -> parent.value.toPost() }, - grandParentAuthor = when (val grandparentAuthor = grandparentAuthor) { - is ProfileViewBasic -> grandparentAuthor.toProfile() + grandParentAuthor = when (val grandParentAuthor = this.grandparentAuthor) { + is ProfileViewBasic -> grandParentAuthor.toProfile() else -> null } ) @@ -41,4 +40,17 @@ fun PostReplyRef.toReply(): BskyPostReply { replyRef = this, grandParentAuthor = this.grandParentAuthor?.toProfile() ) +} + +suspend fun PostReplyRef.hydratedReply(api: Butterfly): BskyPostReply { + val parents = api.api.getPosts(GetPostsQuery(persistentListOf(this.parent.uri, this.root.uri))) + .getOrNull()?.posts?.map { it.toPost() } ?: persistentListOf() + val grandparent = if (parents.first().reply?.replyRef?.parent?.uri != null) { + api.api.getPosts(GetPostsQuery(persistentListOf(parents.first().reply?.replyRef?.parent?.uri!!))).getOrNull()?.posts?.firstOrNull() + } else null + return BskyPostReply( + rootPost = parents.firstOrNull { it.cid == this.root.cid }, + parentPost = parents.firstOrNull { it.cid == this.parent.cid }, + grandParentAuthor = this.grandParentAuthor?.toProfile() ?: grandparent?.author?.toProfile(), + ) } \ No newline at end of file diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/BskyPostThread.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/BskyPostThread.kt index f3fc492..35ee475 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/BskyPostThread.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/BskyPostThread.kt @@ -3,6 +3,7 @@ package com.morpho.app.model.bluesky import androidx.compose.runtime.Immutable +import androidx.compose.ui.util.fastForEachIndexed import app.bsky.feed.ThreadViewPost import app.bsky.feed.ThreadViewPostParentUnion import app.bsky.feed.ThreadViewPostReplyUnion @@ -47,12 +48,118 @@ data class BskyPostThread( } return false } + + fun filterReplies(filter: (ThreadPost) -> Boolean): BskyPostThread { + val threadReplies = this.replies.toMutableList() + threadReplies.fastForEachIndexed { index, reply -> + if (filter(reply)) { + threadReplies.removeAt(index) + } else { + if (reply is ViewablePost) { + threadReplies[index] = reply.copy( + replies = reply.replies.filterNot { filter(it) } + ) + } + } + } + return BskyPostThread( + post = post, + parents = parents, + replies = threadReplies + ) + } + + fun addReply(reply: ThreadPost.ViewablePost): BskyPostThread { + if(reply.uri == post.uri) return BskyPostThread( + post = post, + parents = parents.distinctBy { it.uri }.filterNot { it.uri == reply.uri || it.uri == post.uri }, + replies = replies.distinctBy { it.uri }.filterNot { it.uri == reply.uri || it.uri == post.uri }, + ) + val parent = reply.post.reply?.parentPost?.uri ?: return this + val root = reply.post.reply.rootPost?.uri ?: return this + val newParents = this.parents.toMutableList() + val threadReplies = this.replies.toMutableList() + val inParents = this.parents.indexOfFirst { + it.uri == parent || it.uri == root + } + val inReplies = this.replies.indexOfFirst { + it.uri == parent || it.uri == root + } + if (inParents != -1) { + val replyParent = parents[inParents] + replyParent.addReply(reply) + newParents[inParents] = replyParent + } else if (inReplies != -1) { + val replyParent = threadReplies[inReplies] + replyParent.addReply(reply) + threadReplies[inReplies] = replyParent + } + return BskyPostThread( + post = post, + parents = newParents.distinctBy { it.uri }.filterNot { it.uri == reply.uri || it.uri == post.uri }, + replies = threadReplies.distinctBy { it.uri }.filterNot { it.uri == reply.uri || it.uri == post.uri }, + ) + } + + fun addReply(reply: BskyPost): BskyPostThread { + if(reply.uri == post.uri) return BskyPostThread( + post = post, + parents = parents.distinctBy { it.uri }.filterNot { it.uri == reply.uri || it.uri == post.uri }, + replies = replies.distinctBy { it.uri }.filterNot { it.uri == reply.uri || it.uri == post.uri }, + ) + val parent = reply.reply?.parentPost?.uri ?: return this + val root = reply.reply.rootPost?.uri ?: return this + val newParents = this.parents.toMutableList() + val threadReplies = this.replies.toMutableList() + val inParents = this.parents.indexOfFirst { + it.uri == parent || it.uri == root + } + val inReplies = this.replies.indexOfFirst { + it.uri == parent + } + if (inParents != -1) { + val replyParent = parents[inParents] + replyParent.addReply(reply) + newParents[inParents] = replyParent + } else if (inReplies != -1) { + val replyParent = threadReplies[inReplies] + replyParent.addReply(reply) + threadReplies[inReplies] = replyParent + } + return BskyPostThread( + post = post, + parents = newParents.distinctBy { it.uri }.filterNot { it.uri == reply.uri || it.uri == post.uri }, + replies = threadReplies.distinctBy { it.uri }.filterNot { it.uri == reply.uri || it.uri == post.uri }, + ) + } } +fun List.inParentOrder(): List { + val newList = this.toMutableList() + this.forEachIndexed { index, threadPost -> + when(threadPost) { + is ViewablePost -> { + val parentUri = threadPost.post.reply?.replyRef?.parent?.uri + if (threadPost.post.reply == null) { + newList.add(0, threadPost) + return@forEachIndexed + } + val parentIndex = newList.indexOfFirst { it.uri == parentUri } + if (parentIndex != -1 && parentIndex != index - 1) { + newList.add(parentIndex+1, threadPost) + return@forEachIndexed + } + } + else -> return@forEachIndexed + } + } + return newList.distinctBy { it.uri } +} @Immutable @Serializable sealed interface ThreadPost { + val uri: AtUri? @Immutable @Serializable @@ -60,6 +167,8 @@ sealed interface ThreadPost { val post: BskyPost, val replies: List = persistentListOf(), ) : ThreadPost { + override val uri: AtUri + get() = post.uri override fun equals(other: Any?) : Boolean { return when(other) { null -> false @@ -69,6 +178,8 @@ sealed interface ThreadPost { } } + + operator fun contains(other: Any?) : Boolean { when(other) { is Cid -> { @@ -133,7 +244,7 @@ sealed interface ThreadPost { @Immutable @Serializable data class NotFoundPost( - val uri: AtUri? = null, + override val uri: AtUri? = null, ) : ThreadPost { override fun equals(other: Any?) : Boolean { if (other is AtUri) return uri == other @@ -148,7 +259,7 @@ sealed interface ThreadPost { @Immutable @Serializable data class BlockedPost( - val uri: AtUri? = null, + override val uri: AtUri? = null, ) : ThreadPost { override fun equals(other: Any?) : Boolean { if (other is AtUri) return uri == other @@ -161,6 +272,18 @@ sealed interface ThreadPost { } + fun addReply(reply: BskyPost): ThreadPost { + return addReply(ViewablePost(reply)) + } + + fun addReply(reply: ViewablePost): ThreadPost { + return when(this) { + is ViewablePost -> ViewablePost(post, (replies + reply).distinctBy { it.uri }) + is BlockedPost -> BlockedPost(uri) + is NotFoundPost -> NotFoundPost(uri) + } + } + } fun ThreadViewPost.toThread(): BskyPostThread { @@ -178,7 +301,7 @@ fun ThreadViewPost.toThread(): BskyPostThread { ) } else { val rootPost = parents.last().toPost() - val entryPost = this.post.toPost(BskyPostReply(parents.first().toPost(), rootPost, parents.first().toPost().reply?.parent?.author), null) + val entryPost = this.post.toPost(BskyPostReply(parents.first().toPost(), rootPost, parents.first().toPost().reply?.parentPost?.author), null) return BskyPostThread( post = entryPost, parents = parents.mapIndexed { index, post -> diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/MorphoDataFeed.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/MorphoDataFeed.kt index d0577d9..c4be86c 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/MorphoDataFeed.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/MorphoDataFeed.kt @@ -20,7 +20,7 @@ import kotlin.time.Duration -typealias TunerFunction = (List) -> List +typealias TunerFunction = (List) -> List @@ -29,7 +29,7 @@ typealias TunerFunction = (List) -> List @Serializable data class MorphoDataFeed ( private var _items: MutableList = mutableListOf(), - var cursor: AtCursor = null, + var cursor: AtCursor = AtCursor.EMPTY, val uri: AtUri = AtUri.HOME_URI, var hasNewPosts: Boolean = false, ) { @@ -38,11 +38,11 @@ data class MorphoDataFeed ( fun fromPosts( posts: List, - cursor: AtCursor = null, + cursor: AtCursor = AtCursor.EMPTY, ): MorphoDataFeed { return MorphoDataFeed( _items = posts.map { MorphoDataItem.Post(it) }.toMutableList(), - cursor = cursor, hasNewPosts = cursor == null + cursor = cursor, hasNewPosts = cursor == AtCursor.EMPTY ) } @@ -51,58 +51,60 @@ data class MorphoDataFeed ( ): MorphoDataFeed { return MorphoDataFeed( _items = data.items.toMutableList(), - cursor = data.cursor, hasNewPosts = data.cursor == null + cursor = data.cursor, hasNewPosts = data.cursor == AtCursor.EMPTY ) } + + fun fromFeedGen( feeds: List, - cursor: AtCursor = null, + cursor: AtCursor = AtCursor.EMPTY, ): MorphoDataFeed { return MorphoDataFeed( _items = feeds.map { MorphoDataItem.FeedInfo(it) }.toMutableList(), - cursor = cursor, hasNewPosts = cursor == null + cursor = cursor, hasNewPosts = cursor == AtCursor.EMPTY ) } fun fromProfileList( list: List, - cursor: AtCursor = null, + cursor: AtCursor = AtCursor.EMPTY, ): MorphoDataFeed { return MorphoDataFeed( _items = list.map { MorphoDataItem.ProfileItem(it) }.toMutableList(), - cursor = cursor, hasNewPosts = cursor == null + cursor = cursor, hasNewPosts = cursor == AtCursor.EMPTY ) } fun fromBskyList( lists: List, - cursor: AtCursor = null, + cursor: AtCursor = AtCursor.EMPTY, ): MorphoDataFeed { return MorphoDataFeed( _items = lists.map { MorphoDataItem.ListInfo(it) }.toMutableList(), - cursor = cursor, hasNewPosts = cursor == null + cursor = cursor, hasNewPosts = cursor == AtCursor.EMPTY ) } fun fromModLabelDefs( labels: List, - cursor: AtCursor = null, + cursor: AtCursor = AtCursor.EMPTY, ): MorphoDataFeed { return MorphoDataFeed( _items = labels.map { MorphoDataItem.ModLabel(it) }.toMutableList(), - cursor = cursor, hasNewPosts = cursor == null + cursor = cursor, hasNewPosts = cursor == AtCursor.EMPTY ) } fun fromModServiceDefs( services: List, - cursor: AtCursor = null, + cursor: AtCursor = AtCursor.EMPTY, ): MorphoDataFeed { return MorphoDataFeed( _items = services.map { MorphoDataItem.LabelService(it) }.toMutableList(), - cursor = cursor, hasNewPosts = cursor == null + cursor = cursor, hasNewPosts = cursor == AtCursor.EMPTY ) } @@ -116,7 +118,7 @@ data class MorphoDataFeed ( _items = (posts.mapImmutable { MorphoDataItem.Post(it.toPost()) } + feed._items).toList() .toMutableList(), - cursor = cursor, hasNewPosts = cursor == null + cursor = cursor, hasNewPosts = cursor == AtCursor.EMPTY ) } fun concat( @@ -127,7 +129,7 @@ data class MorphoDataFeed ( return MorphoDataFeed( _items = (feed._items + posts.mapImmutable { MorphoDataItem.Post(it.toPost()) }) .toMutableList(), - cursor = cursor, hasNewPosts = cursor == null + cursor = cursor, hasNewPosts = cursor == AtCursor.EMPTY ) } @@ -138,7 +140,7 @@ data class MorphoDataFeed ( ): MorphoDataFeed { return MorphoDataFeed( _items = (first._items + last._items).toMutableList(), - cursor = cursor, hasNewPosts = cursor == null + cursor = cursor, hasNewPosts = cursor == AtCursor.EMPTY ) } @@ -149,7 +151,7 @@ data class MorphoDataFeed ( ): MorphoDataFeed { return MorphoDataFeed( _items = (first.items + last.items).toMutableList(), - cursor = cursor, hasNewPosts = cursor == null + cursor = cursor, hasNewPosts = cursor == AtCursor.EMPTY ) } @@ -160,7 +162,7 @@ data class MorphoDataFeed ( ): MorphoDataFeed { return MorphoDataFeed( _items = (first.items + last.items).toMutableList(), - cursor = cursor, hasNewPosts = cursor == null + cursor = cursor, hasNewPosts = cursor == AtCursor.EMPTY ) } @@ -169,9 +171,14 @@ data class MorphoDataFeed ( list: List, depth: Int = 3, height: Int = 10, timeRange: Delta = Delta(Duration.parse("4h")), - cursor: AtCursor = null, + cursor: AtCursor = AtCursor.EMPTY, ): Flow> = flow { - emit(collectThreads(fromPosts(list.toBskyPostList(), cursor), depth, height, timeRange) + emit(collectThreads(fromPosts(list.toBskyPostList().fastMap { + when(it) { + is MorphoDataItem.Post -> it.post + is MorphoDataItem.Thread -> it.thread.post + } + }, cursor), depth, height, timeRange) .distinctUntilChanged().last() ) }.flowOn(Dispatchers.Default) @@ -190,8 +197,8 @@ data class MorphoDataFeed ( val post = item.post if(post.reply != null) { val itemCid = post.cid - val parent = post.reply.parent - val root = post.reply.root + val parent = post.reply.parentPost + val root = post.reply.rootPost if(itemCid !in threadCandidates.keys) { var found = false threadCandidates.forEach { thread -> @@ -205,8 +212,8 @@ data class MorphoDataFeed ( found = true return@forEach } else if (parent != null && parent.cid in thread.value.keys) { - if(parent.reply?.parent != null) { - thread.value[parent.reply.parent.cid] = parent.reply.parent + if(parent.reply?.parentPost != null) { + thread.value[parent.reply.parentPost.cid] = parent.reply.parentPost } } } @@ -214,8 +221,8 @@ data class MorphoDataFeed ( threadCandidates[itemCid] = mutableMapOf() if (parent != null) { threadCandidates[itemCid]?.set(parent.cid, parent ) - if(parent.reply?.parent != null) { - threadCandidates[itemCid]?.set(parent.reply.parent.cid, parent.reply.parent) + if(parent.reply?.parentPost != null) { + threadCandidates[itemCid]?.set(parent.reply.parentPost.cid, parent.reply.parentPost) } } if (root != null && threadCandidates[itemCid]?.keys?.contains(root.cid) != true ) { @@ -225,8 +232,8 @@ data class MorphoDataFeed ( } else { if (parent != null && threadCandidates[itemCid]?.keys?.contains(parent.cid) != true ) { threadCandidates[itemCid]?.set(parent.cid, parent ) - if(parent.reply?.parent != null) { - threadCandidates[itemCid]?.set(parent.reply.parent.cid, parent.reply.parent) + if(parent.reply?.parentPost != null) { + threadCandidates[itemCid]?.set(parent.reply.parentPost.cid, parent.reply.parentPost) } } if (root != null && threadCandidates[itemCid]?.keys?.contains(root.cid) != true ) { @@ -252,8 +259,8 @@ data class MorphoDataFeed ( ?.sortedByDescending { it.createdAt } .orEmpty() val parents: Flow = flow { - generateSequence(post.reply?.parent) { - it.reply?.parent + generateSequence(post.reply?.parentPost) { + it.reply?.parentPost }.toList().reversed().map { r-> emit(ThreadPost.ViewablePost( r, @@ -264,7 +271,7 @@ data class MorphoDataFeed ( } val replies: Flow = flow { threads[itemCid]?.filter { - (it.reply?.parent?.cid ?: Cid("")) == itemCid + (it.reply?.parentPost?.cid ?: Cid("")) == itemCid }?.map { p -> emit(ThreadPost.ViewablePost( p, @@ -296,7 +303,7 @@ data class MorphoDataFeed ( private fun findReplies(level: Int, depth: Int, post: BskyPost, list: Flow ): Flow = flow { list.filter { - (it.reply?.parent?.cid ?: Cid("")) == post.cid + (it.reply?.parentPost?.cid ?: Cid("")) == post.cid }.map { if (level < depth) { val r = findReplies(level + 1, depth, it, list) @@ -307,70 +314,130 @@ data class MorphoDataFeed ( } } }.flowOn(Dispatchers.Default) + fun filterByPrefs( - posts: List, + posts: List, prefs: BskyFeedPref, follows: List = persistentListOf(), - ): List { + ): List { var feed = posts.fastFilter { post -> // A-B test perf with fast and normal filter - (!prefs.hideReposts && post.reason is BskyPostReason.BskyPostRepost) - || (!prefs.hideQuotePosts && isQuotePost(post)) - || ((!prefs.hideReplies && (post.reply != null)) - && (isSelfReply(post) || isThreadRoot(post) || post.reposted - || (post.likeCount <= prefs.hideRepliesByLikeCount) ) - && (!prefs.hideRepliesByUnfollowed && isFollowingAllAuthors(post, follows)) ) - || (post.reply == null && !isQuotePost(post) && post.reason == null) + if (post is MorphoDataItem.Post) { + (!prefs.hideReposts && post.reason is BskyPostReason.BskyPostRepost) + || (!prefs.hideQuotePosts && isQuotePost(post.post)) + || ((!prefs.hideReplies && (post.post.reply != null)) + && (isSelfReply(post.post) || isThreadRoot(post.post) || post.post.reposted + || (post.post.likeCount <= prefs.hideRepliesByLikeCount) ) + && (!prefs.hideRepliesByUnfollowed && isFollowingAllAuthors(post.post, follows)) ) + || (post.post.reply == null && !isQuotePost(post.post) && post.reason == null) + } else if (post is MorphoDataItem.Thread) { + (!prefs.hideQuotePosts && isQuotePost(post.thread.post)) + || ((!prefs.hideReplies && (post.thread.post.reply != null)) + && (isSelfReply(post.thread.post) || isThreadRoot(post.thread.post) || post.thread.post.reposted + || (post.thread.post.likeCount <= prefs.hideRepliesByLikeCount) ) + && (!prefs.hideRepliesByUnfollowed && isFollowingAllAuthors(post.thread.post, follows)) ) + || (post.thread.post.reply == null && !isQuotePost(post.thread.post) && post.thread.post.reason == null) + } else false } - feed = filterbyLanguage(feed, prefs.languages) + feed = filterByLanguage(feed, prefs.languages) feed = filterByContentLabel(feed, prefs.labelsToHide) return feed } - fun filterbyLanguage( - posts: List, + fun filterByLanguage( + posts: List, languages: List, - ): List { - return posts.fastFilter { post -> post.langs.any { languages.contains(it) } } + ): List { + if (languages.isEmpty()) return posts + return posts.fastFilter { post -> + when(post) { + is MorphoDataItem.Post -> post.post.langs.any { languages.contains(it) } + is MorphoDataItem.Thread -> post.thread.post.langs.any { languages.contains(it) } + } + } } fun filterByContentLabel( - posts: List, + posts: List, toHide: List = persistentListOf(), - ): List { - return posts.fastFilter { post -> post.labels.none { label -> toHide.fastAny { it.label == label.value } } } + ): List { + return posts.fastFilter { post -> + when(post) { + is MorphoDataItem.Post -> post.post.labels.none { label -> toHide.fastAny { it.label == label.value } } + is MorphoDataItem.Thread -> post.thread.post.labels.none { label -> toHide.fastAny { it.label == label.value } } + } + } } - fun filterBy(did: Did, posts: List): List { - return posts.fastFilter { it.author.did != did } + fun filterBy(did: Did, posts: List): List { + return posts.fastFilter { + when(it) { + is MorphoDataItem.Post -> it.post.author.did != did + is MorphoDataItem.Thread -> it.thread.post.author.did != did + } + } } - fun filterBy(string: String, posts: List) : List { + fun filterBy(string: String, posts: List) : List { return posts.fastFilter { - it.text.contains(string) + when(it) { + is MorphoDataItem.Post -> it.post.text.contains(string) + is MorphoDataItem.Thread -> { + it.thread.post.text.contains(string) || it.thread.parents.any { parent -> + if (parent is ThreadPost.ViewablePost) { + parent.post.text.contains(string) + } else false + } || it.thread.replies.any { reply -> + if (reply is ThreadPost.ViewablePost) { + reply.post.text.contains(string) + } else false + } + } + } } } - fun filterByWord(string: String, posts: List) : List { + fun filterByWord(string: String, posts: List) : List { return filterBy(Regex("""\b$string\b"""), posts) } - fun filterBy(regex: Regex, posts: List) : List { + fun filterBy(regex: Regex, posts: List) : List { return posts.fastFilter { - it.text.contains(regex) + when(it) { + is MorphoDataItem.Post -> it.post.text.contains(regex) + is MorphoDataItem.Thread -> { + it.thread.post.text.contains(regex) || it.thread.parents.any { parent -> + if (parent is ThreadPost.ViewablePost) { + parent.post.text.contains(regex) + } else false + } || it.thread.replies.any { reply -> + if (reply is ThreadPost.ViewablePost) { + reply.post.text.contains(regex) + } else false + } + } + } } } - fun dedupPosts(posts: List): List { + fun dedupPosts(posts: List): List { return posts.fastDistinctBy { post-> // A-B test perf with fast and normal distinctBy - post.cid + when(post) { + is MorphoDataItem.Post -> post.post.cid + is MorphoDataItem.Thread -> post.thread.post.cid + is MorphoDataItem.FeedInfo -> post.feed.cid + is MorphoDataItem.ListInfo -> post.list.cid + is MorphoDataItem.ModLabel -> post.label.identifier + is MorphoDataItem.ProfileItem -> post.profile.did + is MorphoDataItem.LabelService -> post.service.cid + } } } fun collectThreads( apiProvider: Butterfly, - cursor: AtCursor = null, + cursor: AtCursor = AtCursor.EMPTY, posts: List, uri: AtUri = AtUri.HOME_URI, depth: Long = 1, height: Long = 10, @@ -382,8 +449,8 @@ data class MorphoDataFeed ( .none { threads.keys.contains(it.uri) || threads.values.any { thread-> thread.contains(it.uri) } }) { - if (reply.author.did == post.reply?.root?.author?.did - && post.author.did == post.reply.root.author.did + if (reply.author.did == post.reply?.rootPost?.author?.did + && post.author.did == post.reply.rootPost.author.did ) { apiProvider.api.getPostThread( GetPostThreadQuery( @@ -416,7 +483,7 @@ data class MorphoDataFeed ( item is MorphoDataItem.Post && it.value.contains(item.post.uri) } } - emit(MorphoDataFeed(_items = morphoDataItems.toMutableList(), cursor, uri, hasNewPosts = cursor == null)) + emit(MorphoDataFeed(_items = morphoDataItems.toMutableList(), cursor, uri, hasNewPosts = cursor == AtCursor.EMPTY)) }.flowOn(Dispatchers.Default) } @@ -428,7 +495,9 @@ data class MorphoDataFeed ( depth, height, timeRange).distinctUntilChanged().last()) }.flowOn(Dispatchers.Default) - + fun dedupPosts() { + _items = Companion.dedupPosts(_items.toList()).toMutableList() as MutableList + } operator fun plus(feed: MorphoDataFeed) { _items = (_items + feed._items).toMutableList() @@ -453,25 +522,27 @@ data class MorphoDataFeed ( } -fun List.toBskyPostList(): List { - return this.fastMap { it.toPost() } +fun List.toBskyPostList(): List { + return this.fastMap { MorphoDataItem.Post(it.toPost()) } } -fun List.tune( +@Suppress("UNCHECKED_CAST") +fun List.tune( tuners: List = persistentListOf(), -) : List { - var feed = MorphoDataFeed.dedupPosts(this) +) : List { + var feed = MorphoDataFeed.dedupPosts(this ) tuners.fastForEach { tuner-> - feed = tuner(feed) + feed = tuner(feed as List) } - return feed + return feed as List } + fun isFollowingAllAuthors(post: BskyPost, follows: List): Boolean { return follows.fastAny { (post.author.did == it - || post.reply?.parent?.author?.did == it - || post.reply?.root?.author?.did == it) + || post.reply?.parentPost?.author?.did == it + || post.reply?.rootPost?.author?.did == it) } } @@ -485,9 +556,9 @@ fun isQuotePost(post: BskyPost) : Boolean { fun isSelfReply(post: BskyPost) : Boolean { return if (post.reply != null) { - if(post.reply.parent?.author?.did == post.author.did) { + if(post.reply.parentPost?.author?.did == post.author.did) { true - } else post.reply.root?.author?.did == post.author.did + } else post.reply.rootPost?.author?.did == post.author.did } else { false } @@ -495,10 +566,10 @@ fun isSelfReply(post: BskyPost) : Boolean { fun getIfSelfReply(post: BskyPost) : BskyPost? { return if (post.reply != null) { - if(post.reply.parent?.author?.did == post.author.did) { - post.reply.parent - } else if (post.reply.root?.author?.did == post.author.did) { - post.reply.root + if(post.reply.parentPost?.author?.did == post.author.did) { + post.reply.parentPost + } else if (post.reply.rootPost?.author?.did == post.author.did) { + post.reply.rootPost } else { null } @@ -516,5 +587,5 @@ fun isThreadRoot(post: BskyPost) : Boolean { } fun isSecondInThread(post: BskyPost) : Boolean { - return (post.reply?.parent == post.reply?.root && post.replyCount > 0) + return (post.reply?.parentPost == post.reply?.rootPost && post.replyCount > 0) } \ No newline at end of file diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/MorphoDataItem.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/MorphoDataItem.kt index 43177b5..41e8c14 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/MorphoDataItem.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/MorphoDataItem.kt @@ -1,12 +1,16 @@ package com.morpho.app.model.bluesky import androidx.compose.runtime.Immutable +import app.bsky.feed.* import com.morpho.app.CommonParcelable import com.morpho.app.CommonParcelize import com.morpho.app.CommonRawValue import com.morpho.app.util.JavaSerializable +import com.morpho.app.util.deserialize +import com.morpho.butterfly.AtUri import kotlinx.serialization.Serializable + /** * Type union for different types of data items that can be displayed in the app. * Can use interface directly or use subclasses for more specific types where needed. @@ -22,14 +26,237 @@ sealed interface MorphoDataItem: CommonParcelable, JavaSerializable { @Immutable @Serializable @CommonParcelize - sealed interface FeedItem: MorphoDataItem + sealed interface FeedItem: MorphoDataItem { + companion object { + fun fromFeedViewPost(feedPost: FeedViewPost): FeedItem { + val items = mutableListOf() + val reason = feedPost.reason + val post = feedPost.post + val reply = feedPost.reply + var isIncompleteThread = false + var isOrphan = false + if (reply == null) { + val newPost = post.toPost() + return Post(newPost, newPost.reason, isOrphan = true) + } + if (reason != null) { + return Post(post.toPost(reply.toReply(), reason.toReason()), reason.toReason()) + } + var isRootBlocked = false + var isRootNotFound = false + val root = reply.root + val rootUri = when(root) { + is ReplyRefRootUnion.BlockedPost -> { + isRootBlocked = true + (reply.root as ReplyRefRootUnion.BlockedPost).value.uri + } + is ReplyRefRootUnion.NotFoundPost -> { + isRootNotFound = true + (reply.root as ReplyRefRootUnion.NotFoundPost).value.uri + } + is ReplyRefRootUnion.PostView -> { + (reply.root as ReplyRefRootUnion.PostView).value.uri + } + } + val parent = when(val parent = reply.parent) { + is ReplyRefParentUnion.BlockedPost -> { + null + } + is ReplyRefParentUnion.NotFoundPost -> { + null + } + is ReplyRefParentUnion.PostView -> { + (parent as ReplyRefParentUnion.PostView).value + } + } + items.add(feedPost.post) + val grandparent = if (!isRootBlocked && !isRootNotFound + && when(reply.parent) { + is ReplyRefParentUnion.BlockedPost -> { + false + } + is ReplyRefParentUnion.NotFoundPost -> { + false + } + is ReplyRefParentUnion.PostView -> { + val parentRef = reply.parent as ReplyRefParentUnion.PostView + val parentPost = try { + app.bsky.feed.Post.serializer().deserialize(parentRef.value.record) + } catch (e: Exception) { + null + } + parentPost?.reply?.parent?.uri == rootUri + } + }) { + root + } else null + var isGrandParentBlocked = false + var isGrandParentNotFound = false + when(grandparent) { + is ReplyRefRootUnion.BlockedPost -> isGrandParentBlocked = true + is ReplyRefRootUnion.NotFoundPost -> isGrandParentNotFound = true + is ReplyRefRootUnion.PostView -> {} + null -> isGrandParentNotFound = true + } + if(parent != null) items.add(0, parent) + if (isGrandParentBlocked && isGrandParentNotFound) isOrphan = true + if (isRootBlocked || isRootNotFound) { + return Post(post.toPost(reply.toReply(),null), null, isOrphan = true) + } + if (rootUri == parent?.uri) { + return if (items.size == 1) { + Post(post.toPost(reply.toReply(), null), null, isOrphan = isOrphan) + } else { + val parents = items.map { + ThreadPost.ViewablePost(it.toPost(), listOf()) + } + Thread( + BskyPostThread( + post = post.toPost(reply.toReply(), null), + parents = parents, + replies = listOf() + ), + null, + isIncompleteThread = isIncompleteThread, + ) + } + } + if(root is ReplyRefRootUnion.PostView) items.add(0, root.value) + if (grandparent != null && grandparent is ReplyRefRootUnion.PostView) { + items.add(0, grandparent.value) + isIncompleteThread = true + } + return if (items.size == 1) { + Post(post.toPost(reply.toReply(), null), null, isOrphan = isOrphan) + } else { + val parents = items.map { + ThreadPost.ViewablePost(it.toPost(), listOf()) + } + Thread( + BskyPostThread( + post = post.toPost(reply.toReply(), null), + parents = parents, + replies = listOf() + ), + null, + isIncompleteThread = true, + ) + } + } + } + fun getAuthors(): AuthorContext? { + return when(this) { + is Post -> { + AuthorContext( + author = post.author, + parentAuthor = post.reply?.parentPost?.author, + grandParentAuthor = post.reply?.parentPost?.reply?.parentPost?.author, + rootAuthor = post.reply?.rootPost?.author, + ) + } + is Thread -> { + AuthorContext( + author = thread.post.author, + parentAuthor = thread.post.reply?.parentPost?.author, + grandParentAuthor = thread.post.reply?.parentPost?.reply?.parentPost?.author, + rootAuthor = thread.post.reply?.rootPost?.author, + ) + } + } + } + + val key: String + get() = when(this) { + is Post -> { + when(reason) { + is BskyPostReason.BskyPostRepost -> "post_${post.uri}_${reason.indexedAt}_${post.indexedAt}" + is BskyPostReason.BskyPostFeedPost -> "post_${post.uri}_${reason.repost}_${post.indexedAt}" + else -> "post_${post.uri}_${post.reason?.hashCode()?:0}_${post.indexedAt}" + } + } + is Thread -> { + when(reason) { + is BskyPostReason.BskyPostRepost -> "thread_${thread.post.uri}_${reason.indexedAt}_${thread.post.indexedAt}" + is BskyPostReason.BskyPostFeedPost -> "thread_${thread.post.uri}_${reason.repost}_${thread.post.indexedAt}" + else -> "thread_${thread.post.uri}_${thread.post.indexedAt}" + } + } + } + + val rootUri: AtUri + get() = when(this) { + is Post -> post.reply?.replyRef?.root?.uri ?: post.uri + is Thread -> if(thread.post.reply != null) { + thread.post.reply.replyRef?.root?.uri ?: thread.post.uri + } else thread.post.uri + } + + val rootAccessiblePost: BskyPost + get() = when(this) { + is Post -> post.reply?.rootPost ?: post + is Thread -> if(thread.post.reply != null) { + if(thread.post.reply.rootPost != null) { + thread.post.reply.rootPost + } else { + val parent = thread.parents.firstOrNull { + when(it) { + is ThreadPost.ViewablePost -> true + else -> false + } + } + when(parent) { + is ThreadPost.ViewablePost -> parent.post + else -> thread.post + } + } + } else thread.post + } + + val parentAuthor: Profile? + get() = when(this) { + is Post -> post.reply?.parentPost?.author + is Thread -> thread.post.reply?.parentPost?.author + } + val isQuotePost: Boolean + get() = when(this) { + is Post -> when(post.feature) { + is BskyPostFeature.ExternalFeature -> false + is BskyPostFeature.ImagesFeature -> false + is BskyPostFeature.MediaRecordFeature -> true + is BskyPostFeature.RecordFeature -> true + is BskyPostFeature.UnknownEmbed -> false + is BskyPostFeature.VideoFeature -> false + null -> false + } + is Thread -> false + } + + val isReply: Boolean + get() = when(this) { + is Post -> post.reply != null + is Thread -> thread.post.reply != null + } + + val isRepost: Boolean + get() = when(this) { + is Post -> post.reason is BskyPostReason.BskyPostRepost + is Thread -> thread.post.reason is BskyPostReason.BskyPostRepost + } + + val likeCount: Long + get() = when(this) { + is Post -> post.likeCount + is Thread -> thread.post.likeCount + } + } @Immutable @Serializable @CommonParcelize data class Post( val post: @CommonRawValue BskyPost, - val reason: @CommonRawValue BskyPostReason? = null, + val reason: @CommonRawValue BskyPostReason? = post.reason, + val isOrphan: Boolean = false, ): FeedItem @Immutable @@ -38,7 +265,17 @@ sealed interface MorphoDataItem: CommonParcelable, JavaSerializable { data class Thread( val thread: @CommonRawValue BskyPostThread, val reason: @CommonRawValue BskyPostReason? = null, - ): FeedItem + val isIncompleteThread: Boolean = false, + ): FeedItem { + fun addReply(reply: BskyPost): Thread { + return this.copy(thread = thread.addReply(reply)) + } + + fun addReply(reply: ThreadPost.ViewablePost): Thread { + return this.copy(thread = thread.addReply(reply)) + } + + } @Immutable @Serializable @@ -76,4 +313,78 @@ sealed interface MorphoDataItem: CommonParcelable, JavaSerializable { val service: @CommonRawValue BskyLabelService, ): MorphoDataItem + fun containsUri(uri: AtUri): Boolean { + return when(this) { + is Post -> post.uri == uri + is Thread -> { + thread.post.uri == uri || thread.parents.any { parent -> + when(parent) { + is ThreadPost.ViewablePost -> parent.post.uri == uri + is ThreadPost.BlockedPost -> parent.uri == uri + is ThreadPost.NotFoundPost -> parent.uri == uri + } + } || thread.replies.any { reply -> + when(reply) { + is ThreadPost.ViewablePost -> reply.post.uri == uri + is ThreadPost.BlockedPost -> reply.uri == uri + is ThreadPost.NotFoundPost -> reply.uri == uri + } + } + } + is FeedInfo -> feed.uri == uri + is ListInfo -> list.uri == uri + is ModLabel -> label.identifier == uri.atUri + is ProfileItem -> false + is LabelService -> service.uri == uri + } + } + + fun getUris(): List { + return when(this) { + is Post -> listOf(post.uri) + is Thread -> { + ( thread.parents.map { parent -> + when(parent) { + is ThreadPost.ViewablePost -> parent.post.uri + is ThreadPost.BlockedPost -> parent.uri + is ThreadPost.NotFoundPost -> parent.uri + } + } + listOf(thread.post.uri) + thread.replies.map { reply -> + when(reply) { + is ThreadPost.ViewablePost -> reply.post.uri + is ThreadPost.BlockedPost -> reply.uri + is ThreadPost.NotFoundPost -> reply.uri + } + }).filterNotNull() + } + is FeedInfo -> listOf(feed.uri) + is ListInfo -> listOf(list.uri) + is ModLabel -> listOf() + is ProfileItem -> listOf() + is LabelService -> listOf(service.uri) + } + } + + fun getUri(): AtUri? { + return when(this) { + is Post -> post.uri + is Thread -> thread.post.uri + is FeedInfo -> feed.uri + is ListInfo -> list.uri + is ModLabel -> null + is ProfileItem -> null + is LabelService -> service.uri + } + } + + } + +@Immutable +@Serializable +data class AuthorContext( + val author: Profile, + val parentAuthor: Profile? = null, + val grandParentAuthor: Profile? = null, + val rootAuthor: Profile? = null, +) \ No newline at end of file diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/NotificationsList.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/NotificationsList.kt index e84981c..194a555 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/NotificationsList.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/NotificationsList.kt @@ -19,7 +19,7 @@ import kotlinx.serialization.Serializable @Serializable data class NotificationsList( private val notifications: List = persistentListOf(), - val cursor: AtCursor = null, + val cursor: AtCursor = AtCursor.EMPTY, ) { private var _notificationsList: MutableList = mutableListOf() val notificationsList: List diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/BskyDataService.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/BskyDataService.kt index fdc9fab..e48cae9 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/BskyDataService.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/BskyDataService.kt @@ -13,9 +13,6 @@ import com.atproto.repo.GetRecordQuery import com.atproto.repo.StrongRef import com.morpho.app.di.UpdateTick import com.morpho.app.model.bluesky.* -import com.morpho.app.model.bluesky.MorphoDataFeed.Companion.filterByContentLabel -import com.morpho.app.model.bluesky.MorphoDataFeed.Companion.filterByPrefs -import com.morpho.app.model.bluesky.MorphoDataFeed.Companion.filterbyLanguage import com.morpho.app.model.uistate.ContentCardState import com.morpho.app.model.uistate.ContentLoadingState import com.morpho.app.model.uistate.FeedType @@ -76,7 +73,7 @@ suspend fun Flow>>.handleToState( suspend fun getPost(uri: AtUri, api: Butterfly = getKoin().get()): Flow = flow { val query = GetPostsQuery(persistentListOf(uri)) api.api.getPosts(query).onSuccess { response -> - emit(response.posts.first().toPost()) + emit(response.posts.firstOrNull()?.toPost()) }.onFailure { BskyDataService.log.e { "Failed to get post at $uri.\nError: $it" } emit(null) @@ -130,12 +127,10 @@ class BskyDataService: KoinComponent { val api: Butterfly by inject() private val _dataFlows = mutableMapOf>>() - + val useFeedTuners: (MorphoData) -> List = { feed -> + settings.currentUserPrefs.value?.let { FeedTuner.useFeedTuners(it, feed) } ?: listOf(FeedTuner()) + } private val mutex = Mutex() - private var timelineTuners = persistentListOf( - { posts -> filterByContentLabel(posts, contentLabelService.labelsToHide.value) }, - { posts -> filterbyLanguage(posts, languages.value) }, - ) private val contentLabelService by inject() private val settings: SettingsService by inject() private val languages: StateFlow> = settings.languages @@ -153,30 +148,31 @@ class BskyDataService: KoinComponent { suspend fun refresh( uri: AtUri, - cursor: AtCursor = null, + cursor: AtCursor = AtCursor.EMPTY, ): Result>> { val flow = dataFlows[uri] ?: return Result.failure(Exception("No feed to refresh.")) val data = flow.value when(data.feedType) { FeedType.HOME -> { try { - val query = json.decodeFromJsonElement(data.query).copy(cursor = cursor) + val query = json.decodeFromJsonElement(data.query).copy(cursor = cursor.cursor) api.api.getTimeline(query).onSuccess { response -> - - val newPosts = MorphoDataFeed - .collectThreads(api, response.cursor, response.feed.toBskyPostList().tune(timelineTuners)).last() - val feed = if (cursor != null && data.items.isNotEmpty()) { - MorphoDataFeed.concat(data, newPosts) - } else if (cursor == null && data.items.isNotEmpty()) { - MorphoDataFeed.concat(newPosts, data) - } else { - newPosts + if (response.cursor == cursor.cursor && cursor != AtCursor.EMPTY) { + return@onSuccess + } + val new = MorphoData.concatFeed( + query = json.encodeToJsonElement(query), + responseCursor = response.cursor, + oldCursor = cursor, + feed = response.feed, + data = data as MorphoData, + ).collectThreads(api = api).single() + var tunedFeed = new + useFeedTuners(tunedFeed).forEach { tuner -> + tunedFeed = tuner.tune(tunedFeed) } - val newData = feed.toMorphoData("Home") - .copy(query = json.encodeToJsonElement(query)) - mutex.withLock { - _dataFlows[uri]?.update { newData } + _dataFlows[uri]?.update { tunedFeed as MorphoData } } return Result.success(flow) } @@ -187,21 +183,24 @@ class BskyDataService: KoinComponent { } FeedType.PROFILE_POSTS -> { try { - val query = json.decodeFromJsonElement(data.query).copy(cursor = cursor) + val query = json.decodeFromJsonElement(data.query).copy(cursor = cursor.cursor) api.api.getAuthorFeed(query).onSuccess { response -> - val newPosts = MorphoDataFeed - .collectThreads(api, response.cursor, response.feed.toBskyPostList()).last() - val feed = if (cursor != null && data.items.isNotEmpty()) { - MorphoDataFeed.concat(data, newPosts) - } else if (cursor == null && data.items.isNotEmpty()) { - MorphoDataFeed.concat(newPosts, data) - } else { - newPosts + if (response.cursor == cursor.cursor && cursor != AtCursor.EMPTY) { + return@onSuccess + } + var tunedFeed = MorphoData.concatFeed( + query = json.encodeToJsonElement(query), + responseCursor = response.cursor, + oldCursor = cursor, + feed = response.feed, + data = data as MorphoData, + title = "Posts", + ).collectThreads(api = api).single() + useFeedTuners(tunedFeed).forEach { tuner -> + tunedFeed = tuner.tune(tunedFeed) } - val newData = feed.toMorphoData("Posts") - .copy(query = json.encodeToJsonElement(query)) mutex.withLock { - _dataFlows[uri]?.update { newData } + _dataFlows[uri]?.update { tunedFeed as MorphoData } } return Result.success(flow) } @@ -212,21 +211,24 @@ class BskyDataService: KoinComponent { } FeedType.PROFILE_REPLIES -> { try { - val query = Json.decodeFromJsonElement(data.query).copy(cursor = cursor) + val query = Json.decodeFromJsonElement(data.query).copy(cursor = cursor.cursor) api.api.getAuthorFeed(query).onSuccess { response -> - val newPosts = MorphoDataFeed - .collectThreads(api, response.cursor, response.feed.toBskyPostList()).last() - val feed = if (cursor != null && data.items.isNotEmpty()) { - MorphoDataFeed.concat(data, newPosts) - } else if (cursor == null && data.items.isNotEmpty()) { - MorphoDataFeed.concat(newPosts, data) - } else { - newPosts + if (response.cursor == cursor.cursor && cursor != AtCursor.EMPTY) { + return@onSuccess + } + var tunedFeed = MorphoData.concatFeed( + query = json.encodeToJsonElement(query), + responseCursor = response.cursor, + oldCursor = cursor, + feed = response.feed, + data = data as MorphoData, + title = "Replies", + ).collectThreads(api = api).single() + useFeedTuners(tunedFeed).forEach { tuner -> + tunedFeed = tuner.tune(tunedFeed) } - val newData = feed.toMorphoData("Replies") - .copy(query = json.encodeToJsonElement(query)) mutex.withLock { - _dataFlows[uri]?.update { newData } + _dataFlows[uri]?.update { tunedFeed as MorphoData } } return Result.success(flow) } @@ -237,21 +239,24 @@ class BskyDataService: KoinComponent { } FeedType.PROFILE_MEDIA -> { try { - val query = json.decodeFromJsonElement(data.query).copy(cursor = cursor) + val query = json.decodeFromJsonElement(data.query).copy(cursor = cursor.cursor) api.api.getAuthorFeed(query).onSuccess { response -> - val newPosts = MorphoDataFeed - .collectThreads(api, response.cursor, response.feed.toBskyPostList()).last() - val feed = if (cursor != null && data.items.isNotEmpty()) { - MorphoDataFeed.concat(data, newPosts) - } else if (cursor == null && data.items.isNotEmpty()) { - MorphoDataFeed.concat(newPosts, data) - } else { - newPosts + if (response.cursor == cursor.cursor && cursor != AtCursor.EMPTY) { + return@onSuccess + } + var tunedFeed = MorphoData.concatFeed( + query = json.encodeToJsonElement(query), + responseCursor = response.cursor, + oldCursor = cursor, + feed = response.feed, + data = data as MorphoData, + title = "Media", + ) + useFeedTuners(tunedFeed).forEach { tuner -> + tunedFeed = tuner.tune(tunedFeed) } - val newData = feed.toMorphoData("Media") - .copy(query = json.encodeToJsonElement(query)) mutex.withLock { - _dataFlows[uri]?.update { newData } + _dataFlows[uri]?.update { tunedFeed as MorphoData } } return Result.success(flow) } @@ -262,21 +267,24 @@ class BskyDataService: KoinComponent { } FeedType.PROFILE_LIKES -> { try { - val query = json.decodeFromJsonElement(data.query).copy(cursor = cursor) + val query = json.decodeFromJsonElement(data.query).copy(cursor = cursor.cursor) api.api.getActorLikes(query).onSuccess { response -> - val newPosts = MorphoDataFeed - .collectThreads(api, response.cursor, response.feed.toBskyPostList()).last() - val feed = if (cursor != null && data.items.isNotEmpty()) { - MorphoDataFeed.concat(data, newPosts) - } else if (cursor == null && data.items.isNotEmpty()) { - MorphoDataFeed.concat(newPosts, data) - } else { - newPosts + if (response.cursor == cursor.cursor && cursor != AtCursor.EMPTY) { + return@onSuccess + } + var tunedFeed = MorphoData.concatFeed( + query = json.encodeToJsonElement(query), + responseCursor = response.cursor, + oldCursor = cursor, + feed = response.feed, + data = data as MorphoData, + title = "Likes", + ) + useFeedTuners(tunedFeed).forEach { tuner -> + tunedFeed = tuner.tune(tunedFeed) } - val newData = feed.toMorphoData("Likes") - .copy(query = json.encodeToJsonElement(query)) mutex.withLock { - _dataFlows[uri]?.update { newData } + _dataFlows[uri]?.update { tunedFeed as MorphoData } } return Result.success(flow) } @@ -287,14 +295,17 @@ class BskyDataService: KoinComponent { } FeedType.PROFILE_USER_LISTS -> { try { - val query = json.decodeFromJsonElement(data.query).copy(cursor = cursor) + val query = json.decodeFromJsonElement(data.query).copy(cursor = cursor.cursor) api.api.getLists(query).onSuccess { response -> - val newData = if (cursor != null && data.items.isNotEmpty()) { + if (response.cursor == cursor.cursor && cursor != AtCursor.EMPTY) { + return@onSuccess + } + val newData = if (cursor != AtCursor.EMPTY && data.items.isNotEmpty()) { MorphoData.concat(data, response.lists.mapImmutable { MorphoDataItem.ListInfo(it.toList()) }) - } else if (cursor == null && data.items.isNotEmpty()) { + } else if (cursor == AtCursor.EMPTY && data.items.isNotEmpty()) { MorphoData.concat(response.lists.mapImmutable { MorphoDataItem.ListInfo(it.toList()) }, data) } else { - MorphoData("Lists", uri, response.cursor, + MorphoData("Lists", uri, AtCursor(response.cursor, cursor.scroll), response.lists.mapImmutable { MorphoDataItem.ListInfo(it.toList()) }) }.copy(query = json.encodeToJsonElement(query)) mutex.withLock { @@ -311,7 +322,8 @@ class BskyDataService: KoinComponent { try { val query = json.decodeFromJsonElement(data.query) api.api.getServices(query).onSuccess { response -> - val newData = if (cursor != null && data.items.isNotEmpty()) { + + val newData = if (cursor != AtCursor.EMPTY && data.items.isNotEmpty()) { MorphoData.concat(data, response.views.mapImmutable { when(it) { is GetServicesResponseViewUnion.LabelerViewDetailed -> @@ -320,7 +332,7 @@ class BskyDataService: KoinComponent { MorphoDataItem.LabelService(it.value.toLabelService()) } }) - } else if (cursor == null && data.items.isNotEmpty()) { + } else if (cursor == AtCursor.EMPTY && data.items.isNotEmpty()) { MorphoData.concat(response.views.mapImmutable { when(it) { is GetServicesResponseViewUnion.LabelerViewDetailed -> @@ -330,7 +342,7 @@ class BskyDataService: KoinComponent { } }, data) } else { - MorphoData("Services", uri, null, + MorphoData("Services", uri, AtCursor.EMPTY, response.views.mapImmutable { when(it) { is GetServicesResponseViewUnion.LabelerViewDetailed -> @@ -352,14 +364,17 @@ class BskyDataService: KoinComponent { } FeedType.PROFILE_FEEDS_LIST -> { try { - val query = json.decodeFromJsonElement(data.query).copy(cursor = cursor) + val query = json.decodeFromJsonElement(data.query).copy(cursor = cursor.cursor) api.api.getActorFeeds(query).onSuccess { response -> - val newData = if (cursor != null && data.items.isNotEmpty()) { + if (response.cursor == cursor.cursor && cursor != AtCursor.EMPTY) { + return@onSuccess + } + val newData = if (cursor != AtCursor.EMPTY && data.items.isNotEmpty()) { MorphoData.concat(data, response.feeds.mapImmutable { MorphoDataItem.FeedInfo(it.toFeedGenerator()) }) - } else if (cursor == null && data.items.isNotEmpty()) { + } else if (cursor == AtCursor.EMPTY && data.items.isNotEmpty()) { MorphoData.concat(response.feeds.mapImmutable { MorphoDataItem.FeedInfo(it.toFeedGenerator()) }, data) } else { - MorphoData("Feeds", uri, response.cursor, + MorphoData("Feeds", uri, AtCursor(response.cursor, cursor.scroll), response.feeds.mapImmutable { MorphoDataItem.FeedInfo(it.toFeedGenerator()) }) }.copy(query = json.encodeToJsonElement(query)) mutex.withLock { @@ -374,23 +389,50 @@ class BskyDataService: KoinComponent { } FeedType.OTHER -> { try { - val query = json.decodeFromJsonElement(data.query).copy(cursor = cursor) + val query = json.decodeFromJsonElement(data.query).copy(cursor = cursor.cursor) api.api.getFeed(query).onSuccess { response -> - val tuners = persistentListOf() - - val newPosts = MorphoDataFeed - .collectThreads(api, response.cursor, response.feed.toBskyPostList().tune(tuners)).last() - val feed = if (cursor != null && data.items.isNotEmpty()) { - MorphoDataFeed.concat(data, newPosts) - } else if (cursor == null && data.items.isNotEmpty()) { - MorphoDataFeed.concat(newPosts, data) - } else { - newPosts + if (response.cursor == cursor.cursor && cursor != AtCursor.EMPTY) { + return@onSuccess + } + var tunedFeed = MorphoData.concatFeed( + query = json.encodeToJsonElement(query), + responseCursor = response.cursor, + oldCursor = cursor, + feed = response.feed, + data = data as MorphoData, + ).collectThreads(api = api).single() + useFeedTuners(tunedFeed).forEach { tuner -> + tunedFeed = tuner.tune(tunedFeed) } - val newData = feed.toMorphoData(data.title) - .copy(query = json.encodeToJsonElement(query)) mutex.withLock { - _dataFlows[uri]?.update { newData } + _dataFlows[uri]?.update { tunedFeed as MorphoData } + } + return Result.success(flow) + } + } catch (e: Exception) { + log.e { "Failed to refresh feed at $uri.\nError: $e" } + return Result.failure(e) + } + } + FeedType.LIST_FOLLOWING -> { + try { + val query = json.decodeFromJsonElement(data.query).copy(cursor = cursor.cursor) + api.api.getListFeed(query).onSuccess { response -> + if (response.cursor == cursor.cursor && cursor != AtCursor.EMPTY) { + return@onSuccess + } + var tunedFeed = MorphoData.concatFeed( + query = json.encodeToJsonElement(query), + responseCursor = response.cursor, + oldCursor = cursor, + feed = response.feed, + data = data as MorphoData, + ).collectThreads(api = api).single() + useFeedTuners(tunedFeed).forEach { tuner -> + tunedFeed = tuner.tune(tunedFeed) + } + mutex.withLock { + _dataFlows[uri]?.update { tunedFeed as MorphoData } } return Result.success(flow) } @@ -414,31 +456,31 @@ class BskyDataService: KoinComponent { //log.d { "Timeline flow tick." } val (cur, pref) = flows val prev = dataFlows[AtUri.HOME_URI]?.value - val query = GetTimelineQuery(limit = limit, cursor = cur) + val query = GetTimelineQuery(limit = limit, cursor = cur.cursor) api.api.getTimeline(query).onSuccess { response -> - val tuners = persistentListOf() - tuners.add { posts -> filterByPrefs(posts, pref) } - - val newPosts = MorphoDataFeed - .collectThreads(api, response.cursor, response.feed.toBskyPostList().tune(tuners)).last() - val feed = if (cur != null && prev != null && prev.items.isNotEmpty()) { - MorphoDataFeed.concat(prev, newPosts) - } else if (cur == null && prev != null && prev.items.isNotEmpty()) { - MorphoDataFeed.concat(newPosts, prev) - } else { - newPosts + if (response.cursor == cur.cursor && cur != AtCursor.EMPTY) { + return@collect } - val data = feed.toMorphoData("Home") - .copy(query = json.encodeToJsonElement(query)) - - emit(Result.success(data as MorphoData)) + var tunedFeed = MorphoData.concatFeed( + query = json.encodeToJsonElement(query), + responseCursor = response.cursor, + oldCursor = cur, + feed = response.feed, + data = (prev ?: MorphoData.EMPTY()) as MorphoData, + title = "Home", + uri = AtUri.HOME_URI, + ).collectThreads(api = api).single() + useFeedTuners(tunedFeed).forEach { tuner -> + tunedFeed = tuner.tune(tunedFeed) + } + emit(Result.success(tunedFeed)) log.d{ "Timeline " + "Old cursor: $cur " + "New cursor: ${response.cursor}" } log.v { - "${data.items.map { + "${tunedFeed.items.map { when(it) { is MorphoDataItem.Post -> "${it.post.uri}\n" is MorphoDataItem.Thread -> "${it.thread.post.uri}\n" @@ -446,8 +488,8 @@ class BskyDataService: KoinComponent { }}" } mutex.withLock { - if(prev == null) _dataFlows[AtUri.HOME_URI] = MutableStateFlow(data) - else _dataFlows[AtUri.HOME_URI]?.update { data } + if(prev == null) _dataFlows[AtUri.HOME_URI] = MutableStateFlow(tunedFeed as MorphoData) + else _dataFlows[AtUri.HOME_URI]?.update { tunedFeed as MorphoData } } }.onFailure { emit(Result.failure(it)) @@ -474,36 +516,31 @@ class BskyDataService: KoinComponent { val cur = flows.first val pref = flows.second val prev = dataFlows[feedInfo.uri]?.value - val query = GetFeedQuery(feedInfo.uri, limit, cur) + val query = GetFeedQuery(feedInfo.uri, limit, cur.cursor) api.api.getFeed(query).onSuccess { response -> - val tuners = persistentListOf() - if (pref != null) tuners.add { posts -> filterByPrefs(posts, pref) } - - val newPosts = MorphoDataFeed - .collectThreads( - api, - response.cursor, - response.feed.toBskyPostList().tune(tuners), - feedInfo.uri - ).last() - val feed = if (cur != null && prev != null && prev.items.isNotEmpty()) { - MorphoDataFeed.concat(prev, newPosts) - } else if (cur == null && prev != null && prev.items.isNotEmpty()) { - MorphoDataFeed.concat(newPosts, prev) - } else { - newPosts + if (response.cursor == cur.cursor) { + return@collect } - val data = feed.toMorphoData(feedInfo.name) - .copy(query = json.encodeToJsonElement(query)) - - emit(Result.success(data as MorphoData)) + var tunedFeed = MorphoData.concatFeed( + query = json.encodeToJsonElement(query), + responseCursor = response.cursor, + oldCursor = cur, + feed = response.feed, + data = (prev ?: MorphoData.EMPTY()) as MorphoData, + title = feedInfo.name, + uri = feedInfo.uri, + ).collectThreads(api = api).single() + useFeedTuners(tunedFeed).forEach { tuner -> + tunedFeed = tuner.tune(tunedFeed) + } + emit(Result.success(tunedFeed)) log.d{ "Feed: ${feedInfo.name} " + "Old cursor: $cur " + "New cursor: ${response.cursor}" } log.v { - "${data.items.map { + "${tunedFeed.items.map { when(it) { is MorphoDataItem.Post -> it.post.uri is MorphoDataItem.Thread -> it.thread.post.uri @@ -511,8 +548,8 @@ class BskyDataService: KoinComponent { }}" } mutex.withLock { - if(prev == null) _dataFlows[feedInfo.uri] = MutableStateFlow(data) - else _dataFlows[feedInfo.uri]?.update { data } + if(prev == null) _dataFlows[feedInfo.uri] = MutableStateFlow(tunedFeed as MorphoData) + else _dataFlows[feedInfo.uri]?.update { tunedFeed as MorphoData } } }.onFailure { emit(Result.failure(it)) @@ -531,14 +568,17 @@ class BskyDataService: KoinComponent { val uri = AtUri.followsUri(id) cursor.collect { cur -> val prev = dataFlows[uri]?.value - val query = GetFollowsQuery(id, limit, cur) + val query = GetFollowsQuery(id, limit, cur.cursor) api.api.getFollows(query).onSuccess { response -> - val data = if (cur != null && prev != null && prev.items.isNotEmpty()) { + if (response.cursor == cur.cursor) { + return@collect + } + val data = if (cur != AtCursor.EMPTY && prev != null && prev.items.isNotEmpty()) { MorphoData.concat(prev, response.follows.mapImmutable { MorphoDataItem.ProfileItem(it.toProfile()) }) - } else if (cur == null && prev != null && prev.items.isNotEmpty()) { + } else if (cur == AtCursor.EMPTY && prev != null && prev.items.isNotEmpty()) { MorphoData.concat(response.follows.mapImmutable { MorphoDataItem.ProfileItem(it.toProfile()) }, prev) } else { - MorphoData("Following", uri, response.cursor, + MorphoData("Following", uri, AtCursor(response.cursor, cur.scroll), response.follows.mapImmutable { MorphoDataItem.ProfileItem(it.toProfile()) }) }.copy(query = json.encodeToJsonElement(query)) emit(Result.success(data as MorphoData)) @@ -563,14 +603,17 @@ class BskyDataService: KoinComponent { val uri = AtUri.followersUri(id) cursor.collect { cur -> val prev = dataFlows[uri]?.value - val query = GetFollowersQuery(id, limit, cur) + val query = GetFollowersQuery(id, limit, cur.cursor) api.api.getFollowers(query).onSuccess { response -> - val data = if (cur != null && prev != null && prev.items.isNotEmpty()) { + if (response.cursor == cur.cursor) { + return@collect + } + val data = if (cur != AtCursor.EMPTY && prev != null && prev.items.isNotEmpty()) { MorphoData.concat(prev, response.followers.mapImmutable { MorphoDataItem.ProfileItem(it.toProfile()) }) - } else if (cur == null && prev != null && prev.items.isNotEmpty()) { + } else if (cur == AtCursor.EMPTY && prev != null && prev.items.isNotEmpty()) { MorphoData.concat(response.followers.mapImmutable { MorphoDataItem.ProfileItem(it.toProfile()) }, prev) } else { - MorphoData("Following", uri, response.cursor, + MorphoData("Following", uri, AtCursor(response.cursor, cur.scroll), response.followers.mapImmutable { MorphoDataItem.ProfileItem(it.toProfile()) }) }.copy(query = json.encodeToJsonElement(query)) emit(Result.success(data as MorphoData)) @@ -598,23 +641,26 @@ class BskyDataService: KoinComponent { val uri = AtUri.profilePostsUri(id) cursor.collect { cur -> val prev = dataFlows[uri]?.value - val query = GetAuthorFeedQuery(id, limit, cur, GetAuthorFeedFilter.POSTS_NO_REPLIES) + val query = GetAuthorFeedQuery(id, limit, cur.cursor, GetAuthorFeedFilter.POSTS_NO_REPLIES) api.api.getAuthorFeed(query).onSuccess { response -> - val newPosts = MorphoDataFeed - .collectThreads(api, response.cursor, response.feed.toBskyPostList()).last() - val feed = if (cur != null && prev != null && prev.items.isNotEmpty()) { - MorphoDataFeed.concat(prev, newPosts) - } else if (cur == null && prev != null && prev.items.isNotEmpty()) { - MorphoDataFeed.concat(newPosts, prev) - } else { - newPosts + if (response.cursor == cur.cursor) { + return@collect } - val data = feed.toMorphoData("Posts", uri) - .copy(query = json.encodeToJsonElement(query)) - emit(Result.success(data as MorphoData)) + var tunedFeed = MorphoData.concatFeed( + query = json.encodeToJsonElement(query), + responseCursor = response.cursor, + oldCursor = cur, + feed = response.feed, + data = (prev ?: MorphoData.EMPTY()) as MorphoData, + title = "Posts", + ).collectThreads(api = api).single() + useFeedTuners(tunedFeed).forEach { tuner -> + tunedFeed = tuner.tune(tunedFeed) + } + emit(Result.success(tunedFeed)) mutex.withLock { - if(prev == null) _dataFlows[uri] = MutableStateFlow(data) - else _dataFlows[uri]?.update { data } + if(prev == null) _dataFlows[uri] = MutableStateFlow(tunedFeed as MorphoData) + else _dataFlows[uri]?.update { tunedFeed as MorphoData } } }.onFailure { emit(Result.failure(it)) @@ -627,23 +673,26 @@ class BskyDataService: KoinComponent { val uri = AtUri.profileRepliesUri(id) cursor.collect { cur -> val prev = dataFlows[uri]?.value - val query = GetAuthorFeedQuery(id, limit, cur, GetAuthorFeedFilter.POSTS_WITH_REPLIES) + val query = GetAuthorFeedQuery(id, limit, cur.cursor, GetAuthorFeedFilter.POSTS_WITH_REPLIES) api.api.getAuthorFeed(query).onSuccess { response -> - val newPosts = MorphoDataFeed - .collectThreads(api, response.cursor, response.feed.toBskyPostList()).last() - val feed = if (cur != null && prev != null && prev.items.isNotEmpty()) { - MorphoDataFeed.concat(prev, newPosts) - } else if (cur == null && prev != null && prev.items.isNotEmpty()) { - MorphoDataFeed.concat(newPosts, prev) - } else { - newPosts + if (response.cursor == cur.cursor) { + return@collect + } + var tunedFeed = MorphoData.concatFeed( + query = json.encodeToJsonElement(query), + responseCursor = response.cursor, + oldCursor = cur, + feed = response.feed, + data = (prev ?: MorphoData.EMPTY()) as MorphoData, + title = "Replies", + ).collectThreads(api = api).single() + useFeedTuners(tunedFeed).forEach { tuner -> + tunedFeed = tuner.tune(tunedFeed) } - val data = feed.toMorphoData("Replies", uri) - .copy(query = json.encodeToJsonElement(query)) - emit(Result.success(data as MorphoData)) + emit(Result.success(tunedFeed)) mutex.withLock { - if(prev == null) _dataFlows[uri] = MutableStateFlow(data) - else _dataFlows[uri]?.update { data } + if(prev == null) _dataFlows[uri] = MutableStateFlow(tunedFeed as MorphoData) + else _dataFlows[uri]?.update { tunedFeed as MorphoData } } }.onFailure { emit(Result.failure(it)) @@ -656,23 +705,26 @@ class BskyDataService: KoinComponent { val uri = AtUri.profileMediaUri(id) cursor.collect { cur -> val prev = dataFlows[uri]?.value - val query = GetAuthorFeedQuery(id, limit, cur, GetAuthorFeedFilter.POSTS_WITH_MEDIA) + val query = GetAuthorFeedQuery(id, limit, cur.cursor, GetAuthorFeedFilter.POSTS_WITH_MEDIA) api.api.getAuthorFeed(query).onSuccess { response -> - val newPosts = MorphoDataFeed - .collectThreads(api, response.cursor, response.feed.toBskyPostList()).last() - val feed = if (cur != null && prev != null && prev.items.isNotEmpty()) { - MorphoDataFeed.concat(prev, newPosts) - } else if (cur == null && prev != null && prev.items.isNotEmpty()) { - MorphoDataFeed.concat(newPosts, prev) - } else { - newPosts + if (response.cursor == cur.cursor) { + return@collect + } + var tunedFeed = MorphoData.concatFeed( + query = json.encodeToJsonElement(query), + responseCursor = response.cursor, + oldCursor = cur, + feed = response.feed, + data = (prev ?: MorphoData.EMPTY()) as MorphoData, + title = "Media", + ) + useFeedTuners(tunedFeed).forEach { tuner -> + tunedFeed = tuner.tune(tunedFeed) } - val data = feed.toMorphoData("Media", uri) - .copy(query = json.encodeToJsonElement(query)) - emit(Result.success(data as MorphoData)) + emit(Result.success(tunedFeed)) mutex.withLock { - if(prev == null) _dataFlows[uri] = MutableStateFlow(data) - else _dataFlows[uri]?.update { data } + if(prev == null) _dataFlows[uri] = MutableStateFlow(tunedFeed as MorphoData) + else _dataFlows[uri]?.update { tunedFeed as MorphoData } } }.onFailure { emit(Result.failure(it)) @@ -728,14 +780,14 @@ class BskyDataService: KoinComponent { val uri = AtUri.profileUserListsUri(id) cursor.collect { cur -> val prev = dataFlows[uri]?.value - val query = GetListsQuery(id, limit, cur) + val query = GetListsQuery(id, limit, cur.cursor) api.api.getLists(query).onSuccess { response -> - val data = if (cur != null && prev != null && prev.items.isNotEmpty()) { + val data = if (cur != AtCursor.EMPTY && prev != null && prev.items.isNotEmpty()) { MorphoData.concat(prev, response.lists.mapImmutable { MorphoDataItem.ListInfo(it.toList()) }) - } else if (cur == null && prev != null && prev.items.isNotEmpty()) { + } else if (cur == AtCursor.EMPTY && prev != null && prev.items.isNotEmpty()) { MorphoData.concat(response.lists.mapImmutable { MorphoDataItem.ListInfo(it.toList()) }, prev) } else { - MorphoData("Lists", uri, response.cursor, + MorphoData("Lists", uri, AtCursor(response.cursor, cur.scroll), response.lists.mapImmutable { MorphoDataItem.ListInfo(it.toList()) }) }.copy(query = json.encodeToJsonElement(query)) @@ -761,14 +813,14 @@ class BskyDataService: KoinComponent { val uri = AtUri.profileFeedsListUri(id) cursor.onEach { cur -> val prev = dataFlows[uri]?.value - val query = GetActorFeedsQuery(id, limit, cur) + val query = GetActorFeedsQuery(id, limit, cur.cursor) api.api.getActorFeeds(query).onSuccess { response -> - val data = if (cur != null && prev != null && prev.items.isNotEmpty()) { + val data = if (cur != AtCursor.EMPTY && prev != null && prev.items.isNotEmpty()) { MorphoData.concat(prev, response.feeds.mapImmutable { MorphoDataItem.FeedInfo(it.toFeedGenerator()) }) - } else if (cur == null && prev != null && prev.items.isNotEmpty()) { + } else if (cur == AtCursor.EMPTY && prev != null && prev.items.isNotEmpty()) { MorphoData.concat(response.feeds.mapImmutable { MorphoDataItem.FeedInfo(it.toFeedGenerator()) }, prev) } else { - MorphoData("Feeds", uri, response.cursor, + MorphoData("Feeds", uri, AtCursor(response.cursor, cur.scroll), response.feeds.mapImmutable { MorphoDataItem.FeedInfo(it.toFeedGenerator()) }) }.copy(query = json.encodeToJsonElement(query)) @@ -794,7 +846,7 @@ class BskyDataService: KoinComponent { update.collect { val query = GetServicesQuery(listOf(did).toImmutableList(), true) api.api.getServices(query).onSuccess { response -> - val data = MorphoData("Labels", uri, null, + val data = MorphoData("Labels", uri, AtCursor.EMPTY, response.views.mapImmutable { when(it) { is GetServicesResponseViewUnion.LabelerViewDetailed -> @@ -827,23 +879,23 @@ class BskyDataService: KoinComponent { cursor.collect { cur -> val prev = dataFlows[uri]?.value - val query = GetActorLikesQuery(id, limit, cur) + val query = GetActorLikesQuery(id, limit, cur.cursor) api.api.getActorLikes(query) .onSuccess { response -> - val newPosts = MorphoDataFeed - .collectThreads(api, response.cursor, response.feed.toBskyPostList()).last() - val feed = if (cur != null && prev != null && prev.items.isNotEmpty()) { - MorphoDataFeed.concat(prev, newPosts) - } else if (cur == null && prev != null && prev.items.isNotEmpty()) { - MorphoDataFeed.concat(newPosts, prev) - } else { - newPosts + var tunedFeed = MorphoData.concatFeed( + query = json.encodeToJsonElement(query), + responseCursor = response.cursor, + oldCursor = cur, + feed = response.feed, + data = (prev ?: MorphoData.EMPTY()) as MorphoData, + title = "Likes", + ) + useFeedTuners(tunedFeed).forEach { tuner -> + tunedFeed = tuner.tune(tunedFeed) } - val data = feed.toMorphoData("Likes").copy(query = json.encodeToJsonElement(query)) - - emit(Result.success(data as MorphoData)) + emit(Result.success(tunedFeed)) mutex.withLock { - if(prev == null) _dataFlows[uri] = MutableStateFlow(data) - else _dataFlows[uri]?.update { data } + if(prev == null) _dataFlows[uri] = MutableStateFlow(tunedFeed as MorphoData) + else _dataFlows[uri]?.update { tunedFeed as MorphoData } } }.onFailure { emit(Result.failure(it)) @@ -865,7 +917,7 @@ class BskyDataService: KoinComponent { val query = GetProfilesQuery(profiles.toPersistentList()) api.api.getProfiles(query).onSuccess { response -> - val data = MorphoData("Profiles", uri, null, + val data = MorphoData("Profiles", uri, AtCursor.EMPTY, response.profiles.mapImmutable { MorphoDataItem.ProfileItem(it.toProfile()) }, json.encodeToJsonElement(query)) @@ -1031,6 +1083,21 @@ class BskyDataService: KoinComponent { } }.onFailure { emit(null) } } + FeedType.LIST_FOLLOWING -> { + val query = GetListFeedQuery(feed.uri, 1) + api.api.getListFeed(query).onSuccess { response -> + if (response.feed.isNotEmpty()) { + val cid = response.feed.first().post.cid + if (!feed.contains(cid)) { + emit(MorphoDataItem.Post(response.feed.first().toPost())) + } else { + emit(null) + } + } + }.onFailure { + emit(null) + } + } } } }.distinctUntilChanged().flowOn(dispatcher) diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/BskyNotificationService.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/BskyNotificationService.kt index 6dc5ab5..a865778 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/BskyNotificationService.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/BskyNotificationService.kt @@ -111,13 +111,13 @@ class BskyNotificationService: KoinComponent { dispatcher: CoroutineDispatcher = Dispatchers.IO, ): Flow> = flow { cursor.debounce(300).collect { cursor -> - val query = ListNotificationsQuery(limit, cursor) + val query = ListNotificationsQuery(limit, cursor.cursor) val result = api.api.listNotifications(query).map { response -> if(notifications.value.notificationsList.isNotEmpty()) { - if (cursor == null) { + if (cursor == AtCursor.EMPTY) { NotificationsList( response.notifications.mapImmutable { it.toBskyNotification() }, - response.cursor + AtCursor(response.cursor, cursor.scroll) ).concat(notifications.value) } else { notifications.value.concat(response.notifications) @@ -125,7 +125,7 @@ class BskyNotificationService: KoinComponent { } else { NotificationsList( response.notifications.mapImmutable { it.toBskyNotification() }, - response.cursor + AtCursor(response.cursor, cursor.scroll) ) } } diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/MorphoData.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/MorphoData.kt index 0321604..d984659 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/MorphoData.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/MorphoData.kt @@ -2,26 +2,43 @@ package com.morpho.app.model.uidata //import com.rickclephas.kmp.nativecoroutines.NativeCoroutines import androidx.compose.runtime.Immutable -import androidx.compose.ui.util.fastAny -import com.morpho.app.model.bluesky.MorphoDataFeed -import com.morpho.app.model.bluesky.MorphoDataItem +import androidx.compose.ui.util.* +import app.bsky.feed.FeedViewPost +import com.morpho.app.data.BskyUserPreferences +import com.morpho.app.model.bluesky.* import com.morpho.app.model.uistate.FeedType import com.morpho.app.util.JavaSerializable import com.morpho.butterfly.* +import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toPersistentList +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.firstOrNull +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.flowOn import kotlinx.datetime.Instant import kotlinx.serialization.Serializable import kotlinx.serialization.json.JsonElement import kotlinx.serialization.json.JsonObject +import kotlin.time.Duration -typealias AtCursor = String? + +typealias TunerFunction = (List, FeedTuner) -> List + +@Immutable +@Serializable +data class AtCursor(val cursor: String?, val scroll: Int){ + companion object { + val EMPTY: AtCursor = AtCursor(null, 0) + } +} @Immutable @Serializable data class MorphoData( val title: String = "Home", val uri: AtUri = AtUri.HOME_URI, - val cursor: AtCursor = null, + val cursor: AtCursor = AtCursor.EMPTY, val items: List = listOf(), val query: JsonElement = JsonObject(emptyMap()), ): JavaSerializable { @@ -31,19 +48,79 @@ data class MorphoData( return MorphoData( title = "Home", uri = AtUri.HOME_URI, - cursor = null, + cursor = AtCursor.EMPTY, items = listOf(), query = JsonObject(emptyMap()), ) } + fun fromList( + title: String = "Home", + uri: AtUri = AtUri.HOME_URI, + cursor: AtCursor = AtCursor.EMPTY, + items: List, + query: JsonElement = JsonObject(emptyMap()), + ): MorphoData { + return MorphoData( + title = title, + uri = uri, + cursor = cursor, + items = items, + query = query, + ) + } + + fun fromFeed( + feedPosts: List, + cursor: AtCursor = AtCursor.EMPTY, + title: String = "Home", + uri: AtUri = AtUri.HOME_URI, + query: JsonElement = JsonObject(emptyMap()), + ): MorphoData { + val items = feedPosts.map { item -> + MorphoDataItem.FeedItem.fromFeedViewPost(item) + } + return MorphoData( + title = title, + uri = uri, + cursor = cursor, + items = items, + query = query, + ) + } + + fun concatFeed( + query: JsonElement, + responseCursor: String?, + oldCursor: AtCursor, + feed: List, + data: MorphoData, + uri: AtUri = data.uri, + title: String = data.title, + ): MorphoData { + return if (oldCursor != AtCursor.EMPTY && data.items.isNotEmpty()) { + concat(feed.toList(), data, + AtCursor(responseCursor, oldCursor.scroll), + query = query) + } else if (oldCursor == AtCursor.EMPTY && data.items.isNotEmpty()) { + concat(feed.toList(), data, + AtCursor(responseCursor, oldCursor.scroll), + query = query) + } else { + fromFeed( + feed.toList(), AtCursor(responseCursor, oldCursor.scroll), + uri = uri, title = title, query = query) + } + } + fun concat( first: MorphoData, last: MorphoData, - cursor: AtCursor = last.cursor + cursor: AtCursor = last.cursor, + query: JsonElement = JsonObject(emptyMap()), ): MorphoData { - return MorphoData( + return first.copy( items = (first.items union last.items).toPersistentList() .sortedByDescending { when (it) { @@ -63,12 +140,13 @@ data class MorphoData( ) } - fun concat( - first: MorphoData, - last: List, - cursor: AtCursor = first.cursor - ): MorphoData { - return MorphoData( + fun concat( + first: MorphoData, + last: List, + cursor: AtCursor = first.cursor, + query: JsonElement = JsonObject(emptyMap()), + ): MorphoData { + return first.copy( items = (first.items union last).toPersistentList() .sortedByDescending { when (it) { @@ -91,9 +169,9 @@ data class MorphoData( fun concat( first: List, last: MorphoData, - cursor: AtCursor = last.cursor + cursor: AtCursor = last.cursor, ): MorphoData { - return MorphoData( + return last.copy( items = (first union last.items).toPersistentList() .sortedByDescending { when (it) { @@ -113,6 +191,92 @@ data class MorphoData( ) } + fun concat( + posts: List, + feed: MorphoData, + cursor: AtCursor = feed.cursor, + query: JsonElement = JsonObject(emptyMap()), + ): MorphoData { + val new = fromFeed( + feedPosts = posts, + cursor, + feed.title, + feed.uri, + query = feed.query, + ) + return concat(new, feed, cursor, query) + } + + fun fromFeedGenList( + title: String, + uri: AtUri, + feeds: List, + cursor: AtCursor = AtCursor.EMPTY, + ): MorphoData { + return MorphoData( + title = title, + uri = uri, + cursor = cursor, + items = feeds.map { MorphoDataItem.FeedInfo(it) }.toMutableList(), + ) + } + + fun fromProfileList( + title: String, + uri: AtUri, + list: List, + cursor: AtCursor = AtCursor.EMPTY, + ): MorphoData { + return MorphoData( + title = title, + uri = uri, + cursor = cursor, + items = list.map { MorphoDataItem.ProfileItem(it) }.toMutableList(), + ) + } + + fun fromBskyList( + title: String, + uri: AtUri, + lists: List, + cursor: AtCursor = AtCursor.EMPTY, + ): MorphoData { + return MorphoData( + title = title, + uri = uri, + cursor = cursor, + items = lists.map { MorphoDataItem.ListInfo(it) }.toMutableList(), + ) + } + + fun fromModLabelDefs( + title: String, + uri: AtUri, + labels: List, + cursor: AtCursor = AtCursor.EMPTY, + ): MorphoData { + return MorphoData( + title = title, + uri = uri, + cursor = cursor, + items = labels.map { MorphoDataItem.ModLabel(it) }.toMutableList(), + ) + } + + fun fromModServiceDefs( + title: String, + uri: AtUri, + services: List, + cursor: AtCursor = AtCursor.EMPTY, + ): MorphoData { + return MorphoData( + title = title, + uri = uri, + cursor = cursor, + items = services.map { MorphoDataItem.LabelService(it) }.toMutableList(), + ) + } + } val isHome: Boolean @@ -141,6 +305,7 @@ data class MorphoData( uri.atUri.matches(AtUri.ProfileUserListsUriRegex) -> FeedType.PROFILE_USER_LISTS uri.atUri.matches(AtUri.ProfileModServiceUriRegex) -> FeedType.PROFILE_MOD_SERVICE uri.atUri.matches(AtUri.ProfileFeedsListUriRegex) -> FeedType.PROFILE_FEEDS_LIST + uri.atUri.matches(AtUri.ListFeedUriRegex) -> FeedType.LIST_FOLLOWING else -> FeedType.OTHER } @@ -158,6 +323,138 @@ data class MorphoData( } } } + + fun collectThreads( + depth: Int = 3, height: Int = 80, + timeRange: Delta = Delta(Duration.parse("4h")), + repliesBumpThreads: Boolean = !isProfileFeed, + api: Butterfly? = null, // allows to just use local data + ): Flow> = flow { + val threads = mutableListOf() + val replies = mutableListOf() + val posts = mutableListOf() + val threadCandidates = mutableListOf() + items.fastForEach { item -> + when(item) { + is MorphoDataItem.Post -> { + if (item.isReply) replies.add(item) + else if (item.isOrphan) posts.add(item) + else posts.add(item) + } + is MorphoDataItem.Thread -> { + if (!item.isIncompleteThread) threads.add(item) + else threadCandidates.add(item) + } + else -> return@fastForEach + } + } + replies.fastForEachIndexed { index, reply -> + if (reply == null) return@fastForEachIndexed + if (reply.isOrphan) { + val parent = reply.post.reply?.parentPost + ?: reply.post.reply?.replyRef?.parent?.uri?.let { + if (api != null) { + getPost(it, api).firstOrNull() + } else null + } + val root = reply.post.reply?.rootPost + ?: reply.post.reply?.replyRef?.root?.uri?.let { + if (api != null) { + getPost(it, api).firstOrNull() + } else null + } + replies[index] = MorphoDataItem.Post( + reply.post.copy(reply = reply.post.reply?.copy(parentPost = parent, rootPost = root)), + reply.reason, + isOrphan = root != null && parent != null, + ) + } + val newReply = replies[index] ?: return@fastForEachIndexed // Update in case we changed it above + val replyRef = newReply.post.reply?.replyRef ?: return@fastForEachIndexed + val parent = replyRef.parent.uri + val root = replyRef.root.uri + val inThread = threads.indexOfFirst { it?.containsUri(parent) ?: false || it?.containsUri(root) ?: false } + if (inThread != -1) { + val thread = threads.getOrNull(inThread) ?: return@fastForEachIndexed + threads[inThread] = thread.addReply(newReply.post) + replies[index] = null + } + val inCandidates = threadCandidates.indexOfFirst { it?.containsUri(parent) ?: false || it?.containsUri(root) ?: false } + if (inCandidates != -1) { + val thread = threadCandidates.getOrNull(inCandidates) ?: return@fastForEachIndexed + threadCandidates[inCandidates] = thread.addReply(newReply.post) + replies[index] = null + } + + } + threadCandidates.fastForEachIndexed { index, thread -> + if (thread == null) return@fastForEachIndexed + val inThreads = threads.indexOfFirst { t -> t?.containsUri(thread.thread.post.uri) ?: false } + if (inThreads == - 1) { + val threadToSplice = threads.getOrNull(index) ?: return@fastForEachIndexed + threads[index] = threadToSplice.addReply(ThreadPost.ViewablePost(thread.thread.post, thread.thread.replies)) + threadCandidates[index] = null + } + } + threadCandidates.fastFilterNotNull() + if (threadCandidates.isNotEmpty()) threads.addAll(threadCandidates) + val newFeed = posts.toList().fastFilterNotNull() + .distinctBy { it.getUri() } + .filterNot { post -> + if(post.isRepost) return@filterNot false + if(post.isQuotePost) return@filterNot false + post.getUris().any { uri -> threads.any { it?.containsUri(uri) ?: false } } + } + threads.toList().fastFilterNotNull() + replies.toList().fastFilterNotNull() + .distinctBy { it.getUri() } + .filterNot { reply -> + if(reply.isRepost) return@filterNot false + if(reply.isQuotePost) return@filterNot false + reply.getUris().any { uri -> threads.any { it?.containsUri(uri) ?: false } } + } + val sortedFeed = newFeed.sortedByDescending { + when(it) { + is MorphoDataItem.Post -> when(it.reason) { + is BskyPostReason.BskyPostFeedPost -> it.post.createdAt + is BskyPostReason.BskyPostRepost -> it.post.createdAt + is BskyPostReason.SourceFeed -> it.post.createdAt + null -> it.post.createdAt + } + is MorphoDataItem.Thread -> if(!repliesBumpThreads) { + it.rootAccessiblePost.createdAt + } else { + maxOf(it.thread.post.createdAt, + it.thread.replies.fold(it.thread.post.createdAt) { acc, post -> + val postTime = when(post) { + is ThreadPost.ViewablePost -> post.post.createdAt + is ThreadPost.BlockedPost -> Moment(Instant.DISTANT_PAST) + is ThreadPost.NotFoundPost -> Moment(Instant.DISTANT_PAST) + } + maxOf(acc, postTime) + }) + } + } + } + + @Suppress("UNCHECKED_CAST") val newData = + copy( items = sortedFeed.fastDistinctBy { it.getUri() } as List) + emit(newData.dedup()) + }.flowOn(Dispatchers.Default) + + fun dedup(): MorphoData { + val newList = items.fastDistinctBy { when(it) { + is MorphoDataItem.FeedItem -> it.key + is MorphoDataItem.Post -> it.key + is MorphoDataItem.Thread -> it.key + is MorphoDataItem.ListInfo -> it.list.uri + is MorphoDataItem.ModLabel -> it.label.identifier + is MorphoDataItem.ProfileItem -> it.profile.did + is MorphoDataItem.LabelService -> it.service.uri + else -> {it.hashCode()} + } } + return this.copy(items = newList) + } + + } fun MorphoDataFeed.toMorphoData( @@ -178,4 +475,281 @@ fun AtUri.id(api:Butterfly): AtIdentifier { // TODO: make this resolve a handle to a DID if (idString.contains("did:")) Did(idString) else Handle(idString) } +} + +fun areSameAuthor(authors: AuthorContext): Boolean { + val authorDid = authors.author.did + if(authors.parentAuthor != null && authors.parentAuthor.did != authorDid) { + return false + } + if(authors.grandParentAuthor != null && authors.grandParentAuthor.did != authorDid) { + return false + } + if(authors.rootAuthor != null && authors.rootAuthor.did != authorDid) { + return false + } + return true +} + + +@Serializable +data class FeedTuner(val tuners: List = persistentListOf()) { + val seenKeys = mutableSetOf() + val seenUris = mutableSetOf() + val seenRootUris = mutableSetOf() + + companion object { + + fun useFeedTuners( + prefs: BskyUserPreferences, + feed: MorphoData + ): List { + if(feed.isProfileFeed) { + when(feed.feedType) { + FeedType.PROFILE_POSTS -> return listOf(FeedTuner(tuners = persistentListOf(::removeReplies))) + FeedType.PROFILE_USER_LISTS -> return listOf() + FeedType.PROFILE_FEEDS_LIST -> return listOf() + FeedType.PROFILE_MOD_SERVICE -> return listOf() + else -> {} + } + } + val languages = prefs.preferences.languages.toList() + val languageTuner: TunerFunction = { f, t -> + preferredLanguageOnly(languages, f, t) + } + if(feed.feedType == FeedType.OTHER) { + return listOf(FeedTuner(tuners = persistentListOf(languageTuner))) + } + if(feed.feedType == FeedType.LIST_FOLLOWING || feed.feedType == FeedType.HOME) { + val userDid = Did(prefs.user.userDid) + val tuners = mutableListOf(FeedTuner(tuners = persistentListOf(::removeOrphans))) + val feedPrefs = prefs.preferences.feedViewPrefs[feed.uri.atUri] ?: return tuners.toList() + if(feedPrefs.hideReposts) tuners.add(FeedTuner(tuners = persistentListOf(::removeReposts))) + if(feedPrefs.hideReplies) tuners.add(FeedTuner(tuners = persistentListOf(::removeReplies))) + else { + val followedRepliesOnly: TunerFunction = { f, t -> + followedRepliesOnly(userDid, f, t) + } + tuners.add(FeedTuner(tuners = persistentListOf(followedRepliesOnly))) + } + if(feedPrefs.hideQuotePosts) tuners.add(FeedTuner(tuners = persistentListOf(::removeQuotePosts))) + tuners.add(FeedTuner(tuners = persistentListOf(::dedupThreads))) + return tuners.toList() + } + return listOf() + } + + fun removeReplies( + feed: List, + tuner: FeedTuner = FeedTuner(), + ): List { + return feed.filterNot { item -> + when(item) { + is MorphoDataItem.Post -> item.isReply && !item.isRepost && + !(item.getAuthors()?.let { areSameAuthor(it) } ?: false) + is MorphoDataItem.Thread -> !(item.getAuthors()?.let { areSameAuthor(it) } ?: false) + } + } + + } + + fun removeReposts( + feed: List, + tuner: FeedTuner = FeedTuner(), + ): List { + return feed.filterNot { item -> + item.isRepost + } + } + + fun removeQuotePosts( + feed: List, + tuner: FeedTuner = FeedTuner(), + ): List { + return feed.filterNot { item -> + item.isQuotePost + } + } + + fun removeOrphans( + feed: List, + tuner: FeedTuner = FeedTuner(), + ): List { + return feed.filterNot { item -> + when(item) { + is MorphoDataItem.Post -> item.isOrphan + is MorphoDataItem.Thread -> false + } + } + } + + fun dedupThreads( + feed: List, + tuner: FeedTuner = FeedTuner(), + ): List { + return feed.filterNot { item -> + val rootUri = item.rootUri + if(!item.isRepost == tuner.seenRootUris.contains(rootUri)) { + false + } else { + tuner.seenRootUris.add(rootUri) + true + } + } + } + + fun followedRepliesOnly( + userDid: Did, + feed: List, + tuner: FeedTuner = FeedTuner(), + ): List { + return feed.filterNot { item -> + item.isReply && !shouldDisplayReplyInFollowing(item, userDid) + } + } + + fun preferredLanguageOnly( + languages: List = persistentListOf(), + feed: List, + tuner: FeedTuner = FeedTuner(), + ): List { + if (languages.isEmpty()) return feed + val newFeed = feed.filter { item -> + when(item) { + is MorphoDataItem.Post -> { + item.post.langs.isEmpty() || + item.post.langs.any { languages.contains(it) } + } + is MorphoDataItem.Thread -> { + item.thread.post.langs.isEmpty() || + item.thread.post.langs.any { languages.contains(it) } + } + } + }.map { item -> + when(item) { + is MorphoDataItem.Post -> item + is MorphoDataItem.Thread -> { + item.copy( + thread = item.thread.filterReplies { reply -> + when(reply) { + is ThreadPost.ViewablePost -> reply.post.langs.isEmpty() || + reply.post.langs.any { languages.contains(it) } + is ThreadPost.BlockedPost -> true + is ThreadPost.NotFoundPost -> true + } + + } + ) + } + } + } + return newFeed.ifEmpty { feed } + } + } + fun tune( + feed: MorphoData + ): MorphoData { + var workingFeed = feed.items + tuners.forEach { tuner -> + workingFeed = tuner(workingFeed, this) + } + workingFeed = workingFeed.map { item -> + if(seenKeys.contains(item.key)) return@map null + else if(item is MorphoDataItem.Thread) { + val itemUris = item.getUris() + val seenInThisThread = itemUris.filter { seenUris.contains(it) } + if(seenInThisThread.isNotEmpty()) { + if(seenInThisThread.size == itemUris.size) { + return@map null + } else { + val newParents = item.thread.parents.filter { parent -> + when(parent) { + is ThreadPost.ViewablePost -> parent.post.uri in seenInThisThread + is ThreadPost.BlockedPost -> false + is ThreadPost.NotFoundPost -> false + } + } + val newThread = item.copy(thread = item.thread.filterReplies { reply -> + when(reply) { + is ThreadPost.ViewablePost -> reply.post.uri in seenInThisThread + is ThreadPost.BlockedPost -> false + is ThreadPost.NotFoundPost -> false + } + }.copy(parents = newParents)) + seenUris.addAll(itemUris) + if(newThread.thread.replies.isEmpty() && newThread.thread.parents.isEmpty()) { + return@map null + } else { + return@map newThread + } + } + } else { + seenUris.addAll(itemUris) + item + } + } else { + val disableDedub = item.isReply && item.isRepost + if(!disableDedub) seenKeys.add(item.key) + item + } + }.filterNotNull() + return feed.copy(items = workingFeed) + } + +} + +/// Algo copied from official app +/// https://github.com/bluesky-social/social-app/blob/main/src/lib/api/feed-manip.ts#L445 +/// as of commit https://github.com/bluesky-social/social-app/commit/e2a244b99889743a8788b0c464d3e150bc8047ad +/// The algorithm is a controversial, so we may want to change it or offer more options. +fun shouldDisplayReplyInFollowing( + item: MorphoDataItem.FeedItem, + userDid: Did, +): Boolean { + val authors = item.getAuthors() + val author = authors?.author + val rootAuthor = authors?.rootAuthor + val parentAuthor = authors?.parentAuthor + val grandParentAuthor = authors?.grandParentAuthor + if (!isSelfOrFollowing(author, userDid)) + return false // Only show replies from self or people you follow. + + if(parentAuthor == null || parentAuthor.did == author?.did + && rootAuthor == null || rootAuthor?.did == author?.did + && grandParentAuthor == null || grandParentAuthor?.did == author?.did + ) return true // Always show self-threads. + + if ( + parentAuthor.did != author?.did && + rootAuthor?.did == author?.did && + item is MorphoDataItem.Thread + ) { + // If you follow A, show A -> someone[>0 likes] -> A chains too. + // This is different from cases below because you only know one person. + val parentPost = when(val p = item.thread.parents.lastOrNull()) { + is ThreadPost.ViewablePost -> p.post + else -> null + } + if(parentPost != null && parentPost.likeCount > 0) + return true + } + // From this point on we need at least one more reason to show it. + if ( + parentAuthor.did != author?.did && isSelfOrFollowing(parentAuthor, userDid) + ) return true + if ( + grandParentAuthor != null && + grandParentAuthor.did != author?.did && + isSelfOrFollowing(grandParentAuthor, userDid) + ) return true + if ( + rootAuthor != null && + rootAuthor.did != author?.did && + isSelfOrFollowing(rootAuthor, userDid) + ) return true + return false +} + +fun isSelfOrFollowing(profile: Profile?, userDid: Did): Boolean { + return profile?.did == userDid || profile?.followedByMe == true } \ No newline at end of file diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uistate/SkylineState.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uistate/SkylineState.kt index 10ff828..615c192 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uistate/SkylineState.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uistate/SkylineState.kt @@ -33,5 +33,6 @@ enum class FeedType { PROFILE_USER_LISTS, PROFILE_MOD_SERVICE, PROFILE_FEEDS_LIST, + LIST_FOLLOWING, OTHER, } diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/main/MainScreenModel.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/main/MainScreenModel.kt index 1092de8..50409c9 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/main/MainScreenModel.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/main/MainScreenModel.kt @@ -215,16 +215,32 @@ open class MainScreenModel: BaseScreenModel() { } } - fun updateFeed(uri: AtUri, newCursor: AtCursor = null): Boolean { + fun updateFeed(uri: AtUri, newCursor: AtCursor = AtCursor.EMPTY): Boolean { val cursor = _cursors[uri] ?: return false + if(newCursor.cursor == null && newCursor.scroll > 0) { + val state = _feedStates.firstOrNull { it.value.uri == uri } + val index = _feedStates.indexOfFirst { it.value.uri == uri } + if(state != null) { + val newState = state.value.copy(feed = state.value.feed.copy(cursor = newCursor)) + _feedStates[index] = MutableStateFlow(newState).asStateFlow() + return true + } + val profileFeedState = _profileFeeds.firstOrNull { it.value.uri == uri } + val profileFeedIndex = _profileFeeds.indexOfFirst { it.value.uri == uri } + if(profileFeedState != null) { + val newState = profileFeedState.value.copy(feed = profileFeedState.value.feed.copy(cursor = newCursor)) + _profileFeeds[profileFeedIndex] = MutableStateFlow(newState).asStateFlow() + return true + } + } return cursor.tryEmit(newCursor) } - fun updateFeed(feed: FeedGenerator, newCursor: AtCursor = null): Boolean { + fun updateFeed(feed: FeedGenerator, newCursor: AtCursor = AtCursor.EMPTY): Boolean { return updateFeed(feed.uri, newCursor) } - fun updateFeed(entry: ContentCardMapEntry, newCursor: AtCursor = null): Boolean { + fun updateFeed(entry: ContentCardMapEntry, newCursor: AtCursor = AtCursor.EMPTY): Boolean { return updateFeed(entry.uri, newCursor) } @@ -305,13 +321,13 @@ open class MainScreenModel: BaseScreenModel() { // Delete the feed if it's already there, initializing from scratch if(force && feedService != null) dataService.removeFeed(feed.uri) _cursors[feed.uri] = feed.cursorFlow - if(start) feed.cursorFlow.emit(null) + if(start) feed.cursorFlow.emit(AtCursor.EMPTY) val feedState = _feedStates .firstOrNull { it.value.uri == feed.uri } val newFeed = dataService .feed(info, feed.cursorFlow, limit) - .handleToState(MorphoData(info.name, feed.uri, feed.cursorFlow.replayCache.lastOrNull())) + .handleToState(MorphoData(info.name, feed.uri, feed.cursorFlow.replayCache.lastOrNull() ?: AtCursor.EMPTY)) if (feedState == null) { _feedStates.add(newFeed) @@ -336,13 +352,13 @@ open class MainScreenModel: BaseScreenModel() { // Delete the feed if it's already there, initializing from scratch if(force && feedService != null) dataService.removeFeed(feed.uri) _cursors[feed.uri] = cursor - if(start) cursor.emit(null) + if(start) cursor.emit(AtCursor.EMPTY) val feedState = _feedStates .firstOrNull { it.value.uri == feed.uri } val newFeed = dataService .feed(info, cursor, limit) - .handleToState(MorphoData(feed.displayName, feed.uri, cursor.replayCache.lastOrNull())) + .handleToState(MorphoData(feed.displayName, feed.uri, cursor.replayCache.lastOrNull() ?: AtCursor.EMPTY)) if (feedState == null) { _feedStates.add(newFeed) @@ -375,13 +391,13 @@ open class MainScreenModel: BaseScreenModel() { // Delete the feed if it's already there, initializing from scratch if(force && feedService != null) dataService.removeFeed(timeline.uri) _cursors[timeline.uri] = timeline.cursorFlow - timeline.cursorFlow.emit(null) + timeline.cursorFlow.emit(AtCursor.EMPTY) val feedState = _feedStates .firstOrNull { it.value.uri == timeline.uri } log.d { "Timeline state: $feedState"} val newFeed = dataService .timeline(timeline.cursorFlow, 100, prefs) - .handleToState(MorphoData(cursor = timeline.cursorFlow.replayCache.lastOrNull())) + .handleToState(MorphoData(cursor = timeline.cursorFlow.replayCache.lastOrNull() ?: AtCursor.EMPTY)) if (feedState == null) { _feedStates.add(newFeed) @@ -406,12 +422,12 @@ open class MainScreenModel: BaseScreenModel() { // Delete the feed if it's already there, initializing from scratch if(force && feedService != null) dataService.removeFeed(uri) _cursors[uri] = cursor - cursor.emit(null) + cursor.emit(AtCursor.EMPTY) val feedState = _feedStates .firstOrNull { it.value.uri == uri } val newFeed = dataService .timeline(cursor, 100, prefs) - .handleToState(MorphoData(cursor = cursor.replayCache.lastOrNull())) + .handleToState(MorphoData(cursor = cursor.replayCache.lastOrNull() ?: AtCursor.EMPTY)) if (feedState == null) { _feedStates.add(newFeed) @@ -447,7 +463,7 @@ open class MainScreenModel: BaseScreenModel() { if (profile == null) { emit(null); return@flow } val newFeed = dataService .profileTabContent(id, feed.feedType, feed.cursorFlow, limit) - .handleToState(profile, MorphoData(feed.title, feed.uri, feed.cursorFlow.replayCache.lastOrNull())) + .handleToState(profile, MorphoData(feed.title, feed.uri, feed.cursorFlow.replayCache.lastOrNull() ?: AtCursor.EMPTY)) if (feedState == null) { _profileFeeds.add(newFeed) emit(_profileFeeds.last().value) @@ -456,7 +472,7 @@ open class MainScreenModel: BaseScreenModel() { _profileFeeds[i] = newFeed emit(_profileFeeds[i].value) } - feed.cursorFlow.emit(null) + feed.cursorFlow.emit(AtCursor.EMPTY) } suspend fun initProfileContent( @@ -478,37 +494,43 @@ open class MainScreenModel: BaseScreenModel() { _cursors[AtUri.profilePostsUri(p.did)] = postsCursor val posts = dataService .authorFeed(p.did, FeedType.PROFILE_POSTS, postsCursor.asSharedFlow(), 50) - .handleToState(p, MorphoData("Posts", AtUri.profilePostsUri(p.did), postsCursor.replayCache.lastOrNull())) + .handleToState(p, MorphoData("Posts", AtUri.profilePostsUri(p.did), + postsCursor.replayCache.lastOrNull() ?: AtCursor.EMPTY)) val repliesCursor: MutableSharedFlow = initAtCursor() _cursors[AtUri.profileRepliesUri(p.did)] = repliesCursor val replies = dataService .authorFeed(p.did, FeedType.PROFILE_REPLIES, repliesCursor.asSharedFlow(), 50) - .handleToState(p, MorphoData("Replies", AtUri.profileRepliesUri(p.did), repliesCursor.replayCache.lastOrNull())) + .handleToState(p, MorphoData("Replies", AtUri.profileRepliesUri(p.did), + repliesCursor.replayCache.lastOrNull() ?: AtCursor.EMPTY)) val mediaCursor: MutableSharedFlow = initAtCursor() _cursors[AtUri.profileMediaUri(p.did)] = mediaCursor val media = dataService .authorFeed(p.did, FeedType.PROFILE_MEDIA, mediaCursor.asSharedFlow(), 50) - .handleToState(p, MorphoData("Media", AtUri.profileMediaUri(p.did), mediaCursor.replayCache.lastOrNull())) + .handleToState(p, MorphoData("Media", AtUri.profileMediaUri(p.did), + mediaCursor.replayCache.lastOrNull() ?: AtCursor.EMPTY)) val likesCursor: MutableSharedFlow = initAtCursor() _cursors[AtUri.profileLikesUri(p.did)] = likesCursor val likes = dataService .profileLikes(p.did, likesCursor.asSharedFlow(), 50) - .handleToState(p, MorphoData("Likes", AtUri.profileLikesUri(p.did), likesCursor.replayCache.lastOrNull())) + .handleToState(p, MorphoData("Likes", AtUri.profileLikesUri(p.did), + likesCursor.replayCache.lastOrNull() ?: AtCursor.EMPTY)) val listsCursor: MutableSharedFlow = initAtCursor() _cursors[AtUri.profileUserListsUri(p.did)] = listsCursor val lists = dataService .profileLists(p.did, listsCursor.asSharedFlow(), 50) - .handleToState(p, MorphoData("Lists", AtUri.profileUserListsUri(p.did), listsCursor.replayCache.lastOrNull())) + .handleToState(p, MorphoData("Lists", AtUri.profileUserListsUri(p.did), + listsCursor.replayCache.lastOrNull() ?: AtCursor.EMPTY)) val feedsCursor: MutableSharedFlow = initAtCursor() _cursors[AtUri.profileFeedsListUri(p.did)] = feedsCursor val feeds = dataService .profileFeedsList(p.did, feedsCursor.asSharedFlow(), 50) - .handleToState(p, MorphoData("Feeds", AtUri.profileFeedsListUri(p.did), feedsCursor.replayCache.lastOrNull())) + .handleToState(p, MorphoData("Feeds", AtUri.profileFeedsListUri(p.did), + feedsCursor.replayCache.lastOrNull() ?: AtCursor.EMPTY)) if (p is BskyLabelService) { val servicesCursor: MutableSharedFlow = initAtCursor() @@ -516,8 +538,9 @@ open class MainScreenModel: BaseScreenModel() { val services = dataService .profileServiceView(p.did, servicesCursor.map { Unit } .shareIn(viewModelScope, SharingStarted.Lazily) - ).handleToState(p, MorphoData("Labels", AtUri.profileModServiceUri(p.did), servicesCursor.replayCache.lastOrNull())) - servicesCursor.emit(null) + ).handleToState(p, MorphoData("Labels", AtUri.profileModServiceUri(p.did), + servicesCursor.replayCache.lastOrNull() ?: AtCursor.EMPTY)) + servicesCursor.emit(AtCursor.EMPTY) ContentCardState.FullProfile( p, posts.stateIn(viewModelScope), @@ -530,7 +553,7 @@ open class MainScreenModel: BaseScreenModel() { ContentLoadingState.Idle ) } else { - postsCursor.emit(null) + postsCursor.emit(AtCursor.EMPTY) ContentCardState.FullProfile( p, posts.stateIn(viewModelScope), diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/main/tabbed/TabbedHomeView.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/main/tabbed/TabbedHomeView.kt index bdb8750..e9f5805 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/main/tabbed/TabbedHomeView.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/main/tabbed/TabbedHomeView.kt @@ -11,6 +11,8 @@ import androidx.compose.animation.slideInHorizontally import androidx.compose.animation.slideOutHorizontally import androidx.compose.animation.togetherWith import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material3.* import androidx.compose.material3.TabRowDefaults.tabIndicatorOffset import androidx.compose.runtime.* @@ -56,6 +58,9 @@ public fun CurrentSkylineScreen( sm: TabbedMainScreenModel, paddingValues: PaddingValues, state: StateFlow>?, + listState: LazyListState = rememberLazyListState( + initialFirstVisibleItemIndex = state?.value?.feed?.cursor?.scroll ?: 0 + ), modifier: Modifier ) { val navigator = LocalNavigator.currentOrThrow @@ -66,6 +71,7 @@ public fun CurrentSkylineScreen( sm = sm, paddingValues = paddingValues, state = state, + listState = listState, modifier = modifier ) } @@ -79,12 +85,14 @@ abstract class SkylineTab: NavTab { sm: TabbedMainScreenModel, paddingValues: PaddingValues, state: StateFlow>?, + listState: LazyListState, modifier: Modifier ) @OptIn(ExperimentalVoyagerApi::class) @Composable - final override fun Content() = Content(TabbedMainScreenModel(),PaddingValues(0.dp),null,Modifier) + final override fun Content() = + Content(TabbedMainScreenModel(),PaddingValues(0.dp),null, rememberLazyListState(), Modifier) } @@ -101,7 +109,6 @@ fun TabScreen.TabbedHomeView( var selectedTabIndex by rememberSaveable { mutableIntStateOf(0) } - var insets = WindowInsets.navigationBars.asPaddingValues() LifecycleEffectOnce { sm.initTabs() @@ -124,9 +131,13 @@ fun TabScreen.TabbedHomeView( Navigator( tabs.first(), disposeBehavior = NavigatorDisposeBehavior( - disposeNestedNavigators = false, + //disposeNestedNavigators = false, ) ) { nav -> + val listState = rememberLazyListState( + initialFirstVisibleItemIndex = + sm.uiState.tabStates[selectedTabIndex].value.feed.cursor.scroll + ) TabbedScreenScaffold( navBar = { navBar(navigator) }, topContent = { @@ -142,12 +153,17 @@ fun TabScreen.TabbedHomeView( } else nav.replace(tabs[index]) } else if(index > selectedTabIndex) nav.push(tabs[index]) selectedTabIndex = index + sm.refreshTab( + index, + sm.uiState.tabStates[index].value.feed.cursor + .copy(scroll = listState.firstVisibleItemIndex) + ) } ) }, content = { insets, state -> - SkylineTabTransition(nav, sm, insets, state) + SkylineTabTransition(nav, sm, insets, state, listState) }, modifier = Modifier, state = sm.uiState.tabStates.getOrNull(selectedTabIndex) as StateFlow>? @@ -165,13 +181,16 @@ fun SkylineTabTransition( sm: TabbedMainScreenModel, insets: PaddingValues = PaddingValues(0.dp), state: StateFlow>?, + listState: LazyListState = rememberLazyListState( + initialFirstVisibleItemIndex = state?.value?.feed?.cursor?.scroll ?: 0 + ), modifier: Modifier = Modifier, animationSpec: FiniteAnimationSpec = spring( stiffness = Spring.StiffnessMediumLow, visibilityThreshold = IntOffset.VisibilityThreshold ), content: ScreenTransitionContent = { - CurrentSkylineScreen(sm, insets, state, Modifier) + CurrentSkylineScreen(sm, insets, state, listState, modifier) } ) { ScreenTransition( @@ -260,6 +279,7 @@ data class HomeSkylineTab @OptIn(ExperimentalVoyagerApi::class) constructor( sm: TabbedMainScreenModel, paddingValues: PaddingValues, state: StateFlow>?, + listState: LazyListState, modifier: Modifier ) { @@ -268,6 +288,7 @@ data class HomeSkylineTab @OptIn(ExperimentalVoyagerApi::class) constructor( refresh = { cursor -> sm.refreshTab(index.toInt(), cursor) }, + listState = listState, ) } diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/main/tabbed/TabbedMainScreenModel.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/main/tabbed/TabbedMainScreenModel.kt index a4ffbb7..fb783a3 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/main/tabbed/TabbedMainScreenModel.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/main/tabbed/TabbedMainScreenModel.kt @@ -151,7 +151,7 @@ class TabbedMainScreenModel : MainScreenModel() { uiState = uiState.copy(loadingState = UiLoadingState.Idle, tabs = tabFlow, tabStates = newFeeds.toImmutableList()) } - fun refreshTab(index: Int, cursor: AtCursor = null) :Boolean { + fun refreshTab(index: Int, cursor: AtCursor = AtCursor.EMPTY) :Boolean { return if(index < 0 || index > tabs.lastIndex) false else updateFeed(tabs[index], cursor) } diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/notifications/NotificationsView.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/notifications/NotificationsView.kt index a675b29..4c022c0 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/notifications/NotificationsView.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/notifications/NotificationsView.kt @@ -26,6 +26,7 @@ import cafe.adriel.voyager.navigator.currentOrThrow import com.morpho.app.model.bluesky.BskyPost import com.morpho.app.model.bluesky.DraftPost import com.morpho.app.model.bluesky.NotificationsListItem +import com.morpho.app.model.uidata.AtCursor import com.morpho.app.model.uidata.getPost import com.morpho.app.screens.base.tabbed.ProfileTab import com.morpho.app.screens.base.tabbed.TabScreen @@ -86,7 +87,7 @@ fun TabScreen.NotificationViewContent( refreshing, { sm.notifService.updateNotificationsSeen() - sm.refreshNotifications(null) + sm.refreshNotifications(AtCursor.EMPTY) } ) val notifications by sm.uiState.value.notifications.collectAsState(persistentListOf()) diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/notifications/TabbedNotificationScreenModel.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/notifications/TabbedNotificationScreenModel.kt index 8811f2c..e71f19a 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/notifications/TabbedNotificationScreenModel.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/notifications/TabbedNotificationScreenModel.kt @@ -31,7 +31,7 @@ class TabbedNotificationScreenModel: BaseScreenModel() { init { viewModelScope.launch { val f = notifService.notifications(cursorFlow).map { it.getOrNull() } - cursorFlow.emit(null) + cursorFlow.emit(AtCursor.EMPTY) f.collect { if(it != null) { _uiState.update { diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/profile/TabbedProfileView.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/profile/TabbedProfileView.kt index 129fc84..b4c4c2b 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/profile/TabbedProfileView.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/profile/TabbedProfileView.kt @@ -5,6 +5,8 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.runtime.saveable.rememberSaveable @@ -172,6 +174,10 @@ fun TabScreen.TabbedProfileContent( disposeNestedNavigators = false, tabDisposable = { TabDisposable(navigator = it, tabs = tabs) } ) { + val listState = rememberLazyListState( + initialFirstVisibleItemIndex = + sm.profileUiState.tabStates[selectedTabIndex].value.feed.cursor.scroll + ) TabbedProfileScreenScaffold( navBar = { navBar(navigator) }, @@ -181,12 +187,20 @@ fun TabScreen.TabbedProfileContent( sm.profileState?.profile, ownProfile, scrollBehavior, tabs.toImmutableList(), onBackClicked = { navigator.pop() }, - onTabChanged = { selectedTabIndex = it }, + onTabChanged = { index -> + selectedTabIndex = index + sm.refreshTab( + index, + sm.profileUiState.tabStates[index].value.feed.cursor + .copy(scroll = listState.firstVisibleItemIndex) + ) + }, tabIndex = selectedTabIndex, ) }, content = { insets, state -> - CurrentProfileScreen(sm, insets, state, Modifier) + + CurrentProfileScreen(sm, insets, state, listState, Modifier) }, state = sm.profileUiState.tabStates.getOrNull(selectedTabIndex), scrollBehavior = scrollBehavior, @@ -226,6 +240,9 @@ public fun CurrentProfileScreen( sm: TabbedProfileViewModel, paddingValues: PaddingValues, state: StateFlow>?, + listState: LazyListState = rememberLazyListState( + initialFirstVisibleItemIndex = state?.value?.feed?.cursor?.scroll ?: 0 + ), modifier: Modifier ) { val navigator = LocalNavigator.currentOrThrow @@ -236,6 +253,7 @@ public fun CurrentProfileScreen( sm = sm, paddingValues = paddingValues, state = state, + listState = listState, modifier = modifier ) } @@ -249,12 +267,13 @@ abstract class ProfileTabScreen: NavTab { sm: TabbedProfileViewModel, paddingValues: PaddingValues, state: StateFlow>?, + listState: LazyListState, modifier: Modifier ) @OptIn(ExperimentalVoyagerApi::class) @Composable - final override fun Content() = Content(TabbedProfileViewModel(),PaddingValues(0.dp),null,Modifier) + final override fun Content() = Content(TabbedProfileViewModel(),PaddingValues(0.dp),null, rememberLazyListState(), Modifier) } @Serializable @@ -270,11 +289,12 @@ data class ProfileSkylineTab( sm: TabbedProfileViewModel, paddingValues: PaddingValues, state: StateFlow>?, + listState: LazyListState, modifier: Modifier ) { TabbedSkylineFragment(sm, state, paddingValues, refresh = { cursor -> sm.refreshTab(index.toInt(), cursor) - }, isProfileFeed = true) + }, isProfileFeed = true, listState = listState) } override val key: ScreenKey diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/profile/TabbedProfileViewModel.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/profile/TabbedProfileViewModel.kt index b28039c..75d4c81 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/profile/TabbedProfileViewModel.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/profile/TabbedProfileViewModel.kt @@ -92,7 +92,7 @@ class TabbedProfileViewModel( profileState!!.postsState.value!!.uri, profileState!!.postsState.value!!.feed.title, cursors[profileState!!.postsState.value!!.uri] - ?: MutableStateFlow(null) + ?: MutableStateFlow(AtCursor.EMPTY) ) ) tabStates.add(profileState!!.postsState as StateFlow>) @@ -103,7 +103,7 @@ class TabbedProfileViewModel( profileState!!.postRepliesState.value!!.uri, profileState!!.postRepliesState.value!!.feed.title, cursors[profileState!!.postRepliesState.value!!.uri] - ?: MutableStateFlow(null) + ?: MutableStateFlow(AtCursor.EMPTY) ) ) tabStates.add(profileState!!.postRepliesState as StateFlow>) @@ -114,7 +114,7 @@ class TabbedProfileViewModel( profileState!!.mediaState.value!!.uri, profileState!!.mediaState.value!!.feed.title, cursors[profileState!!.mediaState.value!!.uri] - ?: MutableStateFlow(null) + ?: MutableStateFlow(AtCursor.EMPTY) ) ) tabStates.add(profileState!!.mediaState as StateFlow>) @@ -125,7 +125,7 @@ class TabbedProfileViewModel( profileState!!.likesState.value!!.uri, profileState!!.likesState.value!!.feed.title, cursors[profileState!!.likesState.value!!.uri] - ?: MutableStateFlow(null) + ?: MutableStateFlow(AtCursor.EMPTY) ) ) tabStates.add(profileState!!.likesState as StateFlow>) @@ -136,7 +136,7 @@ class TabbedProfileViewModel( profileState!!.feedsState.value!!.uri, profileState!!.feedsState.value!!.feed.title, cursors[profileState!!.feedsState.value!!.uri] - ?: MutableStateFlow(null) + ?: MutableStateFlow(AtCursor.EMPTY) ) ) tabStates.add(profileState!!.feedsState as StateFlow>) @@ -147,7 +147,7 @@ class TabbedProfileViewModel( profileState!!.listsState.value!!.uri, profileState!!.listsState.value!!.feed.title, cursors[profileState!!.listsState.value!!.uri] - ?: MutableStateFlow(null) + ?: MutableStateFlow(AtCursor.EMPTY) ) ) tabStates.add(profileState!!.listsState as StateFlow>) @@ -160,7 +160,7 @@ class TabbedProfileViewModel( profileState!!.modServiceState.value!!.uri, profileState!!.modServiceState.value!!.feed.title, cursors[profileState!!.modServiceState.value!!.uri] - ?: MutableStateFlow(null) + ?: MutableStateFlow(AtCursor.EMPTY) ) ) tabStates.add(profileState!!.modServiceState as StateFlow>) @@ -171,7 +171,7 @@ class TabbedProfileViewModel( profileState!!.listsState.value!!.uri, profileState!!.listsState.value!!.feed.title, cursors[profileState!!.listsState.value!!.uri] - ?: MutableStateFlow(null) + ?: MutableStateFlow(AtCursor.EMPTY) ) ) tabStates.add(profileState!!.listsState as StateFlow>) @@ -182,7 +182,7 @@ class TabbedProfileViewModel( profileState!!.postsState.value!!.uri, profileState!!.postsState.value!!.feed.title, cursors[profileState!!.postsState.value!!.uri] - ?: MutableStateFlow(null) + ?: MutableStateFlow(AtCursor.EMPTY) ) ) tabStates.add(profileState!!.postsState as StateFlow>) @@ -193,7 +193,7 @@ class TabbedProfileViewModel( profileState!!.postRepliesState.value!!.uri, profileState!!.postRepliesState.value!!.feed.title, cursors[profileState!!.postRepliesState.value!!.uri] - ?: MutableStateFlow(null) + ?: MutableStateFlow(AtCursor.EMPTY) ) ) tabStates.add(profileState!!.postRepliesState as StateFlow>) @@ -204,7 +204,7 @@ class TabbedProfileViewModel( profileState!!.feedsState.value!!.uri, profileState!!.feedsState.value!!.feed.title, cursors[profileState!!.feedsState.value!!.uri] - ?: MutableStateFlow(null) + ?: MutableStateFlow(AtCursor.EMPTY) ) ) tabStates.add(profileState!!.feedsState as StateFlow>) @@ -232,7 +232,7 @@ class TabbedProfileViewModel( } - fun refreshTab(index: Int, cursor: AtCursor = null) :Boolean { + fun refreshTab(index: Int, cursor: AtCursor = AtCursor.EMPTY) :Boolean { return if(index < 0 || index > tabs.lastIndex) false else updateFeed(tabs[index], cursor) } diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/common/SkylineFragment.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/common/SkylineFragment.kt index 6f868b7..1056b27 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/common/SkylineFragment.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/common/SkylineFragment.kt @@ -1,6 +1,7 @@ package com.morpho.app.ui.common import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.border import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.items @@ -13,7 +14,9 @@ import androidx.compose.material.pullrefresh.pullRefresh import androidx.compose.material.pullrefresh.rememberPullRefreshState import androidx.compose.material3.* import androidx.compose.runtime.* +import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp import androidx.constraintlayout.compose.ConstraintLayout import com.atproto.repo.StrongRef @@ -22,6 +25,7 @@ import com.morpho.app.model.bluesky.MorphoDataItem import com.morpho.app.model.uidata.AtCursor import com.morpho.app.model.uidata.ContentHandling import com.morpho.app.model.uistate.ContentCardState +import com.morpho.app.model.uistate.ContentLoadingState import com.morpho.app.ui.elements.MenuOptions import com.morpho.app.ui.elements.WrappedLazyColumn import com.morpho.app.ui.post.PostFragment @@ -42,7 +46,7 @@ fun SkylineFragment ( onItemClicked: OnPostClicked, onProfileClicked: (AtIdentifier) -> Unit = {}, onPostButtonClicked: () -> Unit = {}, - refresh: (AtCursor) -> Unit = {}, + refresh: (AtCursor) -> Unit = { }, onReplyClicked: (BskyPost) -> Unit = { }, onRepostClicked: (BskyPost) -> Unit = { }, onLikeClicked: (StrongRef) -> Unit = { }, @@ -51,6 +55,10 @@ fun SkylineFragment ( getContentHandling: (BskyPost) -> List = { listOf() }, contentPadding: PaddingValues = PaddingValues(0.dp), isProfileFeed: Boolean = false, + listState: LazyListState = rememberLazyListState( + initialFirstVisibleItemIndex = content.value.feed.cursor.scroll + ), + debuggable: Boolean = true, ) { val currentRefresh by rememberUpdatedState(refresh) @@ -59,12 +67,11 @@ fun SkylineFragment ( val loading = state.value.loadingState val cursor by rememberUpdatedState(state.value.feed.cursor) + val scope = rememberCoroutineScope() var refreshing by remember { mutableStateOf(false) } - val listState: LazyListState = rememberLazyListState() - - val data = remember(loading, state, cursor, refreshing) { + val data = rememberSaveable(loading, state, cursor, refreshing) { state.value.feed } val scrolledDownSome by remember { @@ -73,28 +80,35 @@ fun SkylineFragment ( } } + val scrollCursor by rememberSaveable { derivedStateOf { + listState.firstVisibleItemIndex + } } + val scrolledDownLots by remember { derivedStateOf { listState.firstVisibleItemIndex > 20 } } + fun refreshPull() = scope.launch { refreshing = true - launch { currentRefresh(null) } + launch { currentRefresh(AtCursor.EMPTY) } .invokeOnCompletion { refreshing = false } } -// LaunchedEffect( -// data.items.isNotEmpty() && -// loading == ContentLoadingState.Idle && -// !listState.canScrollForward && -// !refreshing && -// scrolledDownSome -// ) { -// currentRefresh(cursor) -// } + + + LaunchedEffect( + data.items.isNotEmpty() && + loading == ContentLoadingState.Idle && + !listState.canScrollForward && + !refreshing && + scrolledDownSome + ) { + currentRefresh(cursor.copy(scroll = scrollCursor)) + } val refreshState = rememberPullRefreshState(refreshing, ::refreshPull) @@ -207,7 +221,7 @@ fun SkylineFragment ( is MorphoDataItem.Thread -> { SkylineThreadFragment( thread = item.thread, - modifier = Modifier + modifier = if(debuggable) Modifier.border(1.dp, Color.White) else Modifier .fillMaxWidth() .padding(vertical = 2.dp, horizontal = 4.dp), onItemClicked = onItemClicked, @@ -218,11 +232,12 @@ fun SkylineFragment ( onMenuClicked = onMenuClicked, onLikeClicked = onLikeClicked, getContentHandling = getContentHandling, + debuggable = debuggable, ) } is MorphoDataItem.Post -> { PostFragment( - modifier = Modifier + modifier = if(debuggable) Modifier.border(1.dp, Color.Blue) else Modifier .fillMaxWidth() .padding(vertical = 2.dp, horizontal = 4.dp), post = item.post, @@ -244,7 +259,7 @@ fun SkylineFragment ( item { Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.Center) { TextButton( - onClick = { currentRefresh(cursor) }, + onClick = { currentRefresh(cursor.copy(scroll = data.items.size - 1)) }, modifier = Modifier.padding(6.dp) ) { Text("Load more...") diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/common/SkylineThreadFragment.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/common/SkylineThreadFragment.kt index c5ef40e..77c7036 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/common/SkylineThreadFragment.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/common/SkylineThreadFragment.kt @@ -1,5 +1,6 @@ package com.morpho.app.ui.common +import androidx.compose.foundation.border import androidx.compose.foundation.layout.* import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.NavigateNext @@ -8,6 +9,7 @@ import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp import androidx.compose.ui.util.fastForEach import androidx.compose.ui.util.fastForEachIndexed @@ -36,7 +38,8 @@ inline fun SkylineThreadFragment( crossinline onLikeClicked: (StrongRef) -> Unit = { }, noinline onMenuClicked: (MenuOptions, BskyPost) -> Unit = { _, _ -> }, crossinline onUnClicked: (type: RecordType, uri: AtUri) -> Unit = { _, _ -> }, - crossinline getContentHandling: (BskyPost) -> List = { listOf() } + crossinline getContentHandling: (BskyPost) -> List = { listOf() }, + debuggable: Boolean = false, ) { val threadPost = remember { ThreadPost.ViewablePost(thread.post, thread.replies) } val hasReplies = rememberSaveable { threadPost.replies.isNotEmpty() } @@ -46,7 +49,7 @@ inline fun SkylineThreadFragment( Surface( tonalElevation = if (hasReplies) 1.dp else 0.dp, shape = MaterialTheme.shapes.extraSmall, - modifier = if (hasReplies) Modifier.padding(2.dp) else Modifier.fillMaxWidth() + modifier = if (hasReplies) modifier.padding(2.dp) else modifier.fillMaxWidth() ) { Column( ) { @@ -64,7 +67,7 @@ inline fun SkylineThreadFragment( post = root.post, role = PostFragmentRole.ThreadBranchStart, elevate = true, - modifier = Modifier, + modifier = if(debuggable) Modifier.border(1.dp, Color.Cyan) else Modifier, onItemClicked = {onItemClicked(it) }, onProfileClicked = { onProfileClicked(it) }, onUnClicked = { type,uri-> onUnClicked(type,uri) }, @@ -89,6 +92,7 @@ inline fun SkylineThreadFragment( if(thread.parents.size > 3) { ThreadItem( item = thread.parents[0], + modifier = if(debuggable) Modifier.border(1.dp, Color.Green) else Modifier, role = PostFragmentRole.ThreadBranchStart, indentLevel = 1, elevate = true, @@ -153,6 +157,7 @@ inline fun SkylineThreadFragment( ThreadItem( item = post, role = role, + modifier = if(debuggable) Modifier.border(1.dp, Color.White) else Modifier, indentLevel = 1, reason = reason, elevate = true, @@ -172,6 +177,7 @@ inline fun SkylineThreadFragment( item = thread.parents[thread.parents.lastIndex], role = PostFragmentRole.ThreadBranchEnd, indentLevel = 1, + modifier = if(debuggable) Modifier.border(1.dp, Color.Yellow) else Modifier, elevate = true, onItemClicked = onItemClicked, onProfileClicked = onProfileClicked, @@ -200,10 +206,11 @@ inline fun SkylineThreadFragment( else -> PostFragmentRole.ThreadBranchMiddle } } - if (post is ThreadPost.ViewablePost) { + if (post is ThreadPost.ViewablePost && post.uri != threadPost.uri) { ThreadItem( item = post, role = role, + modifier = if(debuggable) Modifier.border(1.dp, Color.Red) else Modifier, indentLevel = 1, reason = reason, elevate = true, @@ -230,9 +237,9 @@ inline fun SkylineThreadFragment( ThreadItem( item = threadPost, role = role, - reason = thread.post.reason, + reason = null, elevate = true, - modifier = Modifier + modifier = if(debuggable) Modifier.border(1.dp, Color.Magenta) else Modifier .padding(4.dp), onItemClicked = onItemClicked, onProfileClicked = onProfileClicked, @@ -263,7 +270,7 @@ inline fun SkylineThreadFragment( role = role, reason = thread.post.reason, elevate = true, - modifier = Modifier + modifier = if(debuggable) Modifier.border(1.dp, Color.Blue) else Modifier .padding(4.dp), onItemClicked = onItemClicked, onProfileClicked = onProfileClicked, @@ -278,6 +285,7 @@ inline fun SkylineThreadFragment( if (hasReplies) { Surface( + modifier = if(debuggable) Modifier.border(1.dp, Color.Black) else Modifier, tonalElevation = 1.dp, //border = BorderStroke(1.dp, MaterialTheme.colorScheme.secondary), shape = MaterialTheme.shapes.extraSmall diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/common/TabbedSkylineFragment.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/common/TabbedSkylineFragment.kt index 03526b9..91d1e99 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/common/TabbedSkylineFragment.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/common/TabbedSkylineFragment.kt @@ -1,6 +1,8 @@ package com.morpho.app.ui.common import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.* @@ -35,8 +37,11 @@ fun > TabbedSkylin sm: T, state: StateFlow?, paddingValues: PaddingValues = PaddingValues(0.dp), - refresh: (AtCursor) -> Unit = { }, - isProfileFeed: Boolean = false + refresh: (AtCursor) -> Unit = {}, + isProfileFeed: Boolean = false, + listState: LazyListState = rememberLazyListState( + initialFirstVisibleItemIndex = state?.value?.feed?.cursor?.scroll ?: 0 + ), ) { val navigator = if (LocalNavigator.current?.parent?.instanceOf(TabNavigator::class) == true) { LocalNavigator.currentOrThrow @@ -93,6 +98,7 @@ fun > TabbedSkylin getContentHandling = { post -> sm.labelService.getContentHandlingForPost(post)}, contentPadding = paddingValues, isProfileFeed = isProfileFeed, + listState = listState, ) if(repostClicked) { RepostQueryDialog( diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/elements/ContentHider.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/elements/ContentHider.kt index 3994433..063dd85 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/elements/ContentHider.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/elements/ContentHider.kt @@ -1,9 +1,6 @@ package com.morpho.app.ui.elements -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.* import androidx.compose.foundation.text.selection.DisableSelection import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Info @@ -36,43 +33,45 @@ public fun ContentHider( ) } val reason = toHide.firstOrNull() - if (toHide.isNotEmpty()) { - TextButton( - onClick = { hideContent = !hideContent }, - modifier = Modifier.fillMaxWidth(), - shape = ButtonDefaults.textShape, - colors = ButtonDefaults.elevatedButtonColors(), - elevation = ButtonDefaults.filledTonalButtonElevation() - ) { - Icon( - imageVector = reason?.icon ?: Icons.Default.Info, - contentDescription = reason?.source?.description - ) - DisableSelection { - Text( - text = reason?.source?.name ?: "", - modifier = Modifier.padding(horizontal = 4.dp) + Column { + if (toHide.isNotEmpty()) { + TextButton( + onClick = { hideContent = !hideContent }, + modifier = Modifier.fillMaxWidth(), + shape = ButtonDefaults.textShape, + colors = ButtonDefaults.elevatedButtonColors(), + elevation = ButtonDefaults.filledTonalButtonElevation() + ) { + Icon( + imageVector = reason?.icon ?: Icons.Default.Info, + contentDescription = reason?.source?.description ) - } + DisableSelection { + Text( + text = reason?.source?.name ?: "", + modifier = Modifier.padding(horizontal = 4.dp) + ) + } - Spacer( - modifier = Modifier - .width(1.dp) - .weight(0.3f) - ) - DisableSelection { - Text( - text = if (hideContent) { - "Show" - } else { - "Hide" - } + Spacer( + modifier = Modifier + .width(1.dp) + .weight(0.3f) ) - } + DisableSelection { + Text( + text = if (hideContent) { + "Show" + } else { + "Hide" + } + ) + } + } + } + if (!hideContent) { + content() } - } - if (!hideContent) { - content() } } \ No newline at end of file diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/post/PostFragment.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/post/PostFragment.kt index 02e201d..750c81f 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/post/PostFragment.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/post/PostFragment.kt @@ -74,7 +74,7 @@ fun PostFragment( PostFragmentRole.ThreadEnd -> Modifier.padding(start = 2.dp, top = 0.dp, end = 2.dp, bottom = 0.dp) }} val uriHandler = LocalUriHandler.current - WrappedColumn(modifier = padding.fillMaxWidth()) { + WrappedColumn(modifier = modifier.then(padding.fillMaxWidth())) { val delta = remember { getFormattedDateTimeSince(post.createdAt) } val indent = remember { when(role) { PostFragmentRole.Solo -> indentLevel.toFloat() @@ -155,7 +155,6 @@ fun PostFragment( reasons = contentHandling, scope = LabelScope.Content, ) { - Row( modifier = Modifier.padding(bottom = 2.dp).padding(start = 0.dp, end = 6.dp) .fillMaxWidth(indentLevel(indent)) @@ -269,8 +268,8 @@ fun PostFragment( ) } - if (post.reply?.parent != null) { - ReplyIndicator(post.reply.parent) + if (post.reply?.parentPost != null) { + ReplyIndicator(post.reply.parentPost) } if (post.facets.fastAny { @@ -329,11 +328,8 @@ fun PostFragment( } } - - } } - } @OptIn(ExperimentalResourceApi::class) diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/post/PostImage.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/post/PostImage.kt index f029165..82f3ba6 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/post/PostImage.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/post/PostImage.kt @@ -100,11 +100,11 @@ fun PostImageThumb( if (ratio > 1) { height /= ratio height = height.roundToInt().toFloat() - width = (height / ratio).roundToInt().toFloat() + width = width.roundToInt().toFloat() } else { width /= ratio width = width.roundToInt().toFloat() - height = (width / ratio).roundToInt().toFloat() + height = height.roundToInt().toFloat() } AsyncImage( model = ImageRequest.Builder(LocalPlatformContext.current) diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/thread/ThreadItem.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/thread/ThreadItem.kt index 970170c..5005444 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/thread/ThreadItem.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/thread/ThreadItem.kt @@ -35,7 +35,8 @@ inline fun ThreadItem( is ThreadPost.ViewablePost -> { if (role == PostFragmentRole.PrimaryThreadRoot) { FullPostFragment( - post = item.post.copy(reason = reason), + post = item.post.copy(reason = reason, reply = item.post.reply?.copy(parentPost = null)), + modifier = modifier, onItemClicked = {onItemClicked(it) }, onProfileClicked = { onProfileClicked(it) }, onUnClicked = { type,uri-> onUnClicked(type,uri) }, @@ -47,8 +48,9 @@ inline fun ThreadItem( ) } else { PostFragment( - post = item.post.copy(reason = reason), + post = item.post.copy(reason = reason, reply = item.post.reply?.copy(parentPost = null)), role = role, + modifier = modifier, indentLevel = indentLevel, elevate = elevate, onItemClicked = {onItemClicked(it) }, @@ -64,6 +66,7 @@ inline fun ThreadItem( } is ThreadPost.BlockedPost -> { BlockedPostFragment( + modifier = modifier, post = item.uri, role = role, indentLevel = indentLevel, @@ -71,6 +74,7 @@ inline fun ThreadItem( } is ThreadPost.NotFoundPost -> { NotFoundPostFragment( + modifier = modifier, post = item.uri, role = role, indentLevel = indentLevel, diff --git a/Morpho/composeApp/src/commonTest/kotlin/com/morpho/app/model/bluesky/BskyPostTest.kt b/Morpho/composeApp/src/commonTest/kotlin/com/morpho/app/model/bluesky/BskyPostTest.kt index 016f8cc..472666b 100644 --- a/Morpho/composeApp/src/commonTest/kotlin/com/morpho/app/model/bluesky/BskyPostTest.kt +++ b/Morpho/composeApp/src/commonTest/kotlin/com/morpho/app/model/bluesky/BskyPostTest.kt @@ -22,7 +22,7 @@ class BskyPostTest { is ThreadPost.ViewablePost -> { assertEquals( (thread.parents.first() as ThreadPost.ViewablePost).post.uri, - p.post.reply?.root?.uri, + p.post.reply?.rootPost?.uri, "Root post uri should match first(highest) parent uri" ) @@ -50,7 +50,7 @@ class BskyPostTest { is ThreadPost.ViewablePost -> { assertEquals( (thread.parents.first() as ThreadPost.ViewablePost).post.uri, - p.post.reply?.root?.uri, + p.post.reply?.rootPost?.uri, "Root post uri should match first(highest) parent uri" ) } From 59051a7029d5230ec9cd369d0e09b0429d8e4c8f Mon Sep 17 00:00:00 2001 From: Orual Date: Fri, 13 Sep 2024 15:42:31 -0400 Subject: [PATCH 05/42] Ongoing session refresh and android state saving issues, hopefully fixed. --- Morpho/composeApp/build.gradle.kts | 12 +- .../com/morpho/app/MorphoApplication.kt | 3 +- .../com/morpho/app/util/Savers.android.kt | 3 - .../com/morpho/app/util/Savers.apple.kt | 3 - .../com/morpho/app/model/bluesky/BskyFacet.kt | 14 +- .../com/morpho/app/model/bluesky/BskyLabel.kt | 22 +- .../app/model/bluesky/BskyLabelService.kt | 8 +- .../com/morpho/app/model/bluesky/BskyList.kt | 11 +- .../com/morpho/app/model/bluesky/BskyPost.kt | 14 +- .../app/model/bluesky/BskyPostFeature.kt | 42 ++- .../app/model/bluesky/BskyPostReason.kt | 8 +- .../morpho/app/model/bluesky/BskyPostReply.kt | 27 +- .../app/model/bluesky/BskyPostThread.kt | 16 +- .../morpho/app/model/bluesky/FeedGenerator.kt | 8 +- .../com/morpho/app/model/bluesky/LitePost.kt | 8 +- .../app/model/bluesky/MorphoDataItem.kt | 26 +- .../com/morpho/app/model/bluesky/Profile.kt | 79 +++++- .../app/model/uidata/BskyDataService.kt | 35 ++- .../app/model/uidata/ContentCardMapEntry.kt | 11 +- .../app/model/uidata/ContentLabelService.kt | 106 ++++++-- .../com/morpho/app/model/uidata/Delta.kt | 6 +- .../com/morpho/app/model/uidata/FeedInfo.kt | 11 +- .../com/morpho/app/model/uidata/Moment.kt | 158 +++++++++++- .../com/morpho/app/model/uidata/MorphoData.kt | 242 ++++++++++++------ .../app/model/uistate/ContentCardState.kt | 4 +- .../morpho/app/model/uistate/LoadingState.kt | 10 +- .../model/uistate/PostThreadContentState.kt | 4 +- .../app/screens/base/tabbed/NavigationTabs.kt | 17 +- .../app/screens/main/MainScreenModel.kt | 8 +- .../app/screens/main/tabbed/TabbedHomeView.kt | 7 +- .../app/screens/profile/TabbedProfileView.kt | 10 +- .../morpho/app/ui/common/SkylineFragment.kt | 9 +- .../app/ui/common/SkylineThreadFragment.kt | 87 ++++--- .../morpho/app/ui/elements/ContentHider.kt | 86 ++++--- .../com/morpho/app/ui/elements/RichText.kt | 15 +- .../com/morpho/app/ui/elements/Wrappers.kt | 13 +- .../morpho/app/ui/post/BlockedPostFragment.kt | 3 +- .../morpho/app/ui/post/FullPostFragment.kt | 21 +- .../app/ui/post/NotFoundPostFragment.kt | 3 +- .../com/morpho/app/ui/post/PostActions.kt | 11 +- .../com/morpho/app/ui/post/PostFragment.kt | 85 ++---- .../com/morpho/app/ui/post/PostLinkEmbed.kt | 22 +- .../kotlin/com/morpho/app/ui/theme/Color.kt | 30 +-- .../morpho/app/ui/thread/ThreadFragment.kt | 69 ++--- .../com/morpho/app/ui/thread/ThreadReply.kt | 5 +- .../com/morpho/app/ui/thread/ThreadTree.kt | 159 +++++++++--- .../kotlin/com/morpho/app/util/Savers.kt | 4 - .../kotlin/com/morpho/app/util/encodings.kt | 4 +- .../kotlin/com/morpho/app/util/time.kt | 23 +- .../com/morpho/app/util/Savers.desktop.kt | 2 - Morpho/gradle/libs.versions.toml | 6 +- gradle/libs.versions.toml | 6 +- 52 files changed, 1100 insertions(+), 496 deletions(-) diff --git a/Morpho/composeApp/build.gradle.kts b/Morpho/composeApp/build.gradle.kts index 08af764..4e35d5b 100644 --- a/Morpho/composeApp/build.gradle.kts +++ b/Morpho/composeApp/build.gradle.kts @@ -11,7 +11,7 @@ plugins { alias(libs.plugins.androidApplication) id("kotlin-parcelize") - id("kotlin-kapt") + //id("kotlin-kapt") //id("com.rickclephas.kmp.nativecoroutines") version "1.0.0-ALPHA-27" } @@ -88,6 +88,13 @@ kotlin { commonMain.dependencies { implementation("com.morpho:shared") + implementation("com.russhwolf:multiplatform-settings:1.2.0") + implementation("com.russhwolf:multiplatform-settings-serialization:1.2.0") + implementation("com.russhwolf:multiplatform-settings-coroutines:1.2.0") + implementation("com.russhwolf:multiplatform-settings-datastore:1.2.0") + implementation("com.russhwolf:multiplatform-settings-no-arg:1.2.0") + implementation("androidx.datastore:datastore-preferences-core:1.1.1") + implementation("androidx.datastore:datastore-core:1.1.1") implementation(compose.runtime) implementation(compose.foundation) @@ -180,6 +187,9 @@ kotlin { implementation(libs.slf4j.api) //implementation(libs.slf4j.simple) + implementation("com.gu.android:toolargetool:0.3.0") + api("dev.icerock.moko:parcelize:0.9.0") + } nativeMain.dependencies { diff --git a/Morpho/composeApp/src/androidMain/kotlin/com/morpho/app/MorphoApplication.kt b/Morpho/composeApp/src/androidMain/kotlin/com/morpho/app/MorphoApplication.kt index 7f6fbce..4da1904 100644 --- a/Morpho/composeApp/src/androidMain/kotlin/com/morpho/app/MorphoApplication.kt +++ b/Morpho/composeApp/src/androidMain/kotlin/com/morpho/app/MorphoApplication.kt @@ -3,6 +3,7 @@ package com.morpho.app import android.app.Application import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.DefaultLifecycleObserver +import com.gu.toolargetool.TooLargeTool import com.morpho.app.data.PreferencesRepository import com.morpho.app.di.appModule import com.morpho.app.di.dataModule @@ -32,7 +33,7 @@ class AndroidMainViewModel(app: Application): AndroidViewModel(app), DefaultLife class MorphoApplication : Application() { override fun onCreate() { - + TooLargeTool.startLogging(this); val koin = startKoin { androidContext(this@MorphoApplication) diff --git a/Morpho/composeApp/src/androidMain/kotlin/com/morpho/app/util/Savers.android.kt b/Morpho/composeApp/src/androidMain/kotlin/com/morpho/app/util/Savers.android.kt index ccea826..48368fc 100644 --- a/Morpho/composeApp/src/androidMain/kotlin/com/morpho/app/util/Savers.android.kt +++ b/Morpho/composeApp/src/androidMain/kotlin/com/morpho/app/util/Savers.android.kt @@ -1,5 +1,2 @@ package com.morpho.app.util -// commonMain - module core -@Suppress("EXPECT_ACTUAL_CLASSIFIERS_ARE_IN_BETA_WARNING") -actual typealias JavaSerializable = java.io.Serializable \ No newline at end of file diff --git a/Morpho/composeApp/src/appleMain/kotlin/com/morpho/app/util/Savers.apple.kt b/Morpho/composeApp/src/appleMain/kotlin/com/morpho/app/util/Savers.apple.kt index 598c2bc..48368fc 100644 --- a/Morpho/composeApp/src/appleMain/kotlin/com/morpho/app/util/Savers.apple.kt +++ b/Morpho/composeApp/src/appleMain/kotlin/com/morpho/app/util/Savers.apple.kt @@ -1,5 +1,2 @@ package com.morpho.app.util -// commonMain - module core -@Suppress("EXPECT_ACTUAL_CLASSIFIERS_ARE_IN_BETA_WARNING") -actual interface JavaSerializable \ No newline at end of file diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/BskyFacet.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/BskyFacet.kt index 54a26be..b7ba40e 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/BskyFacet.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/BskyFacet.kt @@ -9,21 +9,25 @@ import com.morpho.butterfly.Did import com.morpho.butterfly.Handle import com.morpho.butterfly.Uri import com.morpho.butterfly.model.ReadOnlyList +import dev.icerock.moko.parcelize.Parcelable +import dev.icerock.moko.parcelize.Parcelize import kotlinx.collections.immutable.toImmutableList import kotlinx.serialization.Serializable - +@Parcelize @Immutable @Serializable data class BskyFacet( val start: Int, val end: Int, val facetType: List, -) +): Parcelable + +@Parcelize @Immutable @Serializable -sealed interface FacetType { +sealed interface FacetType: Parcelable { @Immutable @Serializable data class UserHandleMention( @@ -83,9 +87,10 @@ sealed interface FacetType { } +@Parcelize @Immutable @Serializable -sealed interface BlueMojiImageLink { +sealed interface BlueMojiImageLink: Parcelable { val url: String val apng: Boolean val lottie: Boolean @@ -116,6 +121,7 @@ sealed interface BlueMojiImageLink { } + @Immutable @Serializable enum class RichTextFormat { diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/BskyLabel.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/BskyLabel.kt index 7f07d14..ca6234e 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/BskyLabel.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/BskyLabel.kt @@ -6,11 +6,16 @@ import androidx.compose.runtime.Immutable import androidx.compose.ui.text.intl.Locale import app.bsky.actor.Visibility import com.atproto.label.* +import com.morpho.app.model.uidata.MaybeMomentParceler import com.morpho.app.model.uidata.Moment +import com.morpho.app.model.uidata.MomentParceler import com.morpho.butterfly.AtUri import com.morpho.butterfly.Cid import com.morpho.butterfly.Did import com.morpho.butterfly.Language +import dev.icerock.moko.parcelize.Parcelable +import dev.icerock.moko.parcelize.Parcelize +import dev.icerock.moko.parcelize.TypeParceler import kotlinx.collections.immutable.ImmutableMap import kotlinx.collections.immutable.toImmutableMap import kotlinx.datetime.Clock @@ -20,6 +25,7 @@ import kotlinx.serialization.Serializable import kotlinx.serialization.cbor.ByteString @OptIn(ExperimentalSerializationApi::class) +@Parcelize @Serializable @Immutable data class BskyLabel( @@ -29,12 +35,14 @@ data class BskyLabel( val cid: Cid?, val value: String, val overwritesPrevious: Boolean?, + @TypeParceler() val createdTimestamp: Moment, + @TypeParceler() val expirationTimestamp: Moment?, @OptIn(kotlinx.serialization.ExperimentalSerializationApi::class) @ByteString val signature: ByteArray?, -) { +): Parcelable { override fun equals(other: Any?): Boolean { if (this === other) return true if (other == null || this::class != other::class) return false @@ -121,6 +129,7 @@ enum class LabelTarget { Content } +@Parcelize @Immutable @Serializable open class ModBehaviour( @@ -132,7 +141,7 @@ open class ModBehaviour( val contentList: LabelAction = LabelAction.None, val contentView: LabelAction = LabelAction.None, val contentMedia: LabelAction = LabelAction.None, -) { +): Parcelable { init { require(avatar != LabelAction.Inform) require(banner != LabelAction.Inform && banner != LabelAction.Alert) @@ -172,13 +181,14 @@ open class ModBehaviour( } } +@Parcelize @Immutable @Serializable data class ModBehaviours( val account: ModBehaviour = ModBehaviour(), val profile: ModBehaviour = ModBehaviour(), val content: ModBehaviour = ModBehaviour(), -) { +): Parcelable { fun forScope(scope: LabelScope, target: LabelTarget): List { return when (target) { LabelTarget.Account -> when (scope) { @@ -338,6 +348,7 @@ fun Visibility.toLabelSetting(): LabelSetting { } +@Parcelize @Serializable @Immutable data class BskyLabelDefinition( @@ -349,7 +360,7 @@ data class BskyLabelDefinition( val localizedName: String, val localizedDescription: String, val allDescriptions: ImmutableMap -) { +): Parcelable { fun getVisibility(): Visibility { return when(defaultSetting) { LabelSetting.IGNORE -> Visibility.SHOW @@ -384,12 +395,13 @@ fun LabelValueDefinition.toModLabelDef() :BskyLabelDefinition { } +@Parcelize @Serializable @Immutable data class LocalizedLabelDescription( val localizedName: String, val localizedDescription: String, -) +): Parcelable @Suppress("unused") fun Label.toLabel(): BskyLabel { diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/BskyLabelService.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/BskyLabelService.kt index ee9e359..d86af2d 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/BskyLabelService.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/BskyLabelService.kt @@ -1,19 +1,22 @@ package com.morpho.app.model.bluesky import androidx.compose.runtime.Immutable -import app.bsky.actor.ProfileAssociated import app.bsky.labeler.LabelerView import app.bsky.labeler.LabelerViewDetailed import com.morpho.app.model.uidata.Moment +import com.morpho.app.model.uidata.MomentParceler import com.morpho.app.util.mapImmutable import com.morpho.butterfly.AtUri import com.morpho.butterfly.Cid import com.morpho.butterfly.Did import com.morpho.butterfly.Handle +import dev.icerock.moko.parcelize.Parcelize +import dev.icerock.moko.parcelize.TypeParceler import kotlinx.collections.immutable.persistentListOf import kotlinx.datetime.Clock import kotlinx.serialization.Serializable +@Parcelize @Serializable @Immutable open class BskyLabelService( @@ -23,6 +26,7 @@ open class BskyLabelService( val likeCount: Long?, val liked: Boolean, val likeUri: AtUri?, + @TypeParceler() override val indexedAt: Moment, val policies: List, override val labels: List, @@ -53,7 +57,7 @@ open class BskyLabelService( get() = 0 override val knownFollowers: List get() = listOf() - override val associated: ProfileAssociated? + override val associated: BskyProfileAssociated? get() = null override val createdAt: Moment? get() = null diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/BskyList.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/BskyList.kt index 684add7..4459b64 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/BskyList.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/BskyList.kt @@ -6,16 +6,21 @@ import app.bsky.graph.ListView import app.bsky.graph.ListViewBasic import app.bsky.graph.ListViewerState import com.morpho.app.model.uidata.Moment +import com.morpho.app.model.uidata.MomentParceler import com.morpho.app.util.mapImmutable import com.morpho.butterfly.* import com.morpho.butterfly.model.ReadOnlyList +import dev.icerock.moko.parcelize.Parcelable +import dev.icerock.moko.parcelize.Parcelize +import dev.icerock.moko.parcelize.TypeParceler import kotlinx.collections.immutable.toImmutableList import kotlinx.datetime.Clock import kotlinx.serialization.Serializable +@Parcelize @Serializable @Immutable -sealed interface BskyList { +sealed interface BskyList: Parcelable { val uri: AtUri val cid: Cid val purpose: ListType @@ -28,7 +33,7 @@ sealed interface BskyList { - +@Parcelize @Serializable @Immutable data class UserList( @@ -42,6 +47,7 @@ data class UserList( override val avatar: String? = null, override val viewerMuted: Boolean, override val viewerBlocked: AtUri? = null, + @TypeParceler() override val indexedAt: Moment, val labels: List = listOf(), val listItems: List = listOf(), @@ -86,6 +92,7 @@ data class UserListBasic( override val avatar: String? = null, override val viewerMuted: Boolean, override val viewerBlocked: AtUri? = null, + @TypeParceler() override val indexedAt: Moment, ): BskyList { override fun equals(other: Any?) : Boolean { diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/BskyPost.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/BskyPost.kt index 262da2e..0cb6c88 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/BskyPost.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/BskyPost.kt @@ -3,16 +3,20 @@ package com.morpho.app.model.bluesky import androidx.compose.runtime.Immutable import app.bsky.feed.* import com.morpho.app.model.uidata.Moment +import com.morpho.app.model.uidata.MomentParceler import com.morpho.app.util.deserialize import com.morpho.app.util.mapImmutable import com.morpho.butterfly.AtUri import com.morpho.butterfly.Cid import com.morpho.butterfly.Language +import dev.icerock.moko.parcelize.Parcelable +import dev.icerock.moko.parcelize.Parcelize +import dev.icerock.moko.parcelize.TypeParceler import kotlinx.collections.immutable.persistentListOf import kotlinx.datetime.Clock import kotlinx.serialization.Serializable - +@Parcelize @Serializable @Immutable data class BskyPost ( @@ -24,12 +28,14 @@ data class BskyPost ( val facets: List = listOf(), @Serializable val tags: List = listOf(), + @TypeParceler() val createdAt: Moment, @Serializable val feature: BskyPostFeature? = null, val replyCount: Long, val repostCount: Long, val likeCount: Long, + @TypeParceler() val indexedAt: Moment, val reposted: Boolean, val repostUri: AtUri? = null, @@ -41,8 +47,8 @@ data class BskyPost ( val reason: BskyPostReason? = null, @Serializable val langs: List = listOf(), -) { - override operator fun equals(other: Any?) : Boolean { +): Parcelable { + override fun equals(other: Any?) : Boolean { return when(other) { null -> false is Cid -> other == cid @@ -150,7 +156,7 @@ fun PostView.toPost( } // copy in the replyRef if it's not already there val replyRef = reply?.copy( - replyRef = postRecord.reply, + replyRef = postRecord.reply?.toReplyRef(), grandParentAuthor = reply.grandParentAuthor ?: postRecord.reply?.grandParentAuthor?.toProfile() ) ?: postRecord.reply?.toReply() diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/BskyPostFeature.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/BskyPostFeature.kt index 992a4c3..ba86165 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/BskyPostFeature.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/BskyPostFeature.kt @@ -5,16 +5,20 @@ import app.bsky.embed.* import app.bsky.feed.Post import app.bsky.feed.PostEmbedUnion import app.bsky.feed.PostViewEmbedUnion +import com.morpho.app.CommonRawValue import com.morpho.app.model.uidata.Moment +import com.morpho.app.model.uidata.MomentParceler import com.morpho.app.util.mapImmutable import com.morpho.butterfly.* import com.morpho.butterfly.model.Blob +import dev.icerock.moko.parcelize.* import kotlinx.serialization.Serializable import kotlinx.serialization.json.JsonElement +@Parcelize @Immutable @Serializable -sealed interface BskyPostFeature { +sealed interface BskyPostFeature: Parcelable { @Immutable @Serializable data class ImagesFeature( @@ -26,6 +30,7 @@ sealed interface BskyPostFeature { data class VideoFeature( val video: VideoEmbed, val alt: String, + @TypeParceler() val aspectRatio: AspectRatio?, ) : BskyPostFeature, TimelinePostMedia @@ -58,18 +63,20 @@ sealed interface BskyPostFeature { ) : BskyPostFeature, TimelinePostMedia } +@Parcelize @Immutable @Serializable -sealed interface TimelinePostMedia +sealed interface TimelinePostMedia: Parcelable +@Parcelize @Immutable @Serializable -sealed interface VideoEmbed +sealed interface VideoEmbed: Parcelable @Immutable @Serializable data class EmbedVideoView( - val cid: Cid, + val cid: Cid, val playlist: AtUri, val thumbnail: AtUri, ): VideoEmbed @@ -81,6 +88,7 @@ data class EmbedVideo( val captions: List?, ): VideoEmbed +@Parcelize @Immutable @Serializable data class EmbedImage( @@ -88,13 +96,14 @@ data class EmbedImage( val fullsize: String, val alt: String, val aspectRatio: AspectRatio? = null, -) +): Parcelable +@Parcelize @Immutable @Serializable -sealed interface EmbedRecord { +sealed interface EmbedRecord: Parcelable { @Immutable @Serializable @@ -173,8 +182,9 @@ sealed interface EmbedRecord { data class StarterPack( val uri: AtUri, val cid: Cid, - val record: JsonElement, + val record: @CommonRawValue JsonElement, val creator: Profile, + @TypeParceler() val indexedAt: Moment, val labels: List, ) : EmbedRecord @@ -501,4 +511,22 @@ private fun VideoViewVideo.toEmbedVideoFeature(): BskyPostFeature.VideoFeature { alt = this.alt?:"", aspectRatio = this.aspectRatio, ) +} + +object MaybeAspectRatioParceler : Parceler { + override fun create(parcel: Parcel): AspectRatio? { + val moment = parcel.readString() + val width = moment?.substringAfter("w:")?.substringBefore("h:")?.toLongOrNull() + val height = moment?.substringAfter("h:")?.substringBefore("w:")?.toLongOrNull() + return if(width != null && height != null) { + AspectRatio(width, height) + } else { + null + } + } + + override fun AspectRatio?.write(parcel: Parcel, flags: Int) { + parcel.writeString("w:${this?.width}") + parcel.writeString("h:${this?.height}") + } } \ No newline at end of file diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/BskyPostReason.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/BskyPostReason.kt index 0a34953..6f1770e 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/BskyPostReason.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/BskyPostReason.kt @@ -4,16 +4,22 @@ import androidx.compose.runtime.Immutable import app.bsky.feed.FeedViewPostReasonUnion import app.bsky.feed.SkeletonFeedPostReasonUnion import com.morpho.app.model.uidata.Moment +import com.morpho.app.model.uidata.MomentParceler import com.morpho.butterfly.AtUri +import dev.icerock.moko.parcelize.Parcelable +import dev.icerock.moko.parcelize.Parcelize +import dev.icerock.moko.parcelize.TypeParceler import kotlinx.serialization.Serializable +@Parcelize @Immutable @Serializable -sealed interface BskyPostReason { +sealed interface BskyPostReason: Parcelable { @Immutable @Serializable data class BskyPostRepost( val repostAuthor: Profile, + @TypeParceler() val indexedAt: Moment, ) : BskyPostReason diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/BskyPostReply.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/BskyPostReply.kt index d388190..861b06a 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/BskyPostReply.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/BskyPostReply.kt @@ -3,18 +3,22 @@ package com.morpho.app.model.bluesky import androidx.compose.runtime.Immutable import app.bsky.actor.ProfileViewBasic import app.bsky.feed.* +import com.atproto.repo.StrongRef import com.morpho.butterfly.Butterfly +import dev.icerock.moko.parcelize.Parcelable +import dev.icerock.moko.parcelize.Parcelize import kotlinx.collections.immutable.persistentListOf import kotlinx.serialization.Serializable +@Parcelize @Immutable @Serializable data class BskyPostReply( val rootPost: BskyPost? = null, val parentPost: BskyPost? = null, val grandParentAuthor: Profile? = null, - val replyRef: PostReplyRef? = null -) + val replyRef: BskyPostReplyRef? = null +): Parcelable fun ReplyRef.toReply(): BskyPostReply { return BskyPostReply( @@ -35,9 +39,26 @@ fun ReplyRef.toReply(): BskyPostReply { ) } +@Parcelize +@Immutable +@Serializable +public data class BskyPostReplyRef( + public val root: StrongRef, + public val parent: StrongRef, + public val grandParentAuthor: Profile? = null, +): Parcelable + +fun PostReplyRef.toReplyRef(): BskyPostReplyRef { + return BskyPostReplyRef( + root = this.root, + parent = this.parent, + grandParentAuthor = this.grandParentAuthor?.toProfile() + ) +} + fun PostReplyRef.toReply(): BskyPostReply { return BskyPostReply( - replyRef = this, + replyRef = this.toReplyRef(), grandParentAuthor = this.grandParentAuthor?.toProfile() ) } diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/BskyPostThread.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/BskyPostThread.kt index 35ee475..b4239e7 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/BskyPostThread.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/BskyPostThread.kt @@ -1,7 +1,6 @@ @file:Suppress("MemberVisibilityCanBePrivate") package com.morpho.app.model.bluesky - import androidx.compose.runtime.Immutable import androidx.compose.ui.util.fastForEachIndexed import app.bsky.feed.ThreadViewPost @@ -11,18 +10,21 @@ import com.morpho.app.model.bluesky.ThreadPost.* import com.morpho.app.util.mapImmutable import com.morpho.butterfly.AtUri import com.morpho.butterfly.Cid +import dev.icerock.moko.parcelize.Parcelable +import dev.icerock.moko.parcelize.Parcelize import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList import kotlinx.serialization.Serializable +@Parcelize @Immutable @Serializable data class BskyPostThread( val post: BskyPost, val parents: List, val replies: List, -) { +): Parcelable { operator fun contains(other: Any?) : Boolean { when(other) { null -> return false @@ -156,9 +158,10 @@ fun List.inParentOrder(): List { return newList.distinctBy { it.uri } } +@Parcelize @Immutable @Serializable -sealed interface ThreadPost { +sealed interface ThreadPost:Parcelable { val uri: AtUri? @Immutable @@ -284,6 +287,13 @@ sealed interface ThreadPost { } } + fun hasReplies(): Boolean { + return when(this) { + is ViewablePost -> replies.isNotEmpty() + else -> false + } + } + } fun ThreadViewPost.toThread(): BskyPostThread { diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/FeedGenerator.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/FeedGenerator.kt index 4e191a9..97c414d 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/FeedGenerator.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/FeedGenerator.kt @@ -6,13 +6,18 @@ import app.bsky.actor.FeedType import app.bsky.actor.SavedFeed import app.bsky.feed.GeneratorView import com.morpho.app.model.uidata.Moment +import com.morpho.app.model.uidata.MomentParceler import com.morpho.app.util.mapImmutable import com.morpho.butterfly.AtUri import com.morpho.butterfly.Cid import com.morpho.butterfly.Did import com.morpho.butterfly.model.TID +import dev.icerock.moko.parcelize.Parcelable +import dev.icerock.moko.parcelize.Parcelize +import dev.icerock.moko.parcelize.TypeParceler import kotlinx.serialization.Serializable +@Parcelize @Serializable @Immutable data class FeedGenerator( @@ -27,8 +32,9 @@ data class FeedGenerator( public val likeCount: Long, public val likedByMe: Boolean, public val likeRecord: AtUri?, + @TypeParceler() public val indexedAt: Moment, -) +): Parcelable fun GeneratorView.toFeedGenerator() : FeedGenerator { diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/LitePost.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/LitePost.kt index 83eb2db..10fdd89 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/LitePost.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/LitePost.kt @@ -3,10 +3,15 @@ package com.morpho.app.model.bluesky import androidx.compose.runtime.Immutable import app.bsky.feed.Post import com.morpho.app.model.uidata.Moment +import com.morpho.app.model.uidata.MomentParceler import com.morpho.app.util.mapImmutable import com.morpho.butterfly.Language +import dev.icerock.moko.parcelize.Parcelable +import dev.icerock.moko.parcelize.Parcelize +import dev.icerock.moko.parcelize.TypeParceler import kotlinx.serialization.Serializable +@Parcelize @Immutable @Serializable data class LitePost( @@ -14,8 +19,9 @@ data class LitePost( val facets: List, val feature: BskyPostFeature?, val langs: List, + @TypeParceler() val createdAt: Moment, -) +): Parcelable fun Post.toLitePost(): LitePost { return LitePost( diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/MorphoDataItem.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/MorphoDataItem.kt index 41e8c14..d5a0e5e 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/MorphoDataItem.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/MorphoDataItem.kt @@ -2,12 +2,11 @@ package com.morpho.app.model.bluesky import androidx.compose.runtime.Immutable import app.bsky.feed.* -import com.morpho.app.CommonParcelable import com.morpho.app.CommonParcelize -import com.morpho.app.CommonRawValue -import com.morpho.app.util.JavaSerializable import com.morpho.app.util.deserialize import com.morpho.butterfly.AtUri +import dev.icerock.moko.parcelize.Parcelable +import dev.icerock.moko.parcelize.Parcelize import kotlinx.serialization.Serializable @@ -18,10 +17,11 @@ import kotlinx.serialization.Serializable * This would help keep "when" statements from scenario where we want * e.g. PostItems and ThreadItems from needing to handle all possible subtypes. */ +@Parcelize @Immutable @Serializable @CommonParcelize -sealed interface MorphoDataItem: CommonParcelable, JavaSerializable { +sealed interface MorphoDataItem: Parcelable { @Immutable @Serializable @@ -254,8 +254,8 @@ sealed interface MorphoDataItem: CommonParcelable, JavaSerializable { @Serializable @CommonParcelize data class Post( - val post: @CommonRawValue BskyPost, - val reason: @CommonRawValue BskyPostReason? = post.reason, + val post: BskyPost, + val reason: BskyPostReason? = post.reason, val isOrphan: Boolean = false, ): FeedItem @@ -263,8 +263,8 @@ sealed interface MorphoDataItem: CommonParcelable, JavaSerializable { @Serializable @CommonParcelize data class Thread( - val thread: @CommonRawValue BskyPostThread, - val reason: @CommonRawValue BskyPostReason? = null, + val thread: BskyPostThread, + val reason: BskyPostReason? = null, val isIncompleteThread: Boolean = false, ): FeedItem { fun addReply(reply: BskyPost): Thread { @@ -281,21 +281,21 @@ sealed interface MorphoDataItem: CommonParcelable, JavaSerializable { @Serializable @CommonParcelize data class FeedInfo( - val feed: @CommonRawValue FeedGenerator, + val feed: FeedGenerator, ): MorphoDataItem @Immutable @Serializable @CommonParcelize data class ProfileItem( - val profile: @CommonRawValue Profile, + val profile:Profile, ): MorphoDataItem @Immutable @Serializable @CommonParcelize data class ListInfo( - val list: @CommonRawValue BskyList, + val list: BskyList, ): MorphoDataItem @@ -303,14 +303,14 @@ sealed interface MorphoDataItem: CommonParcelable, JavaSerializable { @Serializable @CommonParcelize data class ModLabel( - val label: @CommonRawValue BskyLabelDefinition, + val label: BskyLabelDefinition, ): MorphoDataItem @Immutable @Serializable @CommonParcelize data class LabelService( - val service: @CommonRawValue BskyLabelService, + val service: BskyLabelService, ): MorphoDataItem fun containsUri(uri: AtUri): Boolean { diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/Profile.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/Profile.kt index 9bd67e1..73ac0b7 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/Profile.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/Profile.kt @@ -3,12 +3,16 @@ package com.morpho.app.model.bluesky import androidx.compose.runtime.Immutable import app.bsky.actor.* +import com.morpho.app.model.uidata.MaybeMomentParceler import com.morpho.app.model.uidata.Moment -import com.morpho.app.util.JavaSerializable +import com.morpho.app.model.uidata.MomentParceler import com.morpho.app.util.mapImmutable import com.morpho.butterfly.AtUri import com.morpho.butterfly.Did import com.morpho.butterfly.Handle +import dev.icerock.moko.parcelize.Parcelable +import dev.icerock.moko.parcelize.Parcelize +import dev.icerock.moko.parcelize.TypeParceler import kotlinx.collections.immutable.toImmutableList import kotlinx.serialization.Serializable @@ -20,9 +24,10 @@ enum class ProfileType { Service } +@Parcelize @Immutable @Serializable -sealed interface Profile: JavaSerializable { +sealed interface Profile: Parcelable { val did: Did val handle: Handle val displayName: String? @@ -38,8 +43,10 @@ sealed interface Profile: JavaSerializable { val knownFollowers: List @Serializable val labels: List - val associated: ProfileAssociated? + val associated: BskyProfileAssociated? + @TypeParceler() val createdAt: Moment? + @TypeParceler() val indexedAt: Moment? val type: ProfileType get() = when (this) { @@ -55,14 +62,17 @@ sealed interface Profile: JavaSerializable { get() = followedBy != null } +@Parcelize @Immutable @Serializable -data class BlockRecord(val uri: AtUri) +data class BlockRecord(val uri: AtUri): Parcelable +@Parcelize @Immutable @Serializable -data class FollowRecord(val uri: AtUri) +data class FollowRecord(val uri: AtUri): Parcelable +@Parcelize @Immutable @Serializable data class BasicProfile( @@ -81,13 +91,16 @@ data class BasicProfile( override val blockingByList: UserListBasic?, override val numKnownFollowers: Long, override val knownFollowers: List, - override val associated: ProfileAssociated?, + override val associated: BskyProfileAssociated?, + @TypeParceler() override val createdAt: Moment?, -) : Profile { + ) : Profile, Parcelable{ + @TypeParceler() override val indexedAt: Moment? = null } +@Parcelize @Immutable @Serializable data class DetailedProfile( @@ -100,7 +113,9 @@ data class DetailedProfile( val followersCount: Long, val followsCount: Long, val postsCount: Long, + @TypeParceler() override val createdAt: Moment?, + @TypeParceler() override val indexedAt: Moment?, override val mutedByMe: Boolean, override val following: FollowRecord?, @@ -113,7 +128,7 @@ data class DetailedProfile( override val blockingByList: UserListBasic?, override val numKnownFollowers: Long, override val knownFollowers: List, - override val associated: ProfileAssociated?, + override val associated: BskyProfileAssociated?, ) : Profile { fun toSerializableProfile(): SerializableProfile { return SerializableProfile( @@ -144,6 +159,7 @@ data class DetailedProfile( } +@Parcelize @Immutable @Serializable data class SerializableProfile( @@ -156,7 +172,9 @@ data class SerializableProfile( val followersCount: Long, val followsCount: Long, val postsCount: Long, + @TypeParceler() val indexedAt: Moment?, + @TypeParceler() val createdAt: Moment?, val mutedByMe: Boolean, val following: FollowRecord?, @@ -169,8 +187,8 @@ data class SerializableProfile( val blockingByList: UserListBasic?, val numKnownFollowers: Long, val knownFollowers: List, - val associated: ProfileAssociated?, -) { + val associated: BskyProfileAssociated?, +): Parcelable { val type: ProfileType get() = ProfileType.Detailed fun toProfile(): DetailedProfile { @@ -224,7 +242,7 @@ fun ProfileViewDetailed.toProfile(): DetailedProfile { blockingByList = viewer?.blockingByList?.toList(), numKnownFollowers = viewer?.knownFollowers?.count ?: 0, knownFollowers = viewer?.knownFollowers?.followers?.mapImmutable { it.toProfile() }?.toList() ?: listOf(), - associated = associated, + associated = associated?.toBskyProfileAssociated(), createdAt = createdAt?.let(::Moment), ) } @@ -245,7 +263,7 @@ fun ProfileViewBasic.toProfile(): Profile { blockingByList = viewer?.blockingByList?.toList(), numKnownFollowers = viewer?.knownFollowers?.count ?: 0, knownFollowers = viewer?.knownFollowers?.followers?.mapImmutable { it.toProfile() }?.toList() ?: listOf(), - associated = associated, + associated = associated?.toBskyProfileAssociated(), createdAt = createdAt?.let(::Moment), ) } @@ -266,7 +284,7 @@ fun ProfileView.toProfile(): Profile { blockingByList = viewer?.blockingByList?.toList(), numKnownFollowers = viewer?.knownFollowers?.count ?: 0, knownFollowers = viewer?.knownFollowers?.followers?.mapImmutable { it.toProfile() }?.toList() ?: listOf(), - associated = associated, + associated = associated?.toBskyProfileAssociated(), createdAt = createdAt?.let(::Moment), ) } @@ -293,4 +311,37 @@ fun Profile.toProfileViewBasic(): ProfileViewBasic { labels = labels.mapImmutable { it.toAtProtoLabel() }, ) -} \ No newline at end of file +} + +fun ProfileAssociated.toBskyProfileAssociated(): BskyProfileAssociated { + return BskyProfileAssociated( + lists = this.lists, + feedGens = this.feedGens, + labeler = this.labeler, + starterPacks = this.starterPacks, + chat = this.chat?.toProfileAssociatedChat() + ) +} +@Immutable +@Parcelize +@Serializable +public data class BskyProfileAssociated( + public val lists: Long? = null, + public val feedGens: Long? = null, + public val labeler: Boolean? = null, + public val starterPacks: Long? = null, + public val chat: BskyProfileAssociatedChat? = null, +): Parcelable + +fun ProfileAssociatedChat.toProfileAssociatedChat(): BskyProfileAssociatedChat { + return BskyProfileAssociatedChat( + allowIncoming = this.allowIncoming + ) +} + +@Parcelize +@Immutable +@Serializable +public data class BskyProfileAssociatedChat( + public val allowIncoming: AllowIncoming, +): Parcelable \ No newline at end of file diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/BskyDataService.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/BskyDataService.kt index e48cae9..978fefe 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/BskyDataService.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/BskyDataService.kt @@ -166,7 +166,8 @@ class BskyDataService: KoinComponent { oldCursor = cursor, feed = response.feed, data = data as MorphoData, - ).collectThreads(api = api).single() + api = api, + ).single() var tunedFeed = new useFeedTuners(tunedFeed).forEach { tuner -> tunedFeed = tuner.tune(tunedFeed) @@ -195,7 +196,8 @@ class BskyDataService: KoinComponent { feed = response.feed, data = data as MorphoData, title = "Posts", - ).collectThreads(api = api).single() + api = api, + ).single() useFeedTuners(tunedFeed).forEach { tuner -> tunedFeed = tuner.tune(tunedFeed) } @@ -223,7 +225,8 @@ class BskyDataService: KoinComponent { feed = response.feed, data = data as MorphoData, title = "Replies", - ).collectThreads(api = api).single() + api = api, + ).single() useFeedTuners(tunedFeed).forEach { tuner -> tunedFeed = tuner.tune(tunedFeed) } @@ -244,7 +247,7 @@ class BskyDataService: KoinComponent { if (response.cursor == cursor.cursor && cursor != AtCursor.EMPTY) { return@onSuccess } - var tunedFeed = MorphoData.concatFeed( + var tunedFeed = MorphoData.concatNonThreadedFeed( query = json.encodeToJsonElement(query), responseCursor = response.cursor, oldCursor = cursor, @@ -272,7 +275,7 @@ class BskyDataService: KoinComponent { if (response.cursor == cursor.cursor && cursor != AtCursor.EMPTY) { return@onSuccess } - var tunedFeed = MorphoData.concatFeed( + var tunedFeed = MorphoData.concatNonThreadedFeed( query = json.encodeToJsonElement(query), responseCursor = response.cursor, oldCursor = cursor, @@ -400,7 +403,8 @@ class BskyDataService: KoinComponent { oldCursor = cursor, feed = response.feed, data = data as MorphoData, - ).collectThreads(api = api).single() + api = api, + ).single() useFeedTuners(tunedFeed).forEach { tuner -> tunedFeed = tuner.tune(tunedFeed) } @@ -427,7 +431,8 @@ class BskyDataService: KoinComponent { oldCursor = cursor, feed = response.feed, data = data as MorphoData, - ).collectThreads(api = api).single() + api = api, + ).single() useFeedTuners(tunedFeed).forEach { tuner -> tunedFeed = tuner.tune(tunedFeed) } @@ -469,7 +474,8 @@ class BskyDataService: KoinComponent { data = (prev ?: MorphoData.EMPTY()) as MorphoData, title = "Home", uri = AtUri.HOME_URI, - ).collectThreads(api = api).single() + api = api, + ).single() useFeedTuners(tunedFeed).forEach { tuner -> tunedFeed = tuner.tune(tunedFeed) } @@ -529,7 +535,8 @@ class BskyDataService: KoinComponent { data = (prev ?: MorphoData.EMPTY()) as MorphoData, title = feedInfo.name, uri = feedInfo.uri, - ).collectThreads(api = api).single() + api = api, + ).single() useFeedTuners(tunedFeed).forEach { tuner -> tunedFeed = tuner.tune(tunedFeed) } @@ -653,7 +660,8 @@ class BskyDataService: KoinComponent { feed = response.feed, data = (prev ?: MorphoData.EMPTY()) as MorphoData, title = "Posts", - ).collectThreads(api = api).single() + api = api, + ).single() useFeedTuners(tunedFeed).forEach { tuner -> tunedFeed = tuner.tune(tunedFeed) } @@ -685,7 +693,8 @@ class BskyDataService: KoinComponent { feed = response.feed, data = (prev ?: MorphoData.EMPTY()) as MorphoData, title = "Replies", - ).collectThreads(api = api).single() + api = api, + ).single() useFeedTuners(tunedFeed).forEach { tuner -> tunedFeed = tuner.tune(tunedFeed) } @@ -710,7 +719,7 @@ class BskyDataService: KoinComponent { if (response.cursor == cur.cursor) { return@collect } - var tunedFeed = MorphoData.concatFeed( + var tunedFeed = MorphoData.concatNonThreadedFeed( query = json.encodeToJsonElement(query), responseCursor = response.cursor, oldCursor = cur, @@ -881,7 +890,7 @@ class BskyDataService: KoinComponent { val query = GetActorLikesQuery(id, limit, cur.cursor) api.api.getActorLikes(query) .onSuccess { response -> - var tunedFeed = MorphoData.concatFeed( + var tunedFeed = MorphoData.concatNonThreadedFeed( query = json.encodeToJsonElement(query), responseCursor = response.cursor, oldCursor = cur, diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/ContentCardMapEntry.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/ContentCardMapEntry.kt index 63d554a..121df72 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/ContentCardMapEntry.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/ContentCardMapEntry.kt @@ -2,7 +2,6 @@ package com.morpho.app.model.uidata import androidx.compose.runtime.Immutable import com.morpho.app.model.uistate.FeedType -import com.morpho.app.util.JavaSerializable import com.morpho.app.util.MutableSharedFlowSerializer import com.morpho.butterfly.AtIdentifier import com.morpho.butterfly.AtUri @@ -11,10 +10,11 @@ import kotlinx.serialization.Serializable @Immutable @Serializable -sealed interface ContentCardMapEntry: JavaSerializable { +sealed interface ContentCardMapEntry { val uri: AtUri val title: String @Serializable(with = MutableSharedFlowSerializer::class) + //@TypeParceler, AtCursorMutableSharedFlowParceler> val cursorFlow: MutableSharedFlow val avatar: String? @@ -24,6 +24,7 @@ sealed interface ContentCardMapEntry: JavaSerializable { override val uri: AtUri = AtUri.HOME_URI override val title: String = "Home" @Serializable(with = MutableSharedFlowSerializer::class) + //@TypeParceler, AtCursorMutableSharedFlowParceler> override val cursorFlow: MutableSharedFlow = initAtCursor() override val avatar: String? = null } @@ -38,6 +39,7 @@ sealed interface ContentCardMapEntry: JavaSerializable { override val uri: AtUri, override val title: String = uri.atUri, @Serializable(with = MutableSharedFlowSerializer::class) + //@TypeParceler, AtCursorMutableSharedFlowParceler> override val cursorFlow: MutableSharedFlow = initAtCursor(), override val avatar: String? = null, ) : ContentCardMapEntry, Skyline @@ -48,6 +50,7 @@ sealed interface ContentCardMapEntry: JavaSerializable { override val uri: AtUri, override val title: String = uri.atUri, @Serializable(with = MutableSharedFlowSerializer::class) + //@TypeParceler, AtCursorMutableSharedFlowParceler> override val cursorFlow: MutableSharedFlow = initAtCursor(), override val avatar: String? = null, ) : ContentCardMapEntry @@ -58,6 +61,7 @@ sealed interface ContentCardMapEntry: JavaSerializable { override val uri: AtUri, override val title: String = uri.atUri, @Serializable(with = MutableSharedFlowSerializer::class) + //@TypeParceler, AtCursorMutableSharedFlowParceler> override val cursorFlow: MutableSharedFlow = initAtCursor(), override val avatar: String? = null, ) : ContentCardMapEntry @@ -68,6 +72,7 @@ sealed interface ContentCardMapEntry: JavaSerializable { override val uri: AtUri, override val title: String = uri.atUri, @Serializable(with = MutableSharedFlowSerializer::class) + //@TypeParceler, AtCursorMutableSharedFlowParceler> override val cursorFlow: MutableSharedFlow = initAtCursor(), override val avatar: String? = null, ) : ContentCardMapEntry @@ -78,6 +83,7 @@ sealed interface ContentCardMapEntry: JavaSerializable { override val uri: AtUri, override val title: String = uri.atUri, @Serializable(with = MutableSharedFlowSerializer::class) + //@TypeParceler, AtCursorMutableSharedFlowParceler> override val cursorFlow: MutableSharedFlow = initAtCursor(), override val avatar: String? = null, ) : ContentCardMapEntry @@ -89,6 +95,7 @@ sealed interface ContentCardMapEntry: JavaSerializable { override val uri: AtUri = AtUri.profileUri(id), override val title: String = uri.atUri, @Serializable(with = MutableSharedFlowSerializer::class) + //@TypeParceler, AtCursorMutableSharedFlowParceler> override val cursorFlow: MutableSharedFlow = initAtCursor(), override val avatar: String? = null, ) : ContentCardMapEntry diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/ContentLabelService.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/ContentLabelService.kt index 5f99bc6..7974ddd 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/ContentLabelService.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/ContentLabelService.kt @@ -1,7 +1,9 @@ package com.morpho.app.model.uidata import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.HideImage import androidx.compose.material.icons.filled.Info +import androidx.compose.material.icons.filled.StopCircle import androidx.compose.material.icons.filled.Warning import androidx.compose.runtime.Immutable import androidx.compose.ui.graphics.vector.ImageVector @@ -12,11 +14,12 @@ import app.bsky.actor.Visibility import com.atproto.label.LabelValue import com.atproto.label.Severity import com.morpho.app.model.bluesky.* -import com.morpho.app.util.JavaSerializable import com.morpho.butterfly.AtUri import com.morpho.butterfly.Butterfly import com.morpho.butterfly.Language import com.morpho.butterfly.model.ReadOnlyList +import dev.icerock.moko.parcelize.Parcelable +import dev.icerock.moko.parcelize.Parcelize import kotlinx.collections.immutable.* import kotlinx.coroutines.* import kotlinx.coroutines.flow.SharingStarted @@ -28,26 +31,72 @@ import org.koin.core.component.KoinComponent import org.koin.core.component.inject import org.lighthousegames.logging.logging - +@Parcelize data class ContentHandling( val scope: LabelScope, val action: LabelAction, val source: LabelDescription, val id: String, + val icon: LabelIcon, +): Parcelable + +@Parcelize +@Immutable +@Serializable +sealed interface LabelIcon: Parcelable { + val labelerAvatar: String? + val icon: ImageVector - val icon: ImageVector, -): JavaSerializable + @Serializable + @Immutable + data class CircleBanSign( + override val labelerAvatar: String? + ): LabelIcon { + override val icon: ImageVector + get() = Icons.Default.StopCircle + } + @Serializable + @Immutable + data class Warning( + override val labelerAvatar: String? + ): LabelIcon { + override val icon: ImageVector + get() = Icons.Default.Warning + } + @Serializable + @Immutable + data class EyeSlash( + override val labelerAvatar: String? + ): LabelIcon { + override val icon: ImageVector + get() = Icons.Default.HideImage + } + + @Serializable + @Immutable + data class CircleInfo( + override val labelerAvatar: String? + ): LabelIcon { + override val icon: ImageVector + get() = Icons.Default.Info + } + +} + +@Parcelize @Immutable @Serializable -sealed interface LabelDescription: JavaSerializable { +sealed interface LabelDescription: Parcelable { val name: String val description: String + @Parcelize @Immutable @Serializable - sealed interface Block: LabelDescription + sealed interface Block: LabelDescription, Parcelable + @Parcelize @Immutable @Serializable data object Blocking: Block { @@ -55,12 +104,14 @@ sealed interface LabelDescription: JavaSerializable { override val description: String = "You have blocked this user. You cannot view their content" } + @Parcelize @Immutable @Serializable data object BlockedBy: Block { override val name: String = "User Blocking You" override val description: String = "This user has blocked you. You cannot view their content." } + @Parcelize @Immutable @Serializable data class BlockList( @@ -70,6 +121,7 @@ sealed interface LabelDescription: JavaSerializable { override val name: String = "User Blocked by $listName" override val description: String = "This user is on a block list you subscribe to. You cannot view their content." } + @Parcelize @Immutable @Serializable data object OtherBlocked: Block { @@ -77,9 +129,12 @@ sealed interface LabelDescription: JavaSerializable { override val description: String = "This content is not available because one of the users involved has blocked the other." } + @Parcelize @Immutable @Serializable - sealed interface Muted: LabelDescription + sealed interface Muted: LabelDescription, Parcelable + + @Parcelize @Immutable @Serializable data class MuteList( @@ -89,12 +144,14 @@ sealed interface LabelDescription: JavaSerializable { override val name: String = "User Muted by $listName" override val description: String = "This user is on a mute list you subscribe to." } + @Parcelize @Immutable @Serializable data object YouMuted: Muted { override val name: String = "Account Muted" override val description: String = "You have muted this user." } + @Parcelize @Immutable @Serializable data class MutedWord(val word: String): Muted { @@ -102,6 +159,7 @@ sealed interface LabelDescription: JavaSerializable { override val description: String = "This post contains the word or tag \"$word\". You've chosen to hide it." } + @Parcelize @Immutable @Serializable data class HiddenPost(val uri: AtUri): LabelDescription { @@ -109,6 +167,7 @@ sealed interface LabelDescription: JavaSerializable { override val description: String = "You have hidden this post." } + @Parcelize @Immutable @Serializable data class Label( @@ -118,9 +177,10 @@ sealed interface LabelDescription: JavaSerializable { ): LabelDescription } +@Parcelize @Immutable @Serializable -sealed interface LabelSource: JavaSerializable { +sealed interface LabelSource: Parcelable { @Immutable @Serializable data object User: LabelSource @@ -136,9 +196,10 @@ sealed interface LabelSource: JavaSerializable { ): LabelSource } +@Parcelize @Immutable @Serializable -sealed interface LabelCause: JavaSerializable { +sealed interface LabelCause: Parcelable { val downgraded: Boolean val priority: Int val source: LabelSource @@ -219,7 +280,7 @@ sealed interface LabelCause: JavaSerializable { } - +@Parcelize @Serializable @Immutable open class InterpretedLabelDefinition( @@ -235,12 +296,12 @@ open class InterpretedLabelDefinition( val localizedDescription: String = "", @Contextual val allDescriptions: ImmutableMap = persistentMapOf(), -): JavaSerializable { +): Parcelable { companion object { } - public fun toContentHandling(target: LabelTarget, icon: ImageVector? = null): ContentHandling { + public fun toContentHandling(target: LabelTarget, avatar: String? = null): ContentHandling { val action = behaviours.forScope(whatToHide, target).minOrNull() ?: when(defaultSetting) { LabelSetting.HIDE -> LabelAction.Blur LabelSetting.WARN -> LabelAction.Alert @@ -256,10 +317,10 @@ open class InterpretedLabelDefinition( description = localizedDescription, severity = severity, ), - icon = icon ?: when(severity) { - Severity.ALERT -> Icons.Default.Warning - Severity.NONE -> Icons.Default.Info - Severity.INFORM -> Icons.Default.Info + icon = when(severity) { + Severity.ALERT -> LabelIcon.Warning(labelerAvatar = avatar) + Severity.NONE -> LabelIcon.CircleInfo(labelerAvatar = avatar) + Severity.INFORM -> LabelIcon.CircleInfo(labelerAvatar = avatar) } ) } @@ -274,6 +335,8 @@ val LABELS: PersistentMap = persistentMa LabelValue.NUDITY to Nudity, LabelValue.GRAPHIC_MEDIA to GraphicMedia, ) + +@Parcelize @Immutable @Serializable data object Hide: InterpretedLabelDefinition( @@ -340,6 +403,7 @@ data object Warn: InterpretedLabelDefinition( localizedDescription = "Warn", ) +@Parcelize @Immutable @Serializable data object NoUnauthed: InterpretedLabelDefinition( @@ -873,7 +937,7 @@ class ContentLabelService: KoinComponent { action = LabelAction.Blur, source = LabelDescription.Blocking, id = "blocking", - icon = Icons.Default.Info, + icon = LabelIcon.CircleInfo(labelerAvatar = null), )) } is LabelCause.BlockedBy -> { @@ -882,7 +946,7 @@ class ContentLabelService: KoinComponent { action = LabelAction.Blur, source = LabelDescription.BlockedBy, id = "blocked-by", - icon = Icons.Default.Info, + icon = LabelIcon.CircleInfo(labelerAvatar = null), )) } is LabelCause.BlockOther -> { @@ -891,7 +955,7 @@ class ContentLabelService: KoinComponent { action = LabelAction.Blur, source = LabelDescription.OtherBlocked, id = "blocked-other", - icon = Icons.Default.Info, + icon = LabelIcon.CircleInfo(labelerAvatar = null), )) } is LabelCause.Muted -> { @@ -901,7 +965,7 @@ class ContentLabelService: KoinComponent { action = LabelAction.Blur, source = LabelDescription.YouMuted, id = "muted", - icon = Icons.Default.Info, + icon = LabelIcon.CircleInfo(labelerAvatar = null), )) } is LabelCause.MutedWord -> { @@ -911,7 +975,7 @@ class ContentLabelService: KoinComponent { action = LabelAction.Blur, source = LabelDescription.MutedWord("Some word"), id = "muted-word", - icon = Icons.Default.Info, + icon = LabelIcon.CircleInfo(labelerAvatar = null), ) ) } diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/Delta.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/Delta.kt index e81ecd4..2352dbe 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/Delta.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/Delta.kt @@ -1,12 +1,14 @@ package com.morpho.app.model.uidata import androidx.compose.runtime.Immutable -import com.morpho.app.util.JavaSerializable +import dev.icerock.moko.parcelize.Parcelable +import dev.icerock.moko.parcelize.Parcelize import kotlinx.serialization.Serializable import kotlin.time.Duration +@Parcelize @Immutable @Serializable data class Delta( val duration: Duration, -): JavaSerializable \ No newline at end of file +): Parcelable \ No newline at end of file diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/FeedInfo.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/FeedInfo.kt index 3e2e321..200f895 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/FeedInfo.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/FeedInfo.kt @@ -1,16 +1,14 @@ package com.morpho.app.model.uidata -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.RssFeed import androidx.compose.runtime.Immutable -import androidx.compose.ui.graphics.vector.ImageVector import com.morpho.app.model.bluesky.FeedGenerator import com.morpho.app.model.bluesky.UserList -import com.morpho.app.util.JavaSerializable import com.morpho.butterfly.AtUri +import dev.icerock.moko.parcelize.Parcelable +import dev.icerock.moko.parcelize.Parcelize import kotlinx.serialization.Serializable -import kotlinx.serialization.Transient +@Parcelize @Immutable @Serializable data class FeedInfo( @@ -18,7 +16,6 @@ data class FeedInfo( val name: String, val description: String? = null, val avatar: String? = null, - @Transient val icon: ImageVector = Icons.Default.RssFeed, val feed: FeedGenerator? = null, val list: UserList? = null, -): JavaSerializable \ No newline at end of file +): Parcelable \ No newline at end of file diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/Moment.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/Moment.kt index 0346f41..e747695 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/Moment.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/Moment.kt @@ -1,16 +1,29 @@ package com.morpho.app.model.uidata import androidx.compose.runtime.Immutable +import com.morpho.app.model.bluesky.BskyPostThread +import com.morpho.app.model.bluesky.MorphoDataItem +import com.morpho.app.model.uistate.ContentCardState.ProfileTimeline +import com.morpho.butterfly.json +import dev.icerock.moko.parcelize.Parcel +import dev.icerock.moko.parcelize.Parceler +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow import kotlinx.datetime.Instant import kotlinx.serialization.Serializable +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.JsonObject import kotlin.jvm.JvmInline + @Serializable @Immutable @JvmInline value class Moment( val instant: Instant, -) : Comparable { +): Comparable { operator fun plus(delta: Delta): Moment = Moment(instant + delta.duration) operator fun minus(delta: Delta): Moment = Moment(instant - delta.duration) @@ -19,3 +32,146 @@ value class Moment( override fun compareTo(other: Moment): Int = instant.compareTo(instant) } + +object MomentParceler : Parceler{ + override fun create(parcel: Parcel): Moment { + val moment = parcel.readString()?.substringAfter("t")?.substringBefore("Z") + return moment?.let { Moment(Instant.fromEpochMilliseconds(it.toLong())) } + ?: Moment(Instant.DISTANT_PAST) + } + + override fun Moment.write(parcel: Parcel, flags: Int) { + parcel.writeString("t${this.instant.toEpochMilliseconds()}Z") + } +} + +object MaybeMomentParceler : Parceler{ + override fun create(parcel: Parcel): Moment? { + val moment = parcel.readString()?.substringAfter("t")?.substringBefore("Z") + if(moment == "0") return null + return moment?.let { Moment(Instant.fromEpochMilliseconds(it.toLong())) } + } + + override fun Moment?.write(parcel: Parcel, flags: Int) { + if(this == null) { + parcel.writeString("t0Z") + return + } else { + parcel.writeString("t${this.instant.toEpochMilliseconds()}Z") + } + } +} + +object JsonElementParceler : Parceler{ + override fun create(parcel: Parcel): JsonElement { + val serialized = parcel.readString() + return serialized?.let { json.parseToJsonElement(it) } ?: JsonObject(emptyMap()) + } + + override fun JsonElement.write(parcel: Parcel, flags: Int) { + parcel.writeString(json.encodeToString(JsonElement.serializer(), this)) + } +} + +object AtCursorMutableSharedFlowParceler : Parceler>{ + override fun create(parcel: Parcel): MutableSharedFlow { + val serialized = parcel.readString() + val flow = initAtCursor() + if (serialized != null) { + json.decodeFromString(AtCursor.serializer(), serialized).let { cursor -> + flow.tryEmit(cursor) + } + } + return flow + } + + override fun MutableSharedFlow.write(parcel: Parcel, flags: Int) { + val serialized = json.encodeToString(AtCursor.serializer(), this.replayCache.lastOrNull() ?: AtCursor.EMPTY) + parcel.writeString(serialized) + } +} + +object PostThreadStateFlowParceler : Parceler>{ + override fun create(parcel: Parcel): StateFlow { + val serialized = parcel.readString() + val flow = MutableStateFlow(null) + return flow.asStateFlow() + } + + override fun StateFlow.write(parcel: Parcel, flags: Int) { + if(this.value == null) { + parcel.writeString("null") + return + } + val serialized = json.encodeToString(BskyPostThread.serializer(), this.value!!) + parcel.writeString(serialized) + } +} + +object ProfileTimelineStateFlowParceler : Parceler?>>{ + override fun create(parcel: Parcel): StateFlow?> { + val serialized = parcel.readString() + val flow = MutableStateFlow(null) + return flow.asStateFlow() + } + + override fun StateFlow?>.write(parcel: Parcel, flags: Int) { + if(this.value == null) { + parcel.writeString("null") + return + } + + parcel.writeString("${this.value!!.uri}") + } +} + +object ProfileListsStateFlowParceler : Parceler?>>{ + override fun create(parcel: Parcel): StateFlow?> { + val serialized = parcel.readString() + val flow = MutableStateFlow(null) + return flow.asStateFlow() + } + + override fun StateFlow?>.write(parcel: Parcel, flags: Int) { + if(this.value == null) { + parcel.writeString("null") + return + } + + parcel.writeString("${this.value!!.uri}") + } +} + +object ProfileFeedsStateFlowParceler : Parceler?>>{ + override fun create(parcel: Parcel): StateFlow?> { + val serialized = parcel.readString() + val flow = MutableStateFlow(null) + return flow.asStateFlow() + } + + override fun StateFlow?>.write(parcel: Parcel, flags: Int) { + if(this.value == null) { + parcel.writeString("null") + return + } + + parcel.writeString("${this.value!!.uri}") + } +} + +object ProfileLabelServiceStateFlowParceler : Parceler?>>{ + override fun create(parcel: Parcel): StateFlow?> { + val serialized = parcel.readString() + val flow = MutableStateFlow(null) + return flow.asStateFlow() + } + + override fun StateFlow?>.write(parcel: Parcel, flags: Int) { + if(this.value == null) { + parcel.writeString("null") + return + } + + parcel.writeString("${this.value!!.uri}") + } +} \ No newline at end of file diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/MorphoData.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/MorphoData.kt index d984659..159be1d 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/MorphoData.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/MorphoData.kt @@ -7,15 +7,13 @@ import app.bsky.feed.FeedViewPost import com.morpho.app.data.BskyUserPreferences import com.morpho.app.model.bluesky.* import com.morpho.app.model.uistate.FeedType -import com.morpho.app.util.JavaSerializable import com.morpho.butterfly.* +import dev.icerock.moko.parcelize.Parcelable +import dev.icerock.moko.parcelize.Parcelize import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toPersistentList import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.firstOrNull -import kotlinx.coroutines.flow.flow -import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.* import kotlinx.datetime.Instant import kotlinx.serialization.Serializable import kotlinx.serialization.json.JsonElement @@ -25,14 +23,16 @@ import kotlin.time.Duration typealias TunerFunction = (List, FeedTuner) -> List +@Parcelize @Immutable @Serializable -data class AtCursor(val cursor: String?, val scroll: Int){ +data class AtCursor(val cursor: String?, val scroll: Int): Parcelable { companion object { val EMPTY: AtCursor = AtCursor(null, 0) } } + @Immutable @Serializable data class MorphoData( @@ -40,8 +40,9 @@ data class MorphoData( val uri: AtUri = AtUri.HOME_URI, val cursor: AtCursor = AtCursor.EMPTY, val items: List = listOf(), + //@TypeParceler() val query: JsonElement = JsonObject(emptyMap()), -): JavaSerializable { +) { companion object { fun EMPTY(): MorphoData { @@ -97,19 +98,40 @@ data class MorphoData( data: MorphoData, uri: AtUri = data.uri, title: String = data.title, + api: Butterfly? = null, + ): Flow> = flow { + val newItems = fromFeed( + feed.toList(), AtCursor(responseCursor, 0), + uri = uri, title = title, query = query).collectThreads().single() + emit(if (oldCursor != AtCursor.EMPTY && data.items.isNotEmpty()) { + val newScroll = maxOf(data.items.size, oldCursor.scroll) + concat(data, newItems, AtCursor(responseCursor, newScroll), query = query) + } else if (oldCursor == AtCursor.EMPTY && data.items.isNotEmpty()) { + concat(newItems, data,AtCursor(responseCursor, 0), query = query) + } else { + newItems + }) + } + + fun concatNonThreadedFeed( + query: JsonElement, + responseCursor: String?, + oldCursor: AtCursor, + feed: List, + data: MorphoData, + uri: AtUri = data.uri, + title: String = data.title, ): MorphoData { + val newItems = fromFeed( + feed.toList(), AtCursor(responseCursor, 0), + uri = uri, title = title, query = query) return if (oldCursor != AtCursor.EMPTY && data.items.isNotEmpty()) { - concat(feed.toList(), data, - AtCursor(responseCursor, oldCursor.scroll), - query = query) + val newScroll = if(oldCursor.scroll == 0) 0 else maxOf(data.items.size, oldCursor.scroll) + concat(data, newItems, AtCursor(responseCursor, newScroll), query = query) } else if (oldCursor == AtCursor.EMPTY && data.items.isNotEmpty()) { - concat(feed.toList(), data, - AtCursor(responseCursor, oldCursor.scroll), - query = query) + concat(newItems, data,AtCursor(responseCursor, 0), query = query) } else { - fromFeed( - feed.toList(), AtCursor(responseCursor, oldCursor.scroll), - uri = uri, title = title, query = query) + newItems } } @@ -121,21 +143,21 @@ data class MorphoData( query: JsonElement = JsonObject(emptyMap()), ): MorphoData { return first.copy( - items = (first.items union last.items).toPersistentList() - .sortedByDescending { - when (it) { - is MorphoDataItem.Post -> it.post.createdAt - is MorphoDataItem.Thread -> it.thread.post.createdAt - is MorphoDataItem.FeedInfo -> it.feed.indexedAt - is MorphoDataItem.ListInfo -> it.list.indexedAt - is MorphoDataItem.ModLabel -> Moment(Instant.DISTANT_PAST) - is MorphoDataItem.ProfileItem -> Moment(Instant.DISTANT_PAST) - is MorphoDataItem.LabelService -> it.service.indexedAt - else -> { - Moment(Instant.DISTANT_PAST) - } - } - }.toList(), + items = (first.items + last.items).toPersistentList(), +// .sortedByDescending { +// when (it) { +// is MorphoDataItem.Post -> it.post.createdAt +// is MorphoDataItem.Thread -> it.thread.post.createdAt +// is MorphoDataItem.FeedInfo -> it.feed.indexedAt +// is MorphoDataItem.ListInfo -> it.list.indexedAt +// is MorphoDataItem.ModLabel -> Moment(Instant.DISTANT_PAST) +// is MorphoDataItem.ProfileItem -> Moment(Instant.DISTANT_PAST) +// is MorphoDataItem.LabelService -> it.service.indexedAt +// else -> { +// Moment(Instant.DISTANT_PAST) +// } +// } +// }.toList(), cursor = cursor, title = first.title, uri = first.uri ) } @@ -147,21 +169,7 @@ data class MorphoData( query: JsonElement = JsonObject(emptyMap()), ): MorphoData { return first.copy( - items = (first.items union last).toPersistentList() - .sortedByDescending { - when (it) { - is MorphoDataItem.Post -> it.post.createdAt - is MorphoDataItem.Thread -> it.thread.post.createdAt - is MorphoDataItem.FeedInfo -> it.feed.indexedAt - is MorphoDataItem.ListInfo -> it.list.indexedAt - is MorphoDataItem.ModLabel -> Moment(Instant.DISTANT_PAST) - is MorphoDataItem.ProfileItem -> Moment(Instant.DISTANT_PAST) - is MorphoDataItem.LabelService -> it.service.indexedAt - else -> { - Moment(Instant.DISTANT_PAST) - } - } - }.toList(), + items = (first.items + last), cursor = cursor, title = first.title, uri = first.uri ) } @@ -172,21 +180,7 @@ data class MorphoData( cursor: AtCursor = last.cursor, ): MorphoData { return last.copy( - items = (first union last.items).toPersistentList() - .sortedByDescending { - when (it) { - is MorphoDataItem.Post -> it.post.createdAt - is MorphoDataItem.Thread -> it.thread.post.createdAt - is MorphoDataItem.FeedInfo -> it.feed.indexedAt - is MorphoDataItem.ListInfo -> it.list.indexedAt - is MorphoDataItem.ModLabel -> Moment(Instant.DISTANT_PAST) - is MorphoDataItem.ProfileItem -> Moment(Instant.DISTANT_PAST) - is MorphoDataItem.LabelService -> it.service.indexedAt - else -> { - Moment(Instant.DISTANT_PAST) - } - } - }.toList(), + items = (first + last.items), cursor = cursor, title = last.title, uri = last.uri ) } @@ -389,33 +383,120 @@ data class MorphoData( } threadCandidates.fastForEachIndexed { index, thread -> if (thread == null) return@fastForEachIndexed - val inThreads = threads.indexOfFirst { t -> t?.containsUri(thread.thread.post.uri) ?: false } - if (inThreads == - 1) { - val threadToSplice = threads.getOrNull(index) ?: return@fastForEachIndexed - threads[index] = threadToSplice.addReply(ThreadPost.ViewablePost(thread.thread.post, thread.thread.replies)) - threadCandidates[index] = null + val rootInThreads = threads.indexOfFirst { t -> t?.containsUri(thread.rootUri) ?: false } + if (rootInThreads == - 1) { + val threadToSplice = threads.getOrNull(rootInThreads) ?: return@fastForEachIndexed + if( + thread.thread.parents.firstOrNull() is ThreadPost.ViewablePost + && threadToSplice.thread.parents.firstOrNull() is ThreadPost.ViewablePost + && thread.rootUri == threadToSplice.rootUri + ) { + if(thread.thread.parents.size == 1 && threadToSplice.thread.parents.size == 1) { + // Both threads have the same, viewable root post and are only one level deep in terms of parents + val newEntry = thread.thread.parents.first() as ThreadPost.ViewablePost + val oldEntry = threadToSplice.thread.parents.first() as ThreadPost.ViewablePost + + val newReplies = (newEntry.replies + oldEntry.replies).distinctBy { it.uri }.toMutableList() + newReplies.add(ThreadPost.ViewablePost(thread.thread.post, thread.thread.replies)) + if( thread.getUri() != threadToSplice.getUri() ) + newReplies.add(ThreadPost.ViewablePost(threadToSplice.thread.post, threadToSplice.thread.replies)) + val newThread = BskyPostThread( + post = newEntry.post, + parents = listOf(), + replies = newReplies.distinctBy { it.uri }, + ) + threads[rootInThreads] = threadToSplice.copy(thread = newThread, isIncompleteThread = false) + threadCandidates[index] = null + } else if(thread.thread.parents.size == 2 && threadToSplice.thread.parents.size == 2) { + // Both threads have the same, viewable root post and parent chains are both length 2 + val newEntry = thread.thread.parents.first() as ThreadPost.ViewablePost + + val newReplies = mutableListOf() + if(thread.thread.parents.lastOrNull() !is ThreadPost.ViewablePost) return@fastForEachIndexed + if(threadToSplice.thread.parents.lastOrNull() !is ThreadPost.ViewablePost) return@fastForEachIndexed + val newParent = thread.thread.parents.last() as ThreadPost.ViewablePost + val oldParent = threadToSplice.thread.parents.last() as ThreadPost.ViewablePost + val newReply = ThreadPost.ViewablePost(thread.thread.post, thread.thread.replies) + val oldReply = ThreadPost.ViewablePost(threadToSplice.thread.post, threadToSplice.thread.replies) + newParent.addReply(newReply) + oldParent.addReply(oldReply) + newReplies.add(newReply) + newReplies.add(oldReply) + val newThread = BskyPostThread( + post = newEntry.post, + parents = listOf(newParent), + replies = newReplies.distinctBy { it.uri }, + ) + threads[rootInThreads] = threadToSplice.copy(thread = newThread, isIncompleteThread = false) + threadCandidates[index] = null + } + + } + } else { + val inThreads = threads.indexOfFirst { t -> t?.containsUri(thread.thread.post.uri) ?: false } + if (inThreads == - 1) { + val threadToSplice = threads.getOrNull(index) ?: return@fastForEachIndexed + threads[index] = threadToSplice.addReply(ThreadPost.ViewablePost(thread.thread.post, thread.thread.replies)) + threadCandidates[index] = null + } } } threadCandidates.fastFilterNotNull() if (threadCandidates.isNotEmpty()) threads.addAll(threadCandidates) - val newFeed = posts.toList().fastFilterNotNull() + val newReplies = replies.filterNotNull() .distinctBy { it.getUri() } - .filterNot { post -> + .filterNot { reply -> + if(reply.isRepost) return@filterNot false + if(reply.isQuotePost) return@filterNot false + reply.getUris().any { uri -> threads.any { it?.containsUri(uri) ?: false } } + }.sortedByDescending { when(it.reason) { + is BskyPostReason.BskyPostRepost -> it.reason.indexedAt + else -> it.post.createdAt + } }.iterator() + var newPosts = posts.toList().filterNotNull() + newPosts = newPosts.distinctBy { it.getUri() } + newPosts = newPosts.filterNot { post -> if(post.isRepost) return@filterNot false if(post.isQuotePost) return@filterNot false post.getUris().any { uri -> threads.any { it?.containsUri(uri) ?: false } } - } + threads.toList().fastFilterNotNull() + replies.toList().fastFilterNotNull() - .distinctBy { it.getUri() } - .filterNot { reply -> - if(reply.isRepost) return@filterNot false - if(reply.isQuotePost) return@filterNot false - reply.getUris().any { uri -> threads.any { it?.containsUri(uri) ?: false } } - } - val sortedFeed = newFeed.sortedByDescending { + }.sortedByDescending { when(it.reason) { + is BskyPostReason.BskyPostRepost -> it.reason.indexedAt + else -> it.post.createdAt + } } + val newPostsIter = newPosts.iterator() + var newThreads = threads.toList().filterNotNull() + newThreads = newThreads.sortedByDescending { if(!repliesBumpThreads) { + it.rootAccessiblePost.createdAt + } else { + maxOf(it.thread.post.createdAt, + it.thread.replies.fold(it.thread.post.createdAt) { acc, post -> + val postTime = when(post) { + is ThreadPost.ViewablePost -> post.post.createdAt + is ThreadPost.BlockedPost -> Moment(Instant.DISTANT_PAST) + is ThreadPost.NotFoundPost -> Moment(Instant.DISTANT_PAST) + } + maxOf(acc, postTime) + }) + } } + newThreads = newThreads.distinctBy { it.getUri() } + .filterNot { thread -> + thread.getUris().filterNot { uri -> + newThreads.fastAny { it.getUri() == uri } }.size > 1 + } + val newThreadsIter = newThreads.iterator() + val newFeed = mutableListOf() + while(newPostsIter.hasNext() || newThreadsIter.hasNext() || newReplies.hasNext() ) { + if(newPostsIter.hasNext()) newFeed.add(newPostsIter.next()) + if(newThreadsIter.hasNext()) newFeed.add(newThreadsIter.next()) + if(newReplies.hasNext()) newFeed.add(newReplies.next()) + } + val dedupedFeed = newFeed.distinctBy { it.getUri() } + //println("New feed:\n${newFeed.joinToString("\n")}") + val sortedFeed = dedupedFeed.sortedByDescending { when(it) { is MorphoDataItem.Post -> when(it.reason) { is BskyPostReason.BskyPostFeedPost -> it.post.createdAt - is BskyPostReason.BskyPostRepost -> it.post.createdAt + is BskyPostReason.BskyPostRepost -> it.reason.indexedAt is BskyPostReason.SourceFeed -> it.post.createdAt null -> it.post.createdAt } @@ -434,10 +515,9 @@ data class MorphoData( } } } - - @Suppress("UNCHECKED_CAST") val newData = - copy( items = sortedFeed.fastDistinctBy { it.getUri() } as List) - emit(newData.dedup()) + //println("sorted feed:\n${sortedFeed.joinToString("\n")}") + @Suppress("UNCHECKED_CAST") val newData = copy( items = sortedFeed as List) + emit(newData) }.flowOn(Dispatchers.Default) fun dedup(): MorphoData { diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uistate/ContentCardState.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uistate/ContentCardState.kt index 2670414..04f0be8 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uistate/ContentCardState.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uistate/ContentCardState.kt @@ -2,16 +2,16 @@ package com.morpho.app.model.uistate import com.morpho.app.model.bluesky.* import com.morpho.app.model.uidata.MorphoData -import com.morpho.app.util.JavaSerializable import com.morpho.butterfly.AtUri import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.serialization.Serializable + @Suppress("unused") @Serializable -sealed interface ContentCardState: JavaSerializable { +sealed interface ContentCardState { val uri: AtUri val feed: MorphoData val hasNewPosts: Boolean diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uistate/LoadingState.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uistate/LoadingState.kt index b099704..b360686 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uistate/LoadingState.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uistate/LoadingState.kt @@ -1,24 +1,20 @@ package com.morpho.app.model.uistate import androidx.compose.runtime.Immutable -import com.morpho.app.CommonParcelable -import com.morpho.app.CommonParcelize -import com.morpho.app.util.JavaSerializable import kotlinx.serialization.Serializable -@CommonParcelize @Immutable @Serializable -sealed interface UiLoadingState: CommonParcelable, JavaSerializable { +sealed interface UiLoadingState { data object Loading : UiLoadingState data object Idle : UiLoadingState data class Error(val errorMessage: String) : UiLoadingState } -@CommonParcelize + @Immutable @Serializable -sealed interface ContentLoadingState: CommonParcelable, JavaSerializable { +sealed interface ContentLoadingState { data object Loading : ContentLoadingState data object Idle : ContentLoadingState data class Error(val errorMessage: String) : ContentLoadingState diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uistate/PostThreadContentState.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uistate/PostThreadContentState.kt index a2fc50e..91789c1 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uistate/PostThreadContentState.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uistate/PostThreadContentState.kt @@ -1,9 +1,7 @@ package com.morpho.app.model.uistate -import com.morpho.app.util.JavaSerializable - -interface PostThreadContentState: JavaSerializable { +interface PostThreadContentState { val hasNewPosts: Boolean val loadingState: ContentLoadingState val isLoading: Boolean diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/base/tabbed/NavigationTabs.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/base/tabbed/NavigationTabs.kt index 7c47004..47d31cc 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/base/tabbed/NavigationTabs.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/base/tabbed/NavigationTabs.kt @@ -26,9 +26,10 @@ import com.morpho.app.screens.thread.ThreadTopBar import com.morpho.app.screens.thread.ThreadViewContent import com.morpho.app.ui.common.LoadingCircle import com.morpho.app.ui.common.TabbedScreenScaffold -import com.morpho.app.util.JavaSerializable import com.morpho.butterfly.AtIdentifier import com.morpho.butterfly.AtUri +import dev.icerock.moko.parcelize.Parcelable +import dev.icerock.moko.parcelize.Parcelize import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.launch import kotlinx.serialization.Contextual @@ -36,16 +37,17 @@ import kotlinx.serialization.Serializable import kotlinx.serialization.Transient +@Parcelize @Immutable @Serializable data class TabScreenOptions( val index: Int, val icon: @Composable () -> Unit, val title: String, -) +): Parcelable -interface TabScreen: Screen, JavaSerializable { +interface TabScreen: Screen, Parcelable { val navBar: @Composable (Navigator) -> Unit @@ -57,6 +59,7 @@ interface TabScreen: Screen, JavaSerializable { } +@Parcelize @Immutable @Serializable data class HomeTab( @@ -89,6 +92,7 @@ data class HomeTab( } +@Parcelize @Immutable @Serializable data object SearchTab: TabScreen { @@ -117,6 +121,7 @@ data object SearchTab: TabScreen { } +@Parcelize @Immutable @Serializable data object FeedsTab: TabScreen { @@ -144,6 +149,7 @@ data object FeedsTab: TabScreen { } +@Parcelize @Immutable @Serializable data object NotificationsTab: TabScreen { @@ -177,6 +183,9 @@ data object NotificationsTab: TabScreen { } +@Parcelize +@Serializable +@Immutable data class ProfileTab( val id: AtIdentifier, ): TabScreen { @@ -207,6 +216,7 @@ data class ProfileTab( } +@Parcelize @Immutable @Serializable data class ThreadTab( @@ -252,6 +262,7 @@ data class ThreadTab( } } +@Parcelize @Immutable @Serializable data object MyProfileTab: TabScreen { diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/main/MainScreenModel.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/main/MainScreenModel.kt index 50409c9..7260310 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/main/MainScreenModel.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/main/MainScreenModel.kt @@ -1,7 +1,5 @@ package com.morpho.app.screens.main -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Home import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue @@ -129,8 +127,7 @@ open class MainScreenModel: BaseScreenModel() { uri == AtUri.HOME_URI -> return FeedInfo( uri, "Home", - "Your home feed", - icon = Icons.Default.Home + "Your home feed" ) else -> { @@ -142,8 +139,7 @@ open class MainScreenModel: BaseScreenModel() { is UIFeedType.Timeline -> return FeedInfo( uri, "Home", - "Your home feed", - icon = Icons.Default.Home + "Your home feed" ) } }?.let { diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/main/tabbed/TabbedHomeView.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/main/tabbed/TabbedHomeView.kt index e9f5805..63bdbec 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/main/tabbed/TabbedHomeView.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/main/tabbed/TabbedHomeView.kt @@ -45,7 +45,8 @@ import com.morpho.app.ui.common.TabbedScreenScaffold import com.morpho.app.ui.common.TabbedSkylineFragment import com.morpho.app.ui.elements.AvatarShape import com.morpho.app.ui.elements.OutlinedAvatar -import com.morpho.app.util.JavaSerializable +import dev.icerock.moko.parcelize.Parcelable +import dev.icerock.moko.parcelize.Parcelize import kotlinx.coroutines.flow.StateFlow import kotlinx.serialization.Serializable import org.jetbrains.compose.resources.ExperimentalResourceApi @@ -265,13 +266,13 @@ fun HomeTabRow( } - +@Parcelize @Serializable data class HomeSkylineTab @OptIn(ExperimentalVoyagerApi::class) constructor( val index: UShort, val title: String, val avatar: String? = null, -): SkylineTab(), JavaSerializable { +): SkylineTab(), Parcelable { @OptIn(ExperimentalMaterial3Api::class, ExperimentalVoyagerApi::class) @Composable diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/profile/TabbedProfileView.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/profile/TabbedProfileView.kt index b4c4c2b..f7e742b 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/profile/TabbedProfileView.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/profile/TabbedProfileView.kt @@ -37,8 +37,9 @@ import com.morpho.app.ui.common.LoadingCircle import com.morpho.app.ui.common.TabbedProfileScreenScaffold import com.morpho.app.ui.common.TabbedSkylineFragment import com.morpho.app.ui.profile.DetailedProfileFragment -import com.morpho.app.util.JavaSerializable import com.morpho.butterfly.AtIdentifier +import dev.icerock.moko.parcelize.Parcelable +import dev.icerock.moko.parcelize.Parcelize import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.flow.StateFlow import kotlinx.serialization.Serializable @@ -153,7 +154,7 @@ fun TabScreen.TabbedProfileContent( var selectedTabIndex by rememberSaveable { mutableIntStateOf(0) } val scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior(rememberTopAppBarState()) val ownProfile = remember { sm.api.atpUser?.id == id } - val tabs = rememberSaveable( + val tabs = remember( sm.tabFlow, sm.profileUiState.loadingState, ) { @@ -165,7 +166,7 @@ fun TabScreen.TabbedProfileContent( ) } } - val tabsCreated = rememberSaveable(tabs.size, sm.profileUiState.loadingState) { + val tabsCreated = remember(tabs.size, sm.profileUiState.loadingState) { tabs.isNotEmpty() && sm.profileUiState.loadingState == UiLoadingState.Idle } if (tabsCreated) { @@ -276,12 +277,13 @@ abstract class ProfileTabScreen: NavTab { final override fun Content() = Content(TabbedProfileViewModel(),PaddingValues(0.dp),null, rememberLazyListState(), Modifier) } +@Parcelize @Serializable data class ProfileSkylineTab( val index: UShort, val ownProfile: Boolean = false, val title: String, -): ProfileTabScreen(), JavaSerializable { +): ProfileTabScreen(), Parcelable { @OptIn(ExperimentalMaterial3Api::class, ExperimentalVoyagerApi::class) @Composable diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/common/SkylineFragment.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/common/SkylineFragment.kt index 1056b27..e476941 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/common/SkylineFragment.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/common/SkylineFragment.kt @@ -14,7 +14,6 @@ import androidx.compose.material.pullrefresh.pullRefresh import androidx.compose.material.pullrefresh.rememberPullRefreshState import androidx.compose.material3.* import androidx.compose.runtime.* -import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp @@ -58,7 +57,7 @@ fun SkylineFragment ( listState: LazyListState = rememberLazyListState( initialFirstVisibleItemIndex = content.value.feed.cursor.scroll ), - debuggable: Boolean = true, + debuggable: Boolean = false, ) { val currentRefresh by rememberUpdatedState(refresh) @@ -71,7 +70,7 @@ fun SkylineFragment ( val scope = rememberCoroutineScope() var refreshing by remember { mutableStateOf(false) } - val data = rememberSaveable(loading, state, cursor, refreshing) { + val data = remember(loading, state, cursor, refreshing) { state.value.feed } val scrolledDownSome by remember { @@ -80,7 +79,7 @@ fun SkylineFragment ( } } - val scrollCursor by rememberSaveable { derivedStateOf { + val scrollCursor by remember { derivedStateOf { listState.firstVisibleItemIndex } } @@ -223,6 +222,7 @@ fun SkylineFragment ( thread = item.thread, modifier = if(debuggable) Modifier.border(1.dp, Color.White) else Modifier .fillMaxWidth() + //.padding(horizontal = 4.dp), .padding(vertical = 2.dp, horizontal = 4.dp), onItemClicked = onItemClicked, onProfileClicked = onProfileClicked, @@ -239,6 +239,7 @@ fun SkylineFragment ( PostFragment( modifier = if(debuggable) Modifier.border(1.dp, Color.Blue) else Modifier .fillMaxWidth() + //.padding(horizontal = 4.dp), .padding(vertical = 2.dp, horizontal = 4.dp), post = item.post, onItemClicked = onItemClicked, diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/common/SkylineThreadFragment.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/common/SkylineThreadFragment.kt index 77c7036..478dd33 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/common/SkylineThreadFragment.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/common/SkylineThreadFragment.kt @@ -45,16 +45,16 @@ inline fun SkylineThreadFragment( val hasReplies = rememberSaveable { threadPost.replies.isNotEmpty() } var showReplies by remember { mutableStateOf(threadPost.replies.size <= 2)} var showFullThread by remember { mutableStateOf(thread.parents.size <= 3)} + val parents = remember { thread.parents.distinctBy { it.uri } } Surface( tonalElevation = if (hasReplies) 1.dp else 0.dp, shape = MaterialTheme.shapes.extraSmall, modifier = if (hasReplies) modifier.padding(2.dp) else modifier.fillMaxWidth() ) { - Column( - ) { - if (thread.parents.isNotEmpty()) { - when (val root = thread.parents[0]) { + Column { + if (parents.isNotEmpty()) { + when (val root = parents[0]) { is ThreadPost.ViewablePost -> if (root.post.uri == thread.post.uri) { Surface( tonalElevation = 1.dp, @@ -65,7 +65,7 @@ inline fun SkylineThreadFragment( ) { PostFragment( post = root.post, - role = PostFragmentRole.ThreadBranchStart, + role = PostFragmentRole.Solo, elevate = true, modifier = if(debuggable) Modifier.border(1.dp, Color.Cyan) else Modifier, onItemClicked = {onItemClicked(it) }, @@ -89,7 +89,7 @@ inline fun SkylineThreadFragment( modifier = Modifier .padding(4.dp), ) { - if(thread.parents.size > 3) { + if(parents.size > 3) { ThreadItem( item = thread.parents[0], modifier = if(debuggable) Modifier.border(1.dp, Color.Green) else Modifier, @@ -136,7 +136,7 @@ inline fun SkylineThreadFragment( if (showFullThread) { - thread.parents.fastForEachIndexed { index, post -> + parents.fastForEachIndexed { index, post -> val reason = remember { when (post) { is ThreadPost.BlockedPost -> null @@ -148,12 +148,18 @@ inline fun SkylineThreadFragment( } val role = remember { when (index) { - thread.parents.lastIndex -> PostFragmentRole.ThreadBranchEnd - 0 -> PostFragmentRole.ThreadBranchStart + 0 -> PostFragmentRole.Solo + 1 -> PostFragmentRole.ThreadBranchStart + parents.lastIndex -> PostFragmentRole.ThreadBranchEnd else -> PostFragmentRole.ThreadBranchMiddle } } - if (post is ThreadPost.ViewablePost && (index < thread.parents.lastIndex) && (index != 0)) { + if ( + post is ThreadPost.ViewablePost + && post.uri != threadPost.uri + && (index > 0 || parents.lastIndex < 2) + && index < parents.lastIndex + ) { ThreadItem( item = post, role = role, @@ -173,23 +179,28 @@ inline fun SkylineThreadFragment( } } } - ThreadItem( - item = thread.parents[thread.parents.lastIndex], - role = PostFragmentRole.ThreadBranchEnd, - indentLevel = 1, - modifier = if(debuggable) Modifier.border(1.dp, Color.Yellow) else Modifier, - elevate = true, - onItemClicked = onItemClicked, - onProfileClicked = onProfileClicked, - onUnClicked = onUnClicked, - onRepostClicked = onRepostClicked, - onReplyClicked = onReplyClicked, - onLikeClicked = onLikeClicked, - onMenuClicked = onMenuClicked, - getContentHandling = getContentHandling - ) + if (parents[parents.lastIndex] is ThreadPost.ViewablePost) { + ThreadItem( + item = parents[parents.lastIndex], + role = PostFragmentRole.ThreadBranchEnd, + indentLevel = 1, + modifier = if (debuggable) Modifier.border( + 1.dp, + Color.Yellow + ) else Modifier, + elevate = true, + onItemClicked = onItemClicked, + onProfileClicked = onProfileClicked, + onUnClicked = onUnClicked, + onRepostClicked = onRepostClicked, + onReplyClicked = onReplyClicked, + onLikeClicked = onLikeClicked, + onMenuClicked = onMenuClicked, + getContentHandling = getContentHandling + ) + } } else { - thread.parents.fastForEachIndexed { index, post -> + parents.fastForEachIndexed { index, post -> val reason = remember { when (post) { is ThreadPost.BlockedPost -> null @@ -201,12 +212,14 @@ inline fun SkylineThreadFragment( } val role = remember { when (index) { - thread.parents.lastIndex -> PostFragmentRole.ThreadBranchEnd - 0 -> PostFragmentRole.ThreadBranchStart + 0 -> PostFragmentRole.ThreadRootUnfocused + parents.lastIndex -> PostFragmentRole.ThreadBranchEnd else -> PostFragmentRole.ThreadBranchMiddle } } - if (post is ThreadPost.ViewablePost && post.uri != threadPost.uri) { + if (post is ThreadPost.ViewablePost + && post.uri != threadPost.uri + ) { ThreadItem( item = post, role = role, @@ -228,9 +241,9 @@ inline fun SkylineThreadFragment( } val role = remember { - when (thread.parents.size) { + when (parents.size) { 0 -> PostFragmentRole.Solo - 1 -> PostFragmentRole.ThreadEnd + 1 -> PostFragmentRole.Solo else -> PostFragmentRole.Solo } } @@ -239,8 +252,8 @@ inline fun SkylineThreadFragment( role = role, reason = null, elevate = true, - modifier = if(debuggable) Modifier.border(1.dp, Color.Magenta) else Modifier - .padding(4.dp), + modifier = if(debuggable) Modifier.border(1.dp, Color.Magenta) else Modifier, + //.padding(4.dp), onItemClicked = onItemClicked, onProfileClicked = onProfileClicked, onUnClicked = onUnClicked, @@ -259,7 +272,7 @@ inline fun SkylineThreadFragment( } } else { val role = remember { - when (thread.parents.size) { + when (parents.size) { 0 -> PostFragmentRole.Solo 1 -> PostFragmentRole.ThreadEnd else -> PostFragmentRole.Solo @@ -268,7 +281,7 @@ inline fun SkylineThreadFragment( ThreadItem( item = threadPost, role = role, - reason = thread.post.reason, + reason = null, elevate = true, modifier = if(debuggable) Modifier.border(1.dp, Color.Blue) else Modifier .padding(4.dp), @@ -323,7 +336,7 @@ inline fun SkylineThreadFragment( val replies = remember {threadPost.replies.filterIsInstance()} replies.fastForEach { post: ThreadPost -> if (post is ThreadPost.ViewablePost) { - if (post.replies.isNotEmpty()) { + if (post.replies.isNotEmpty() && replies.size > 1) { ThreadTree( reply = post, indentLevel = 1, modifier = Modifier.padding(4.dp), @@ -339,7 +352,7 @@ inline fun SkylineThreadFragment( } else { ThreadItem( item = post, - role = PostFragmentRole.ThreadRootUnfocused, + role = PostFragmentRole.ThreadEnd, indentLevel = 1, modifier = Modifier.padding(4.dp), onItemClicked = onItemClicked, diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/elements/ContentHider.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/elements/ContentHider.kt index 063dd85..8d089eb 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/elements/ContentHider.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/elements/ContentHider.kt @@ -1,15 +1,30 @@ package com.morpho.app.ui.elements +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.core.Spring +import androidx.compose.animation.core.VisibilityThreshold +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.core.spring +import androidx.compose.animation.expandVertically +import androidx.compose.animation.shrinkVertically +import androidx.compose.foundation.Image +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.CornerSize import androidx.compose.foundation.text.selection.DisableSelection import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ChevronRight import androidx.compose.material.icons.filled.Info -import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text -import androidx.compose.material3.TextButton import androidx.compose.runtime.* +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.rotate +import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.unit.IntSize import androidx.compose.ui.unit.dp import androidx.compose.ui.util.fastFilter import com.morpho.app.model.bluesky.LabelAction @@ -32,46 +47,53 @@ public fun ContentHider( toHide.isNotEmpty() ) } + val reason = toHide.firstOrNull() + val degrees by animateFloatAsState(if (!hideContent) -90f else 90f) Column { if (toHide.isNotEmpty()) { - TextButton( - onClick = { hideContent = !hideContent }, - modifier = Modifier.fillMaxWidth(), - shape = ButtonDefaults.textShape, - colors = ButtonDefaults.elevatedButtonColors(), - elevation = ButtonDefaults.filledTonalButtonElevation() - ) { - Icon( - imageVector = reason?.icon ?: Icons.Default.Info, - contentDescription = reason?.source?.description - ) - DisableSelection { - Text( - text = reason?.source?.name ?: "", - modifier = Modifier.padding(horizontal = 4.dp) + Row(modifier = if (hideContent) Modifier.clip(MaterialTheme.shapes.small) + .clickable { hideContent = !hideContent }.fillMaxWidth().padding(12.dp) + else Modifier + .clip(MaterialTheme.shapes.small.copy(bottomEnd = CornerSize(0.dp), bottomStart = CornerSize(0.dp))) + .clickable { hideContent = !hideContent }.fillMaxWidth().padding(12.dp) + , horizontalArrangement = Arrangement.SpaceBetween) { + Row(verticalAlignment = Alignment.CenterVertically) { + Icon( + imageVector = reason?.icon?.icon?: Icons.Default.Info, + contentDescription = reason?.source?.description, + tint = MaterialTheme.colorScheme.onSurfaceVariant + ) + Spacer(modifier = Modifier.width(8.dp)) + Text(reason?.source?.name ?: "", + style = MaterialTheme.typography.labelLarge, + color = MaterialTheme.colorScheme.onSurfaceVariant ) } - - Spacer( - modifier = Modifier - .width(1.dp) - .weight(0.3f) - ) DisableSelection { - Text( - text = if (hideContent) { - "Show" - } else { - "Hide" - } + Image( + Icons.Default.ChevronRight, + contentDescription = null, + modifier = Modifier.rotate(degrees), + colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.onSurfaceVariant) ) - } + } } } - if (!hideContent) { - content() + AnimatedVisibility( + visible = !hideContent, + enter = expandVertically( + spring( + stiffness = Spring.StiffnessMediumLow, + visibilityThreshold = IntSize.VisibilityThreshold + ) + ), + exit = shrinkVertically() + ) { + Column(modifier = Modifier.fillMaxWidth()) { + content() + } } } } \ No newline at end of file diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/elements/RichText.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/elements/RichText.kt index 015697b..3d0669e 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/elements/RichText.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/elements/RichText.kt @@ -47,6 +47,11 @@ fun RichTextElement( val splitText = text.split("◌").listIterator() // special BlueMoji character val formattedText = buildAnnotatedString { pushStyle(SpanStyle(MaterialTheme.colorScheme.onSurface)) + pushStyle(SpanStyle( + fontStyle = MaterialTheme.typography.bodyMedium.fontStyle, + fontWeight = FontWeight(275), + fontSize = MaterialTheme.typography.bodyMedium.fontSize, + )) append(splitText.next()) facets.fastForEach { facet -> val bounds = text.utf16FacetIndex(utf8Text, facet.start, facet.end) @@ -57,7 +62,7 @@ fun RichTextElement( is FacetType.ExternalLink -> { addStringAnnotation(tag = "Link", facetType.uri.uri, start, end) addStyle( - style = SpanStyle(MaterialTheme.colorScheme.tertiary), + style = SpanStyle(MaterialTheme.colorScheme.tertiary, fontWeight = FontWeight.Normal), start = start, end = end ) @@ -65,7 +70,7 @@ fun RichTextElement( is FacetType.PollBlueOption -> { addStringAnnotation(tag = "PollBlue", facetType.number.toString(), start, end) addStyle( - style = SpanStyle(MaterialTheme.colorScheme.tertiary), + style = SpanStyle(MaterialTheme.colorScheme.tertiary, fontWeight = FontWeight.Normal), start = start, end = end ) @@ -74,7 +79,7 @@ fun RichTextElement( is FacetType.Tag -> { addStringAnnotation(tag = "Tag", facetType.tag, start, end) addStyle( - style = SpanStyle(MaterialTheme.colorScheme.tertiary), + style = SpanStyle(MaterialTheme.colorScheme.tertiary, fontWeight = FontWeight.Normal), start = start, end = end ) @@ -82,7 +87,7 @@ fun RichTextElement( is FacetType.UserDidMention -> { addStringAnnotation(tag = "Mention", facetType.did.did, start, end) addStyle( - style = SpanStyle(MaterialTheme.colorScheme.tertiary), + style = SpanStyle(MaterialTheme.colorScheme.tertiary, fontWeight = FontWeight.Normal), start = start, end = end ) @@ -90,7 +95,7 @@ fun RichTextElement( is FacetType.UserHandleMention -> { addStringAnnotation(tag = "Mention", facetType.handle.handle, start, end) addStyle( - style = SpanStyle(MaterialTheme.colorScheme.tertiary), + style = SpanStyle(MaterialTheme.colorScheme.tertiary, fontWeight = FontWeight.Normal), start = start, end = end ) diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/elements/Wrappers.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/elements/Wrappers.kt index 8abe281..f7bf6a4 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/elements/Wrappers.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/elements/Wrappers.kt @@ -15,8 +15,17 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp @Composable -fun WrappedColumn(modifier: Modifier = Modifier, content: @Composable ColumnScope.() -> Unit) { - Column(modifier = modifier, content = content) +fun WrappedColumn( + modifier: Modifier = Modifier, + horizontalAlignment: Alignment.Horizontal = Alignment.CenterHorizontally, + verticalArrangement: Arrangement.Vertical = Arrangement.SpaceEvenly, + content: @Composable ColumnScope.() -> Unit +) { + Column( + horizontalAlignment = horizontalAlignment, + verticalArrangement = verticalArrangement, + modifier = modifier, + content = content) } @Composable diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/post/BlockedPostFragment.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/post/BlockedPostFragment.kt index ee4a302..2b428f9 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/post/BlockedPostFragment.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/post/BlockedPostFragment.kt @@ -10,6 +10,7 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.max import com.morpho.butterfly.AtUri @@ -39,7 +40,7 @@ fun BlockedPostFragment( SelectionContainer { Text( text = "Post by blocked or blocking user", - style = MaterialTheme.typography.titleMedium, + style = MaterialTheme.typography.bodyLarge.copy(fontWeight = FontWeight.ExtraLight), modifier = Modifier .padding(12.dp) .align(Alignment.CenterHorizontally) diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/post/FullPostFragment.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/post/FullPostFragment.kt index 695e321..dcd9811 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/post/FullPostFragment.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/post/FullPostFragment.kt @@ -31,13 +31,13 @@ import com.morpho.app.model.bluesky.LabelAction import com.morpho.app.model.bluesky.LabelScope import com.morpho.app.model.uidata.ContentHandling import com.morpho.app.model.uidata.LabelDescription +import com.morpho.app.model.uidata.LabelIcon import com.morpho.app.ui.elements.* import com.morpho.app.util.openBrowser import com.morpho.butterfly.AtIdentifier import com.morpho.butterfly.AtUri import com.morpho.butterfly.model.RecordType import kotlinx.collections.immutable.toImmutableList -import kotlinx.datetime.Instant import kotlinx.datetime.TimeZone import kotlinx.datetime.toLocalDateTime import org.jetbrains.compose.resources.ExperimentalResourceApi @@ -57,13 +57,14 @@ fun FullPostFragment( getContentHandling: (BskyPost) -> List = { listOf() } ) { val postDate = remember { post.createdAt.instant.toLocalDateTime(TimeZone.currentSystemDefault()).date } + val postTime = remember { post.createdAt.instant.toLocalDateTime(TimeZone.currentSystemDefault()).time } var menuExpanded by remember { mutableStateOf(false) } val contentHandling = remember { if (post.author.mutedByMe) { getContentHandling(post) + ContentHandling( scope = LabelScope.Content, id = "muted", - icon = Icons.Default.MoreHoriz, + icon = LabelIcon.EyeSlash(labelerAvatar = null), action = LabelAction.Blur, source = LabelDescription.YouMuted, ) @@ -147,7 +148,7 @@ fun FullPostFragment( Icon( imageVector = Icons.Default.MoreHoriz, contentDescription = "More", - tint = MaterialTheme.colorScheme.onSurface + tint = MaterialTheme.colorScheme.onSurfaceVariant ) } DisableSelection { PostMenu(menuExpanded, { @@ -195,12 +196,18 @@ fun FullPostFragment( }, ) } + val postTimestamp = remember { - val seconds = post.createdAt.instant.epochSeconds % 60 - Instant.fromEpochSeconds(post.createdAt.instant.epochSeconds - seconds) - .toLocalDateTime(TimeZone.currentSystemDefault()).time + // attmepts to cleanly handle 12-hour time while stripping seconds and sub-seconds + val string = postTime.toString() + if(string.contains("AM") || string.contains("PM")) { + val ampm = if(string.contains("AM")) "AM" else "PM" + val components = string.split(":") + "${components[0]}:${components[1]} $ampm" + } else { + string.substringBeforeLast(":") + } } - PostFeatureElement( post.feature, onItemClicked, contentHandling = contentHandling ) diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/post/NotFoundPostFragment.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/post/NotFoundPostFragment.kt index 6770b98..3e9ae80 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/post/NotFoundPostFragment.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/post/NotFoundPostFragment.kt @@ -10,6 +10,7 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.max import com.morpho.app.ui.elements.WrappedColumn @@ -40,7 +41,7 @@ fun NotFoundPostFragment( SelectionContainer { Text( text = "Post deleted or not found", - style = MaterialTheme.typography.titleMedium, + style = MaterialTheme.typography.bodyLarge.copy(fontWeight = FontWeight.ExtraLight), modifier = Modifier .padding(12.dp) .align(Alignment.CenterHorizontally) diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/post/PostActions.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/post/PostActions.kt index 14c093e..64a3e0b 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/post/PostActions.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/post/PostActions.kt @@ -103,7 +103,7 @@ inline fun PostAction( active: Boolean = false, ) { var clicked by rememberSaveable { mutableStateOf(active) } - val inactiveColor = MaterialTheme.colorScheme.onSurface + val inactiveColor = MaterialTheme.colorScheme.onSurfaceVariant var num by rememberSaveable { mutableLongStateOf(parameter) } val color = remember { mutableStateOf(if (clicked) activeColor else inactiveColor) } val icon = remember { mutableStateOf(if (clicked) iconActive else iconNormal) } @@ -123,8 +123,8 @@ inline fun PostAction( onUnClicked() } }, - modifier = Modifier - .padding(0.dp), + shape = MaterialTheme.shapes.small, + modifier = modifier, contentPadding = PaddingValues(0.dp) ) { Icon( @@ -135,10 +135,11 @@ inline fun PostAction( .size(20.dp) .padding(0.dp) ) + Text( - text = if (num > 0) num.toString() else "", + text = if (num > 0) "$num" else "", color = color.value, - modifier = Modifier.padding(start = 6.dp)//.offset(y=(-1).dp) + modifier = Modifier.padding(start = 6.dp) ) } } \ No newline at end of file diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/post/PostFragment.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/post/PostFragment.kt index 750c81f..aad921b 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/post/PostFragment.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/post/PostFragment.kt @@ -2,14 +2,11 @@ package com.morpho.app.ui.post import androidx.compose.foundation.ExperimentalFoundationApi -import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.* -import androidx.compose.foundation.shape.CornerSize import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.Reply -import androidx.compose.material.icons.filled.MoreHoriz import androidx.compose.material.icons.filled.Repeat import androidx.compose.material3.* import androidx.compose.runtime.Composable @@ -30,6 +27,7 @@ import com.atproto.repo.StrongRef import com.morpho.app.model.bluesky.* import com.morpho.app.model.uidata.ContentHandling import com.morpho.app.model.uidata.LabelDescription +import com.morpho.app.model.uidata.LabelIcon import com.morpho.app.ui.common.OnPostClicked import com.morpho.app.ui.elements.* import com.morpho.app.ui.lists.FeedListEntryFragment @@ -65,13 +63,13 @@ fun PostFragment( getContentHandling: (BskyPost) -> List = { listOf() } ) { val padding = remember { when(role) { - PostFragmentRole.Solo -> Modifier.padding(2.dp) + PostFragmentRole.Solo -> if(indentLevel == 0) Modifier.padding(2.dp) else Modifier PostFragmentRole.PrimaryThreadRoot -> Modifier.padding(2.dp) - PostFragmentRole.ThreadBranchStart -> Modifier.padding(start = 2.dp, top = 0.dp, end = 2.dp, bottom = 0.dp) - PostFragmentRole.ThreadBranchMiddle -> Modifier.padding(start = 2.dp, top = 0.dp, end = 2.dp, bottom = 0.dp) - PostFragmentRole.ThreadBranchEnd -> Modifier.padding(start = 2.dp, top = 0.dp, end = 2.dp, bottom = 0.dp) + PostFragmentRole.ThreadBranchStart -> Modifier.padding(start = 0.dp, top = 0.dp, end = 2.dp, bottom = 2.dp) + PostFragmentRole.ThreadBranchMiddle -> Modifier.padding(start = 0.dp, top = 0.dp, end = 2.dp, bottom = 2.dp) + PostFragmentRole.ThreadBranchEnd -> Modifier.padding(start = 0.dp, top = 0.dp, end = 2.dp, bottom = 2.dp) PostFragmentRole.ThreadRootUnfocused -> Modifier.padding(2.dp) - PostFragmentRole.ThreadEnd -> Modifier.padding(start = 2.dp, top = 0.dp, end = 2.dp, bottom = 0.dp) + PostFragmentRole.ThreadEnd -> Modifier.padding(start = 2.dp, top = 0.dp, end = 2.dp, bottom = 2.dp) }} val uriHandler = LocalUriHandler.current WrappedColumn(modifier = modifier.then(padding.fillMaxWidth())) { @@ -85,40 +83,14 @@ fun PostFragment( PostFragmentRole.ThreadRootUnfocused -> indentLevel.toFloat() PostFragmentRole.ThreadEnd -> 0.0f }} - val baseShape = MaterialTheme.shapes.small - val shape = when(role) { - PostFragmentRole.Solo -> baseShape - PostFragmentRole.PrimaryThreadRoot -> baseShape - PostFragmentRole.ThreadBranchStart -> { - baseShape.copy( - bottomEnd = CornerSize(0.dp), - bottomStart = CornerSize(0.dp), - ) } - PostFragmentRole.ThreadBranchMiddle -> { - baseShape.copy( - topEnd = CornerSize(0.dp), - topStart = CornerSize(0.dp), - bottomEnd = CornerSize(0.dp), - bottomStart = CornerSize(0.dp), - ) } - PostFragmentRole.ThreadBranchEnd -> { - baseShape.copy( - topEnd = CornerSize(0.dp), - topStart = CornerSize(0.dp), - ) } - PostFragmentRole.ThreadRootUnfocused -> baseShape - PostFragmentRole.ThreadEnd -> { - baseShape.copy( - topEnd = CornerSize(0.dp), - topStart = CornerSize(0.dp), - ) } - } + val interactionSource = remember { MutableInteractionSource() } val indication = remember { MorphoHighlightIndication() } - val bgColor = if (role == PostFragmentRole.ThreadEnd) { + val bgColor = if (role == PostFragmentRole.PrimaryThreadRoot) { MaterialTheme.colorScheme.background } else { - MaterialTheme.colorScheme.surfaceColorAtElevation(if (elevate || indentLevel > 0) 2.dp else 0.dp) + MaterialTheme.colorScheme.surfaceColorAtElevation(if (elevate ) 2.dp else + if (indentLevel > 0) (indentLevel*2).dp else 0.dp) } val contentHandling = remember { @@ -126,7 +98,7 @@ fun PostFragment( getContentHandling(post) + ContentHandling( scope = LabelScope.Content, id = "muted", - icon = Icons.Default.MoreHoriz, + icon = LabelIcon.EyeSlash(labelerAvatar = null), action = LabelAction.Blur, source = LabelDescription.YouMuted, ) @@ -136,13 +108,14 @@ fun PostFragment( } Surface ( - shadowElevation = if (elevate || indentLevel > 0) 1.dp else 0.dp, - tonalElevation = if ((elevate || indentLevel > 0) && role != PostFragmentRole.ThreadEnd) 2.dp else 0.dp, - shape = shape, + shadowElevation = if (elevate || indentLevel > 0) 2.dp else 0.dp, + tonalElevation = if (elevate && role != PostFragmentRole.ThreadEnd) 2.dp + else if (indentLevel > 0) (indentLevel*2).dp else 0.dp, + shape = MaterialTheme.shapes.small, + //color = bgColor, modifier = modifier .fillMaxWidth(indentLevel(indent)) .align(Alignment.End) - .background(bgColor, shape) .clickable( interactionSource = interactionSource, indication = indication, @@ -156,8 +129,8 @@ fun PostFragment( scope = LabelScope.Content, ) { Row( - modifier = Modifier.padding(bottom = 2.dp).padding(start = 0.dp, end = 6.dp) - .fillMaxWidth(indentLevel(indent)) + modifier = Modifier.padding(end = 6.dp) + .fillMaxWidth()//.fillMaxWidth(indentLevel(indent)) ) { if (indent < 2) { @@ -167,19 +140,19 @@ fun PostFragment( size = 45.dp, outlineColor = MaterialTheme.colorScheme.background, onClicked = { onProfileClicked(post.author.did) }, - avatarShape = AvatarShape.Corner + avatarShape = AvatarShape.Corner, + modifier = Modifier.padding(end = 2.dp) ) } Column( Modifier - .padding(top = 2.dp) - .padding(horizontal = 6.dp) - .fillMaxWidth(indentLevel(indent)), + .padding(top = 4.dp, start = 2.dp, end = 6.dp) + .fillMaxWidth()//.fillMaxWidth(indentLevel(indent)) ) { if (post.reason is BskyPostReason.BskyPostRepost) { Row( - modifier = Modifier, + modifier = Modifier.padding(start = 2.dp), verticalAlignment = Alignment.CenterVertically ) { Icon( @@ -190,7 +163,7 @@ fun PostFragment( ) Text( text = "Reposted by ${post.reason.repostAuthor.displayName}", - style = MaterialTheme.typography.bodySmall, + style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.secondary, modifier = Modifier.padding(start = 5.dp) ) @@ -198,7 +171,7 @@ fun PostFragment( } Row( - modifier = Modifier.padding(top = 4.dp).padding(horizontal = 4.dp), + modifier = Modifier.padding(top = 2.dp, start = 2.dp, end = 4.dp), horizontalArrangement = Arrangement.End ) { if (indent >= 2) { @@ -216,9 +189,7 @@ fun PostFragment( withStyle( style = SpanStyle( color = MaterialTheme.colorScheme.onSurface, - fontSize = MaterialTheme.typography.labelLarge.fontSize.times( - 1.2f - ), + fontSize = MaterialTheme.typography.labelLarge.fontSize, fontWeight = FontWeight.Medium ) ) { @@ -228,7 +199,7 @@ fun PostFragment( style = SpanStyle( color = MaterialTheme.colorScheme.onSurfaceVariant, fontSize = MaterialTheme.typography.labelLarge.fontSize.times( - 1.0f + 0.8f ) ) ) { @@ -351,7 +322,7 @@ internal inline fun ReplyIndicator( ) Text( text = stringResource(Res.string.replyIndicator, parent.author.handle), - style = MaterialTheme.typography.bodySmall, + style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.secondary, modifier = Modifier.padding(start = 5.dp) ) diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/post/PostLinkEmbed.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/post/PostLinkEmbed.kt index 96883df..df1a174 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/post/PostLinkEmbed.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/post/PostLinkEmbed.kt @@ -4,6 +4,7 @@ import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.ExperimentalLayoutApi import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.CornerSize import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.material3.Text @@ -37,7 +38,7 @@ fun PostLinkEmbed( Surface( shape = MaterialTheme.shapes.extraSmall, tonalElevation = 3.dp, - shadowElevation = 1.dp, + shadowElevation = 2.dp, modifier = modifier //border = BorderStroke(1.dp,MaterialTheme.colorScheme.secondary) ) { @@ -55,7 +56,8 @@ fun PostLinkEmbed( modifier = Modifier .fillMaxWidth() .align(Alignment.CenterHorizontally) - .clip(MaterialTheme.shapes.extraSmall) + .clip(MaterialTheme.shapes.extraSmall + .copy(bottomEnd = CornerSize(0.dp), bottomStart = CornerSize(0.dp))) .clickable { linkPress(linkData.uri.uri) } ) WrappedColumn( @@ -63,15 +65,17 @@ fun PostLinkEmbed( ) { Text( text = linkData.title, - style = MaterialTheme.typography.titleMedium, + style = MaterialTheme.typography.titleSmall, modifier = Modifier.padding(8.dp) ) - val bskyTxt = remember { makeBlueskyText(linkData.description) } - RichTextElement( - text = bskyTxt.text, - facets = bskyTxt.facets, - modifier = Modifier.padding(start = 8.dp, end = 8.dp, top = 4.dp, bottom = 8.dp) - ) + if(linkData.description.isNotEmpty()) { + val bskyTxt = remember { makeBlueskyText(linkData.description) } + RichTextElement( + text = bskyTxt.text, + facets = bskyTxt.facets, + modifier = Modifier.padding(start = 8.dp, end = 8.dp, top = 4.dp, bottom = 8.dp) + ) + } } } diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/theme/Color.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/theme/Color.kt index 7bd91b5..6dd94c7 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/theme/Color.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/theme/Color.kt @@ -7,13 +7,13 @@ import kotlin.math.max import kotlin.math.min -val morphoLightPrimary = Color(0xffb05cce) +val morphoLightPrimary =Color(0xff5079be) val morphoLightOnPrimary = Color(0xffdde2e7) -val morphoLightPrimaryContainer = Color(0xffbf75d6) +val morphoLightPrimaryContainer = Color(0xff95b4ea) val morphoLightOnPrimaryContainer = Color(0xff3a3746) -val morphoLightSecondary = Color(0xff5079be) +val morphoLightSecondary = Color(0xffb05cce) val morphoLightOnSecondary = Color(0xffc5cdd9) -val morphoLightSecondaryContainer = Color(0xff6996e0) +val morphoLightSecondaryContainer = Color(0xffcea1de) val morphoLightOnSecondaryContainer = Color(0xff313a44) val morphoLightTertiary = Color(0xff608e32) val morphoLightOnTertiary = Color(0xffe5eee4) @@ -35,32 +35,32 @@ val morphoLightInverseSurface = Color(0xff2a2b34) val morphoLightPrimaryInverse = Color(0xff2c1635) val morphoLightSurfaceDim = Color(0xffBAC3CB) -val morphoDarkPrimary = Color(0xffd38aea) -val morphoDarkOnPrimary = Color(0xff202023) -val morphoDarkPrimaryContainer = Color(0xffd38aea) -val morphoDarkOnPrimaryContainer = Color(0xffc5cdd9) -val morphoDarkSecondary = Color(0xff6cb6eb) -val morphoDarkOnSecondary = Color(0xff354157) -val morphoDarkSecondaryContainer = Color(0xff5cb6eb) +val morphoDarkPrimary = Color(0xff7f93e8) +val morphoDarkOnPrimary = Color(0xff242934) +val morphoDarkPrimaryContainer = Color(0xff6c8aeb) +val morphoDarkOnPrimaryContainer = Color(0xff22222d) +val morphoDarkSecondary = Color(0xffd38aea) +val morphoDarkOnSecondary = Color(0xff3f3557) +val morphoDarkSecondaryContainer =Color(0xff8f5da1) val morphoDarkOnSecondaryContainer = Color(0xffc5cdd9) val morphoDarkTertiary = Color(0xffa0c980) val morphoDarkOnTertiary = Color(0xff394634) val morphoDarkTertiaryContainer = Color(0xffa0c980) -val morphoDarkOnTertiaryContainer = Color(0xffc2bcea) +val morphoDarkOnTertiaryContainer = Color(0xffc6eabc) val morphoDarkError = Color(0xffec7279) val morphoDarkErrorContainer = Color(0xff55393d) val morphoDarkOnError = Color(0xff55393d) val morphoDarkOnErrorContainer = Color(0xffec7279) val morphoDarkBackground = Color(0xff2c2e34) -val morphoDarkOnBackground = Color(0xFFEAE1D9) +val morphoDarkOnBackground = Color(0xffb6b5c2) val morphoDarkSurface = Color(0xff33353f) -val morphoDarkOnSurface = Color(0xffd9eadb) +val morphoDarkOnSurface = Color(0xfff6f6f6) val morphoDarkSurfaceVariant = Color(0xff414550) val morphoDarkOnSurfaceVariant = Color(0xffc5cdd9) val morphoDarkOutline = Color(0xff80849c) val morphoDarkInverseOnSurface = Color(0xff535c6a) val morphoDarkSurfaceDim = Color(0xff24262a) -val morphoDarkInverseSurface = Color(0xffead9ea) +val morphoDarkInverseSurface = Color(0xffd7dee3) val morphoDarkPrimaryInverse = Color(0xff492452) fun Color.contrastAgainst(background: Color): Float { diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/thread/ThreadFragment.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/thread/ThreadFragment.kt index 3fdf3fa..3244627 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/thread/ThreadFragment.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/thread/ThreadFragment.kt @@ -105,6 +105,7 @@ fun ThreadFragment( indentLevel = 1, reason = reason, elevate = true, + modifier = Modifier.padding(horizontal = 4.dp, vertical = 1.dp), onItemClicked = onItemClicked, onProfileClicked = onProfileClicked, onUnClicked = onUnClicked, @@ -119,6 +120,7 @@ fun ThreadFragment( item { ThreadItem( item = threadPost, + indentLevel = 1, role = PostFragmentRole.PrimaryThreadRoot, reason = thread.post.reason, onItemClicked = onItemClicked, @@ -159,7 +161,7 @@ fun ThreadFragment( item = threadPost, role = PostFragmentRole.PrimaryThreadRoot, reason = thread.post.reason, - modifier = Modifier.padding(vertical = 4.dp), + modifier = Modifier.padding(vertical = 2.dp), onItemClicked = onItemClicked, onProfileClicked = onProfileClicked, onUnClicked = onUnClicked, @@ -173,39 +175,40 @@ fun ThreadFragment( } if (hasReplies){ replies.fastForEach { reply -> - if (reply.replies.isNotEmpty()) { - item { - ThreadTree( - reply = reply, modifier = Modifier.padding(4.dp), - indentLevel = 1, - comparator = comparator, - onItemClicked = {onItemClicked(it) }, - onProfileClicked = { onProfileClicked(it) }, - onUnClicked = { type,uri-> onUnClicked(type,uri) }, - onRepostClicked = { onRepostClicked(it) }, - onReplyClicked = { onReplyClicked(it) }, - onMenuClicked = { option, post -> onMenuClicked(option, post) }, - onLikeClicked = { onLikeClicked(it) }, - getContentHandling = { getContentHandling(it) } - ) - } - } else { - item { - ThreadItem( - item = reply, role = PostFragmentRole.Solo, indentLevel = 1, - modifier = Modifier.padding(4.dp), - onItemClicked = onItemClicked, - onProfileClicked = onProfileClicked, - onUnClicked = onUnClicked, - onRepostClicked = onRepostClicked, - onReplyClicked = onReplyClicked, - onMenuClicked = onMenuClicked, - onLikeClicked = onLikeClicked, - getContentHandling = getContentHandling - ) - } + if (reply.replies.isNotEmpty()) { + item { + ThreadTree( + reply = reply, + modifier = Modifier.padding(vertical = 1.dp, horizontal = 3.dp), + indentLevel = 1, + comparator = comparator, + onItemClicked = {onItemClicked(it) }, + onProfileClicked = { onProfileClicked(it) }, + onUnClicked = { type,uri-> onUnClicked(type,uri) }, + onRepostClicked = { onRepostClicked(it) }, + onReplyClicked = { onReplyClicked(it) }, + onMenuClicked = { option, post -> onMenuClicked(option, post) }, + onLikeClicked = { onLikeClicked(it) }, + getContentHandling = { getContentHandling(it) } + ) } - + } else { + item { + ThreadItem( + item = reply, role = PostFragmentRole.Solo, indentLevel = 0, + elevate = true, + modifier = Modifier.padding(horizontal = 3.dp, vertical = 1.dp), + onItemClicked = onItemClicked, + onProfileClicked = onProfileClicked, + onUnClicked = onUnClicked, + onRepostClicked = onRepostClicked, + onReplyClicked = onReplyClicked, + onMenuClicked = onMenuClicked, + onLikeClicked = onLikeClicked, + getContentHandling = getContentHandling + ) + } + } } } } diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/thread/ThreadReply.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/thread/ThreadReply.kt index 0f04da6..7e393e1 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/thread/ThreadReply.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/thread/ThreadReply.kt @@ -33,7 +33,9 @@ inline fun ThreadReply( ) { when(item) { is ThreadPost.ViewablePost -> { - val r = if (item.replies.isEmpty()) { + val r = if (role == PostFragmentRole.ThreadBranchStart || role == PostFragmentRole.Solo) { + role + } else if (item.replies.isEmpty()) { PostFragmentRole.ThreadBranchEnd } else { PostFragmentRole.ThreadBranchMiddle @@ -43,6 +45,7 @@ inline fun ThreadReply( role = r, indentLevel = indentLevel, modifier = modifier, + elevate = r != PostFragmentRole.ThreadBranchStart, onItemClicked = {onItemClicked(it) }, onProfileClicked = { onProfileClicked(it) }, onUnClicked = { type,uri-> onUnClicked(type,uri) }, diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/thread/ThreadTree.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/thread/ThreadTree.kt index 93097a7..6c0d1f4 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/thread/ThreadTree.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/thread/ThreadTree.kt @@ -1,6 +1,5 @@ package com.morpho.app.ui.thread -import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyScopeMarker @@ -10,11 +9,14 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.drawBehind import androidx.compose.ui.geometry.Offset -import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.geometry.Size import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.StrokeCap +import androidx.compose.ui.graphics.drawscope.Stroke import androidx.compose.ui.unit.dp -import androidx.compose.ui.util.fastForEach +import androidx.compose.ui.util.fastForEachIndexed import com.atproto.repo.StrongRef import com.morpho.app.model.bluesky.BskyPost import com.morpho.app.model.bluesky.ThreadPost @@ -36,7 +38,7 @@ fun ThreadTree( indentLevel: Int = 1, comparator: Comparator = compareBy { if (it is ThreadPost.ViewablePost) { - it.post.indexedAt.instant.epochSeconds + it.post.createdAt.instant.epochSeconds } else { it.hashCode().toLong() } @@ -48,15 +50,35 @@ fun ThreadTree( onLikeClicked: (StrongRef) -> Unit = { }, onMenuClicked: (MenuOptions, BskyPost) -> Unit = { _, _ -> }, onUnClicked: (type: RecordType, uri: AtUri) -> Unit = { _, _ -> }, - getContentHandling: (BskyPost) -> List = { listOf() } + getContentHandling: (BskyPost) -> List = { listOf() }, + end: Boolean = false, ) { - + val lineColour = MaterialTheme.colorScheme.onTertiaryContainer//.copy(alpha = 0.8f) + val lineColour2 = MaterialTheme.colorScheme.outline//.copy(alpha = 0.8f) + val bgColour = MaterialTheme.colorScheme.background if(reply is ThreadPost.ViewablePost) { if (reply.replies.isEmpty()) { ThreadReply( - item = reply, role = PostFragmentRole.Solo, indentLevel = indentLevel, - modifier = Modifier.padding(top = 2.dp), + item = reply, role = PostFragmentRole.ThreadBranchEnd, indentLevel = indentLevel, + modifier = if(indentLevel > 1) Modifier + .drawBehind { + drawLine( + color = lineColour, + cap = StrokeCap.Butt, + start = Offset(9.dp.toPx(), 22.dp.toPx()), + end = Offset(100.dp.toPx(), 22.dp.toPx()), + strokeWidth = Stroke.HairlineWidth + ) + if(end) { + drawRect( + color = bgColour, + topLeft = Offset(4.dp.toPx(), 23.dp.toPx()), + size = Size(100.dp.toPx(), size.height - 23.dp.toPx()), + ) + } + }.padding(top = 2.dp, start = 6.dp,) + else Modifier.padding(top = 2.dp, start = 6.dp, bottom = 2.dp), onItemClicked = onItemClicked, onProfileClicked = onProfileClicked, onUnClicked = onUnClicked, @@ -69,43 +91,56 @@ fun ThreadTree( } else { val replies = remember { reply.replies.sortedWith(comparator) } WrappedColumn( - modifier = modifier - .fillMaxWidth() - .padding(top = 2.dp) - + modifier = if(indentLevel == 1) modifier.fillMaxWidth().padding(start = 0.dp, end = 4.dp) + else modifier.fillMaxWidth().padding(start = 1.dp, bottom = 2.dp) ) { - val lineColour = if (indentLevel % 4 == 0) { - MaterialTheme.colorScheme.tertiary.copy(0.7f) - } else if (indentLevel % 2 == 0) { - MaterialTheme.colorScheme.secondary.copy(0.7f) - } else { - MaterialTheme.colorScheme.primary.copy(0.7f) - } + Surface( - //shadowElevation = if (indentLevel > 0) 1.dp else 0.dp, - border = BorderStroke( - 1.dp, Brush.sweepGradient( - 0.0f to Color.Transparent, 0.2f to Color.Transparent, - 0.4f to lineColour, 0.7f to lineColour, - 0.9f to Color.Transparent, - center = Offset(100f, 500f) - ) - ), - tonalElevation = 2.dp, + //shadowElevation = if (indentLevel % 2 > 0) 2.dp else 0.dp, + tonalElevation = if(replies.size > 1) (indentLevel*2).dp else 0.dp, + //border = BorderStroke(1.dp, MaterialTheme.colorScheme.secondaryContainer), + color = Color.Transparent, + //color = if(replies.size > 1) MaterialTheme.colorScheme.surfaceColorAtElevation((indentLevel*2).dp) + //else Color.Transparent, shape = MaterialTheme.shapes.small, - modifier = Modifier - .fillMaxWidth(indentLevel(indentLevel / 2.0f)) + modifier = Modifier.fillMaxWidth(indentLevel(indentLevel / 2.0f)) .align(Alignment.End) ) { WrappedColumn( + horizontalAlignment = Alignment.End, + modifier = if(replies.size > 1) Modifier.fillMaxWidth() + .drawBehind { + if(!end) + drawLine( + color = lineColour, + cap = StrokeCap.Butt, + start = Offset(8.dp.toPx(), 10.dp.toPx()), + end = Offset(8.dp.toPx(), size.height - 22.dp.toPx()), + strokeWidth = Stroke.HairlineWidth + ) + } else Modifier.fillMaxWidth() + .drawBehind { + if(replies.size == 1) + drawLine( + color = lineColour2, + cap = StrokeCap.Butt, + start = Offset(12.dp.toPx(), 6.dp.toPx()), + end = Offset(12.dp.toPx(), size.height - 22.dp.toPx()), + strokeWidth = 2.dp.toPx(), + ) + } + + ) { ThreadReply( item = reply, role = PostFragmentRole.ThreadBranchStart, - indentLevel = indentLevel, - modifier = Modifier.padding(top = 2.dp), + indentLevel = 1, + modifier = if(replies.size > 1) Modifier.padding(start = 2.dp, top = 2.dp) + else if(replies.size == 1) Modifier.padding(start = 1.dp, top = 1.dp) + else Modifier, onItemClicked = {onItemClicked(it) }, onProfileClicked = { onProfileClicked(it) }, onUnClicked = { type,uri-> onUnClicked(type,uri) }, @@ -115,10 +150,60 @@ fun ThreadTree( onLikeClicked = { onLikeClicked(it) }, getContentHandling = { getContentHandling(it) } ) + if(replies.size > 1) { + Surface( + color = Color.Transparent, + //shadowElevation = if (indentLevel > 0) 2.dp else 0.dp, + //tonalElevation = (indentLevel*2).dp, + //border = BorderStroke(1.dp, MaterialTheme.colorScheme.tertiaryContainer), + //color = MaterialTheme.colorScheme.surfaceColorAtElevation((indentLevel*2).dp), + shape = MaterialTheme.shapes.small, + modifier = Modifier.padding(top = 2.dp, start = 0.dp) + .fillMaxWidth() + ) { + WrappedColumn( - replies.fastForEach { reply -> - ThreadTree( - reply = reply, modifier = modifier, indentLevel = indentLevel, + modifier = Modifier.fillMaxWidth() + ) { + replies.fastForEachIndexed { index,reply -> + ThreadTree( + reply = reply, + modifier = Modifier.drawBehind { + drawLine( + color = lineColour, + cap = StrokeCap.Butt, + start = Offset(9.dp.toPx(), 20.dp.toPx()), + end = Offset(100.dp.toPx(), 20.dp.toPx()), + strokeWidth = Stroke.HairlineWidth + ) + if(index == replies.lastIndex) { + drawRect( + color = bgColour, + topLeft = Offset(4.dp.toPx(), 21.dp.toPx()), + size = Size(100.dp.toPx(), size.height - 21.dp.toPx()), + ) + } + }.padding(start = 3.dp), + indentLevel = indentLevel + 1, + onItemClicked = { onItemClicked(it) }, + onProfileClicked = { onProfileClicked(it) }, + onUnClicked = { type, uri -> onUnClicked(type, uri) }, + onRepostClicked = { onRepostClicked(it) }, + onReplyClicked = { onReplyClicked(it) }, + onMenuClicked = { option, p -> onMenuClicked(option, p) }, + onLikeClicked = { onLikeClicked(it) }, + getContentHandling = { getContentHandling(it) }, + end = index == replies.lastIndex + ) + } + } + } + } else if(replies.size == 1) { + ThreadReply( + item = replies.first(), + role = PostFragmentRole.ThreadBranchEnd, + indentLevel = indentLevel, + modifier = Modifier.padding(start = 4.dp, top = 2.dp), onItemClicked = { onItemClicked(it) }, onProfileClicked = { onProfileClicked(it) }, onUnClicked = { type, uri -> onUnClicked(type, uri) }, @@ -126,8 +211,8 @@ fun ThreadTree( onReplyClicked = { onReplyClicked(it) }, onMenuClicked = { option, p -> onMenuClicked(option, p) }, onLikeClicked = { onLikeClicked(it) }, - getContentHandling = { getContentHandling(it) } ) + } } } diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/util/Savers.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/util/Savers.kt index 80331ac..323ace5 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/util/Savers.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/util/Savers.kt @@ -13,10 +13,6 @@ import kotlinx.serialization.descriptors.SerialDescriptor import kotlinx.serialization.encoding.Decoder import kotlinx.serialization.encoding.Encoder -// commonMain - module core -@Suppress("EXPECT_ACTUAL_CLASSIFIERS_ARE_IN_BETA_WARNING") -expect interface JavaSerializable - val atUriSaver: Saver = listSaver( save = { listOf(it.atUri)}, restore = { diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/util/encodings.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/util/encodings.kt index 5ee51a6..db25887 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/util/encodings.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/util/encodings.kt @@ -36,7 +36,7 @@ fun Byte.isSingleByte(): Boolean { } fun String.utf16FacetIndex(utf8: ByteString, start: Int, end: Int): Pair { - val utf8FacetText = utf8.substring(start, end) + val utf8FacetText = utf8.substring(max(0, start), min(utf8.size-1, end)) //println("utf8FacetText: '${utf8FacetText.utf8()}'") //println("utf8Start: ${utf8.indexOf(utf8FacetText)}, utf8End: ${utf8.indexOf(utf8FacetText) + utf8FacetText.size}") val utf16FacetText = utf8FacetText.utf8() @@ -48,7 +48,7 @@ fun String.utf16FacetIndex(utf8: ByteString, start: Int, end: Int): Pair { val utf8 = this.encodeUtf8() - val utf8FacetText = utf8.substring(start, end-1) + val utf8FacetText = utf8.substring(max(0, start), min(utf8.size-1, end)) //println("utf8FacetText: '${utf8FacetText.utf8()}'") //println("utf8Start: ${utf8.indexOf(utf8FacetText)}, utf8End: ${utf8.indexOf(utf8FacetText) + utf8FacetText.size}") val utf16FacetText = utf8FacetText.utf8() diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/util/time.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/util/time.kt index f6893fd..0abee60 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/util/time.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/util/time.kt @@ -1,12 +1,7 @@ package com.morpho.app.util -import kotlinx.datetime.Clock -import kotlinx.datetime.TimeZone -import kotlinx.datetime.minus -import kotlinx.datetime.toInstant -import kotlinx.datetime.toLocalDateTime -import kotlinx.datetime.todayIn import com.morpho.app.model.uidata.Moment +import kotlinx.datetime.* fun getFormattedDateTimeSince(moment: Moment): String { val postDate = moment.instant.toLocalDateTime(TimeZone.currentSystemDefault()).date @@ -19,19 +14,21 @@ fun getFormattedDateTimeSince(moment: Moment): String { deltaTime.toComponents { hours, minutes, seconds, _ -> return when { deltaDays >= 180 -> { - moment.instant.toLocalDateTime(TimeZone.currentSystemDefault()).date.toString() + postTime.date.toString() } - deltaDays >= 1 -> { - - "${if(dateDiff.years > 0) "${dateDiff.years} yrs " else ""}${if(dateDiff.months > 0) "${dateDiff.months} months " else ""}${if(dateDiff.days > 0 && dateDiff.months == 0) "${dateDiff.days} days " else ""}ago" + dateDiff.months > 0 -> { + "${dateDiff.months} months ago" + } + dateDiff.days > 0 -> { + "${dateDiff.days} days ago" } (deltaDays == 0 && hours >= 12)-> { "$hours h ago" } - (deltaDays == 0 && hours >= 1)-> { - "${hours}:${minutes} ago" + (deltaDays == 0 && hours >= 2)-> { + "$hours h $minutes m ago" } - (deltaDays == 0 && hours.toInt() == 0 && minutes > 1) -> { + (deltaDays == 0 && hours.toInt() <= 1 && minutes > 1) -> { "$minutes m ago" } (deltaDays == 0 && hours.toInt() == 0 && minutes == 0) -> { diff --git a/Morpho/composeApp/src/desktopMain/kotlin/com/morpho/app/util/Savers.desktop.kt b/Morpho/composeApp/src/desktopMain/kotlin/com/morpho/app/util/Savers.desktop.kt index b3464f5..48368fc 100644 --- a/Morpho/composeApp/src/desktopMain/kotlin/com/morpho/app/util/Savers.desktop.kt +++ b/Morpho/composeApp/src/desktopMain/kotlin/com/morpho/app/util/Savers.desktop.kt @@ -1,4 +1,2 @@ package com.morpho.app.util -// commonMain - module core -actual interface JavaSerializable \ No newline at end of file diff --git a/Morpho/gradle/libs.versions.toml b/Morpho/gradle/libs.versions.toml index 888aeee..374e77d 100644 --- a/Morpho/gradle/libs.versions.toml +++ b/Morpho/gradle/libs.versions.toml @@ -22,13 +22,13 @@ junit = "4.13.2" jwt = "1.2.6" kjwt = "0.9.0" kmmViewmodelCore = "1.0.0-ALPHA-20" -kotlin = "2.0.10" -ksp-version = "2.0.10-1.0.24" +kotlin = "2.0.20" +ksp-version = "2.0.20-1.0.24" koin-bom = "3.5.3" koin-ksp = "1.3.1" koin-compose = "1.1.2" kotlinJwt = "1.3.1" -kotlin-reflect = "2.0.10" +kotlin-reflect = "2.0.20" kotlin-gradle-plugin = "1.9.0" kotlinx-serialization = "1.6.3" kotlinx-datetime = "0.6.0" diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 855889e..61f0758 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -22,13 +22,13 @@ junit = "4.13.2" jwt = "1.2.6" kjwt = "0.9.0" kmmViewmodelCore = "1.0.0-ALPHA-20" -kotlin = "2.0.10" +kotlin = "2.0.20" kotlinJwt = "1.3.1" -ksp-version = "2.0.10-1.0.24" +ksp-version = "2.0.20-1.0.24" koin-bom = "3.5.3" koin-ksp = "1.3.1" koin-compose = "1.1.2" -kotlin-reflect = "2.0.10" +kotlin-reflect = "2.0.20" kotlin-gradle-plugin = "2.0.10" kotlinx-serialization = "1.6.3" kotlinx-datetime = "0.6.0" From 3cd820ce64c500d583b24578c57ec30ea9a1f91b Mon Sep 17 00:00:00 2001 From: Orual Date: Fri, 13 Sep 2024 23:02:04 -0400 Subject: [PATCH 06/42] Ok, session refresh definitely works now. Working on a refactor of the api client to something more like the Agent model in the official client. Think it ends up simplifying the application code a fair bit going forward. --- .../commonMain/kotlin/com/morpho/app/ui/elements/RichText.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/elements/RichText.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/elements/RichText.kt index 3d0669e..f818e47 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/elements/RichText.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/elements/RichText.kt @@ -49,7 +49,7 @@ fun RichTextElement( pushStyle(SpanStyle(MaterialTheme.colorScheme.onSurface)) pushStyle(SpanStyle( fontStyle = MaterialTheme.typography.bodyMedium.fontStyle, - fontWeight = FontWeight(275), + fontWeight = MaterialTheme.typography.bodyMedium.fontWeight,//FontWeight(275), fontSize = MaterialTheme.typography.bodyMedium.fontSize, )) append(splitText.next()) From 8b87178da57286a05f6e35de928111b496e9ab22 Mon Sep 17 00:00:00 2001 From: Orual Date: Sat, 14 Sep 2024 15:39:37 -0400 Subject: [PATCH 07/42] Reverted a change around ScreenModel/ViewModel It was running into a bug in Voyager which seemingly didn't have a fix, only open issues. Likely also helps the Parecelize/Parcelable nonsense I was dealing with before. Do need to make sure of state saving on mobile, however. --- .../commonMain/kotlin/com/morpho/app/App.kt | 38 +++++++++-------- .../app/screens/base/BaseScreenModel.kt | 10 ++--- .../app/screens/base/tabbed/NavigationTabs.kt | 10 ++--- .../screens/base/tabbed/TabbedBaseScreen.kt | 5 +-- .../morpho/app/screens/login/LoginScreen.kt | 16 +++---- .../app/screens/login/LoginScreenModel.kt | 4 +- .../app/screens/main/MainScreenModel.kt | 42 +++++++++---------- .../app/screens/main/tabbed/TabbedHomeView.kt | 9 ++-- .../main/tabbed/TabbedMainScreenModel.kt | 6 +-- .../notifications/NotificationsView.kt | 8 ++-- .../TabbedNotificationScreenModel.kt | 4 +- .../app/screens/profile/TabbedProfileView.kt | 9 ++-- .../screens/profile/TabbedProfileViewModel.kt | 4 +- .../morpho/app/screens/thread/ThreadView.kt | 4 +- .../app/ui/common/TabbedSkylineFragment.kt | 4 +- .../composeApp/src/desktopMain/kotlin/main.kt | 19 +++++---- 16 files changed, 100 insertions(+), 92 deletions(-) diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/App.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/App.kt index d653110..2ca9d7f 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/App.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/App.kt @@ -4,6 +4,8 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue +import cafe.adriel.voyager.core.annotation.ExperimentalVoyagerApi +import cafe.adriel.voyager.jetpack.ProvideNavigatorLifecycleKMPSupport import cafe.adriel.voyager.navigator.tab.CurrentTab import cafe.adriel.voyager.navigator.tab.TabDisposable import cafe.adriel.voyager.navigator.tab.TabNavigator @@ -13,32 +15,34 @@ import com.morpho.app.screens.login.LoginScreen import org.jetbrains.compose.resources.ExperimentalResourceApi import org.jetbrains.compose.ui.tooling.preview.Preview import org.koin.compose.KoinContext -import org.koin.compose.koinInject +import org.koin.compose.getKoin -@OptIn(ExperimentalResourceApi::class) +@OptIn(ExperimentalResourceApi::class, ExperimentalVoyagerApi::class) @Composable @Preview fun App() { KoinContext { MaterialTheme { - val screenModel = koinInject() - val loggedIn by derivedStateOf { screenModel.isLoggedIn } + ProvideNavigatorLifecycleKMPSupport { + val screenModel = getKoin().get() + val loggedIn by derivedStateOf { screenModel.isLoggedIn } - TabNavigator( - tab = if(loggedIn) { - TabbedBaseScreen - } else { - LoginScreen - }, - tabDisposable = { - TabDisposable( - navigator = it, - tabs = listOf(TabbedBaseScreen, LoginScreen) - ) + TabNavigator( + tab = if (loggedIn) { + TabbedBaseScreen + } else { + LoginScreen + }, + tabDisposable = { + TabDisposable( + navigator = it, + tabs = listOf(TabbedBaseScreen, LoginScreen) + ) + } + ) { + CurrentTab() } - ) { - CurrentTab() } } } diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/base/BaseScreenModel.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/base/BaseScreenModel.kt index bba0016..0271a09 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/base/BaseScreenModel.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/base/BaseScreenModel.kt @@ -1,7 +1,7 @@ package com.morpho.app.screens.base -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope +import cafe.adriel.voyager.core.model.ScreenModel +import cafe.adriel.voyager.core.model.screenModelScope import com.morpho.app.data.PreferencesRepository import com.morpho.app.model.bluesky.BskyPost import com.morpho.app.model.uidata.BskyNotificationService @@ -18,7 +18,7 @@ import org.koin.core.component.KoinComponent import org.koin.core.component.inject import org.lighthousegames.logging.logging -open class BaseScreenModel : ViewModel(), KoinComponent { +open class BaseScreenModel : ScreenModel, KoinComponent { val api: Butterfly by inject() val preferences: PreferencesRepository by inject() val notifService: BskyNotificationService by inject() @@ -31,11 +31,11 @@ open class BaseScreenModel : ViewModel(), KoinComponent { val log = logging() } - fun createRecord(record: RecordUnion) = viewModelScope.launch(Dispatchers.IO) { + fun createRecord(record: RecordUnion) = screenModelScope.launch(Dispatchers.IO) { api.createRecord(record) } - fun deleteRecord(type: RecordType, rkey: AtUri) = viewModelScope.launch(Dispatchers.IO) { + fun deleteRecord(type: RecordType, rkey: AtUri) = screenModelScope.launch(Dispatchers.IO) { api.deleteRecord(type, rkey) } diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/base/tabbed/NavigationTabs.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/base/tabbed/NavigationTabs.kt index 47d31cc..bd3339c 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/base/tabbed/NavigationTabs.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/base/tabbed/NavigationTabs.kt @@ -7,13 +7,13 @@ import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.* import androidx.compose.ui.Modifier -import androidx.lifecycle.viewModelScope import cafe.adriel.voyager.core.annotation.ExperimentalVoyagerApi import cafe.adriel.voyager.core.lifecycle.LifecycleEffectOnce +import cafe.adriel.voyager.core.model.rememberNavigatorScreenModel +import cafe.adriel.voyager.core.model.screenModelScope import cafe.adriel.voyager.core.screen.Screen import cafe.adriel.voyager.core.screen.ScreenKey import cafe.adriel.voyager.core.screen.uniqueScreenKey -import cafe.adriel.voyager.jetpack.navigatorViewModel import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.Navigator import cafe.adriel.voyager.navigator.currentOrThrow @@ -75,7 +75,7 @@ data class HomeTab( @OptIn(ExperimentalVoyagerApi::class) @Composable override fun Content() { - val sm = navigatorViewModel { TabbedMainScreenModel() } + val sm = LocalNavigator.currentOrThrow.rememberNavigatorScreenModel { TabbedMainScreenModel() } TabbedHomeView(sm) } @@ -233,10 +233,10 @@ data class ThreadTab( @Composable override fun Content() { val navigator = LocalNavigator.currentOrThrow - val sm = navigatorViewModel { TabbedMainScreenModel() } + val sm = navigator.rememberNavigatorScreenModel { TabbedMainScreenModel() } var threadState: StateFlow? by remember { mutableStateOf(null)} LifecycleEffectOnce { - sm.viewModelScope.launch { threadState = sm.loadThread(uri) } + sm.screenModelScope.launch { threadState = sm.loadThread(uri) } } if(threadState != null) { ThreadViewContent(threadState!!, navigator) diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/base/tabbed/TabbedBaseScreen.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/base/tabbed/TabbedBaseScreen.kt index ef6b388..b56639a 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/base/tabbed/TabbedBaseScreen.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/base/tabbed/TabbedBaseScreen.kt @@ -15,7 +15,6 @@ import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import cafe.adriel.voyager.core.annotation.ExperimentalVoyagerApi import cafe.adriel.voyager.core.screen.ScreenKey -import cafe.adriel.voyager.jetpack.ProvideNavigatorLifecycleKMPSupport import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.Navigator import cafe.adriel.voyager.navigator.NavigatorDisposeBehavior @@ -40,7 +39,7 @@ data object TabbedBaseScreen: Tab { @OptIn(ExperimentalMaterial3Api::class, ExperimentalVoyagerApi::class) @Composable override fun Content() { - ProvideNavigatorLifecycleKMPSupport { + //ProvideNavigatorLifecycleKMPSupport { Navigator( HomeTab("startHome"), disposeBehavior = NavigatorDisposeBehavior( @@ -50,7 +49,7 @@ data object TabbedBaseScreen: Tab { /*LaunchedEffect(Unit) { navigator.replaceAll(HomeTab("startHome2")) }*/ SlideTabTransition(navigator) } - } + //} } diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/login/LoginScreen.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/login/LoginScreen.kt index 7f9814e..9c61ea5 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/login/LoginScreen.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/login/LoginScreen.kt @@ -13,9 +13,11 @@ import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.PasswordVisualTransformation import androidx.compose.ui.unit.dp -import androidx.lifecycle.viewModelScope -import androidx.lifecycle.viewmodel.compose.viewModel +import cafe.adriel.voyager.core.model.rememberNavigatorScreenModel +import cafe.adriel.voyager.core.model.screenModelScope import cafe.adriel.voyager.core.screen.ScreenKey +import cafe.adriel.voyager.navigator.LocalNavigator +import cafe.adriel.voyager.navigator.currentOrThrow import cafe.adriel.voyager.navigator.tab.LocalTabNavigator import cafe.adriel.voyager.navigator.tab.Tab import cafe.adriel.voyager.navigator.tab.TabOptions @@ -40,7 +42,7 @@ data object LoginScreen: Tab, CommonParcelable { val focusManager = LocalFocusManager.current val snackbarHostState = remember { SnackbarHostState() } val tabNavigator = LocalTabNavigator.current - val screenModel = viewModel { LoginScreenModel() } + val screenModel = LocalNavigator.currentOrThrow.rememberNavigatorScreenModel { LoginScreenModel() } if(screenModel.isLoggedIn) { tabNavigator.current = TabbedBaseScreen @@ -148,7 +150,7 @@ fun SignupView( Button(onClick = { if(screenModel.handle.isNotBlank() && screenModel.password.isNotBlank() && !isAppPassword(screenModel.password) && !appPWOverride) { - screenModel.viewModelScope.launch { + screenModel.screenModelScope.launch { val result = snackbarHostState.showSnackbar( message = "Please Use an App Password", actionLabel = "Security Sucks", @@ -170,7 +172,7 @@ fun SignupView( screenModel.onLoginClicked(screenModel.handle) focusManager.clearFocus() } else { - screenModel.viewModelScope.launch { + screenModel.screenModelScope.launch { snackbarHostState.showSnackbar( message = "Handle/Email or Password missing", withDismissAction = true @@ -232,7 +234,7 @@ fun LoginView( Button(onClick = { if(screenModel.handle.isNotBlank() && screenModel.password.isNotBlank() && !isAppPassword(screenModel.password) && !appPWOverride) { - screenModel.viewModelScope.launch { + screenModel.screenModelScope.launch { val result = snackbarHostState.showSnackbar( message = "Please Use an App Password", actionLabel = "Security Sucks", @@ -254,7 +256,7 @@ fun LoginView( screenModel.onLoginClicked(screenModel.handle) focusManager.clearFocus() } else { - screenModel.viewModelScope.launch { + screenModel.screenModelScope.launch { snackbarHostState.showSnackbar( message = "Handle/Email or Password missing", withDismissAction = true diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/login/LoginScreenModel.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/login/LoginScreenModel.kt index 3ecc613..32f6b35 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/login/LoginScreenModel.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/login/LoginScreenModel.kt @@ -3,7 +3,7 @@ package com.morpho.app.screens.login import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue -import androidx.lifecycle.viewModelScope +import cafe.adriel.voyager.core.model.screenModelScope import com.morpho.app.model.uistate.AuthState import com.morpho.app.model.uistate.LoginState import com.morpho.app.model.uistate.UiLoadingState @@ -38,7 +38,7 @@ class LoginScreenModel: BaseScreenModel() { // If the url is fucked up, just try Bluesky if(checkValidUrl(service) != null) Server.CustomServer(service) else Server.BlueskySocial } - viewModelScope.launch { + screenModelScope.launch { api.makeLoginRequest(credentials, server).onSuccess { loginState = loginState.copy( loadingState = UiLoadingState.Idle, diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/main/MainScreenModel.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/main/MainScreenModel.kt index 7260310..52b952b 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/main/MainScreenModel.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/main/MainScreenModel.kt @@ -3,13 +3,13 @@ package com.morpho.app.screens.main import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue -import androidx.lifecycle.viewModelScope import app.bsky.actor.GetProfileQuery import app.bsky.actor.SavedFeed import app.bsky.feed.GetFeedGeneratorsQuery import app.bsky.feed.GetPostThreadQuery import app.bsky.feed.GetPostThreadResponseThreadUnion import app.bsky.feed.GetPostsQuery +import cafe.adriel.voyager.core.model.screenModelScope import cafe.adriel.voyager.core.stack.mutableStateStackOf import com.morpho.app.model.bluesky.* import com.morpho.app.model.uidata.* @@ -108,7 +108,7 @@ open class MainScreenModel: BaseScreenModel() { } } if(populateFeeds) { - viewModelScope.launch(Dispatchers.Default) { + screenModelScope.launch(Dispatchers.Default) { settings.pinnedFeeds.collect { feeds -> log.d { "Pinned Feeds: $feeds" } pinnedFeeds.value = feeds @@ -249,7 +249,7 @@ open class MainScreenModel: BaseScreenModel() { if(feed == null) { emit(null); return@flow } if(update == null) dataService.peekLatest(feed.value.feed).onEach { emit(it) } else dataService.peekLatest(feed.value.feed, update).onEach { emit(it) } - }.stateIn(viewModelScope) + }.stateIn(screenModelScope) suspend fun loadThread(uri: AtUri): StateFlow? { val state = _threadStates.firstOrNull { it.value.uri == uri } @@ -292,7 +292,7 @@ open class MainScreenModel: BaseScreenModel() { } } emit(r.getOrDefault(state.copy(loadingState = ContentLoadingState.Error("Failed to load thread")))) - }.stateIn(viewModelScope) + }.stateIn(screenModelScope) private fun indexOf(state: ContentCardState): Int { return when(state) { @@ -331,7 +331,7 @@ open class MainScreenModel: BaseScreenModel() { } else { val i = _feedStates.indexOf(feedState) - _feedStates[i] = newFeed.filterNotNull().stateIn(viewModelScope) + _feedStates[i] = newFeed.filterNotNull().stateIn(screenModelScope) emit(newFeed.value) } } @@ -380,7 +380,7 @@ open class MainScreenModel: BaseScreenModel() { log.d { "Preferences found"} settings.feedViewPrefs.map { it["home"] ?: BskyFeedPref() - }.stateIn(viewModelScope, SharingStarted.Lazily, BskyFeedPref()) + }.stateIn(screenModelScope, SharingStarted.Lazily, BskyFeedPref()) } val feedService = dataService.dataFlows[timeline.uri] log.d { "Timeline service: $feedService"} @@ -412,7 +412,7 @@ open class MainScreenModel: BaseScreenModel() { val uri = AtUri.HOME_URI val prefs = settings.feedViewPrefs.map { it["home"] ?: BskyFeedPref() - }.filterNotNull().stateIn(viewModelScope) + }.filterNotNull().stateIn(screenModelScope) val feedService = dataService.dataFlows[uri] // Delete the feed if it's already there, initializing from scratch @@ -533,31 +533,31 @@ open class MainScreenModel: BaseScreenModel() { _cursors[AtUri.profileModServiceUri(p.did)] = servicesCursor val services = dataService .profileServiceView(p.did, servicesCursor.map { Unit } - .shareIn(viewModelScope, SharingStarted.Lazily) + .shareIn(screenModelScope, SharingStarted.Lazily) ).handleToState(p, MorphoData("Labels", AtUri.profileModServiceUri(p.did), servicesCursor.replayCache.lastOrNull() ?: AtCursor.EMPTY)) servicesCursor.emit(AtCursor.EMPTY) ContentCardState.FullProfile( p, - posts.stateIn(viewModelScope), - replies.stateIn(viewModelScope), - media.stateIn(viewModelScope), - likes.stateIn(viewModelScope), - lists.stateIn(viewModelScope), - feeds.stateIn(viewModelScope), - services.stateIn(viewModelScope), + posts.stateIn(screenModelScope), + replies.stateIn(screenModelScope), + media.stateIn(screenModelScope), + likes.stateIn(screenModelScope), + lists.stateIn(screenModelScope), + feeds.stateIn(screenModelScope), + services.stateIn(screenModelScope), ContentLoadingState.Idle ) } else { postsCursor.emit(AtCursor.EMPTY) ContentCardState.FullProfile( p, - posts.stateIn(viewModelScope), - replies.stateIn(viewModelScope), - media.stateIn(viewModelScope), - likes.stateIn(viewModelScope), - lists.stateIn(viewModelScope), - feeds.stateIn(viewModelScope), + posts.stateIn(screenModelScope), + replies.stateIn(screenModelScope), + media.stateIn(screenModelScope), + likes.stateIn(screenModelScope), + lists.stateIn(screenModelScope), + feeds.stateIn(screenModelScope), loadingState = ContentLoadingState.Idle ) } diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/main/tabbed/TabbedHomeView.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/main/tabbed/TabbedHomeView.kt index 63bdbec..c2efefe 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/main/tabbed/TabbedHomeView.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/main/tabbed/TabbedHomeView.kt @@ -26,8 +26,7 @@ import cafe.adriel.voyager.core.lifecycle.LifecycleEffectOnce import cafe.adriel.voyager.core.screen.ScreenKey import cafe.adriel.voyager.core.screen.uniqueScreenKey import cafe.adriel.voyager.core.stack.StackEvent -import cafe.adriel.voyager.jetpack.ProvideNavigatorLifecycleKMPSupport -import cafe.adriel.voyager.jetpack.navigatorViewModel +import cafe.adriel.voyager.koin.koinNavigatorScreenModel import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.Navigator import cafe.adriel.voyager.navigator.NavigatorDisposeBehavior @@ -103,9 +102,9 @@ abstract class SkylineTab: NavTab { ) @Composable fun TabScreen.TabbedHomeView( - sm: TabbedMainScreenModel = navigatorViewModel { TabbedMainScreenModel() } + sm: TabbedMainScreenModel = LocalNavigator.currentOrThrow.koinNavigatorScreenModel() ) { - ProvideNavigatorLifecycleKMPSupport { + //ProvideNavigatorLifecycleKMPSupport { val navigator = LocalNavigator.currentOrThrow @@ -172,7 +171,7 @@ fun TabScreen.TabbedHomeView( } } else LoadingCircle() - } + // } } @OptIn(ExperimentalVoyagerApi::class) diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/main/tabbed/TabbedMainScreenModel.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/main/tabbed/TabbedMainScreenModel.kt index fb783a3..230f917 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/main/tabbed/TabbedMainScreenModel.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/main/tabbed/TabbedMainScreenModel.kt @@ -3,9 +3,9 @@ package com.morpho.app.screens.main.tabbed import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue -import androidx.lifecycle.viewModelScope import app.bsky.actor.SavedFeed import app.bsky.feed.GetFeedGeneratorsQuery +import cafe.adriel.voyager.core.model.screenModelScope import com.morpho.app.model.bluesky.FeedGenerator import com.morpho.app.model.bluesky.MorphoDataItem import com.morpho.app.model.bluesky.Profile @@ -52,10 +52,10 @@ class TabbedMainScreenModel : MainScreenModel() { return tabs[index].uri } - fun initTabs() = viewModelScope.launch { + fun initTabs() = screenModelScope.launch { if (initialized) return@launch init(false) - viewModelScope.launch(Dispatchers.Default) { + screenModelScope.launch(Dispatchers.Default) { settings.pinnedFeeds.collect { feeds -> MainScreenModel.log.d { "Pinned Feeds: $feeds" } pinnedFeeds.value = feeds diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/notifications/NotificationsView.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/notifications/NotificationsView.kt index 4c022c0..226863c 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/notifications/NotificationsView.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/notifications/NotificationsView.kt @@ -17,9 +17,9 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.unit.Dp import androidx.constraintlayout.compose.ConstraintLayout -import androidx.lifecycle.viewModelScope import cafe.adriel.voyager.core.annotation.ExperimentalVoyagerApi -import cafe.adriel.voyager.jetpack.navigatorViewModel +import cafe.adriel.voyager.core.model.rememberNavigatorScreenModel +import cafe.adriel.voyager.core.model.screenModelScope import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.Navigator import cafe.adriel.voyager.navigator.currentOrThrow @@ -55,7 +55,7 @@ fun TabScreen.NotificationViewContent( navigator: Navigator = LocalNavigator.currentOrThrow, ) { - val sm = navigatorViewModel { TabbedNotificationScreenModel() } + val sm = navigator.rememberNavigatorScreenModel { TabbedNotificationScreenModel() } val numberUnread by sm.uiState.value.numberUnread.collectAsState(0) var showSettings by remember { mutableStateOf(false) } val hasUnread = remember(numberUnread) { numberUnread > 0 } @@ -220,7 +220,7 @@ fun TabScreen.NotificationViewContent( draft = DraftPost() }, onSend = { finishedDraft -> - sm.viewModelScope.launch(Dispatchers.IO) { + sm.screenModelScope.launch(Dispatchers.IO) { val post = finishedDraft.createPost(sm.api) sm.api.createRecord(RecordUnion.MakePost(post)) } diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/notifications/TabbedNotificationScreenModel.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/notifications/TabbedNotificationScreenModel.kt index e71f19a..6726313 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/notifications/TabbedNotificationScreenModel.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/notifications/TabbedNotificationScreenModel.kt @@ -3,7 +3,7 @@ package com.morpho.app.screens.notifications import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue -import androidx.lifecycle.viewModelScope +import cafe.adriel.voyager.core.model.screenModelScope import com.morpho.app.model.uidata.AtCursor import com.morpho.app.model.uidata.initAtCursor import com.morpho.app.model.uistate.NotificationsUIState @@ -29,7 +29,7 @@ class TabbedNotificationScreenModel: BaseScreenModel() { ) init { - viewModelScope.launch { + screenModelScope.launch { val f = notifService.notifications(cursorFlow).map { it.getOrNull() } cursorFlow.emit(AtCursor.EMPTY) f.collect { diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/profile/TabbedProfileView.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/profile/TabbedProfileView.kt index f7e742b..a8f21aa 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/profile/TabbedProfileView.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/profile/TabbedProfileView.kt @@ -17,8 +17,6 @@ import cafe.adriel.voyager.core.annotation.ExperimentalVoyagerApi import cafe.adriel.voyager.core.lifecycle.LifecycleEffectOnce import cafe.adriel.voyager.core.screen.ScreenKey import cafe.adriel.voyager.core.screen.uniqueScreenKey -import cafe.adriel.voyager.jetpack.ProvideNavigatorLifecycleKMPSupport -import cafe.adriel.voyager.jetpack.navigatorViewModel import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.currentOrThrow import cafe.adriel.voyager.navigator.tab.LocalTabNavigator @@ -44,6 +42,7 @@ import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.flow.StateFlow import kotlinx.serialization.Serializable import org.jetbrains.compose.resources.ExperimentalResourceApi +import org.koin.compose.getKoin import cafe.adriel.voyager.navigator.tab.Tab as NavTab @OptIn(ExperimentalAnimationApi::class, ExperimentalMaterial3Api::class) @@ -136,9 +135,9 @@ fun ProfileTabItem( @Composable fun TabScreen.TabbedProfileContent( id: AtIdentifier? = null, - sm: TabbedProfileViewModel = navigatorViewModel { TabbedProfileViewModel(id) } + sm: TabbedProfileViewModel = getKoin().get() ) { - ProvideNavigatorLifecycleKMPSupport { + //ProvideNavigatorLifecycleKMPSupport { val navigator = LocalNavigator.currentOrThrow @@ -233,7 +232,7 @@ fun TabScreen.TabbedProfileContent( state = sm.profileUiState.tabStates.getOrNull(selectedTabIndex), ) } - } + //} } @Composable diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/profile/TabbedProfileViewModel.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/profile/TabbedProfileViewModel.kt index 75d4c81..137b479 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/profile/TabbedProfileViewModel.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/profile/TabbedProfileViewModel.kt @@ -3,8 +3,8 @@ package com.morpho.app.screens.profile import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue -import androidx.lifecycle.viewModelScope import app.bsky.actor.GetProfileQuery +import cafe.adriel.voyager.core.model.screenModelScope import com.morpho.app.model.bluesky.* import com.morpho.app.model.uidata.AtCursor import com.morpho.app.model.uidata.ContentCardMapEntry @@ -58,7 +58,7 @@ class TabbedProfileViewModel( - fun initProfile() = viewModelScope.launch { + fun initProfile() = screenModelScope.launch { if(initialized) return@launch init(false) if(id != null) { diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/thread/ThreadView.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/thread/ThreadView.kt index a0a653d..1f40966 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/thread/ThreadView.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/thread/ThreadView.kt @@ -10,7 +10,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.unit.dp import cafe.adriel.voyager.core.annotation.ExperimentalVoyagerApi -import cafe.adriel.voyager.jetpack.navigatorViewModel +import cafe.adriel.voyager.core.model.rememberNavigatorScreenModel import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.Navigator import cafe.adriel.voyager.navigator.currentOrThrow @@ -49,7 +49,7 @@ fun TabScreen.ThreadViewContent( navigator:Navigator = LocalNavigator.currentOrThrow, ) { - val sm = navigatorViewModel { MainScreenModel() } + val sm = navigator.rememberNavigatorScreenModel { MainScreenModel() } TabbedScreenScaffold( navBar = { navBar(navigator) }, diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/common/TabbedSkylineFragment.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/common/TabbedSkylineFragment.kt index 91d1e99..3172837 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/common/TabbedSkylineFragment.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/common/TabbedSkylineFragment.kt @@ -8,7 +8,7 @@ import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.* import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.unit.dp -import androidx.lifecycle.viewModelScope +import cafe.adriel.voyager.core.model.screenModelScope import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.currentOrThrow import cafe.adriel.voyager.navigator.tab.TabNavigator @@ -134,7 +134,7 @@ fun > TabbedSkylin draft = DraftPost() }, onSend = { finishedDraft -> - sm.viewModelScope.launch(Dispatchers.IO) { + sm.screenModelScope.launch(Dispatchers.IO) { val post = finishedDraft.createPost(sm.api) sm.api.createRecord(RecordUnion.MakePost(post)) } diff --git a/Morpho/composeApp/src/desktopMain/kotlin/main.kt b/Morpho/composeApp/src/desktopMain/kotlin/main.kt index 9bbf1e7..35b853b 100644 --- a/Morpho/composeApp/src/desktopMain/kotlin/main.kt +++ b/Morpho/composeApp/src/desktopMain/kotlin/main.kt @@ -19,6 +19,8 @@ import androidx.compose.ui.graphics.RectangleShape import androidx.compose.ui.unit.DpSize import androidx.compose.ui.unit.dp import androidx.compose.ui.window.* +import cafe.adriel.voyager.core.annotation.ExperimentalVoyagerApi +import cafe.adriel.voyager.jetpack.ProvideNavigatorLifecycleKMPSupport import ch.qos.logback.classic.LoggerContext import ch.qos.logback.core.util.StatusPrinter2 import com.morpho.app.App @@ -49,8 +51,9 @@ import kotlin.io.path.createDirectories val log = logging("main") -@OptIn(KoinExperimentalAPI::class, ExperimentalResourceApi::class) +@OptIn(KoinExperimentalAPI::class, ExperimentalResourceApi::class, ExperimentalVoyagerApi::class) fun main() = application { + ProvideNavigatorLifecycleKMPSupport { StatusPrinter2().print(LoggerFactory.getILoggerFactory() as LoggerContext) KmLogging.setLoggers(PlatformLogger(VariableLogLevel(LogLevel.Verbose))) val koin = startKoin { @@ -87,10 +90,11 @@ fun main() = application { Window( onCloseRequest = { - runBlocking { - api.refreshSession().join() + // possible hack to catch the exception on exit + try { (::exitApplication)() } catch (e: Exception) { + e.printStackTrace() + } - (::exitApplication)() }, state = windowState, title = "Morpho", @@ -103,10 +107,10 @@ fun main() = application { MorphoWindow( windowState = windowState, onCloseRequest = { - runBlocking { - api.refreshSession().join() + try { (::exitApplication)() } catch (e: Exception) { + e.printStackTrace() + } - (::exitApplication)() } ) { App() @@ -117,6 +121,7 @@ fun main() = application { } } + } } /* From 71b29ef89c481f0241812144c9419adeea0a2852 Mon Sep 17 00:00:00 2001 From: Orual Date: Sun, 15 Sep 2024 14:29:04 -0400 Subject: [PATCH 08/42] Big UI refactoring ongoing. --- Morpho/composeApp/build.gradle.kts | 11 +- .../com/morpho/app/data/BskyDataService.kt | 2 + .../kotlin/com/morpho/app/data/FeedTuner.kt | 388 ++++++++++++++++++ .../com/morpho/app/data/MorphoDataSource.kt | 320 +++++++++++++++ .../kotlin/com/morpho/app/di/AppModule.kt | 13 +- .../app/model/bluesky/FeedSourceInfo.kt | 164 ++++++++ .../morpho/app/model/bluesky/UISavedFeed.kt | 87 ---- .../app/model/uidata/BskyDataService.kt | 1 + .../com/morpho/app/model/uidata/FeedEvent.kt | 190 +++++++++ .../morpho/app/model/uidata/FeedPresenter.kt | 223 ++++++++++ .../morpho/app/model/uidata/ListPresenter.kt | 128 ++++++ .../com/morpho/app/model/uidata/MorphoData.kt | 266 +----------- .../com/morpho/app/model/uidata/UIUpdate.kt | 92 +++++ 13 files changed, 1528 insertions(+), 357 deletions(-) create mode 100644 Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/data/BskyDataService.kt create mode 100644 Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/data/FeedTuner.kt create mode 100644 Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/data/MorphoDataSource.kt create mode 100644 Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/FeedSourceInfo.kt delete mode 100644 Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/UISavedFeed.kt create mode 100644 Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/FeedEvent.kt create mode 100644 Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/FeedPresenter.kt create mode 100644 Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/ListPresenter.kt create mode 100644 Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/UIUpdate.kt diff --git a/Morpho/composeApp/build.gradle.kts b/Morpho/composeApp/build.gradle.kts index 4e35d5b..327bf56 100644 --- a/Morpho/composeApp/build.gradle.kts +++ b/Morpho/composeApp/build.gradle.kts @@ -83,6 +83,9 @@ kotlin { implementation(libs.ktor.client.android) implementation(libs.kotlin.jwt) + + implementation("androidx.paging:paging-runtime:3.3.0-alpha02") + implementation("androidx.paging:paging-compose:3.3.0-alpha02") } commonMain.dependencies { @@ -96,6 +99,9 @@ kotlin { implementation("androidx.datastore:datastore-preferences-core:1.1.1") implementation("androidx.datastore:datastore-core:1.1.1") + implementation("app.cash.paging:paging-common:3.3.0-alpha02-0.5.1") + implementation("app.cash.paging:paging-compose-common:3.3.0-alpha02-0.5.1") + implementation(compose.runtime) implementation(compose.foundation) implementation(compose.material) @@ -192,7 +198,7 @@ kotlin { } nativeMain.dependencies { - + implementation("app.cash.paging:paging-runtime-uikit:3.3.0-alpha02-0.5.1") } desktopMain.dependencies { implementation(compose.desktop.currentOs) @@ -209,6 +215,9 @@ kotlin { implementation(libs.kotlin.test) @OptIn(org.jetbrains.compose.ExperimentalComposeLibrary::class) implementation(compose.uiTest) + implementation("app.cash.paging:paging-testing:3.3.0-alpha02-0.5.1") + + } val desktopTest by getting { dependencies { diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/data/BskyDataService.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/data/BskyDataService.kt new file mode 100644 index 0000000..2d92a6c --- /dev/null +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/data/BskyDataService.kt @@ -0,0 +1,2 @@ +package com.morpho.app.data + diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/data/FeedTuner.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/data/FeedTuner.kt new file mode 100644 index 0000000..12720f0 --- /dev/null +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/data/FeedTuner.kt @@ -0,0 +1,388 @@ +package com.morpho.app.data + +import com.morpho.app.model.bluesky.* +import com.morpho.app.model.uidata.MorphoData +import com.morpho.app.model.uidata.areSameAuthor +import com.morpho.app.model.uistate.FeedType +import com.morpho.butterfly.* +import com.morpho.butterfly.BskyPreferences +import kotlinx.collections.immutable.persistentListOf +import kotlinx.serialization.Serializable + +typealias TunerFunction = (List, FeedTuner) -> List + +@Serializable +data class FeedTuner(val tuners: List> = persistentListOf()) { + val seenKeys = mutableSetOf() + val seenUris = mutableSetOf() + val seenRootUris = mutableSetOf() + + companion object { + fun useFeedTuners( + userDid: Did, + prefs: BskyPreferences, + desc: FeedDescriptor, + ): List> { + if(desc is FeedDescriptor.Author) { + when(desc.filter) { + AuthorFilter.PostsNoReplies -> return listOf( + FeedTuner(tuners = persistentListOf( + Companion::removeReplies + )) as FeedTuner + ) + AuthorFilter.PostsWithReplies ->{ + return listOf() + } + AuthorFilter.PostsAuthorThreads -> { + return listOf() + } + AuthorFilter.PostsWithMedia -> { + return listOf() + } + } + } + val languages = prefs.languages + val languageTuner: TunerFunction = { f, t -> + preferredLanguageOnly(languages, f, t) + } + if(desc is FeedDescriptor.FeedGen) { + return listOf(FeedTuner(tuners = persistentListOf(languageTuner))) as List> + } + if(desc is FeedDescriptor.Home || desc is FeedDescriptor.List) { + val tuners = mutableListOf(FeedTuner(tuners = persistentListOf(Companion::removeOrphans))) + val feedPrefs = prefs.feedView ?: return tuners.toList() as List> + if(feedPrefs.hideReposts == true) tuners.add(FeedTuner(tuners = persistentListOf(Companion::removeReposts))) + if(feedPrefs.hideReplies == true) tuners.add(FeedTuner(tuners = persistentListOf(Companion::removeReplies))) + else { + val followedRepliesOnly: TunerFunction = { f, t -> + followedRepliesOnly(userDid, f, t) + } + tuners.add(FeedTuner(tuners = persistentListOf(followedRepliesOnly))) + } + if(feedPrefs.hideQuotePosts == true) tuners.add( + FeedTuner(tuners = persistentListOf( + Companion::removeQuotePosts + )) + ) + tuners.add(FeedTuner(tuners = persistentListOf(Companion::dedupThreads))) + return tuners.toList() as List> + } + return listOf() + } + + fun useFeedTuners( + prefs: BskyUserPreferences, + feed: MorphoData + ): List> { + if(feed.isProfileFeed) { + when(feed.feedType) { + FeedType.PROFILE_POSTS -> return listOf( + FeedTuner(tuners = persistentListOf( + Companion::removeReplies + )) as FeedTuner + ) + FeedType.PROFILE_USER_LISTS -> return listOf() + FeedType.PROFILE_FEEDS_LIST -> return listOf() + FeedType.PROFILE_MOD_SERVICE -> return listOf() + else -> {} + } + } + val languages = prefs.preferences.languages.toList() + val languageTuner: TunerFunction = { f, t -> + preferredLanguageOnly(languages, f, t) + } + if(feed.feedType == FeedType.OTHER) { + return listOf(FeedTuner(tuners = persistentListOf(languageTuner))) as List> + } + if(feed.feedType == FeedType.LIST_FOLLOWING || feed.feedType == FeedType.HOME) { + val userDid = Did(prefs.user.userDid) + val tuners = mutableListOf(FeedTuner(tuners = persistentListOf(Companion::removeOrphans))) + val feedPrefs = prefs.preferences.feedViewPrefs[feed.uri.atUri] ?: + return tuners.toList() as List> + if(feedPrefs.hideReposts) tuners.add(FeedTuner(tuners = persistentListOf(Companion::removeReposts))) + if(feedPrefs.hideReplies) tuners.add(FeedTuner(tuners = persistentListOf(Companion::removeReplies))) + else { + val followedRepliesOnly: TunerFunction = { f, t -> + followedRepliesOnly(userDid, f, t) + } + tuners.add(FeedTuner(tuners = persistentListOf(followedRepliesOnly))) + } + if(feedPrefs.hideQuotePosts) tuners.add( + FeedTuner(tuners = persistentListOf( + Companion::removeQuotePosts + )) + ) + tuners.add(FeedTuner(tuners = persistentListOf(Companion::dedupThreads))) + return tuners.toList() as List> + } + return listOf() + } + + fun removeReplies( + feed: List, + tuner: FeedTuner = FeedTuner(), + ): List { + return feed.filterNot { item -> + when(item) { + is MorphoDataItem.Post -> item.isReply && !item.isRepost && + !(item.getAuthors()?.let { areSameAuthor(it) } ?: false) + is MorphoDataItem.Thread -> !(item.getAuthors()?.let { areSameAuthor(it) } ?: false) + else -> false + } + } + + } + + fun removeReposts( + feed: List, + tuner: FeedTuner = FeedTuner(), + ): List { + return feed.filterNot { item -> + item.isRepost + } + } + + fun removeQuotePosts( + feed: List, + tuner: FeedTuner = FeedTuner(), + ): List { + return feed.filterNot { item -> + item.isQuotePost + } + } + + fun removeOrphans( + feed: List, + tuner: FeedTuner = FeedTuner(), + ): List { + return feed.filterNot { item -> + when(item) { + is MorphoDataItem.Post -> item.isOrphan + is MorphoDataItem.Thread -> false + else -> false + } + } + } + + fun dedupThreads( + feed: List, + tuner: FeedTuner = FeedTuner(), + ): List { + return feed.filterNot { item -> + val rootUri = item.rootUri + if(!item.isRepost == tuner.seenRootUris.contains(rootUri)) { + false + } else { + tuner.seenRootUris.add(rootUri) + true + } + } + } + + fun followedRepliesOnly( + userDid: Did, + feed: List, + tuner: FeedTuner = FeedTuner(), + ): List { + return feed.filterNot { item -> + item.isReply && !shouldDisplayReplyInFollowing(item, userDid) + } + } + + fun preferredLanguageOnly( + languages: List = persistentListOf(), + feed: List, + tuner: FeedTuner = FeedTuner(), + ): List { + if (languages.isEmpty()) return feed + val newFeed = feed.filter { item -> + when(item) { + is MorphoDataItem.Post -> { + item.post.langs.isEmpty() || + item.post.langs.any { languages.contains(it) } + } + is MorphoDataItem.Thread -> { + item.thread.post.langs.isEmpty() || + item.thread.post.langs.any { languages.contains(it) } + } + else -> false + } + }.map { item -> + when(item) { + is MorphoDataItem.Post -> item + is MorphoDataItem.Thread -> { + item.copy( + thread = item.thread.filterReplies { reply -> + when(reply) { + is ThreadPost.ViewablePost -> reply.post.langs.isEmpty() || + reply.post.langs.any { languages.contains(it) } + is ThreadPost.BlockedPost -> true + is ThreadPost.NotFoundPost -> true + } + + } + ) + } + else -> false + } + } + return newFeed.ifEmpty { feed } as List + } + } + fun tune( + feed: MorphoData + ): MorphoData { + var workingFeed = feed.items + tuners.forEach { tuner -> + workingFeed = tuner(workingFeed, this) + } + workingFeed = workingFeed.map { item -> + if(seenKeys.contains(item.key)) return@map null + else if(item is MorphoDataItem.Thread) { + val itemUris = item.getUris() + val seenInThisThread = itemUris.filter { seenUris.contains(it) } + if(seenInThisThread.isNotEmpty()) { + if(seenInThisThread.size == itemUris.size) { + return@map null + } else { + val newParents = item.thread.parents.filter { parent -> + when(parent) { + is ThreadPost.ViewablePost -> parent.post.uri in seenInThisThread + is ThreadPost.BlockedPost -> false + is ThreadPost.NotFoundPost -> false + } + } + val newThread = item.copy(thread = item.thread.filterReplies { reply -> + when(reply) { + is ThreadPost.ViewablePost -> reply.post.uri in seenInThisThread + is ThreadPost.BlockedPost -> false + is ThreadPost.NotFoundPost -> false + } + }.copy(parents = newParents)) + seenUris.addAll(itemUris) + if(newThread.thread.replies.isEmpty() && newThread.thread.parents.isEmpty()) { + return@map null + } else { + return@map newThread + } + } + } else { + seenUris.addAll(itemUris) + item + } + } else { + val disableDedub = item.isReply && item.isRepost + if(!disableDedub) seenKeys.add(item.key) + item + } + }.filterNotNull() as List + return feed.copy(items = workingFeed) + } + fun tune( + feed: PagedResponse.Feed + ): PagedResponse.Feed { + var workingFeed = feed.items + tuners.forEach { tuner -> + workingFeed = tuner(workingFeed, this) + } + workingFeed = workingFeed.map { item -> + if(seenKeys.contains(item.key)) return@map null + else if(item is MorphoDataItem.Thread) { + val itemUris = item.getUris() + val seenInThisThread = itemUris.filter { seenUris.contains(it) } + if(seenInThisThread.isNotEmpty()) { + if(seenInThisThread.size == itemUris.size) { + return@map null + } else { + val newParents = item.thread.parents.filter { parent -> + when(parent) { + is ThreadPost.ViewablePost -> parent.post.uri in seenInThisThread + is ThreadPost.BlockedPost -> false + is ThreadPost.NotFoundPost -> false + } + } + val newThread = item.copy(thread = item.thread.filterReplies { reply -> + when(reply) { + is ThreadPost.ViewablePost -> reply.post.uri in seenInThisThread + is ThreadPost.BlockedPost -> false + is ThreadPost.NotFoundPost -> false + } + }.copy(parents = newParents)) + seenUris.addAll(itemUris) + if(newThread.thread.replies.isEmpty() && newThread.thread.parents.isEmpty()) { + return@map null + } else { + return@map newThread + } + } + } else { + seenUris.addAll(itemUris) + item + } + } else { + val disableDedub = item.isReply && item.isRepost + if(!disableDedub) seenKeys.add(item.key) + item + } + }.filterNotNull() as List + return feed.copy(items = workingFeed) + } + + +} + +/// Algo copied from official app +/// https://github.com/bluesky-social/social-app/blob/main/src/lib/api/feed-manip.ts#L445 +/// as of commit https://github.com/bluesky-social/social-app/commit/e2a244b99889743a8788b0c464d3e150bc8047ad +/// The algorithm is a controversial, so we may want to change it or offer more options. +fun shouldDisplayReplyInFollowing( + item: MorphoDataItem.FeedItem, + userDid: Did, +): Boolean { + val authors = item.getAuthors() + val author = authors?.author + val rootAuthor = authors?.rootAuthor + val parentAuthor = authors?.parentAuthor + val grandParentAuthor = authors?.grandParentAuthor + if (!isSelfOrFollowing(author, userDid)) + return false // Only show replies from self or people you follow. + + if(parentAuthor == null || parentAuthor.did == author?.did + && rootAuthor == null || rootAuthor?.did == author?.did + && grandParentAuthor == null || grandParentAuthor?.did == author?.did + ) return true // Always show self-threads. + + if ( + parentAuthor.did != author?.did && + rootAuthor?.did == author?.did && + item is MorphoDataItem.Thread + ) { + // If you follow A, show A -> someone[>0 likes] -> A chains too. + // This is different from cases below because you only know one person. + val parentPost = when(val p = item.thread.parents.lastOrNull()) { + is ThreadPost.ViewablePost -> p.post + else -> null + } + if(parentPost != null && parentPost.likeCount > 0) + return true + } + // From this point on we need at least one more reason to show it. + if ( + parentAuthor.did != author?.did && isSelfOrFollowing(parentAuthor, userDid) + ) return true + if ( + grandParentAuthor != null && + grandParentAuthor.did != author?.did && + isSelfOrFollowing(grandParentAuthor, userDid) + ) return true + if ( + rootAuthor != null && + rootAuthor.did != author?.did && + isSelfOrFollowing(rootAuthor, userDid) + ) return true + return false +} + +fun isSelfOrFollowing(profile: Profile?, userDid: Did): Boolean { + return profile?.did == userDid || profile?.followedByMe == true +} \ No newline at end of file diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/data/MorphoDataSource.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/data/MorphoDataSource.kt new file mode 100644 index 0000000..7ffcfbd --- /dev/null +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/data/MorphoDataSource.kt @@ -0,0 +1,320 @@ +package com.morpho.app.data + +import androidx.compose.ui.util.fastAny +import app.cash.paging.PagingConfig +import app.cash.paging.PagingSource +import app.cash.paging.PagingState +import com.morpho.app.model.bluesky.* +import com.morpho.app.model.uidata.Delta +import com.morpho.app.model.uidata.Moment +import com.morpho.butterfly.ButterflyAgent +import com.morpho.butterfly.Cursor +import com.morpho.butterfly.FeedRequest +import com.morpho.butterfly.PagedResponse +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.flowOn +import kotlinx.datetime.Instant +import org.koin.core.component.KoinComponent +import org.koin.core.component.inject +import kotlin.time.Duration + + +abstract class MorphoDataSource: PagingSource(), KoinComponent { + val agent: ButterflyAgent by inject() + override fun getRefreshKey(state: PagingState): Cursor? { + return state.anchorPosition?.let { anchorPosition -> + val anchorPage = state.closestPageToPosition(anchorPosition) + if (anchorPage?.prevKey == null) return Cursor.Empty // First page + else if (anchorPage.nextKey == null) anchorPage.prevKey // Last page + else anchorPage.prevKey // Initial page + } + } + companion object { + val defaultConfig = PagingConfig( + pageSize = 20, + prefetchDistance = 10, + initialLoadSize = 50, + enablePlaceholders = true, + ) + } +} + +data class MorphoFeedSource( + val request: FeedRequest, + val tuners: List> = listOf(), + val repliesBumpThreads: Boolean = false, + val collectThreads: Boolean = true, +): MorphoDataSource() { + override suspend fun load( + params: LoadParams + ): LoadResult { + try { + val limit = params.loadSize + val loadCursor = when(params) { + is LoadParams.Append -> params.key + is LoadParams.Prepend -> Cursor.Empty + is LoadParams.Refresh -> Cursor.Empty + } + return request(loadCursor, limit.toLong()).map { + pagedList -> + + val tunedList = when(pagedList) { + is PagedResponse.Feed -> { + var tunedFeed = pagedList.copy( + items = if(collectThreads) pagedList.items + .collectThreads( + repliesBumpThreads = repliesBumpThreads, + agent = agent + ).getOrNull() ?: pagedList.items + else pagedList.items + ) + tuners.forEach { tuner -> + tunedFeed = tuner.tune(tunedFeed) + } + tunedFeed + } + is PagedResponse.FromRecord -> pagedList.items + is PagedResponse.Profile -> pagedList.items + } + LoadResult.Page( + data = tunedList as List, + prevKey = when(params) { + is LoadParams.Append -> loadCursor + is LoadParams.Prepend -> Cursor.Empty + is LoadParams.Refresh -> Cursor.Empty + }, + nextKey = pagedList.cursor, + ) + }.onFailure { + return LoadResult.Error(it) + }.getOrDefault(LoadResult.Error(Exception("Load failed"))) + } catch (e: Exception) { + return LoadResult.Error(e) + } + } + + fun updates(): Flow = flow { + while (true) { + val newData = peek().getOrNull() + if(newData != null) { + emit(newData) + } + } + }.distinctUntilChanged().flowOn(Dispatchers.Default) + + suspend fun hasNew(): Boolean { + return peek().getOrNull() != null + } + + suspend fun peek(): Result { + try { + request(Cursor.Empty, 1).onSuccess { pagedList -> + return Result.success(pagedList.items.firstOrNull()) + }.onFailure { + return Result.failure(it) + } + } catch (e: Exception) { + return Result.failure(e) + } + return Result.failure(Exception("Should not be reached")) + } +} + +suspend fun List.collectThreads( + depth: Int = 3, height: Int = 80, + timeRange: Delta = Delta(Duration.parse("4h")), + repliesBumpThreads: Boolean = true, + agent: ButterflyAgent? = null, // allows to just use local data +): Result> { + val threads = mutableListOf() + val replies = mutableListOf() + val posts = mutableListOf() + val threadCandidates = mutableListOf() + this.forEach { item -> + when(item) { + is MorphoDataItem.Post -> { + if (item.isReply) replies.add(item) + else if (item.isOrphan) posts.add(item) + else posts.add(item) + } + is MorphoDataItem.Thread -> { + if (!item.isIncompleteThread) threads.add(item) + else threadCandidates.add(item) + } + else -> return Result.failure(Exception("Invalid feed item type")) + } + } + replies.forEachIndexed { index, reply -> + if (reply == null) return@forEachIndexed + if (reply.isOrphan) { + val parent = reply.post.reply?.parentPost + ?: reply.post.reply?.replyRef?.parent?.uri?.let { + agent?.getPosts(listOf(it))?.getOrNull()?.firstOrNull()?.toPost() + } + val root = reply.post.reply?.rootPost + ?: reply.post.reply?.replyRef?.root?.uri?.let { + agent?.getPosts(listOf(it))?.getOrNull()?.firstOrNull()?.toPost() + } + replies[index] = MorphoDataItem.Post( + reply.post.copy(reply = reply.post.reply?.copy(parentPost = parent, rootPost = root)), + reply.reason, + isOrphan = root != null && parent != null, + ) + } + val newReply = replies[index] ?: return@forEachIndexed // Update in case we changed it above + val replyRef = newReply.post.reply?.replyRef ?: return@forEachIndexed + val parent = replyRef.parent.uri + val root = replyRef.root.uri + val inThread = threads.indexOfFirst { it?.containsUri(parent) ?: false || it?.containsUri(root) ?: false } + if (inThread != -1) { + val thread = threads.getOrNull(inThread) ?: return@forEachIndexed + threads[inThread] = thread.addReply(newReply.post) + replies[index] = null + } + val inCandidates = threadCandidates.indexOfFirst { it?.containsUri(parent) ?: false || it?.containsUri(root) ?: false } + if (inCandidates != -1) { + val thread = threadCandidates.getOrNull(inCandidates) ?: return@forEachIndexed + threadCandidates[inCandidates] = thread.addReply(newReply.post) + replies[index] = null + } + + } + threadCandidates.forEachIndexed { index, thread -> + if (thread == null) return@forEachIndexed + val rootInThreads = threads.indexOfFirst { t -> t?.containsUri(thread.rootUri) ?: false } + if (rootInThreads == - 1) { + val threadToSplice = threads.getOrNull(rootInThreads) ?: return@forEachIndexed + if( + thread.thread.parents.firstOrNull() is ThreadPost.ViewablePost + && threadToSplice.thread.parents.firstOrNull() is ThreadPost.ViewablePost + && thread.rootUri == threadToSplice.rootUri + ) { + if(thread.thread.parents.size == 1 && threadToSplice.thread.parents.size == 1) { + // Both threads have the same, viewable root post and are only one level deep in terms of parents + val newEntry = thread.thread.parents.first() as ThreadPost.ViewablePost + val oldEntry = threadToSplice.thread.parents.first() as ThreadPost.ViewablePost + + val newReplies = (newEntry.replies + oldEntry.replies).distinctBy { it.uri }.toMutableList() + newReplies.add(ThreadPost.ViewablePost(thread.thread.post, thread.thread.replies)) + if( thread.getUri() != threadToSplice.getUri() ) + newReplies.add(ThreadPost.ViewablePost(threadToSplice.thread.post, threadToSplice.thread.replies)) + val newThread = BskyPostThread( + post = newEntry.post, + parents = listOf(), + replies = newReplies.distinctBy { it.uri }, + ) + threads[rootInThreads] = threadToSplice.copy(thread = newThread, isIncompleteThread = false) + threadCandidates[index] = null + } else if(thread.thread.parents.size == 2 && threadToSplice.thread.parents.size == 2) { + // Both threads have the same, viewable root post and parent chains are both length 2 + val newEntry = thread.thread.parents.first() as ThreadPost.ViewablePost + + val newReplies = mutableListOf() + if(thread.thread.parents.lastOrNull() !is ThreadPost.ViewablePost) return@forEachIndexed + if(threadToSplice.thread.parents.lastOrNull() !is ThreadPost.ViewablePost) return@forEachIndexed + val newParent = thread.thread.parents.last() as ThreadPost.ViewablePost + val oldParent = threadToSplice.thread.parents.last() as ThreadPost.ViewablePost + val newReply = ThreadPost.ViewablePost(thread.thread.post, thread.thread.replies) + val oldReply = ThreadPost.ViewablePost(threadToSplice.thread.post, threadToSplice.thread.replies) + newParent.addReply(newReply) + oldParent.addReply(oldReply) + newReplies.add(newReply) + newReplies.add(oldReply) + val newThread = BskyPostThread( + post = newEntry.post, + parents = listOf(newParent), + replies = newReplies.distinctBy { it.uri }, + ) + threads[rootInThreads] = threadToSplice.copy(thread = newThread, isIncompleteThread = false) + threadCandidates[index] = null + } + + } + } else { + val inThreads = threads.indexOfFirst { t -> t?.containsUri(thread.thread.post.uri) ?: false } + if (inThreads == - 1) { + val threadToSplice = threads.getOrNull(index) ?: return@forEachIndexed + threads[index] = threadToSplice.addReply(ThreadPost.ViewablePost(thread.thread.post, thread.thread.replies)) + threadCandidates[index] = null + } + } + } + threadCandidates.filterNotNull() + if (threadCandidates.isNotEmpty()) threads.addAll(threadCandidates) + val newReplies = replies.filterNotNull() + .distinctBy { it.getUri() } + .filterNot { reply -> + if(reply.isRepost) return@filterNot false + if(reply.isQuotePost) return@filterNot false + reply.getUris().any { uri -> threads.any { it?.containsUri(uri) ?: false } } + }.sortedByDescending { when(it.reason) { + is BskyPostReason.BskyPostRepost -> it.reason.indexedAt + else -> it.post.createdAt + } }.iterator() + var newPosts = posts.toList().filterNotNull() + newPosts = newPosts.distinctBy { it.getUri() } + newPosts = newPosts.filterNot { post -> + if(post.isRepost) return@filterNot false + if(post.isQuotePost) return@filterNot false + post.getUris().any { uri -> threads.any { it?.containsUri(uri) ?: false } } + }.sortedByDescending { when(it.reason) { + is BskyPostReason.BskyPostRepost -> it.reason.indexedAt + else -> it.post.createdAt + } } + val newPostsIter = newPosts.iterator() + var newThreads = threads.toList().filterNotNull() + newThreads = newThreads.sortedByDescending { if(!repliesBumpThreads) { + it.rootAccessiblePost.createdAt + } else { + maxOf(it.thread.post.createdAt, + it.thread.replies.fold(it.thread.post.createdAt) { acc, post -> + val postTime = when(post) { + is ThreadPost.ViewablePost -> post.post.createdAt + is ThreadPost.BlockedPost -> Moment(Instant.DISTANT_PAST) + is ThreadPost.NotFoundPost -> Moment(Instant.DISTANT_PAST) + } + maxOf(acc, postTime) + }) + } } + newThreads = newThreads.distinctBy { it.getUri() } + .filterNot { thread -> + thread.getUris().filterNot { uri -> + newThreads.fastAny { it.getUri() == uri } }.size > 1 + } + val newThreadsIter = newThreads.iterator() + val newFeed = mutableListOf() + while(newPostsIter.hasNext() || newThreadsIter.hasNext() || newReplies.hasNext() ) { + if(newPostsIter.hasNext()) newFeed.add(newPostsIter.next()) + if(newThreadsIter.hasNext()) newFeed.add(newThreadsIter.next()) + if(newReplies.hasNext()) newFeed.add(newReplies.next()) + } + val dedupedFeed = newFeed.distinctBy { it.getUri() } + val sortedFeed = dedupedFeed.sortedByDescending { + when(it) { + is MorphoDataItem.Post -> when(it.reason) { + is BskyPostReason.BskyPostFeedPost -> it.post.createdAt + is BskyPostReason.BskyPostRepost -> it.reason.indexedAt + is BskyPostReason.SourceFeed -> it.post.createdAt + null -> it.post.createdAt + } + is MorphoDataItem.Thread -> if(!repliesBumpThreads) { + it.rootAccessiblePost.createdAt + } else { + maxOf(it.thread.post.createdAt, + it.thread.replies.fold(it.thread.post.createdAt) { acc, post -> + val postTime = when(post) { + is ThreadPost.ViewablePost -> post.post.createdAt + is ThreadPost.BlockedPost -> Moment(Instant.DISTANT_PAST) + is ThreadPost.NotFoundPost -> Moment(Instant.DISTANT_PAST) + } + maxOf(acc, postTime) + }) + } + + } + } + return Result.success(sortedFeed as List) +} \ No newline at end of file diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/di/AppModule.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/di/AppModule.kt index 338fdca..c42950a 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/di/AppModule.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/di/AppModule.kt @@ -2,10 +2,8 @@ package com.morpho.app.di import com.morpho.app.data.PollBlueService import com.morpho.app.data.PreferencesRepository -import com.morpho.app.model.uidata.BskyDataService -import com.morpho.app.model.uidata.BskyNotificationService -import com.morpho.app.model.uidata.ContentLabelService -import com.morpho.app.model.uidata.SettingsService +import com.morpho.app.model.bluesky.MorphoDataItem +import com.morpho.app.model.uidata.* import com.morpho.app.screens.base.BaseScreenModel import com.morpho.app.screens.login.LoginScreenModel import com.morpho.app.screens.main.MainScreenModel @@ -13,7 +11,9 @@ import com.morpho.app.screens.main.tabbed.TabbedMainScreenModel import com.morpho.app.screens.notifications.TabbedNotificationScreenModel import com.morpho.app.screens.profile.TabbedProfileViewModel import com.morpho.app.util.ClipboardManager +import com.morpho.butterfly.AtpAgent import com.morpho.butterfly.Butterfly +import com.morpho.butterfly.ButterflyAgent import com.morpho.butterfly.auth.SessionRepository import com.morpho.butterfly.auth.UserRepository import com.morpho.butterfly.auth.UserRepositoryImpl @@ -34,6 +34,9 @@ val appModule = module { factory { LoginScreenModel() } factory { p-> UpdateTick(p.get()) } single { ClipboardManager } + factory { p -> UserListPresenter(p.get()) } + factory { p -> UserFeedsPresenter(p.get()) } + factory> { p -> FeedPresenter(p.get()) } } val storageModule = module { @@ -44,6 +47,8 @@ val storageModule = module { val dataModule = module { single { Butterfly() } + single { AtpAgent() } + single { ButterflyAgent() } single { BskyDataService() } single { BskyNotificationService() } single { ContentLabelService() } diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/FeedSourceInfo.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/FeedSourceInfo.kt new file mode 100644 index 0000000..97b6f1c --- /dev/null +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/FeedSourceInfo.kt @@ -0,0 +1,164 @@ +package com.morpho.app.model.bluesky + +import androidx.compose.runtime.Immutable +import app.bsky.actor.FeedType +import app.bsky.feed.GeneratorView +import app.bsky.feed.GetFeedGeneratorQuery +import app.bsky.graph.GetListQuery +import app.bsky.graph.ListView +import com.morpho.butterfly.* +import dev.icerock.moko.parcelize.Parcelable +import dev.icerock.moko.parcelize.Parcelize +import kotlinx.serialization.Serializable + + + +@Serializable +@Immutable +enum class AuthorFilter { + PostsWithReplies, + PostsNoReplies, + PostsAuthorThreads, + PostsWithMedia, +} + +@Serializable +@Immutable +@Parcelize +sealed interface FeedDescriptor: Parcelable { + @Serializable + @Immutable + data object Home: FeedDescriptor + @Serializable + @Immutable + data class Author( + val did: Did, + val filter: AuthorFilter = AuthorFilter.PostsWithReplies + ): FeedDescriptor + @Serializable + @Immutable + data class FeedGen(val uri: AtUri): FeedDescriptor + @Serializable + @Immutable + data class Likes(val did: Did): FeedDescriptor + @Serializable + @Immutable + data class List(val uri: AtUri): FeedDescriptor +} + +@Serializable +@Immutable +@Parcelize +sealed interface FeedSourceInfo: Parcelable { + val uri: AtUri + val cid: Cid + val avatar: String? + val displayName: String? + val description: String? + val creatorDid: Did + val creatorHandle: Handle + val feedDescriptor: FeedDescriptor + val type: Nsid + + @Serializable + @Immutable + data class ListInfo( + override val uri: AtUri, + override val cid: Cid, + override val avatar: String?, + override val displayName: String?, + override val description: String?, + override val creatorDid: Did, + override val creatorHandle: Handle, + override val feedDescriptor: FeedDescriptor, + ): FeedSourceInfo { + override val type: Nsid = Nsid("app.bsky.feed.generator") + } + + @Serializable + @Immutable + data class FeedInfo( + override val uri: AtUri, + override val cid: Cid, + override val avatar: String?, + override val displayName: String?, + override val description: String?, + override val creatorDid: Did, + override val creatorHandle: Handle, + override val feedDescriptor: FeedDescriptor, + val likeCount: Long? = null, + val likeUri: AtUri? = null, + ): FeedSourceInfo { + override val type: Nsid = Nsid("app.bsky.graph.list") + } + + @Serializable + @Immutable + data object Home: FeedSourceInfo { + override val uri: AtUri = AtUri.HOME_URI + override val cid: Cid = Cid("home") + override val avatar: String? = null + override val displayName: String = "Home" + override val description: String = "Your home feed, currently same as Following" + override val creatorDid: Did = Did("did:web:morpho.app") + override val creatorHandle: Handle = Handle(displayName) + override val feedDescriptor: FeedDescriptor = FeedDescriptor.Home + override val type: Nsid = Nsid("app.morpho.feed.home") + } + + @Serializable + @Immutable + data object Following: FeedSourceInfo { + override val creatorDid: Did = Did("did:web:morpho.app") + override val uri: AtUri = AtUri.HOME_URI + override val cid: Cid = Cid("following") + override val avatar: String? = null + override val displayName: String = "Following" + override val description: String = "Your feed of people you follow" + override val creatorHandle: Handle = Handle(displayName) + override val feedDescriptor: FeedDescriptor = FeedDescriptor.Home + override val type: Nsid = Nsid("app.morpho.feed.following") + } +} + +fun GeneratorView.hydrateFeedGenerator(): FeedSourceInfo.FeedInfo { + return FeedSourceInfo.FeedInfo( + uri = this.uri, + cid = this.cid, + avatar = this.avatar, + displayName = this.displayName, + description = this.description, + creatorDid = this.creator.did, + creatorHandle = this.creator.handle, + feedDescriptor = FeedDescriptor.FeedGen(this.uri), + likeCount = this.likeCount, + likeUri = this.viewer?.like, + ) +} + +fun ListView.hydrateList(): FeedSourceInfo.ListInfo { + return FeedSourceInfo.ListInfo( + uri = this.uri, + cid = this.cid, + avatar = this.avatar, + displayName = this.name, + description = this.description, + creatorDid = this.creator.did, + creatorHandle = this.creator.handle, + feedDescriptor = FeedDescriptor.List(this.uri), + ) +} + +suspend fun app.bsky.actor.SavedFeed.toFeedSourceInfo(agent: ButterflyAgent): Result { + return when(this.type) { + FeedType.FEED -> { + agent.api.getFeedGenerator(GetFeedGeneratorQuery(AtUri(this.value))) + .map { feed -> feed.view.hydrateFeedGenerator() } + } + FeedType.LIST -> { + agent.api.getList(GetListQuery(AtUri(this.value), 1)) + .map { list -> list.list.hydrateList() } + } + FeedType.TIMELINE -> Result.success(FeedSourceInfo.Following) + } +} \ No newline at end of file diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/UISavedFeed.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/UISavedFeed.kt deleted file mode 100644 index b128c9f..0000000 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/UISavedFeed.kt +++ /dev/null @@ -1,87 +0,0 @@ -package com.morpho.app.model.bluesky - -import androidx.compose.runtime.Immutable -import app.bsky.actor.FeedType -import app.bsky.feed.GetFeedGeneratorQuery -import app.bsky.graph.GetListQuery -import com.morpho.butterfly.AtUri -import com.morpho.butterfly.Butterfly -import kotlinx.serialization.Serializable - -@Immutable -@Serializable -data class UISavedFeed( - public val avatar: String? = null, - public val title: String, - val description: String? = null, - public val type: UIFeedType, - public val pinned: Boolean, - val feed: FeedGenerator? = null, - val list: UserList? = null, -) - -@Immutable -@Serializable -sealed interface UIFeedType { - val type: FeedType - val value: String - - @Immutable - @Serializable - data class Feed( - val uri: AtUri - ): UIFeedType { - override val type: FeedType = FeedType.FEED - override val value: String = uri.atUri - } - - @Immutable - @Serializable - data class List( - val uri: AtUri - ): UIFeedType { - override val type: FeedType = FeedType.LIST - override val value: String = uri.atUri - } - - @Immutable - @Serializable - data object Timeline: UIFeedType { - override val type: FeedType = FeedType.TIMELINE - override val value: String = "following" - } -} - -suspend fun app.bsky.actor.SavedFeed.toUISavedFeed(api: Butterfly): UISavedFeed { - return when(this.type) { - FeedType.FEED -> { - val feed = api.api.getFeedGenerator(GetFeedGeneratorQuery(AtUri(this.value))) - .getOrNull() - UISavedFeed( - avatar = feed?.view?.avatar, - description = feed?.view?.description, - title = feed?.view?.displayName ?: this.value, - type = UIFeedType.Feed(AtUri(this.value)), - pinned = this.pinned, - feed = feed?.view?.toFeedGenerator() - ) - } - FeedType.LIST -> { - val list = api.api.getList(GetListQuery(AtUri(this.value))).getOrNull()?.list - UISavedFeed( - avatar = list?.avatar, - title = list?.name ?: this.value, - description = list?.description, - type = UIFeedType.List(AtUri(this.value)), - pinned = this.pinned, - list = list?.toList() - ) - } - FeedType.TIMELINE -> UISavedFeed( - avatar = null, - title = "Home", - type = UIFeedType.Timeline, - pinned = this.pinned - ) - } -} \ No newline at end of file diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/BskyDataService.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/BskyDataService.kt index 978fefe..d3cf44a 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/BskyDataService.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/BskyDataService.kt @@ -11,6 +11,7 @@ import app.bsky.labeler.GetServicesQuery import app.bsky.labeler.GetServicesResponseViewUnion import com.atproto.repo.GetRecordQuery import com.atproto.repo.StrongRef +import com.morpho.app.data.FeedTuner import com.morpho.app.di.UpdateTick import com.morpho.app.model.bluesky.* import com.morpho.app.model.uistate.ContentCardState diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/FeedEvent.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/FeedEvent.kt new file mode 100644 index 0000000..977b088 --- /dev/null +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/FeedEvent.kt @@ -0,0 +1,190 @@ +package com.morpho.app.model.uidata + +import app.bsky.actor.* +import com.atproto.repo.StrongRef +import com.morpho.app.model.bluesky.AuthorFilter +import com.morpho.app.model.bluesky.BskyPost +import com.morpho.app.model.bluesky.FeedDescriptor +import com.morpho.app.model.bluesky.FeedSourceInfo +import com.morpho.app.ui.common.ComposerRole +import com.morpho.butterfly.AtIdentifier +import com.morpho.butterfly.AtUri +import com.morpho.butterfly.Did +import com.morpho.butterfly.model.Timestamp +import io.github.vinceglb.filekit.core.PlatformFile +import kotlinx.datetime.Clock + +sealed interface Event { + data class UpdateSeenNotifications( + val seenAt: Timestamp = Clock.System.now() + ): Event + + +} +sealed interface ModerationEvent: Event + +sealed interface LoadEvent: Event +sealed interface ListEvent: Event + +sealed interface FeedEvent: Event { + val uri: AtUri? + data class Load( + val descriptor: FeedDescriptor, + ): FeedEvent, LoadEvent { + override val uri: AtUri = when(descriptor) { + is FeedDescriptor.Author -> when(descriptor.filter) { + AuthorFilter.PostsNoReplies -> AtUri.profilePostsUri(descriptor.did) + AuthorFilter.PostsWithReplies -> AtUri.profileRepliesUri(descriptor.did) + AuthorFilter.PostsAuthorThreads -> AtUri.profileMediaUri(descriptor.did) + AuthorFilter.PostsWithMedia -> AtUri.profileLikesUri(descriptor.did) + } + is FeedDescriptor.FeedGen -> descriptor.uri + is FeedDescriptor.Likes -> AtUri.profileLikesUri(descriptor.did) + is FeedDescriptor.List -> descriptor.uri + is FeedDescriptor.Home -> AtUri.HOME_URI + } + } + + data class LoadLists( + val actor: AtIdentifier, + ): FeedEvent, LoadEvent, ListEvent { + override val uri: AtUri = AtUri.myUserListUri(actor.toString()) + } + + data class LoadSaved( + val info: SavedFeed, + ): FeedEvent, LoadEvent { + override val uri: AtUri = AtUri(info.value) + } + + data class LoadHydrated( + val info: FeedSourceInfo, + ): FeedEvent, LoadEvent { + override val uri: AtUri = info.uri + } + + data class Peek( + val info: FeedSourceInfo + ): FeedEvent { + override val uri: AtUri = info.uri + } + + data class ComposePost( + val post: BskyPost, + val role: ComposerRole = ComposerRole.StandalonePost, + ): FeedEvent, PostEvent { + override val uri: AtUri? = null + } +} + +sealed interface FeedPageEvent: Event { + data class LikeFeed(val like: StrongRef): FeedPageEvent, LikeEvent + data class UnlikeFeed(val uri: AtUri): FeedPageEvent, LikeEvent + data class Save(val info: SavedFeed): FeedPageEvent, PrefsEvent + data class UnSave(val id: String): FeedPageEvent, PrefsEvent +} + +sealed interface CuratedListPage: Event { + data class Pin(val info: SavedFeed): CuratedListPage, PrefsEvent + data class Unpin(val id: String): CuratedListPage, PrefsEvent +} + +sealed interface ModListPage: Event { + data class MuteList(val list: AtUri): ModListPage, ModerationEvent + data class UnmuteList(val uri: AtUri): ModListPage, ModerationEvent + data class BlockList(val list: AtUri): ModListPage, ModerationEvent + data class UnblockList(val uri: AtUri): ModListPage, ModerationEvent +} + +sealed interface PrefsEvent: Event { + data class MuteWord(val word: MutedWord): PrefsEvent + data class UnMuteWord(val word: MutedWord): PrefsEvent + data class SetThreadViewPref(val pref: ThreadViewPref): PrefsEvent + data class SetFeedViewPref(val feed: String, val feedViewPref: FeedViewPref): PrefsEvent +} + +sealed interface ListDataEvent: Event { + data class LoadActor( + val actor: AtIdentifier + ): ListDataEvent, LoadEvent + + data class LoadFromPost( + val post: AtUri + ): ListDataEvent, LoadEvent +} + +sealed interface SearchEvent: Event { + val query: String? + + data class Actors( + val term: String? = null, + override val query: String? = null, + ): SearchEvent + + data class ActorsTypeahead( + val term: String? = null, + override val query: String? = null, + ): SearchEvent + + data class Posts( + override val query: String? = null, + ): SearchEvent +} + +// Unsure about some of these, maybe events should only be repeatable things? +sealed interface LikeEvent: Event + +sealed interface PostEvent: Event { + data class Reply(val post: BskyPost): PostEvent + data class Quote(val post: BskyPost): PostEvent + + + data class LikePost(val like: StrongRef): PostEvent, LikeEvent + data class UnlikePost(val uri: AtUri): PostEvent, LikeEvent + data class Repost(val repost: StrongRef): PostEvent + data class DeleteRepost(val uri: AtUri): PostEvent + + data class Hide(val uri: AtUri): PostEvent, PrefsEvent, ModerationEvent + data class Unhide(val uri: AtUri): PostEvent, PrefsEvent, ModerationEvent + + data class LoadThread(val post: AtUri): PostEvent, LoadEvent + data class ReportPost(val subject: StrongRef): PostEvent, ModerationEvent +} + +sealed interface LabelerEvent: Event { + data class LikeLabeler(val like: StrongRef): LabelerEvent, LikeEvent + data class UnlikeLabeler(val uri: AtUri): LabelerEvent, LikeEvent + data class Subscribe(val did: Did): LabelerEvent, PrefsEvent, ModerationEvent + data class Unsubscribe(val did: Did): LabelerEvent, PrefsEvent, ModerationEvent + + data class SetLabelPref( + val label: String, + val value: Visibility, + val labeler: Did, + ): LabelerEvent, PrefsEvent, ModerationEvent +} + +sealed interface MyProfileEvent: Event { + data object EnterEditing: MyProfileEvent + data object ExitEditing: MyProfileEvent +} + +sealed interface ProfileEditEvent: Event { + data class SetDisplayName(val name: String): ProfileEditEvent, MyProfileEvent + data class SetDescription(val description: String): ProfileEditEvent, MyProfileEvent + data class SetAvatar(val avatar: PlatformFile): ProfileEditEvent, MyProfileEvent + data class SetBanner(val banner: PlatformFile): ProfileEditEvent, MyProfileEvent +} + +sealed interface ActorEvent: Event { + data class Follow(val subject: Did): ActorEvent + data class Unfollow(val uri: AtUri): ActorEvent + + data class Mute(val subject: Did): ActorEvent, PrefsEvent, ModerationEvent + data class Unmute(val subject: Did): ActorEvent, PrefsEvent, ModerationEvent + + data class Block(val subject: Did): ActorEvent, ModerationEvent + data class Unblock(val uri: AtUri): ActorEvent, ModerationEvent + + data class ReportAccount(val subject: Did): ActorEvent, ModerationEvent +} \ No newline at end of file diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/FeedPresenter.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/FeedPresenter.kt new file mode 100644 index 0000000..b81c617 --- /dev/null +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/FeedPresenter.kt @@ -0,0 +1,223 @@ +package com.morpho.app.model.uidata + +import app.bsky.feed.GetAuthorFeedFilter +import app.bsky.feed.GetFeedQuery +import app.bsky.feed.GetListFeedQuery +import app.bsky.graph.GetListQuery +import app.cash.paging.Pager +import com.morpho.app.data.FeedTuner +import com.morpho.app.data.MorphoDataSource +import com.morpho.app.data.MorphoFeedSource +import com.morpho.app.model.bluesky.* +import com.morpho.butterfly.ButterflyAgent +import com.morpho.butterfly.Cursor +import com.morpho.butterfly.FeedRequest +import com.morpho.butterfly.PagedResponse +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map + +class FeedPresenter( + descriptor: FeedDescriptor? = null, +): Presenter() { + + private var dataSource: MorphoFeedSource = + descriptor?.getDataSource(agent) ?: getTimelineDataSource(agent) + + override var pager: Pager = run { + val pagingConfig = MorphoDataSource.defaultConfig + Pager(pagingConfig) { + dataSource + } + } + + private fun switchPager(newDataSource: MorphoFeedSource) { + dataSource = newDataSource + pager = Pager(MorphoDataSource.defaultConfig) { + dataSource + } + } + + override fun produceUpdates(events: Flow): Flow = events.map { event -> + when(event) { + is FeedEvent.ComposePost -> UIUpdate.OpenComposer(event.post, event.role) + is FeedEvent.Load -> { + switchPager(event.descriptor.getDataSource(agent)) + when(event.descriptor) { + is FeedDescriptor.Author -> AuthorFeedUpdate.Feed(event.descriptor.did, event.descriptor.filter, pager.flow) + is FeedDescriptor.FeedGen -> { + val info = agent.api.getList(GetListQuery(event.descriptor.uri, 1)) + .map { it.list.hydrateList() } + if(info.isSuccess) { + switchPager(info.getOrThrow().getDataSource(agent)) + FeedUpdate.Feed(info.getOrThrow(), pager.flow) + } else { + FeedUpdate.Error(info.exceptionOrNull()?.message ?: + "Failed to load saved feed: ${event.descriptor}, error: $info") + } + } + FeedDescriptor.Home -> FeedUpdate.Feed(FeedSourceInfo.Home, pager.flow) + is FeedDescriptor.Likes -> AuthorFeedUpdate.Likes(event.descriptor.did, pager.flow) + is FeedDescriptor.List -> FeedUpdate.Error("Internal error: LoadLists should not be sent to this presenter") + } + } + is FeedEvent.LoadLists -> FeedUpdate.Error("Internal error: LoadLists should not be sent to this presenter") + is FeedEvent.LoadHydrated -> { + switchPager(event.info.getDataSource(agent)) + FeedUpdate.Feed(event.info, pager.flow) + } + is FeedEvent.LoadSaved -> { + val info = event.info.toFeedSourceInfo(agent) + if(info.isSuccess) { + switchPager(info.getOrThrow().getDataSource(agent)) + FeedUpdate.Feed(info.getOrThrow(), pager.flow) + } else { + FeedUpdate.Error(info.exceptionOrNull()?.message ?: "Failed to load saved feed: ${event.info}") + } + } + is FeedEvent.Peek -> FeedUpdate.Peek(event.info, dataSource.updates()) + else -> FeedUpdate.Error("Unknown event type: $event") + } + } +} + +fun FeedSourceInfo.getDataSource( + agent: ButterflyAgent, +): MorphoFeedSource { + return when(this) { + is FeedSourceInfo.FeedInfo -> getFeedDataSource(this.feedDescriptor as FeedDescriptor.FeedGen, agent) + is FeedSourceInfo.ListInfo -> getListDataSource(this.feedDescriptor as FeedDescriptor.List, agent) + else -> getTimelineDataSource(agent) + } +} + +fun FeedDescriptor.getDataSource( + agent: ButterflyAgent, +): MorphoFeedSource { + return when(this) { + is FeedDescriptor.FeedGen -> getFeedDataSource(this, agent) + is FeedDescriptor.List -> getListDataSource(this, agent) + is FeedDescriptor.Home -> getTimelineDataSource(agent) + is FeedDescriptor.Author -> getAuthorFeedDataSource(this, agent) + is FeedDescriptor.Likes -> getLikesDataSource(this, agent) + } +} + +fun getLikesDataSource( + descriptor: FeedDescriptor.Likes, + agent: ButterflyAgent +): MorphoFeedSource { + val request: FeedRequest = { cursor, limit -> + agent.getActorLikes(descriptor.did, limit, cursor.value).map { response -> + val newCursor = response.cursor + val items = response.items + .map { MorphoDataItem.FeedItem.fromFeedViewPost(it) } as List + PagedResponse.Feed(newCursor, items) + } + } + val tuners = agent.id?.let { + FeedTuner.useFeedTuners(it, agent.prefs, descriptor) + } ?: listOf>() + return MorphoFeedSource(request, tuners, repliesBumpThreads = true) +} + +fun getAuthorFeedDataSource( + descriptor: FeedDescriptor.Author, + agent: ButterflyAgent +): MorphoFeedSource { + val request: FeedRequest = when(descriptor.filter) { + AuthorFilter.PostsWithReplies -> { cursor, limit -> + agent.getAuthorFeed(descriptor.did, limit, cursor.value, GetAuthorFeedFilter.POSTS_WITH_REPLIES) + .map { response -> + val newCursor = response.cursor + val items = response.items + .map { MorphoDataItem.FeedItem.fromFeedViewPost(it) } as List + PagedResponse.Feed(newCursor, items) + } + } + AuthorFilter.PostsNoReplies -> { cursor, limit -> + agent.getAuthorFeed(descriptor.did, limit, cursor.value, GetAuthorFeedFilter.POSTS_NO_REPLIES) + .map { response -> + val newCursor = response.cursor + val items = response.items + .map { MorphoDataItem.FeedItem.fromFeedViewPost(it) } as List + PagedResponse.Feed(newCursor, items) + } + } + AuthorFilter.PostsAuthorThreads -> { cursor, limit -> + agent.getAuthorFeed(descriptor.did, limit, cursor.value, GetAuthorFeedFilter.POSTS_WITH_REPLIES) + .map { response -> + val newCursor = response.cursor + val items = response.items + .map { MorphoDataItem.FeedItem.fromFeedViewPost(it) } as List + PagedResponse.Feed(newCursor, items) + } + } + AuthorFilter.PostsWithMedia -> { cursor, limit -> + agent.getAuthorFeed(descriptor.did, limit, cursor.value, GetAuthorFeedFilter.POSTS_WITH_MEDIA) + .map { response -> + val newCursor = response.cursor + val items = response.items + .map { MorphoDataItem.FeedItem.fromFeedViewPost(it) } as List + PagedResponse.Feed(newCursor, items) + } + } + } + val tuners = agent.id?.let { + FeedTuner.useFeedTuners(it, agent.prefs, descriptor) + } ?: listOf>() + return MorphoFeedSource(request, tuners, repliesBumpThreads = true) +} + + +fun getFeedDataSource( + descriptor: FeedDescriptor.FeedGen, + agent: ButterflyAgent +): MorphoFeedSource { + val request: FeedRequest = { cursor, limit -> + agent.api.getFeed(GetFeedQuery(descriptor.uri, limit, cursor.value)).map { response -> + val newCursor = Cursor(response.cursor) + val items = response.feed + .map { MorphoDataItem.FeedItem.fromFeedViewPost(it) } as List + PagedResponse.Feed(newCursor, items) + } + } + val tuners = agent.id?.let { + FeedTuner.useFeedTuners(it, agent.prefs, descriptor) + } ?: listOf>() + return MorphoFeedSource(request, tuners, repliesBumpThreads = true) +} + +fun getListDataSource( + descriptor: FeedDescriptor.List, + agent: ButterflyAgent +): MorphoFeedSource { + val request: FeedRequest = { cursor, limit -> + agent.api.getListFeed(GetListFeedQuery(descriptor.uri, limit, cursor.value)).map { response -> + val newCursor = Cursor(response.cursor) + val items = response.feed + .map { MorphoDataItem.FeedItem.fromFeedViewPost(it) } as List + PagedResponse.Feed(newCursor, items) + } + } + val tuners = agent.id?.let { + FeedTuner.useFeedTuners(it, agent.prefs, descriptor) + } ?: listOf>() + return MorphoFeedSource(request, tuners, repliesBumpThreads = true) +} + +fun getTimelineDataSource( + agent: ButterflyAgent +): MorphoFeedSource { + val request: FeedRequest = { cursor, limit -> + agent.getTimeline(cursor = cursor.value, limit = limit).map { response -> + val newCursor = response.cursor + val items = response.items + .map { MorphoDataItem.FeedItem.fromFeedViewPost(it) } as List + PagedResponse.Feed(newCursor, items) + } + } + val tuners = agent.id?.let { + FeedTuner.useFeedTuners(it, agent.prefs, FeedDescriptor.Home) + } ?: listOf>() + return MorphoFeedSource(request, tuners, repliesBumpThreads = true) +} \ No newline at end of file diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/ListPresenter.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/ListPresenter.kt new file mode 100644 index 0000000..d723c84 --- /dev/null +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/ListPresenter.kt @@ -0,0 +1,128 @@ +package com.morpho.app.model.uidata + +import app.bsky.feed.GetActorFeedsQuery +import app.bsky.graph.GetListsQuery +import app.cash.paging.Pager +import com.morpho.app.data.MorphoDataSource +import com.morpho.app.model.bluesky.MorphoDataItem +import com.morpho.app.model.bluesky.toFeedGenerator +import com.morpho.app.model.bluesky.toList +import com.morpho.butterfly.AtIdentifier +import com.morpho.butterfly.ButterflyAgent +import com.morpho.butterfly.Cursor +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map +import org.koin.core.component.KoinComponent +import org.koin.core.component.inject + +abstract class Presenter: KoinComponent { + val agent: ButterflyAgent by inject() + abstract var pager: Pager + abstract fun produceUpdates(events: Flow): Flow +} + +class UserListPresenter( + val actor: AtIdentifier, +): Presenter() { + override var pager: Pager = run { + val pagingConfig = MorphoDataSource.defaultConfig + Pager(pagingConfig) { + UserListFeedSource(actor) + } + } + + override fun produceUpdates(events: Flow): Flow = events.map { event -> + when(event) { + is FeedEvent.LoadLists -> AuthorFeedUpdate.Lists(actor, pager.flow) + else -> AuthorFeedUpdate.Error("Unknown event type: $event") + } + } + +} + +class UserListFeedSource( + val actor: AtIdentifier, +): MorphoDataSource() { + + override suspend fun load(params: LoadParams): LoadResult { + try { + val limit = params.loadSize + val loadCursor = when(params) { + is LoadParams.Append -> params.key + is LoadParams.Prepend -> Cursor.Empty + is LoadParams.Refresh -> Cursor.Empty + } + return agent.api.getLists(GetListsQuery(actor, limit.toLong(), loadCursor.value)).map { response -> + val newCursor = Cursor(response.cursor) + val items = response.lists + .map { MorphoDataItem.ListInfo(it.toList()) } + LoadResult.Page( + data = items, + prevKey = when(params) { + is LoadParams.Append -> loadCursor + is LoadParams.Prepend -> Cursor.Empty + is LoadParams.Refresh -> Cursor.Empty + }, + nextKey = newCursor, + ) + }.onFailure { + return LoadResult.Error(it) + }.getOrDefault(LoadResult.Error(Exception("Load failed"))) + } catch (e: Exception) { + return LoadResult.Error(e) + } + } +} + +class UserFeedsPresenter( + val actor: AtIdentifier, +): Presenter() { + override var pager: Pager = run { + val pagingConfig = MorphoDataSource.defaultConfig + Pager(pagingConfig) { + UserFeedsFeedSource(actor) + } + } + + override fun produceUpdates(events: Flow): Flow = events.map { event -> + when(event) { + is FeedEvent.LoadLists -> AuthorFeedUpdate.Feeds(actor, pager.flow) + else -> AuthorFeedUpdate.Error("Unknown event type: $event") + } + } + +} + +class UserFeedsFeedSource( + val actor: AtIdentifier, +): MorphoDataSource() { + + override suspend fun load(params: LoadParams): LoadResult { + try { + val limit = params.loadSize + val loadCursor = when(params) { + is LoadParams.Append -> params.key + is LoadParams.Prepend -> Cursor.Empty + is LoadParams.Refresh -> Cursor.Empty + } + return agent.api.getActorFeeds(GetActorFeedsQuery(actor, limit.toLong(), loadCursor.value)).map { response -> + val newCursor = Cursor(response.cursor) + val items = response.feeds + .map { MorphoDataItem.FeedInfo(it.toFeedGenerator()) } + LoadResult.Page( + data = items, + prevKey = when(params) { + is LoadParams.Append -> loadCursor + is LoadParams.Prepend -> Cursor.Empty + is LoadParams.Refresh -> Cursor.Empty + }, + nextKey = newCursor, + ) + }.onFailure { + return LoadResult.Error(it) + }.getOrDefault(LoadResult.Error(Exception("Load failed"))) + } catch (e: Exception) { + return LoadResult.Error(e) + } + } +} \ No newline at end of file diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/MorphoData.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/MorphoData.kt index 159be1d..619d484 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/MorphoData.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/MorphoData.kt @@ -4,13 +4,12 @@ package com.morpho.app.model.uidata import androidx.compose.runtime.Immutable import androidx.compose.ui.util.* import app.bsky.feed.FeedViewPost -import com.morpho.app.data.BskyUserPreferences +import com.morpho.app.data.FeedTuner import com.morpho.app.model.bluesky.* import com.morpho.app.model.uistate.FeedType import com.morpho.butterfly.* import dev.icerock.moko.parcelize.Parcelable import dev.icerock.moko.parcelize.Parcelize -import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toPersistentList import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.* @@ -570,266 +569,3 @@ fun areSameAuthor(authors: AuthorContext): Boolean { } return true } - - -@Serializable -data class FeedTuner(val tuners: List = persistentListOf()) { - val seenKeys = mutableSetOf() - val seenUris = mutableSetOf() - val seenRootUris = mutableSetOf() - - companion object { - - fun useFeedTuners( - prefs: BskyUserPreferences, - feed: MorphoData - ): List { - if(feed.isProfileFeed) { - when(feed.feedType) { - FeedType.PROFILE_POSTS -> return listOf(FeedTuner(tuners = persistentListOf(::removeReplies))) - FeedType.PROFILE_USER_LISTS -> return listOf() - FeedType.PROFILE_FEEDS_LIST -> return listOf() - FeedType.PROFILE_MOD_SERVICE -> return listOf() - else -> {} - } - } - val languages = prefs.preferences.languages.toList() - val languageTuner: TunerFunction = { f, t -> - preferredLanguageOnly(languages, f, t) - } - if(feed.feedType == FeedType.OTHER) { - return listOf(FeedTuner(tuners = persistentListOf(languageTuner))) - } - if(feed.feedType == FeedType.LIST_FOLLOWING || feed.feedType == FeedType.HOME) { - val userDid = Did(prefs.user.userDid) - val tuners = mutableListOf(FeedTuner(tuners = persistentListOf(::removeOrphans))) - val feedPrefs = prefs.preferences.feedViewPrefs[feed.uri.atUri] ?: return tuners.toList() - if(feedPrefs.hideReposts) tuners.add(FeedTuner(tuners = persistentListOf(::removeReposts))) - if(feedPrefs.hideReplies) tuners.add(FeedTuner(tuners = persistentListOf(::removeReplies))) - else { - val followedRepliesOnly: TunerFunction = { f, t -> - followedRepliesOnly(userDid, f, t) - } - tuners.add(FeedTuner(tuners = persistentListOf(followedRepliesOnly))) - } - if(feedPrefs.hideQuotePosts) tuners.add(FeedTuner(tuners = persistentListOf(::removeQuotePosts))) - tuners.add(FeedTuner(tuners = persistentListOf(::dedupThreads))) - return tuners.toList() - } - return listOf() - } - - fun removeReplies( - feed: List, - tuner: FeedTuner = FeedTuner(), - ): List { - return feed.filterNot { item -> - when(item) { - is MorphoDataItem.Post -> item.isReply && !item.isRepost && - !(item.getAuthors()?.let { areSameAuthor(it) } ?: false) - is MorphoDataItem.Thread -> !(item.getAuthors()?.let { areSameAuthor(it) } ?: false) - } - } - - } - - fun removeReposts( - feed: List, - tuner: FeedTuner = FeedTuner(), - ): List { - return feed.filterNot { item -> - item.isRepost - } - } - - fun removeQuotePosts( - feed: List, - tuner: FeedTuner = FeedTuner(), - ): List { - return feed.filterNot { item -> - item.isQuotePost - } - } - - fun removeOrphans( - feed: List, - tuner: FeedTuner = FeedTuner(), - ): List { - return feed.filterNot { item -> - when(item) { - is MorphoDataItem.Post -> item.isOrphan - is MorphoDataItem.Thread -> false - } - } - } - - fun dedupThreads( - feed: List, - tuner: FeedTuner = FeedTuner(), - ): List { - return feed.filterNot { item -> - val rootUri = item.rootUri - if(!item.isRepost == tuner.seenRootUris.contains(rootUri)) { - false - } else { - tuner.seenRootUris.add(rootUri) - true - } - } - } - - fun followedRepliesOnly( - userDid: Did, - feed: List, - tuner: FeedTuner = FeedTuner(), - ): List { - return feed.filterNot { item -> - item.isReply && !shouldDisplayReplyInFollowing(item, userDid) - } - } - - fun preferredLanguageOnly( - languages: List = persistentListOf(), - feed: List, - tuner: FeedTuner = FeedTuner(), - ): List { - if (languages.isEmpty()) return feed - val newFeed = feed.filter { item -> - when(item) { - is MorphoDataItem.Post -> { - item.post.langs.isEmpty() || - item.post.langs.any { languages.contains(it) } - } - is MorphoDataItem.Thread -> { - item.thread.post.langs.isEmpty() || - item.thread.post.langs.any { languages.contains(it) } - } - } - }.map { item -> - when(item) { - is MorphoDataItem.Post -> item - is MorphoDataItem.Thread -> { - item.copy( - thread = item.thread.filterReplies { reply -> - when(reply) { - is ThreadPost.ViewablePost -> reply.post.langs.isEmpty() || - reply.post.langs.any { languages.contains(it) } - is ThreadPost.BlockedPost -> true - is ThreadPost.NotFoundPost -> true - } - - } - ) - } - } - } - return newFeed.ifEmpty { feed } - } - } - fun tune( - feed: MorphoData - ): MorphoData { - var workingFeed = feed.items - tuners.forEach { tuner -> - workingFeed = tuner(workingFeed, this) - } - workingFeed = workingFeed.map { item -> - if(seenKeys.contains(item.key)) return@map null - else if(item is MorphoDataItem.Thread) { - val itemUris = item.getUris() - val seenInThisThread = itemUris.filter { seenUris.contains(it) } - if(seenInThisThread.isNotEmpty()) { - if(seenInThisThread.size == itemUris.size) { - return@map null - } else { - val newParents = item.thread.parents.filter { parent -> - when(parent) { - is ThreadPost.ViewablePost -> parent.post.uri in seenInThisThread - is ThreadPost.BlockedPost -> false - is ThreadPost.NotFoundPost -> false - } - } - val newThread = item.copy(thread = item.thread.filterReplies { reply -> - when(reply) { - is ThreadPost.ViewablePost -> reply.post.uri in seenInThisThread - is ThreadPost.BlockedPost -> false - is ThreadPost.NotFoundPost -> false - } - }.copy(parents = newParents)) - seenUris.addAll(itemUris) - if(newThread.thread.replies.isEmpty() && newThread.thread.parents.isEmpty()) { - return@map null - } else { - return@map newThread - } - } - } else { - seenUris.addAll(itemUris) - item - } - } else { - val disableDedub = item.isReply && item.isRepost - if(!disableDedub) seenKeys.add(item.key) - item - } - }.filterNotNull() - return feed.copy(items = workingFeed) - } - -} - -/// Algo copied from official app -/// https://github.com/bluesky-social/social-app/blob/main/src/lib/api/feed-manip.ts#L445 -/// as of commit https://github.com/bluesky-social/social-app/commit/e2a244b99889743a8788b0c464d3e150bc8047ad -/// The algorithm is a controversial, so we may want to change it or offer more options. -fun shouldDisplayReplyInFollowing( - item: MorphoDataItem.FeedItem, - userDid: Did, -): Boolean { - val authors = item.getAuthors() - val author = authors?.author - val rootAuthor = authors?.rootAuthor - val parentAuthor = authors?.parentAuthor - val grandParentAuthor = authors?.grandParentAuthor - if (!isSelfOrFollowing(author, userDid)) - return false // Only show replies from self or people you follow. - - if(parentAuthor == null || parentAuthor.did == author?.did - && rootAuthor == null || rootAuthor?.did == author?.did - && grandParentAuthor == null || grandParentAuthor?.did == author?.did - ) return true // Always show self-threads. - - if ( - parentAuthor.did != author?.did && - rootAuthor?.did == author?.did && - item is MorphoDataItem.Thread - ) { - // If you follow A, show A -> someone[>0 likes] -> A chains too. - // This is different from cases below because you only know one person. - val parentPost = when(val p = item.thread.parents.lastOrNull()) { - is ThreadPost.ViewablePost -> p.post - else -> null - } - if(parentPost != null && parentPost.likeCount > 0) - return true - } - // From this point on we need at least one more reason to show it. - if ( - parentAuthor.did != author?.did && isSelfOrFollowing(parentAuthor, userDid) - ) return true - if ( - grandParentAuthor != null && - grandParentAuthor.did != author?.did && - isSelfOrFollowing(grandParentAuthor, userDid) - ) return true - if ( - rootAuthor != null && - rootAuthor.did != author?.did && - isSelfOrFollowing(rootAuthor, userDid) - ) return true - return false -} - -fun isSelfOrFollowing(profile: Profile?, userDid: Did): Boolean { - return profile?.did == userDid || profile?.followedByMe == true -} \ No newline at end of file diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/UIUpdate.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/UIUpdate.kt new file mode 100644 index 0000000..eb6a125 --- /dev/null +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/UIUpdate.kt @@ -0,0 +1,92 @@ +package com.morpho.app.model.uidata + +import app.cash.paging.PagingData +import com.morpho.app.model.bluesky.* +import com.morpho.app.ui.common.ComposerRole +import com.morpho.butterfly.AtIdentifier +import kotlinx.coroutines.flow.Flow + +sealed interface UIUpdate { + data class OpenComposer( + val initialContent: BskyPost, + val role: ComposerRole, + ): UIUpdate +} + +sealed interface SearchUpdate: UIUpdate { + data object Empty: SearchUpdate + + data class Error(val error: String): SearchUpdate + + data class ProfileSearchResults( + val query: String? = null, + val term: String? = null, + val results: Flow>, + ): SearchUpdate + + data class ProfileSearchTypeahead( + val query: String? = null, + val term: String? = null, + val results: Flow>, + ): SearchUpdate + + data class PostSearchResults( + val query: String? = null, + val results: Flow>, + ): SearchUpdate +} + +sealed interface FeedUpdate: UIUpdate { + data object Empty: FeedUpdate + + data class Error(val error: String): FeedUpdate + + data class Feed( + val info: FeedSourceInfo, + val feed: Flow>, + ): FeedUpdate + + data class Peek( + val info: FeedSourceInfo, + val post: Flow, + ): FeedUpdate +} + +sealed interface AuthorFeedUpdate: UIUpdate { + + data object Empty: AuthorFeedUpdate + + data class Error(val error: String): AuthorFeedUpdate + + data class Feed( + val actor: AtIdentifier, + val filter: AuthorFilter, + val feed: Flow>, + ): AuthorFeedUpdate + + data class Likes( + val actor: AtIdentifier, + val feed: Flow>, + ): AuthorFeedUpdate + + data class Lists( + val actor: AtIdentifier, + val feed: Flow>, + ): AuthorFeedUpdate + + data class Feeds( + val actor: AtIdentifier, + val feed: Flow>, + ): AuthorFeedUpdate +} + + +sealed interface ThreadUpdate: UIUpdate { + data object Empty: ThreadUpdate + + data class Error(val error: String): ThreadUpdate + + data class Thread( + val results: Flow, + ): ThreadUpdate +} \ No newline at end of file From 5e19db1952c6370518c7beee8bc2eae0d39d7bb6 Mon Sep 17 00:00:00 2001 From: Orual Date: Sun, 15 Sep 2024 22:31:16 -0400 Subject: [PATCH 09/42] Big UI refactoring ongoing. --- .../kotlin/com/morpho/app/Platform.android.kt | 9 + .../kotlin/com/morpho/app/Platform.apple.kt | 8 +- .../kotlin/com/morpho/app/Platform.kt | 3 +- .../kotlin/com/morpho/app/data/MorphoAgent.kt | 11 + .../com/morpho/app/data/MorphoDataSource.kt | 21 +- .../kotlin/com/morpho/app/data/SharedImage.kt | 8 +- .../com/morpho/app/model/bluesky/BskyLabel.kt | 225 +--- .../app/model/bluesky/BskyPostThread.kt | 58 + .../com/morpho/app/model/bluesky/DraftPost.kt | 14 +- .../app/model/uidata/BskyDataService.kt | 1141 ----------------- .../app/model/uidata/ContentCardMapEntry.kt | 57 +- .../app/model/uidata/ContentLabelService.kt | 1098 +++------------- .../com/morpho/app/model/uidata/FeedEvent.kt | 3 +- .../morpho/app/model/uidata/FeedPresenter.kt | 28 +- .../morpho/app/model/uidata/ListPresenter.kt | 47 +- .../com/morpho/app/model/uidata/MorphoData.kt | 13 +- .../app/model/uidata/SettingsService.kt | 356 ----- .../com/morpho/app/model/uidata/UIUpdate.kt | 11 +- .../app/model/uistate/TabbedScreenState.kt | 9 +- .../app/screens/base/BaseScreenModel.kt | 43 +- .../app/screens/main/MainScreenModel.kt | 627 +-------- .../main/tabbed/TabbedMainScreenModel.kt | 204 +-- .../morpho/app/ui/common/SkylineFragment.kt | 214 ++-- .../app/ui/common/TabbedSkylineFragment.kt | 83 +- .../morpho/app/ui/elements/ContentHider.kt | 33 +- .../app/ui/post/NotFoundPostFragment.kt | 151 ++- .../com/morpho/app/ui/post/PostActions.kt | 49 + .../com/morpho/app/ui/post/PostFragment.kt | 1 - .../kotlin/com/morpho/app/util/BlueskyText.kt | 11 +- .../kotlin/com/morpho/app/Platform.desktop.kt | 15 +- .../kotlin/com/morpho/app/Platform.jvm.kt | 6 - .../kotlin/com/morpho/app/Platform.native.kt | 6 + 32 files changed, 801 insertions(+), 3762 deletions(-) create mode 100644 Morpho/composeApp/src/androidMain/kotlin/com/morpho/app/Platform.android.kt create mode 100644 Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/data/MorphoAgent.kt delete mode 100644 Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/BskyDataService.kt delete mode 100644 Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/SettingsService.kt delete mode 100644 Morpho/composeApp/src/desktopMain/kotlin/com/morpho/app/Platform.jvm.kt create mode 100644 Morpho/composeApp/src/nativeMain/kotlin/com/morpho/app/Platform.native.kt diff --git a/Morpho/composeApp/src/androidMain/kotlin/com/morpho/app/Platform.android.kt b/Morpho/composeApp/src/androidMain/kotlin/com/morpho/app/Platform.android.kt new file mode 100644 index 0000000..b1717d1 --- /dev/null +++ b/Morpho/composeApp/src/androidMain/kotlin/com/morpho/app/Platform.android.kt @@ -0,0 +1,9 @@ +package com.morpho.app + +import java.util.Locale + +actual val myLang:String? + get() = Locale.getDefault().language + +actual val myCountry:String? + get() = Locale.getDefault().country \ No newline at end of file diff --git a/Morpho/composeApp/src/appleMain/kotlin/com/morpho/app/Platform.apple.kt b/Morpho/composeApp/src/appleMain/kotlin/com/morpho/app/Platform.apple.kt index ab7f6d4..6309ccc 100644 --- a/Morpho/composeApp/src/appleMain/kotlin/com/morpho/app/Platform.apple.kt +++ b/Morpho/composeApp/src/appleMain/kotlin/com/morpho/app/Platform.apple.kt @@ -3,4 +3,10 @@ package com.morpho.app // For Android @Parcelize @Target(AnnotationTarget.PROPERTY) @Retention(AnnotationRetention.SOURCE) -actual annotation class CommonRawValue \ No newline at end of file +actual annotation class CommonRawValue + +actual val myLang:String? + get() = NSLocale.currentLocale.languageCode + +actual val myCountry:String? + get() = NSLocale.currentLocale.countryCode \ No newline at end of file diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/Platform.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/Platform.kt index aac891b..8f6eb6b 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/Platform.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/Platform.kt @@ -9,7 +9,8 @@ interface Platform { expect fun getPlatform(): Platform - +expect val myLang:String? +expect val myCountry:String? // For Android @Parcelize @OptIn(ExperimentalMultiplatform::class) diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/data/MorphoAgent.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/data/MorphoAgent.kt new file mode 100644 index 0000000..1f1db96 --- /dev/null +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/data/MorphoAgent.kt @@ -0,0 +1,11 @@ +package com.morpho.app.data + +import com.morpho.app.myLang +import com.morpho.butterfly.ButterflyAgent +import com.morpho.butterfly.Language +import org.koin.core.component.inject + +class MorphoAgent: ButterflyAgent() { + val morphoPrefsRepo: PreferencesRepository by inject() + val myLanguage = Language(myLang ?: "en") // TODO: make this configurable +} \ No newline at end of file diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/data/MorphoDataSource.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/data/MorphoDataSource.kt index 7ffcfbd..5217809 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/data/MorphoDataSource.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/data/MorphoDataSource.kt @@ -5,6 +5,7 @@ import app.cash.paging.PagingConfig import app.cash.paging.PagingSource import app.cash.paging.PagingState import com.morpho.app.model.bluesky.* +import com.morpho.app.model.uidata.ContentLabelService import com.morpho.app.model.uidata.Delta import com.morpho.app.model.uidata.Moment import com.morpho.butterfly.ButterflyAgent @@ -23,7 +24,9 @@ import kotlin.time.Duration abstract class MorphoDataSource: PagingSource(), KoinComponent { - val agent: ButterflyAgent by inject() + val agent: MorphoAgent by inject() + val moderator: ContentLabelService by inject() + override fun getRefreshKey(state: PagingState): Cursor? { return state.anchorPosition?.let { anchorPosition -> val anchorPage = state.closestPageToPosition(anchorPosition) @@ -64,12 +67,13 @@ data class MorphoFeedSource( val tunedList = when(pagedList) { is PagedResponse.Feed -> { var tunedFeed = pagedList.copy( - items = if(collectThreads) pagedList.items - .collectThreads( - repliesBumpThreads = repliesBumpThreads, - agent = agent - ).getOrNull() ?: pagedList.items - else pagedList.items + items = if(collectThreads) { + pagedList.items.filter { !moderator.shouldHideItem(it) } + .collectThreads( + repliesBumpThreads = repliesBumpThreads, + agent = agent + ).getOrNull() ?: pagedList.items + } else pagedList.items ) tuners.forEach { tuner -> tunedFeed = tuner.tune(tunedFeed) @@ -317,4 +321,5 @@ suspend fun List.collectThreads( } } return Result.success(sortedFeed as List) -} \ No newline at end of file +} + diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/data/SharedImage.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/data/SharedImage.kt index 849d6a9..997672f 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/data/SharedImage.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/data/SharedImage.kt @@ -2,8 +2,7 @@ package com.morpho.app.data import androidx.compose.ui.graphics.ImageBitmap import app.bsky.embed.AspectRatio -import com.morpho.app.util.deserialize -import com.morpho.butterfly.Butterfly +import com.morpho.butterfly.ButterflyAgent import com.morpho.butterfly.model.Blob import io.github.vinceglb.filekit.core.PlatformFile import io.ktor.util.encodeBase64 @@ -56,10 +55,9 @@ constructor(override val descriptor: SerialDescriptor = PrimitiveSerialDescripto } -suspend fun imageToBlob(image: SharedImage, api: Butterfly): Blob? { +suspend fun imageToBlob(image: SharedImage, agent: ButterflyAgent): Blob? { val byteArray = image.toByteArray(targetSize = MAX_SIZE) ?: return null - val resp = api.api.uploadBlob(byteArray, image.mimeType).getOrNull() ?: return null - return Blob.serializer().deserialize(resp.blob) + return agent.uploadBlob(byteArray, image.mimeType).getOrNull() } fun fileExtToMimeType(filename: String): String { diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/BskyLabel.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/BskyLabel.kt index ca6234e..94db7f2 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/BskyLabel.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/BskyLabel.kt @@ -97,227 +97,6 @@ data class BskyLabel( } } -@Serializable -enum class LabelScope { - Content, - Media, - None, -} - -fun Blurs.toScope(): LabelScope { - return when (this) { - Blurs.CONTENT -> LabelScope.Content - Blurs.MEDIA -> LabelScope.Media - Blurs.NONE -> LabelScope.None - } -} - -@Immutable -@Serializable -enum class LabelAction { - Blur, - Alert, - Inform, - None -} - -@Immutable -@Serializable -enum class LabelTarget { - Account, - Profile, - Content -} - -@Parcelize -@Immutable -@Serializable -open class ModBehaviour( - val profileList: LabelAction = LabelAction.None, - val profileView: LabelAction = LabelAction.None, - val avatar: LabelAction = LabelAction.None, - val banner: LabelAction = LabelAction.None, - val displayName: LabelAction = LabelAction.None, - val contentList: LabelAction = LabelAction.None, - val contentView: LabelAction = LabelAction.None, - val contentMedia: LabelAction = LabelAction.None, -): Parcelable { - init { - require(avatar != LabelAction.Inform) - require(banner != LabelAction.Inform && banner != LabelAction.Alert) - require(displayName != LabelAction.Inform && displayName != LabelAction.Alert) - require(contentMedia != LabelAction.Inform && contentMedia != LabelAction.Alert) - } - - - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (other == null || this::class != other::class) return false - - other as ModBehaviour - - if (profileList != other.profileList) return false - if (profileView != other.profileView) return false - if (avatar != other.avatar) return false - if (banner != other.banner) return false - if (displayName != other.displayName) return false - if (contentList != other.contentList) return false - if (contentView != other.contentView) return false - if (contentMedia != other.contentMedia) return false - - return true - } - - override fun hashCode(): Int { - var result = profileList.hashCode() - result = 31 * result + profileView.hashCode() - result = 31 * result + avatar.hashCode() - result = 31 * result + banner.hashCode() - result = 31 * result + displayName.hashCode() - result = 31 * result + contentList.hashCode() - result = 31 * result + contentView.hashCode() - result = 31 * result + contentMedia.hashCode() - return result - } -} - -@Parcelize -@Immutable -@Serializable -data class ModBehaviours( - val account: ModBehaviour = ModBehaviour(), - val profile: ModBehaviour = ModBehaviour(), - val content: ModBehaviour = ModBehaviour(), -): Parcelable { - fun forScope(scope: LabelScope, target: LabelTarget): List { - return when (target) { - LabelTarget.Account -> when (scope) { - LabelScope.Content -> listOf( - account.contentList, account.contentView, account.avatar, - account.banner, account.profileList, account.profileView, - account.displayName - ) - LabelScope.Media -> listOf(account.contentMedia, account.avatar, account.banner) - LabelScope.None -> listOf() - } - LabelTarget.Profile -> when (scope) { - LabelScope.Content -> listOf(profile.contentList, profile.contentView, profile.displayName) - LabelScope.Media -> listOf(profile.avatar, profile.banner, profile.contentMedia) - LabelScope.None -> listOf() - } - LabelTarget.Content -> when (scope) { - LabelScope.Content -> listOf(content.contentList, content.contentView) - LabelScope.Media -> listOf( - content.contentMedia, - content.avatar, - content.banner - ) - - LabelScope.None -> listOf() - } - } - } -} - -@Immutable -@Serializable -open class DescribedBehaviours( - val behaviours: ModBehaviours, - val label: String, - val description: String, -){ - -} - - -@Immutable -@Serializable -data object BlockBehaviour: ModBehaviour( - profileList = LabelAction.Blur, - profileView = LabelAction.Blur, - avatar = LabelAction.Blur, - banner = LabelAction.Blur, - contentList = LabelAction.Blur, - contentView = LabelAction.Blur, -) - -@Immutable -@Serializable -data object MuteBehaviour: ModBehaviour( - profileList = LabelAction.Inform, - profileView = LabelAction.Alert, - contentList = LabelAction.Blur, - contentView = LabelAction.Inform, -) - -@Immutable -@Serializable -data object MuteWordBehaviour: ModBehaviour( - contentList = LabelAction.Blur, - contentView = LabelAction.Blur, -) - -@Immutable -@Serializable -data object HideBehaviour: ModBehaviour( - contentList = LabelAction.Blur, - contentView = LabelAction.Blur, -) - -@Immutable -@Serializable -data object InappropriateMediaBehaviour: ModBehaviour( - contentMedia = LabelAction.Blur, -) - -@Immutable -@Serializable -data object InappropriateAvatarBehaviour: ModBehaviour( - avatar = LabelAction.Blur, -) - -@Immutable -@Serializable -data object InappropriateBannerBehaviour: ModBehaviour( - banner = LabelAction.Blur, -) - -@Immutable -@Serializable -data object InappropriateDisplayNameBehaviour: ModBehaviour( - displayName = LabelAction.Blur, -) - - -@Serializable -val BlurAllMedia = ModBehaviours( - content = InappropriateMediaBehaviour, - profile = ModBehaviour( - avatar = LabelAction.Blur, - banner = LabelAction.Blur, - contentMedia = LabelAction.Blur, - ), - account = ModBehaviour( - avatar = LabelAction.Blur, - banner = LabelAction.Blur, - contentMedia = LabelAction.Blur, - ), -) - - -@Immutable -@Serializable -data object NoopBehaviour: ModBehaviour() - -@Immutable -@Serializable -enum class LabelValueDefFlag { - NoOverride, - Adult, - Unauthed, - NoSelf, -} - @Immutable @Serializable enum class LabelSetting { @@ -354,7 +133,7 @@ fun Visibility.toLabelSetting(): LabelSetting { data class BskyLabelDefinition( val identifier: String, val severity: Severity, - val whatToHide: LabelScope, + val whatToHide: Blurs, val defaultSetting: LabelSetting?, val adultOnly: Boolean?, val localizedName: String, @@ -385,7 +164,7 @@ fun LabelValueDefinition.toModLabelDef() :BskyLabelDefinition { return BskyLabelDefinition( identifier = identifier, severity = severity, - whatToHide = blurs.toScope(), + whatToHide = blurs, defaultSetting = defaultSetting?.toLabelSetting(), adultOnly = adultOnly, localizedName = localizedDefString.name, diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/BskyPostThread.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/BskyPostThread.kt index b4239e7..414f4c0 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/BskyPostThread.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/BskyPostThread.kt @@ -51,6 +51,28 @@ data class BskyPostThread( return false } + fun anyMutedOrBlocked(): Boolean { + return this.post.author.mutedByMe || this.post.author.blocking + || this.post.author.blockedBy || this.replies.any { it.anyMutedOrBlocked() } + || this.parents.any { it.anyMutedOrBlocked() } + } + + fun containsWord(word: String): Boolean { + return this.post.text.contains(word, ignoreCase = true) + || this.replies.any { it.containsWord(word) } + || this.parents.any { it.containsWord(word) } + } + + fun getLabels(): List { + return this.post.labels + this.replies.flatMap { it.getLabels() } + } + + fun containsLabel(label: String): Boolean { + return this.post.labels.any { it.value == label } + || this.replies.any { it.containsLabel(label) } + || this.parents.any { it.containsLabel(label) } + } + fun filterReplies(filter: (ThreadPost) -> Boolean): BskyPostThread { val threadReplies = this.replies.toMutableList() threadReplies.fastForEachIndexed { index, reply -> @@ -274,6 +296,42 @@ sealed interface ThreadPost:Parcelable { } } + fun anyMutedOrBlocked(): Boolean { + return when(this) { + is ViewablePost -> this.post.author.mutedByMe || this.post.author.blocking + || this.post.author.blockedBy || this.replies.any { it.anyMutedOrBlocked() } + + is BlockedPost -> true + is NotFoundPost -> true + } + } + + fun containsLabel(label: String): Boolean { + return when(this) { + is ViewablePost -> this.post.labels.any { it.value == label } + || this.replies.any { it.containsLabel(label) } + is BlockedPost -> false + is NotFoundPost -> false + } + } + + fun getLabels(): List { + return when(this) { + is ViewablePost -> this.post.labels + this.replies.flatMap { it.getLabels() } + is BlockedPost -> listOf() + is NotFoundPost -> listOf() + } + } + + fun containsWord(word: String): Boolean { + return when(this) { + is ViewablePost -> this.post.text.contains(word, ignoreCase = true) + || this.replies.any { it.containsWord(word) } + is BlockedPost -> false + is NotFoundPost -> false + } + } + fun addReply(reply: BskyPost): ThreadPost { return addReply(ViewablePost(reply)) diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/DraftPost.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/DraftPost.kt index 3ec1af8..acb1e08 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/DraftPost.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/DraftPost.kt @@ -13,7 +13,7 @@ import com.morpho.app.data.SharedImage import com.morpho.app.data.imageToBlob import com.morpho.app.util.makeBlueskyText import com.morpho.app.util.resolveBlueskyText -import com.morpho.butterfly.Butterfly +import com.morpho.butterfly.ButterflyAgent import com.morpho.butterfly.Language import kotlinx.collections.immutable.toPersistentList import kotlinx.datetime.Clock @@ -37,9 +37,9 @@ data class DraftPost( val images: MutableList = mutableListOf(), ) { - suspend fun createPost(api: Butterfly): Post { + suspend fun createPost(agent: ButterflyAgent): Post { val text = makeBlueskyText(text) - val blueskyText = resolveBlueskyText(text, api).getOrDefault(text) + val blueskyText = resolveBlueskyText(text, agent).getOrDefault(text) val replyRef = if (reply != null && reply.reply?.replyRef != null) { val root = reply.reply.replyRef.root val parent = StrongRef(reply.uri, reply.cid) @@ -58,7 +58,7 @@ data class DraftPost( app.bsky.embed.RecordWithMediaMediaUnion.Images( Images( images.mapNotNull { - it.toImageRef(api) + it.toImageRef(agent) }.toPersistentList() ) ) @@ -68,7 +68,7 @@ data class DraftPost( PostEmbedUnion.Images( Images( images.mapNotNull { - it.toImageRef(api) + it.toImageRef(agent) }.toPersistentList() ) ) @@ -96,9 +96,9 @@ data class DraftImage( val altText: String? = null, val aspectRatio: AspectRatio? = null, ) { - suspend fun toImageRef(api: Butterfly) : app.bsky.embed.ImagesImage? { + suspend fun toImageRef(agent: ButterflyAgent) : app.bsky.embed.ImagesImage? { return app.bsky.embed.ImagesImage( - image = imageToBlob(image, api)?: return null, + image = imageToBlob(image, agent)?: return null, alt = altText ?: "", aspectRatio = aspectRatio, ) diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/BskyDataService.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/BskyDataService.kt deleted file mode 100644 index d3cf44a..0000000 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/BskyDataService.kt +++ /dev/null @@ -1,1141 +0,0 @@ -package com.morpho.app.model.uidata - -//import com.rickclephas.kmp.nativecoroutines.NativeCoroutines -import app.bsky.actor.GetProfilesQuery -import app.bsky.actor.ProfileViewBasic -import app.bsky.feed.* -import app.bsky.graph.GetFollowersQuery -import app.bsky.graph.GetFollowsQuery -import app.bsky.graph.GetListsQuery -import app.bsky.labeler.GetServicesQuery -import app.bsky.labeler.GetServicesResponseViewUnion -import com.atproto.repo.GetRecordQuery -import com.atproto.repo.StrongRef -import com.morpho.app.data.FeedTuner -import com.morpho.app.di.UpdateTick -import com.morpho.app.model.bluesky.* -import com.morpho.app.model.uistate.ContentCardState -import com.morpho.app.model.uistate.ContentLoadingState -import com.morpho.app.model.uistate.FeedType -import com.morpho.app.util.json -import com.morpho.app.util.mapImmutable -import com.morpho.butterfly.* -import kotlinx.collections.immutable.* -import kotlinx.coroutines.* -import kotlinx.coroutines.channels.BufferOverflow -import kotlinx.coroutines.flow.* -import kotlinx.coroutines.sync.Mutex -import kotlinx.coroutines.sync.withLock -import kotlinx.serialization.Serializable -import kotlinx.serialization.json.Json -import kotlinx.serialization.json.decodeFromJsonElement -import kotlinx.serialization.json.encodeToJsonElement -import kotlinx.serialization.json.jsonObject -import org.koin.core.component.KoinComponent -import org.koin.core.component.inject -import org.koin.mp.KoinPlatform.getKoin -import org.lighthousegames.logging.logging - -fun initAtCursor(): MutableSharedFlow { - return MutableSharedFlow(1, 1, BufferOverflow.DROP_OLDEST) -} - -suspend fun Flow>>.handleToState( - default: MorphoData, - scope: CoroutineScope = BskyDataService.serviceScope, -): StateFlow> = transform { - if (it.isFailure) { - emit(ContentCardState.Skyline(default, ContentLoadingState.Error(it.exceptionOrNull()?.message ?: "Failed to load feed"), false)) - } else { - emit(ContentCardState.Skyline(it.getOrNull() ?: default, ContentLoadingState.Idle, false)) - } -}.stateIn( - scope, - SharingStarted.Eagerly, - ContentCardState.Skyline(default) -) - -suspend fun Flow>>.handleToState( - profile: Profile, - default: MorphoData, - scope: CoroutineScope = BskyDataService.serviceScope, -): StateFlow> = transform { - if (it.isFailure) { - emit(ContentCardState.ProfileTimeline(profile, default, ContentLoadingState.Error(it.exceptionOrNull()?.message ?: "Failed to load feed"), false)) - } else { - emit(ContentCardState.ProfileTimeline(profile, it.getOrNull() ?: default, ContentLoadingState.Idle, false)) - } -}.stateIn( - scope, - SharingStarted.Eagerly, - ContentCardState.ProfileTimeline(profile, default) -) - -suspend fun getPost(uri: AtUri, api: Butterfly = getKoin().get()): Flow = flow { - val query = GetPostsQuery(persistentListOf(uri)) - api.api.getPosts(query).onSuccess { response -> - emit(response.posts.firstOrNull()?.toPost()) - }.onFailure { - BskyDataService.log.e { "Failed to get post at $uri.\nError: $it" } - emit(null) - } -} - -fun getReplyRefs(uri: AtUri, api: Butterfly = getKoin().get()): Flow> = flow { - uri.toParts().onFailure { emit(Result.failure(it)) }.onSuccess { uriParts-> - api.api.getRecord(GetRecordQuery(uriParts.repo, uriParts.collection, uriParts.rkey)) - .onSuccess { parentResponse -> - val parentReply = parentResponse.value.jsonObject["reply"]?.jsonObject - if(parentReply != null) { - val rootUri = parentReply["root"]?.jsonObject?.get("uri")?.recordType - if (rootUri != null) { - AtUri.parseAtUri(rootUri).onFailure { emit(Result.failure(it)) }.onSuccess { parts -> - api.api.getRecord(GetRecordQuery(parts.repo, parts.collection, parts.rkey)) - .onSuccess { rootResponse -> - val rootRef = rootResponse.cid?.let { StrongRef(rootResponse.uri, it) } - val parentRef = parentResponse.cid?.let { StrongRef(parentResponse.uri, it) } - val grandParentAuthor = parentReply["grandparentAuthor"]?.jsonObject?.let { ProfileViewBasic.serializer().deserialize(it) } - if(rootRef != null && parentRef != null) { - emit(Result.success(PostReplyRef(rootRef, parentRef, grandParentAuthor))) - } else { - emit(Result.failure(Error( - "Failed to get reply refs:\nRoot: $rootResponse\nParent: $parentResponse"))) - } - }.onFailure { emit(Result.failure(it)) } - } - - } - } - }.onFailure { emit(Result.failure(it)) } - } - -} - -suspend fun getPosts(posts: List, api: Butterfly = getKoin().get()): Flow?> = flow { - val query = GetPostsQuery(posts.toPersistentList()) - api.api.getPosts(query).onSuccess { response -> - emit(response.posts.mapImmutable { it.toPost() }) - }.onFailure { - BskyDataService.log.e { "Failed to get post.\nError: $it" } - emit(null) - } -} - -@Suppress("unused", "MemberVisibilityCanBePrivate", "UNCHECKED_CAST") -// TODO: Revisit these casts if we can, but they should be safe -@Serializable -class BskyDataService: KoinComponent { - val api: Butterfly by inject() - - private val _dataFlows = mutableMapOf>>() - val useFeedTuners: (MorphoData) -> List = { feed -> - settings.currentUserPrefs.value?.let { FeedTuner.useFeedTuners(it, feed) } ?: listOf(FeedTuner()) - } - private val mutex = Mutex() - private val contentLabelService by inject() - private val settings: SettingsService by inject() - private val languages: StateFlow> = settings.languages - .stateIn(serviceScope, SharingStarted.Lazily, persistentListOf()) - - - // Secondary way to make sure you have the most recent stuff, in case you lose the original reference - val dataFlows: ImmutableMap>> - get() = _dataFlows.mapValues { it.value.asStateFlow() }.toImmutableMap() - - companion object { - val log = logging() - val serviceScope = CoroutineScope(SupervisorJob() + Dispatchers.Default) - } - - suspend fun refresh( - uri: AtUri, - cursor: AtCursor = AtCursor.EMPTY, - ): Result>> { - val flow = dataFlows[uri] ?: return Result.failure(Exception("No feed to refresh.")) - val data = flow.value - when(data.feedType) { - FeedType.HOME -> { - try { - val query = json.decodeFromJsonElement(data.query).copy(cursor = cursor.cursor) - api.api.getTimeline(query).onSuccess { response -> - if (response.cursor == cursor.cursor && cursor != AtCursor.EMPTY) { - return@onSuccess - } - val new = MorphoData.concatFeed( - query = json.encodeToJsonElement(query), - responseCursor = response.cursor, - oldCursor = cursor, - feed = response.feed, - data = data as MorphoData, - api = api, - ).single() - var tunedFeed = new - useFeedTuners(tunedFeed).forEach { tuner -> - tunedFeed = tuner.tune(tunedFeed) - } - mutex.withLock { - _dataFlows[uri]?.update { tunedFeed as MorphoData } - } - return Result.success(flow) - } - } catch (e: Exception) { - log.e { "Failed to refresh feed at $uri.\nError: $e" } - return Result.failure(e) - } - } - FeedType.PROFILE_POSTS -> { - try { - val query = json.decodeFromJsonElement(data.query).copy(cursor = cursor.cursor) - api.api.getAuthorFeed(query).onSuccess { response -> - if (response.cursor == cursor.cursor && cursor != AtCursor.EMPTY) { - return@onSuccess - } - var tunedFeed = MorphoData.concatFeed( - query = json.encodeToJsonElement(query), - responseCursor = response.cursor, - oldCursor = cursor, - feed = response.feed, - data = data as MorphoData, - title = "Posts", - api = api, - ).single() - useFeedTuners(tunedFeed).forEach { tuner -> - tunedFeed = tuner.tune(tunedFeed) - } - mutex.withLock { - _dataFlows[uri]?.update { tunedFeed as MorphoData } - } - return Result.success(flow) - } - } catch (e: Exception) { - log.e { "Failed to refresh feed at $uri.\nError: $e" } - return Result.failure(e) - } - } - FeedType.PROFILE_REPLIES -> { - try { - val query = Json.decodeFromJsonElement(data.query).copy(cursor = cursor.cursor) - api.api.getAuthorFeed(query).onSuccess { response -> - if (response.cursor == cursor.cursor && cursor != AtCursor.EMPTY) { - return@onSuccess - } - var tunedFeed = MorphoData.concatFeed( - query = json.encodeToJsonElement(query), - responseCursor = response.cursor, - oldCursor = cursor, - feed = response.feed, - data = data as MorphoData, - title = "Replies", - api = api, - ).single() - useFeedTuners(tunedFeed).forEach { tuner -> - tunedFeed = tuner.tune(tunedFeed) - } - mutex.withLock { - _dataFlows[uri]?.update { tunedFeed as MorphoData } - } - return Result.success(flow) - } - } catch (e: Exception) { - log.e { "Failed to refresh feed at $uri.\nError: $e" } - return Result.failure(e) - } - } - FeedType.PROFILE_MEDIA -> { - try { - val query = json.decodeFromJsonElement(data.query).copy(cursor = cursor.cursor) - api.api.getAuthorFeed(query).onSuccess { response -> - if (response.cursor == cursor.cursor && cursor != AtCursor.EMPTY) { - return@onSuccess - } - var tunedFeed = MorphoData.concatNonThreadedFeed( - query = json.encodeToJsonElement(query), - responseCursor = response.cursor, - oldCursor = cursor, - feed = response.feed, - data = data as MorphoData, - title = "Media", - ) - useFeedTuners(tunedFeed).forEach { tuner -> - tunedFeed = tuner.tune(tunedFeed) - } - mutex.withLock { - _dataFlows[uri]?.update { tunedFeed as MorphoData } - } - return Result.success(flow) - } - } catch (e: Exception) { - log.e { "Failed to refresh feed at $uri.\nError: $e" } - return Result.failure(e) - } - } - FeedType.PROFILE_LIKES -> { - try { - val query = json.decodeFromJsonElement(data.query).copy(cursor = cursor.cursor) - api.api.getActorLikes(query).onSuccess { response -> - if (response.cursor == cursor.cursor && cursor != AtCursor.EMPTY) { - return@onSuccess - } - var tunedFeed = MorphoData.concatNonThreadedFeed( - query = json.encodeToJsonElement(query), - responseCursor = response.cursor, - oldCursor = cursor, - feed = response.feed, - data = data as MorphoData, - title = "Likes", - ) - useFeedTuners(tunedFeed).forEach { tuner -> - tunedFeed = tuner.tune(tunedFeed) - } - mutex.withLock { - _dataFlows[uri]?.update { tunedFeed as MorphoData } - } - return Result.success(flow) - } - } catch (e: Exception) { - log.e { "Failed to refresh feed at $uri.\nError: $e" } - return Result.failure(e) - } - } - FeedType.PROFILE_USER_LISTS -> { - try { - val query = json.decodeFromJsonElement(data.query).copy(cursor = cursor.cursor) - api.api.getLists(query).onSuccess { response -> - if (response.cursor == cursor.cursor && cursor != AtCursor.EMPTY) { - return@onSuccess - } - val newData = if (cursor != AtCursor.EMPTY && data.items.isNotEmpty()) { - MorphoData.concat(data, response.lists.mapImmutable { MorphoDataItem.ListInfo(it.toList()) }) - } else if (cursor == AtCursor.EMPTY && data.items.isNotEmpty()) { - MorphoData.concat(response.lists.mapImmutable { MorphoDataItem.ListInfo(it.toList()) }, data) - } else { - MorphoData("Lists", uri, AtCursor(response.cursor, cursor.scroll), - response.lists.mapImmutable { MorphoDataItem.ListInfo(it.toList()) }) - }.copy(query = json.encodeToJsonElement(query)) - mutex.withLock { - _dataFlows[uri]?.update { newData as MorphoData} - } - return Result.success(flow) - } - } catch (e: Exception) { - log.e { "Failed to refresh feed at $uri.\nError: $e" } - return Result.failure(e) - } - } - FeedType.PROFILE_MOD_SERVICE -> { - try { - val query = json.decodeFromJsonElement(data.query) - api.api.getServices(query).onSuccess { response -> - - val newData = if (cursor != AtCursor.EMPTY && data.items.isNotEmpty()) { - MorphoData.concat(data, response.views.mapImmutable { - when(it) { - is GetServicesResponseViewUnion.LabelerViewDetailed -> - MorphoDataItem.LabelService(it.value.toLabelService()) - is GetServicesResponseViewUnion.LabelerView -> - MorphoDataItem.LabelService(it.value.toLabelService()) - } - }) - } else if (cursor == AtCursor.EMPTY && data.items.isNotEmpty()) { - MorphoData.concat(response.views.mapImmutable { - when(it) { - is GetServicesResponseViewUnion.LabelerViewDetailed -> - MorphoDataItem.LabelService(it.value.toLabelService()) - is GetServicesResponseViewUnion.LabelerView -> - MorphoDataItem.LabelService(it.value.toLabelService()) - } - }, data) - } else { - MorphoData("Services", uri, AtCursor.EMPTY, - response.views.mapImmutable { - when(it) { - is GetServicesResponseViewUnion.LabelerViewDetailed -> - MorphoDataItem.LabelService(it.value.toLabelService()) - is GetServicesResponseViewUnion.LabelerView -> - MorphoDataItem.LabelService(it.value.toLabelService()) - } - }) - }.copy(query = json.encodeToJsonElement(query)) - mutex.withLock { - _dataFlows[uri]?.update { newData as MorphoData} - } - return Result.success(flow) - } - } catch (e: Exception) { - log.e { "Failed to refresh feed at $uri.\nError: $e" } - return Result.failure(e) - } - } - FeedType.PROFILE_FEEDS_LIST -> { - try { - val query = json.decodeFromJsonElement(data.query).copy(cursor = cursor.cursor) - api.api.getActorFeeds(query).onSuccess { response -> - if (response.cursor == cursor.cursor && cursor != AtCursor.EMPTY) { - return@onSuccess - } - val newData = if (cursor != AtCursor.EMPTY && data.items.isNotEmpty()) { - MorphoData.concat(data, response.feeds.mapImmutable { MorphoDataItem.FeedInfo(it.toFeedGenerator()) }) - } else if (cursor == AtCursor.EMPTY && data.items.isNotEmpty()) { - MorphoData.concat(response.feeds.mapImmutable { MorphoDataItem.FeedInfo(it.toFeedGenerator()) }, data) - } else { - MorphoData("Feeds", uri, AtCursor(response.cursor, cursor.scroll), - response.feeds.mapImmutable { MorphoDataItem.FeedInfo(it.toFeedGenerator()) }) - }.copy(query = json.encodeToJsonElement(query)) - mutex.withLock { - _dataFlows[uri]?.update { newData as MorphoData} - } - return Result.success(flow) - } - } catch (e: Exception) { - log.e { "Failed to refresh feed at $uri.\nError: $e" } - return Result.failure(e) - } - } - FeedType.OTHER -> { - try { - val query = json.decodeFromJsonElement(data.query).copy(cursor = cursor.cursor) - api.api.getFeed(query).onSuccess { response -> - if (response.cursor == cursor.cursor && cursor != AtCursor.EMPTY) { - return@onSuccess - } - var tunedFeed = MorphoData.concatFeed( - query = json.encodeToJsonElement(query), - responseCursor = response.cursor, - oldCursor = cursor, - feed = response.feed, - data = data as MorphoData, - api = api, - ).single() - useFeedTuners(tunedFeed).forEach { tuner -> - tunedFeed = tuner.tune(tunedFeed) - } - mutex.withLock { - _dataFlows[uri]?.update { tunedFeed as MorphoData } - } - return Result.success(flow) - } - } catch (e: Exception) { - log.e { "Failed to refresh feed at $uri.\nError: $e" } - return Result.failure(e) - } - } - FeedType.LIST_FOLLOWING -> { - try { - val query = json.decodeFromJsonElement(data.query).copy(cursor = cursor.cursor) - api.api.getListFeed(query).onSuccess { response -> - if (response.cursor == cursor.cursor && cursor != AtCursor.EMPTY) { - return@onSuccess - } - var tunedFeed = MorphoData.concatFeed( - query = json.encodeToJsonElement(query), - responseCursor = response.cursor, - oldCursor = cursor, - feed = response.feed, - data = data as MorphoData, - api = api, - ).single() - useFeedTuners(tunedFeed).forEach { tuner -> - tunedFeed = tuner.tune(tunedFeed) - } - mutex.withLock { - _dataFlows[uri]?.update { tunedFeed as MorphoData } - } - return Result.success(flow) - } - } catch (e: Exception) { - log.e { "Failed to refresh feed at $uri.\nError: $e" } - return Result.failure(e) - } - } - } - return Result.failure(Exception("Invalid feed type.")) - } - @OptIn(FlowPreview::class) - suspend fun timeline( - cursor: SharedFlow, - limit: Long = 50, - feedPref: StateFlow = MutableStateFlow(BskyFeedPref()), - dispatcher: CoroutineDispatcher = Dispatchers.IO, - ): Flow>> = flow { - cursor.debounce(300).combine(feedPref) { c, f -> c to f } - .collect { flows -> - //log.d { "Timeline flow tick." } - val (cur, pref) = flows - val prev = dataFlows[AtUri.HOME_URI]?.value - val query = GetTimelineQuery(limit = limit, cursor = cur.cursor) - api.api.getTimeline(query).onSuccess { response -> - if (response.cursor == cur.cursor && cur != AtCursor.EMPTY) { - return@collect - } - var tunedFeed = MorphoData.concatFeed( - query = json.encodeToJsonElement(query), - responseCursor = response.cursor, - oldCursor = cur, - feed = response.feed, - data = (prev ?: MorphoData.EMPTY()) as MorphoData, - title = "Home", - uri = AtUri.HOME_URI, - api = api, - ).single() - useFeedTuners(tunedFeed).forEach { tuner -> - tunedFeed = tuner.tune(tunedFeed) - } - emit(Result.success(tunedFeed)) - log.d{ - "Timeline " + - "Old cursor: $cur " + - "New cursor: ${response.cursor}" - } - log.v { - "${tunedFeed.items.map { - when(it) { - is MorphoDataItem.Post -> "${it.post.uri}\n" - is MorphoDataItem.Thread -> "${it.thread.post.uri}\n" - } - }}" - } - mutex.withLock { - if(prev == null) _dataFlows[AtUri.HOME_URI] = MutableStateFlow(tunedFeed as MorphoData) - else _dataFlows[AtUri.HOME_URI]?.update { tunedFeed as MorphoData } - } - }.onFailure { - emit(Result.failure(it)) - log.e { "Failed to get timeline.\nError: $it" } - log.v { "Cursor: $cur | Limit: $limit\nFeedPref: $pref\n" } - } - } - }.distinctUntilChanged().flowOn(dispatcher + CoroutineName("Timeline")) - //.stateIn(scope, SharingStarted.WhileSubscribed(100), Result.success( - // MorphoData("Home", AtUri.HOME_URI, null) - //)) - - @OptIn(FlowPreview::class) - suspend fun feed( - feedInfo: FeedInfo, - cursor: SharedFlow, - limit: Long = 50, - feedPref: StateFlow = MutableStateFlow(null), - dispatcher: CoroutineDispatcher = Dispatchers.IO, - ): Flow>> = flow { - cursor.debounce(300).combine(feedPref) { c, f -> c to f } - .collect { flows -> - //log.d { "Feed flow tick."} - val cur = flows.first - val pref = flows.second - val prev = dataFlows[feedInfo.uri]?.value - val query = GetFeedQuery(feedInfo.uri, limit, cur.cursor) - api.api.getFeed(query).onSuccess { response -> - if (response.cursor == cur.cursor) { - return@collect - } - var tunedFeed = MorphoData.concatFeed( - query = json.encodeToJsonElement(query), - responseCursor = response.cursor, - oldCursor = cur, - feed = response.feed, - data = (prev ?: MorphoData.EMPTY()) as MorphoData, - title = feedInfo.name, - uri = feedInfo.uri, - api = api, - ).single() - useFeedTuners(tunedFeed).forEach { tuner -> - tunedFeed = tuner.tune(tunedFeed) - } - emit(Result.success(tunedFeed)) - log.d{ - "Feed: ${feedInfo.name} " + - "Old cursor: $cur " + - "New cursor: ${response.cursor}" - } - log.v { - "${tunedFeed.items.map { - when(it) { - is MorphoDataItem.Post -> it.post.uri - is MorphoDataItem.Thread -> it.thread.post.uri - } - }}" - } - mutex.withLock { - if(prev == null) _dataFlows[feedInfo.uri] = MutableStateFlow(tunedFeed as MorphoData) - else _dataFlows[feedInfo.uri]?.update { tunedFeed as MorphoData } - } - }.onFailure { - emit(Result.failure(it)) - log.e { "Failed to get feed at ${feedInfo.uri}.\nError: $it" } - log.v { "Cursor: $cur | Limit: $limit\nFeedPref: $pref" } - } - } - }.distinctUntilChanged().flowOn(dispatcher) - - suspend fun following( - id: AtIdentifier, - cursor: SharedFlow, - limit: Long = 50, - dispatcher: CoroutineDispatcher = Dispatchers.IO, - ): Flow>> = flow { - val uri = AtUri.followsUri(id) - cursor.collect { cur -> - val prev = dataFlows[uri]?.value - val query = GetFollowsQuery(id, limit, cur.cursor) - api.api.getFollows(query).onSuccess { response -> - if (response.cursor == cur.cursor) { - return@collect - } - val data = if (cur != AtCursor.EMPTY && prev != null && prev.items.isNotEmpty()) { - MorphoData.concat(prev, response.follows.mapImmutable { MorphoDataItem.ProfileItem(it.toProfile()) }) - } else if (cur == AtCursor.EMPTY && prev != null && prev.items.isNotEmpty()) { - MorphoData.concat(response.follows.mapImmutable { MorphoDataItem.ProfileItem(it.toProfile()) }, prev) - } else { - MorphoData("Following", uri, AtCursor(response.cursor, cur.scroll), - response.follows.mapImmutable { MorphoDataItem.ProfileItem(it.toProfile()) }) - }.copy(query = json.encodeToJsonElement(query)) - emit(Result.success(data as MorphoData)) - mutex.withLock { - if(prev == null) _dataFlows[uri] = MutableStateFlow(data as MorphoData) - else _dataFlows[uri]?.update { data as MorphoData } - } - }.onFailure { - emit(Result.failure(it)) - log.e { "Failed to get follows for $id.\nError: $it" } - log.v { "Cursor: $cur | Limit: $limit" } - } - } - }.distinctUntilChanged().flowOn(dispatcher + CoroutineName("Follows of $id")) - - suspend fun followers( - id: AtIdentifier, - cursor: SharedFlow, - limit: Long = 50, - dispatcher: CoroutineDispatcher = Dispatchers.IO, - ): Flow>> = flow { - val uri = AtUri.followersUri(id) - cursor.collect { cur -> - val prev = dataFlows[uri]?.value - val query = GetFollowersQuery(id, limit, cur.cursor) - api.api.getFollowers(query).onSuccess { response -> - if (response.cursor == cur.cursor) { - return@collect - } - val data = if (cur != AtCursor.EMPTY && prev != null && prev.items.isNotEmpty()) { - MorphoData.concat(prev, response.followers.mapImmutable { MorphoDataItem.ProfileItem(it.toProfile()) }) - } else if (cur == AtCursor.EMPTY && prev != null && prev.items.isNotEmpty()) { - MorphoData.concat(response.followers.mapImmutable { MorphoDataItem.ProfileItem(it.toProfile()) }, prev) - } else { - MorphoData("Following", uri, AtCursor(response.cursor, cur.scroll), - response.followers.mapImmutable { MorphoDataItem.ProfileItem(it.toProfile()) }) - }.copy(query = json.encodeToJsonElement(query)) - emit(Result.success(data as MorphoData)) - mutex.withLock { - if(prev == null) _dataFlows[uri] = MutableStateFlow(data as MorphoData) - else _dataFlows[uri]?.update { data as MorphoData } - } - }.onFailure { - emit(Result.failure(it)) - log.e { "Failed to get followers for $id.\nError: $it" } - log.v { "Cursor: $cur | Limit: $limit" } - } - } - }.distinctUntilChanged().flowOn(dispatcher + CoroutineName("Followers of $id")) - - suspend fun authorFeed( - id: AtIdentifier, - type: FeedType, - cursor: SharedFlow, - limit: Long = 50, - dispatcher: CoroutineDispatcher = Dispatchers.IO, - ): Flow>> = flow>> { - when(type){ - FeedType.PROFILE_POSTS -> { - val uri = AtUri.profilePostsUri(id) - cursor.collect { cur -> - val prev = dataFlows[uri]?.value - val query = GetAuthorFeedQuery(id, limit, cur.cursor, GetAuthorFeedFilter.POSTS_NO_REPLIES) - api.api.getAuthorFeed(query).onSuccess { response -> - if (response.cursor == cur.cursor) { - return@collect - } - var tunedFeed = MorphoData.concatFeed( - query = json.encodeToJsonElement(query), - responseCursor = response.cursor, - oldCursor = cur, - feed = response.feed, - data = (prev ?: MorphoData.EMPTY()) as MorphoData, - title = "Posts", - api = api, - ).single() - useFeedTuners(tunedFeed).forEach { tuner -> - tunedFeed = tuner.tune(tunedFeed) - } - emit(Result.success(tunedFeed)) - mutex.withLock { - if(prev == null) _dataFlows[uri] = MutableStateFlow(tunedFeed as MorphoData) - else _dataFlows[uri]?.update { tunedFeed as MorphoData } - } - }.onFailure { - emit(Result.failure(it)) - log.e { "Failed to get posts feed for $id.\nError: $it" } - log.v { "Cursor: $cur | Limit: $limit\n" } - } - } - } - FeedType.PROFILE_REPLIES -> { - val uri = AtUri.profileRepliesUri(id) - cursor.collect { cur -> - val prev = dataFlows[uri]?.value - val query = GetAuthorFeedQuery(id, limit, cur.cursor, GetAuthorFeedFilter.POSTS_WITH_REPLIES) - api.api.getAuthorFeed(query).onSuccess { response -> - if (response.cursor == cur.cursor) { - return@collect - } - var tunedFeed = MorphoData.concatFeed( - query = json.encodeToJsonElement(query), - responseCursor = response.cursor, - oldCursor = cur, - feed = response.feed, - data = (prev ?: MorphoData.EMPTY()) as MorphoData, - title = "Replies", - api = api, - ).single() - useFeedTuners(tunedFeed).forEach { tuner -> - tunedFeed = tuner.tune(tunedFeed) - } - emit(Result.success(tunedFeed)) - mutex.withLock { - if(prev == null) _dataFlows[uri] = MutableStateFlow(tunedFeed as MorphoData) - else _dataFlows[uri]?.update { tunedFeed as MorphoData } - } - }.onFailure { - emit(Result.failure(it)) - log.e { "Failed to get reply feed of $id.\nError: $it" } - log.v { "Cursor: $cur | Limit: $limit\n" } - } - } - } - FeedType.PROFILE_MEDIA -> { - val uri = AtUri.profileMediaUri(id) - cursor.collect { cur -> - val prev = dataFlows[uri]?.value - val query = GetAuthorFeedQuery(id, limit, cur.cursor, GetAuthorFeedFilter.POSTS_WITH_MEDIA) - api.api.getAuthorFeed(query).onSuccess { response -> - if (response.cursor == cur.cursor) { - return@collect - } - var tunedFeed = MorphoData.concatNonThreadedFeed( - query = json.encodeToJsonElement(query), - responseCursor = response.cursor, - oldCursor = cur, - feed = response.feed, - data = (prev ?: MorphoData.EMPTY()) as MorphoData, - title = "Media", - ) - useFeedTuners(tunedFeed).forEach { tuner -> - tunedFeed = tuner.tune(tunedFeed) - } - emit(Result.success(tunedFeed)) - mutex.withLock { - if(prev == null) _dataFlows[uri] = MutableStateFlow(tunedFeed as MorphoData) - else _dataFlows[uri]?.update { tunedFeed as MorphoData } - } - }.onFailure { - emit(Result.failure(it)) - log.e { "Failed to get media feed of $id.\nError: $it" } - log.v { "Cursor: $cur | Limit: $limit\n" } - } - } - } - else -> { - emit(Result.failure(Exception("Invalid profile tab type."))) - } - } - }.distinctUntilChanged().flowOn(dispatcher + CoroutineName("${type.name} feed for $id")) - - suspend fun profileTabContent( - id: AtIdentifier, - type: FeedType, - cursor: SharedFlow, - limit: Long = 50, - dispatcher: CoroutineDispatcher = Dispatchers.Default, - scope: CoroutineScope = serviceScope, - ): Flow>> = flow { - when(type) { - FeedType.PROFILE_FEEDS_LIST -> { - profileFeedsList(id, cursor, limit, dispatcher) - .collect { emit(it as Result>) } - } - FeedType.PROFILE_USER_LISTS -> { - profileLists(id, cursor, limit, dispatcher) - .collect { emit(it as Result>) } - } - FeedType.PROFILE_LIKES -> { - profileLikes(id, cursor, limit, dispatcher) - .collect { emit(it as Result>) } - } - FeedType.PROFILE_MOD_SERVICE -> { - if (id.toString().startsWith("did:")) - profileServiceView(Did(id.toString()), cursor.map { Unit }.shareIn(scope, SharingStarted.Lazily), dispatcher) - .collect { emit(it as Result>) } - } - else -> { - authorFeed(id, type, cursor, limit, dispatcher) - .collect { emit(it as Result>) } - } - } - }.distinctUntilChanged().flowOn(dispatcher + CoroutineName("${type.name} content for $id")) - suspend fun profileLists( - id: AtIdentifier, - cursor: SharedFlow, - limit: Long = 50, - dispatcher: CoroutineDispatcher = Dispatchers.Default, - ): Flow>> = flow>> { - val uri = AtUri.profileUserListsUri(id) - cursor.collect { cur -> - val prev = dataFlows[uri]?.value - val query = GetListsQuery(id, limit, cur.cursor) - api.api.getLists(query).onSuccess { response -> - val data = if (cur != AtCursor.EMPTY && prev != null && prev.items.isNotEmpty()) { - MorphoData.concat(prev, response.lists.mapImmutable { MorphoDataItem.ListInfo(it.toList()) }) - } else if (cur == AtCursor.EMPTY && prev != null && prev.items.isNotEmpty()) { - MorphoData.concat(response.lists.mapImmutable { MorphoDataItem.ListInfo(it.toList()) }, prev) - } else { - MorphoData("Lists", uri, AtCursor(response.cursor, cur.scroll), - response.lists.mapImmutable { MorphoDataItem.ListInfo(it.toList()) }) - }.copy(query = json.encodeToJsonElement(query)) - - emit(Result.success(data as MorphoData)) - mutex.withLock { - if(prev == null) _dataFlows[uri] = MutableStateFlow(data as MorphoData) - else _dataFlows[uri]?.update { data as MorphoData } - } - }.onFailure { - emit(Result.failure(it)) - log.e { "Failed to get lists for $id.\nError: $it" } - log.v { "Cursor: $cur | Limit: $limit" } - } - } - }.distinctUntilChanged().flowOn(dispatcher + CoroutineName("Lists made by $id")) - - suspend fun profileFeedsList( - id: AtIdentifier, - cursor: SharedFlow, - limit: Long = 50, - dispatcher: CoroutineDispatcher = Dispatchers.IO, - ): Flow>> = flow>> { - val uri = AtUri.profileFeedsListUri(id) - cursor.onEach { cur -> - val prev = dataFlows[uri]?.value - val query = GetActorFeedsQuery(id, limit, cur.cursor) - api.api.getActorFeeds(query).onSuccess { response -> - val data = if (cur != AtCursor.EMPTY && prev != null && prev.items.isNotEmpty()) { - MorphoData.concat(prev, response.feeds.mapImmutable { MorphoDataItem.FeedInfo(it.toFeedGenerator()) }) - } else if (cur == AtCursor.EMPTY && prev != null && prev.items.isNotEmpty()) { - MorphoData.concat(response.feeds.mapImmutable { MorphoDataItem.FeedInfo(it.toFeedGenerator()) }, prev) - } else { - MorphoData("Feeds", uri, AtCursor(response.cursor, cur.scroll), - response.feeds.mapImmutable { MorphoDataItem.FeedInfo(it.toFeedGenerator()) }) - }.copy(query = json.encodeToJsonElement(query)) - - emit(Result.success(data as MorphoData)) - mutex.withLock { - if(prev == null) _dataFlows[uri] = MutableStateFlow(data as MorphoData) - else _dataFlows[uri]?.update { data as MorphoData } - } - }.onFailure { - emit(Result.failure(it)) - log.e { "Failed to get feeds for $id.\nError: $it" } - log.v { "Cursor: $cur | Limit: $limit" } - } - } - }.distinctUntilChanged().flowOn(dispatcher + CoroutineName("Feeds made by $id")) - - suspend fun profileServiceView( - did: Did, - update: SharedFlow, - dispatcher: CoroutineDispatcher = Dispatchers.IO, - ): Flow>> = flow>> { - val uri = AtUri.profileModServiceUri(did) - update.collect { - val query = GetServicesQuery(listOf(did).toImmutableList(), true) - api.api.getServices(query).onSuccess { response -> - val data = MorphoData("Labels", uri, AtCursor.EMPTY, - response.views.mapImmutable { - when(it) { - is GetServicesResponseViewUnion.LabelerViewDetailed -> - MorphoDataItem.LabelService(it.value.toLabelService()) - is GetServicesResponseViewUnion.LabelerView -> - MorphoDataItem.LabelService(it.value.toLabelService()) - } - }) - - emit(Result.success(data)) - mutex.withLock { - if(dataFlows[uri] == null) _dataFlows[uri] = MutableStateFlow(data as MorphoData) - else _dataFlows[uri]?.update { data as MorphoData } - } - }.onFailure { - emit(Result.failure(it)) - log.e { "Failed to get label services for $did.\nError: $it" } - } - } - }.distinctUntilChanged() - .flowOn(dispatcher + CoroutineName("Label Services of $did")) - - suspend fun profileLikes( - id: AtIdentifier, - cursor: SharedFlow, - limit: Long = 50, - dispatcher: CoroutineDispatcher = Dispatchers.IO, - ): Flow>> = flow>> { - val uri = AtUri.profileUserListsUri(id) - cursor.collect { cur -> - val prev = dataFlows[uri]?.value - - val query = GetActorLikesQuery(id, limit, cur.cursor) - api.api.getActorLikes(query) .onSuccess { response -> - var tunedFeed = MorphoData.concatNonThreadedFeed( - query = json.encodeToJsonElement(query), - responseCursor = response.cursor, - oldCursor = cur, - feed = response.feed, - data = (prev ?: MorphoData.EMPTY()) as MorphoData, - title = "Likes", - ) - useFeedTuners(tunedFeed).forEach { tuner -> - tunedFeed = tuner.tune(tunedFeed) - } - emit(Result.success(tunedFeed)) - mutex.withLock { - if(prev == null) _dataFlows[uri] = MutableStateFlow(tunedFeed as MorphoData) - else _dataFlows[uri]?.update { tunedFeed as MorphoData } - } - }.onFailure { - emit(Result.failure(it)) - log.e { "Failed to get likes for $id.\nError: $it" } - log.v { "Cursor: $cur | Limit: $limit" } - } - - } - }.distinctUntilChanged() - .flowOn(dispatcher + CoroutineName("Likes of $id")) - - suspend fun profiles( - profiles: List, - update: SharedFlow, - dispatcher: CoroutineDispatcher = Dispatchers.IO, - ): Flow>> = flow>> { - val uri = AtUri.myUserListUri(profiles.hashCode().toString()) - update.collect { - val query = GetProfilesQuery(profiles.toPersistentList()) - api.api.getProfiles(query).onSuccess { response -> - - val data = MorphoData("Profiles", uri, AtCursor.EMPTY, - response.profiles.mapImmutable { MorphoDataItem.ProfileItem(it.toProfile()) }, - json.encodeToJsonElement(query)) - - emit(Result.success(data)) - mutex.withLock { - if(dataFlows[uri] == null) _dataFlows[uri] = MutableStateFlow(data as MorphoData) - else _dataFlows[uri]?.update { data as MorphoData } - } - }.onFailure { - emit(Result.failure(it)) - log.e { "Failed to get profiles.\nError: $it" } - log.v { "$profiles" } - } - } - }.distinctUntilChanged().flowOn(dispatcher) - suspend fun peekLatest( - feed: MorphoData, - update: SharedFlow = MutableSharedFlow(), - dispatcher: CoroutineDispatcher = Dispatchers.IO, - ): Flow = flow { - update.collect { - when(feed.feedType) { - FeedType.HOME -> { - val query = GetTimelineQuery(limit = 1, cursor = null) - api.api.getTimeline(query).onSuccess { response -> - if (response.feed.isNotEmpty()) { - val cid = response.feed.first().post.cid - if (!feed.contains(cid)) { - emit(MorphoDataItem.Post(response.feed.first().toPost())) - } else { - emit(null) - } - } - }.onFailure { emit(null) } - } - FeedType.PROFILE_POSTS -> { - val query = GetAuthorFeedQuery(feed.uri.id(api), 1, null, GetAuthorFeedFilter.POSTS_NO_REPLIES) - api.api.getAuthorFeed(query) - .onSuccess { response -> - if (response.feed.isNotEmpty()) { - val cid = response.feed.first().post.cid - if (!feed.contains(cid)) { - emit(MorphoDataItem.Post(response.feed.first().toPost())) - } else { - emit(null) - } - } - }.onFailure { emit(null) } - } - FeedType.PROFILE_REPLIES -> { - val query = GetAuthorFeedQuery(feed.uri.id(api), 1, null, GetAuthorFeedFilter.POSTS_WITH_REPLIES) - api.api.getAuthorFeed(query) - .onSuccess { response -> - if (response.feed.isNotEmpty()) { - val cid = response.feed.first().post.cid - if (!feed.contains(cid)) { - emit(MorphoDataItem.Post(response.feed.first().toPost())) - } else { - emit(null) - } - } - }.onFailure { emit(null) } - } - FeedType.PROFILE_MEDIA -> { - val query = GetAuthorFeedQuery(feed.uri.id(api), 1, null, GetAuthorFeedFilter.POSTS_WITH_MEDIA) - api.api.getAuthorFeed(query) - .onSuccess { response -> - if (response.feed.isNotEmpty()) { - val cid = response.feed.first().post.cid - if (!feed.contains(cid)) { - emit(MorphoDataItem.Post(response.feed.first().toPost())) - } else { - emit(null) - } - } - }.onFailure { emit(null) } - } - FeedType.PROFILE_LIKES -> { - val query = GetActorLikesQuery(feed.uri.id(api), 1, null) - api.api.getActorLikes(query) - .onSuccess { response -> - if (response.feed.isNotEmpty()) { - val cid = response.feed.first().post.cid - if (!feed.contains(cid)) { - emit(MorphoDataItem.Post(response.feed.first().toPost())) - } else { - emit(null) - } - } - }.onFailure { emit(null) } - } - FeedType.PROFILE_USER_LISTS -> { - val query = GetListsQuery(feed.uri.id(api), 1) - api.api.getLists(query) - .onSuccess { response -> - if (response.lists.isNotEmpty()) { - val cid = response.lists.first().cid - if (!feed.contains(cid)) { - emit(MorphoDataItem.ListInfo(response.lists.first().toList())) - } else { - emit(null) - } - } - }.onFailure { emit(null) } - } - FeedType.PROFILE_MOD_SERVICE -> { - val id = feed.uri.id(api) - if(Did.Regex.matches(id.toString())) emit(null) - else { - val query = GetServicesQuery(persistentListOf(Did(id.toString())), true) - api.api.getServices(query) - .onSuccess { response -> - if (response.views.isNotEmpty()) { - when(response.views.first()) { - is GetServicesResponseViewUnion.LabelerViewDetailed -> { - val cid = (response.views.first() as GetServicesResponseViewUnion.LabelerViewDetailed).value.cid - if (!feed.contains(cid)) { - emit(MorphoDataItem.LabelService((response.views.first() as GetServicesResponseViewUnion.LabelerViewDetailed).value.toLabelService())) - } else { - emit(null) - } - } - - is GetServicesResponseViewUnion.LabelerView -> { - val cid = (response.views.first() as GetServicesResponseViewUnion.LabelerView).value.cid - if (!feed.contains(cid)) { - emit(MorphoDataItem.LabelService((response.views.first() as GetServicesResponseViewUnion.LabelerView).value.toLabelService())) - } else { - emit(null) - } - } - } - } - }.onFailure { emit(null) } - } - } - FeedType.PROFILE_FEEDS_LIST -> { - val query = GetActorFeedsQuery(feed.uri.id(api), 1) - api.api.getActorFeeds(query) - .onSuccess { response -> - if (response.feeds.isNotEmpty()) { - val cid = response.feeds.first().cid - if (!feed.contains(cid)) { - emit(MorphoDataItem.FeedInfo(response.feeds.first().toFeedGenerator())) - } else { - emit(null) - } - } - }.onFailure { emit(null) } - } - FeedType.OTHER -> { - // assume it's a custom feed for now, but we should probably add more types - val query = GetFeedQuery(feed.uri, 1) - api.api.getFeed(query) - .onSuccess { response -> - if (response.feed.isNotEmpty()) { - val cid = response.feed.first().post.cid - if (!feed.contains(cid)) { - emit(MorphoDataItem.Post(response.feed.first().toPost())) - } else { - emit(null) - } - } - }.onFailure { emit(null) } - } - FeedType.LIST_FOLLOWING -> { - val query = GetListFeedQuery(feed.uri, 1) - api.api.getListFeed(query).onSuccess { response -> - if (response.feed.isNotEmpty()) { - val cid = response.feed.first().post.cid - if (!feed.contains(cid)) { - emit(MorphoDataItem.Post(response.feed.first().toPost())) - } else { - emit(null) - } - } - }.onFailure { - emit(null) - } - } - } - } - }.distinctUntilChanged().flowOn(dispatcher) - - - - fun checkIfNewTimeline( - interval: Long = 60000, - dispatcher: CoroutineDispatcher = Dispatchers.IO - ): Flow = flow { - val updateTick = UpdateTick(interval) - updateTick.tick(true) - updateTick.t.collect { - val query = GetTimelineQuery(limit = 1, cursor = null) - api.api.getTimeline(query).onSuccess { response -> - if (response.feed.isNotEmpty()) { - val cid = response.feed.first().post.cid - if (dataFlows[AtUri.HOME_URI]?.value?.contains(cid) == false) { - emit(true) - } else { - emit(false) - } - } - }.onFailure { emit(false) } - } - }.distinctUntilChanged().flowOn(dispatcher) - - fun removeFeed(uri: AtUri): MorphoData? { - return _dataFlows.remove(uri)?.value - } -} diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/ContentCardMapEntry.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/ContentCardMapEntry.kt index 121df72..2929119 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/ContentCardMapEntry.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/ContentCardMapEntry.kt @@ -6,26 +6,27 @@ import com.morpho.app.util.MutableSharedFlowSerializer import com.morpho.butterfly.AtIdentifier import com.morpho.butterfly.AtUri import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.serialization.Serializable @Immutable @Serializable -sealed interface ContentCardMapEntry { +sealed interface ContentCardMapEntry { val uri: AtUri val title: String @Serializable(with = MutableSharedFlowSerializer::class) - //@TypeParceler, AtCursorMutableSharedFlowParceler> - val cursorFlow: MutableSharedFlow + val events: MutableSharedFlow + @Serializable(with = MutableSharedFlowSerializer::class) + val updates: MutableStateFlow val avatar: String? @Immutable @Serializable - data object Home: ContentCardMapEntry, Skyline { + data object Home: ContentCardMapEntry, Skyline { override val uri: AtUri = AtUri.HOME_URI override val title: String = "Home" - @Serializable(with = MutableSharedFlowSerializer::class) - //@TypeParceler, AtCursorMutableSharedFlowParceler> - override val cursorFlow: MutableSharedFlow = initAtCursor() + override val events: MutableSharedFlow = MutableSharedFlow() + override val updates: MutableStateFlow = MutableStateFlow(FeedUpdate.Empty) override val avatar: String? = null } @@ -38,55 +39,50 @@ sealed interface ContentCardMapEntry { data class Feed( override val uri: AtUri, override val title: String = uri.atUri, - @Serializable(with = MutableSharedFlowSerializer::class) - //@TypeParceler, AtCursorMutableSharedFlowParceler> - override val cursorFlow: MutableSharedFlow = initAtCursor(), + override val events: MutableSharedFlow = MutableSharedFlow(), + override val updates: MutableStateFlow = MutableStateFlow(FeedUpdate.Empty), override val avatar: String? = null, - ) : ContentCardMapEntry, Skyline + ) : ContentCardMapEntry, Skyline @Immutable @Serializable data class PostThread( override val uri: AtUri, override val title: String = uri.atUri, - @Serializable(with = MutableSharedFlowSerializer::class) - //@TypeParceler, AtCursorMutableSharedFlowParceler> - override val cursorFlow: MutableSharedFlow = initAtCursor(), + override val events: MutableSharedFlow = MutableSharedFlow(), + override val updates: MutableStateFlow = MutableStateFlow(UIUpdate.Empty), override val avatar: String? = null, - ) : ContentCardMapEntry + ) : ContentCardMapEntry @Immutable @Serializable data class UserList( override val uri: AtUri, override val title: String = uri.atUri, - @Serializable(with = MutableSharedFlowSerializer::class) - //@TypeParceler, AtCursorMutableSharedFlowParceler> - override val cursorFlow: MutableSharedFlow = initAtCursor(), + override val events: MutableSharedFlow = MutableSharedFlow(), + override val updates: MutableStateFlow = MutableStateFlow(UIUpdate.Empty), override val avatar: String? = null, - ) : ContentCardMapEntry + ) : ContentCardMapEntry @Immutable @Serializable data class FeedList( override val uri: AtUri, override val title: String = uri.atUri, - @Serializable(with = MutableSharedFlowSerializer::class) - //@TypeParceler, AtCursorMutableSharedFlowParceler> - override val cursorFlow: MutableSharedFlow = initAtCursor(), + override val events: MutableSharedFlow = MutableSharedFlow(), + override val updates: MutableStateFlow = MutableStateFlow(UIUpdate.Empty), override val avatar: String? = null, - ) : ContentCardMapEntry + ) : ContentCardMapEntry @Immutable @Serializable data class ServiceList( override val uri: AtUri, override val title: String = uri.atUri, - @Serializable(with = MutableSharedFlowSerializer::class) - //@TypeParceler, AtCursorMutableSharedFlowParceler> - override val cursorFlow: MutableSharedFlow = initAtCursor(), + override val events: MutableSharedFlow = MutableSharedFlow(), + override val updates: MutableStateFlow = MutableStateFlow(UIUpdate.Empty), override val avatar: String? = null, - ) : ContentCardMapEntry + ) : ContentCardMapEntry @Immutable @Serializable @@ -94,11 +90,10 @@ sealed interface ContentCardMapEntry { val id: AtIdentifier, override val uri: AtUri = AtUri.profileUri(id), override val title: String = uri.atUri, - @Serializable(with = MutableSharedFlowSerializer::class) - //@TypeParceler, AtCursorMutableSharedFlowParceler> - override val cursorFlow: MutableSharedFlow = initAtCursor(), + override val events: MutableSharedFlow = MutableSharedFlow(), + override val updates: MutableStateFlow = MutableStateFlow(UIUpdate.Empty), override val avatar: String? = null, - ) : ContentCardMapEntry + ) : ContentCardMapEntry val isHome: Boolean get() = uri == AtUri.HOME_URI diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/ContentLabelService.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/ContentLabelService.kt index 7974ddd..b6bbf0c 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/ContentLabelService.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/ContentLabelService.kt @@ -1,991 +1,189 @@ package com.morpho.app.model.uidata -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.HideImage -import androidx.compose.material.icons.filled.Info -import androidx.compose.material.icons.filled.StopCircle -import androidx.compose.material.icons.filled.Warning -import androidx.compose.runtime.Immutable -import androidx.compose.ui.graphics.vector.ImageVector -import androidx.compose.ui.util.fastAny -import androidx.compose.ui.util.fastFilter -import androidx.compose.ui.util.fastForEach +import app.bsky.actor.MuteTargetGroup +import app.bsky.actor.MutedWord import app.bsky.actor.Visibility -import com.atproto.label.LabelValue +import app.bsky.labeler.LabelerViewDetailed +import com.atproto.label.Blurs import com.atproto.label.Severity -import com.morpho.app.model.bluesky.* -import com.morpho.butterfly.AtUri -import com.morpho.butterfly.Butterfly -import com.morpho.butterfly.Language -import com.morpho.butterfly.model.ReadOnlyList -import dev.icerock.moko.parcelize.Parcelable -import dev.icerock.moko.parcelize.Parcelize -import kotlinx.collections.immutable.* -import kotlinx.coroutines.* -import kotlinx.coroutines.flow.SharingStarted -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.stateIn -import kotlinx.serialization.Contextual -import kotlinx.serialization.Serializable +import com.morpho.app.data.MorphoAgent +import com.morpho.app.model.bluesky.BskyPost +import com.morpho.app.model.bluesky.MorphoDataItem +import com.morpho.app.model.bluesky.toAtProtoLabel +import com.morpho.app.model.bluesky.toListVewBasic +import com.morpho.butterfly.* +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.launch import org.koin.core.component.KoinComponent import org.koin.core.component.inject import org.lighthousegames.logging.logging -@Parcelize -data class ContentHandling( - val scope: LabelScope, - val action: LabelAction, - val source: LabelDescription, - val id: String, - val icon: LabelIcon, -): Parcelable - -@Parcelize -@Immutable -@Serializable -sealed interface LabelIcon: Parcelable { - val labelerAvatar: String? - val icon: ImageVector - - @Serializable - @Immutable - data class CircleBanSign( - override val labelerAvatar: String? - ): LabelIcon { - override val icon: ImageVector - get() = Icons.Default.StopCircle - } - - @Serializable - @Immutable - data class Warning( - override val labelerAvatar: String? - ): LabelIcon { - override val icon: ImageVector - get() = Icons.Default.Warning - } - - @Serializable - @Immutable - data class EyeSlash( - override val labelerAvatar: String? - ): LabelIcon { - override val icon: ImageVector - get() = Icons.Default.HideImage - } - - @Serializable - @Immutable - data class CircleInfo( - override val labelerAvatar: String? - ): LabelIcon { - override val icon: ImageVector - get() = Icons.Default.Info - } - -} - -@Parcelize -@Immutable -@Serializable -sealed interface LabelDescription: Parcelable { - val name: String - val description: String - - @Parcelize - @Immutable - @Serializable - sealed interface Block: LabelDescription, Parcelable - @Parcelize - @Immutable - @Serializable - data object Blocking: Block { - override val name: String = "User Blocked" - override val description: String = "You have blocked this user. You cannot view their content" - - } - @Parcelize - @Immutable - @Serializable - data object BlockedBy: Block { - override val name: String = "User Blocking You" - override val description: String = "This user has blocked you. You cannot view their content." - } - @Parcelize - @Immutable - @Serializable - data class BlockList( - val listName: String, - val listUri: AtUri, - ): Block { - override val name: String = "User Blocked by $listName" - override val description: String = "This user is on a block list you subscribe to. You cannot view their content." - } - @Parcelize - @Immutable - @Serializable - data object OtherBlocked: Block { - override val name: String = "Content Not Available" - override val description: String = "This content is not available because one of the users involved has blocked the other." - } - - @Parcelize - @Immutable - @Serializable - sealed interface Muted: LabelDescription, Parcelable - - @Parcelize - @Immutable - @Serializable - data class MuteList( - val listName: String, - val listUri: AtUri, - ): Muted { - override val name: String = "User Muted by $listName" - override val description: String = "This user is on a mute list you subscribe to." - } - @Parcelize - @Immutable - @Serializable - data object YouMuted: Muted { - override val name: String = "Account Muted" - override val description: String = "You have muted this user." - } - @Parcelize - @Immutable - @Serializable - data class MutedWord(val word: String): Muted { - override val name: String = "Post Hidden by Muted Word" - override val description: String = "This post contains the word or tag \"$word\". You've chosen to hide it." - } - - @Parcelize - @Immutable - @Serializable - data class HiddenPost(val uri: AtUri): LabelDescription { - override val name: String = "Post Hidden by You" - override val description: String = "You have hidden this post." - } - - @Parcelize - @Immutable - @Serializable - data class Label( - override val name: String, - override val description: String, - val severity: Severity, - ): LabelDescription -} - -@Parcelize -@Immutable -@Serializable -sealed interface LabelSource: Parcelable { - @Immutable - @Serializable - data object User: LabelSource - @Immutable - @Serializable - data class List( - val list: BskyList, - ): LabelSource - @Immutable - @Serializable - data class Labeler( - val labeler: BskyLabelService, - ): LabelSource -} - -@Parcelize -@Immutable -@Serializable -sealed interface LabelCause: Parcelable { - val downgraded: Boolean - val priority: Int - val source: LabelSource - @Immutable - @Serializable - data class Blocking( - override val source: LabelSource, - override val downgraded: Boolean, - ): LabelCause { - override val priority: Int = 3 - } - @Immutable - @Serializable - data class BlockedBy( - override val source: LabelSource, - override val downgraded: Boolean, - ): LabelCause { - override val priority: Int = 4 - } - - @Immutable - @Serializable - data class BlockOther( - override val source: LabelSource, - override val downgraded: Boolean, - ): LabelCause { - override val priority: Int = 4 - } - - @Immutable - @Serializable - data class Label( - override val source: LabelSource, - val label: BskyLabel, - val labelDef: InterpretedLabelDefinition, - val target: LabelTarget, - val setting: LabelSetting, - val behaviour: ModBehaviour, - val noOverride: Boolean, - override val priority: Int, - override val downgraded: Boolean, - ): LabelCause { - init { - require( - priority == 1 || priority == 2 || priority == 3 || - priority == 5 || priority == 7 || priority == 8 - ) - } - } - - @Immutable - @Serializable - data class Muted( - override val source: LabelSource, - override val downgraded: Boolean, - ): LabelCause { - override val priority: Int = 6 - } - - @Immutable - @Serializable - data class MutedWord( - override val source: LabelSource, - override val downgraded: Boolean, - ): LabelCause { - override val priority: Int = 6 - } - - @Immutable - @Serializable - data class Hidden( - override val source: LabelSource, - override val downgraded: Boolean, - ): LabelCause { - override val priority: Int = 6 - } - -} - - -@Parcelize -@Serializable -@Immutable -open class InterpretedLabelDefinition( - val identifier: String, - val configurable: Boolean, - val severity: Severity, - val whatToHide: LabelScope, - val defaultSetting: LabelSetting?, - @Contextual - val flags: List = persistentListOf(), - val behaviours: ModBehaviours, - val localizedName: String = "", - val localizedDescription: String = "", - @Contextual - val allDescriptions: ImmutableMap = persistentMapOf(), -): Parcelable { +class ContentLabelService: KoinComponent { + val agent: MorphoAgent by inject() + val serviceScope = CoroutineScope(SupervisorJob() + Dispatchers.Default) companion object { - + val log = logging("ContentLabelService") } - public fun toContentHandling(target: LabelTarget, avatar: String? = null): ContentHandling { - val action = behaviours.forScope(whatToHide, target).minOrNull() ?: when(defaultSetting) { - LabelSetting.HIDE -> LabelAction.Blur - LabelSetting.WARN -> LabelAction.Alert - LabelSetting.IGNORE -> LabelAction.Inform - null -> LabelAction.None - } - return ContentHandling( - id = identifier, - scope = whatToHide, - action = action, - source = LabelDescription.Label( - name = localizedName, - description = localizedDescription, - severity = severity, - ), - icon = when(severity) { - Severity.ALERT -> LabelIcon.Warning(labelerAvatar = avatar) - Severity.NONE -> LabelIcon.CircleInfo(labelerAvatar = avatar) - Severity.INFORM -> LabelIcon.CircleInfo(labelerAvatar = avatar) - } - ) - } -} - -val LABELS: PersistentMap = persistentMapOf( - LabelValue.HIDE to Hide, - LabelValue.WARN to Warn, - LabelValue.NO_UNAUTHENTICATED to NoUnauthed, - LabelValue.PORN to Porn, - LabelValue.SEXUAL to Sexual, - LabelValue.NUDITY to Nudity, - LabelValue.GRAPHIC_MEDIA to GraphicMedia, -) - -@Parcelize -@Immutable -@Serializable -data object Hide: InterpretedLabelDefinition( - "!hide", - false, - Severity.ALERT, - LabelScope.Content, - LabelSetting.HIDE, - persistentListOf(LabelValueDefFlag.NoSelf, LabelValueDefFlag.NoOverride), - ModBehaviours( - account = ModBehaviour( - profileList = LabelAction.Blur, - profileView = LabelAction.Blur, - avatar = LabelAction.Blur, - banner = LabelAction.Blur, - displayName = LabelAction.Blur, - contentList = LabelAction.Blur, - contentView = LabelAction.Blur, - ), - profile = ModBehaviour( - avatar = LabelAction.Blur, - banner = LabelAction.Blur, - displayName = LabelAction.Blur, - ), - content = ModBehaviour( - contentList = LabelAction.Blur, - contentView = LabelAction.Blur, - ), - ), - localizedName = "Hide", - localizedDescription = "Hide", -) - -@Immutable -@Serializable -data object Warn: InterpretedLabelDefinition( - "!warn", - false, - Severity.NONE, - LabelScope.Content, - LabelSetting.WARN, - persistentListOf(LabelValueDefFlag.NoSelf), - ModBehaviours( - account = ModBehaviour( - profileList = LabelAction.Blur, - profileView = LabelAction.Blur, - avatar = LabelAction.Blur, - banner = LabelAction.Blur, - displayName = LabelAction.Blur, - contentList = LabelAction.Blur, - contentView = LabelAction.Blur, - ), - profile = ModBehaviour( - avatar = LabelAction.Blur, - banner = LabelAction.Blur, - displayName = LabelAction.Blur, - ), - content = ModBehaviour( - contentList = LabelAction.Blur, - contentView = LabelAction.Blur, - ), - ), - localizedName = "Warn", - localizedDescription = "Warn", -) + val modPrefs: ModerationPreferences + get() = agent.prefs.modPrefs -@Parcelize -@Immutable -@Serializable -data object NoUnauthed: InterpretedLabelDefinition( - "!no-unauthenticated", - false, - Severity.NONE, - LabelScope.Content, - LabelSetting.HIDE, - persistentListOf(LabelValueDefFlag.NoOverride, LabelValueDefFlag.Unauthed), - ModBehaviours( - account = ModBehaviour( - profileList = LabelAction.Blur, - profileView = LabelAction.Blur, - avatar = LabelAction.Blur, - banner = LabelAction.Blur, - displayName = LabelAction.Blur, - contentList = LabelAction.Blur, - contentView = LabelAction.Blur, - ), - profile = ModBehaviour( - avatar = LabelAction.Blur, - banner = LabelAction.Blur, - displayName = LabelAction.Blur, - ), - content = ModBehaviour( - contentList = LabelAction.Blur, - contentView = LabelAction.Blur, - ), - ), - localizedName = "No Unauthenticated", - localizedDescription = "Do not show to unauthenticated users", -) + val hiddenPosts: List + get() = modPrefs.hiddenPosts -@Immutable -@Serializable -data object Porn: InterpretedLabelDefinition( - "porn", - true, - Severity.NONE, - LabelScope.Media, - LabelSetting.HIDE, - persistentListOf(LabelValueDefFlag.Adult), - ModBehaviours( - account = ModBehaviour( - avatar = LabelAction.Blur, - banner = LabelAction.Blur, - ), - profile = ModBehaviour( - avatar = LabelAction.Blur, - banner = LabelAction.Blur, - ), - content = ModBehaviour( - contentMedia = LabelAction.Blur, - ), - ), - localizedName = "Sexually Explicit", - localizedDescription = "This content is sexually explicit", -) + val mutedWords: List + get() = modPrefs.mutedWords -@Immutable -@Serializable -data object Sexual: InterpretedLabelDefinition( - "sexual", - true, - Severity.NONE, - LabelScope.Media, - LabelSetting.HIDE, - persistentListOf(LabelValueDefFlag.Adult), - ModBehaviours( - account = ModBehaviour( - avatar = LabelAction.Blur, - banner = LabelAction.Blur, - ), - profile = ModBehaviour( - avatar = LabelAction.Blur, - banner = LabelAction.Blur, - ), - content = ModBehaviour( - contentMedia = LabelAction.Blur, - ), - ), - localizedName = "Suggestive", - localizedDescription = "This content may be suggestive or sexual in nature", -) -@Immutable -@Serializable -data object Nudity: InterpretedLabelDefinition( - "nudity", - true, - Severity.NONE, - LabelScope.Media, - LabelSetting.HIDE, - persistentListOf(LabelValueDefFlag.Adult), - ModBehaviours( - account = ModBehaviour( - avatar = LabelAction.Blur, - banner = LabelAction.Blur, - ), - profile = ModBehaviour( - avatar = LabelAction.Blur, - banner = LabelAction.Blur, - ), - content = ModBehaviour( - contentMedia = LabelAction.Blur, - ), - ), - localizedName = "Nudity", - localizedDescription = "This content contains nudity, artistic or otherwise", -) + val labelers: Map> + get() = modPrefs.labelers -@Immutable -@Serializable -data object GraphicMedia: InterpretedLabelDefinition( - "graphic-media", - true, - Severity.NONE, - LabelScope.Media, - LabelSetting.HIDE, - persistentListOf(LabelValueDefFlag.Adult), - ModBehaviours( - account = ModBehaviour( - avatar = LabelAction.Blur, - banner = LabelAction.Blur, - ), - profile = ModBehaviour( - avatar = LabelAction.Blur, - banner = LabelAction.Blur, - ), - content = ModBehaviour( - contentMedia = LabelAction.Blur, - ), - ), - localizedName = "Graphic Content", - localizedDescription = "This content is graphic or violent in nature", -) + val labels: Map + get() = modPrefs.labels + var labelDefinitions: Map> = emptyMap() + private set -class ContentLabelService: KoinComponent { - val api:Butterfly by inject() - val settings: SettingsService by inject() - - val labelers = settings.labelers.stateIn(serviceScope, SharingStarted.Lazily, persistentListOf()) - val labelPrefs = settings.contentLabelPrefs.stateIn(serviceScope, SharingStarted.Lazily, persistentListOf()) - val mutedUsers = settings.mutedUsers.stateIn(serviceScope, SharingStarted.Lazily, persistentListOf()) - val mutedWords = settings.mutedWords.stateIn(serviceScope, SharingStarted.Lazily, persistentListOf()) - val hiddenPosts = settings.hiddenPosts.stateIn(serviceScope, SharingStarted.Lazily, persistentListOf()) - val showAdultContent = settings.showAdultContent.stateIn(serviceScope, SharingStarted.Lazily, false) - val feedPrefs = settings.feedViewPrefs.stateIn(serviceScope, SharingStarted.Lazily, mapOf()) - val labelsToHide = labelPrefs.map { contentLabelPrefs -> - contentLabelPrefs.fastFilter { it.visibility == Visibility.HIDE } - }.stateIn(serviceScope, SharingStarted.Eagerly, persistentListOf()) - - private val handlingCache = mutableMapOf>() - private val definitionCache = mutableMapOf() - - companion object { - val log = logging() - val serviceScope = CoroutineScope(SupervisorJob() + Dispatchers.Default) - } + var labelerDetails: Map = emptyMap() + private set init { serviceScope.launch { - while(!api.isLoggedIn()) { - delay(100) - } - if (api.isLoggedIn()) { - initDefinitionCache() - } - + agent.getLabelDefinitions(modPrefs) + agent.getLabelersDetailed(labelers.keys.map { Did(it) }) } } - private fun initDefinitionCache() { - val labelers = labelers.value - log.verbose { "Labelers: $labelers" } - val labelPrefs = labelPrefs.value - log.verbose { "Label prefs: $labelPrefs" } - val labelPrefMap = labelPrefs.associateBy { if (it.labelerDid == null) it.label else it.labelerDid.toString() } - val labelerMap = labelers.associateBy { it.did.toString() } - log.verbose { "Labeler map: $labelerMap" } - val labelMap = labelerMap.mapValues { (id, labeler) -> - val labelPref = labelPrefMap[id] - if (labelPref != null) { - val policy = labeler.policies.firstOrNull { it.identifier == labelPref.label } - if (policy != null) { - Pair( - labeler.labels.first { it.value == policy.identifier }, - policy.copy(defaultSetting = labelPref.visibility.toLabelSetting()), - ) - } else { - Pair( - labeler.labels.first { label -> - labeler.policies.fastAny { it.identifier == label.value } }, - labeler.policies.first { def -> - labeler.labels.fastAny { it.value == def.identifier } }, - ) - } - } else { - Pair( - labeler.labels.first { label -> - labeler.policies.fastAny { it.identifier == label.value } }, - labeler.policies.first { def -> - labeler.labels.fastAny { it.value == def.identifier } }, - ) - } - } - val definitionMap = labelMap.mapValues { (id, pair) -> - val (label, policy) = pair - val name = label.value - val flags = mutableListOf() - var interpreted: InterpretedLabelDefinition? = null - if (policy.adultOnly == true) { - flags.add(LabelValueDefFlag.Adult) - } - when (label.getLabelValue()) { - LabelValue.HIDE -> interpreted = Hide - LabelValue.WARN -> interpreted = Warn - LabelValue.NO_UNAUTHENTICATED -> interpreted = NoUnauthed - LabelValue.PORN -> interpreted = Porn - LabelValue.SEXUAL -> interpreted = Sexual - LabelValue.NSFL -> interpreted = GraphicMedia - LabelValue.GORE -> interpreted = GraphicMedia - LabelValue.GRAPHIC_MEDIA -> interpreted = GraphicMedia - else -> {} + fun shouldHideItem(item: MorphoDataItem.FeedItem): Boolean { + return when (item) { + is MorphoDataItem.Post -> { + item.post.author.mutedByMe + || item.post.author.blocking + || item.post.author.blockedBy + || hiddenPosts.any { uri -> item.containsUri(uri) } + || mutedWords.any { + item.post.text.contains(it.value, ignoreCase = true) + } || if(!modPrefs.adultContentEnabled) { + val adultLabels = item.post.labels.filter { label -> + labelDefinitions[label.creator.did]?.get(label.value)?.flags + ?.contains(LabelValueDefFlag.Adult) == true + } + adultLabels.isNotEmpty() + } else { + item.post.labels.any { label -> + labels[label.value] == Visibility.HIDE + } + } } - - if (interpreted == null) { - val behaviours = when (policy.whatToHide) { - LabelScope.Content -> ModBehaviours( - account = ModBehaviour( - contentList = LabelAction.Blur, - contentView = LabelAction.Blur, - ), - profile = ModBehaviour( - contentList = LabelAction.Blur, - contentView = LabelAction.Blur, - ), - content = ModBehaviour( - contentList = LabelAction.Blur, - contentView = LabelAction.Blur, - ), - ) - LabelScope.Media -> BlurAllMedia - LabelScope.None -> ModBehaviours( - NoopBehaviour, - NoopBehaviour, - NoopBehaviour, - ) - } - interpreted = InterpretedLabelDefinition( - policy.identifier, - true, - policy.severity, - policy.whatToHide, - policy.defaultSetting, - flags.toImmutableList(), - behaviours, - localizedName = policy.localizedName, - localizedDescription = policy.localizedDescription, - allDescriptions = policy.allDescriptions, - ) + is MorphoDataItem.Thread -> { + item.thread.anyMutedOrBlocked() + || hiddenPosts.any { uri -> item.containsUri(uri) } + || mutedWords.any { + item.thread.containsWord(it.value) + } || if(!modPrefs.adultContentEnabled) { + val adultLabels = item.thread.getLabels().filter { label -> + labelDefinitions[label.creator.did]?.get(label.value)?.flags + ?.contains(LabelValueDefFlag.Adult) == true + } + adultLabels.isNotEmpty() + } else { + item.thread.getLabels().any { label -> + labels[label.value] == Visibility.HIDE + } + } } - Pair(name, interpreted) - }.values.toMap() - definitionCache.putAll(definitionMap) + } } - fun getContentHandlingForPost(post: BskyPost): List { -// // TODO: Add some way to invalidate the cache -// if (handlingCache.containsKey(post.uri)) { -// return handlingCache[post.uri]!! -// } - val result = mutableListOf() - val causes = mutableListOf() - val labels = post.labels - if (hiddenPosts.value.contains(post.uri)) { - causes.add(LabelCause.Hidden(LabelSource.User, false)) - result.add(Hide.toContentHandling(LabelTarget.Content)) - // Short circuit if the post is hidden, we shouldn't really get here - // Generally it will be filtered out at the feed retrieval level - return result.toImmutableList() + fun getContentHandlingForPost(post: BskyPost): List> { + val result = mutableListOf>() + val postLabels = post.labels + + if(post.author.mutedByMe) { + result.add(ContentHandling( + scope = Blurs.CONTENT, + action = LabelAction.Blur, + source = LabelDescription.YouMuted, + id = "muted", + icon = LabelIcon.EyeSlash(labelerAvatar = null), + ) to LabelCause.Muted(LabelSource.User, false)) + } + if(post.author.mutedByList != null) { + val list = post.author.mutedByList!! + result.add(ContentHandling( + scope = Blurs.CONTENT, + action = LabelAction.Blur, + source = LabelDescription.MuteList( + list.name, + list.uri, + ), + id = "muted-word", + icon = LabelIcon.EyeSlash( labelerAvatar = list.avatar), + ) to LabelCause.Muted(LabelSource.List(list.toListVewBasic()), false)) + } + val anyMutedWords = mutedWords.filter { post.text.contains(it.value, ignoreCase = true) } + if(anyMutedWords.isNotEmpty()) anyMutedWords.forEach { word -> + if(!word.targets.contains(MutedWordTarget("content"))) return@forEach + if(word.actorTarget == MuteTargetGroup.EXCLUDE_FOLLOWING && post.author.followedByMe) return@forEach + result.add(ContentHandling( + scope = Blurs.CONTENT, + action = LabelAction.Blur, + source = LabelDescription.MutedWord(word.value), + id = "muted-word", + icon = LabelIcon.EyeSlash(), + ) to LabelCause.MutedWord(LabelSource.User, false)) } - if (labels.isNotEmpty()) { - log.verbose { "Post ${post.uri} has labels: ${labels.joinToString { it.value }}" } - if (!showAdultContent.value) { - val adultLabeler = labelPrefs.value.fastFilter { prefLabel -> - labels.fastAny { bskyLabel -> - prefLabel.label == bskyLabel.value && - labelers.value.fastAny { it.policies.fastAny { policy -> - policy.adultOnly == true && policy.identifier == prefLabel.label - } } - } - } - val adultLabel = labels.firstOrNull { bskyLabel -> - val value = bskyLabel.getLabelValue() - value == LabelValue.GRAPHIC_MEDIA - || value == LabelValue.GORE - || value == LabelValue.NSFL - || value == LabelValue.PORN - || value == LabelValue.SEXUAL - || value == LabelValue.NUDITY - || adultLabeler.isNotEmpty() - } - log.debug { "Post ${post.uri} has adult label: $adultLabel" } - when (adultLabel?.getLabelValue()) { - LabelValue.PORN -> causes.add(LabelCause.Label( - LabelSource.Labeler(labelers.value.firstOrNull { it.did == adultLabel.creator } ?: BlueskyHardcodedLabeler), - adultLabel, - Porn, - LabelTarget.Content, - LabelSetting.HIDE, - Porn.behaviours.content, - noOverride = true, - priority = 7, - downgraded = false, - )) - LabelValue.SEXUAL -> causes.add(LabelCause.Label( - LabelSource.Labeler(labelers.value.firstOrNull { it.did == adultLabel.creator } ?: BlueskyHardcodedLabeler), - adultLabel, - Sexual, - LabelTarget.Content, - LabelSetting.HIDE, - Sexual.behaviours.content, - noOverride = true, - priority = 7, - downgraded = false, - )) - LabelValue.NUDITY -> causes.add(LabelCause.Label( - LabelSource.Labeler(labelers.value.firstOrNull { it.did == adultLabel.creator } ?: BlueskyHardcodedLabeler), - adultLabel, - Nudity, - LabelTarget.Content, - LabelSetting.HIDE, - Nudity.behaviours.content, - noOverride = true, - priority = 7, - downgraded = false, - )) - LabelValue.GRAPHIC_MEDIA -> causes.add(LabelCause.Label( - LabelSource.Labeler(labelers.value.firstOrNull { it.did == adultLabel.creator } ?: BlueskyHardcodedLabeler), - adultLabel, - GraphicMedia, - LabelTarget.Content, - LabelSetting.HIDE, - GraphicMedia.behaviours.content, - noOverride = true, - priority = 8, - downgraded = false, - )) - LabelValue.NSFL -> causes.add(LabelCause.Label( - LabelSource.Labeler(labelers.value.firstOrNull { it.did == adultLabel.creator } ?: BlueskyHardcodedLabeler), - adultLabel, - GraphicMedia, - LabelTarget.Content, - LabelSetting.HIDE, - GraphicMedia.behaviours.content, - noOverride = true, - priority = 8, - downgraded = false, - )) - LabelValue.GORE -> causes.add(LabelCause.Label( - LabelSource.Labeler(labelers.value.firstOrNull { it.did == adultLabel.creator } ?: BlueskyHardcodedLabeler), - adultLabel, - GraphicMedia, - LabelTarget.Content, - LabelSetting.HIDE, - GraphicMedia.behaviours.content, - noOverride = true, - priority = 8, - downgraded = false, - )) - null -> {} - else -> { - adultLabeler.fastForEach { prefLabel -> - val labeler = labelers.value.firstOrNull { it.did == prefLabel.labelerDid } - val labelDef = labeler?.policies?.firstOrNull { it.identifier == prefLabel.label } - if (labeler != null && labelDef != null) { - val cached = definitionCache[prefLabel.label] - if (cached != null) { - val cause = LabelCause.Label( - LabelSource.Labeler(labeler), - adultLabel, - cached, - LabelTarget.Content, - prefLabel.visibility.toLabelSetting(), - cached.behaviours.content, - noOverride = false, - priority = 7, - downgraded = false, - ) - causes.add(cause) - } else { - val behaviours = when (labelDef.whatToHide) { - LabelScope.Content -> ModBehaviours( - account = ModBehaviour( - contentList = LabelAction.Blur, - contentView = LabelAction.Blur, - ), - profile = ModBehaviour( - contentList = LabelAction.Blur, - contentView = LabelAction.Blur, - ), - content = ModBehaviour( - contentList = LabelAction.Blur, - contentView = LabelAction.Blur, - ), - ) - LabelScope.Media -> BlurAllMedia - LabelScope.None -> ModBehaviours( - NoopBehaviour, - NoopBehaviour, - NoopBehaviour, - ) - } - val interpreted = InterpretedLabelDefinition( - adultLabel.value, - true, - labelDef.severity, - labelDef.whatToHide, - labelDef.defaultSetting, - persistentListOf(LabelValueDefFlag.Adult), - behaviours, - localizedName = labelDef.localizedName, - localizedDescription = labelDef.localizedDescription, - ) - val cause = LabelCause.Label( - LabelSource.Labeler(labeler), - adultLabel, - interpreted, - LabelTarget.Content, - prefLabel.visibility.toLabelSetting(), - interpreted.behaviours.content, - noOverride = false, - priority = 7, - downgraded = false, - ) - causes.add(cause) - definitionCache[prefLabel.label] = interpreted - } - } - } - } - } - } - val labelsWeCareAbout = labelPrefs.value.fastFilter { prefLabel -> - labels.fastAny { it.value == prefLabel.label } + if (postLabels.isNotEmpty()) { + log.verbose { "Post ${post.uri} has labels: ${postLabels.joinToString { it.value }}" } + // Adult content hiding if someone doesn't have it enabled is handled earlier, + // before rendering starts, as is Visibility.HIDE + // so we don't need to worry about it here + val relevantLabels = labels.filter { prefLabel -> + (prefLabel.value == Visibility.WARN || prefLabel.value == Visibility.HIDE) + && postLabels.any { it.value == it.value } }.toList() + .sortedBy { it.second.ordering } + val filteredPostLabels = postLabels.filter { label -> + relevantLabels.any { label.value == it.first } } - log.verbose { "Post ${post.uri} has labels we care about: ${labelsWeCareAbout.joinToString { it.label }}" } - labelsWeCareAbout.fastForEach { prefLabel -> - val cachedInterpretation = definitionCache[prefLabel.label] - if (cachedInterpretation != null) { - log.verbose { "Post ${post.uri} has cached interpretation for ${prefLabel.label}" } - val cause = LabelCause.Label( - LabelSource.Labeler(labelers.value.firstOrNull { it.did == prefLabel.labelerDid }!!), - labels.first { it.value == prefLabel.label }, - cachedInterpretation, - LabelTarget.Content, - prefLabel.visibility.toLabelSetting(), - cachedInterpretation.behaviours.content, - noOverride = false, - priority = 5, - downgraded = false, + val possibleCauses = filteredPostLabels.mapNotNull { label -> + labelDefinitions[label.creator.did]?.get(label.value)?.let { labelDef -> + val localizedDefString = labelDef.allDescriptions.firstOrNull { + it.lang == agent.myLanguage + } ?: labelDef.allDescriptions.firstOrNull { it.lang.tag == "en" } + val localLabelDef = labelDef.copy( + localizedName = localizedDefString?.name ?: labelDef.localizedName, + localizedDescription = localizedDefString?.description + ?: labelDef.localizedDescription, ) - causes.add(cause) - } else { - val labeler = labelers.value.firstOrNull { it.did == prefLabel.labelerDid } - val labelDef = labeler?.policies?.firstOrNull { it.identifier == prefLabel.label } - if (labeler != null && labelDef != null) { - val behaviours = when (labelDef.whatToHide) { - LabelScope.Content -> ModBehaviours( - account = ModBehaviour( - contentList = LabelAction.Blur, - contentView = LabelAction.Blur, - ), - profile = ModBehaviour( - contentList = LabelAction.Blur, - contentView = LabelAction.Blur, - ), - content = ModBehaviour( - contentList = LabelAction.Blur, - contentView = LabelAction.Blur, - ), - ) - LabelScope.Media -> BlurAllMedia - LabelScope.None -> ModBehaviours( - NoopBehaviour, - NoopBehaviour, - NoopBehaviour, - ) - } - val interpreted = InterpretedLabelDefinition( - labelDef.identifier, - true, - labelDef.severity, - labelDef.whatToHide, - labelDef.defaultSetting, - persistentListOf(LabelValueDefFlag.Adult), - behaviours, - localizedName = labelDef.localizedName, - localizedDescription = labelDef.localizedDescription, - ) - val cause = LabelCause.Label( - LabelSource.Labeler(labeler), - labels.first { it.value == prefLabel.label }, - interpreted, - LabelTarget.Content, - prefLabel.visibility.toLabelSetting(), - interpreted.behaviours.content, - noOverride = false, - priority = 5, - downgraded = false, - ) - causes.add(cause) - definitionCache[prefLabel.label] = interpreted - } - } - } - } - causes.sortByDescending { it.priority } - causes.fastForEach { cause -> - // TODO: handle stuff from lists and so on - when (cause) { - is LabelCause.Blocking -> { - result.add(ContentHandling( - scope = LabelScope.Content, - action = LabelAction.Blur, - source = LabelDescription.Blocking, - id = "blocking", - icon = LabelIcon.CircleInfo(labelerAvatar = null), - )) - } - is LabelCause.BlockedBy -> { - result.add(ContentHandling( - scope = LabelScope.Content, - action = LabelAction.Blur, - source = LabelDescription.BlockedBy, - id = "blocked-by", - icon = LabelIcon.CircleInfo(labelerAvatar = null), - )) - } - is LabelCause.BlockOther -> { - result.add(ContentHandling( - scope = LabelScope.Content, - action = LabelAction.Blur, - source = LabelDescription.OtherBlocked, - id = "blocked-other", - icon = LabelIcon.CircleInfo(labelerAvatar = null), - )) - } - is LabelCause.Muted -> { - result.add(ContentHandling( - scope = LabelScope.Content, - action = LabelAction.Blur, - source = LabelDescription.YouMuted, - id = "muted", - icon = LabelIcon.CircleInfo(labelerAvatar = null), - )) - } - is LabelCause.MutedWord -> { - result.add( - ContentHandling( - scope = LabelScope.Content, - action = LabelAction.Blur, - source = LabelDescription.MutedWord("Some word"), - id = "muted-word", - icon = LabelIcon.CircleInfo(labelerAvatar = null), - ) + LabelCause.Label( + LabelSource.Labeler(labelerDetails[label.creator.did]!!), + label.toAtProtoLabel(), + localLabelDef, + localLabelDef.whatToHide, + labels[label.value] ?: labelDef.defaultSetting ?: Visibility.IGNORE, + localLabelDef.behaviours.content, + noOverride = !localLabelDef.configurable, + priority = when (localLabelDef.severity) { + Severity.INFORM -> 5 + Severity.ALERT -> 1 + Severity.NONE -> 8 + }, + downgraded = false, + ) to localLabelDef.toContentHandling( + LabelTarget.Content, + avatar = labelerDetails[label.creator.did]?.creator?.avatar ) } - is LabelCause.Label -> { - val handling = cause.labelDef.toContentHandling(cause.target) - result.add(handling) - } - is LabelCause.Hidden -> { - result.add(Hide.toContentHandling(LabelTarget.Content)) - } + }.sortedBy{ it.first.priority } + possibleCauses.forEach { (cause, handling) -> + result.add(handling to cause) } } diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/FeedEvent.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/FeedEvent.kt index 977b088..cf257ee 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/FeedEvent.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/FeedEvent.kt @@ -133,6 +133,7 @@ sealed interface SearchEvent: Event { // Unsure about some of these, maybe events should only be repeatable things? sealed interface LikeEvent: Event +sealed interface ThreadEvent: Event sealed interface PostEvent: Event { data class Reply(val post: BskyPost): PostEvent @@ -147,7 +148,7 @@ sealed interface PostEvent: Event { data class Hide(val uri: AtUri): PostEvent, PrefsEvent, ModerationEvent data class Unhide(val uri: AtUri): PostEvent, PrefsEvent, ModerationEvent - data class LoadThread(val post: AtUri): PostEvent, LoadEvent + data class LoadThread(val post: AtUri): PostEvent, LoadEvent, ThreadEvent data class ReportPost(val subject: StrongRef): PostEvent, ModerationEvent } diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/FeedPresenter.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/FeedPresenter.kt index b81c617..6195d3d 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/FeedPresenter.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/FeedPresenter.kt @@ -5,6 +5,7 @@ import app.bsky.feed.GetFeedQuery import app.bsky.feed.GetListFeedQuery import app.bsky.graph.GetListQuery import app.cash.paging.Pager +import app.cash.paging.cachedIn import com.morpho.app.data.FeedTuner import com.morpho.app.data.MorphoDataSource import com.morpho.app.data.MorphoFeedSource @@ -43,35 +44,42 @@ class FeedPresenter( is FeedEvent.Load -> { switchPager(event.descriptor.getDataSource(agent)) when(event.descriptor) { - is FeedDescriptor.Author -> AuthorFeedUpdate.Feed(event.descriptor.did, event.descriptor.filter, pager.flow) + is FeedDescriptor.Author -> AuthorFeedUpdate.Feed( + event.descriptor.did, event.descriptor.filter, pager.flow.cachedIn(presenterScope)) is FeedDescriptor.FeedGen -> { - val info = agent.api.getList(GetListQuery(event.descriptor.uri, 1)) + val info = agent.api + .getList(GetListQuery(event.descriptor.uri, 1)) .map { it.list.hydrateList() } if(info.isSuccess) { switchPager(info.getOrThrow().getDataSource(agent)) - FeedUpdate.Feed(info.getOrThrow(), pager.flow) + FeedUpdate.Feed(info.getOrThrow(), pager.flow.cachedIn(presenterScope)) } else { FeedUpdate.Error(info.exceptionOrNull()?.message ?: "Failed to load saved feed: ${event.descriptor}, error: $info") } } - FeedDescriptor.Home -> FeedUpdate.Feed(FeedSourceInfo.Home, pager.flow) - is FeedDescriptor.Likes -> AuthorFeedUpdate.Likes(event.descriptor.did, pager.flow) - is FeedDescriptor.List -> FeedUpdate.Error("Internal error: LoadLists should not be sent to this presenter") + FeedDescriptor.Home -> FeedUpdate.Feed( + FeedSourceInfo.Home, pager.flow.cachedIn(presenterScope)) + is FeedDescriptor.Likes -> AuthorFeedUpdate.Likes( + event.descriptor.did, pager.flow.cachedIn(presenterScope)) + is FeedDescriptor.List -> FeedUpdate.Error( + "Internal error: LoadLists should not be sent to this presenter") } } - is FeedEvent.LoadLists -> FeedUpdate.Error("Internal error: LoadLists should not be sent to this presenter") + is FeedEvent.LoadLists -> FeedUpdate.Error( + "Internal error: LoadLists should not be sent to this presenter") is FeedEvent.LoadHydrated -> { switchPager(event.info.getDataSource(agent)) - FeedUpdate.Feed(event.info, pager.flow) + FeedUpdate.Feed(event.info, pager.flow.cachedIn(presenterScope)) } is FeedEvent.LoadSaved -> { val info = event.info.toFeedSourceInfo(agent) if(info.isSuccess) { switchPager(info.getOrThrow().getDataSource(agent)) - FeedUpdate.Feed(info.getOrThrow(), pager.flow) + FeedUpdate.Feed(info.getOrThrow(), pager.flow.cachedIn(presenterScope)) } else { - FeedUpdate.Error(info.exceptionOrNull()?.message ?: "Failed to load saved feed: ${event.info}") + FeedUpdate.Error(info.exceptionOrNull()?.message ?: + "Failed to load saved feed: ${event.info}") } } is FeedEvent.Peek -> FeedUpdate.Peek(event.info, dataSource.updates()) diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/ListPresenter.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/ListPresenter.kt index d723c84..70670b1 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/ListPresenter.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/ListPresenter.kt @@ -3,20 +3,25 @@ package com.morpho.app.model.uidata import app.bsky.feed.GetActorFeedsQuery import app.bsky.graph.GetListsQuery import app.cash.paging.Pager +import app.cash.paging.cachedIn +import com.morpho.app.data.MorphoAgent import com.morpho.app.data.MorphoDataSource import com.morpho.app.model.bluesky.MorphoDataItem import com.morpho.app.model.bluesky.toFeedGenerator import com.morpho.app.model.bluesky.toList import com.morpho.butterfly.AtIdentifier -import com.morpho.butterfly.ButterflyAgent import com.morpho.butterfly.Cursor +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map import org.koin.core.component.KoinComponent import org.koin.core.component.inject abstract class Presenter: KoinComponent { - val agent: ButterflyAgent by inject() + val agent: MorphoAgent by inject() + val presenterScope = CoroutineScope(SupervisorJob() + Dispatchers.Default) abstract var pager: Pager abstract fun produceUpdates(events: Flow): Flow } @@ -33,7 +38,7 @@ class UserListPresenter( override fun produceUpdates(events: Flow): Flow = events.map { event -> when(event) { - is FeedEvent.LoadLists -> AuthorFeedUpdate.Lists(actor, pager.flow) + is FeedEvent.LoadLists -> AuthorFeedUpdate.Lists(actor, pager.flow.cachedIn(presenterScope)) else -> AuthorFeedUpdate.Error("Unknown event type: $event") } } @@ -86,7 +91,7 @@ class UserFeedsPresenter( override fun produceUpdates(events: Flow): Flow = events.map { event -> when(event) { - is FeedEvent.LoadLists -> AuthorFeedUpdate.Feeds(actor, pager.flow) + is FeedEvent.LoadLists -> AuthorFeedUpdate.Feeds(actor, pager.flow.cachedIn(presenterScope)) else -> AuthorFeedUpdate.Error("Unknown event type: $event") } } @@ -105,22 +110,24 @@ class UserFeedsFeedSource( is LoadParams.Prepend -> Cursor.Empty is LoadParams.Refresh -> Cursor.Empty } - return agent.api.getActorFeeds(GetActorFeedsQuery(actor, limit.toLong(), loadCursor.value)).map { response -> - val newCursor = Cursor(response.cursor) - val items = response.feeds - .map { MorphoDataItem.FeedInfo(it.toFeedGenerator()) } - LoadResult.Page( - data = items, - prevKey = when(params) { - is LoadParams.Append -> loadCursor - is LoadParams.Prepend -> Cursor.Empty - is LoadParams.Refresh -> Cursor.Empty - }, - nextKey = newCursor, - ) - }.onFailure { - return LoadResult.Error(it) - }.getOrDefault(LoadResult.Error(Exception("Load failed"))) + return agent.api + .getActorFeeds(GetActorFeedsQuery(actor, limit.toLong(), loadCursor.value)) + .map { response -> + val newCursor = Cursor(response.cursor) + val items = response.feeds + .map { MorphoDataItem.FeedInfo(it.toFeedGenerator()) } + LoadResult.Page( + data = items, + prevKey = when(params) { + is LoadParams.Append -> loadCursor + is LoadParams.Prepend -> Cursor.Empty + is LoadParams.Refresh -> Cursor.Empty + }, + nextKey = newCursor, + ) + }.onFailure { + return LoadResult.Error(it) + }.getOrDefault(LoadResult.Error(Exception("Load failed"))) } catch (e: Exception) { return LoadResult.Error(e) } diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/MorphoData.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/MorphoData.kt index 619d484..0f317ae 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/MorphoData.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/MorphoData.kt @@ -12,7 +12,10 @@ import dev.icerock.moko.parcelize.Parcelable import dev.icerock.moko.parcelize.Parcelize import kotlinx.collections.immutable.toPersistentList import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.* +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.single import kotlinx.datetime.Instant import kotlinx.serialization.Serializable import kotlinx.serialization.json.JsonElement @@ -20,7 +23,7 @@ import kotlinx.serialization.json.JsonObject import kotlin.time.Duration -typealias TunerFunction = (List, FeedTuner) -> List +typealias TunerFunction = (List, FeedTuner) -> List @Parcelize @Immutable @@ -347,13 +350,15 @@ data class MorphoData( val parent = reply.post.reply?.parentPost ?: reply.post.reply?.replyRef?.parent?.uri?.let { if (api != null) { - getPost(it, api).firstOrNull() + null // stubbed out before removing + //getPost(it, api).firstOrNull() } else null } val root = reply.post.reply?.rootPost ?: reply.post.reply?.replyRef?.root?.uri?.let { if (api != null) { - getPost(it, api).firstOrNull() + null // stubbed out before removing + //getPost(it, api).firstOrNull() } else null } replies[index] = MorphoDataItem.Post( diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/SettingsService.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/SettingsService.kt deleted file mode 100644 index f3177bd..0000000 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/SettingsService.kt +++ /dev/null @@ -1,356 +0,0 @@ -package com.morpho.app.model.uidata - -import androidx.compose.ui.util.fastAny -import androidx.compose.ui.util.fastMap -import app.bsky.actor.* -import app.bsky.graph.* -import app.bsky.labeler.GetServicesQuery -import app.bsky.labeler.GetServicesResponseViewUnion -import com.morpho.app.data.AccessibilityPreferences -import com.morpho.app.data.BskyUserPreferences -import com.morpho.app.data.PreferencesRepository -import com.morpho.app.model.bluesky.* -import com.morpho.app.model.uistate.NotificationsFilterState -import com.morpho.butterfly.* -import com.morpho.butterfly.model.RecordType -import com.morpho.butterfly.model.RecordUnion -import kotlinx.collections.immutable.toImmutableList -import kotlinx.collections.immutable.toPersistentList -import kotlinx.coroutines.* -import kotlinx.coroutines.flow.* -import org.koin.core.component.KoinComponent -import org.koin.core.component.inject -import org.lighthousegames.logging.logging -import kotlin.collections.List - - -@OptIn(ExperimentalCoroutinesApi::class) -class SettingsService: KoinComponent { - companion object { - val log = logging() - val serviceScope = CoroutineScope(SupervisorJob() + Dispatchers.Default) - } - - val api: Butterfly by inject() - val prefs: PreferencesRepository by inject() - - - private var _currentUser: MutableStateFlow = MutableStateFlow(null) - private var _currentUserPrefs: MutableStateFlow = MutableStateFlow(null) - - val currentUser = _currentUser.asStateFlow() - var currentUserPrefs = _currentUserPrefs.asStateFlow() - - - val languages: Flow> = currentUserPrefs.transform { - if(it != null) emit(it.preferences.languages) - }.distinctUntilChanged() - - val notificationsFilter: Flow = currentUserPrefs.transform { - if(it?.morphoPrefs?.notificationsFilter != null) emit(it.morphoPrefs.notificationsFilter) - }.distinctUntilChanged() - - val threadViewPrefs: Flow = currentUserPrefs.transform { - if(it?.preferences?.threadViewPrefs != null) emit(it.preferences.threadViewPrefs!!) - }.distinctUntilChanged() - - val feedViewPrefs: Flow> = currentUserPrefs.transform { - if(it?.preferences?.feedViewPrefs != null) emit(it.preferences.feedViewPrefs) - }.distinctUntilChanged() - - val mergeFeeds: Flow = currentUserPrefs.transform { - if(it?.preferences?.mergeFeeds != null) emit(it.preferences.mergeFeeds) - }.distinctUntilChanged() - - val contentLabelPrefs: Flow> = currentUserPrefs.transform { - if(it?.preferences?.contentLabelPrefs != null) emit(it.preferences.contentLabelPrefs) - }.distinctUntilChanged() - - val mutedWords: Flow> = currentUserPrefs.transform { - if(it?.preferences?.mutedWords != null) emit(it.preferences.mutedWords) - }.distinctUntilChanged() - - val mutedUsers: Flow> = currentUserPrefs.transform { - if(it?.preferences?.mutes != null) emit(it.preferences.mutes) - }.distinctUntilChanged() - - val hiddenPosts: Flow> = currentUserPrefs.transform { - if(it?.preferences?.hiddenPosts != null) emit(it.preferences.hiddenPosts) - }.distinctUntilChanged() - - val showAdultContent: Flow = currentUserPrefs.transform { - if(it?.preferences?.adultContent?.enabled != null) emit(it.preferences.adultContent?.enabled ?: false) - }.distinctUntilChanged() - - val savedFeeds: Flow> = currentUserPrefs.transform { preferences -> - if(preferences?.preferences?.savedFeeds != null) emit(preferences.preferences.savedFeeds!!.items.map { it.toUISavedFeed(api) }) - }.distinctUntilChanged() - val pinnedFeeds: Flow> = currentUserPrefs.transform { preferences -> - if(preferences?.preferences?.savedFeeds != null) - emit(preferences.preferences.savedFeeds!!.items.filter { it.pinned }.map { - it.toUISavedFeed(api) - }) - }.distinctUntilChanged() - - val labelers: Flow> = currentUserPrefs.transformLatest { preferences -> - if (preferences?.preferences?.labelers?.isNotEmpty() == true) - emit(preferences.preferences.labelers.toImmutableList() - .let { labelerList -> GetServicesQuery(labelerList) }.let { query -> - api.api.getServices(query) - .map { resp -> - resp.views.map { service -> - when(service) { - is GetServicesResponseViewUnion.LabelerView -> - service.value.toLabelService() - is GetServicesResponseViewUnion.LabelerViewDetailed -> - service.value.toLabelService() - } - } - }.getOrNull() - } ?: emptyList()) - }.distinctUntilChanged() - - - init { - serviceScope.launch { - while(!api.isLoggedIn()) { - delay(100) - } - _currentUser.value = api.atpUser?.let { prefs.getUser(it.id).getOrNull() } - if(_currentUser.value != null && _currentUserPrefs.value == null) { - _currentUserPrefs.value = prefs.getFullPrefsLocal(api.atpUser!!.id).getOrNull() ?: - prefs.getFullPrefsRemote(api.atpUser!!.id).getOrNull() -// currentUserPrefs = prefs.userPrefs(api.atpUser!!.id).stateIn( -// serviceScope, -// SharingStarted.Eagerly, -// prefs.getFullPrefsLocal(api.atpUser!!.id).getOrNull() -// ) - } - } - } - - fun setUser(id: AtIdentifier) = serviceScope.launch { - _currentUser.value = prefs.getUser(id).getOrNull() - api.switchUser(id) - if(_currentUser.value != null) { - currentUserPrefs = prefs.userPrefs(id).stateIn( - serviceScope, - SharingStarted.Eagerly, - prefs.getFullPrefsRemote(id).getOrNull() - ) - } - delay(10000) - if(_currentUserPrefs.value != null) { - currentUserPrefs = _currentUserPrefs.asStateFlow() - } - } - - fun setAccessibilityPrefs(newPrefs: AccessibilityPreferences) = serviceScope.launch { - val updatedPrefs = _currentUserPrefs.updateAndGet { - it?.copy(morphoPrefs = it.morphoPrefs.copy(accessibility = newPrefs)) - } - if (updatedPrefs != null) { - prefs.setPreferencesRemote(currentUser.value!!, updatedPrefs.preferences, updatedPrefs.morphoPrefs) - } - } - - fun setNotificationsPrefs(newPrefs: NotificationsFilterState) = serviceScope.launch { - val updatedPrefs = _currentUserPrefs.updateAndGet { - it?.copy(morphoPrefs = it.morphoPrefs.copy(notificationsFilter = newPrefs)) - } - if (updatedPrefs != null) { - prefs.setPreferencesRemote(currentUser.value!!, updatedPrefs.preferences, updatedPrefs.morphoPrefs) - } - } - - fun setThreadViewPrefs(newPrefs: ThreadViewPref) = serviceScope.launch { - val updatedPrefs = _currentUserPrefs.updateAndGet { - it?.copy(preferences = it.preferences.copy(threadViewPrefs = newPrefs)) - } - if (updatedPrefs != null) { - prefs.setPreferencesRemote(currentUser.value!!, updatedPrefs.preferences, updatedPrefs.morphoPrefs) - } - } - - fun toggleMergeFeeds() = serviceScope.launch { - val updatedPrefs = _currentUserPrefs.updateAndGet { - it?.copy(preferences = it.preferences.copy(mergeFeeds = !it.preferences.mergeFeeds)) - } - if (updatedPrefs != null) { - prefs.setPreferencesRemote(currentUser.value!!, updatedPrefs.preferences, updatedPrefs.morphoPrefs) - } - } - - fun addMutedWord(newWord: MutedWord) = serviceScope.launch { - val sanitizedMuteWord = newWord.copy(value = newWord.value.trim().replace( - "/^#(?!\\ufe0f)/", "" - ).replace("/[\\r\\n\\u00AD\\u2060\\u200D\\u200C\\u200B]+/", "")) - val updatedPrefs = _currentUserPrefs - .updateAndGet { - it?.copy(preferences = it.preferences.copy( - mutedWords = it.preferences.mutedWords + sanitizedMuteWord - )) - } - if (updatedPrefs != null) { - prefs.setPreferencesRemote(currentUser.value!!, updatedPrefs.preferences, updatedPrefs.morphoPrefs) - } - } - - fun removeMutedWord(word: MutedWord) = serviceScope.launch { - val updatedPrefs = _currentUserPrefs - .updateAndGet { preferences -> - preferences?.copy(preferences = preferences.preferences.copy( - mutedWords = preferences.preferences.mutedWords.filterNot { it == word } - )) - } - if (updatedPrefs != null) { - prefs.setPreferencesRemote(currentUser.value!!, updatedPrefs.preferences, updatedPrefs.morphoPrefs) - } - } - - fun addMutedUser(newMute: BasicProfile) = serviceScope.launch { - _currentUserPrefs.update { - it?.copy(preferences = it.preferences.copy(mutes = it.preferences.mutes + newMute)) - } - api.api.muteActor(MuteActorRequest(newMute.did)) - } - - fun muteUserList(newMute: AtUri) = serviceScope.launch { - val list = api.api.getList(GetListQuery(newMute)).getOrNull() ?: return@launch - _currentUserPrefs.update { preferences -> - preferences?.copy(preferences = preferences.preferences.copy( - mutes = preferences.preferences.mutes + list.list.items.map { it.toProfile() as BasicProfile})) - } - api.api.muteActorList(MuteActorListRequest(newMute)) - } - - fun removeMutedUser(oldMute: BasicProfile) = serviceScope.launch { - _currentUserPrefs.update { preferences -> - preferences?.copy(preferences = preferences.preferences.copy( - mutes = preferences.preferences.mutes.filterNot { it.did == oldMute.did } - )) - } - api.api.unmuteActor(UnmuteActorRequest(oldMute.did)) - } - - fun unmuteUserList(oldMute: AtUri) = serviceScope.launch { - val list = api.api.getList(GetListQuery(oldMute)).getOrNull() ?: return@launch - _currentUserPrefs.update { preferences -> - preferences?.copy(preferences = preferences.preferences.copy( - mutes = preferences.preferences.mutes.filterNot { prefMut -> - list.list.items.fastAny { - it.did == prefMut.did - } } - )) - } - api.api.unmuteActorList(UnmuteActorListRequest(oldMute)) - } - - fun hidePost(post: AtUri) = serviceScope.launch { - val updatedPrefs = _currentUserPrefs - .updateAndGet { preferences -> - preferences?.copy(preferences = preferences.preferences.copy( - hiddenPosts = preferences.preferences.hiddenPosts + post - )) - } - if (updatedPrefs != null) { - prefs.setPreferencesRemote(currentUser.value!!, updatedPrefs.preferences, updatedPrefs.morphoPrefs) - } - } - - fun unhidePost(post: AtUri) = serviceScope.launch { - val updatedPrefs = _currentUserPrefs - .updateAndGet { preferences -> - preferences?.copy(preferences = preferences.preferences.copy( - hiddenPosts = preferences.preferences.hiddenPosts.filterNot { it == post } - )) - } - if (updatedPrefs != null) { - prefs.setPreferencesRemote(currentUser.value!!, updatedPrefs.preferences, updatedPrefs.morphoPrefs) - } - } - - fun setAdultContentPref(showAdultContent: Boolean) = serviceScope.launch { - val updatedPrefs = _currentUserPrefs.updateAndGet { prefs -> - prefs?.copy(preferences = prefs.preferences.copy(adultContent = AdultContentPref(showAdultContent))) - } - if (updatedPrefs != null) { - prefs.setPreferencesRemote(currentUser.value!!, updatedPrefs.preferences, updatedPrefs.morphoPrefs) - } - } - - fun addSavedFeed(newFeed: SavedFeed) = serviceScope.launch { - val updatedPrefs = _currentUserPrefs - .updateAndGet { - it?.copy(preferences = it.preferences.copy( - savedFeeds = it.preferences.savedFeeds?.copy( - items = (it.preferences.savedFeeds!!.items + newFeed).toPersistentList()))) - } - - if (updatedPrefs != null) { - prefs.setPreferencesRemote(currentUser.value!!, updatedPrefs.preferences, updatedPrefs.morphoPrefs) - } - } - - fun addSavedFeeds(newFeeds: List) = serviceScope.launch { - val updatedPrefs = _currentUserPrefs - .updateAndGet { - it?.copy(preferences = it.preferences.copy( - savedFeeds = it.preferences.savedFeeds?.copy( - items = (it.preferences.savedFeeds!!.items + newFeeds).toPersistentList()))) - } - - if (updatedPrefs != null) { - prefs.setPreferencesRemote(currentUser.value!!, updatedPrefs.preferences, updatedPrefs.morphoPrefs) - } - } - - - fun updateSavedFeed(newFeed: SavedFeed) = serviceScope.launch { - val updatedPrefs = _currentUserPrefs - .updateAndGet { preferences -> - preferences?.copy(preferences = preferences.preferences.copy( - savedFeeds = preferences.preferences.savedFeeds?.copy( - items = (preferences.preferences.savedFeeds!!.items.fastMap { - if (it.value == newFeed.value) newFeed else it - }).toPersistentList()))) - } - if (updatedPrefs != null) { - prefs.setPreferencesRemote(currentUser.value!!, updatedPrefs.preferences, updatedPrefs.morphoPrefs) - } - } - - fun removeSavedFeed(uri: AtUri) = serviceScope.launch { - val updatedPrefs = _currentUserPrefs - .updateAndGet { preferences -> - preferences?.copy(preferences = preferences.preferences.copy( - savedFeeds = preferences.preferences.savedFeeds?.copy( - items = (preferences.preferences.savedFeeds!!.items.filter { - it.value != uri.toString() - }).toPersistentList()))) - } - if (updatedPrefs != null) { - prefs.setPreferencesRemote(currentUser.value!!, updatedPrefs.preferences, updatedPrefs.morphoPrefs) - } - } - - fun blockUser(user: BasicProfile) = serviceScope.launch { - api.createRecord(RecordUnion.Block(user.did)) - } - - fun unblockUser(user: Did) = serviceScope.launch { - val profile = api.api.getProfile(GetProfileQuery(user)).getOrNull() ?: return@launch - api.deleteRecord(RecordType.Block, profile.viewer?.blocking) - } - - fun followUser(user: Did) = serviceScope.launch { - api.createRecord(RecordUnion.Follow(user)) - } - - fun unfollowUser(user: Did) = serviceScope.launch { - val profile = api.api.getProfile(GetProfileQuery(user)).getOrNull() ?: return@launch - api.deleteRecord(RecordType.Follow, profile.viewer?.following) - } - - -} \ No newline at end of file diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/UIUpdate.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/UIUpdate.kt index eb6a125..014c234 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/UIUpdate.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/UIUpdate.kt @@ -11,6 +11,7 @@ sealed interface UIUpdate { val initialContent: BskyPost, val role: ComposerRole, ): UIUpdate + data object Empty: UIUpdate } sealed interface SearchUpdate: UIUpdate { @@ -36,20 +37,20 @@ sealed interface SearchUpdate: UIUpdate { ): SearchUpdate } -sealed interface FeedUpdate: UIUpdate { - data object Empty: FeedUpdate +sealed interface FeedUpdate: UIUpdate { + data object Empty: FeedUpdate - data class Error(val error: String): FeedUpdate + data class Error(val error: String): FeedUpdate data class Feed( val info: FeedSourceInfo, val feed: Flow>, - ): FeedUpdate + ): FeedUpdate data class Peek( val info: FeedSourceInfo, val post: Flow, - ): FeedUpdate + ): FeedUpdate } sealed interface AuthorFeedUpdate: UIUpdate { diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uistate/TabbedScreenState.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uistate/TabbedScreenState.kt index b3f6061..ddf1626 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uistate/TabbedScreenState.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uistate/TabbedScreenState.kt @@ -6,6 +6,7 @@ package com.morpho.app.model.uistate import androidx.compose.runtime.Immutable import com.morpho.app.model.bluesky.MorphoDataItem import com.morpho.app.model.uidata.ContentCardMapEntry +import com.morpho.app.model.uidata.Event import com.morpho.app.util.StateFlowSerializer import com.morpho.butterfly.AtUri import kotlinx.coroutines.flow.MutableStateFlow @@ -19,8 +20,8 @@ import kotlinx.serialization.Serializable data class TabbedScreenState( override val loadingState: UiLoadingState = UiLoadingState.Idle, @Serializable(with = StateFlowSerializer::class) - val tabs: StateFlow> = - MutableStateFlow>(listOf()).asStateFlow(), + val tabs: StateFlow>> = + MutableStateFlow>>(listOf()).asStateFlow(), val tabStates: List>> = listOf(), ): UiState { @@ -40,8 +41,8 @@ data class TabbedScreenState( data class TabbedProfileScreenState( override val loadingState: UiLoadingState = UiLoadingState.Idle, @Serializable(with = StateFlowSerializer::class) - val tabs: StateFlow> = - MutableStateFlow>(listOf()).asStateFlow(), + val tabs: StateFlow>> = + MutableStateFlow>>(listOf()).asStateFlow(), val tabStates: List>> = listOf(), ): UiState { diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/base/BaseScreenModel.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/base/BaseScreenModel.kt index 0271a09..8bab90d 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/base/BaseScreenModel.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/base/BaseScreenModel.kt @@ -1,47 +1,44 @@ package com.morpho.app.screens.base +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue import cafe.adriel.voyager.core.model.ScreenModel -import cafe.adriel.voyager.core.model.screenModelScope -import com.morpho.app.data.PreferencesRepository -import com.morpho.app.model.bluesky.BskyPost +import com.morpho.app.data.MorphoAgent import com.morpho.app.model.uidata.BskyNotificationService import com.morpho.app.model.uidata.ContentLabelService -import com.morpho.butterfly.AtUri -import com.morpho.butterfly.Butterfly -import com.morpho.butterfly.model.RecordType -import com.morpho.butterfly.model.RecordUnion -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.IO -import kotlinx.coroutines.flow.firstOrNull -import kotlinx.coroutines.launch +import com.morpho.app.model.uidata.Event +import com.morpho.butterfly.Did +import kotlinx.coroutines.channels.BufferOverflow +import kotlinx.coroutines.flow.MutableSharedFlow import org.koin.core.component.KoinComponent import org.koin.core.component.inject import org.lighthousegames.logging.logging open class BaseScreenModel : ScreenModel, KoinComponent { - val api: Butterfly by inject() - val preferences: PreferencesRepository by inject() + val agent: MorphoAgent by inject() val notifService: BskyNotificationService by inject() val labelService: ContentLabelService by inject() + + var userDid: Did? by mutableStateOf(agent.id) + protected set + + val globalEvents = MutableSharedFlow( + extraBufferCapacity = 100, onBufferOverflow = BufferOverflow.DROP_OLDEST) + val isLoggedIn: Boolean - get() = api.isLoggedIn() + get() = agent.isLoggedIn companion object { val log = logging() } - fun createRecord(record: RecordUnion) = screenModelScope.launch(Dispatchers.IO) { - api.createRecord(record) - } + init { - fun deleteRecord(type: RecordType, rkey: AtUri) = screenModelScope.launch(Dispatchers.IO) { - api.deleteRecord(type, rkey) } - suspend fun getPost(uri: AtUri): BskyPost? { - return com.morpho.app.model.uidata.getPost(uri, api).firstOrNull() + suspend fun sendGlobalEvent(event: Event) { + globalEvents.emit(event) } - - } \ No newline at end of file diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/main/MainScreenModel.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/main/MainScreenModel.kt index 52b952b..ccaf3df 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/main/MainScreenModel.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/main/MainScreenModel.kt @@ -1,615 +1,74 @@ package com.morpho.app.screens.main import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue -import app.bsky.actor.GetProfileQuery import app.bsky.actor.SavedFeed -import app.bsky.feed.GetFeedGeneratorsQuery -import app.bsky.feed.GetPostThreadQuery -import app.bsky.feed.GetPostThreadResponseThreadUnion -import app.bsky.feed.GetPostsQuery import cafe.adriel.voyager.core.model.screenModelScope -import cafe.adriel.voyager.core.stack.mutableStateStackOf import com.morpho.app.model.bluesky.* -import com.morpho.app.model.uidata.* -import com.morpho.app.model.uistate.ContentCardState -import com.morpho.app.model.uistate.ContentLoadingState -import com.morpho.app.model.uistate.FeedType +import com.morpho.app.model.uidata.ContentCardMapEntry +import com.morpho.app.model.uidata.FeedEvent +import com.morpho.app.model.uidata.FeedPresenter +import com.morpho.app.model.uidata.FeedUpdate import com.morpho.app.screens.base.BaseScreenModel -import com.morpho.butterfly.AtIdentifier -import com.morpho.butterfly.AtUri -import kotlinx.collections.immutable.ImmutableMap -import kotlinx.collections.immutable.persistentListOf -import kotlinx.collections.immutable.toImmutableMap -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.flow.* +import kotlinx.coroutines.channels.BufferOverflow +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.emitAll +import kotlinx.coroutines.flow.filterIsInstance import kotlinx.coroutines.launch -import kotlinx.coroutines.runBlocking -import org.koin.core.component.inject import org.lighthousegames.logging.logging -@Suppress("UNCHECKED_CAST") -// TODO: Revisit these casts if we can, but they should be safe open class MainScreenModel: BaseScreenModel() { - protected val dataService: BskyDataService by inject() - - - protected val _feedStates = mutableListOf>>() - val feedStates: List>> - get() = _feedStates.toList() - protected val _threadStates = mutableListOf>() - protected val _profileStates = mutableListOf>>() - protected val _profileFeeds = mutableListOf>>() - - - val history = mutableStateStackOf() - val settings: SettingsService by inject() - - protected val pinnedFeeds = MutableStateFlow(emptyList()) - protected val savedFeeds = MutableStateFlow(emptyList()) - - protected val _cursors = mutableMapOf>() - public val cursors: ImmutableMap> - get() = _cursors.toImmutableMap() - - var userId: AtIdentifier? by mutableStateOf(null) + var userProfile: DetailedProfile? by mutableStateOf(null) protected set - var currentUser: DetailedProfile? by mutableStateOf(null) - protected set - - protected var initialized = false - companion object { - val log = logging() - } - - suspend fun init(populateFeeds: Boolean = true) = runBlocking { - if(initialized) return@runBlocking - initialized = true - userId = api.atpUser?.id - - if(userId != null){ - if(preferences.prefs.firstOrNull().isNullOrEmpty()){ - val prefs = userId?.let { - preferences.getPreferences(it, true) - }?.getOrNull() - log.d { "Preferences: $prefs" } - if(prefs != null) { - currentUser = settings.currentUser.value?.getProfile() - } else { - log.e { "Failed to get preferences" } - } - } else if(preferences.getUser(userId!!).isFailure) { - currentUser = userId?.let { GetProfileQuery(it) }?.let { - api.api.getProfile(it).getOrNull()?.toProfile() - } - val prefs = userId?.let { - api.api.getPreferences().getOrNull()?.toPreferences() - } - if(prefs != null && currentUser != null) { - preferences.setPreferences(BskyUser.makeUser(currentUser!!), prefs) - } else { - log.e { "Failed to get preferences" } - } - } else { - log.d { "Preferences already set maybe?" } - } - currentUser = settings.currentUser.value?.getProfile() - - if(currentUser == null) { - currentUser = userId?.let { GetProfileQuery(it) }?.let { - api.api.getProfile(it).getOrNull()?.toProfile() - } - - } - } - if(populateFeeds) { - screenModelScope.launch(Dispatchers.Default) { - settings.pinnedFeeds.collect { feeds -> - log.d { "Pinned Feeds: $feeds" } - pinnedFeeds.value = feeds - } - settings.savedFeeds.collect { feeds -> - log.d { "Saved Feeds: $feeds" } - savedFeeds.value = feeds - } - } - initFeeds() - } - } - - fun getFeedInfo(uri: AtUri) : FeedInfo? { - when { - uri == AtUri.HOME_URI -> return FeedInfo( - uri, - "Home", - "Your home feed" - ) - - else -> { - if(savedFeeds.value.isNotEmpty()) { - savedFeeds.value.firstOrNull { - when (it.type) { - is UIFeedType.Feed -> it.type.uri == uri - is UIFeedType.List -> it.type.uri == uri - is UIFeedType.Timeline -> return FeedInfo( - uri, - "Home", - "Your home feed" - ) - } - }?.let { - if (it.type is UIFeedType.Feed) { - return FeedInfo(uri, it.title, it.description, it.avatar, feed = it.feed) - } else if (it.type is UIFeedType.List) { - return FeedInfo(uri, it.title, it.description, it.avatar, list = it.list) - } - // TODO: Get the feed info from the data service - return null - } - } - - } - } - log.e { "Failed to get feed info for $uri" } - return null - } + val feedSources = mutableStateListOf() + val feedPresenters = mutableMapOf>() + val pinnedFeeds: List + get() = agent.prefs.saved.filter { it.pinned } - protected open suspend fun initFeeds() { - val tlFlow = if(userId != null) { - initTimeline(initAtCursor()).first() - } else null - if (tlFlow == null) { - log.e { "Failed to initialize timeline" } - // Init some default feeds - } + val feedStates = mutableMapOf>() - if (settings.pinnedFeeds.last().isNotEmpty()) { - settings.pinnedFeeds.collectLatest { feeds -> - feeds.forEach { feed -> - val flow = - initFeed(feed.feed!!, initAtCursor(), force = true, start = true).first() - if (flow == null) { - log.e { "Failed to initialize feed: ${feed.title}" } - } - } - } - } else { - // Init some default feeds - api.api.getFeedGenerators(GetFeedGeneratorsQuery( - persistentListOf( - AtUri("at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.generator/whats-hot"), - AtUri("at://did:plc:tenurhgjptubkk5zf5qhi3og/app.bsky.feed.generator/discover"), - AtUri("at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.generator/with-friends"), - AtUri("at://did:plc:tenurhgjptubkk5zf5qhi3og/app.bsky.feed.generator/feed-of-feeds"), - ) - )).onSuccess { resp -> - settings.addSavedFeeds(resp.feeds.map { feed -> - SavedFeed( - feed.uri.atUri, - pinned = true, - type = app.bsky.actor.FeedType.FEED, - value = feed.uri.atUri, + companion object { + val log = logging("MainScreenModel") + } + + init { + if(isLoggedIn) screenModelScope.launch { + userProfile = userDid?.let { agent.getProfile(it).getOrNull()?.toProfile() } + feedSources.add(FeedSourceInfo.Home) + feedSources.addAll(pinnedFeeds.mapNotNull { feed -> feed.toFeedSourceInfo(agent).getOrNull() }) + feedPresenters.putAll(feedSources.map { source -> + source to FeedPresenter(source.feedDescriptor) + }) + feedStates.putAll(feedSources.map { source -> + source to ContentCardMapEntry.Feed( + source.uri, source.displayName?:"", + events = MutableSharedFlow( + extraBufferCapacity = 10, + onBufferOverflow = BufferOverflow.DROP_OLDEST), + updates = MutableStateFlow(FeedUpdate.Empty)) + }) + + + screenModelScope.launch { + feedPresenters.forEach { (source, presenter) -> + val entry = feedStates[source]?: return@forEach + entry.updates.emitAll( + presenter.produceUpdates(entry.events.filterIsInstance()) ) - }) - settings.savedFeeds.collectLatest { feeds -> - feeds.forEach { feed -> - val flow = - initFeed(feed.feed!!, initAtCursor(), force = true, start = false).first() - if (flow == null) { - log.e { "Failed to initialize feed: ${feed.title}" } - } - } } - } - } - } - fun updateFeed(uri: AtUri, newCursor: AtCursor = AtCursor.EMPTY): Boolean { - val cursor = _cursors[uri] ?: return false - if(newCursor.cursor == null && newCursor.scroll > 0) { - val state = _feedStates.firstOrNull { it.value.uri == uri } - val index = _feedStates.indexOfFirst { it.value.uri == uri } - if(state != null) { - val newState = state.value.copy(feed = state.value.feed.copy(cursor = newCursor)) - _feedStates[index] = MutableStateFlow(newState).asStateFlow() - return true - } - val profileFeedState = _profileFeeds.firstOrNull { it.value.uri == uri } - val profileFeedIndex = _profileFeeds.indexOfFirst { it.value.uri == uri } - if(profileFeedState != null) { - val newState = profileFeedState.value.copy(feed = profileFeedState.value.feed.copy(cursor = newCursor)) - _profileFeeds[profileFeedIndex] = MutableStateFlow(newState).asStateFlow() - return true - } } - return cursor.tryEmit(newCursor) - } - - fun updateFeed(feed: FeedGenerator, newCursor: AtCursor = AtCursor.EMPTY): Boolean { - return updateFeed(feed.uri, newCursor) - } - - fun updateFeed(entry: ContentCardMapEntry, newCursor: AtCursor = AtCursor.EMPTY): Boolean { - return updateFeed(entry.uri, newCursor) - } - - open suspend fun peekLatest(entry: ContentCardMapEntry, update: SharedFlow? = null): StateFlow = flow { - val feed = - _feedStates.firstOrNull { it.value.uri == entry.uri } - ?: _profileFeeds.firstOrNull { it.value.uri == entry.uri } - ?: _profileStates.firstOrNull { it.value.uri == entry.uri } - ?: _threadStates.firstOrNull { it.value.uri == entry.uri } - if(feed == null) { emit(null); return@flow } - if(update == null) dataService.peekLatest(feed.value.feed).onEach { emit(it) } - else dataService.peekLatest(feed.value.feed, update).onEach { emit(it) } - }.stateIn(screenModelScope) - - suspend fun loadThread(uri: AtUri): StateFlow? { - val state = _threadStates.firstOrNull { it.value.uri == uri } - if(state != null) return state - val post = - api.api.getPosts(GetPostsQuery(persistentListOf(uri))).map { it.posts.firstOrNull()?.toPost() }.getOrNull() - ?: return null - return loadThread(ContentCardState.PostThread(post, MutableStateFlow(null).asStateFlow(), ContentLoadingState.Loading)) } - @OptIn(ExperimentalCoroutinesApi::class) - suspend fun loadThread(state: ContentCardState.PostThread): StateFlow = flow { - val r = api.api.getPostThread(GetPostThreadQuery(state.uri, 15, 200)).map { response -> - response.thread.let { thread -> - when (thread) { - is GetPostThreadResponseThreadUnion.BlockedPost -> { - ContentCardState.PostThread( - state.post, - MutableStateFlow(null).asStateFlow(), - ContentLoadingState.Error("Blocked post") - ) - } - is GetPostThreadResponseThreadUnion.NotFoundPost -> { - ContentCardState.PostThread( - state.post, - MutableStateFlow(null).asStateFlow(), - ContentLoadingState.Error("Post not found") - ) - } - is GetPostThreadResponseThreadUnion.ThreadViewPost -> { - ContentCardState.PostThread( - thread.value.toPost(), - MutableStateFlow(thread.value.toThread()).asStateFlow(), - ContentLoadingState.Idle - ) - } - } - } - } - emit(r.getOrDefault(state.copy(loadingState = ContentLoadingState.Error("Failed to load thread")))) - }.stateIn(screenModelScope) - private fun indexOf(state: ContentCardState): Int { - return when(state) { - is ContentCardState.FullProfile<*> -> _profileStates.indexOfFirst { it.value.uri == state.uri } - is ContentCardState.PostThread -> _threadStates.indexOfFirst { it.value.uri == state.uri } - is ContentCardState.ProfileTimeline -> _profileFeeds.indexOfFirst { it.value.uri == state.uri } - is ContentCardState.Skyline<*> -> _feedStates.indexOfFirst { it.value.uri == state.uri } - is ContentCardState.UserList -> TODO() - } - } - - suspend fun initFeed( - feed: ContentCardMapEntry.Feed, - force: Boolean = false, - start: Boolean = true, - limit: Long = 100, - ): Flow?> = flow { - val info = getFeedInfo(feed.uri) - if(info == null) { emit(null); return@flow } - val feedService = dataService.dataFlows[feed.uri] - - // Delete the feed if it's already there, initializing from scratch - if(force && feedService != null) dataService.removeFeed(feed.uri) - _cursors[feed.uri] = feed.cursorFlow - if(start) feed.cursorFlow.emit(AtCursor.EMPTY) - - val feedState = _feedStates - .firstOrNull { it.value.uri == feed.uri } - val newFeed = dataService - .feed(info, feed.cursorFlow, limit) - .handleToState(MorphoData(info.name, feed.uri, feed.cursorFlow.replayCache.lastOrNull() ?: AtCursor.EMPTY)) - - if (feedState == null) { - _feedStates.add(newFeed) - emit(newFeed.value) - - } else { - val i = _feedStates.indexOf(feedState) - _feedStates[i] = newFeed.filterNotNull().stateIn(screenModelScope) - emit(newFeed.value) - } - } - - suspend fun initFeed( - feed: FeedGenerator, - cursor: MutableSharedFlow, - force: Boolean = false, - start: Boolean = true, - limit: Long = 100, - ): Flow?> = flow { - val feedService = dataService.dataFlows[feed.uri] - val info = FeedInfo(feed.uri, feed.displayName, feed.description, feed.avatar, feed = feed) - // Delete the feed if it's already there, initializing from scratch - if(force && feedService != null) dataService.removeFeed(feed.uri) - _cursors[feed.uri] = cursor - if(start) cursor.emit(AtCursor.EMPTY) - - val feedState = _feedStates - .firstOrNull { it.value.uri == feed.uri } - val newFeed = dataService - .feed(info, cursor, limit) - .handleToState(MorphoData(feed.displayName, feed.uri, cursor.replayCache.lastOrNull() ?: AtCursor.EMPTY)) - - if (feedState == null) { - _feedStates.add(newFeed) - emit(newFeed.value) - - } else { - val i = _feedStates.indexOf(feedState) - _feedStates[i] = newFeed - emit(newFeed.value) - } - } - - - suspend fun initTimeline( - timeline: ContentCardMapEntry.Home, - force: Boolean = false, - ): Flow?> = flow { - if(timeline.uri != AtUri.HOME_URI) { emit(null); return@flow } - val prefs = if(preferences.prefs.firstOrNull().isNullOrEmpty()) { - log.d { "No preferences found"} - MutableStateFlow(BskyFeedPref()) - } else { - log.d { "Preferences found"} - settings.feedViewPrefs.map { - it["home"] ?: BskyFeedPref() - }.stateIn(screenModelScope, SharingStarted.Lazily, BskyFeedPref()) - } - val feedService = dataService.dataFlows[timeline.uri] - log.d { "Timeline service: $feedService"} - // Delete the feed if it's already there, initializing from scratch - if(force && feedService != null) dataService.removeFeed(timeline.uri) - _cursors[timeline.uri] = timeline.cursorFlow - timeline.cursorFlow.emit(AtCursor.EMPTY) - val feedState = _feedStates - .firstOrNull { it.value.uri == timeline.uri } - log.d { "Timeline state: $feedState"} - val newFeed = dataService - .timeline(timeline.cursorFlow, 100, prefs) - .handleToState(MorphoData(cursor = timeline.cursorFlow.replayCache.lastOrNull() ?: AtCursor.EMPTY)) - - if (feedState == null) { - _feedStates.add(newFeed) - emit(newFeed.value) - } else { - val i = _feedStates.indexOf(feedState) - _feedStates[i] = newFeed - emit(newFeed.value) - } - } - - suspend fun initTimeline( - cursor: MutableSharedFlow, - force: Boolean = false, - ): Flow?> = flow { - val uri = AtUri.HOME_URI - val prefs = settings.feedViewPrefs.map { - it["home"] ?: BskyFeedPref() - }.filterNotNull().stateIn(screenModelScope) - val feedService = dataService.dataFlows[uri] - - // Delete the feed if it's already there, initializing from scratch - if(force && feedService != null) dataService.removeFeed(uri) - _cursors[uri] = cursor - cursor.emit(AtCursor.EMPTY) - val feedState = _feedStates - .firstOrNull { it.value.uri == uri } - val newFeed = dataService - .timeline(cursor, 100, prefs) - .handleToState(MorphoData(cursor = cursor.replayCache.lastOrNull() ?: AtCursor.EMPTY)) - - if (feedState == null) { - _feedStates.add(newFeed) - emit(newFeed.value) - } else { - val i = _feedStates.indexOf(feedState) - _feedStates[i] = newFeed - emit(newFeed.value) - } - } - - suspend fun initProfileTabContent( - feed: ContentCardMapEntry, - force: Boolean = false, - limit: Long = 100, - ): Flow?> = flow { - // Has to be a profile feed - if(!feed.uri.isProfileFeed) { emit(null); return@flow } - val id = feed.uri.id(api) - val feedService = dataService.dataFlows[feed.uri] - - // Delete the feed if it's already there, initializing from scratch - if(force && feedService != null) dataService.removeFeed(feed.uri) - _cursors[feed.uri] = feed.cursorFlow - - val feedState = - if(_profileStates.firstOrNull { it.value.profile.did == id } != null) { - _profileStates.firstOrNull { it.value.profile.did == id } - } else _profileFeeds.firstOrNull { it.value.uri == feed.uri } - val profile = if(_profileStates.firstOrNull{ it.value.profile.did == id } != null) { - _profileStates.firstOrNull{ it.value.profile.did == id }?.value?.profile - } else api.api.getProfile(GetProfileQuery(id)).getOrNull()?.toProfile() - if (profile == null) { emit(null); return@flow } - val newFeed = dataService - .profileTabContent(id, feed.feedType, feed.cursorFlow, limit) - .handleToState(profile, MorphoData(feed.title, feed.uri, feed.cursorFlow.replayCache.lastOrNull() ?: AtCursor.EMPTY)) - if (feedState == null) { - _profileFeeds.add(newFeed) - emit(_profileFeeds.last().value) - } else { - val i = _profileFeeds.indexOf(feedState) - _profileFeeds[i] = newFeed - emit(_profileFeeds[i].value) - } - feed.cursorFlow.emit(AtCursor.EMPTY) - } - - suspend fun initProfileContent( - profile: ContentCardMapEntry.Profile, - force: Boolean = false, - fill: Boolean = false, - ): Flow?> = flow { - val feedService = dataService.dataFlows[profile.uri] - // Delete the feed if it's already there, initializing from scratch - if(force && feedService != null) dataService.removeFeed(profile.uri) - val feedState = _profileStates.firstOrNull { it.value.profile.did == profile.uri.id(api) } - val p = if(_profileStates.firstOrNull{ it.value.profile.did == profile.id } != null) { - _profileStates.firstOrNull{ it.value.profile.did == profile.id }?.value?.profile - } else api.api.getProfile(GetProfileQuery(profile.id)).getOrNull()?.toProfile() - if (p == null) { emit(null); return@flow } - val newProfile = if(fill) { - - val postsCursor: MutableSharedFlow = initAtCursor() - _cursors[AtUri.profilePostsUri(p.did)] = postsCursor - val posts = dataService - .authorFeed(p.did, FeedType.PROFILE_POSTS, postsCursor.asSharedFlow(), 50) - .handleToState(p, MorphoData("Posts", AtUri.profilePostsUri(p.did), - postsCursor.replayCache.lastOrNull() ?: AtCursor.EMPTY)) - - val repliesCursor: MutableSharedFlow = initAtCursor() - _cursors[AtUri.profileRepliesUri(p.did)] = repliesCursor - val replies = dataService - .authorFeed(p.did, FeedType.PROFILE_REPLIES, repliesCursor.asSharedFlow(), 50) - .handleToState(p, MorphoData("Replies", AtUri.profileRepliesUri(p.did), - repliesCursor.replayCache.lastOrNull() ?: AtCursor.EMPTY)) - - val mediaCursor: MutableSharedFlow = initAtCursor() - _cursors[AtUri.profileMediaUri(p.did)] = mediaCursor - val media = dataService - .authorFeed(p.did, FeedType.PROFILE_MEDIA, mediaCursor.asSharedFlow(), 50) - .handleToState(p, MorphoData("Media", AtUri.profileMediaUri(p.did), - mediaCursor.replayCache.lastOrNull() ?: AtCursor.EMPTY)) - - val likesCursor: MutableSharedFlow = initAtCursor() - _cursors[AtUri.profileLikesUri(p.did)] = likesCursor - val likes = dataService - .profileLikes(p.did, likesCursor.asSharedFlow(), 50) - .handleToState(p, MorphoData("Likes", AtUri.profileLikesUri(p.did), - likesCursor.replayCache.lastOrNull() ?: AtCursor.EMPTY)) - - val listsCursor: MutableSharedFlow = initAtCursor() - _cursors[AtUri.profileUserListsUri(p.did)] = listsCursor - val lists = dataService - .profileLists(p.did, listsCursor.asSharedFlow(), 50) - .handleToState(p, MorphoData("Lists", AtUri.profileUserListsUri(p.did), - listsCursor.replayCache.lastOrNull() ?: AtCursor.EMPTY)) - - val feedsCursor: MutableSharedFlow = initAtCursor() - _cursors[AtUri.profileFeedsListUri(p.did)] = feedsCursor - val feeds = dataService - .profileFeedsList(p.did, feedsCursor.asSharedFlow(), 50) - .handleToState(p, MorphoData("Feeds", AtUri.profileFeedsListUri(p.did), - feedsCursor.replayCache.lastOrNull() ?: AtCursor.EMPTY)) - - if (p is BskyLabelService) { - val servicesCursor: MutableSharedFlow = initAtCursor() - _cursors[AtUri.profileModServiceUri(p.did)] = servicesCursor - val services = dataService - .profileServiceView(p.did, servicesCursor.map { Unit } - .shareIn(screenModelScope, SharingStarted.Lazily) - ).handleToState(p, MorphoData("Labels", AtUri.profileModServiceUri(p.did), - servicesCursor.replayCache.lastOrNull() ?: AtCursor.EMPTY)) - servicesCursor.emit(AtCursor.EMPTY) - ContentCardState.FullProfile( - p, - posts.stateIn(screenModelScope), - replies.stateIn(screenModelScope), - media.stateIn(screenModelScope), - likes.stateIn(screenModelScope), - lists.stateIn(screenModelScope), - feeds.stateIn(screenModelScope), - services.stateIn(screenModelScope), - ContentLoadingState.Idle - ) - } else { - postsCursor.emit(AtCursor.EMPTY) - ContentCardState.FullProfile( - p, - posts.stateIn(screenModelScope), - replies.stateIn(screenModelScope), - media.stateIn(screenModelScope), - likes.stateIn(screenModelScope), - lists.stateIn(screenModelScope), - feeds.stateIn(screenModelScope), - loadingState = ContentLoadingState.Idle - ) - } - - } else { - ContentCardState.FullProfile(p, loadingState = ContentLoadingState.Loading) - } - if (feedState == null) { - _profileStates.add(MutableStateFlow(newProfile).asStateFlow() as StateFlow>) - emit(_profileStates.last().value) - } else { - val i = _profileStates.indexOf(feedState) - _profileStates[i] = MutableStateFlow(newProfile).asStateFlow() as StateFlow> - emit(_profileStates[i].value) - } - } - - fun unloadContent(state: ContentCardState): MorphoData? { - - when(state) { - is ContentCardState.Skyline<*> -> { - _feedStates.removeAll { it.value.uri == state.uri } - } - is ContentCardState.PostThread -> { - _threadStates.removeAll { it.value.uri == state.uri } - } - is ContentCardState.FullProfile<*> -> { - _profileStates.removeAll { it.value.uri == state.uri } - unloadContent(state.postsState.value as ContentCardState) - unloadContent(state.postRepliesState.value as ContentCardState) - unloadContent(state.mediaState.value as ContentCardState) - unloadContent(state.likesState.value as ContentCardState) - } - is ContentCardState.ProfileTimeline -> { - _profileFeeds.removeAll { it.value.uri == state.uri } - } - - is ContentCardState.UserList -> TODO() - } - return dataService.removeFeed(state.uri) as MorphoData? - } - - protected open fun unloadContent(entry: ContentCardMapEntry): MorphoData? { - return unloadContent(entry.uri) - } - - protected fun unloadContent(uri: AtUri): MorphoData? { - val state = _feedStates.firstOrNull { it.value.uri == uri } - ?: _threadStates.firstOrNull { it.value.uri == uri } - ?: _profileStates.firstOrNull { it.value.uri == uri } - ?: _profileFeeds.firstOrNull { it.value.uri == uri } - ?: return null - return unloadContent(state.value) as MorphoData? - } } \ No newline at end of file diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/main/tabbed/TabbedMainScreenModel.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/main/tabbed/TabbedMainScreenModel.kt index 230f917..76113f0 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/main/tabbed/TabbedMainScreenModel.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/main/tabbed/TabbedMainScreenModel.kt @@ -1,27 +1,10 @@ package com.morpho.app.screens.main.tabbed -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.setValue -import app.bsky.actor.SavedFeed -import app.bsky.feed.GetFeedGeneratorsQuery import cafe.adriel.voyager.core.model.screenModelScope -import com.morpho.app.model.bluesky.FeedGenerator -import com.morpho.app.model.bluesky.MorphoDataItem -import com.morpho.app.model.bluesky.Profile -import com.morpho.app.model.uidata.AtCursor import com.morpho.app.model.uidata.ContentCardMapEntry -import com.morpho.app.model.uidata.MorphoData -import com.morpho.app.model.uistate.ContentCardState -import com.morpho.app.model.uistate.TabbedScreenState -import com.morpho.app.model.uistate.UiLoadingState +import com.morpho.app.model.uidata.Event import com.morpho.app.screens.main.MainScreenModel import com.morpho.butterfly.AtUri -import kotlinx.collections.immutable.persistentListOf -import kotlinx.collections.immutable.toImmutableList -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch import kotlinx.serialization.Contextual import kotlinx.serialization.Serializable @@ -31,195 +14,22 @@ import org.lighthousegames.logging.logging @Serializable class TabbedMainScreenModel : MainScreenModel() { - @Contextual private val tabs = mutableListOf() + @Contextual private val tabs = mutableListOf>() - private val _tabFlow = MutableStateFlow(tabs.toList()) - @Contextual val tabFlow: StateFlow> - get() = _tabFlow.asStateFlow() - - var uiState: TabbedScreenState by mutableStateOf( - TabbedScreenState( - loadingState = UiLoadingState.Loading, - tabs = tabFlow - )) - private set companion object { - val log = logging() + val log = logging("TabbedMainScreenModel") } - fun uriForTab(index: Int): AtUri { - return tabs[index].uri - } - - fun initTabs() = screenModelScope.launch { - if (initialized) return@launch - init(false) - screenModelScope.launch(Dispatchers.Default) { - settings.pinnedFeeds.collect { feeds -> - MainScreenModel.log.d { "Pinned Feeds: $feeds" } - pinnedFeeds.value = feeds - } - settings.savedFeeds.collect { feeds -> - MainScreenModel.log.d { "Saved Feeds: $feeds" } - savedFeeds.value = feeds - } - } - initialized = true - val home = initHomeTab() - - tabs.clear() - val newFeeds = mutableListOf>>() - if(home.isSuccess) { - val homeState = _feedStates.firstOrNull { - it.value.uri == home.getOrThrow().first.uri - } - if (homeState != null && home.getOrNull()?.second != null) { - tabs.add(home.getOrThrow().first) - _tabFlow.value = tabs.toImmutableList() - newFeeds.add(homeState as StateFlow>) - //uiState = uiState.copy(loadingState = UiLoadingState.Idle, tabs = tabFlow, tabStates = newFeeds.toImmutableList()) - } else { - log.e { "Failed to initialize home tab state" } - log.d { - "Home tab: ${home.getOrNull()?.first}\n" + - "Home state: ${homeState?.value}" - } - } - } - while(pinnedFeeds.value.isEmpty()) { - delay(10) - } + init { + if(isLoggedIn) screenModelScope.launch { - if (pinnedFeeds.value.isNotEmpty()) { - log.d { "Pinned feeds: ${pinnedFeeds.value}" } - pinnedFeeds.value.forEach { feed -> - if(feed.feed == null) return@forEach - val result = initFeedTab(feed.feed) - if (result.isFailure) { - MainScreenModel.log.e { "Failed to initialize feed: ${feed.title}" } - } else { - feedStates.firstOrNull { - it.value.uri == result.getOrNull()?.first?.uri - }?.let { state -> - tabs.add(result.getOrNull()?.first!!) - newFeeds.add(state as StateFlow>) - } - } - } - } else if(false) { // Temporarily disabled - // Init some default feeds - api.api.getFeedGenerators(GetFeedGeneratorsQuery( - persistentListOf( - AtUri("at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.generator/whats-hot"), - AtUri("at://did:plc:tenurhgjptubkk5zf5qhi3og/app.bsky.feed.generator/discover"), - AtUri("at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.generator/with-friends"), - AtUri("at://did:plc:tenurhgjptubkk5zf5qhi3og/app.bsky.feed.generator/feed-of-feeds"), - ) - )).onSuccess { resp -> - resp.feeds.forEach { feed -> - settings.addSavedFeed( - SavedFeed( - feed.uri.atUri, - pinned = true, - type = app.bsky.actor.FeedType.FEED, - value = feed.uri.atUri - )) - - } - pinnedFeeds.value.associateBy { pinnedFeeds.value.indexOf(it) }.mapValues { feedGen -> - val result = initFeedTab(feedGen.value.feed!!) - if (result.isFailure) { - MainScreenModel.log.e { "Failed to initialize feed: ${feedGen.value.title}" } - } else { - feedStates.firstOrNull { - it.value.uri == result.getOrNull()?.first?.uri - }?.let { state -> - tabs.add(result.getOrNull()?.first!!) - newFeeds.add(state as StateFlow>) - } - } - } - } - } else { - log.d { "Saved Feeds: ${savedFeeds.value}" } - log.d { - "Prefs ${preferences.prefs.firstOrNull()}" - } } - _tabFlow.value = tabs.toImmutableList() - uiState = uiState.copy(loadingState = UiLoadingState.Idle, tabs = tabFlow, tabStates = newFeeds.toImmutableList()) } - fun refreshTab(index: Int, cursor: AtCursor = AtCursor.EMPTY) :Boolean { - return if(index < 0 || index > tabs.lastIndex) false - else updateFeed(tabs[index], cursor) - } - - - suspend fun initHomeTab(): - Result>> { - val home = ContentCardMapEntry.Home - _cursors[home.uri] = home.cursorFlow - val f = initTimeline(home, force = false).first() - - return if(f != null) { - Result.success(Pair(home, f)) - } else { - val ul = unloadContent(home) - log.e { "Failed to initialize home tab" } - log.v { "Deleted Feed: ${ul?.items}" } - Result.failure(Exception("Failed to initialize home tab")) - } - } - - suspend fun initFeedTab( - feed:FeedGenerator - ): Result>> { - val title = feed.displayName - val tab = ContentCardMapEntry.Feed(feed.uri, title, avatar = feed.avatar) - _cursors[tab.uri] = tab.cursorFlow - val f = initFeed(feed, tab.cursorFlow, force = true, start = false, limit = 50).firstOrNull() - return if(f != null) { - Result.success(Pair(tab, f)) - } else { - val ul = unloadContent(tab) - log.e { "Failed to initialize feed tab $title" } - log.v { "Deleted Feed: ${ul?.items}" } - Result.failure(Exception("Failed to initialize feed tab $title")) - } - } - - suspend fun initProfileTab(profile: Profile): Result>> { - val title = profile.displayName ?: profile.handle.handle - val tab = ContentCardMapEntry.Profile(profile.did, AtUri.profileUri(profile.did), title) - _cursors[tab.uri] = tab.cursorFlow - val f = initProfileContent(tab, force = true, fill = true).first() - return if(f != null) { - Result.success(Pair(tab, f)) - } else { - val ul = unloadContent(tab) - log.e { "Failed to initialize profile tab $title" } - log.v { "Deleted Feed: ${ul?.items}" } - Result.failure(Exception("Failed to initialize profile tab $title")) - } - } - - override fun unloadContent(entry: ContentCardMapEntry): MorphoData? { - val maybeTab = uiState.tabMap[entry.uri] - return if(maybeTab == null) { - history.popUntil { it == entry } - unloadContent(entry.uri) - } else { - unloadContent(maybeTab) - } + fun uriForTab(index: Int): AtUri { + return tabs[index].uri } - fun unloadTab(index: Int): MorphoData? { - if(index < 0 || index > tabs.lastIndex) return null - val uri = tabs[index].uri - val state = uiState.tabMap[uri] ?: return null - return unloadContent(state) - } } \ No newline at end of file diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/common/SkylineFragment.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/common/SkylineFragment.kt index e476941..291822c 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/common/SkylineFragment.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/common/SkylineFragment.kt @@ -1,10 +1,10 @@ package com.morpho.app.ui.common import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.border +import androidx.compose.foundation.gestures.snapping.rememberSnapFlingBehavior import androidx.compose.foundation.layout.* -import androidx.compose.foundation.lazy.LazyListState -import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material.icons.Icons @@ -18,15 +18,20 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp import androidx.constraintlayout.compose.ConstraintLayout +import androidx.paging.PagingData +import app.cash.paging.LoadStateError +import app.cash.paging.LoadStateLoading +import app.cash.paging.LoadStateNotLoading +import app.cash.paging.compose.collectAsLazyPagingItems +import app.cash.paging.compose.itemKey import com.atproto.repo.StrongRef import com.morpho.app.model.bluesky.BskyPost import com.morpho.app.model.bluesky.MorphoDataItem -import com.morpho.app.model.uidata.AtCursor import com.morpho.app.model.uidata.ContentHandling -import com.morpho.app.model.uistate.ContentCardState -import com.morpho.app.model.uistate.ContentLoadingState +import com.morpho.app.model.uidata.FeedUpdate import com.morpho.app.ui.elements.MenuOptions import com.morpho.app.ui.elements.WrappedLazyColumn +import com.morpho.app.ui.post.PlaceholderSkylineItem import com.morpho.app.ui.post.PostFragment import com.morpho.butterfly.AtIdentifier import com.morpho.butterfly.AtUri @@ -37,15 +42,13 @@ import kotlinx.coroutines.launch typealias OnPostClicked = (AtUri) -> Unit -@OptIn(ExperimentalMaterialApi::class) +@OptIn(ExperimentalMaterialApi::class, ExperimentalFoundationApi::class) @Composable -fun SkylineFragment ( - content: StateFlow>, +fun SkylineFragment ( modifier: Modifier = Modifier, onItemClicked: OnPostClicked, onProfileClicked: (AtIdentifier) -> Unit = {}, onPostButtonClicked: () -> Unit = {}, - refresh: (AtCursor) -> Unit = { }, onReplyClicked: (BskyPost) -> Unit = { }, onRepostClicked: (BskyPost) -> Unit = { }, onLikeClicked: (StrongRef) -> Unit = { }, @@ -54,35 +57,31 @@ fun SkylineFragment ( getContentHandling: (BskyPost) -> List = { listOf() }, contentPadding: PaddingValues = PaddingValues(0.dp), isProfileFeed: Boolean = false, - listState: LazyListState = rememberLazyListState( - initialFirstVisibleItemIndex = content.value.feed.cursor.scroll - ), debuggable: Boolean = false, + feedUpdate: StateFlow>, ) { - val currentRefresh by rememberUpdatedState(refresh) - + val scope = rememberCoroutineScope() - val state = content.collectAsState() - val loading = state.value.loadingState - val cursor by rememberUpdatedState(state.value.feed.cursor) + val listState = rememberLazyListState() + val state = feedUpdate.collectAsState() + val pager = remember { when(state.value) { + is FeedUpdate.Feed -> (state.value as FeedUpdate.Feed).feed + is FeedUpdate.Peek -> null + else -> null + } } + val data = pager?.collectAsLazyPagingItems() + val pagerState = pager?.collectAsState( + if(data != null) PagingData.from(data.itemSnapshotList.items) else PagingData.empty() + ) - val scope = rememberCoroutineScope() - var refreshing by remember { mutableStateOf(false) } - val data = remember(loading, state, cursor, refreshing) { - state.value.feed - } val scrolledDownSome by remember { derivedStateOf { listState.firstVisibleItemIndex > 5 } } - val scrollCursor by remember { derivedStateOf { - listState.firstVisibleItemIndex - } } - val scrolledDownLots by remember { derivedStateOf { listState.firstVisibleItemIndex > 20 @@ -90,35 +89,16 @@ fun SkylineFragment ( } - fun refreshPull() = scope.launch { - refreshing = true - launch { currentRefresh(AtCursor.EMPTY) } - .invokeOnCompletion { refreshing = false } - - } - - - - LaunchedEffect( - data.items.isNotEmpty() && - loading == ContentLoadingState.Idle && - !listState.canScrollForward && - !refreshing && - scrolledDownSome - ) { - currentRefresh(cursor.copy(scroll = scrollCursor)) - } - + fun refreshPull() = data?.refresh() + val refreshing by remember { mutableStateOf(false) } val refreshState = rememberPullRefreshState(refreshing, ::refreshPull) - ConstraintLayout( modifier = if(isProfileFeed) { Modifier - .fillMaxWidth() - .systemBarsPadding() + .fillMaxSize().systemBarsPadding() } else { Modifier @@ -139,9 +119,15 @@ fun SkylineFragment ( top.linkTo(parent.top) bottom.linkTo(parent.bottom) }, - //flingBehavior = rememberSnapFlingBehavior(lazyListState = listState), + flingBehavior = rememberSnapFlingBehavior(lazyListState = listState), contentPadding = if(isProfileFeed) { - contentPadding + //contentPadding + PaddingValues( + bottom = contentPadding.calculateBottomPadding(), +// top = WindowInsets.safeContent.only(WindowInsetsSides.Top).asPaddingValues() +// .calculateTopPadding() + top = contentPadding.calculateTopPadding() + ) } else { PaddingValues( bottom = contentPadding.calculateBottomPadding(), @@ -198,82 +184,80 @@ fun SkylineFragment ( } } } - - items( - data.items, -// key = { -// when(it) { -// is MorphoDataItem.Post -> "post_${it.post.uri}_${it.post.hashCode()}_${it.post.cid}".encodeBase64() -// is MorphoDataItem.Thread -> "thread_${it.thread.post.uri}_${it.thread.hashCode()}_${it.thread.post.cid}".encodeBase64() -// else -> "${it.hashCode()}".encodeBase64() -// } -// }, - contentType = { - when(it) { - is MorphoDataItem.Post -> MorphoDataItem.Post::class - is MorphoDataItem.Thread -> MorphoDataItem.Thread::class - else -> {} - } + when(val loadState = data?.loadState?.refresh) { + is LoadStateError -> { + item { Text("Error: ${loadState.error}") } + item { Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.Center) { + TextButton(onClick = { data.retry() }) { + Text("Retry") + } } } } - ) {item -> - when(item) { - is MorphoDataItem.Thread -> { - SkylineThreadFragment( - thread = item.thread, - modifier = if(debuggable) Modifier.border(1.dp, Color.White) else Modifier - .fillMaxWidth() - //.padding(horizontal = 4.dp), - .padding(vertical = 2.dp, horizontal = 4.dp), - onItemClicked = onItemClicked, - onProfileClicked = onProfileClicked, - onUnClicked = onUnClicked, - onRepostClicked = onRepostClicked, - onReplyClicked = onReplyClicked, - onMenuClicked = onMenuClicked, - onLikeClicked = onLikeClicked, - getContentHandling = getContentHandling, - debuggable = debuggable, - ) - } - is MorphoDataItem.Post -> { - PostFragment( - modifier = if(debuggable) Modifier.border(1.dp, Color.Blue) else Modifier - .fillMaxWidth() - //.padding(horizontal = 4.dp), - .padding(vertical = 2.dp, horizontal = 4.dp), - post = item.post, - onItemClicked = onItemClicked, - onProfileClicked = onProfileClicked, - elevate = true, - onUnClicked = onUnClicked, - onRepostClicked = onRepostClicked, - onReplyClicked = onReplyClicked, - onMenuClicked = onMenuClicked, - onLikeClicked = onLikeClicked, - getContentHandling = getContentHandling, - ) - } - else -> {} - } - } - item { - Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.Center) { - TextButton( - onClick = { currentRefresh(cursor.copy(scroll = data.items.size - 1)) }, - modifier = Modifier.padding(6.dp) - ) { - Text("Load more...") + is LoadStateNotLoading -> { + items( + data.itemCount, + key = data.itemKey { it.key } + ) { index -> + when(val item = data[index]) { + is MorphoDataItem.Thread -> { + SkylineThreadFragment( + thread = item.thread, + modifier = if(debuggable) Modifier.border(1.dp, Color.White) else Modifier + .fillMaxWidth() + //.padding(horizontal = 4.dp), + .padding(vertical = 2.dp, horizontal = 4.dp), + onItemClicked = onItemClicked, + onProfileClicked = onProfileClicked, + onUnClicked = onUnClicked, + onRepostClicked = onRepostClicked, + onReplyClicked = onReplyClicked, + onMenuClicked = onMenuClicked, + onLikeClicked = onLikeClicked, + getContentHandling = getContentHandling, + debuggable = debuggable, + ) + } + is MorphoDataItem.Post -> { + PostFragment( + modifier = if(debuggable) Modifier.border(1.dp, Color.Blue) else Modifier + .fillMaxWidth() + //.padding(horizontal = 4.dp), + .padding(vertical = 2.dp, horizontal = 4.dp), + post = item.post, + onItemClicked = onItemClicked, + onProfileClicked = onProfileClicked, + elevate = true, + onUnClicked = onUnClicked, + onRepostClicked = onRepostClicked, + onReplyClicked = onReplyClicked, + onMenuClicked = onMenuClicked, + onLikeClicked = onLikeClicked, + getContentHandling = getContentHandling, + ) + } + + else -> { + PlaceholderSkylineItem( + modifier = if(debuggable) Modifier.border(1.dp, Color.Black) else Modifier + .fillMaxWidth() + //.padding(horizontal = 4.dp), + .padding(vertical = 2.dp, horizontal = 4.dp), + elevate = true, + ) + } + } } } + else -> { item { LoadingCircle() } } } + if (data?.loadState?.append == LoadStateLoading) item { LoadingCircle() } } if (scrolledDownSome) { OutlinedIconButton( onClick = { scope.launch { - refreshPull() + //refreshPull() if (scrolledDownLots) { listState.scrollToItem(0) } else { diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/common/TabbedSkylineFragment.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/common/TabbedSkylineFragment.kt index 3172837..6d41610 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/common/TabbedSkylineFragment.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/common/TabbedSkylineFragment.kt @@ -1,14 +1,11 @@ package com.morpho.app.ui.common import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.lazy.LazyListState -import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.* import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.unit.dp -import cafe.adriel.voyager.core.model.screenModelScope import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.currentOrThrow import cafe.adriel.voyager.navigator.tab.TabNavigator @@ -16,14 +13,13 @@ import com.atproto.repo.StrongRef import com.morpho.app.model.bluesky.BskyPost import com.morpho.app.model.bluesky.DraftPost import com.morpho.app.model.bluesky.MorphoDataItem -import com.morpho.app.model.uidata.AtCursor -import com.morpho.app.model.uistate.ContentCardState +import com.morpho.app.model.uidata.FeedUpdate +import com.morpho.app.model.uidata.UIUpdate import com.morpho.app.screens.base.tabbed.ProfileTab import com.morpho.app.screens.base.tabbed.ThreadTab -import com.morpho.app.screens.main.MainScreenModel import com.morpho.app.ui.elements.doMenuOperation import com.morpho.app.util.ClipboardManager -import com.morpho.butterfly.model.RecordUnion +import com.morpho.butterfly.ButterflyAgent import io.ktor.util.reflect.instanceOf import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.IO @@ -33,19 +29,17 @@ import org.koin.compose.getKoin @OptIn(ExperimentalMaterial3Api::class) @Composable -fun > TabbedSkylineFragment( - sm: T, - state: StateFlow?, +fun TabbedSkylineFragment( paddingValues: PaddingValues = PaddingValues(0.dp), - refresh: (AtCursor) -> Unit = {}, isProfileFeed: Boolean = false, - listState: LazyListState = rememberLazyListState( - initialFirstVisibleItemIndex = state?.value?.feed?.cursor?.scroll ?: 0 - ), + feedUpdate: StateFlow, + uiState: StateFlow, ) { + val agent = getKoin().get() val navigator = if (LocalNavigator.current?.parent?.instanceOf(TabNavigator::class) == true) { LocalNavigator.currentOrThrow } else LocalNavigator.currentOrThrow.parent!! + val scope = rememberCoroutineScope() var repostClicked by remember { mutableStateOf(false) } var initialContent: BskyPost? by remember { mutableStateOf(null) } var showComposer by remember { mutableStateOf(false) } @@ -76,30 +70,30 @@ fun > TabbedSkylin showComposer = true } } - val content = state?.collectAsState() val clipboard = getKoin().get() - if(content?.value != null) { - - SkylineFragment( - content = state, - onProfileClicked = { actor -> navigator.push(ProfileTab(actor)) }, - onItemClicked = { uri -> navigator.push(ThreadTab(uri)) }, - refresh = { cursor -> refresh(cursor)}, - onUnClicked = { type, rkey -> sm.deleteRecord(type, rkey) }, - onRepostClicked = { onRepostClicked(it) }, - onMenuClicked = { option, post -> - doMenuOperation(option, post, - clipboardManager = clipboard, - uriHandler = uriHandler - ) }, - onReplyClicked = { onReplyClicked(it) }, - onLikeClicked = { uri -> sm.createRecord(RecordUnion.Like(uri)) }, - onPostButtonClicked = { onPostButtonClicked() }, - getContentHandling = { post -> sm.labelService.getContentHandlingForPost(post)}, - contentPadding = paddingValues, - isProfileFeed = isProfileFeed, - listState = listState, - ) + if(uiState.value !is UIUpdate.Empty) { + if(feedUpdate.value is FeedUpdate<*>) { + SkylineFragment( + onProfileClicked = { actor -> navigator.push(ProfileTab(actor)) }, + onItemClicked = { uri -> navigator.push(ThreadTab(uri)) }, + onUnClicked = { type, rkey -> agent.deleteRecord(type, rkey) }, + onRepostClicked = { onRepostClicked(it) }, + onMenuClicked = { option, post -> + doMenuOperation(option, post, + clipboardManager = clipboard, + uriHandler = uriHandler + ) }, + onReplyClicked = { onReplyClicked(it) }, + onLikeClicked = { ref -> agent.like(ref) }, + onPostButtonClicked = { onPostButtonClicked() }, + getContentHandling = { post -> listOf() }, + contentPadding = paddingValues, + isProfileFeed = isProfileFeed, + feedUpdate = feedUpdate as StateFlow>, + ) + } else { + LoadingCircle() + } if(repostClicked) { RepostQueryDialog( onDismissRequest = { @@ -108,11 +102,7 @@ fun > TabbedSkylin }, onRepost = { repostClicked = false - initialContent?.let { post -> - RecordUnion.Repost( - StrongRef(post.uri, post.cid) - ) - }?.let { sm.api.createRecord(it) } + initialContent?.let { agent.repost(StrongRef(it.uri, it.cid)) } }, onQuotePost = { composerRole = ComposerRole.QuotePost @@ -134,18 +124,13 @@ fun > TabbedSkylin draft = DraftPost() }, onSend = { finishedDraft -> - sm.screenModelScope.launch(Dispatchers.IO) { - val post = finishedDraft.createPost(sm.api) - sm.api.createRecord(RecordUnion.MakePost(post)) - } + scope.launch(Dispatchers.IO) { agent.post(finishedDraft.createPost(agent)) } showComposer = false }, onUpdate = { draft = it } ) } - } else { - LoadingCircle() - } + } else LoadingCircle() } diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/elements/ContentHider.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/elements/ContentHider.kt index 8d089eb..4253523 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/elements/ContentHider.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/elements/ContentHider.kt @@ -27,21 +27,22 @@ import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.unit.IntSize import androidx.compose.ui.unit.dp import androidx.compose.ui.util.fastFilter -import com.morpho.app.model.bluesky.LabelAction -import com.morpho.app.model.bluesky.LabelScope -import com.morpho.app.model.uidata.ContentHandling +import com.atproto.label.Blurs +import com.morpho.butterfly.ContentHandling +import com.morpho.butterfly.LabelAction @Composable public fun ContentHider( reasons: List = listOf(), - scope: LabelScope, + scope: Blurs, modifier: Modifier = Modifier, content: @Composable () -> Unit, ) { val scopedBehaviours = reasons.filter { it.scope == scope } - val toHide = scopedBehaviours.fastFilter { it.action == LabelAction.Blur || it.action == LabelAction.Alert } + val toHide = scopedBehaviours + .fastFilter { it.action == LabelAction.Blur || it.action == LabelAction.Alert } var hideContent by remember { mutableStateOf( toHide.isNotEmpty() @@ -59,11 +60,23 @@ public fun ContentHider( .clickable { hideContent = !hideContent }.fillMaxWidth().padding(12.dp) , horizontalArrangement = Arrangement.SpaceBetween) { Row(verticalAlignment = Alignment.CenterVertically) { - Icon( - imageVector = reason?.icon?.icon?: Icons.Default.Info, - contentDescription = reason?.source?.description, - tint = MaterialTheme.colorScheme.onSurfaceVariant - ) + + if(reason?.icon?.labelerAvatar!= null) { + OutlinedAvatar( + url = reason.icon.labelerAvatar!!, + contentDescription = reason.source.description, + modifier = Modifier.size(20.dp), + avatarShape = AvatarShape.Circle, + outlineColor = MaterialTheme.colorScheme.onSurfaceVariant, + ) + + } else { + Icon( + imageVector = reason?.icon?.icon?: Icons.Default.Info, + contentDescription = reason?.source?.description, + tint = MaterialTheme.colorScheme.onSurfaceVariant + ) + } Spacer(modifier = Modifier.width(8.dp)) Text(reason?.source?.name ?: "", style = MaterialTheme.typography.labelLarge, diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/post/NotFoundPostFragment.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/post/NotFoundPostFragment.kt index 3e9ae80..71f7039 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/post/NotFoundPostFragment.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/post/NotFoundPostFragment.kt @@ -1,18 +1,24 @@ package com.morpho.app.ui.post -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.* import androidx.compose.foundation.text.selection.SelectionContainer import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.material3.Text +import androidx.compose.material3.surfaceColorAtElevation import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.text.withStyle import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.max +import com.morpho.app.ui.elements.AvatarShape +import com.morpho.app.ui.elements.OutlinedAvatar import com.morpho.app.ui.elements.WrappedColumn import com.morpho.butterfly.AtUri import morpho.app.ui.utils.indentLevel @@ -50,4 +56,143 @@ fun NotFoundPostFragment( } } } +} + +@Composable +fun PlaceholderSkylineItem( + modifier: Modifier = Modifier, + role: PostFragmentRole = PostFragmentRole.Solo, + indentLevel: Int = 0, + elevate: Boolean = false, +) { + val padding = remember { when(role) { + PostFragmentRole.Solo -> if(indentLevel == 0) Modifier.padding(2.dp) else Modifier + PostFragmentRole.PrimaryThreadRoot -> Modifier.padding(2.dp) + PostFragmentRole.ThreadBranchStart -> Modifier.padding(start = 0.dp, top = 0.dp, end = 2.dp, bottom = 2.dp) + PostFragmentRole.ThreadBranchMiddle -> Modifier.padding(start = 0.dp, top = 0.dp, end = 2.dp, bottom = 2.dp) + PostFragmentRole.ThreadBranchEnd -> Modifier.padding(start = 0.dp, top = 0.dp, end = 2.dp, bottom = 2.dp) + PostFragmentRole.ThreadRootUnfocused -> Modifier.padding(2.dp) + PostFragmentRole.ThreadEnd -> Modifier.padding(start = 2.dp, top = 0.dp, end = 2.dp, bottom = 2.dp) + }} + WrappedColumn(modifier = modifier.then(padding.fillMaxWidth())) { + val indent = remember { when(role) { + PostFragmentRole.Solo -> indentLevel.toFloat() + PostFragmentRole.PrimaryThreadRoot -> indentLevel.toFloat() + PostFragmentRole.ThreadBranchStart -> 0.0f//indentLevel.toFloat() + PostFragmentRole.ThreadBranchMiddle -> 0.0f//indentLevel.toFloat()-1 + PostFragmentRole.ThreadBranchEnd -> 0.0f//indentLevel.toFloat()-1 + PostFragmentRole.ThreadRootUnfocused -> indentLevel.toFloat() + PostFragmentRole.ThreadEnd -> 0.0f + }} + + val bgColor = if (role == PostFragmentRole.PrimaryThreadRoot) { + MaterialTheme.colorScheme.background + } else { + MaterialTheme.colorScheme.surfaceColorAtElevation(if (elevate ) 2.dp else + if (indentLevel > 0) (indentLevel*2).dp else 0.dp) + } + + Surface ( + shadowElevation = if (elevate || indentLevel > 0) 2.dp else 0.dp, + tonalElevation = if (elevate && role != PostFragmentRole.ThreadEnd) 2.dp + else if (indentLevel > 0) (indentLevel*2).dp else 0.dp, + shape = MaterialTheme.shapes.small, + //color = bgColor, + modifier = modifier + .fillMaxWidth(indentLevel(indent)) + .align(Alignment.End) + + ) { + Row( + modifier = Modifier.padding(end = 6.dp) + .fillMaxWidth()//.fillMaxWidth(indentLevel(indent)) + ) { + + if (indent < 2) { + OutlinedAvatar( + url = "", + contentDescription = "Placeholder avatar", + size = 45.dp, + outlineColor = MaterialTheme.colorScheme.background, + avatarShape = AvatarShape.Corner, + modifier = Modifier.padding(end = 2.dp) + ) + } + + Column( + Modifier + .padding(top = 4.dp, start = 2.dp, end = 6.dp) + .fillMaxWidth()//.fillMaxWidth(indentLevel(indent)) + ) { + + Row( + modifier = Modifier.padding(top = 2.dp, start = 2.dp, end = 4.dp), + horizontalArrangement = Arrangement.End + ) { + if (indent >= 2) { + OutlinedAvatar( + url = "", + contentDescription = "Placeholder avatar", + size = 30.dp, + avatarShape = AvatarShape.Rounded, + outlineColor = MaterialTheme.colorScheme.background, + ) + } + Text( + text = buildAnnotatedString { + withStyle( + style = SpanStyle( + color = MaterialTheme.colorScheme.onSurface, + fontSize = MaterialTheme.typography.labelLarge.fontSize, + fontWeight = FontWeight.Medium + ) + ) { + " " + } + withStyle( + style = SpanStyle( + color = MaterialTheme.colorScheme.onSurfaceVariant, + fontSize = MaterialTheme.typography.labelLarge.fontSize.times( + 0.8f + ) + ) + ) { + append("@ ") + } + + }, + maxLines = 1, + style = MaterialTheme.typography.labelLarge, + overflow = TextOverflow.Ellipsis, + modifier = Modifier + .wrapContentWidth(Alignment.Start) + .weight(10.0F) + .alignByBaseline() + ) + + Spacer(modifier = Modifier.width(1.dp).weight(0.1F)) + Text( + text = " ", + color = MaterialTheme.colorScheme.onSurfaceVariant, + style = MaterialTheme.typography.labelLarge, + fontSize = MaterialTheme.typography.labelLarge.fontSize.div(1.2F), + modifier = Modifier + .wrapContentWidth(Alignment.End) + //.weight(3.0F) + .alignByBaseline(), + maxLines = 1, + overflow = TextOverflow.Visible, + softWrap = false, + ) + } + + Spacer(Modifier.height(100.dp)) + + DummyPostActions() + } + } + + } + } + } } \ No newline at end of file diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/post/PostActions.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/post/PostActions.kt index 64a3e0b..92591a8 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/post/PostActions.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/post/PostActions.kt @@ -87,6 +87,55 @@ fun PostActions( } } +@OptIn(ExperimentalLayoutApi::class) +@Composable +fun DummyPostActions( + modifier: Modifier = Modifier, + showMenu: Boolean = true, +) { + var menuExpanded by remember { mutableStateOf(false) } + Row( + horizontalArrangement = Arrangement.SpaceEvenly + ) { + PostAction( + parameter = 0, + iconNormal = Icons.Outlined.ChatBubbleOutline, + contentDescription = "Reply ", + + onUnClicked = { }, + ) + PostAction( + parameter = 0, + iconNormal = Icons.Outlined.Repeat, + contentDescription = "Repost ", + active = false + ) + PostAction( + parameter = 0, + iconNormal = Icons.Outlined.FavoriteBorder, + iconActive = Icons.Default.Favorite, + contentDescription = "Like ", + activeColor = Color(0xFFEC7B9E), + active = false + ) + if (showMenu) { + + PostAction( + parameter = -1, + iconNormal = Icons.Default.MoreHoriz, + contentDescription = "More ", + onClicked = { + menuExpanded = true + }, + onUnClicked = { + menuExpanded = true + }, + ) + } + + } +} + @OptIn(ExperimentalLayoutApi::class) diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/post/PostFragment.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/post/PostFragment.kt index aad921b..51e602d 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/post/PostFragment.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/post/PostFragment.kt @@ -44,7 +44,6 @@ import morpho.composeapp.generated.resources.replyIndicator import org.jetbrains.compose.resources.ExperimentalResourceApi import org.jetbrains.compose.resources.stringResource - @OptIn(ExperimentalLayoutApi::class, ExperimentalFoundationApi::class) @Composable fun PostFragment( diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/util/BlueskyText.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/util/BlueskyText.kt index f075a18..e124455 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/util/BlueskyText.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/util/BlueskyText.kt @@ -3,11 +3,10 @@ package com.morpho.app.util import androidx.compose.ui.util.fastFilterNotNull import androidx.compose.ui.util.fastFlatMap import androidx.compose.ui.util.fastMap -import com.atproto.identity.ResolveHandleQuery import com.morpho.app.model.bluesky.BskyFacet import com.morpho.app.model.bluesky.FacetType import com.morpho.app.model.bluesky.RichTextFormat -import com.morpho.butterfly.Butterfly +import com.morpho.butterfly.ButterflyAgent import kotlinx.serialization.Serializable @Serializable @@ -74,16 +73,16 @@ sealed interface Segment { expect fun makeBlueskyText(text: String): BlueskyText -suspend fun resolveBlueskyText(text: BlueskyText, api: Butterfly): Result = runCatching { +suspend fun resolveBlueskyText(text: BlueskyText, agent: ButterflyAgent): Result = runCatching { val facets:List = text.facets.fastFlatMap { facet: BskyFacet -> facet.facetType.fastMap { if (it is FacetType.UserHandleMention) { // Resolve handles - val response = api.api.resolveHandle(ResolveHandleQuery(it.handle)).getOrNull() - if (response != null) { + val did = agent.resolveHandle(it.handle).getOrNull() + if (did != null) { val index = facet.facetType.indexOf(it) val facetTypes = facet.facetType.toMutableList() - facetTypes[index] = FacetType.UserDidMention(response.did) + facetTypes[index] = FacetType.UserDidMention(did) facet.copy(facetType = facetTypes) } else null diff --git a/Morpho/composeApp/src/desktopMain/kotlin/com/morpho/app/Platform.desktop.kt b/Morpho/composeApp/src/desktopMain/kotlin/com/morpho/app/Platform.desktop.kt index ce37ec9..77fb745 100644 --- a/Morpho/composeApp/src/desktopMain/kotlin/com/morpho/app/Platform.desktop.kt +++ b/Morpho/composeApp/src/desktopMain/kotlin/com/morpho/app/Platform.desktop.kt @@ -1,6 +1,7 @@ @file:Suppress("EXPECT_ACTUAL_CLASSIFIERS_ARE_IN_BETA_WARNING") package com.morpho.app import kotlinx.datetime.LocalDateTime +import java.util.Locale // Note: no need to define CommonParcelize here (bc its @OptionalExpectation) actual interface CommonParcelable // not used on iOS @@ -12,4 +13,16 @@ actual object LocalDateTimeParceler : CommonParceler // not used // For Android @Parcelize @Target(AnnotationTarget.TYPE) @Retention(AnnotationRetention.SOURCE) -actual annotation class CommonRawValue \ No newline at end of file +actual annotation class CommonRawValue + + +class JVMPlatform: Platform { + override val name: String = "Java ${System.getProperty("java.version")}" +} + +actual fun getPlatform(): Platform = JVMPlatform() + +actual val myCountry: String? + get() = Locale.getDefault().country +actual val myLang: String? + get() = Locale.getDefault().language \ No newline at end of file diff --git a/Morpho/composeApp/src/desktopMain/kotlin/com/morpho/app/Platform.jvm.kt b/Morpho/composeApp/src/desktopMain/kotlin/com/morpho/app/Platform.jvm.kt deleted file mode 100644 index 25020e0..0000000 --- a/Morpho/composeApp/src/desktopMain/kotlin/com/morpho/app/Platform.jvm.kt +++ /dev/null @@ -1,6 +0,0 @@ -package com.morpho.app -class JVMPlatform: Platform { - override val name: String = "Java ${System.getProperty("java.version")}" -} - -actual fun getPlatform(): Platform = JVMPlatform() \ No newline at end of file diff --git a/Morpho/composeApp/src/nativeMain/kotlin/com/morpho/app/Platform.native.kt b/Morpho/composeApp/src/nativeMain/kotlin/com/morpho/app/Platform.native.kt new file mode 100644 index 0000000..9b56323 --- /dev/null +++ b/Morpho/composeApp/src/nativeMain/kotlin/com/morpho/app/Platform.native.kt @@ -0,0 +1,6 @@ +package com.morpho.app + +actual val myCountry: String? + get() = TODO("Not yet implemented") +actual val myLang: String? + get() = TODO("Not yet implemented") \ No newline at end of file From 1739d170b4a80187718702ce2ef44dbabe141c3c Mon Sep 17 00:00:00 2001 From: Orual Date: Sun, 15 Sep 2024 23:50:01 -0400 Subject: [PATCH 10/42] Big UI refactoring ongoing. --- .../com/morpho/app/data/MorphoDataSource.kt | 1 + .../morpho/app/model/uidata/FeedPresenter.kt | 1 + .../app/screens/main/MainScreenModel.kt | 5 +++- .../main/tabbed/TabbedMainScreenModel.kt | 27 ++++++++++++++----- 4 files changed, 26 insertions(+), 8 deletions(-) diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/data/MorphoDataSource.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/data/MorphoDataSource.kt index 5217809..cc0ad46 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/data/MorphoDataSource.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/data/MorphoDataSource.kt @@ -45,6 +45,7 @@ abstract class MorphoDataSource: PagingSource( val request: FeedRequest, val tuners: List> = listOf(), diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/FeedPresenter.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/FeedPresenter.kt index 6195d3d..09083b6 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/FeedPresenter.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/FeedPresenter.kt @@ -17,6 +17,7 @@ import com.morpho.butterfly.PagedResponse import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map + class FeedPresenter( descriptor: FeedDescriptor? = null, ): Presenter() { diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/main/MainScreenModel.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/main/MainScreenModel.kt index ccaf3df..518e00f 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/main/MainScreenModel.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/main/MainScreenModel.kt @@ -34,6 +34,8 @@ open class MainScreenModel: BaseScreenModel() { val feedStates = mutableMapOf>() + var initialized = false + companion object { val log = logging("MainScreenModel") } @@ -41,7 +43,6 @@ open class MainScreenModel: BaseScreenModel() { init { if(isLoggedIn) screenModelScope.launch { userProfile = userDid?.let { agent.getProfile(it).getOrNull()?.toProfile() } - feedSources.add(FeedSourceInfo.Home) feedSources.addAll(pinnedFeeds.mapNotNull { feed -> feed.toFeedSourceInfo(agent).getOrNull() }) feedPresenters.putAll(feedSources.map { source -> source to FeedPresenter(source.feedDescriptor) @@ -65,6 +66,8 @@ open class MainScreenModel: BaseScreenModel() { } } + initialized = true + } } diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/main/tabbed/TabbedMainScreenModel.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/main/tabbed/TabbedMainScreenModel.kt index 76113f0..2a9bd44 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/main/tabbed/TabbedMainScreenModel.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/main/tabbed/TabbedMainScreenModel.kt @@ -1,30 +1,43 @@ package com.morpho.app.screens.main.tabbed +import app.bsky.actor.FeedType import cafe.adriel.voyager.core.model.screenModelScope import com.morpho.app.model.uidata.ContentCardMapEntry -import com.morpho.app.model.uidata.Event +import com.morpho.app.model.uidata.FeedEvent import com.morpho.app.screens.main.MainScreenModel import com.morpho.butterfly.AtUri +import kotlinx.coroutines.delay import kotlinx.coroutines.launch -import kotlinx.serialization.Contextual -import kotlinx.serialization.Serializable import org.lighthousegames.logging.logging -@Suppress("UNCHECKED_CAST") -@Serializable + class TabbedMainScreenModel : MainScreenModel() { - @Contextual private val tabs = mutableListOf>() + private val _tabs = mutableListOf>() + val tabs: List> + get() = _tabs.toList() + val timelineIndex = agent.prefs.timelineIndex ?: agent.prefs.saved.indexOfFirst { + it.type == FeedType.TIMELINE + }.let { if(it == -1) 0 else it } + val lastPinnedIndex = agent.prefs.saved.indexOfLast { it.pinned } companion object { val log = logging("TabbedMainScreenModel") } init { - if(isLoggedIn) screenModelScope.launch { + screenModelScope.launch { + while(!initialized) { + delay(10) + } + for(i in 0 .. lastPinnedIndex) { + val source = feedSources[i] + feedStates[source]?.let { _tabs.add(it) } + } } + } fun uriForTab(index: Int): AtUri { From 87fac14c27d7fc7abd92d6674d65181cc1b88bcf Mon Sep 17 00:00:00 2001 From: Orual Date: Mon, 16 Sep 2024 20:29:37 -0400 Subject: [PATCH 11/42] Big UI refactoring ongoing. --- .../ui/common/TabbedScreenScaffold.android.kt | 13 +- .../ui/common/TabbedScreenScaffold.apple.kt | 13 +- .../com/morpho/app/data/MorphoDataSource.kt | 2 +- .../kotlin/com/morpho/app/di/AppModule.kt | 2 - .../app/model/bluesky/BskyLabelService.kt | 40 +- .../app/model/bluesky/FeedSourceInfo.kt | 11 +- .../app/model/bluesky/NotificationsList.kt | 201 --------- .../app/model/bluesky/NotificationsSource.kt | 192 +++++++++ .../model/uidata/BskyNotificationService.kt | 158 -------- .../app/model/uidata/ContentCardMapEntry.kt | 45 +- .../model/uidata/{FeedEvent.kt => Events.kt} | 84 ++-- .../morpho/app/model/uidata/FeedPresenter.kt | 14 +- .../{ListPresenter.kt => Presenters.kt} | 17 +- .../app/model/uidata/ProfilePresenters.kt | 232 +++++++++++ .../com/morpho/app/model/uidata/UIUpdate.kt | 44 +- .../app/model/uistate/ContentCardState.kt | 189 +++++---- .../app/model/uistate/NotificationsState.kt | 28 +- .../model/uistate/PostThreadContentState.kt | 7 - .../morpho/app/model/uistate/SkylineState.kt | 18 - .../app/screens/base/BaseScreenModel.kt | 70 +++- .../app/screens/base/tabbed/NavigationTabs.kt | 40 +- .../screens/base/tabbed/TabbedBaseScreen.kt | 6 +- .../app/screens/login/LoginScreenModel.kt | 2 +- .../app/screens/main/MainScreenModel.kt | 25 +- .../app/screens/main/tabbed/TabbedHomeView.kt | 81 ++-- .../main/tabbed/TabbedMainScreenModel.kt | 15 +- .../notifications/NotificationsView.kt | 162 ++++---- .../TabbedNotificationScreenModel.kt | 64 --- .../app/screens/profile/TabbedProfileView.kt | 383 ++++++++++-------- .../screens/profile/TabbedProfileViewModel.kt | 263 ------------ .../morpho/app/screens/thread/ThreadView.kt | 63 ++- .../morpho/app/ui/common/SkylineFragment.kt | 66 +-- .../app/ui/common/TabbedScreenScaffold.kt | 13 +- .../app/ui/common/TabbedSkylineFragment.kt | 49 ++- .../ui/notifications/NotificationsElement.kt | 27 +- .../app/ui/post/NotFoundPostFragment.kt | 167 ++++---- .../com/morpho/app/ui/post/PollBlueEmbed.kt | 2 +- .../com/morpho/app/ui/post/PostFragment.kt | 23 +- .../ui/common/TabbedScreenScaffold.desktop.kt | 13 +- .../kotlin/com/morpho/app/Platform.native.kt | 6 - 40 files changed, 1352 insertions(+), 1498 deletions(-) delete mode 100644 Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/NotificationsList.kt create mode 100644 Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/NotificationsSource.kt delete mode 100644 Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/BskyNotificationService.kt rename Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/{FeedEvent.kt => Events.kt} (71%) rename Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/{ListPresenter.kt => Presenters.kt} (89%) create mode 100644 Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/ProfilePresenters.kt delete mode 100644 Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/notifications/TabbedNotificationScreenModel.kt delete mode 100644 Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/profile/TabbedProfileViewModel.kt delete mode 100644 Morpho/composeApp/src/nativeMain/kotlin/com/morpho/app/Platform.native.kt diff --git a/Morpho/composeApp/src/androidMain/kotlin/com/morpho/app/ui/common/TabbedScreenScaffold.android.kt b/Morpho/composeApp/src/androidMain/kotlin/com/morpho/app/ui/common/TabbedScreenScaffold.android.kt index a616d2b..5aa82de 100644 --- a/Morpho/composeApp/src/androidMain/kotlin/com/morpho/app/ui/common/TabbedScreenScaffold.android.kt +++ b/Morpho/composeApp/src/androidMain/kotlin/com/morpho/app/ui/common/TabbedScreenScaffold.android.kt @@ -9,16 +9,15 @@ import androidx.compose.material3.TopAppBarScrollBehavior import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.input.nestedscroll.NestedScrollConnection -import com.morpho.app.model.bluesky.MorphoDataItem +import com.morpho.app.model.uidata.Event import com.morpho.app.model.uistate.ContentCardState -import kotlinx.coroutines.flow.StateFlow @Composable actual fun TabbedScreenScaffold( navBar: @Composable () -> Unit, - content: @Composable (PaddingValues, StateFlow?) -> Unit, + content: @Composable (PaddingValues, T?) -> Unit, topContent: @Composable () -> Unit, - state: StateFlow?, + state: T?, modifier: Modifier, ) { Scaffold( @@ -34,11 +33,11 @@ actual fun TabbedScreenScaffold( @OptIn(ExperimentalMaterial3Api::class) @Composable -actual fun TabbedProfileScreenScaffold( +actual fun TabbedProfileScreenScaffold( navBar: @Composable () -> Unit, - content: @Composable (PaddingValues, StateFlow>?) -> Unit, + content: @Composable (PaddingValues, ContentCardState?) -> Unit, topContent: @Composable (TopAppBarScrollBehavior) -> Unit, - state: StateFlow>?, + state: ContentCardState?, modifier: Modifier, scrollBehavior: TopAppBarScrollBehavior, nestedScrollConnection: NestedScrollConnection, diff --git a/Morpho/composeApp/src/appleMain/kotlin/com/morpho/app/ui/common/TabbedScreenScaffold.apple.kt b/Morpho/composeApp/src/appleMain/kotlin/com/morpho/app/ui/common/TabbedScreenScaffold.apple.kt index fccc58d..d076299 100644 --- a/Morpho/composeApp/src/appleMain/kotlin/com/morpho/app/ui/common/TabbedScreenScaffold.apple.kt +++ b/Morpho/composeApp/src/appleMain/kotlin/com/morpho/app/ui/common/TabbedScreenScaffold.apple.kt @@ -9,16 +9,15 @@ import androidx.compose.material3.TopAppBarScrollBehavior import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.input.nestedscroll.NestedScrollConnection -import com.morpho.app.model.bluesky.MorphoDataItem +import com.morpho.app.model.uidata.Event import com.morpho.app.model.uistate.ContentCardState -import kotlinx.coroutines.flow.StateFlow @Composable actual fun TabbedScreenScaffold( navBar: @Composable () -> Unit, - content: @Composable (PaddingValues, StateFlow?) -> Unit, + content: @Composable (PaddingValues, T?) -> Unit, topContent: @Composable () -> Unit, - state: StateFlow?, + state: T?, modifier: Modifier, ) { Scaffold( @@ -34,11 +33,11 @@ actual fun TabbedScreenScaffold( @OptIn(ExperimentalMaterial3Api::class) @Composable -actual fun TabbedProfileScreenScaffold( +actual fun TabbedProfileScreenScaffold( navBar: @Composable () -> Unit, - content: @Composable (PaddingValues, StateFlow>?) -> Unit, + content: @Composable (PaddingValues, ContentCardState?) -> Unit, topContent: @Composable (TopAppBarScrollBehavior) -> Unit, - state: StateFlow>?, + state: ContentCardState?, modifier: Modifier, scrollBehavior: TopAppBarScrollBehavior, nestedScrollConnection: NestedScrollConnection, diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/data/MorphoDataSource.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/data/MorphoDataSource.kt index cc0ad46..ade276d 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/data/MorphoDataSource.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/data/MorphoDataSource.kt @@ -23,7 +23,7 @@ import org.koin.core.component.inject import kotlin.time.Duration -abstract class MorphoDataSource: PagingSource(), KoinComponent { +abstract class MorphoDataSource: PagingSource(), KoinComponent { val agent: MorphoAgent by inject() val moderator: ContentLabelService by inject() diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/di/AppModule.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/di/AppModule.kt index c42950a..4cd4112 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/di/AppModule.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/di/AppModule.kt @@ -8,8 +8,6 @@ import com.morpho.app.screens.base.BaseScreenModel import com.morpho.app.screens.login.LoginScreenModel import com.morpho.app.screens.main.MainScreenModel import com.morpho.app.screens.main.tabbed.TabbedMainScreenModel -import com.morpho.app.screens.notifications.TabbedNotificationScreenModel -import com.morpho.app.screens.profile.TabbedProfileViewModel import com.morpho.app.util.ClipboardManager import com.morpho.butterfly.AtpAgent import com.morpho.butterfly.Butterfly diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/BskyLabelService.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/BskyLabelService.kt index d86af2d..bab0a28 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/BskyLabelService.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/BskyLabelService.kt @@ -27,44 +27,18 @@ open class BskyLabelService( val liked: Boolean, val likeUri: AtUri?, @TypeParceler() - override val indexedAt: Moment, + val indexedAt: Moment, val policies: List, - override val labels: List, -): Profile { - override val did: Did + val labels: List, +) { + val did: Did get() = creator?.did ?: Did("did:blank:did") - override val handle: Handle + val handle: Handle get() = creator?.handle ?: Handle("blank.handle") - override val displayName: String? + val displayName: String? get() = creator?.displayName - override val avatar: String? + val avatar: String? get() = creator?.avatar - override val mutedByMe: Boolean - get() = creator?.mutedByMe ?: false - override val mutedByList: UserListBasic? - get() = null - override val block: BlockRecord? - get() = null - override val blockedBy: Boolean - get() = false - override val blockingByList: UserListBasic? - get() = null - override val following: FollowRecord? - get() = null - override val followedBy: FollowRecord? - get() = null - override val numKnownFollowers: Long - get() = 0 - override val knownFollowers: List - get() = listOf() - override val associated: BskyProfileAssociated? - get() = null - override val createdAt: Moment? - get() = null - override val followingMe: Boolean - get() = false - override val followedByMe: Boolean - get() = false } public data object BlueskyHardcodedLabeler: BskyLabelService( diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/FeedSourceInfo.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/FeedSourceInfo.kt index 97b6f1c..f1ef193 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/FeedSourceInfo.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/FeedSourceInfo.kt @@ -6,13 +6,13 @@ import app.bsky.feed.GeneratorView import app.bsky.feed.GetFeedGeneratorQuery import app.bsky.graph.GetListQuery import app.bsky.graph.ListView +import com.morpho.app.model.uidata.ContentCardMapEntry import com.morpho.butterfly.* import dev.icerock.moko.parcelize.Parcelable import dev.icerock.moko.parcelize.Parcelize import kotlinx.serialization.Serializable - @Serializable @Immutable enum class AuthorFilter { @@ -121,6 +121,15 @@ sealed interface FeedSourceInfo: Parcelable { } } +fun FeedSourceInfo.toContentCardMapEntry(): ContentCardMapEntry { + return when(this) { + is FeedSourceInfo.FeedInfo -> ContentCardMapEntry.Feed(this.uri, this.displayName?: "", this.avatar) + FeedSourceInfo.Following -> ContentCardMapEntry.Home + FeedSourceInfo.Home -> ContentCardMapEntry.Home + is FeedSourceInfo.ListInfo -> ContentCardMapEntry.ListFeed(this.uri, this.displayName?: "", this.avatar) + } +} + fun GeneratorView.hydrateFeedGenerator(): FeedSourceInfo.FeedInfo { return FeedSourceInfo.FeedInfo( uri = this.uri, diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/NotificationsList.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/NotificationsList.kt deleted file mode 100644 index 194a555..0000000 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/NotificationsList.kt +++ /dev/null @@ -1,201 +0,0 @@ -package com.morpho.app.model.bluesky - -import androidx.compose.ui.util.fastMap -import app.bsky.notification.ListNotificationsNotification -import app.bsky.notification.ListNotificationsReason -import com.morpho.app.model.uidata.AtCursor -import com.morpho.app.util.mapImmutable -import com.morpho.butterfly.AtUri -import kotlinx.collections.immutable.persistentListOf -import kotlinx.collections.immutable.toImmutableList -import kotlinx.collections.immutable.toPersistentList -import kotlinx.serialization.Serializable - -/** - * Notifications combined for display - * Will show unread if any of a category are unread - * Categorizes either by what it refers to (for likes and so on) or by type (for follows) - */ -@Serializable -data class NotificationsList( - private val notifications: List = persistentListOf(), - val cursor: AtCursor = AtCursor.EMPTY, -) { - private var _notificationsList: MutableList = mutableListOf() - val notificationsList: List - get() { - if (!initialized) { - initList() - } - return _notificationsList.fastMap { it.toImmutable() }.toList() - } - - private var initialized = false - init { - initList() - } - - private fun initList() { - if (initialized) return - val seen = mutableListOf() - notifications.map { notif -> - if(notif.reasonSubject != null && seen.contains(notif.reasonSubject)) { - val index = _notificationsList.indexOfFirst { - it.reasonSubject == notif.reasonSubject - } - if (index >= 0 && notif.reason == _notificationsList[index].reason) { - _notificationsList[index].notifications.add(notif) - _notificationsList[index].isRead = if (notif.isRead) true else _notificationsList[index].isRead - } else { - _notificationsList.add( - MutableNotificationsListItem( - notifications = mutableListOf(notif), - reason = notif.reason, - isRead = notif.isRead, - reasonSubject = notif.reasonSubject - ) - ) - } - } else if (notif.reasonSubject != null) { - seen.add(notif.reasonSubject!!) - _notificationsList.add( - MutableNotificationsListItem( - notifications = mutableListOf(notif), - reason = notif.reason, - isRead = notif.isRead, - reasonSubject = notif.reasonSubject - ) - ) - } else { - val index = _notificationsList.indexOfFirst { item-> - item.reason == notif.reason - } - if (index >= 0) { - _notificationsList[index].notifications.add(notif) - _notificationsList[index].isRead = if (notif.isRead) true else _notificationsList[index].isRead - } else { - _notificationsList.add( - MutableNotificationsListItem( - notifications = mutableListOf(notif), - reason = notif.reason, - isRead = notif.isRead, - reasonSubject = notif.reasonSubject - ) - ) - } - } - } - initialized = true - } - fun concat(new: List): NotificationsList { - return NotificationsList( - notifications.toPersistentList().addAll(new.map { - it.toBskyNotification() - }) - ) - } - - fun concat(new: NotificationsList): NotificationsList { - return NotificationsList( - notifications.toPersistentList().addAll(new.notifications) - ) - } - fun markAllRead(): NotificationsList { - _notificationsList.map { - it.isRead = true - } - val newNotifs = notifications.mapImmutable { - when(it) { - is BskyNotification.Follow -> it.copy(isRead = true) - is BskyNotification.Like -> it.copy(isRead = true) - is BskyNotification.Post -> it.copy(isRead = true) - is BskyNotification.Repost -> it.copy(isRead = true) - is BskyNotification.Unknown -> it.copy(isRead = true) - } - } - return this.copy( - notifications = newNotifs - ) - } - - fun markRead(uri: AtUri): NotificationsList { - _notificationsList.forEach { notificationsListItem -> - if(notificationsListItem.notifications.firstOrNull { it.uri == uri } != null) { - notificationsListItem.isRead = true - notificationsListItem.notifications.map { - when(it) { - is BskyNotification.Follow -> it.copy(isRead = true) - is BskyNotification.Like -> it.copy(isRead = true) - is BskyNotification.Post -> it.copy(isRead = true) - is BskyNotification.Repost -> it.copy(isRead = true) - is BskyNotification.Unknown -> it.copy(isRead = true) - } - } - } - } - return this - } -} - -@Serializable -data class MutableNotificationsListItem( - val notifications: MutableList = mutableListOf(), - val reason: ListNotificationsReason, - var isRead: Boolean = false, - val reasonSubject: AtUri? = null, -) { - companion object { - fun fromImmutable(item: NotificationsListItem): MutableNotificationsListItem { - return MutableNotificationsListItem( - notifications = item.notifications.toMutableList(), - reason = item.reason, - isRead = item.isRead, - reasonSubject = item.reasonSubject - ) - } - } - fun toImmutable(): NotificationsListItem { - return NotificationsListItem( - notifications = notifications.toImmutableList(), - reason = reason, - isRead = isRead, - reasonSubject = reasonSubject - ) - } -} - -@Serializable -data class NotificationsListItem( - val notifications: List, - val reason: ListNotificationsReason, - val isRead: Boolean, - val reasonSubject: AtUri?, -) { - companion object { - fun fromMutable(item: MutableNotificationsListItem) { - NotificationsListItem( - notifications = item.notifications.toImmutableList(), - reason = item.reason, - isRead = item.isRead, - reasonSubject = item.reasonSubject - ) - } - } - - override fun hashCode(): Int { - return notifications.hashCode() + reason.hashCode() + reasonSubject.hashCode() - } - - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (other == null || this::class != other::class) return false - - other as NotificationsListItem - - if (reason != other.reason) return false - if (reasonSubject != other.reasonSubject) return false - if (notifications != other.notifications) return false - - return true - } -} \ No newline at end of file diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/NotificationsSource.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/NotificationsSource.kt new file mode 100644 index 0000000..32be980 --- /dev/null +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/NotificationsSource.kt @@ -0,0 +1,192 @@ +package com.morpho.app.model.bluesky + +import app.bsky.notification.ListNotificationsReason +import app.cash.paging.PagingConfig +import app.cash.paging.compose.LazyPagingItems +import com.morpho.app.data.MorphoDataSource +import com.morpho.app.model.uistate.NotificationsFilterState +import com.morpho.butterfly.AtUri +import com.morpho.butterfly.Cursor +import kotlinx.serialization.Serializable +import org.lighthousegames.logging.logging + +class NotificationsSource: MorphoDataSource() { + companion object { + val log = logging() + val defaultConfig = PagingConfig( + pageSize = 20, + prefetchDistance = 20, + initialLoadSize = 50, + enablePlaceholders = false, + ) + } + + override suspend fun load(params: LoadParams): LoadResult { + try { + val limit = params.loadSize + val loadCursor = when(params) { + is LoadParams.Append -> params.key + is LoadParams.Prepend -> Cursor.Empty + is LoadParams.Refresh -> Cursor.Empty + } + return agent.listNotifications(limit.toLong(), loadCursor.value).map { response -> + val newCursor = response.cursor + val items = response.items.map { it.toBskyNotification()} + LoadResult.Page( + data = items, + prevKey = when(params) { + is LoadParams.Append -> loadCursor + is LoadParams.Prepend -> Cursor.Empty + is LoadParams.Refresh -> Cursor.Empty + }, + nextKey = newCursor, + ) + }.onFailure { + return LoadResult.Error(it) + }.getOrDefault(LoadResult.Error(Exception("Load failed"))) + } catch (e: Exception) { + return LoadResult.Error(e) + } + } +} + +fun LazyPagingItems.collectNotifications( + toMark: List = listOf() +) : List { + val seen = mutableListOf() + val workList = mutableListOf() + this.itemSnapshotList.map { notif -> + if (notif == null) return@map NotificationsListItem( + notifications = listOf(), + reason = ListNotificationsReason.PLACEHOLDER, + isRead = false, + reasonSubject = null, + ) + if(notif.reasonSubject != null && seen.contains(notif.reasonSubject)) { + val index = workList.indexOfFirst { + it.reasonSubject == notif.reasonSubject + } + if (index >= 0 && notif.reason == workList[index].reason) { + workList[index].notifications.add(notif) + workList[index].isRead = if (notif.isRead) true else workList[index].isRead + } else { + workList.add( + MutableNotificationsListItem( + notifications = mutableListOf(notif), + reason = notif.reason, + isRead = notif.isRead, + reasonSubject = notif.reasonSubject + ) + ) + } + } else if (notif.reasonSubject != null) { + seen.add(notif.reasonSubject!!) + workList.add( + MutableNotificationsListItem( + notifications = mutableListOf(notif), + reason = notif.reason, + isRead = notif.isRead, + reasonSubject = notif.reasonSubject + ) + ) + } else { + val index = workList.indexOfFirst { item-> + item.reason == notif.reason + } + if (index >= 0) { + workList[index].notifications.add(notif) + workList[index].isRead = if (notif.isRead) true else workList[index].isRead + } else { + workList.add( + MutableNotificationsListItem( + notifications = mutableListOf(notif), + reason = notif.reason, + isRead = notif.isRead, + reasonSubject = notif.reasonSubject + ) + ) + } + } + } + return workList.map { it.toImmutable() } +} + +fun List.filterNotifications( + filter: NotificationsFilterState, +): List { + return this.filter { + (if(it.isRead) filter.showAlreadyRead else true) && + when(it.reason) { + ListNotificationsReason.LIKE -> filter.showLikes + ListNotificationsReason.REPOST -> filter.showReposts + ListNotificationsReason.FOLLOW -> filter.showFollows + ListNotificationsReason.MENTION -> filter.showMentions + ListNotificationsReason.REPLY -> filter.showReplies + ListNotificationsReason.QUOTE -> filter.showQuotes + else -> true + } + }.toList() +} + +@Serializable +data class MutableNotificationsListItem( + val notifications: MutableList = mutableListOf(), + val reason: ListNotificationsReason, + var isRead: Boolean = false, + val reasonSubject: AtUri? = null, +) { + companion object { + fun fromImmutable(item: NotificationsListItem): MutableNotificationsListItem { + return MutableNotificationsListItem( + notifications = item.notifications.distinctBy { it.author.did }.toMutableList(), + reason = item.reason, + isRead = item.isRead, + reasonSubject = item.reasonSubject + ) + } + } + fun toImmutable(): NotificationsListItem { + return NotificationsListItem( + notifications = notifications.distinctBy { it.author.did }, + reason = reason, + isRead = isRead, + reasonSubject = reasonSubject + ) + } +} + +@Serializable +data class NotificationsListItem( + val notifications: List, + val reason: ListNotificationsReason, + val isRead: Boolean, + val reasonSubject: AtUri?, +) { + companion object { + fun fromMutable(item: MutableNotificationsListItem) { + NotificationsListItem( + notifications = item.notifications.distinctBy { it.author.did }, + reason = item.reason, + isRead = item.isRead, + reasonSubject = item.reasonSubject + ) + } + } + + override fun hashCode(): Int { + return notifications.hashCode() + reason.hashCode() + reasonSubject.hashCode() + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other == null || this::class != other::class) return false + + other as NotificationsListItem + + if (reason != other.reason) return false + if (reasonSubject != other.reasonSubject) return false + if (notifications != other.notifications) return false + + return true + } +} \ No newline at end of file diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/BskyNotificationService.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/BskyNotificationService.kt deleted file mode 100644 index a865778..0000000 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/BskyNotificationService.kt +++ /dev/null @@ -1,158 +0,0 @@ -package com.morpho.app.model.uidata - -import app.bsky.notification.GetUnreadCountQuery -import app.bsky.notification.ListNotificationsQuery -import app.bsky.notification.ListNotificationsReason -import app.bsky.notification.UpdateSeenRequest -import com.morpho.app.di.UpdateTick -import com.morpho.app.model.bluesky.NotificationsList -import com.morpho.app.model.bluesky.NotificationsListItem -import com.morpho.app.model.bluesky.toBskyNotification -import com.morpho.app.model.uistate.NotificationsFilterState -import com.morpho.app.util.mapImmutable -import com.morpho.butterfly.AtUri -import com.morpho.butterfly.Butterfly -import kotlinx.coroutines.* -import kotlinx.coroutines.flow.* -import kotlinx.coroutines.sync.Mutex -import kotlinx.coroutines.sync.withLock -import kotlinx.datetime.Clock -import kotlinx.serialization.Serializable -import org.koin.core.component.KoinComponent -import org.koin.core.component.inject -import org.lighthousegames.logging.logging - -@Serializable -class BskyNotificationService: KoinComponent { - val api: Butterfly by inject() - - private val mutex = Mutex() - - private var _notifications = MutableStateFlow(NotificationsList()) - - val notifications: StateFlow - get() = _notifications.asStateFlow() - - private var _filter = MutableStateFlow(NotificationsFilterState()) - - val filter: StateFlow - get() = _filter.asStateFlow() - - - companion object { - val log = logging() - val serviceScope = CoroutineScope(SupervisorJob() + Dispatchers.Default) - } - - fun updateFilter(filterState: NotificationsFilterState) = serviceScope.launch { - mutex.withLock { - _filter.update { filterState } - } - } - - fun updateNotificationsSeen() = serviceScope.launch { - api.api.updateSeen(UpdateSeenRequest(Clock.System.now())) - _notifications.update { it.markAllRead() } - } - - fun markAsRead(uri: AtUri) = serviceScope.launch { - _notifications.update { it.markRead(uri) } - } - - fun getUnreadCountLocal(): Long { - return _notifications.value.notificationsList.count { !it.isRead }.toLong() - } - - suspend fun getUnreadCount(): Result { - return api.api.getUnreadCount(GetUnreadCountQuery(Clock.System.now())).map { - log.d { "Unread count: ${it.count}" } - it.count - } - } - - @OptIn(FlowPreview::class) - suspend fun unreadCount( - update: SharedFlow, - dispatcher: CoroutineDispatcher = Dispatchers.IO, - ): Flow = flow { - update.debounce(300).collect { - getUnreadCount().onSuccess { - emit(it) - }.onFailure { - log.e { "Failed to get unread count: $it" } - log.e { "Falling back to local" } - emit(getUnreadCountLocal()) - } - } - }.distinctUntilChanged().flowOn(dispatcher + CoroutineName("UnreadCount")) - - suspend fun unreadNotifTick(interval: Long = 60000): SharedFlow = flow { - val updateTick = UpdateTick(interval) - updateTick.tick(true) - updateTick.t.collect { - emit(Unit) - } - }.shareIn(serviceScope, SharingStarted.WhileSubscribed(), 1) - - fun unreadCountFlow( - interval: Long = 60000, - dispatcher: CoroutineDispatcher = Dispatchers.IO - ): Flow = flow { - unreadCount(unreadNotifTick(interval), dispatcher).collect { - emit(it) - } - }.distinctUntilChanged().flowOn(dispatcher + CoroutineName("UnreadCountFlow")) - - - @OptIn(FlowPreview::class) - suspend fun notifications( - cursor: SharedFlow = initAtCursor(), - limit: Long = 50, - dispatcher: CoroutineDispatcher = Dispatchers.IO, - ): Flow> = flow { - cursor.debounce(300).collect { cursor -> - val query = ListNotificationsQuery(limit, cursor.cursor) - val result = api.api.listNotifications(query).map { response -> - if(notifications.value.notificationsList.isNotEmpty()) { - if (cursor == AtCursor.EMPTY) { - NotificationsList( - response.notifications.mapImmutable { it.toBskyNotification() }, - AtCursor(response.cursor, cursor.scroll) - ).concat(notifications.value) - } else { - notifications.value.concat(response.notifications) - } - } else { - NotificationsList( - response.notifications.mapImmutable { it.toBskyNotification() }, - AtCursor(response.cursor, cursor.scroll) - ) - } - } - if (result.isSuccess) { - mutex.withLock { - _notifications.update { result.getOrThrow() } - } - } else log.e { "Failed to get notifications: ${result.exceptionOrNull()}" } - emit(result) - } - }.distinctUntilChanged().flowOn(dispatcher + CoroutineName("Notifications")) -} - -fun filterNotifications( - list: List, - filter: NotificationsFilterState, -): List { - return list.filter { - (if(it.isRead) filter.showAlreadyRead else true) && - when(it.reason) { - ListNotificationsReason.LIKE -> filter.showLikes - ListNotificationsReason.REPOST -> filter.showReposts - ListNotificationsReason.FOLLOW -> filter.showFollows - ListNotificationsReason.MENTION -> filter.showMentions - ListNotificationsReason.REPLY -> filter.showReplies - ListNotificationsReason.QUOTE -> filter.showQuotes - else -> true - } - }.toList() -} \ No newline at end of file diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/ContentCardMapEntry.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/ContentCardMapEntry.kt index 2929119..3aa369d 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/ContentCardMapEntry.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/ContentCardMapEntry.kt @@ -2,31 +2,22 @@ package com.morpho.app.model.uidata import androidx.compose.runtime.Immutable import com.morpho.app.model.uistate.FeedType -import com.morpho.app.util.MutableSharedFlowSerializer import com.morpho.butterfly.AtIdentifier import com.morpho.butterfly.AtUri -import kotlinx.coroutines.flow.MutableSharedFlow -import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.serialization.Serializable @Immutable @Serializable -sealed interface ContentCardMapEntry { +sealed interface ContentCardMapEntry { val uri: AtUri val title: String - @Serializable(with = MutableSharedFlowSerializer::class) - val events: MutableSharedFlow - @Serializable(with = MutableSharedFlowSerializer::class) - val updates: MutableStateFlow val avatar: String? @Immutable @Serializable - data object Home: ContentCardMapEntry, Skyline { + data object Home: ContentCardMapEntry, Skyline { override val uri: AtUri = AtUri.HOME_URI override val title: String = "Home" - override val events: MutableSharedFlow = MutableSharedFlow() - override val updates: MutableStateFlow = MutableStateFlow(FeedUpdate.Empty) override val avatar: String? = null } @@ -39,50 +30,48 @@ sealed interface ContentCardMapEntry { data class Feed( override val uri: AtUri, override val title: String = uri.atUri, - override val events: MutableSharedFlow = MutableSharedFlow(), - override val updates: MutableStateFlow = MutableStateFlow(FeedUpdate.Empty), override val avatar: String? = null, - ) : ContentCardMapEntry, Skyline + ) : ContentCardMapEntry, Skyline + + @Immutable + @Serializable + data class ListFeed( + override val uri: AtUri, + override val title: String = uri.atUri, + override val avatar: String? = null, + ) : ContentCardMapEntry, Skyline @Immutable @Serializable data class PostThread( override val uri: AtUri, override val title: String = uri.atUri, - override val events: MutableSharedFlow = MutableSharedFlow(), - override val updates: MutableStateFlow = MutableStateFlow(UIUpdate.Empty), override val avatar: String? = null, - ) : ContentCardMapEntry + ) : ContentCardMapEntry @Immutable @Serializable data class UserList( override val uri: AtUri, override val title: String = uri.atUri, - override val events: MutableSharedFlow = MutableSharedFlow(), - override val updates: MutableStateFlow = MutableStateFlow(UIUpdate.Empty), override val avatar: String? = null, - ) : ContentCardMapEntry + ) : ContentCardMapEntry @Immutable @Serializable data class FeedList( override val uri: AtUri, override val title: String = uri.atUri, - override val events: MutableSharedFlow = MutableSharedFlow(), - override val updates: MutableStateFlow = MutableStateFlow(UIUpdate.Empty), override val avatar: String? = null, - ) : ContentCardMapEntry + ) : ContentCardMapEntry @Immutable @Serializable data class ServiceList( override val uri: AtUri, override val title: String = uri.atUri, - override val events: MutableSharedFlow = MutableSharedFlow(), - override val updates: MutableStateFlow = MutableStateFlow(UIUpdate.Empty), override val avatar: String? = null, - ) : ContentCardMapEntry + ) : ContentCardMapEntry @Immutable @Serializable @@ -90,10 +79,8 @@ sealed interface ContentCardMapEntry { val id: AtIdentifier, override val uri: AtUri = AtUri.profileUri(id), override val title: String = uri.atUri, - override val events: MutableSharedFlow = MutableSharedFlow(), - override val updates: MutableStateFlow = MutableStateFlow(UIUpdate.Empty), override val avatar: String? = null, - ) : ContentCardMapEntry + ) : ContentCardMapEntry val isHome: Boolean get() = uri == AtUri.HOME_URI diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/FeedEvent.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/Events.kt similarity index 71% rename from Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/FeedEvent.kt rename to Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/Events.kt index cf257ee..dbbf080 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/FeedEvent.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/Events.kt @@ -6,6 +6,7 @@ import com.morpho.app.model.bluesky.AuthorFilter import com.morpho.app.model.bluesky.BskyPost import com.morpho.app.model.bluesky.FeedDescriptor import com.morpho.app.model.bluesky.FeedSourceInfo +import com.morpho.app.model.uistate.ListsOrFeeds import com.morpho.app.ui.common.ComposerRole import com.morpho.butterfly.AtIdentifier import com.morpho.butterfly.AtUri @@ -19,7 +20,10 @@ sealed interface Event { val seenAt: Timestamp = Clock.System.now() ): Event - + data class ComposePost( + val post: BskyPost, + val role: ComposerRole = ComposerRole.StandalonePost, + ): Event, PostEvent } sealed interface ModerationEvent: Event @@ -35,8 +39,8 @@ sealed interface FeedEvent: Event { is FeedDescriptor.Author -> when(descriptor.filter) { AuthorFilter.PostsNoReplies -> AtUri.profilePostsUri(descriptor.did) AuthorFilter.PostsWithReplies -> AtUri.profileRepliesUri(descriptor.did) - AuthorFilter.PostsAuthorThreads -> AtUri.profileMediaUri(descriptor.did) - AuthorFilter.PostsWithMedia -> AtUri.profileLikesUri(descriptor.did) + AuthorFilter.PostsAuthorThreads -> AtUri.profileRepliesUri(descriptor.did) + AuthorFilter.PostsWithMedia -> AtUri.profileMediaUri(descriptor.did) } is FeedDescriptor.FeedGen -> descriptor.uri is FeedDescriptor.Likes -> AtUri.profileLikesUri(descriptor.did) @@ -47,8 +51,22 @@ sealed interface FeedEvent: Event { data class LoadLists( val actor: AtIdentifier, + val listsOrFeeds: ListsOrFeeds, ): FeedEvent, LoadEvent, ListEvent { - override val uri: AtUri = AtUri.myUserListUri(actor.toString()) + override val uri: AtUri = AtUri.profileUserListsUri(actor) + } + + data class LoadFeed( + val actor: AtIdentifier, + val filter: AuthorFilter?, + ): FeedEvent, LoadEvent, ListEvent { + override val uri: AtUri = when(filter) { + AuthorFilter.PostsNoReplies -> AtUri.profilePostsUri(actor) + AuthorFilter.PostsWithReplies -> AtUri.profileRepliesUri(actor) + AuthorFilter.PostsAuthorThreads -> AtUri.profileRepliesUri(actor) + AuthorFilter.PostsWithMedia -> AtUri.profileMediaUri(actor) + null -> AtUri.profileLikesUri(actor) + } } data class LoadSaved( @@ -69,31 +87,29 @@ sealed interface FeedEvent: Event { override val uri: AtUri = info.uri } - data class ComposePost( - val post: BskyPost, - val role: ComposerRole = ComposerRole.StandalonePost, - ): FeedEvent, PostEvent { - override val uri: AtUri? = null - } + } -sealed interface FeedPageEvent: Event { +sealed interface PageEvent: Event +sealed interface ListPageEvent: PageEvent + +sealed interface FeedPageEvent: PageEvent { data class LikeFeed(val like: StrongRef): FeedPageEvent, LikeEvent data class UnlikeFeed(val uri: AtUri): FeedPageEvent, LikeEvent data class Save(val info: SavedFeed): FeedPageEvent, PrefsEvent data class UnSave(val id: String): FeedPageEvent, PrefsEvent } -sealed interface CuratedListPage: Event { - data class Pin(val info: SavedFeed): CuratedListPage, PrefsEvent - data class Unpin(val id: String): CuratedListPage, PrefsEvent +sealed interface CuratedListPageEvent: ListPageEvent { + data class Pin(val info: SavedFeed): CuratedListPageEvent, PrefsEvent + data class Unpin(val id: String): CuratedListPageEvent, PrefsEvent } -sealed interface ModListPage: Event { - data class MuteList(val list: AtUri): ModListPage, ModerationEvent - data class UnmuteList(val uri: AtUri): ModListPage, ModerationEvent - data class BlockList(val list: AtUri): ModListPage, ModerationEvent - data class UnblockList(val uri: AtUri): ModListPage, ModerationEvent +sealed interface ModListPageEvent: ListPageEvent { + data class MuteList(val list: AtUri): ModListPageEvent, ModerationEvent + data class UnmuteList(val uri: AtUri): ModListPageEvent, ModerationEvent + data class BlockList(val list: AtUri): ModListPageEvent, ModerationEvent + data class UnblockList(val uri: AtUri): ModListPageEvent, ModerationEvent } sealed interface PrefsEvent: Event { @@ -165,27 +181,31 @@ sealed interface LabelerEvent: Event { ): LabelerEvent, PrefsEvent, ModerationEvent } -sealed interface MyProfileEvent: Event { +sealed interface MyProfileEvent: ProfileEvent { data object EnterEditing: MyProfileEvent data object ExitEditing: MyProfileEvent } -sealed interface ProfileEditEvent: Event { - data class SetDisplayName(val name: String): ProfileEditEvent, MyProfileEvent - data class SetDescription(val description: String): ProfileEditEvent, MyProfileEvent - data class SetAvatar(val avatar: PlatformFile): ProfileEditEvent, MyProfileEvent - data class SetBanner(val banner: PlatformFile): ProfileEditEvent, MyProfileEvent +sealed interface ProfileEditEvent: MyProfileEvent { + data class SetDisplayName(val name: String): ProfileEditEvent + data class SetDescription(val description: String): ProfileEditEvent + data class SetAvatar(val avatar: PlatformFile): ProfileEditEvent + data class SetBanner(val banner: PlatformFile): ProfileEditEvent } sealed interface ActorEvent: Event { - data class Follow(val subject: Did): ActorEvent - data class Unfollow(val uri: AtUri): ActorEvent - data class Mute(val subject: Did): ActorEvent, PrefsEvent, ModerationEvent - data class Unmute(val subject: Did): ActorEvent, PrefsEvent, ModerationEvent +} + +sealed interface ProfileEvent: ActorEvent { + data class Follow(val subject: Did): ProfileEvent + data class Unfollow(val uri: AtUri): ProfileEvent + + data class Mute(val subject: Did): ProfileEvent, PrefsEvent, ModerationEvent + data class Unmute(val subject: Did): ProfileEvent, PrefsEvent, ModerationEvent - data class Block(val subject: Did): ActorEvent, ModerationEvent - data class Unblock(val uri: AtUri): ActorEvent, ModerationEvent + data class Block(val subject: Did): ProfileEvent, ModerationEvent + data class Unblock(val uri: AtUri): ProfileEvent, ModerationEvent - data class ReportAccount(val subject: Did): ActorEvent, ModerationEvent + data class ReportAccount(val subject: Did): ProfileEvent, ModerationEvent } \ No newline at end of file diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/FeedPresenter.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/FeedPresenter.kt index 09083b6..8014e2f 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/FeedPresenter.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/FeedPresenter.kt @@ -18,30 +18,30 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map -class FeedPresenter( +class FeedPresenter( descriptor: FeedDescriptor? = null, -): Presenter() { +): PagedPresenter() { - private var dataSource: MorphoFeedSource = + private var dataSource: MorphoFeedSource = descriptor?.getDataSource(agent) ?: getTimelineDataSource(agent) - override var pager: Pager = run { + override var pager: Pager = run { val pagingConfig = MorphoDataSource.defaultConfig Pager(pagingConfig) { dataSource } } - private fun switchPager(newDataSource: MorphoFeedSource) { + private fun switchPager(newDataSource: MorphoFeedSource) { dataSource = newDataSource pager = Pager(MorphoDataSource.defaultConfig) { dataSource } } - override fun produceUpdates(events: Flow): Flow = events.map { event -> + override fun produceUpdates(events: Flow): Flow = events.map { event -> when(event) { - is FeedEvent.ComposePost -> UIUpdate.OpenComposer(event.post, event.role) + is Event.ComposePost -> UIUpdate.OpenComposer(event.post, event.role) is FeedEvent.Load -> { switchPager(event.descriptor.getDataSource(agent)) when(event.descriptor) { diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/ListPresenter.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/Presenters.kt similarity index 89% rename from Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/ListPresenter.kt rename to Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/Presenters.kt index 70670b1..d337efe 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/ListPresenter.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/Presenters.kt @@ -19,16 +19,21 @@ import kotlinx.coroutines.flow.map import org.koin.core.component.KoinComponent import org.koin.core.component.inject -abstract class Presenter: KoinComponent { +abstract class Presenter: KoinComponent { val agent: MorphoAgent by inject() val presenterScope = CoroutineScope(SupervisorJob() + Dispatchers.Default) + abstract fun produceUpdates(events: Flow): Flow +} + + + +abstract class PagedPresenter: Presenter() { abstract var pager: Pager - abstract fun produceUpdates(events: Flow): Flow } class UserListPresenter( val actor: AtIdentifier, -): Presenter() { +): PagedPresenter() { override var pager: Pager = run { val pagingConfig = MorphoDataSource.defaultConfig Pager(pagingConfig) { @@ -36,7 +41,7 @@ class UserListPresenter( } } - override fun produceUpdates(events: Flow): Flow = events.map { event -> + override fun produceUpdates(events: Flow): Flow = events.map { event -> when(event) { is FeedEvent.LoadLists -> AuthorFeedUpdate.Lists(actor, pager.flow.cachedIn(presenterScope)) else -> AuthorFeedUpdate.Error("Unknown event type: $event") @@ -81,7 +86,7 @@ class UserListFeedSource( class UserFeedsPresenter( val actor: AtIdentifier, -): Presenter() { +): PagedPresenter() { override var pager: Pager = run { val pagingConfig = MorphoDataSource.defaultConfig Pager(pagingConfig) { @@ -89,7 +94,7 @@ class UserFeedsPresenter( } } - override fun produceUpdates(events: Flow): Flow = events.map { event -> + override fun produceUpdates(events: Flow): Flow = events.map { event -> when(event) { is FeedEvent.LoadLists -> AuthorFeedUpdate.Feeds(actor, pager.flow.cachedIn(presenterScope)) else -> AuthorFeedUpdate.Error("Unknown event type: $event") diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/ProfilePresenters.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/ProfilePresenters.kt new file mode 100644 index 0000000..8c34735 --- /dev/null +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/ProfilePresenters.kt @@ -0,0 +1,232 @@ +package com.morpho.app.model.uidata + +import app.bsky.feed.GetActorFeedsQuery +import app.bsky.graph.GetListsQuery +import com.morpho.app.data.MorphoAgent +import com.morpho.app.model.bluesky.AuthorFilter +import com.morpho.app.model.bluesky.FeedDescriptor +import com.morpho.app.model.bluesky.toLabelService +import com.morpho.app.model.bluesky.toProfile +import com.morpho.app.model.uistate.ContentCardState +import com.morpho.app.model.uistate.ListsOrFeeds +import com.morpho.butterfly.Did +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.merge +import org.lighthousegames.logging.logging + +class MyProfilePresenter( + val profileState: ContentCardState.MyProfile, +): Presenter() { + + + val postsPresenter = FeedPresenter( + descriptor = FeedDescriptor.Author(profileState.profile.did, AuthorFilter.PostsNoReplies) + ) + val postsUpdates: Flow = postsPresenter.produceUpdates(profileState.events) + val postRepliesPresenter = FeedPresenter( + descriptor = FeedDescriptor.Author(profileState.profile.did, AuthorFilter.PostsWithReplies) + ) + val postRepliesUpdates: Flow = postRepliesPresenter.produceUpdates(profileState.events) + val mediaPresenter = FeedPresenter( + descriptor = FeedDescriptor.Author(profileState.profile.did, AuthorFilter.PostsWithMedia) + ) + val mediaUpdates: Flow = mediaPresenter.produceUpdates(profileState.events) + val likesPresenter = FeedPresenter( + descriptor = FeedDescriptor.Likes(profileState.profile.did) + ) + val likesUpdates: Flow = likesPresenter.produceUpdates(profileState.events) + val listsPresenter = UserListPresenter(profileState.profile.did) + val listsUpdates: Flow = listsPresenter.produceUpdates(profileState.events) + val feedsPresenter = UserFeedsPresenter(profileState.profile.did) + val feedsUpdates: Flow = feedsPresenter.produceUpdates(profileState.events) + + companion object { + val log = logging("ProfilePresenter") + suspend fun initialize( + agent: MorphoAgent, + ): ContentCardState.MyProfile? { + val id = agent.id ?: return null + val profile = agent.getProfile(id).getOrNull()?.toProfile() ?: return null + val hasFeeds = agent.api + .getActorFeeds(GetActorFeedsQuery(id, 1, null)).getOrNull()?.feeds?.isNotEmpty() + ?: false + val hasLists = agent.api + .getLists(GetListsQuery(id, 1, null)).getOrNull()?.lists?.isNotEmpty() ?: false + val maybeLabeler = agent.getLabelers(listOf(profile.did)) + .getOrNull()?.firstOrNull()?.toLabelService() + + return ContentCardState.MyProfile( + profile = profile, + lists = if (hasLists) ContentCardState.ProfileList( + profile = profile, + ListsOrFeeds.Lists, + ) else null, + feeds = if (hasFeeds) ContentCardState.ProfileList( + profile = profile, + ListsOrFeeds.Feeds, + ) else null, + labeler = if (maybeLabeler != null) ContentCardState.ProfileLabeler( + profile = maybeLabeler, + uri = maybeLabeler.uri, + ) else null, + ) + } + suspend fun create( + agent: MorphoAgent, + ): MyProfilePresenter? { + val state = initialize(agent) ?: return null + return MyProfilePresenter(state) + } + } + + + override fun produceUpdates(events: Flow): Flow { + val did = profileState.profile.did + val combined = merge(events, profileState.events) + val profileUpdates = combined.map { event -> + when (event) { + + is Event.ComposePost -> UIUpdate.OpenComposer(event.post, event.role) + + else -> { + log.d { "Unhandled event: $event" } + UIUpdate.NoOp + } + } + } as Flow + return merge( + profileUpdates, postsUpdates, postRepliesUpdates, mediaUpdates, + likesUpdates, listsUpdates, feedsUpdates + ) + } +} + +class ProfilePresenter( + val profileState: ContentCardState.FullProfile, +): Presenter() { + + + val postsPresenter = FeedPresenter( + descriptor = FeedDescriptor.Author(profileState.profile.did, AuthorFilter.PostsNoReplies) + ) + val postsUpdates: Flow = postsPresenter.produceUpdates(profileState.events) + val postRepliesPresenter = FeedPresenter( + descriptor = FeedDescriptor.Author(profileState.profile.did, AuthorFilter.PostsWithReplies) + ) + val postRepliesUpdates: Flow = postRepliesPresenter.produceUpdates(profileState.events) + val mediaPresenter = FeedPresenter( + descriptor = FeedDescriptor.Author(profileState.profile.did, AuthorFilter.PostsWithMedia) + ) + val mediaUpdates: Flow = mediaPresenter.produceUpdates(profileState.events) + val listsPresenter = UserListPresenter(profileState.profile.did) + val listsUpdates: Flow = listsPresenter.produceUpdates(profileState.events) + val feedsPresenter = UserFeedsPresenter(profileState.profile.did) + val feedsUpdates: Flow = feedsPresenter.produceUpdates(profileState.events) + + companion object { + val log = logging("ProfilePresenter") + suspend fun initialize( + agent: MorphoAgent, + actor: Did, + ): ContentCardState.FullProfile? { + val profile = agent.getProfile(actor).getOrNull()?.toProfile() ?: return null + val hasFeeds = agent.api + .getActorFeeds(GetActorFeedsQuery(actor, 1, null)).getOrNull()?.feeds?.isNotEmpty() + ?: false + val hasLists = agent.api + .getLists(GetListsQuery(actor, 1, null)).getOrNull()?.lists?.isNotEmpty() ?: false + val maybeLabeler = agent.getLabelers(listOf(profile.did)) + .getOrNull()?.firstOrNull()?.toLabelService() + + return ContentCardState.FullProfile( + profile = profile, + lists = if (hasLists) ContentCardState.ProfileList( + profile = profile, + ListsOrFeeds.Lists, + ) else null, + feeds = if (hasFeeds) ContentCardState.ProfileList( + profile = profile, + ListsOrFeeds.Feeds, + ) else null, + labeler = if (maybeLabeler != null) ContentCardState.ProfileLabeler( + profile = maybeLabeler, + uri = maybeLabeler.uri, + ) else null, + ) + } + suspend fun create( + agent: MorphoAgent, + actor: Did, + ): ProfilePresenter? { + val state = initialize(agent, actor) ?: return null + return ProfilePresenter(state) + } + } + + + override fun produceUpdates(events: Flow): Flow { + val did = profileState.profile.did + val combined = merge(events, profileState.events) + val profileUpdates = combined.map { event -> + when (event) { + is ProfileEvent.Block -> if(did == event.subject) { + agent.block(event.subject) + ActorUpdate.Blocked + } else UIUpdate.NoOp + is ProfileEvent.Follow -> if(did == event.subject) { + agent.follow(event.subject) + ActorUpdate.Followed + } else UIUpdate.NoOp + is ProfileEvent.Mute -> if(did == event.subject) { + agent.mute(event.subject) + ActorUpdate.Muted + } else UIUpdate.NoOp + is ProfileEvent.ReportAccount -> if(did == event.subject) { + ActorUpdate.Reported + } else UIUpdate.NoOp + is ProfileEvent.Unblock -> if(profileState.profile.block?.uri == event.uri) { + agent.unblock(event.uri) + ActorUpdate.Unblocked + } else UIUpdate.NoOp + is ProfileEvent.Unfollow -> if(profileState.profile.following?.uri == event.uri) { + agent.deleteFollow(event.uri) + ActorUpdate.Unfollowed + } else UIUpdate.NoOp + is ProfileEvent.Unmute -> if(profileState.profile.mutedByMe) { + agent.unmute(event.subject) + ActorUpdate.Unmuted + } else UIUpdate.NoOp + is Event.ComposePost -> UIUpdate.OpenComposer(event.post, event.role) + is LabelerEvent.LikeLabeler -> { + agent.like(event.like) + ActorUpdate.Liked + } + is LabelerEvent.SetLabelPref -> { + // TODO: update labeler + UIUpdate.NoOp + } + is LabelerEvent.Subscribe -> { + agent.addLabeler(event.did) + UIUpdate.NoOp + } + is LabelerEvent.UnlikeLabeler -> if (profileState.labeler?.profile?.likeUri == event.uri) { + agent.deleteLike(event.uri) + ActorUpdate.Unliked + } else UIUpdate.NoOp + is LabelerEvent.Unsubscribe -> { + agent.removeLabeler(event.did) + UIUpdate.NoOp + } + else -> { + log.d { "Unhandled event: $event" } + UIUpdate.NoOp + } + } + } as Flow + return merge( + profileUpdates, postsUpdates, postRepliesUpdates, + mediaUpdates, listsUpdates, feedsUpdates + ) + } +} \ No newline at end of file diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/UIUpdate.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/UIUpdate.kt index 014c234..62d259f 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/UIUpdate.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/UIUpdate.kt @@ -12,6 +12,7 @@ sealed interface UIUpdate { val role: ComposerRole, ): UIUpdate data object Empty: UIUpdate + data object NoOp: UIUpdate } sealed interface SearchUpdate: UIUpdate { @@ -42,15 +43,15 @@ sealed interface FeedUpdate: UIUpdate { data class Error(val error: String): FeedUpdate - data class Feed( + data class Feed( val info: FeedSourceInfo, - val feed: Flow>, - ): FeedUpdate + val feed: Flow>, + ): FeedUpdate - data class Peek( + data class Peek( val info: FeedSourceInfo, - val post: Flow, - ): FeedUpdate + val post: Flow, + ): FeedUpdate } sealed interface AuthorFeedUpdate: UIUpdate { @@ -59,15 +60,15 @@ sealed interface AuthorFeedUpdate: UIUpdate { data class Error(val error: String): AuthorFeedUpdate - data class Feed( + data class Feed( val actor: AtIdentifier, val filter: AuthorFilter, - val feed: Flow>, + val feed: Flow>, ): AuthorFeedUpdate - data class Likes( + data class Likes( val actor: AtIdentifier, - val feed: Flow>, + val feed: Flow>, ): AuthorFeedUpdate data class Lists( @@ -90,4 +91,27 @@ sealed interface ThreadUpdate: UIUpdate { data class Thread( val results: Flow, ): ThreadUpdate +} + +sealed interface MyProfileUpdate: UIUpdate { + data object Empty: MyProfileUpdate + + data class Error(val error: String): MyProfileUpdate + data object Editing: MyProfileUpdate + data object ExitEditing: MyProfileUpdate +} + +sealed interface ActorUpdate: UIUpdate { + data object Empty : ActorUpdate + + data class Error(val error: String) : ActorUpdate + data object Followed : ActorUpdate + data object Unfollowed : ActorUpdate + data object Muted : ActorUpdate + data object Unmuted : ActorUpdate + data object Blocked : ActorUpdate + data object Unblocked : ActorUpdate + data object Reported : ActorUpdate + data object Liked : ActorUpdate + data object Unliked : ActorUpdate } \ No newline at end of file diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uistate/ContentCardState.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uistate/ContentCardState.kt index 04f0be8..5268882 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uistate/ContentCardState.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uistate/ContentCardState.kt @@ -1,46 +1,43 @@ package com.morpho.app.model.uistate import com.morpho.app.model.bluesky.* -import com.morpho.app.model.uidata.MorphoData +import com.morpho.app.model.uidata.* +import com.morpho.app.util.MutableSharedFlowSerializer +import com.morpho.app.util.MutableStateFlowSerializer import com.morpho.butterfly.AtUri +import kotlinx.coroutines.channels.BufferOverflow +import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asStateFlow import kotlinx.serialization.Serializable @Suppress("unused") @Serializable -sealed interface ContentCardState { +sealed interface ContentCardState { val uri: AtUri - val feed: MorphoData - val hasNewPosts: Boolean - val loadingState: ContentLoadingState + @Serializable(with = MutableSharedFlowSerializer::class) + val events: MutableSharedFlow + @Serializable(with = MutableStateFlowSerializer::class) + val updates: MutableStateFlow @Serializable - data class Skyline( - override val feed: MorphoData, - override val loadingState: ContentLoadingState = ContentLoadingState.Loading, - override val hasNewPosts: Boolean = false, - ) : ContentCardState, SkylineContentState { - override val uri: AtUri = feed.uri - } + data class Skyline( + override val uri: AtUri, + override val events: MutableSharedFlow = MutableSharedFlow( + extraBufferCapacity = 10, + onBufferOverflow = BufferOverflow.DROP_OLDEST), + override val updates: MutableStateFlow = MutableStateFlow(FeedUpdate.Empty), + ) : ContentCardState @Serializable data class PostThread( val post: BskyPost, - val thread: StateFlow = MutableStateFlow(null).asStateFlow(), - override val loadingState: ContentLoadingState = ContentLoadingState.Loading, - override val hasNewPosts: Boolean = false, - - ): ContentCardState, PostThreadContentState { - + override val events: MutableSharedFlow = MutableSharedFlow( + extraBufferCapacity = 10, + onBufferOverflow = BufferOverflow.DROP_OLDEST), + override val updates: MutableStateFlow = MutableStateFlow(UIUpdate.Empty), + ): ContentCardState { override val uri: AtUri = post.uri - override val feed: MorphoData = MorphoData( - uri = post.uri, - title = "${post.author.displayName}'s Thread", - ) - init { require(post.uri.atUri.contains("app.bsky.feed.post")) { "Invalid post uri: $uri" @@ -49,69 +46,103 @@ sealed interface ContentCardState { } @Serializable - data class ProfileTimeline( + data class ProfileTimeline( + val profile: DetailedProfile, + val filter: AuthorFilter? = AuthorFilter.PostsWithReplies, + override val events: MutableSharedFlow = MutableSharedFlow( + extraBufferCapacity = 10, + onBufferOverflow = BufferOverflow.DROP_OLDEST), + override val updates: MutableStateFlow = MutableStateFlow(FeedUpdate.Empty), + ) : ContentCardState { + override val uri: AtUri = when(filter) { + AuthorFilter.PostsWithReplies -> AtUri.profileRepliesUri(profile.did) + AuthorFilter.PostsNoReplies -> AtUri.profilePostsUri(profile.did) + AuthorFilter.PostsAuthorThreads -> AtUri.profileRepliesUri(profile.did) + AuthorFilter.PostsWithMedia -> AtUri.profileMediaUri(profile.did) + null -> AtUri.profileLikesUri(profile.did) + } + } + + data class ProfileList( val profile: Profile, - override val feed: MorphoData, - override val loadingState: ContentLoadingState = ContentLoadingState.Loading, - override val hasNewPosts: Boolean = false, - ) : ContentCardState, SkylineContentState { - override val uri: AtUri = feed.uri - /*init { - require( - AtUri.ProfilePostsUriRegex.matches(uri.atUri) || - AtUri.ProfileRepliesUriRegex.matches(uri.atUri) || - AtUri.ProfileMediaUriRegex.matches(uri.atUri) || - AtUri.ProfileLikesUriRegex.matches(uri.atUri) || - AtUri.ProfileFeedsListUriRegex.matches(uri.atUri) || - AtUri.ProfileUserListsUriRegex.matches(uri.atUri) || - AtUri.ProfileModServiceUriRegex.matches(uri.atUri) || - uri == AtUri.MY_PROFILE_URI - ) { "Invalid profile feed uri: $uri" } - }*/ + val listsOrFeeds: ListsOrFeeds = ListsOrFeeds.Lists, + override val events: MutableSharedFlow = MutableSharedFlow( + extraBufferCapacity = 10, + onBufferOverflow = BufferOverflow.DROP_OLDEST), + override val updates: MutableStateFlow = MutableStateFlow(UIUpdate.Empty), + ): ContentCardState { + override val uri: AtUri = when(listsOrFeeds) { + ListsOrFeeds.Lists -> AtUri.profileUserListsUri(profile.did) + ListsOrFeeds.Feeds -> AtUri.profileFeedsListUri(profile.did) + } } - @Serializable - data class FullProfile( - val profile: T, - val postsState: StateFlow?> = MutableStateFlow(null).asStateFlow(), - val postRepliesState: StateFlow?> = MutableStateFlow(null).asStateFlow(), - val mediaState: StateFlow?> = MutableStateFlow(null).asStateFlow(), - val likesState: StateFlow?> = MutableStateFlow(null).asStateFlow(), - val listsState: StateFlow?> = MutableStateFlow(null).asStateFlow(), - val feedsState: StateFlow?> = MutableStateFlow(null).asStateFlow(), - val modServiceState: StateFlow?> = MutableStateFlow(null).asStateFlow(), - override val loadingState: ContentLoadingState = ContentLoadingState.Loading, - override val hasNewPosts: Boolean = false, - ) : ContentCardState { - override val uri: AtUri = - when(profile) { - is DetailedProfile -> AtUri.profileUri(profile.did) - is BskyLabelService -> profile.uri - else -> throw IllegalArgumentException("Invalid profile type: $profile") - } - override val feed: MorphoData = MorphoData( - uri = uri, - title = profile.displayName.orEmpty(), - ) + data class ProfileLabeler( + val profile: BskyLabelService, + override val uri: AtUri, + override val events: MutableSharedFlow = MutableSharedFlow( + extraBufferCapacity = 10, + onBufferOverflow = BufferOverflow.DROP_OLDEST), + override val updates: MutableStateFlow = MutableStateFlow(UIUpdate.Empty), + ): ContentCardState + + data class FullProfile( + val profile: DetailedProfile, + val lists: ProfileList? = null, + val feeds: ProfileList? = null, + val labeler: ProfileLabeler? = null, + val posts: ProfileTimeline = ProfileTimeline(profile, AuthorFilter.PostsNoReplies), + val postReplies: ProfileTimeline = ProfileTimeline(profile, AuthorFilter.PostsWithReplies), + val media: ProfileTimeline = ProfileTimeline(profile, AuthorFilter.PostsWithMedia), + override val events: MutableSharedFlow = MutableSharedFlow( + extraBufferCapacity = 10, + onBufferOverflow = BufferOverflow.DROP_OLDEST), + override val updates: MutableStateFlow = MutableStateFlow(UIUpdate.Empty), + ) : ContentCardState { + override val uri: AtUri = AtUri.profileUri(profile.did) + } + data class MyProfile( + val profile: DetailedProfile, + val lists: ProfileList? = null, + val feeds: ProfileList? = null, + val labeler: ProfileLabeler? = null, + val posts: ProfileTimeline = ProfileTimeline(profile, AuthorFilter.PostsNoReplies), + val postReplies: ProfileTimeline = ProfileTimeline(profile, AuthorFilter.PostsWithReplies), + val media: ProfileTimeline = ProfileTimeline(profile, AuthorFilter.PostsWithMedia), + val likes: ProfileTimeline = ProfileTimeline(profile, null), + override val events: MutableSharedFlow = MutableSharedFlow( + extraBufferCapacity = 10, + onBufferOverflow = BufferOverflow.DROP_OLDEST), + override val updates: MutableStateFlow = MutableStateFlow(UIUpdate.Empty), + ) : ContentCardState { + override val uri: AtUri = AtUri.profileUri(profile.did) + } - val feedsLoaded: Boolean - get() = postsState.value?.loadingState == ContentLoadingState.Idle && - postRepliesState.value?.loadingState == ContentLoadingState.Idle && - mediaState.value?.loadingState == ContentLoadingState.Idle && - ((likesState.value == null) || (likesState.value?.loadingState == ContentLoadingState.Idle)) + @Serializable + data class UserListPage( + val list: BskyList, + override val events: MutableSharedFlow = MutableSharedFlow( + extraBufferCapacity = 10, + onBufferOverflow = BufferOverflow.DROP_OLDEST), + override val updates: MutableStateFlow = MutableStateFlow(UIUpdate.Empty), + ) : ContentCardState { + override val uri: AtUri = list.uri } @Serializable - data class UserList( + data class FeedPage( val list: BskyList, - override val loadingState: ContentLoadingState = ContentLoadingState.Loading, - override val hasNewPosts: Boolean = false, - ) : ContentCardState { + override val events: MutableSharedFlow = MutableSharedFlow( + extraBufferCapacity = 10, + onBufferOverflow = BufferOverflow.DROP_OLDEST), + override val updates: MutableStateFlow = MutableStateFlow(UIUpdate.Empty), + ) : ContentCardState { override val uri: AtUri = list.uri - override val feed: MorphoData = MorphoData( - uri = list.uri, - title = list.name, - ) } +} + +enum class ListsOrFeeds { + Lists, + Feeds } \ No newline at end of file diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uistate/NotificationsState.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uistate/NotificationsState.kt index 2d64fc2..492f65e 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uistate/NotificationsState.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uistate/NotificationsState.kt @@ -1,41 +1,17 @@ package com.morpho.app.model.uistate import androidx.compose.runtime.Immutable -import com.morpho.app.model.bluesky.NotificationsList -import com.morpho.app.model.bluesky.NotificationsListItem -import com.morpho.app.model.uidata.AtCursor -import com.morpho.app.model.uidata.filterNotifications -import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.map import kotlinx.serialization.Serializable import org.koin.core.component.KoinComponent - @Serializable data class NotificationsUIState( - private val notificationsList: StateFlow = MutableStateFlow(NotificationsList()), - val filterState: StateFlow = MutableStateFlow(NotificationsFilterState()), + val filterState: MutableStateFlow = MutableStateFlow(NotificationsFilterState()), val showPosts: Boolean = true, override val loadingState: UiLoadingState = UiLoadingState.Loading, -): KoinComponent, UiState { - - val cursor:AtCursor - get() = notificationsList.value.cursor - - val notifications: Flow> - get() = notificationsList.map { - filterNotifications(it.notificationsList, filterState.value) - } - - //@NativeCoroutines - val numberUnread: Flow - get() = notifications.map { items -> items.filterNot { it.isRead }.size } - -} - +): KoinComponent, UiState @Immutable @Serializable data class NotificationsFilterState( diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uistate/PostThreadContentState.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uistate/PostThreadContentState.kt index 91789c1..ed56b17 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uistate/PostThreadContentState.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uistate/PostThreadContentState.kt @@ -1,9 +1,2 @@ package com.morpho.app.model.uistate - -interface PostThreadContentState { - val hasNewPosts: Boolean - val loadingState: ContentLoadingState - val isLoading: Boolean - get() = loadingState == ContentLoadingState.Loading -} \ No newline at end of file diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uistate/SkylineState.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uistate/SkylineState.kt index 615c192..d71b309 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uistate/SkylineState.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uistate/SkylineState.kt @@ -1,27 +1,9 @@ package com.morpho.app.model.uistate import androidx.compose.runtime.Immutable -import com.morpho.app.model.bluesky.MorphoDataItem -import com.morpho.app.model.uidata.MorphoData import kotlinx.serialization.Serializable -interface SkylineContentState { - val hasNewPosts: Boolean - val feed: MorphoData - val loadingState: ContentLoadingState - val isLoading: Boolean - get() = loadingState == ContentLoadingState.Loading -} - -@Immutable -@Serializable -data class SkylineState( - override val feed: MorphoData, - override val loadingState: ContentLoadingState = ContentLoadingState.Loading, - override val hasNewPosts: Boolean = false, -): SkylineContentState - @Immutable @Serializable enum class FeedType { diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/base/BaseScreenModel.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/base/BaseScreenModel.kt index 8bab90d..6f31804 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/base/BaseScreenModel.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/base/BaseScreenModel.kt @@ -3,21 +3,26 @@ package com.morpho.app.screens.base import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue +import app.cash.paging.Pager +import app.cash.paging.cachedIn import cafe.adriel.voyager.core.model.ScreenModel +import cafe.adriel.voyager.core.model.screenModelScope import com.morpho.app.data.MorphoAgent -import com.morpho.app.model.uidata.BskyNotificationService -import com.morpho.app.model.uidata.ContentLabelService -import com.morpho.app.model.uidata.Event +import com.morpho.app.model.bluesky.BskyPost +import com.morpho.app.model.bluesky.NotificationsSource +import com.morpho.app.model.bluesky.toPost +import com.morpho.app.model.uidata.* +import com.morpho.butterfly.AtUri import com.morpho.butterfly.Did import kotlinx.coroutines.channels.BufferOverflow -import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.* +import kotlinx.coroutines.launch import org.koin.core.component.KoinComponent import org.koin.core.component.inject import org.lighthousegames.logging.logging open class BaseScreenModel : ScreenModel, KoinComponent { val agent: MorphoAgent by inject() - val notifService: BskyNotificationService by inject() val labelService: ContentLabelService by inject() @@ -30,6 +35,12 @@ open class BaseScreenModel : ScreenModel, KoinComponent { val isLoggedIn: Boolean get() = agent.isLoggedIn + val notificationsRaw = Pager(NotificationsSource.defaultConfig) { + NotificationsSource() + }.flow.cachedIn(screenModelScope) + + + companion object { val log = logging() } @@ -41,4 +52,53 @@ open class BaseScreenModel : ScreenModel, KoinComponent { suspend fun sendGlobalEvent(event: Event) { globalEvents.emit(event) } + + fun getProfilePresenter( + id: Did, + init: Boolean = false, + eventStream: Flow = globalEvents + ): Flow>> = flow { + val presenter = ProfilePresenter.create(agent, id)?: return@flow + if(!init) emit(Pair(presenter, MutableStateFlow(UIUpdate.Empty))) + else { + val stateFlow = MutableStateFlow(UIUpdate.Empty) + emit(Pair(presenter, stateFlow)) + screenModelScope.launch { + stateFlow.emitAll(presenter.produceUpdates(eventStream)) + } + } + } + + fun getMyProfilePresenter( + init: Boolean = false, + eventStream: Flow = globalEvents + ): Flow>> = flow { + val presenter = MyProfilePresenter.create(agent)?: return@flow + if(!init) emit(Pair(presenter, MutableStateFlow(UIUpdate.Empty))) + else { + val stateFlow = MutableStateFlow(UIUpdate.Empty) + emit(Pair(presenter, stateFlow)) + screenModelScope.launch { + stateFlow.emitAll(presenter.produceUpdates(eventStream)) + } + } + } + + fun unreadNotificationsCount() = flow { + emit(agent.unreadNotificationsCount().getOrDefault(0)) + }.stateIn(screenModelScope, SharingStarted.WhileSubscribed(), 0L) + + fun markNotificationsAsRead() = screenModelScope.launch { + agent.updateSeenNotifications() + globalEvents.emit(Event.UpdateSeenNotifications()) + } + + suspend fun getPost(uri: AtUri): Result { + return agent.getPosts(listOf(uri)).map { + if(it.isEmpty()) Result.failure(Exception("Post not found")) + else Result.success(it.first().toPost()) + }.getOrDefault(Result.failure(Exception("Post not found"))) + } + + } \ No newline at end of file diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/base/tabbed/NavigationTabs.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/base/tabbed/NavigationTabs.kt index bd3339c..69cb416 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/base/tabbed/NavigationTabs.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/base/tabbed/NavigationTabs.kt @@ -8,9 +8,7 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.* import androidx.compose.ui.Modifier import cafe.adriel.voyager.core.annotation.ExperimentalVoyagerApi -import cafe.adriel.voyager.core.lifecycle.LifecycleEffectOnce import cafe.adriel.voyager.core.model.rememberNavigatorScreenModel -import cafe.adriel.voyager.core.model.screenModelScope import cafe.adriel.voyager.core.screen.Screen import cafe.adriel.voyager.core.screen.ScreenKey import cafe.adriel.voyager.core.screen.uniqueScreenKey @@ -26,12 +24,11 @@ import com.morpho.app.screens.thread.ThreadTopBar import com.morpho.app.screens.thread.ThreadViewContent import com.morpho.app.ui.common.LoadingCircle import com.morpho.app.ui.common.TabbedScreenScaffold -import com.morpho.butterfly.AtIdentifier import com.morpho.butterfly.AtUri +import com.morpho.butterfly.Did import dev.icerock.moko.parcelize.Parcelable import dev.icerock.moko.parcelize.Parcelize import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.launch import kotlinx.serialization.Contextual import kotlinx.serialization.Serializable import kotlinx.serialization.Transient @@ -187,7 +184,7 @@ data object NotificationsTab: TabScreen { @Serializable @Immutable data class ProfileTab( - val id: AtIdentifier, + val id: Did, ): TabScreen { override val key: ScreenKey @@ -199,7 +196,21 @@ data class ProfileTab( @OptIn(ExperimentalMaterial3Api::class) @Composable override fun Content() { - TabbedProfileContent(id) + val sm = LocalNavigator.currentOrThrow.rememberNavigatorScreenModel { TabbedMainScreenModel() } + val eventStream = sm.globalEvents + val profilePresenter by sm.getProfilePresenter(id).collectAsState(null) + val myProfilePresenter by sm.getMyProfilePresenter().collectAsState(null) + if(profilePresenter != null && myProfilePresenter != null) { + val presenter = profilePresenter!!.first + val updates = profilePresenter!!.second + + val myProfileState = myProfilePresenter!!.first.profileState + TabbedProfileContent( + profileState = presenter.profileState, + myProfileState = myProfileState, + eventCallback = { eventStream.tryEmit(it) } + ) + } } @@ -235,9 +246,6 @@ data class ThreadTab( val navigator = LocalNavigator.currentOrThrow val sm = navigator.rememberNavigatorScreenModel { TabbedMainScreenModel() } var threadState: StateFlow? by remember { mutableStateOf(null)} - LifecycleEffectOnce { - sm.screenModelScope.launch { threadState = sm.loadThread(uri) } - } if(threadState != null) { ThreadViewContent(threadState!!, navigator) } else { @@ -277,7 +285,19 @@ data object MyProfileTab: TabScreen { @OptIn(ExperimentalMaterial3Api::class) @Composable override fun Content() { - TabbedProfileContent() + val sm = LocalNavigator.currentOrThrow.rememberNavigatorScreenModel { TabbedMainScreenModel() } + val eventStream = sm.globalEvents + val myProfilePresenter by sm.getMyProfilePresenter().collectAsState(null) + if(myProfilePresenter != null) { + + val myProfileState = myProfilePresenter!!.first.profileState + TabbedProfileContent( + ownProfile = true, + profileState = null, + myProfileState = myProfileState, + eventCallback = { eventStream.tryEmit(it) } + ) + } } diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/base/tabbed/TabbedBaseScreen.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/base/tabbed/TabbedBaseScreen.kt index b56639a..f4c7a3f 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/base/tabbed/TabbedBaseScreen.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/base/tabbed/TabbedBaseScreen.kt @@ -22,8 +22,6 @@ import cafe.adriel.voyager.navigator.currentOrThrow import cafe.adriel.voyager.navigator.tab.Tab import cafe.adriel.voyager.navigator.tab.TabNavigator import cafe.adriel.voyager.navigator.tab.TabOptions -import com.morpho.app.model.uidata.BskyDataService -import com.morpho.app.model.uidata.BskyNotificationService import com.morpho.app.ui.common.SlideTabTransition import com.morpho.app.ui.theme.roundedTopR import io.ktor.util.reflect.instanceOf @@ -110,11 +108,9 @@ fun TabNavigationItem( } is HomeTab -> { - val dataService = koinInject() - val hasNew by dataService.checkIfNewTimeline().collectAsState(false) BadgedBox( badge = { - if (hasNew) { + if (false) { /// TODO: put this back in later Badge( modifier = Modifier.size(4.dp), containerColor = MaterialTheme.colorScheme.secondary diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/login/LoginScreenModel.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/login/LoginScreenModel.kt index 32f6b35..bd5e738 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/login/LoginScreenModel.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/login/LoginScreenModel.kt @@ -39,7 +39,7 @@ class LoginScreenModel: BaseScreenModel() { if(checkValidUrl(service) != null) Server.CustomServer(service) else Server.BlueskySocial } screenModelScope.launch { - api.makeLoginRequest(credentials, server).onSuccess { + agent.login(credentials, server).onSuccess { loginState = loginState.copy( loadingState = UiLoadingState.Idle, authState = AuthState.Success(it) diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/main/MainScreenModel.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/main/MainScreenModel.kt index 518e00f..2edbc84 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/main/MainScreenModel.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/main/MainScreenModel.kt @@ -6,15 +6,15 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import app.bsky.actor.SavedFeed import cafe.adriel.voyager.core.model.screenModelScope -import com.morpho.app.model.bluesky.* -import com.morpho.app.model.uidata.ContentCardMapEntry +import com.morpho.app.model.bluesky.DetailedProfile +import com.morpho.app.model.bluesky.FeedSourceInfo +import com.morpho.app.model.bluesky.toFeedSourceInfo +import com.morpho.app.model.bluesky.toProfile import com.morpho.app.model.uidata.FeedEvent import com.morpho.app.model.uidata.FeedPresenter -import com.morpho.app.model.uidata.FeedUpdate +import com.morpho.app.model.uistate.ContentCardState import com.morpho.app.screens.base.BaseScreenModel -import kotlinx.coroutines.channels.BufferOverflow -import kotlinx.coroutines.flow.MutableSharedFlow -import kotlinx.coroutines.flow.MutableStateFlow +import com.morpho.butterfly.AtUri import kotlinx.coroutines.flow.emitAll import kotlinx.coroutines.flow.filterIsInstance import kotlinx.coroutines.launch @@ -27,12 +27,12 @@ open class MainScreenModel: BaseScreenModel() { protected set val feedSources = mutableStateListOf() - val feedPresenters = mutableMapOf>() + val feedPresenters = mutableMapOf>() val pinnedFeeds: List get() = agent.prefs.saved.filter { it.pinned } - val feedStates = mutableMapOf>() + val feedStates = mutableMapOf>() var initialized = false @@ -45,15 +45,10 @@ open class MainScreenModel: BaseScreenModel() { userProfile = userDid?.let { agent.getProfile(it).getOrNull()?.toProfile() } feedSources.addAll(pinnedFeeds.mapNotNull { feed -> feed.toFeedSourceInfo(agent).getOrNull() }) feedPresenters.putAll(feedSources.map { source -> - source to FeedPresenter(source.feedDescriptor) + source.uri to FeedPresenter(source.feedDescriptor) }) feedStates.putAll(feedSources.map { source -> - source to ContentCardMapEntry.Feed( - source.uri, source.displayName?:"", - events = MutableSharedFlow( - extraBufferCapacity = 10, - onBufferOverflow = BufferOverflow.DROP_OLDEST), - updates = MutableStateFlow(FeedUpdate.Empty)) + source.uri to ContentCardState.Skyline(source.uri) }) diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/main/tabbed/TabbedHomeView.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/main/tabbed/TabbedHomeView.kt index c2efefe..48a9cbb 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/main/tabbed/TabbedHomeView.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/main/tabbed/TabbedHomeView.kt @@ -11,8 +11,6 @@ import androidx.compose.animation.slideInHorizontally import androidx.compose.animation.slideOutHorizontally import androidx.compose.animation.togetherWith import androidx.compose.foundation.layout.* -import androidx.compose.foundation.lazy.LazyListState -import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material3.* import androidx.compose.material3.TabRowDefaults.tabIndicatorOffset import androidx.compose.runtime.* @@ -22,7 +20,6 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.dp import cafe.adriel.voyager.core.annotation.ExperimentalVoyagerApi -import cafe.adriel.voyager.core.lifecycle.LifecycleEffectOnce import cafe.adriel.voyager.core.screen.ScreenKey import cafe.adriel.voyager.core.screen.uniqueScreenKey import cafe.adriel.voyager.core.stack.StackEvent @@ -35,9 +32,8 @@ import cafe.adriel.voyager.navigator.tab.TabOptions import cafe.adriel.voyager.transitions.ScreenTransition import cafe.adriel.voyager.transitions.ScreenTransitionContent import coil3.annotation.ExperimentalCoilApi -import com.morpho.app.model.bluesky.MorphoDataItem +import com.morpho.app.model.uidata.Event import com.morpho.app.model.uistate.ContentCardState -import com.morpho.app.model.uistate.UiLoadingState import com.morpho.app.screens.base.tabbed.TabScreen import com.morpho.app.ui.common.LoadingCircle import com.morpho.app.ui.common.TabbedScreenScaffold @@ -46,7 +42,6 @@ import com.morpho.app.ui.elements.AvatarShape import com.morpho.app.ui.elements.OutlinedAvatar import dev.icerock.moko.parcelize.Parcelable import dev.icerock.moko.parcelize.Parcelize -import kotlinx.coroutines.flow.StateFlow import kotlinx.serialization.Serializable import org.jetbrains.compose.resources.ExperimentalResourceApi import kotlin.math.max @@ -54,13 +49,10 @@ import kotlin.math.min import cafe.adriel.voyager.navigator.tab.Tab as NavTab @Composable -public fun CurrentSkylineScreen( +public fun CurrentSkylineScreen( sm: TabbedMainScreenModel, paddingValues: PaddingValues, - state: StateFlow>?, - listState: LazyListState = rememberLazyListState( - initialFirstVisibleItemIndex = state?.value?.feed?.cursor?.scroll ?: 0 - ), + state: ContentCardState?, modifier: Modifier ) { val navigator = LocalNavigator.currentOrThrow @@ -71,7 +63,6 @@ public fun CurrentSkylineScreen( sm = sm, paddingValues = paddingValues, state = state, - listState = listState, modifier = modifier ) } @@ -81,22 +72,20 @@ public fun CurrentSkylineScreen( abstract class SkylineTab: NavTab { @Composable - abstract fun Content( + abstract fun Content( sm: TabbedMainScreenModel, paddingValues: PaddingValues, - state: StateFlow>?, - listState: LazyListState, + state: ContentCardState?, modifier: Modifier ) @OptIn(ExperimentalVoyagerApi::class) @Composable final override fun Content() = - Content(TabbedMainScreenModel(),PaddingValues(0.dp),null, rememberLazyListState(), Modifier) + Content(TabbedMainScreenModel(),PaddingValues(0.dp), null, Modifier) } -@Suppress("UNCHECKED_CAST") @OptIn(ExperimentalAnimationApi::class, ExperimentalMaterial3Api::class, ExperimentalVoyagerApi::class ) @@ -108,25 +97,21 @@ fun TabScreen.TabbedHomeView( val navigator = LocalNavigator.currentOrThrow - var selectedTabIndex by rememberSaveable { mutableIntStateOf(0) } + var selectedTabIndex by rememberSaveable { mutableIntStateOf(sm.timelineIndex) } + - LifecycleEffectOnce { - sm.initTabs() - } val tabs = remember( - sm.tabFlow.value, sm.uiState.loadingState, sm.uiState.tabs.value.size + sm.tabs, sm.loaded, sm.tabs.size ) { - List(sm.uiState.tabs.value.size) { index -> + List(sm.tabs.size) { index -> HomeSkylineTab( index = index.toUShort(), - title = sm.uiState.tabs.value[index].title, - avatar = sm.uiState.tabs.value[index].avatar, + title = sm.tabs[index].title, + avatar = sm.tabs[index].avatar, ) } } - val tabsCreated = remember(tabs.size, sm.uiState.loadingState) { - tabs.isNotEmpty() && sm.uiState.loadingState == UiLoadingState.Idle - } + val tabsCreated by derivedStateOf { sm.loaded } if (tabsCreated) { Navigator( tabs.first(), @@ -134,10 +119,7 @@ fun TabScreen.TabbedHomeView( //disposeNestedNavigators = false, ) ) { nav -> - val listState = rememberLazyListState( - initialFirstVisibleItemIndex = - sm.uiState.tabStates[selectedTabIndex].value.feed.cursor.scroll - ) + val tabUri = sm.uriForTab(selectedTabIndex) TabbedScreenScaffold( navBar = { navBar(navigator) }, topContent = { @@ -153,20 +135,15 @@ fun TabScreen.TabbedHomeView( } else nav.replace(tabs[index]) } else if(index > selectedTabIndex) nav.push(tabs[index]) selectedTabIndex = index - sm.refreshTab( - index, - sm.uiState.tabStates[index].value.feed.cursor - .copy(scroll = listState.firstVisibleItemIndex) - ) } ) }, content = { insets, state -> - SkylineTabTransition(nav, sm, insets, state, listState) + SkylineTabTransition(nav, sm, insets, state) }, modifier = Modifier, - state = sm.uiState.tabStates.getOrNull(selectedTabIndex) as StateFlow>? + state = sm.feedStates.get(tabUri) as ContentCardState.Skyline? ) } @@ -176,21 +153,18 @@ fun TabScreen.TabbedHomeView( @OptIn(ExperimentalVoyagerApi::class) @Composable -fun SkylineTabTransition( +fun SkylineTabTransition( navigator: Navigator, sm: TabbedMainScreenModel, insets: PaddingValues = PaddingValues(0.dp), - state: StateFlow>?, - listState: LazyListState = rememberLazyListState( - initialFirstVisibleItemIndex = state?.value?.feed?.cursor?.scroll ?: 0 - ), + state: ContentCardState?, modifier: Modifier = Modifier, animationSpec: FiniteAnimationSpec = spring( stiffness = Spring.StiffnessMediumLow, visibilityThreshold = IntOffset.VisibilityThreshold ), content: ScreenTransitionContent = { - CurrentSkylineScreen(sm, insets, state, listState, modifier) + CurrentSkylineScreen(sm, insets, state, modifier) } ) { ScreenTransition( @@ -275,21 +249,20 @@ data class HomeSkylineTab @OptIn(ExperimentalVoyagerApi::class) constructor( @OptIn(ExperimentalMaterial3Api::class, ExperimentalVoyagerApi::class) @Composable - override fun Content( + override fun Content( sm: TabbedMainScreenModel, paddingValues: PaddingValues, - state: StateFlow>?, - listState: LazyListState, + state: ContentCardState?, modifier: Modifier ) { - + if(state == null) return TabbedSkylineFragment( - sm, state, paddingValues, - refresh = { cursor -> - sm.refreshTab(index.toInt(), cursor) - }, - listState = listState, + paddingValues = paddingValues, + isProfileFeed = false, + feedUpdate = state.updates, + uiEvents = sm.globalEvents, ) + } override val key: ScreenKey diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/main/tabbed/TabbedMainScreenModel.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/main/tabbed/TabbedMainScreenModel.kt index 2a9bd44..52ff1f8 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/main/tabbed/TabbedMainScreenModel.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/main/tabbed/TabbedMainScreenModel.kt @@ -1,9 +1,13 @@ package com.morpho.app.screens.main.tabbed +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateListOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue import app.bsky.actor.FeedType import cafe.adriel.voyager.core.model.screenModelScope +import com.morpho.app.model.bluesky.toContentCardMapEntry import com.morpho.app.model.uidata.ContentCardMapEntry -import com.morpho.app.model.uidata.FeedEvent import com.morpho.app.screens.main.MainScreenModel import com.morpho.butterfly.AtUri import kotlinx.coroutines.delay @@ -13,15 +17,16 @@ import org.lighthousegames.logging.logging class TabbedMainScreenModel : MainScreenModel() { - private val _tabs = mutableListOf>() - val tabs: List> - get() = _tabs.toList() + val tabs = mutableStateListOf() val timelineIndex = agent.prefs.timelineIndex ?: agent.prefs.saved.indexOfFirst { it.type == FeedType.TIMELINE }.let { if(it == -1) 0 else it } val lastPinnedIndex = agent.prefs.saved.indexOfLast { it.pinned } + var loaded by mutableStateOf(false) + + companion object { val log = logging("TabbedMainScreenModel") } @@ -33,7 +38,7 @@ class TabbedMainScreenModel : MainScreenModel() { } for(i in 0 .. lastPinnedIndex) { val source = feedSources[i] - feedStates[source]?.let { _tabs.add(it) } + tabs.add(source.toContentCardMapEntry()) } } diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/notifications/NotificationsView.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/notifications/NotificationsView.kt index 226863c..c3b4bf9 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/notifications/NotificationsView.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/notifications/NotificationsView.kt @@ -1,8 +1,6 @@ package com.morpho.app.screens.notifications -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material.icons.Icons @@ -13,10 +11,14 @@ import androidx.compose.material.pullrefresh.pullRefresh import androidx.compose.material.pullrefresh.rememberPullRefreshState import androidx.compose.material3.* import androidx.compose.runtime.* +import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.unit.Dp import androidx.constraintlayout.compose.ConstraintLayout +import app.cash.paging.LoadStateError +import app.cash.paging.LoadStateNotLoading +import app.cash.paging.compose.collectAsLazyPagingItems import cafe.adriel.voyager.core.annotation.ExperimentalVoyagerApi import cafe.adriel.voyager.core.model.rememberNavigatorScreenModel import cafe.adriel.voyager.core.model.screenModelScope @@ -26,21 +28,22 @@ import cafe.adriel.voyager.navigator.currentOrThrow import com.morpho.app.model.bluesky.BskyPost import com.morpho.app.model.bluesky.DraftPost import com.morpho.app.model.bluesky.NotificationsListItem -import com.morpho.app.model.uidata.AtCursor -import com.morpho.app.model.uidata.getPost +import com.morpho.app.model.bluesky.collectNotifications +import com.morpho.app.model.uistate.NotificationsUIState import com.morpho.app.screens.base.tabbed.ProfileTab import com.morpho.app.screens.base.tabbed.TabScreen import com.morpho.app.screens.base.tabbed.ThreadTab +import com.morpho.app.screens.main.tabbed.TabbedMainScreenModel import com.morpho.app.ui.common.BottomSheetPostComposer import com.morpho.app.ui.common.ComposerRole +import com.morpho.app.ui.common.LoadingCircle import com.morpho.app.ui.common.TabbedScreenScaffold import com.morpho.app.ui.elements.WrappedLazyColumn import com.morpho.app.ui.elements.doMenuOperation import com.morpho.app.ui.notifications.NotificationsElement import com.morpho.app.ui.notifications.NotificationsFilterElement import com.morpho.app.util.ClipboardManager -import com.morpho.butterfly.model.RecordUnion -import kotlinx.collections.immutable.persistentListOf +import com.morpho.butterfly.AtUri import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.IO import kotlinx.coroutines.launch @@ -55,13 +58,15 @@ fun TabScreen.NotificationViewContent( navigator: Navigator = LocalNavigator.currentOrThrow, ) { - val sm = navigator.rememberNavigatorScreenModel { TabbedNotificationScreenModel() } - val numberUnread by sm.uiState.value.numberUnread.collectAsState(0) + val sm = navigator.rememberNavigatorScreenModel { TabbedMainScreenModel() } + val numberUnread = sm.unreadNotificationsCount().value var showSettings by remember { mutableStateOf(false) } val hasUnread = remember(numberUnread) { numberUnread > 0 } val listState = rememberLazyListState() val scope = rememberCoroutineScope() val uriHandler = LocalUriHandler.current + val pager = sm.notificationsRaw.collectAsLazyPagingItems() + var uiState by rememberSaveable { mutableStateOf(NotificationsUIState()) } TabbedScreenScaffold( navBar = { navBar(navigator) }, topContent = { @@ -75,22 +80,18 @@ fun TabScreen.NotificationViewContent( }, showSettings = showSettings, hasUnread = hasUnread, - markAsRead = { sm.markAllRead() } + markAsRead = { sm.markNotificationsAsRead() } ) }, - state = sm.uiState, + state = uiState, modifier = Modifier, content = { insets, state -> val refreshing by remember { mutableStateOf(false)} val refreshState = rememberPullRefreshState( refreshing, - { - sm.notifService.updateNotificationsSeen() - sm.refreshNotifications(AtCursor.EMPTY) - } - ) - val notifications by sm.uiState.value.notifications.collectAsState(persistentListOf()) + { sm.notifService.updateNotificationsSeen() + pager.refresh() }) var repostClicked by remember { mutableStateOf(false)} @@ -103,15 +104,6 @@ fun TabScreen.NotificationViewContent( var draft by remember{ mutableStateOf(DraftPost()) } val clipboardManager = getKoin().get() - val cursor by rememberUpdatedState(sm.uiState.value.cursor) - - LaunchedEffect( - notifications.isNotEmpty() && - !listState.canScrollForward && - !refreshing - ) { - sm.refreshNotifications(cursor) - } ConstraintLayout( @@ -139,73 +131,73 @@ fun TabScreen.NotificationViewContent( Column { HorizontalDivider(Modifier.fillMaxWidth(),thickness = Dp.Hairline) NotificationsFilterElement( - sm.uiState.value.filterState, + uiState.filterState, onFilterClicked = { - sm.notifService.updateFilter(it).invokeOnCompletion { - // forcing a refresh should reload the list with new filters - sm.refreshNotifications(cursor) - } + pager.refresh() } ) HorizontalDivider(Modifier.fillMaxWidth(),thickness = Dp.Hairline) } } } - items( - count = notifications.size, - //key = { index -> notifications[index].hashCode() }, - contentType = { - NotificationsListItem + when(val loadState = pager.loadState.refresh) { + is LoadStateError ->{ + item { Text("Error: ${loadState.error}") } + item { Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.Center) { + TextButton(onClick = { pager.retry() }) { + Text("Retry") + } } } } - ) { index -> - if (state != null) { - NotificationsElement( - item = notifications[index], - showPost = state.value.showPosts, - getPost = { getPost(it, sm.api)}, - onUnClicked = { type, rkey -> - sm.api.deleteRecord(type, rkey) - }, - onAvatarClicked = { - navigator.push(ProfileTab(it)) - }, - onRepostClicked = { - initialContent = it - repostClicked = true - }, - onReplyClicked = { - initialContent = it - composerRole = ComposerRole.Reply - showComposer = true - }, - onMenuClicked = { option, post -> - doMenuOperation(option, post, - clipboardManager = clipboardManager, - uriHandler = uriHandler - ) }, - onLikeClicked = { - sm.api.createRecord(RecordUnion.Like(it)) - }, - onPostClicked = { - navigator.push(ThreadTab(it)) - }, - // If someone hides their read notifications, - // we don't want to just mark them as read unprompted. - // Might cause them to disappear unexpectedly. - readOnLoad = !state.value.filterState.value.showAlreadyRead, - markRead = { sm.markAsRead(it) } - ) - } - } - item { - TextButton( - onClick = { - sm.refreshNotifications(cursor) + is LoadStateNotLoading -> { + val toMarkRead = mutableStateListOf() + val notifications = pager.collectNotifications(toMarkRead) + items( + count = pager.itemCount, + //key = { index -> notifications[index].hashCode() }, + contentType = { + NotificationsListItem + } + ) { index -> + if (state != null) { + NotificationsElement( + item = notifications[index], + showPost = state.showPosts, + getPost = { sm.getPost(it).getOrNull() }, + onUnClicked = { type, rkey -> + sm.agent.deleteRecord(type, rkey) + }, + onAvatarClicked = { + navigator.push(ProfileTab(it)) + }, + onRepostClicked = { + initialContent = it + repostClicked = true + }, + onReplyClicked = { + initialContent = it + composerRole = ComposerRole.Reply + showComposer = true + }, + onMenuClicked = { option, post -> + doMenuOperation(option, post, + clipboardManager = clipboardManager, + uriHandler = uriHandler + ) }, + onLikeClicked = { sm.agent.like(it) }, + onPostClicked = { + navigator.push(ThreadTab(it)) + }, + // If someone hides their read notifications, + // we don't want to just mark them as read unprompted. + // Might cause them to disappear unexpectedly. + readOnLoad = !state.filterState.value.showAlreadyRead, + markRead = { toMarkRead.add(it) }, + resolveHandle = { handle -> sm.agent.resolveHandle(handle).getOrNull() } + ) + } } - ) { - Text("Load More") } - + else -> { item { LoadingCircle() } } } } if(showComposer) { @@ -221,8 +213,8 @@ fun TabScreen.NotificationViewContent( }, onSend = { finishedDraft -> sm.screenModelScope.launch(Dispatchers.IO) { - val post = finishedDraft.createPost(sm.api) - sm.api.createRecord(RecordUnion.MakePost(post)) + val post = finishedDraft.createPost(sm.agent) + sm.agent.post(post) } showComposer = false }, diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/notifications/TabbedNotificationScreenModel.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/notifications/TabbedNotificationScreenModel.kt deleted file mode 100644 index 6726313..0000000 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/notifications/TabbedNotificationScreenModel.kt +++ /dev/null @@ -1,64 +0,0 @@ -package com.morpho.app.screens.notifications - -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.setValue -import cafe.adriel.voyager.core.model.screenModelScope -import com.morpho.app.model.uidata.AtCursor -import com.morpho.app.model.uidata.initAtCursor -import com.morpho.app.model.uistate.NotificationsUIState -import com.morpho.app.model.uistate.UiLoadingState -import com.morpho.app.screens.base.BaseScreenModel -import com.morpho.butterfly.AtUri -import kotlinx.coroutines.flow.* -import kotlinx.coroutines.launch - -class TabbedNotificationScreenModel: BaseScreenModel() { - - private val cursorFlow = initAtCursor() - - private var showPosts by mutableStateOf(true) - private var _uiState: MutableStateFlow = - MutableStateFlow( - NotificationsUIState( - notifService.notifications, - notifService.filter, - showPosts, - UiLoadingState.Loading - ) - ) - - init { - screenModelScope.launch { - val f = notifService.notifications(cursorFlow).map { it.getOrNull() } - cursorFlow.emit(AtCursor.EMPTY) - f.collect { - if(it != null) { - _uiState.update { - NotificationsUIState( - notifService.notifications, - notifService.filter, - showPosts, - UiLoadingState.Idle - ) - } - } - } - } - } - - val uiState: StateFlow - get() = _uiState.asStateFlow() - - fun markAllRead() { - notifService.updateNotificationsSeen() - } - - fun markAsRead(uri: AtUri) { - notifService.markAsRead(uri) - } - - fun refreshNotifications(cursor: AtCursor): Boolean { - return cursorFlow.tryEmit(cursor) - } -} \ No newline at end of file diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/profile/TabbedProfileView.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/profile/TabbedProfileView.kt index a8f21aa..5916aa8 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/profile/TabbedProfileView.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/profile/TabbedProfileView.kt @@ -5,8 +5,6 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.lazy.LazyListState -import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.runtime.saveable.rememberSaveable @@ -14,7 +12,6 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.unit.dp import cafe.adriel.voyager.core.annotation.ExperimentalVoyagerApi -import cafe.adriel.voyager.core.lifecycle.LifecycleEffectOnce import cafe.adriel.voyager.core.screen.ScreenKey import cafe.adriel.voyager.core.screen.uniqueScreenKey import cafe.adriel.voyager.navigator.LocalNavigator @@ -24,32 +21,24 @@ import cafe.adriel.voyager.navigator.tab.TabDisposable import cafe.adriel.voyager.navigator.tab.TabNavigator import cafe.adriel.voyager.navigator.tab.TabOptions import coil3.annotation.ExperimentalCoilApi -import com.morpho.app.model.bluesky.BskyLabelService -import com.morpho.app.model.bluesky.DetailedProfile -import com.morpho.app.model.bluesky.MorphoDataItem -import com.morpho.app.model.bluesky.Profile +import com.morpho.app.model.uidata.Event +import com.morpho.app.model.uidata.FeedEvent import com.morpho.app.model.uistate.ContentCardState -import com.morpho.app.model.uistate.UiLoadingState import com.morpho.app.screens.base.tabbed.TabScreen import com.morpho.app.ui.common.LoadingCircle import com.morpho.app.ui.common.TabbedProfileScreenScaffold import com.morpho.app.ui.common.TabbedSkylineFragment import com.morpho.app.ui.profile.DetailedProfileFragment -import com.morpho.butterfly.AtIdentifier import dev.icerock.moko.parcelize.Parcelable import dev.icerock.moko.parcelize.Parcelize -import kotlinx.collections.immutable.toImmutableList -import kotlinx.coroutines.flow.StateFlow import kotlinx.serialization.Serializable import org.jetbrains.compose.resources.ExperimentalResourceApi -import org.koin.compose.getKoin import cafe.adriel.voyager.navigator.tab.Tab as NavTab @OptIn(ExperimentalAnimationApi::class, ExperimentalMaterial3Api::class) @Composable -fun TabbedProfileTopBar( - profile: Profile?, - ownProfile: Boolean, +fun MyTabbedProfileTopBar( + profile: ContentCardState.MyProfile, scrollBehavior: TopAppBarScrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior( rememberTopAppBarState()), tabs: List, @@ -63,58 +52,82 @@ fun TabbedProfileTopBar( .fillMaxWidth() .nestedScroll(scrollBehavior.nestedScrollConnection), ) { - when(profile != null) { - true -> { - when(profile) { - is DetailedProfile -> DetailedProfileFragment( - profile = profile, - myProfile = ownProfile, - isTopLevel = true, - scrollBehavior = scrollBehavior, - onBackClicked = onBackClicked, - ) - is BskyLabelService -> { TODO("Make different title card for label services")} - else -> { /* Shouldn't happen */ } - } + DetailedProfileFragment( + profile = profile.profile, + myProfile = true, + isTopLevel = true, + scrollBehavior = scrollBehavior, + onBackClicked = onBackClicked, + ) - SecondaryScrollableTabRow( - selectedTabIndex = selectedTabIndex, - edgePadding = 4.dp, - modifier = Modifier.fillMaxWidth() + SecondaryScrollableTabRow( + selectedTabIndex = selectedTabIndex, + edgePadding = 4.dp, + modifier = Modifier.fillMaxWidth() + ) { + tabs.forEachIndexed { index, tab -> + ProfileTabItem( + tab, index ) { - tabs.forEachIndexed { index, tab -> - ProfileTabItem( - tab, index.toUShort() - ) { - selectedTabIndex = index - onTabChanged(selectedTabIndex) - } - } + selectedTabIndex = index + onTabChanged(selectedTabIndex) } } - false -> { - // Loading - } } + } +} + +@OptIn(ExperimentalAnimationApi::class, ExperimentalMaterial3Api::class) +@Composable +fun TabbedProfileTopBar( + profile: ContentCardState.FullProfile, + scrollBehavior: TopAppBarScrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior( + rememberTopAppBarState()), + tabs: List, + onTabChanged: (Int) -> Unit = {}, + onBackClicked: () -> Unit, + tabIndex: Int = 0, +) { + var selectedTabIndex by rememberSaveable { mutableIntStateOf(tabIndex) } + Column( + modifier = Modifier + .fillMaxWidth() + .nestedScroll(scrollBehavior.nestedScrollConnection), + ) { + DetailedProfileFragment( + profile = profile.profile, + myProfile = true, + isTopLevel = true, + scrollBehavior = scrollBehavior, + onBackClicked = onBackClicked, + ) + SecondaryScrollableTabRow( + selectedTabIndex = selectedTabIndex, + edgePadding = 4.dp, + modifier = Modifier.fillMaxWidth() + ) { + tabs.forEachIndexed { index, tab -> + ProfileTabItem( + tab, index + ) { + selectedTabIndex = index + onTabChanged(selectedTabIndex) + } + } + } } } @Composable fun ProfileTabItem( tab: ProfileSkylineTab, - currentIndex: UShort, + currentIndex: Int, onClick: () -> Unit = {}, ) { val navigator = LocalTabNavigator.current - val tabModifier = Modifier - .padding( - bottom = 12.dp, - top = 6.dp, - start = 6.dp, - end = 6.dp - ) + .padding(bottom = 12.dp, top = 6.dp, start = 6.dp, end = 6.dp) Tab( selected = currentIndex == tab.index, onClick = { @@ -122,10 +135,7 @@ fun ProfileTabItem( navigator.current = tab }, ) { - Text( - text = tab.title, - modifier = tabModifier - ) + Text(text = tab.title, modifier = tabModifier) } } @@ -134,115 +144,82 @@ fun ProfileTabItem( ) @Composable fun TabScreen.TabbedProfileContent( - id: AtIdentifier? = null, - sm: TabbedProfileViewModel = getKoin().get() + ownProfile: Boolean = false, + myProfileState: ContentCardState.MyProfile, + profileState: ContentCardState.FullProfile?, + eventCallback: (Event) -> Unit = {}, ) { //ProvideNavigatorLifecycleKMPSupport { val navigator = LocalNavigator.currentOrThrow - - - LifecycleEffectOnce { sm.initProfile() } - /*val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior( - state = rememberTopAppBarState(), - snapAnimationSpec = spring( - stiffness = Spring.StiffnessMediumLow, - dampingRatio = Spring.DampingRatioNoBouncy - ), - //flingAnimationSpec = exponentialDecay() - )*/ var selectedTabIndex by rememberSaveable { mutableIntStateOf(0) } val scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior(rememberTopAppBarState()) - val ownProfile = remember { sm.api.atpUser?.id == id } - val tabs = remember( - sm.tabFlow, - sm.profileUiState.loadingState, - ) { - List(sm.tabFlow.value.size) { index -> - ProfileSkylineTab( - index = index.toUShort(), - ownProfile = ownProfile, - title = sm.tabFlow.value[index].title, - ) - } - } - val tabsCreated = remember(tabs.size, sm.profileUiState.loadingState) { - tabs.isNotEmpty() && sm.profileUiState.loadingState == UiLoadingState.Idle + val tabs = remember(myProfileState, profileState) { + if(ownProfile) myProfileState.toTabList() else profileState?.toTabList() ?: listOf() } - if (tabsCreated) { - TabNavigator( - tab = tabs.first(), - disposeNestedNavigators = false, - tabDisposable = { TabDisposable(navigator = it, tabs = tabs) } - ) { - val listState = rememberLazyListState( - initialFirstVisibleItemIndex = - sm.profileUiState.tabStates[selectedTabIndex].value.feed.cursor.scroll - ) - - TabbedProfileScreenScaffold( - navBar = { navBar(navigator) }, - modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection), - topContent = { - TabbedProfileTopBar( - sm.profileState?.profile, - ownProfile, scrollBehavior, tabs.toImmutableList(), - onBackClicked = { navigator.pop() }, - onTabChanged = { index -> - selectedTabIndex = index - sm.refreshTab( - index, - sm.profileUiState.tabStates[index].value.feed.cursor - .copy(scroll = listState.firstVisibleItemIndex) - ) - }, - tabIndex = selectedTabIndex, - ) - }, - content = { insets, state -> - - CurrentProfileScreen(sm, insets, state, listState, Modifier) + TabNavigator( + tab = tabs.first(), + disposeNestedNavigators = true, + tabDisposable = { TabDisposable(navigator = it, tabs = tabs) } + ) { + TabbedProfileScreenScaffold( + navBar = { navBar(navigator) }, + modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection), + topContent = { + if(ownProfile) MyTabbedProfileTopBar( + profile = myProfileState, + scrollBehavior = scrollBehavior, + tabs = tabs, + onBackClicked = { navigator.pop() }, + onTabChanged = { index -> + selectedTabIndex = index + val state = myProfileState.indexToState(index) + val actor = myProfileState.profile.did + when(state) { + is ContentCardState.ProfileTimeline -> state.events + .tryEmit(FeedEvent.LoadFeed(actor, state.filter)) + is ContentCardState.ProfileList -> state.events + .tryEmit(FeedEvent.LoadLists(actor, state.listsOrFeeds)) + is ContentCardState.ProfileLabeler -> {} + else -> {} + } }, - state = sm.profileUiState.tabStates.getOrNull(selectedTabIndex), + tabIndex = selectedTabIndex, + ) else if(profileState != null) TabbedProfileTopBar( + profile = profileState, scrollBehavior = scrollBehavior, - ) - } - } else { - TabbedProfileScreenScaffold( - navBar = { navBar(navigator) }, - modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection), - topContent = { - if (sm.profileState?.profile != null) { - DetailedProfileFragment( - profile = sm.profileState?.profile!! as DetailedProfile, - myProfile = ownProfile, - isTopLevel = true, - scrollBehavior = scrollBehavior, - onBackClicked = { navigator.pop() } - ) - } else { - TopAppBar( - title = { Text("Loading...") } - ) - } - }, - content = { _, _ -> - LoadingCircle() - }, - scrollBehavior = scrollBehavior, - state = sm.profileUiState.tabStates.getOrNull(selectedTabIndex), - ) - } + tabs = tabs, + onBackClicked = { navigator.pop() }, + onTabChanged = { index -> + selectedTabIndex = index + val state = profileState.indexToState(index) + val actor = profileState.profile.did + when(state) { + is ContentCardState.ProfileTimeline -> state.events + .tryEmit(FeedEvent.LoadFeed(actor, state.filter)) + is ContentCardState.ProfileList -> state.events + .tryEmit(FeedEvent.LoadLists(actor, state.listsOrFeeds)) + is ContentCardState.ProfileLabeler -> {} + else -> {} + } + }, + tabIndex = selectedTabIndex, + ) else LoadingCircle() + }, + content = { insets, state -> CurrentProfileScreen(eventCallback, insets, state, Modifier) }, + state = if(ownProfile) myProfileState.indexToState(selectedTabIndex) + else profileState?.indexToState(selectedTabIndex), + scrollBehavior = scrollBehavior, + ) + } //} } + @Composable -public fun CurrentProfileScreen( - sm: TabbedProfileViewModel, +public fun CurrentProfileScreen( + eventCallback: (Event) -> Unit, paddingValues: PaddingValues, - state: StateFlow>?, - listState: LazyListState = rememberLazyListState( - initialFirstVisibleItemIndex = state?.value?.feed?.cursor?.scroll ?: 0 - ), + state: ContentCardState?, modifier: Modifier ) { val navigator = LocalNavigator.currentOrThrow @@ -250,10 +227,9 @@ public fun CurrentProfileScreen( navigator.saveableState("currentScreen") { currentScreen.Content( - sm = sm, + eventCallback = eventCallback, paddingValues = paddingValues, state = state, - listState = listState, modifier = modifier ) } @@ -263,39 +239,44 @@ public fun CurrentProfileScreen( abstract class ProfileTabScreen: NavTab { @Composable - abstract fun Content( - sm: TabbedProfileViewModel, + abstract fun Content( + eventCallback: (Event) -> Unit, paddingValues: PaddingValues, - state: StateFlow>?, - listState: LazyListState, + state: ContentCardState?, modifier: Modifier ) @OptIn(ExperimentalVoyagerApi::class) @Composable - final override fun Content() = Content(TabbedProfileViewModel(),PaddingValues(0.dp),null, rememberLazyListState(), Modifier) + final override fun Content() = Content( + eventCallback = {}, PaddingValues(0.dp),null, Modifier + ) + } @Parcelize @Serializable data class ProfileSkylineTab( - val index: UShort, + val index: Int, val ownProfile: Boolean = false, val title: String, ): ProfileTabScreen(), Parcelable { @OptIn(ExperimentalMaterial3Api::class, ExperimentalVoyagerApi::class) @Composable - override fun Content( - sm: TabbedProfileViewModel, + override fun Content( + eventCallback: (Event) -> Unit, paddingValues: PaddingValues, - state: StateFlow>?, - listState: LazyListState, + state: ContentCardState?, modifier: Modifier ) { - TabbedSkylineFragment(sm, state, paddingValues, refresh = { cursor -> - sm.refreshTab(index.toInt(), cursor) - }, isProfileFeed = true, listState = listState) + if(state == null) return + TabbedSkylineFragment( + paddingValues, + isProfileFeed = true, + uiUpdate = state.updates, + eventCallback = eventCallback, + ) } override val key: ScreenKey @@ -307,10 +288,78 @@ data class ProfileSkylineTab( @Composable get() { return TabOptions( - index = index, + index = index.toUShort(), title = title, //icon = icon, ) } } + +fun countProfileTabs(profileState: ContentCardState.FullProfile): Int { + var count = 3 + if(profileState.lists != null) count++ + if(profileState.feeds != null) count++ + if(profileState.labeler != null) count++ + return count +} +fun countMyProfileTabs(profileState: ContentCardState.MyProfile): Int { + var count = 4 + if(profileState.lists != null) count++ + if(profileState.feeds != null) count++ + if(profileState.labeler != null) count++ + return count +} + + +fun ContentCardState.FullProfile.toTabList(): List { + val tabs = mutableListOf() + if(labeler != null) tabs.add(ProfileSkylineTab(0, false, "Labels")) + var index = if(labeler != null) 1 else 0 + tabs.add(ProfileSkylineTab(index++, false, "Posts")) + tabs.add(ProfileSkylineTab(index++, false, "Replies")) + tabs.add(ProfileSkylineTab(index++, false, "Media")) + if(lists != null) tabs.add(ProfileSkylineTab(index++, false, "Lists")) + if(feeds != null) tabs.add(ProfileSkylineTab(index, false, "Feeds")) + return tabs.toList() +} + +fun ContentCardState.MyProfile.toTabList(): List { + val tabs = mutableListOf() + if(labeler != null) tabs.add(ProfileSkylineTab(0, true, "Labels")) + var index = if(labeler != null) 1 else 0 + tabs.add(ProfileSkylineTab(index++, true, "Posts")) + tabs.add(ProfileSkylineTab(index++, true, "Replies")) + tabs.add(ProfileSkylineTab(index++, true, "Media")) + if(lists != null) tabs.add(ProfileSkylineTab(index++, true, "Lists")) + if(feeds != null) tabs.add(ProfileSkylineTab(index++, true, "Feeds")) + if(labeler != null) tabs.add(ProfileSkylineTab(index, true, "Labels")) + return tabs.toList() +} + +fun ContentCardState.MyProfile.indexToState(index: Int): ContentCardState? { + return when(index) { + 0 -> labeler ?: posts + 1 -> if(labeler == null) postReplies else posts + 2 -> if(labeler == null) media else postReplies + 3 -> if(labeler == null) likes else media + 4 -> if(labeler == null) lists ?: feeds else likes + 5 -> if(labeler == null) feeds else lists ?: feeds + 6 -> if(labeler == null) null else feeds + else -> throw IllegalArgumentException("Invalid index: $index") + } +} + +fun ContentCardState.FullProfile.indexToState(index: Int): ContentCardState? { + return when(index) { + 0 -> labeler ?: posts + 1 -> if(labeler == null) postReplies else posts + 2 -> if(labeler == null) media else postReplies + 3 -> if(labeler == null) lists ?: feeds else media + 4 -> if(labeler == null) feeds else lists ?: feeds + 5 -> if(labeler == null) null else feeds + else -> throw IllegalArgumentException("Invalid index: $index") + } +} + + diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/profile/TabbedProfileViewModel.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/profile/TabbedProfileViewModel.kt deleted file mode 100644 index 137b479..0000000 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/profile/TabbedProfileViewModel.kt +++ /dev/null @@ -1,263 +0,0 @@ -package com.morpho.app.screens.profile - -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.setValue -import app.bsky.actor.GetProfileQuery -import cafe.adriel.voyager.core.model.screenModelScope -import com.morpho.app.model.bluesky.* -import com.morpho.app.model.uidata.AtCursor -import com.morpho.app.model.uidata.ContentCardMapEntry -import com.morpho.app.model.uidata.MorphoData -import com.morpho.app.model.uistate.ContentCardState -import com.morpho.app.model.uistate.TabbedProfileScreenState -import com.morpho.app.model.uistate.UiLoadingState -import com.morpho.app.screens.main.MainScreenModel -import com.morpho.butterfly.AtIdentifier -import kotlinx.collections.immutable.toImmutableList -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.launch -import kotlinx.serialization.Serializable -import org.lighthousegames.logging.logging - -@Serializable -@Suppress("UNCHECKED_CAST") -// TODO: Revisit these casts if we can, but they should be safe -class TabbedProfileViewModel( - val id: AtIdentifier? = null -): MainScreenModel() { - - companion object { - val log = logging() - } - var profileUiState: TabbedProfileScreenState by mutableStateOf( - TabbedProfileScreenState( - loadingState = UiLoadingState.Loading, - tabs = tabFlow - )) - private set - - var profileState: ContentCardState.FullProfile? by mutableStateOf(null) - private set - - private val tabs = mutableListOf() - - private val _tabFlow = MutableStateFlow(tabs.toList()) - val tabFlow: StateFlow> - get() = _tabFlow.asStateFlow() - - var profileId: AtIdentifier? by mutableStateOf(null) - private set - - var myProfile: Boolean = false - private set - - - - - fun initProfile() = screenModelScope.launch { - if(initialized) return@launch - init(false) - if(id != null) { - profileId = id - myProfile = api.atpUser?.id == id - } else { - profileId = api.atpUser?.id - myProfile = true - } - log.d { "Profile of: $profileId"} - initialized = true - if(profileId == null) { - profileUiState = profileUiState.copy( - loadingState = UiLoadingState.Error("Profile not found") - ) - return@launch - } - - profileId?.let { GetProfileQuery(it) }?.let { query -> - api.api.getProfile(query) - .onSuccess { resp -> - profileState = loadProfile(resp.toProfile()) - log.d { "Profile loaded: ${resp.toProfile()}" } - if (profileState != null) { - val tabStates = mutableListOf>>() - when(profileState!!.profile) { - is DetailedProfile -> { - if (profileState?.postsState != null) { - tabs.add( - ContentCardMapEntry.Feed( - profileState!!.postsState.value!!.uri, - profileState!!.postsState.value!!.feed.title, - cursors[profileState!!.postsState.value!!.uri] - ?: MutableStateFlow(AtCursor.EMPTY) - ) - ) - tabStates.add(profileState!!.postsState as StateFlow>) - } - if (profileState?.postRepliesState != null) { - tabs.add( - ContentCardMapEntry.PostThread( - profileState!!.postRepliesState.value!!.uri, - profileState!!.postRepliesState.value!!.feed.title, - cursors[profileState!!.postRepliesState.value!!.uri] - ?: MutableStateFlow(AtCursor.EMPTY) - ) - ) - tabStates.add(profileState!!.postRepliesState as StateFlow>) - } - if (profileState?.mediaState != null) { - tabs.add( - ContentCardMapEntry.Feed( - profileState!!.mediaState.value!!.uri, - profileState!!.mediaState.value!!.feed.title, - cursors[profileState!!.mediaState.value!!.uri] - ?: MutableStateFlow(AtCursor.EMPTY) - ) - ) - tabStates.add(profileState!!.mediaState as StateFlow>) - } - if(myProfile && profileState?.likesState != null) { - tabs.add( - ContentCardMapEntry.Feed( - profileState!!.likesState.value!!.uri, - profileState!!.likesState.value!!.feed.title, - cursors[profileState!!.likesState.value!!.uri] - ?: MutableStateFlow(AtCursor.EMPTY) - ) - ) - tabStates.add(profileState!!.likesState as StateFlow>) - } - if (profileState?.feedsState != null) { - tabs.add( - ContentCardMapEntry.FeedList( - profileState!!.feedsState.value!!.uri, - profileState!!.feedsState.value!!.feed.title, - cursors[profileState!!.feedsState.value!!.uri] - ?: MutableStateFlow(AtCursor.EMPTY) - ) - ) - tabStates.add(profileState!!.feedsState as StateFlow>) - } - if (profileState?.listsState != null) { - tabs.add( - ContentCardMapEntry.UserList( - profileState!!.listsState.value!!.uri, - profileState!!.listsState.value!!.feed.title, - cursors[profileState!!.listsState.value!!.uri] - ?: MutableStateFlow(AtCursor.EMPTY) - ) - ) - tabStates.add(profileState!!.listsState as StateFlow>) - } - } - is BskyLabelService -> { - if (profileState?.modServiceState != null) { - tabs.add( - ContentCardMapEntry.ServiceList( - profileState!!.modServiceState.value!!.uri, - profileState!!.modServiceState.value!!.feed.title, - cursors[profileState!!.modServiceState.value!!.uri] - ?: MutableStateFlow(AtCursor.EMPTY) - ) - ) - tabStates.add(profileState!!.modServiceState as StateFlow>) - } - if (profileState?.listsState != null) { - tabs.add( - ContentCardMapEntry.UserList( - profileState!!.listsState.value!!.uri, - profileState!!.listsState.value!!.feed.title, - cursors[profileState!!.listsState.value!!.uri] - ?: MutableStateFlow(AtCursor.EMPTY) - ) - ) - tabStates.add(profileState!!.listsState as StateFlow>) - } - if (profileState?.postsState != null) { - tabs.add( - ContentCardMapEntry.Feed( - profileState!!.postsState.value!!.uri, - profileState!!.postsState.value!!.feed.title, - cursors[profileState!!.postsState.value!!.uri] - ?: MutableStateFlow(AtCursor.EMPTY) - ) - ) - tabStates.add(profileState!!.postsState as StateFlow>) - } - if (profileState?.postRepliesState != null) { - tabs.add( - ContentCardMapEntry.PostThread( - profileState!!.postRepliesState.value!!.uri, - profileState!!.postRepliesState.value!!.feed.title, - cursors[profileState!!.postRepliesState.value!!.uri] - ?: MutableStateFlow(AtCursor.EMPTY) - ) - ) - tabStates.add(profileState!!.postRepliesState as StateFlow>) - } - if (profileState?.feedsState != null) { - tabs.add( - ContentCardMapEntry.FeedList( - profileState!!.feedsState.value!!.uri, - profileState!!.feedsState.value!!.feed.title, - cursors[profileState!!.feedsState.value!!.uri] - ?: MutableStateFlow(AtCursor.EMPTY) - ) - ) - tabStates.add(profileState!!.feedsState as StateFlow>) - } - } - else -> {} - } - _tabFlow.value = tabs.toImmutableList() - log.d { "Tabs: ${tabs.map { it.title }}"} - profileUiState = profileUiState.copy( - tabs = tabFlow, - tabStates = tabStates.toImmutableList(), - loadingState = UiLoadingState.Idle - ) - } - }.onFailure { - profileUiState = profileUiState - .copy( - loadingState = UiLoadingState - .Error("Profile not loaded") - ) - log.e(it) { "Profile not loaded. Error: $it" } - } - } - - } - - fun refreshTab(index: Int, cursor: AtCursor = AtCursor.EMPTY) :Boolean { - return if(index < 0 || index > tabs.lastIndex) false - else updateFeed(tabs[index], cursor) - } - - suspend fun loadProfile(profile: DetailedProfile): ContentCardState.FullProfile? { - val profileEntry = ContentCardMapEntry.Profile(profile.did) - return initProfileContent(profileEntry, force = true, fill = true).first() - } - - - override fun unloadContent(entry: ContentCardMapEntry): MorphoData? { - val maybeTab = profileUiState.tabMap[entry.uri] - return if(maybeTab == null) { - history.popUntil { it == entry } - unloadContent(entry.uri) - } else { - unloadContent(maybeTab) - } - } - - fun unloadTab(index: Int): MorphoData? { - if(index < 0 || index > tabs.lastIndex) return null - val uri = tabs[index].uri - val state = profileUiState.tabMap[uri] ?: return null - return unloadContent(state) - } - -} \ No newline at end of file diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/thread/ThreadView.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/thread/ThreadView.kt index 1f40966..fe9ca83 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/thread/ThreadView.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/thread/ThreadView.kt @@ -11,32 +11,33 @@ import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.unit.dp import cafe.adriel.voyager.core.annotation.ExperimentalVoyagerApi import cafe.adriel.voyager.core.model.rememberNavigatorScreenModel +import cafe.adriel.voyager.core.model.screenModelScope import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.Navigator import cafe.adriel.voyager.navigator.currentOrThrow import com.atproto.repo.StrongRef +import com.morpho.app.data.MorphoAgent import com.morpho.app.model.bluesky.BskyPost import com.morpho.app.model.bluesky.BskyPostThread import com.morpho.app.model.bluesky.DraftPost +import com.morpho.app.model.uidata.ThreadUpdate import com.morpho.app.model.uistate.ContentCardState import com.morpho.app.screens.base.tabbed.ProfileTab import com.morpho.app.screens.base.tabbed.TabScreen import com.morpho.app.screens.base.tabbed.ThreadTab import com.morpho.app.screens.main.MainScreenModel -import com.morpho.app.ui.common.BottomSheetPostComposer -import com.morpho.app.ui.common.ComposerRole -import com.morpho.app.ui.common.RepostQueryDialog -import com.morpho.app.ui.common.TabbedScreenScaffold +import com.morpho.app.ui.common.* import com.morpho.app.ui.elements.doMenuOperation import com.morpho.app.ui.thread.ThreadFragment import com.morpho.app.util.ClipboardManager +import com.morpho.butterfly.AtIdentifier import com.morpho.butterfly.AtUri -import com.morpho.butterfly.Butterfly +import com.morpho.butterfly.Did import com.morpho.butterfly.model.RecordType import com.morpho.butterfly.model.RecordUnion import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.IO -import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.filterIsInstance import kotlinx.coroutines.launch import org.koin.compose.getKoin @@ -45,11 +46,12 @@ import org.koin.compose.getKoin ) @Composable fun TabScreen.ThreadViewContent( - threadState: StateFlow, + cardState: ContentCardState.PostThread, navigator:Navigator = LocalNavigator.currentOrThrow, ) { val sm = navigator.rememberNavigatorScreenModel { MainScreenModel() } + val threadState by cardState.updates.filterIsInstance().collectAsState(ThreadUpdate.Empty) TabbedScreenScaffold( navBar = { navBar(navigator) }, @@ -59,15 +61,32 @@ fun TabScreen.ThreadViewContent( modifier = Modifier, state = threadState, content = { insets, state -> - if (state != null) { - state.value.thread.value?.let { thread -> - ThreadView( - thread, - insets = insets, - navigator = navigator, - createRecord = { sm.createRecord(it) }, - deleteRecord = { type, uri -> sm.deleteRecord(type, uri) } - ) + when(state) { + is ThreadUpdate.Empty -> { + LoadingCircle() + } + + is ThreadUpdate.Error -> { + Text("Error: ${state.error}") + } + + is ThreadUpdate.Thread -> { + val thread = state.results.collectAsState(null).value + if(thread != null) { + ThreadView( + thread = thread, + insets = insets, + navigator = navigator, + createRecord = { sm.screenModelScope.launch { sm.agent.createRecord(it) } }, + deleteRecord = { type, uri -> sm.screenModelScope.launch { + sm.agent.deleteRecord(type, uri) + } }, + resolveHandle = { handle -> sm.agent.resolveHandle(handle).getOrNull() } + ) + } + } + else -> { + Text("Unknown state: $state") } } @@ -96,6 +115,7 @@ fun ThreadView( navigator: Navigator = LocalNavigator.currentOrThrow, createRecord: (RecordUnion) -> Unit = { }, deleteRecord: (RecordType, AtUri) -> Unit = { _, _ -> }, + resolveHandle: suspend (AtIdentifier) -> Did?, ) { var repostClicked by remember { mutableStateOf(false)} var initialContent: BskyPost? by remember { mutableStateOf(null) } @@ -111,7 +131,12 @@ fun ThreadView( ThreadFragment(thread = thread, contentPadding = insets, onItemClicked = { navigator.push(ThreadTab(it)) }, - onProfileClicked = { navigator.push(ProfileTab(it)) }, + onProfileClicked = { + scope.launch { + val did = resolveHandle(it) + if(did != null) navigator.push(ProfileTab(did)) + } + }, onUnClicked = {type, uri -> deleteRecord(type, uri)}, onRepostClicked = { initialContent = it @@ -151,7 +176,7 @@ fun ThreadView( ) } if(showComposer) { - val api = getKoin().get() + val agent = getKoin().get() BottomSheetPostComposer( onDismissRequest = { showComposer = false }, sheetState = sheetState, @@ -165,7 +190,7 @@ fun ThreadView( }, onSend = { finishedDraft -> scope.launch(Dispatchers.IO) { - val post = finishedDraft.createPost(api) + val post = finishedDraft.createPost(agent) createRecord(RecordUnion.MakePost(post)) } showComposer = false diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/common/SkylineFragment.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/common/SkylineFragment.kt index 291822c..7f03315 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/common/SkylineFragment.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/common/SkylineFragment.kt @@ -18,7 +18,6 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp import androidx.constraintlayout.compose.ConstraintLayout -import androidx.paging.PagingData import app.cash.paging.LoadStateError import app.cash.paging.LoadStateLoading import app.cash.paging.LoadStateNotLoading @@ -27,16 +26,19 @@ import app.cash.paging.compose.itemKey import com.atproto.repo.StrongRef import com.morpho.app.model.bluesky.BskyPost import com.morpho.app.model.bluesky.MorphoDataItem -import com.morpho.app.model.uidata.ContentHandling +import com.morpho.app.model.uidata.AuthorFeedUpdate import com.morpho.app.model.uidata.FeedUpdate +import com.morpho.app.model.uidata.UIUpdate import com.morpho.app.ui.elements.MenuOptions import com.morpho.app.ui.elements.WrappedLazyColumn import com.morpho.app.ui.post.PlaceholderSkylineItem import com.morpho.app.ui.post.PostFragment import com.morpho.butterfly.AtIdentifier import com.morpho.butterfly.AtUri +import com.morpho.butterfly.ContentHandling import com.morpho.butterfly.model.RecordType -import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.filterIsInstance import kotlinx.coroutines.launch typealias OnPostClicked = (AtUri) -> Unit @@ -44,36 +46,41 @@ typealias OnPostClicked = (AtUri) -> Unit @OptIn(ExperimentalMaterialApi::class, ExperimentalFoundationApi::class) @Composable -fun SkylineFragment ( +inline fun SkylineFragment ( modifier: Modifier = Modifier, - onItemClicked: OnPostClicked, - onProfileClicked: (AtIdentifier) -> Unit = {}, - onPostButtonClicked: () -> Unit = {}, - onReplyClicked: (BskyPost) -> Unit = { }, - onRepostClicked: (BskyPost) -> Unit = { }, - onLikeClicked: (StrongRef) -> Unit = { }, - onMenuClicked: (MenuOptions, BskyPost) -> Unit = { _, _ -> }, - onUnClicked: (type: RecordType, uri: AtUri) -> Unit = { _, _ -> }, - getContentHandling: (BskyPost) -> List = { listOf() }, + noinline onItemClicked: OnPostClicked, + noinline onProfileClicked: (AtIdentifier) -> Unit = {}, + crossinline onPostButtonClicked: () -> Unit = {}, + noinline onReplyClicked: (BskyPost) -> Unit = { }, + noinline onRepostClicked: (BskyPost) -> Unit = { }, + noinline onLikeClicked: (StrongRef) -> Unit = { }, + noinline onMenuClicked: (MenuOptions, BskyPost) -> Unit = { _, _ -> }, + noinline onUnClicked: (type: RecordType, uri: AtUri) -> Unit = { _, _ -> }, + noinline getContentHandling: (BskyPost) -> List = { listOf() }, contentPadding: PaddingValues = PaddingValues(0.dp), isProfileFeed: Boolean = false, debuggable: Boolean = false, - feedUpdate: StateFlow>, + feedUpdate: Flow, ) { val scope = rememberCoroutineScope() val listState = rememberLazyListState() - val state = feedUpdate.collectAsState() - val pager = remember { when(state.value) { - is FeedUpdate.Feed -> (state.value as FeedUpdate.Feed).feed - is FeedUpdate.Peek -> null + val state = feedUpdate.filterIsInstance().collectAsState( + when(feedUpdate) { + is AuthorFeedUpdate -> AuthorFeedUpdate.Empty + is FeedUpdate<*> -> FeedUpdate.Empty + else -> UIUpdate.Empty + } + ) + val data = when(state.value) { + is AuthorFeedUpdate.Feed -> (state.value as AuthorFeedUpdate.Feed).feed.collectAsLazyPagingItems() + is AuthorFeedUpdate.Feeds -> (state.value as AuthorFeedUpdate.Feeds).feed.collectAsLazyPagingItems() + is AuthorFeedUpdate.Likes -> (state.value as AuthorFeedUpdate.Likes).feed.collectAsLazyPagingItems() + is AuthorFeedUpdate.Lists -> (state.value as AuthorFeedUpdate.Lists).feed.collectAsLazyPagingItems() + is FeedUpdate.Feed -> (state.value as FeedUpdate.Feed).feed.collectAsLazyPagingItems() else -> null - } } + } - val data = pager?.collectAsLazyPagingItems() - val pagerState = pager?.collectAsState( - if(data != null) PagingData.from(data.itemSnapshotList.items) else PagingData.empty() - ) val scrolledDownSome by remember { @@ -89,10 +96,8 @@ fun SkylineFragment ( } - fun refreshPull() = data?.refresh() - val refreshing by remember { mutableStateOf(false) } - val refreshState = rememberPullRefreshState(refreshing, ::refreshPull) + val refreshState = rememberPullRefreshState(refreshing, {data?.refresh()}) ConstraintLayout( @@ -150,7 +155,7 @@ fun SkylineFragment ( .weight(0.4f) ) IconButton( - onClick = { refreshPull() }, + onClick = {data?.refresh()}, colors = IconButtonDefaults.iconButtonColors( containerColor = MaterialTheme.colorScheme.background, contentColor = MaterialTheme.colorScheme.onSurfaceVariant @@ -196,7 +201,12 @@ fun SkylineFragment ( is LoadStateNotLoading -> { items( data.itemCount, - key = data.itemKey { it.key } + key = data.itemKey {when(it) { + is MorphoDataItem.FeedItem -> it.key + is MorphoDataItem.Post -> it.key + is MorphoDataItem.Thread -> it.key + else -> it.hashCode() + } } ) { index -> when(val item = data[index]) { is MorphoDataItem.Thread -> { diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/common/TabbedScreenScaffold.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/common/TabbedScreenScaffold.kt index 3a27264..a24a0ad 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/common/TabbedScreenScaffold.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/common/TabbedScreenScaffold.kt @@ -20,26 +20,25 @@ import cafe.adriel.voyager.navigator.CurrentScreen import cafe.adriel.voyager.navigator.Navigator import cafe.adriel.voyager.transitions.ScreenTransition import cafe.adriel.voyager.transitions.ScreenTransitionContent -import com.morpho.app.model.bluesky.MorphoDataItem +import com.morpho.app.model.uidata.Event import com.morpho.app.model.uistate.ContentCardState -import kotlinx.coroutines.flow.StateFlow @Composable expect fun TabbedScreenScaffold( navBar: @Composable () -> Unit, - content: @Composable (PaddingValues, StateFlow?) -> Unit, + content: @Composable (PaddingValues, T?) -> Unit, topContent: @Composable () -> Unit, - state: StateFlow?, + state: T?, modifier: Modifier, ) @ExperimentalMaterial3Api @Composable -expect fun TabbedProfileScreenScaffold( +expect fun TabbedProfileScreenScaffold( navBar: @Composable () -> Unit, - content: @Composable (PaddingValues,StateFlow>?) -> Unit, + content: @Composable (PaddingValues,ContentCardState?) -> Unit, topContent: @Composable (TopAppBarScrollBehavior) -> Unit, - state: StateFlow>?, + state: ContentCardState?, modifier: Modifier = Modifier, scrollBehavior: TopAppBarScrollBehavior, nestedScrollConnection: NestedScrollConnection = scrollBehavior.nestedScrollConnection, diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/common/TabbedSkylineFragment.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/common/TabbedSkylineFragment.kt index 6d41610..01b94d9 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/common/TabbedSkylineFragment.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/common/TabbedSkylineFragment.kt @@ -12,8 +12,7 @@ import cafe.adriel.voyager.navigator.tab.TabNavigator import com.atproto.repo.StrongRef import com.morpho.app.model.bluesky.BskyPost import com.morpho.app.model.bluesky.DraftPost -import com.morpho.app.model.bluesky.MorphoDataItem -import com.morpho.app.model.uidata.FeedUpdate +import com.morpho.app.model.uidata.Event import com.morpho.app.model.uidata.UIUpdate import com.morpho.app.screens.base.tabbed.ProfileTab import com.morpho.app.screens.base.tabbed.ThreadTab @@ -24,6 +23,7 @@ import io.ktor.util.reflect.instanceOf import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.IO import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.filterIsInstance import kotlinx.coroutines.launch import org.koin.compose.getKoin @@ -32,10 +32,11 @@ import org.koin.compose.getKoin fun TabbedSkylineFragment( paddingValues: PaddingValues = PaddingValues(0.dp), isProfileFeed: Boolean = false, - feedUpdate: StateFlow, - uiState: StateFlow, + uiUpdate: StateFlow, + eventCallback: (Event) -> Unit = {}, ) { val agent = getKoin().get() + val uiState = uiUpdate.collectAsState(initial = UIUpdate.Empty) val navigator = if (LocalNavigator.current?.parent?.instanceOf(TabNavigator::class) == true) { LocalNavigator.currentOrThrow } else LocalNavigator.currentOrThrow.parent!! @@ -72,28 +73,24 @@ fun TabbedSkylineFragment( } val clipboard = getKoin().get() if(uiState.value !is UIUpdate.Empty) { - if(feedUpdate.value is FeedUpdate<*>) { - SkylineFragment( - onProfileClicked = { actor -> navigator.push(ProfileTab(actor)) }, - onItemClicked = { uri -> navigator.push(ThreadTab(uri)) }, - onUnClicked = { type, rkey -> agent.deleteRecord(type, rkey) }, - onRepostClicked = { onRepostClicked(it) }, - onMenuClicked = { option, post -> - doMenuOperation(option, post, - clipboardManager = clipboard, - uriHandler = uriHandler - ) }, - onReplyClicked = { onReplyClicked(it) }, - onLikeClicked = { ref -> agent.like(ref) }, - onPostButtonClicked = { onPostButtonClicked() }, - getContentHandling = { post -> listOf() }, - contentPadding = paddingValues, - isProfileFeed = isProfileFeed, - feedUpdate = feedUpdate as StateFlow>, - ) - } else { - LoadingCircle() - } + SkylineFragment( + onProfileClicked = { actor -> navigator.push(ProfileTab(actor)) }, + onItemClicked = { uri -> navigator.push(ThreadTab(uri)) }, + onUnClicked = { type, rkey -> agent.deleteRecord(type, rkey) }, + onRepostClicked = { onRepostClicked(it) }, + onMenuClicked = { option, post -> + doMenuOperation(option, post, + clipboardManager = clipboard, + uriHandler = uriHandler + ) }, + onReplyClicked = { onReplyClicked(it) }, + onLikeClicked = { ref -> agent.like(ref) }, + onPostButtonClicked = { onPostButtonClicked() }, + getContentHandling = { post -> listOf() }, + contentPadding = paddingValues, + isProfileFeed = isProfileFeed, + feedUpdate = uiUpdate.filterIsInstance(), + ) if(repostClicked) { RepostQueryDialog( onDismissRequest = { diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/notifications/NotificationsElement.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/notifications/NotificationsElement.kt index 551ca83..231128e 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/notifications/NotificationsElement.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/notifications/NotificationsElement.kt @@ -22,46 +22,48 @@ import com.morpho.app.ui.post.PostFragment import com.morpho.app.util.getFormattedDateTimeSince import com.morpho.butterfly.AtIdentifier import com.morpho.butterfly.AtUri +import com.morpho.butterfly.Did import com.morpho.butterfly.model.RecordType -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.firstOrNull +import kotlinx.coroutines.launch @Composable fun NotificationsElement( item: NotificationsListItem, showPost: Boolean = true, - getPost: suspend (AtUri) -> Flow, + getPost: suspend (AtUri) -> BskyPost?, onPostClicked: OnPostClicked, - onAvatarClicked: (AtIdentifier) -> Unit = {}, + onAvatarClicked: (Did) -> Unit = {}, onReplyClicked: (BskyPost) -> Unit = { }, onRepostClicked: (BskyPost) -> Unit = { }, onLikeClicked: (StrongRef) -> Unit = { }, onMenuClicked: (MenuOptions, BskyPost) -> Unit = { _, _ -> }, onUnClicked: (type: RecordType, uri: AtUri) -> Unit = { _, _ -> }, readOnLoad: Boolean = false, - markRead: (AtUri) -> Unit = { } + markRead: (AtUri) -> Unit = { }, + resolveHandle: suspend (AtIdentifier) -> Did?, ) { var expand by remember { mutableStateOf(showPost) } var post: BskyPost? by remember { mutableStateOf(null) } val delta = remember { getFormattedDateTimeSince(item.notifications.first().indexedAt) } + val scope = rememberCoroutineScope() LaunchedEffect(expand) { @Suppress("REDUNDANT_ELSE_IN_WHEN") when (val notif = item.notifications.first()) { is BskyNotification.Like -> { - if(showPost) post = getPost(notif.subject.uri).firstOrNull() + if(showPost) post = getPost(notif.subject.uri) } is BskyNotification.Follow -> {} is BskyNotification.Post -> { post = notif.post - if(showPost) post = getPost(notif.uri).firstOrNull() + if(showPost) post = getPost(notif.uri) } is BskyNotification.Repost -> { - if(showPost) post = getPost(notif.subject.uri).firstOrNull() + if(showPost) post = getPost(notif.subject.uri) } is BskyNotification.Unknown -> { if (notif.reasonSubject != null && showPost) { - post = getPost(notif.reasonSubject!!).firstOrNull() + post = getPost(notif.reasonSubject!!) } } @@ -132,6 +134,7 @@ fun NotificationsElement( NotificationText(reason = item.reason, number = number, name = firstName, delta = delta) if (expand && post != null) { // TODO: maybe do a more compact variant + PostFragment( post = post!!, elevate = true, onItemClicked = { @@ -140,8 +143,10 @@ fun NotificationsElement( }, onProfileClicked = { if(!readOnLoad) markRead(item.notifications.first().uri) - onAvatarClicked(it) - }, + scope.launch { + resolveHandle(it)?.let { did -> onAvatarClicked(did) } + } + }, onUnClicked = { type, uri -> if(!readOnLoad) markRead(item.notifications.first().uri) onUnClicked(type, uri) diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/post/NotFoundPostFragment.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/post/NotFoundPostFragment.kt index 71f7039..d048f57 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/post/NotFoundPostFragment.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/post/NotFoundPostFragment.kt @@ -88,8 +88,12 @@ fun PlaceholderSkylineItem( val bgColor = if (role == PostFragmentRole.PrimaryThreadRoot) { MaterialTheme.colorScheme.background } else { - MaterialTheme.colorScheme.surfaceColorAtElevation(if (elevate ) 2.dp else - if (indentLevel > 0) (indentLevel*2).dp else 0.dp) + MaterialTheme.colorScheme + .surfaceColorAtElevation( + if (elevate ) 2.dp + else if (indentLevel > 0) (indentLevel*2).dp + else 0.dp + ) } Surface ( @@ -103,96 +107,95 @@ fun PlaceholderSkylineItem( .align(Alignment.End) ) { - Row( - modifier = Modifier.padding(end = 6.dp) + Row( + modifier = Modifier.padding(end = 6.dp) + .fillMaxWidth()//.fillMaxWidth(indentLevel(indent)) + ) { + + if (indent < 2) { + OutlinedAvatar( + url = "", + contentDescription = "Placeholder avatar", + size = 45.dp, + outlineColor = MaterialTheme.colorScheme.background, + avatarShape = AvatarShape.Corner, + modifier = Modifier.padding(end = 2.dp) + ) + } + + Column( + Modifier + .padding(top = 4.dp, start = 2.dp, end = 6.dp) .fillMaxWidth()//.fillMaxWidth(indentLevel(indent)) ) { - if (indent < 2) { - OutlinedAvatar( - url = "", - contentDescription = "Placeholder avatar", - size = 45.dp, - outlineColor = MaterialTheme.colorScheme.background, - avatarShape = AvatarShape.Corner, - modifier = Modifier.padding(end = 2.dp) - ) - } - - Column( - Modifier - .padding(top = 4.dp, start = 2.dp, end = 6.dp) - .fillMaxWidth()//.fillMaxWidth(indentLevel(indent)) + Row( + modifier = Modifier.padding(top = 2.dp, start = 2.dp, end = 4.dp), + horizontalArrangement = Arrangement.End ) { - - Row( - modifier = Modifier.padding(top = 2.dp, start = 2.dp, end = 4.dp), - horizontalArrangement = Arrangement.End - ) { - if (indent >= 2) { - OutlinedAvatar( - url = "", - contentDescription = "Placeholder avatar", - size = 30.dp, - avatarShape = AvatarShape.Rounded, - outlineColor = MaterialTheme.colorScheme.background, - ) - } - Text( - text = buildAnnotatedString { - withStyle( - style = SpanStyle( - color = MaterialTheme.colorScheme.onSurface, - fontSize = MaterialTheme.typography.labelLarge.fontSize, - fontWeight = FontWeight.Medium - ) - ) { - " " - } - withStyle( - style = SpanStyle( - color = MaterialTheme.colorScheme.onSurfaceVariant, - fontSize = MaterialTheme.typography.labelLarge.fontSize.times( - 0.8f - ) - ) - ) { - append("@ ") - } - - }, - maxLines = 1, - style = MaterialTheme.typography.labelLarge, - overflow = TextOverflow.Ellipsis, - modifier = Modifier - .wrapContentWidth(Alignment.Start) - .weight(10.0F) - .alignByBaseline() - ) - - Spacer(modifier = Modifier.width(1.dp).weight(0.1F)) - Text( - text = " ", - color = MaterialTheme.colorScheme.onSurfaceVariant, - style = MaterialTheme.typography.labelLarge, - fontSize = MaterialTheme.typography.labelLarge.fontSize.div(1.2F), - modifier = Modifier - .wrapContentWidth(Alignment.End) - //.weight(3.0F) - .alignByBaseline(), - maxLines = 1, - overflow = TextOverflow.Visible, - softWrap = false, + if (indent >= 2) { + OutlinedAvatar( + url = "", + contentDescription = "Placeholder avatar", + size = 30.dp, + avatarShape = AvatarShape.Rounded, + outlineColor = MaterialTheme.colorScheme.background, ) } + Text( + text = buildAnnotatedString { + withStyle( + style = SpanStyle( + color = MaterialTheme.colorScheme.onSurface, + fontSize = MaterialTheme.typography.labelLarge.fontSize, + fontWeight = FontWeight.Medium + ) + ) { + " " + } + withStyle( + style = SpanStyle( + color = MaterialTheme.colorScheme.onSurfaceVariant, + fontSize = MaterialTheme.typography.labelLarge.fontSize.times( + 0.8f + ) + ) + ) { + append("@ ") + } - Spacer(Modifier.height(100.dp)) + }, + maxLines = 1, + style = MaterialTheme.typography.labelLarge, + overflow = TextOverflow.Ellipsis, + modifier = Modifier + .wrapContentWidth(Alignment.Start) + .weight(10.0F) + .alignByBaseline() + ) - DummyPostActions() + Spacer(modifier = Modifier.width(1.dp).weight(0.1F)) + Text( + text = " ", + color = MaterialTheme.colorScheme.onSurfaceVariant, + style = MaterialTheme.typography.labelLarge, + fontSize = MaterialTheme.typography.labelLarge.fontSize.div(1.2F), + modifier = Modifier + .wrapContentWidth(Alignment.End) + //.weight(3.0F) + .alignByBaseline(), + maxLines = 1, + overflow = TextOverflow.Visible, + softWrap = false, + ) } - } + Spacer(Modifier.height(100.dp)) + + DummyPostActions() + } } + } } -} \ No newline at end of file +} diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/post/PollBlueEmbed.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/post/PollBlueEmbed.kt index 6a413af..62c8cd1 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/post/PollBlueEmbed.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/post/PollBlueEmbed.kt @@ -25,11 +25,11 @@ import com.morpho.app.data.stripPollOptionCharacters import com.morpho.app.model.bluesky.BskyFacet import com.morpho.app.model.bluesky.BskyPost import com.morpho.app.model.bluesky.FacetType -import com.morpho.app.model.uidata.ContentHandling import com.morpho.app.ui.elements.RichTextElement import com.morpho.app.util.openBrowser import com.morpho.app.util.utf8Slice import com.morpho.butterfly.AtIdentifier +import com.morpho.butterfly.ContentHandling import com.morpho.butterfly.Uri import kotlinx.coroutines.launch import org.koin.compose.getKoin diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/post/PostFragment.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/post/PostFragment.kt index 51e602d..2b04281 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/post/PostFragment.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/post/PostFragment.kt @@ -23,19 +23,16 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.withStyle import androidx.compose.ui.unit.dp import androidx.compose.ui.util.fastAny +import com.atproto.label.Blurs import com.atproto.repo.StrongRef import com.morpho.app.model.bluesky.* -import com.morpho.app.model.uidata.ContentHandling -import com.morpho.app.model.uidata.LabelDescription -import com.morpho.app.model.uidata.LabelIcon import com.morpho.app.ui.common.OnPostClicked import com.morpho.app.ui.elements.* import com.morpho.app.ui.lists.FeedListEntryFragment import com.morpho.app.ui.lists.UserListEntryFragment import com.morpho.app.util.getFormattedDateTimeSince import com.morpho.app.util.openBrowser -import com.morpho.butterfly.AtIdentifier -import com.morpho.butterfly.AtUri +import com.morpho.butterfly.* import com.morpho.butterfly.model.RecordType import kotlinx.collections.immutable.toImmutableList import morpho.app.ui.utils.indentLevel @@ -95,7 +92,7 @@ fun PostFragment( val contentHandling = remember { if (post.author.mutedByMe) { getContentHandling(post) + ContentHandling( - scope = LabelScope.Content, + scope = Blurs.CONTENT, id = "muted", icon = LabelIcon.EyeSlash(labelerAvatar = null), action = LabelAction.Blur, @@ -125,7 +122,7 @@ fun PostFragment( ) { ContentHider( reasons = contentHandling, - scope = LabelScope.Content, + scope = Blurs.CONTENT, ) { Row( modifier = Modifier.padding(end = 6.dp) @@ -350,7 +347,7 @@ inline fun ColumnScope.PostFeatureElement( is BskyPostFeature.ImagesFeature -> { ContentHider( reasons = contentHandling, - scope = LabelScope.Media, + scope = Blurs.MEDIA, modifier = Modifier.padding(horizontal = 2.dp) ) { PostImages(imagesFeature = feature, @@ -361,7 +358,7 @@ inline fun ColumnScope.PostFeatureElement( is BskyPostFeature.MediaRecordFeature -> { ContentHider( reasons = contentHandling, - scope = LabelScope.Media, + scope = Blurs.MEDIA, ) { RecordFeature( record = feature.record, @@ -376,7 +373,7 @@ inline fun ColumnScope.PostFeatureElement( is BskyPostFeature.RecordFeature -> { ContentHider( reasons = contentHandling, - scope = LabelScope.Content, + scope = Blurs.MEDIA, ) { RecordFeature( record = feature.record, @@ -390,7 +387,7 @@ inline fun ColumnScope.PostFeatureElement( is BskyPostFeature.VideoFeature -> { ContentHider( reasons = contentHandling, - scope = LabelScope.Media, + scope = Blurs.MEDIA, ) { VideoEmbedThumb( video = feature.video, @@ -425,7 +422,7 @@ inline fun ColumnScope.RecordFeature( ContentHider( reasons = contentHandling, - scope = LabelScope.Media, + scope = Blurs.MEDIA, modifier = Modifier .padding(horizontal = 2.dp) .align(Alignment.CenterHorizontally) @@ -463,7 +460,7 @@ inline fun ColumnScope.RecordFeature( if(record != null) { ContentHider( reasons = contentHandling, - scope = LabelScope.Content, + scope = Blurs.CONTENT, modifier = Modifier.align(Alignment.CenterHorizontally) ) { when (record) { diff --git a/Morpho/composeApp/src/desktopMain/kotlin/com/morpho/app/ui/common/TabbedScreenScaffold.desktop.kt b/Morpho/composeApp/src/desktopMain/kotlin/com/morpho/app/ui/common/TabbedScreenScaffold.desktop.kt index 437dcef..f895f08 100644 --- a/Morpho/composeApp/src/desktopMain/kotlin/com/morpho/app/ui/common/TabbedScreenScaffold.desktop.kt +++ b/Morpho/composeApp/src/desktopMain/kotlin/com/morpho/app/ui/common/TabbedScreenScaffold.desktop.kt @@ -10,17 +10,16 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.input.nestedscroll.NestedScrollConnection import androidx.compose.ui.input.nestedscroll.nestedScroll -import com.morpho.app.model.bluesky.MorphoDataItem +import com.morpho.app.model.uidata.Event import com.morpho.app.model.uistate.ContentCardState import com.morpho.app.ui.elements.WrappedColumn -import kotlinx.coroutines.flow.StateFlow @Composable actual fun TabbedScreenScaffold( navBar: @Composable () -> Unit, - content: @Composable (PaddingValues, StateFlow?) -> Unit, + content: @Composable (PaddingValues, T?) -> Unit, topContent: @Composable () -> Unit, - state: StateFlow?, + state: T?, modifier: Modifier, ) { Scaffold( @@ -40,11 +39,11 @@ actual fun TabbedScreenScaffold( @OptIn(ExperimentalMaterial3Api::class) @Composable -actual fun TabbedProfileScreenScaffold( +actual fun TabbedProfileScreenScaffold( navBar: @Composable () -> Unit, - content: @Composable (PaddingValues, StateFlow>?) -> Unit, + content: @Composable (PaddingValues, ContentCardState?) -> Unit, topContent: @Composable (TopAppBarScrollBehavior) -> Unit, - state: StateFlow>?, + state: ContentCardState?, modifier: Modifier, scrollBehavior: TopAppBarScrollBehavior, nestedScrollConnection: NestedScrollConnection, diff --git a/Morpho/composeApp/src/nativeMain/kotlin/com/morpho/app/Platform.native.kt b/Morpho/composeApp/src/nativeMain/kotlin/com/morpho/app/Platform.native.kt deleted file mode 100644 index 9b56323..0000000 --- a/Morpho/composeApp/src/nativeMain/kotlin/com/morpho/app/Platform.native.kt +++ /dev/null @@ -1,6 +0,0 @@ -package com.morpho.app - -actual val myCountry: String? - get() = TODO("Not yet implemented") -actual val myLang: String? - get() = TODO("Not yet implemented") \ No newline at end of file From d2e06a7ac03deb16e35c10175116b16ec58f9051 Mon Sep 17 00:00:00 2001 From: Orual Date: Mon, 16 Sep 2024 21:39:24 -0400 Subject: [PATCH 12/42] Big UI refactoring feels like it's nearing completion. App starts up again (on desktop at least). Now to just make sure events are being dispatched correctly. --- .../ui/common/TabbedScreenScaffold.android.kt | 2 +- .../ui/common/TabbedScreenScaffold.apple.kt | 2 +- .../com/morpho/app/data/MorphoDataSource.kt | 10 +- .../kotlin/com/morpho/app/di/AppModule.kt | 23 +- .../app/model/bluesky/BskyPostThread.kt | 33 +- .../app/model/bluesky/FeedSourceInfo.kt | 4 +- .../app/model/bluesky/MorphoDataFeed.kt | 591 ------------------ .../app/model/bluesky/MorphoDataItem.kt | 4 +- .../com/morpho/app/model/uidata/Events.kt | 2 + .../com/morpho/app/model/uidata/Moment.kt | 109 ---- .../com/morpho/app/model/uidata/MorphoData.kt | 21 +- .../com/morpho/app/model/uidata/UIUpdate.kt | 2 +- .../app/model/uistate/TabbedScreenState.kt | 58 -- .../app/screens/base/BaseScreenModel.kt | 6 +- .../app/screens/base/tabbed/NavigationTabs.kt | 9 +- .../screens/base/tabbed/TabbedBaseScreen.kt | 7 +- .../app/screens/main/MainScreenModel.kt | 1 + .../app/screens/main/tabbed/TabbedHomeView.kt | 6 +- .../main/tabbed/TabbedMainScreenModel.kt | 20 + .../notifications/NotificationsView.kt | 10 +- .../morpho/app/screens/thread/ThreadView.kt | 23 +- .../app/ui/common/SkylineThreadFragment.kt | 4 +- .../app/ui/common/TabbedScreenScaffold.kt | 2 +- .../app/ui/common/TabbedSkylineFragment.kt | 7 +- .../morpho/app/ui/notifications/ReasonIcon.kt | 10 +- .../morpho/app/ui/post/FullPostFragment.kt | 13 +- .../morpho/app/ui/thread/ThreadFragment.kt | 4 +- .../com/morpho/app/ui/thread/ThreadItem.kt | 2 +- .../com/morpho/app/ui/thread/ThreadReply.kt | 2 +- .../com/morpho/app/ui/thread/ThreadTree.kt | 2 +- .../kotlin/com/morpho/app/util/LinkParsing.kt | 3 +- .../ui/common/TabbedScreenScaffold.desktop.kt | 2 +- .../composeApp/src/desktopMain/kotlin/main.kt | 4 +- 33 files changed, 134 insertions(+), 864 deletions(-) delete mode 100644 Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/MorphoDataFeed.kt delete mode 100644 Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uistate/TabbedScreenState.kt diff --git a/Morpho/composeApp/src/androidMain/kotlin/com/morpho/app/ui/common/TabbedScreenScaffold.android.kt b/Morpho/composeApp/src/androidMain/kotlin/com/morpho/app/ui/common/TabbedScreenScaffold.android.kt index 5aa82de..b405b82 100644 --- a/Morpho/composeApp/src/androidMain/kotlin/com/morpho/app/ui/common/TabbedScreenScaffold.android.kt +++ b/Morpho/composeApp/src/androidMain/kotlin/com/morpho/app/ui/common/TabbedScreenScaffold.android.kt @@ -35,7 +35,7 @@ actual fun TabbedScreenScaffold( @Composable actual fun TabbedProfileScreenScaffold( navBar: @Composable () -> Unit, - content: @Composable (PaddingValues, ContentCardState?) -> Unit, + content: @Composable (PaddingValues, ContentCardState< out T>?) -> Unit, topContent: @Composable (TopAppBarScrollBehavior) -> Unit, state: ContentCardState?, modifier: Modifier, diff --git a/Morpho/composeApp/src/appleMain/kotlin/com/morpho/app/ui/common/TabbedScreenScaffold.apple.kt b/Morpho/composeApp/src/appleMain/kotlin/com/morpho/app/ui/common/TabbedScreenScaffold.apple.kt index d076299..2f37bb8 100644 --- a/Morpho/composeApp/src/appleMain/kotlin/com/morpho/app/ui/common/TabbedScreenScaffold.apple.kt +++ b/Morpho/composeApp/src/appleMain/kotlin/com/morpho/app/ui/common/TabbedScreenScaffold.apple.kt @@ -35,7 +35,7 @@ actual fun TabbedScreenScaffold( @Composable actual fun TabbedProfileScreenScaffold( navBar: @Composable () -> Unit, - content: @Composable (PaddingValues, ContentCardState?) -> Unit, + content: @Composable (PaddingValues, ContentCardState?) -> Unit, topContent: @Composable (TopAppBarScrollBehavior) -> Unit, state: ContentCardState?, modifier: Modifier, diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/data/MorphoDataSource.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/data/MorphoDataSource.kt index ade276d..cd9df84 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/data/MorphoDataSource.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/data/MorphoDataSource.kt @@ -203,9 +203,9 @@ suspend fun List.collectThreads( val oldEntry = threadToSplice.thread.parents.first() as ThreadPost.ViewablePost val newReplies = (newEntry.replies + oldEntry.replies).distinctBy { it.uri }.toMutableList() - newReplies.add(ThreadPost.ViewablePost(thread.thread.post, thread.thread.replies)) + newReplies.add(ThreadPost.ViewablePost(thread.thread.post, thread.thread.parents.last(), thread.thread.replies)) if( thread.getUri() != threadToSplice.getUri() ) - newReplies.add(ThreadPost.ViewablePost(threadToSplice.thread.post, threadToSplice.thread.replies)) + newReplies.add(ThreadPost.ViewablePost(threadToSplice.thread.post, threadToSplice.thread.parents.last(),threadToSplice.thread.replies)) val newThread = BskyPostThread( post = newEntry.post, parents = listOf(), @@ -222,8 +222,8 @@ suspend fun List.collectThreads( if(threadToSplice.thread.parents.lastOrNull() !is ThreadPost.ViewablePost) return@forEachIndexed val newParent = thread.thread.parents.last() as ThreadPost.ViewablePost val oldParent = threadToSplice.thread.parents.last() as ThreadPost.ViewablePost - val newReply = ThreadPost.ViewablePost(thread.thread.post, thread.thread.replies) - val oldReply = ThreadPost.ViewablePost(threadToSplice.thread.post, threadToSplice.thread.replies) + val newReply = ThreadPost.ViewablePost(thread.thread.post, thread.thread.parents.last(), thread.thread.replies) + val oldReply = ThreadPost.ViewablePost(threadToSplice.thread.post, threadToSplice.thread.parents.last(), threadToSplice.thread.replies) newParent.addReply(newReply) oldParent.addReply(oldReply) newReplies.add(newReply) @@ -242,7 +242,7 @@ suspend fun List.collectThreads( val inThreads = threads.indexOfFirst { t -> t?.containsUri(thread.thread.post.uri) ?: false } if (inThreads == - 1) { val threadToSplice = threads.getOrNull(index) ?: return@forEachIndexed - threads[index] = threadToSplice.addReply(ThreadPost.ViewablePost(thread.thread.post, thread.thread.replies)) + threads[index] = threadToSplice.addReply(ThreadPost.ViewablePost(thread.thread.post, thread.thread.parents.last(), thread.thread.replies)) threadCandidates[index] = null } } diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/di/AppModule.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/di/AppModule.kt index 4cd4112..5c52247 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/di/AppModule.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/di/AppModule.kt @@ -1,8 +1,8 @@ package com.morpho.app.di +import com.morpho.app.data.MorphoAgent import com.morpho.app.data.PollBlueService import com.morpho.app.data.PreferencesRepository -import com.morpho.app.model.bluesky.MorphoDataItem import com.morpho.app.model.uidata.* import com.morpho.app.screens.base.BaseScreenModel import com.morpho.app.screens.login.LoginScreenModel @@ -10,7 +10,6 @@ import com.morpho.app.screens.main.MainScreenModel import com.morpho.app.screens.main.tabbed.TabbedMainScreenModel import com.morpho.app.util.ClipboardManager import com.morpho.butterfly.AtpAgent -import com.morpho.butterfly.Butterfly import com.morpho.butterfly.ButterflyAgent import com.morpho.butterfly.auth.SessionRepository import com.morpho.butterfly.auth.UserRepository @@ -25,16 +24,12 @@ import org.koin.dsl.module val appModule = module { single { BaseScreenModel() } - factory { MainScreenModel() } - factory { TabbedMainScreenModel() } - factory { TabbedProfileViewModel() } - factory { TabbedNotificationScreenModel() } - factory { LoginScreenModel() } + single { MainScreenModel() } + single { TabbedMainScreenModel() } + single { LoginScreenModel() } factory { p-> UpdateTick(p.get()) } single { ClipboardManager } - factory { p -> UserListPresenter(p.get()) } - factory { p -> UserFeedsPresenter(p.get()) } - factory> { p -> FeedPresenter(p.get()) } + } val storageModule = module { @@ -44,14 +39,14 @@ val storageModule = module { } val dataModule = module { - single { Butterfly() } single { AtpAgent() } single { ButterflyAgent() } - single { BskyDataService() } - single { BskyNotificationService() } + single { MorphoAgent() } single { ContentLabelService() } single { PollBlueService() } - single { SettingsService() } + factory { p -> UserListPresenter(p.get()) } + factory { p -> UserFeedsPresenter(p.get()) } + factory> { p -> FeedPresenter(p.get()) } } @Suppress("MemberVisibilityCanBePrivate") diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/BskyPostThread.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/BskyPostThread.kt index 414f4c0..6e5a9e4 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/BskyPostThread.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/BskyPostThread.kt @@ -190,6 +190,7 @@ sealed interface ThreadPost:Parcelable { @Serializable data class ViewablePost( val post: BskyPost, + val parent: ThreadPost? = null, val replies: List = persistentListOf(), ) : ThreadPost { override val uri: AtUri @@ -339,7 +340,7 @@ sealed interface ThreadPost:Parcelable { fun addReply(reply: ViewablePost): ThreadPost { return when(this) { - is ViewablePost -> ViewablePost(post, (replies + reply).distinctBy { it.uri }) + is ViewablePost -> ViewablePost(post, parent, (replies + reply).distinctBy { it.uri }) is BlockedPost -> BlockedPost(uri) is NotFoundPost -> NotFoundPost(uri) } @@ -365,7 +366,7 @@ fun ThreadViewPost.toThread(): BskyPostThread { return BskyPostThread( post = post.toPost(), parents = persistentListOf(), - replies = replies.mapImmutable { it.toThreadPost(post.toPost(), post.toPost()) } + replies = replies.mapImmutable { it.toThreadPost() } ) } else { val rootPost = parents.last().toPost() @@ -379,32 +380,48 @@ fun ThreadViewPost.toThread(): BskyPostThread { } else { parents[index + 1].toPost() }, - rootPost ) }.reversed().toImmutableList(), - replies = replies.mapImmutable { reply -> reply.toThreadPost(entryPost, rootPost) }, + replies = replies.mapImmutable { reply -> reply.toThreadPost() }, ) } } -fun ThreadViewPost.toThreadPost(parent: BskyPost, root: BskyPost): ThreadPost { +fun ThreadViewPost.toThreadPost( root: BskyPost): ThreadPost { val post = post.toPost(null, null) return ViewablePost( post = post, - replies = replies.mapImmutable { it.toThreadPost(post, root) } + parent = parent?.toThreadPost(), + replies = replies.mapImmutable { it.toThreadPost() } ) } -fun ThreadViewPostReplyUnion.toThreadPost(parent: BskyPost, root: BskyPost): ThreadPost = when (this) { +fun ThreadViewPostReplyUnion.toThreadPost(): ThreadPost = when (this) { is ThreadViewPostReplyUnion.ThreadViewPost -> { val post = value.post.toPost(null, null) ViewablePost( post = post, - replies = value.replies.mapImmutable { it.toThreadPost(post, root) } + parent = value.parent?.toThreadPost(), + replies = value.replies.mapImmutable { it.toThreadPost() } ) } is ThreadViewPostReplyUnion.NotFoundPost -> NotFoundPost(value.uri) is ThreadViewPostReplyUnion.BlockedPost -> BlockedPost(value.uri) } +fun ThreadViewPostParentUnion.toThreadPost(): ThreadPost = when (this) { + is ThreadViewPostParentUnion.ThreadViewPost -> { + val post = value.post.toPost(null, null) + ViewablePost( + post = post, + parent = value.parent?.toThreadPost(), + replies = value.replies.mapImmutable { it.toThreadPost() } + ) + } + is ThreadViewPostParentUnion.NotFoundPost -> NotFoundPost(value.uri) + is ThreadViewPostParentUnion.BlockedPost -> BlockedPost(value.uri) +} + + + diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/FeedSourceInfo.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/FeedSourceInfo.kt index f1ef193..a388845 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/FeedSourceInfo.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/FeedSourceInfo.kt @@ -101,7 +101,7 @@ sealed interface FeedSourceInfo: Parcelable { override val displayName: String = "Home" override val description: String = "Your home feed, currently same as Following" override val creatorDid: Did = Did("did:web:morpho.app") - override val creatorHandle: Handle = Handle(displayName) + override val creatorHandle: Handle = Handle("${displayName.lowercase()}.morpho.app") override val feedDescriptor: FeedDescriptor = FeedDescriptor.Home override val type: Nsid = Nsid("app.morpho.feed.home") } @@ -115,7 +115,7 @@ sealed interface FeedSourceInfo: Parcelable { override val avatar: String? = null override val displayName: String = "Following" override val description: String = "Your feed of people you follow" - override val creatorHandle: Handle = Handle(displayName) + override val creatorHandle: Handle = Handle("${displayName.lowercase()}.morpho.app") override val feedDescriptor: FeedDescriptor = FeedDescriptor.Home override val type: Nsid = Nsid("app.morpho.feed.following") } diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/MorphoDataFeed.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/MorphoDataFeed.kt deleted file mode 100644 index c4be86c..0000000 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/MorphoDataFeed.kt +++ /dev/null @@ -1,591 +0,0 @@ -package com.morpho.app.model.bluesky - -//import com.rickclephas.kmp.nativecoroutines.NativeCoroutines -import androidx.compose.ui.util.* -import app.bsky.actor.ContentLabelPref -import app.bsky.feed.FeedViewPost -import app.bsky.feed.GetPostThreadQuery -import app.bsky.feed.GetPostThreadResponseThreadUnion -import com.morpho.app.model.uidata.AtCursor -import com.morpho.app.model.uidata.Delta -import com.morpho.app.model.uidata.MorphoData -import com.morpho.app.util.mapImmutable -import com.morpho.butterfly.* -import kotlinx.collections.immutable.persistentListOf -import kotlinx.collections.immutable.toImmutableList -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.* -import kotlinx.serialization.Serializable -import kotlin.time.Duration - - - -typealias TunerFunction = (List) -> List - - - - -@Suppress("unused", "UNCHECKED_CAST") -@Serializable -data class MorphoDataFeed ( - private var _items: MutableList = mutableListOf(), - var cursor: AtCursor = AtCursor.EMPTY, - val uri: AtUri = AtUri.HOME_URI, - var hasNewPosts: Boolean = false, -) { - val items: List = _items.toList() - companion object { - - fun fromPosts( - posts: List, - cursor: AtCursor = AtCursor.EMPTY, - ): MorphoDataFeed { - return MorphoDataFeed( - _items = posts.map { MorphoDataItem.Post(it) }.toMutableList(), - cursor = cursor, hasNewPosts = cursor == AtCursor.EMPTY - ) - } - - fun fromMorphoData( - data: MorphoData - ): MorphoDataFeed { - return MorphoDataFeed( - _items = data.items.toMutableList(), - cursor = data.cursor, hasNewPosts = data.cursor == AtCursor.EMPTY - ) - } - - - - - fun fromFeedGen( - feeds: List, - cursor: AtCursor = AtCursor.EMPTY, - ): MorphoDataFeed { - return MorphoDataFeed( - _items = feeds.map { MorphoDataItem.FeedInfo(it) }.toMutableList(), - cursor = cursor, hasNewPosts = cursor == AtCursor.EMPTY - ) - } - - fun fromProfileList( - list: List, - cursor: AtCursor = AtCursor.EMPTY, - ): MorphoDataFeed { - return MorphoDataFeed( - _items = list.map { MorphoDataItem.ProfileItem(it) }.toMutableList(), - cursor = cursor, hasNewPosts = cursor == AtCursor.EMPTY - ) - } - - fun fromBskyList( - lists: List, - cursor: AtCursor = AtCursor.EMPTY, - ): MorphoDataFeed { - return MorphoDataFeed( - _items = lists.map { MorphoDataItem.ListInfo(it) }.toMutableList(), - cursor = cursor, hasNewPosts = cursor == AtCursor.EMPTY - ) - } - - fun fromModLabelDefs( - labels: List, - cursor: AtCursor = AtCursor.EMPTY, - ): MorphoDataFeed { - return MorphoDataFeed( - _items = labels.map { MorphoDataItem.ModLabel(it) }.toMutableList(), - cursor = cursor, hasNewPosts = cursor == AtCursor.EMPTY - ) - } - - fun fromModServiceDefs( - services: List, - cursor: AtCursor = AtCursor.EMPTY, - ): MorphoDataFeed { - return MorphoDataFeed( - _items = services.map { MorphoDataItem.LabelService(it) }.toMutableList(), - cursor = cursor, hasNewPosts = cursor == AtCursor.EMPTY - ) - } - - - fun concat( - posts: List, - feed: MorphoDataFeed, - cursor: AtCursor = feed.cursor, - ): MorphoDataFeed { - return MorphoDataFeed( - _items = (posts.mapImmutable { MorphoDataItem.Post(it.toPost()) } + feed._items).toList() - .toMutableList(), - - cursor = cursor, hasNewPosts = cursor == AtCursor.EMPTY - ) - } - fun concat( - feed: MorphoDataFeed, - posts: List, - cursor: AtCursor = feed.cursor, - ): MorphoDataFeed { - return MorphoDataFeed( - _items = (feed._items + posts.mapImmutable { MorphoDataItem.Post(it.toPost()) }) - .toMutableList(), - cursor = cursor, hasNewPosts = cursor == AtCursor.EMPTY - ) - } - - fun concat( - first: MorphoDataFeed, - last: MorphoDataFeed, - cursor: AtCursor = last.cursor - ): MorphoDataFeed { - return MorphoDataFeed( - _items = (first._items + last._items).toMutableList(), - cursor = cursor, hasNewPosts = cursor == AtCursor.EMPTY - ) - } - - fun concat( - first: MorphoData, - last: MorphoDataFeed, - cursor: AtCursor = last.cursor - ): MorphoDataFeed { - return MorphoDataFeed( - _items = (first.items + last.items).toMutableList(), - cursor = cursor, hasNewPosts = cursor == AtCursor.EMPTY - ) - } - - fun concat( - first: MorphoDataFeed, - last: MorphoData, - cursor: AtCursor = last.cursor - ): MorphoDataFeed { - return MorphoDataFeed( - _items = (first.items + last.items).toMutableList(), - cursor = cursor, hasNewPosts = cursor == AtCursor.EMPTY - ) - } - - //@NativeCoroutines - fun collectThreads( - list: List, - depth: Int = 3, height: Int = 10, - timeRange: Delta = Delta(Duration.parse("4h")), - cursor: AtCursor = AtCursor.EMPTY, - ): Flow> = flow { - emit(collectThreads(fromPosts(list.toBskyPostList().fastMap { - when(it) { - is MorphoDataItem.Post -> it.post - is MorphoDataItem.Thread -> it.thread.post - } - }, cursor), depth, height, timeRange) - .distinctUntilChanged().last() - ) - }.flowOn(Dispatchers.Default) - - //@NativeCoroutines - fun collectThreads( - feed: MorphoDataFeed, - depth: Int = 3, height: Int = 10, - timeRange: Delta = Delta(Duration.parse("4h")), - cursor: AtCursor = feed.cursor, - ): Flow> = flow { - val threadCandidates = mutableMapOf>() - - feed._items.map { item -> - if (item is MorphoDataItem.Post) { - val post = item.post - if(post.reply != null) { - val itemCid = post.cid - val parent = post.reply.parentPost - val root = post.reply.rootPost - if(itemCid !in threadCandidates.keys) { - var found = false - threadCandidates.forEach { thread -> - if(itemCid in thread.value.keys) { - if (parent != null && parent.cid !in thread.value.keys) { - thread.value[parent.cid] = parent - } - if (root != null && root.cid !in thread.value.keys) { - thread.value[root.cid] = root - } - found = true - return@forEach - } else if (parent != null && parent.cid in thread.value.keys) { - if(parent.reply?.parentPost != null) { - thread.value[parent.reply.parentPost.cid] = parent.reply.parentPost - } - } - } - if(!found) { - threadCandidates[itemCid] = mutableMapOf() - if (parent != null) { - threadCandidates[itemCid]?.set(parent.cid, parent ) - if(parent.reply?.parentPost != null) { - threadCandidates[itemCid]?.set(parent.reply.parentPost.cid, parent.reply.parentPost) - } - } - if (root != null && threadCandidates[itemCid]?.keys?.contains(root.cid) != true ) { - threadCandidates[itemCid]?.set(root.cid, root ) - } - } - } else { - if (parent != null && threadCandidates[itemCid]?.keys?.contains(parent.cid) != true ) { - threadCandidates[itemCid]?.set(parent.cid, parent ) - if(parent.reply?.parentPost != null) { - threadCandidates[itemCid]?.set(parent.reply.parentPost.cid, parent.reply.parentPost) - } - } - if (root != null && threadCandidates[itemCid]?.keys?.contains(root.cid) != true ) { - threadCandidates[itemCid]?.set(root.cid, root ) - } - } - } - } - } - - val threads = mutableMapOf>() - threadCandidates.map { thread -> - if (thread.value.values.isNotEmpty()) threads[thread.key] = thread.value.values.toMutableList() - } - feed._items.mapIndexed { index, item -> - if (item is MorphoDataItem.Post){ - val post = item.post - val itemCid = post.cid - if( itemCid in threads.keys) { - val level = 1 - val thread = threads[itemCid] - ?.filter { (it.createdAt - post.createdAt).duration <= timeRange.duration } - ?.sortedByDescending { it.createdAt } - .orEmpty() - val parents: Flow = flow { - generateSequence(post.reply?.parentPost) { - it.reply?.parentPost - }.toList().reversed().map { r-> - emit(ThreadPost.ViewablePost( - r, - findReplies(level, height, r, thread.asFlow()) - .toList().toImmutableList() - )) - } - } - val replies: Flow = flow { - threads[itemCid]?.filter { - (it.reply?.parentPost?.cid ?: Cid("")) == itemCid - }?.map { p -> - emit(ThreadPost.ViewablePost( - p, - findReplies(level, depth, p, thread.asFlow()) - .toList().toImmutableList() - )) - }.orEmpty() - } - feed._items[index] = MorphoDataItem.Thread( - BskyPostThread( - post, - parents.toList().toImmutableList(), - replies.toList().toImmutableList() - ) - ) - } - } - - } - feed._items.sortedByDescending { - when(it) { - is MorphoDataItem.Post -> it.post.createdAt - is MorphoDataItem.Thread -> it.thread.post.createdAt - } - } - emit(MorphoDataFeed(feed._items, cursor, feed.uri, feed.hasNewPosts)) - }.flowOn(Dispatchers.Default) - - private fun findReplies(level: Int, depth: Int, post: BskyPost, list: Flow - ): Flow = flow { - list.filter { - (it.reply?.parentPost?.cid ?: Cid("")) == post.cid - }.map { - if (level < depth) { - val r = findReplies(level + 1, depth, it, list) - .distinctUntilChanged().toList() - emit(ThreadPost.ViewablePost(it, r.toImmutableList())) - } else { - emit(ThreadPost.ViewablePost(it)) - } - } - }.flowOn(Dispatchers.Default) - - fun filterByPrefs( - posts: List, - prefs: BskyFeedPref, - follows: List = persistentListOf(), - ): List { - var feed = posts.fastFilter { post -> // A-B test perf with fast and normal filter - if (post is MorphoDataItem.Post) { - (!prefs.hideReposts && post.reason is BskyPostReason.BskyPostRepost) - || (!prefs.hideQuotePosts && isQuotePost(post.post)) - || ((!prefs.hideReplies && (post.post.reply != null)) - && (isSelfReply(post.post) || isThreadRoot(post.post) || post.post.reposted - || (post.post.likeCount <= prefs.hideRepliesByLikeCount) ) - && (!prefs.hideRepliesByUnfollowed && isFollowingAllAuthors(post.post, follows)) ) - || (post.post.reply == null && !isQuotePost(post.post) && post.reason == null) - } else if (post is MorphoDataItem.Thread) { - (!prefs.hideQuotePosts && isQuotePost(post.thread.post)) - || ((!prefs.hideReplies && (post.thread.post.reply != null)) - && (isSelfReply(post.thread.post) || isThreadRoot(post.thread.post) || post.thread.post.reposted - || (post.thread.post.likeCount <= prefs.hideRepliesByLikeCount) ) - && (!prefs.hideRepliesByUnfollowed && isFollowingAllAuthors(post.thread.post, follows)) ) - || (post.thread.post.reply == null && !isQuotePost(post.thread.post) && post.thread.post.reason == null) - } else false - } - feed = filterByLanguage(feed, prefs.languages) - feed = filterByContentLabel(feed, prefs.labelsToHide) - return feed - } - - - - fun filterByLanguage( - posts: List, - languages: List, - ): List { - if (languages.isEmpty()) return posts - return posts.fastFilter { post -> - when(post) { - is MorphoDataItem.Post -> post.post.langs.any { languages.contains(it) } - is MorphoDataItem.Thread -> post.thread.post.langs.any { languages.contains(it) } - } - } - } - - fun filterByContentLabel( - posts: List, - toHide: List = persistentListOf(), - ): List { - return posts.fastFilter { post -> - when(post) { - is MorphoDataItem.Post -> post.post.labels.none { label -> toHide.fastAny { it.label == label.value } } - is MorphoDataItem.Thread -> post.thread.post.labels.none { label -> toHide.fastAny { it.label == label.value } } - } - } - } - - fun filterBy(did: Did, posts: List): List { - return posts.fastFilter { - when(it) { - is MorphoDataItem.Post -> it.post.author.did != did - is MorphoDataItem.Thread -> it.thread.post.author.did != did - } - } - } - - fun filterBy(string: String, posts: List) : List { - return posts.fastFilter { - when(it) { - is MorphoDataItem.Post -> it.post.text.contains(string) - is MorphoDataItem.Thread -> { - it.thread.post.text.contains(string) || it.thread.parents.any { parent -> - if (parent is ThreadPost.ViewablePost) { - parent.post.text.contains(string) - } else false - } || it.thread.replies.any { reply -> - if (reply is ThreadPost.ViewablePost) { - reply.post.text.contains(string) - } else false - } - } - } - } - } - - fun filterByWord(string: String, posts: List) : List { - return filterBy(Regex("""\b$string\b"""), posts) - } - - fun filterBy(regex: Regex, posts: List) : List { - return posts.fastFilter { - when(it) { - is MorphoDataItem.Post -> it.post.text.contains(regex) - is MorphoDataItem.Thread -> { - it.thread.post.text.contains(regex) || it.thread.parents.any { parent -> - if (parent is ThreadPost.ViewablePost) { - parent.post.text.contains(regex) - } else false - } || it.thread.replies.any { reply -> - if (reply is ThreadPost.ViewablePost) { - reply.post.text.contains(regex) - } else false - } - } - } - } - } - - fun dedupPosts(posts: List): List { - return posts.fastDistinctBy { post-> // A-B test perf with fast and normal distinctBy - when(post) { - is MorphoDataItem.Post -> post.post.cid - is MorphoDataItem.Thread -> post.thread.post.cid - is MorphoDataItem.FeedInfo -> post.feed.cid - is MorphoDataItem.ListInfo -> post.list.cid - is MorphoDataItem.ModLabel -> post.label.identifier - is MorphoDataItem.ProfileItem -> post.profile.did - is MorphoDataItem.LabelService -> post.service.cid - } - } - } - - fun collectThreads( - apiProvider: Butterfly, - cursor: AtCursor = AtCursor.EMPTY, - posts: List, - uri: AtUri = AtUri.HOME_URI, - depth: Long = 1, height: Long = 10, - ): Flow> = flow { - val threads: MutableMap = mutableMapOf() - posts.asReversed().fastMap { post -> - val reply = getIfSelfReply(post) - if ((reply != null) && posts.filterNot { it.uri == post.uri } - .none { threads.keys.contains(it.uri) || threads.values.any { thread-> - thread.contains(it.uri) - } }) { - if (reply.author.did == post.reply?.rootPost?.author?.did - && post.author.did == post.reply.rootPost.author.did - ) { - apiProvider.api.getPostThread( - GetPostThreadQuery( - reply.uri, - depth, - height - ) - ).onSuccess { - when (val thread = it.thread) { - is GetPostThreadResponseThreadUnion.BlockedPost -> {} - is GetPostThreadResponseThreadUnion.NotFoundPost -> {} - is GetPostThreadResponseThreadUnion.ThreadViewPost -> { - threads[post.uri] = thread.value.toThread() - } - } - } - } - } - } - var morphoDataItems: List = posts.fastMap { - val thread = threads[it.uri] - if (threads.containsKey(it.uri) && thread != null) { - MorphoDataItem.Thread(thread) - } else { - MorphoDataItem.Post(it) - } - } - morphoDataItems = morphoDataItems.fastFilter { item -> - threads.none { - item is MorphoDataItem.Post && it.value.contains(item.post.uri) - } - } - emit(MorphoDataFeed(_items = morphoDataItems.toMutableList(), cursor, uri, hasNewPosts = cursor == AtCursor.EMPTY)) - }.flowOn(Dispatchers.Default) - } - - fun collectThreads( - depth: Int = 3, height: Int = 80, - timeRange: Delta = Delta(Duration.parse("4h")) - ): Flow> = flow { - emit(collectThreads(this@MorphoDataFeed as MorphoDataFeed, - depth, height, timeRange).distinctUntilChanged().last()) - }.flowOn(Dispatchers.Default) - - fun dedupPosts() { - _items = Companion.dedupPosts(_items.toList()).toMutableList() as MutableList - } - - operator fun plus(feed: MorphoDataFeed) { - _items = (_items + feed._items).toMutableList() - cursor = feed.cursor - } - - operator fun contains(cid: Cid): Boolean { - return _items.fastAny { - when(it) { - is MorphoDataItem.Post -> it.post.cid == cid - is MorphoDataItem.Thread -> it.thread.contains(cid) - is MorphoDataItem.FeedInfo -> it.feed.cid == cid - is MorphoDataItem.ListInfo -> it.list.cid == cid - is MorphoDataItem.ModLabel -> false - is MorphoDataItem.ProfileItem -> false - is MorphoDataItem.LabelService -> it.service.cid == cid - else -> {false} - } - } - } - - - -} -fun List.toBskyPostList(): List { - return this.fastMap { MorphoDataItem.Post(it.toPost()) } -} - -@Suppress("UNCHECKED_CAST") -fun List.tune( - tuners: List = persistentListOf(), -) : List { - var feed = MorphoDataFeed.dedupPosts(this ) - tuners.fastForEach { tuner-> - feed = tuner(feed as List) - } - return feed as List -} - - -fun isFollowingAllAuthors(post: BskyPost, follows: List): Boolean { - return follows.fastAny { - (post.author.did == it - || post.reply?.parentPost?.author?.did == it - || post.reply?.rootPost?.author?.did == it) - } -} - -fun isQuotePost(post: BskyPost) : Boolean { - return when(post.feature) { - is BskyPostFeature.MediaRecordFeature -> true - is BskyPostFeature.RecordFeature -> true - else -> false - } -} - -fun isSelfReply(post: BskyPost) : Boolean { - return if (post.reply != null) { - if(post.reply.parentPost?.author?.did == post.author.did) { - true - } else post.reply.rootPost?.author?.did == post.author.did - } else { - false - } -} - -fun getIfSelfReply(post: BskyPost) : BskyPost? { - return if (post.reply != null) { - if(post.reply.parentPost?.author?.did == post.author.did) { - post.reply.parentPost - } else if (post.reply.rootPost?.author?.did == post.author.did) { - post.reply.rootPost - } else { - null - } - } else { - null - } -} - -fun isInThread(post: BskyPost) : Boolean { - return post.reply != null -} - -fun isThreadRoot(post: BskyPost) : Boolean { - return (post.replyCount > 0 && post.reply == null) -} - -fun isSecondInThread(post: BskyPost) : Boolean { - return (post.reply?.parentPost == post.reply?.rootPost && post.replyCount > 0) -} \ No newline at end of file diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/MorphoDataItem.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/MorphoDataItem.kt index d5a0e5e..8c15fb8 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/MorphoDataItem.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/MorphoDataItem.kt @@ -108,7 +108,7 @@ sealed interface MorphoDataItem: Parcelable { Post(post.toPost(reply.toReply(), null), null, isOrphan = isOrphan) } else { val parents = items.map { - ThreadPost.ViewablePost(it.toPost(), listOf()) + ThreadPost.ViewablePost(it.toPost(), null, listOf()) } Thread( BskyPostThread( @@ -130,7 +130,7 @@ sealed interface MorphoDataItem: Parcelable { Post(post.toPost(reply.toReply(), null), null, isOrphan = isOrphan) } else { val parents = items.map { - ThreadPost.ViewablePost(it.toPost(), listOf()) + ThreadPost.ViewablePost(it.toPost(), null,listOf()) } Thread( BskyPostThread( diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/Events.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/Events.kt index dbbf080..9763d1f 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/Events.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/Events.kt @@ -14,7 +14,9 @@ import com.morpho.butterfly.Did import com.morpho.butterfly.model.Timestamp import io.github.vinceglb.filekit.core.PlatformFile import kotlinx.datetime.Clock +import kotlinx.serialization.Serializable +@Serializable sealed interface Event { data class UpdateSeenNotifications( val seenAt: Timestamp = Clock.System.now() diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/Moment.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/Moment.kt index e747695..f16f152 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/Moment.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/Moment.kt @@ -1,16 +1,9 @@ package com.morpho.app.model.uidata import androidx.compose.runtime.Immutable -import com.morpho.app.model.bluesky.BskyPostThread -import com.morpho.app.model.bluesky.MorphoDataItem -import com.morpho.app.model.uistate.ContentCardState.ProfileTimeline import com.morpho.butterfly.json import dev.icerock.moko.parcelize.Parcel import dev.icerock.moko.parcelize.Parceler -import kotlinx.coroutines.flow.MutableSharedFlow -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asStateFlow import kotlinx.datetime.Instant import kotlinx.serialization.Serializable import kotlinx.serialization.json.JsonElement @@ -73,105 +66,3 @@ object JsonElementParceler : Parceler{ } } -object AtCursorMutableSharedFlowParceler : Parceler>{ - override fun create(parcel: Parcel): MutableSharedFlow { - val serialized = parcel.readString() - val flow = initAtCursor() - if (serialized != null) { - json.decodeFromString(AtCursor.serializer(), serialized).let { cursor -> - flow.tryEmit(cursor) - } - } - return flow - } - - override fun MutableSharedFlow.write(parcel: Parcel, flags: Int) { - val serialized = json.encodeToString(AtCursor.serializer(), this.replayCache.lastOrNull() ?: AtCursor.EMPTY) - parcel.writeString(serialized) - } -} - -object PostThreadStateFlowParceler : Parceler>{ - override fun create(parcel: Parcel): StateFlow { - val serialized = parcel.readString() - val flow = MutableStateFlow(null) - return flow.asStateFlow() - } - - override fun StateFlow.write(parcel: Parcel, flags: Int) { - if(this.value == null) { - parcel.writeString("null") - return - } - val serialized = json.encodeToString(BskyPostThread.serializer(), this.value!!) - parcel.writeString(serialized) - } -} - -object ProfileTimelineStateFlowParceler : Parceler?>>{ - override fun create(parcel: Parcel): StateFlow?> { - val serialized = parcel.readString() - val flow = MutableStateFlow(null) - return flow.asStateFlow() - } - - override fun StateFlow?>.write(parcel: Parcel, flags: Int) { - if(this.value == null) { - parcel.writeString("null") - return - } - - parcel.writeString("${this.value!!.uri}") - } -} - -object ProfileListsStateFlowParceler : Parceler?>>{ - override fun create(parcel: Parcel): StateFlow?> { - val serialized = parcel.readString() - val flow = MutableStateFlow(null) - return flow.asStateFlow() - } - - override fun StateFlow?>.write(parcel: Parcel, flags: Int) { - if(this.value == null) { - parcel.writeString("null") - return - } - - parcel.writeString("${this.value!!.uri}") - } -} - -object ProfileFeedsStateFlowParceler : Parceler?>>{ - override fun create(parcel: Parcel): StateFlow?> { - val serialized = parcel.readString() - val flow = MutableStateFlow(null) - return flow.asStateFlow() - } - - override fun StateFlow?>.write(parcel: Parcel, flags: Int) { - if(this.value == null) { - parcel.writeString("null") - return - } - - parcel.writeString("${this.value!!.uri}") - } -} - -object ProfileLabelServiceStateFlowParceler : Parceler?>>{ - override fun create(parcel: Parcel): StateFlow?> { - val serialized = parcel.readString() - val flow = MutableStateFlow(null) - return flow.asStateFlow() - } - - override fun StateFlow?>.write(parcel: Parcel, flags: Int) { - if(this.value == null) { - parcel.writeString("null") - return - } - - parcel.writeString("${this.value!!.uri}") - } -} \ No newline at end of file diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/MorphoData.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/MorphoData.kt index 0f317ae..f3bdbcc 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/MorphoData.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/MorphoData.kt @@ -401,9 +401,9 @@ data class MorphoData( val oldEntry = threadToSplice.thread.parents.first() as ThreadPost.ViewablePost val newReplies = (newEntry.replies + oldEntry.replies).distinctBy { it.uri }.toMutableList() - newReplies.add(ThreadPost.ViewablePost(thread.thread.post, thread.thread.replies)) + newReplies.add(ThreadPost.ViewablePost(thread.thread.post, null, thread.thread.replies)) if( thread.getUri() != threadToSplice.getUri() ) - newReplies.add(ThreadPost.ViewablePost(threadToSplice.thread.post, threadToSplice.thread.replies)) + newReplies.add(ThreadPost.ViewablePost(threadToSplice.thread.post, null, threadToSplice.thread.replies)) val newThread = BskyPostThread( post = newEntry.post, parents = listOf(), @@ -420,8 +420,8 @@ data class MorphoData( if(threadToSplice.thread.parents.lastOrNull() !is ThreadPost.ViewablePost) return@fastForEachIndexed val newParent = thread.thread.parents.last() as ThreadPost.ViewablePost val oldParent = threadToSplice.thread.parents.last() as ThreadPost.ViewablePost - val newReply = ThreadPost.ViewablePost(thread.thread.post, thread.thread.replies) - val oldReply = ThreadPost.ViewablePost(threadToSplice.thread.post, threadToSplice.thread.replies) + val newReply = ThreadPost.ViewablePost(thread.thread.post, null, thread.thread.replies) + val oldReply = ThreadPost.ViewablePost(threadToSplice.thread.post, null, threadToSplice.thread.replies) newParent.addReply(newReply) oldParent.addReply(oldReply) newReplies.add(newReply) @@ -440,7 +440,7 @@ data class MorphoData( val inThreads = threads.indexOfFirst { t -> t?.containsUri(thread.thread.post.uri) ?: false } if (inThreads == - 1) { val threadToSplice = threads.getOrNull(index) ?: return@fastForEachIndexed - threads[index] = threadToSplice.addReply(ThreadPost.ViewablePost(thread.thread.post, thread.thread.replies)) + threads[index] = threadToSplice.addReply(ThreadPost.ViewablePost(thread.thread.post, null, thread.thread.replies)) threadCandidates[index] = null } } @@ -541,17 +541,6 @@ data class MorphoData( } -fun MorphoDataFeed.toMorphoData( - title: String = "", - newUri: AtUri? = null -): MorphoData { - return MorphoData( - title = title, - uri = newUri ?: uri, - cursor = cursor, - items = items - ) -} fun AtUri.id(api:Butterfly): AtIdentifier { val idString = atUri.substringAfter("at://").split("/")[0] diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/UIUpdate.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/UIUpdate.kt index 62d259f..7fff47d 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/UIUpdate.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/UIUpdate.kt @@ -89,7 +89,7 @@ sealed interface ThreadUpdate: UIUpdate { data class Error(val error: String): ThreadUpdate data class Thread( - val results: Flow, + val results: BskyPostThread, ): ThreadUpdate } diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uistate/TabbedScreenState.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uistate/TabbedScreenState.kt deleted file mode 100644 index ddf1626..0000000 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uistate/TabbedScreenState.kt +++ /dev/null @@ -1,58 +0,0 @@ -@file:Suppress("MemberVisibilityCanBePrivate") - -package com.morpho.app.model.uistate - -//import com.rickclephas.kmp.nativecoroutines.NativeCoroutines -import androidx.compose.runtime.Immutable -import com.morpho.app.model.bluesky.MorphoDataItem -import com.morpho.app.model.uidata.ContentCardMapEntry -import com.morpho.app.model.uidata.Event -import com.morpho.app.util.StateFlowSerializer -import com.morpho.butterfly.AtUri -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.serialization.Serializable - - -@Immutable -@Serializable -data class TabbedScreenState( - override val loadingState: UiLoadingState = UiLoadingState.Idle, - @Serializable(with = StateFlowSerializer::class) - val tabs: StateFlow>> = - MutableStateFlow>>(listOf()).asStateFlow(), - val tabStates: List>> = listOf(), -): UiState { - - val tabMap: Map> - get() = tabStates.associateBy { it.value.uri } - .filter { entry -> entry.value.value.uri in tabs.value.map { it.uri } } - .mapValues { it.value.value }.toMap() - val tabsWithNewPosts: List - get() = tabMap.filterValues { it.hasNewPosts }.keys.toList() - -} - - - -@Immutable -@Serializable -data class TabbedProfileScreenState( - override val loadingState: UiLoadingState = UiLoadingState.Idle, - @Serializable(with = StateFlowSerializer::class) - val tabs: StateFlow>> = - MutableStateFlow>>(listOf()).asStateFlow(), - val tabStates: List>> = listOf(), -): UiState { - - val tabMap: Map> - get() = tabStates.associateBy { it.value.uri } - .filter { entry -> entry.value.value.uri in tabs.value.map { it.uri } } - .mapValues { it.value.value }.toMap() - val tabsWithNewPosts: List - get() = tabMap.filterValues { it.hasNewPosts }.keys.toList() - - -} - diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/base/BaseScreenModel.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/base/BaseScreenModel.kt index 6f31804..0c0493f 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/base/BaseScreenModel.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/base/BaseScreenModel.kt @@ -49,8 +49,8 @@ open class BaseScreenModel : ScreenModel, KoinComponent { } - suspend fun sendGlobalEvent(event: Event) { - globalEvents.emit(event) + fun sendGlobalEvent(event: Event) { + globalEvents.tryEmit(event) } fun getProfilePresenter( @@ -88,7 +88,7 @@ open class BaseScreenModel : ScreenModel, KoinComponent { emit(agent.unreadNotificationsCount().getOrDefault(0)) }.stateIn(screenModelScope, SharingStarted.WhileSubscribed(), 0L) - fun markNotificationsAsRead() = screenModelScope.launch { + fun updateSeenNotifications() = screenModelScope.launch { agent.updateSeenNotifications() globalEvents.emit(Event.UpdateSeenNotifications()) } diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/base/tabbed/NavigationTabs.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/base/tabbed/NavigationTabs.kt index 69cb416..9530c92 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/base/tabbed/NavigationTabs.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/base/tabbed/NavigationTabs.kt @@ -5,7 +5,10 @@ import androidx.compose.material.icons.filled.* import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme -import androidx.compose.runtime.* +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Immutable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import cafe.adriel.voyager.core.annotation.ExperimentalVoyagerApi import cafe.adriel.voyager.core.model.rememberNavigatorScreenModel @@ -15,7 +18,6 @@ import cafe.adriel.voyager.core.screen.uniqueScreenKey import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.Navigator import cafe.adriel.voyager.navigator.currentOrThrow -import com.morpho.app.model.uistate.ContentCardState import com.morpho.app.screens.main.tabbed.TabbedHomeView import com.morpho.app.screens.main.tabbed.TabbedMainScreenModel import com.morpho.app.screens.notifications.NotificationViewContent @@ -28,7 +30,6 @@ import com.morpho.butterfly.AtUri import com.morpho.butterfly.Did import dev.icerock.moko.parcelize.Parcelable import dev.icerock.moko.parcelize.Parcelize -import kotlinx.coroutines.flow.StateFlow import kotlinx.serialization.Contextual import kotlinx.serialization.Serializable import kotlinx.serialization.Transient @@ -245,7 +246,7 @@ data class ThreadTab( override fun Content() { val navigator = LocalNavigator.currentOrThrow val sm = navigator.rememberNavigatorScreenModel { TabbedMainScreenModel() } - var threadState: StateFlow? by remember { mutableStateOf(null)} + val threadState by sm.getThread(uri).collectAsState(null) if(threadState != null) { ThreadViewContent(threadState!!, navigator) } else { diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/base/tabbed/TabbedBaseScreen.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/base/tabbed/TabbedBaseScreen.kt index f4c7a3f..c9cd21b 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/base/tabbed/TabbedBaseScreen.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/base/tabbed/TabbedBaseScreen.kt @@ -14,6 +14,7 @@ import androidx.compose.ui.semantics.semantics import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import cafe.adriel.voyager.core.annotation.ExperimentalVoyagerApi +import cafe.adriel.voyager.core.model.rememberNavigatorScreenModel import cafe.adriel.voyager.core.screen.ScreenKey import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.Navigator @@ -22,11 +23,11 @@ import cafe.adriel.voyager.navigator.currentOrThrow import cafe.adriel.voyager.navigator.tab.Tab import cafe.adriel.voyager.navigator.tab.TabNavigator import cafe.adriel.voyager.navigator.tab.TabOptions +import com.morpho.app.screens.main.tabbed.TabbedMainScreenModel import com.morpho.app.ui.common.SlideTabTransition import com.morpho.app.ui.theme.roundedTopR import io.ktor.util.reflect.instanceOf import kotlinx.serialization.Serializable -import org.koin.compose.koinInject import kotlin.math.min @Serializable @@ -86,8 +87,8 @@ fun TabNavigationItem( icon = { when (tab) { is NotificationsTab -> { - val notifService = koinInject() - val unread by notifService.unreadCountFlow().collectAsState(0) + val sm = LocalNavigator.currentOrThrow.rememberNavigatorScreenModel { TabbedMainScreenModel() } + val unread by sm.unreadNotificationsCount().collectAsState(0) BadgedBox( badge = { if (unread > 0) { diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/main/MainScreenModel.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/main/MainScreenModel.kt index 2edbc84..1648c4d 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/main/MainScreenModel.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/main/MainScreenModel.kt @@ -42,6 +42,7 @@ open class MainScreenModel: BaseScreenModel() { init { if(isLoggedIn) screenModelScope.launch { + agent.getPreferences() userProfile = userDid?.let { agent.getProfile(it).getOrNull()?.toProfile() } feedSources.addAll(pinnedFeeds.mapNotNull { feed -> feed.toFeedSourceInfo(agent).getOrNull() }) feedPresenters.putAll(feedSources.map { source -> diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/main/tabbed/TabbedHomeView.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/main/tabbed/TabbedHomeView.kt index 48a9cbb..1413cf9 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/main/tabbed/TabbedHomeView.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/main/tabbed/TabbedHomeView.kt @@ -143,7 +143,7 @@ fun TabScreen.TabbedHomeView( SkylineTabTransition(nav, sm, insets, state) }, modifier = Modifier, - state = sm.feedStates.get(tabUri) as ContentCardState.Skyline? + state = sm.feedStates[tabUri] as ContentCardState.Skyline? ) } @@ -259,8 +259,8 @@ data class HomeSkylineTab @OptIn(ExperimentalVoyagerApi::class) constructor( TabbedSkylineFragment( paddingValues = paddingValues, isProfileFeed = false, - feedUpdate = state.updates, - uiEvents = sm.globalEvents, + uiUpdate = state.updates, + eventCallback = { sm.sendGlobalEvent(it) }, ) } diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/main/tabbed/TabbedMainScreenModel.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/main/tabbed/TabbedMainScreenModel.kt index 52ff1f8..474b109 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/main/tabbed/TabbedMainScreenModel.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/main/tabbed/TabbedMainScreenModel.kt @@ -5,12 +5,18 @@ import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import app.bsky.actor.FeedType +import app.bsky.feed.GetPostThreadResponseThreadUnion import cafe.adriel.voyager.core.model.screenModelScope import com.morpho.app.model.bluesky.toContentCardMapEntry +import com.morpho.app.model.bluesky.toThread import com.morpho.app.model.uidata.ContentCardMapEntry +import com.morpho.app.model.uidata.ThreadUpdate +import com.morpho.app.model.uistate.ContentCardState import com.morpho.app.screens.main.MainScreenModel import com.morpho.butterfly.AtUri import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow import kotlinx.coroutines.launch import org.lighthousegames.logging.logging @@ -49,5 +55,19 @@ class TabbedMainScreenModel : MainScreenModel() { return tabs[index].uri } + fun getThread(uri: AtUri): Flow = flow { + val post = getPost(uri).getOrNull() + if(post != null) { + val state = ContentCardState.PostThread(post) + val thread = when(val thread = agent.getPostThread(uri,).getOrNull()?.thread) { + is GetPostThreadResponseThreadUnion.ThreadViewPost -> thread.value.toThread() + else -> null + } + if(thread != null) state.updates.emit(ThreadUpdate.Thread(thread)) + emit(state) + } + + } + } \ No newline at end of file diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/notifications/NotificationsView.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/notifications/NotificationsView.kt index c3b4bf9..832cf6a 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/notifications/NotificationsView.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/notifications/NotificationsView.kt @@ -67,6 +67,7 @@ fun TabScreen.NotificationViewContent( val uriHandler = LocalUriHandler.current val pager = sm.notificationsRaw.collectAsLazyPagingItems() var uiState by rememberSaveable { mutableStateOf(NotificationsUIState()) } + val toMarkRead = mutableStateListOf() TabbedScreenScaffold( navBar = { navBar(navigator) }, topContent = { @@ -80,7 +81,9 @@ fun TabScreen.NotificationViewContent( }, showSettings = showSettings, hasUnread = hasUnread, - markAsRead = { sm.markNotificationsAsRead() } + markAsRead = { + sm.updateSeenNotifications() + } ) }, state = uiState, @@ -90,7 +93,7 @@ fun TabScreen.NotificationViewContent( val refreshing by remember { mutableStateOf(false)} val refreshState = rememberPullRefreshState( refreshing, - { sm.notifService.updateNotificationsSeen() + { sm.updateSeenNotifications() pager.refresh() }) @@ -133,6 +136,7 @@ fun TabScreen.NotificationViewContent( NotificationsFilterElement( uiState.filterState, onFilterClicked = { + uiState.filterState.value = it pager.refresh() } ) @@ -149,7 +153,7 @@ fun TabScreen.NotificationViewContent( } } } } is LoadStateNotLoading -> { - val toMarkRead = mutableStateListOf() + val notifications = pager.collectNotifications(toMarkRead) items( count = pager.itemCount, diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/thread/ThreadView.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/thread/ThreadView.kt index fe9ca83..c6e7109 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/thread/ThreadView.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/thread/ThreadView.kt @@ -71,19 +71,16 @@ fun TabScreen.ThreadViewContent( } is ThreadUpdate.Thread -> { - val thread = state.results.collectAsState(null).value - if(thread != null) { - ThreadView( - thread = thread, - insets = insets, - navigator = navigator, - createRecord = { sm.screenModelScope.launch { sm.agent.createRecord(it) } }, - deleteRecord = { type, uri -> sm.screenModelScope.launch { - sm.agent.deleteRecord(type, uri) - } }, - resolveHandle = { handle -> sm.agent.resolveHandle(handle).getOrNull() } - ) - } + ThreadView( + thread = state.results, + insets = insets, + navigator = navigator, + createRecord = { sm.screenModelScope.launch { sm.agent.createRecord(it) } }, + deleteRecord = { type, uri -> sm.screenModelScope.launch { + sm.agent.deleteRecord(type, uri) + } }, + resolveHandle = { handle -> sm.agent.resolveHandle(handle).getOrNull() } + ) } else -> { Text("Unknown state: $state") diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/common/SkylineThreadFragment.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/common/SkylineThreadFragment.kt index 478dd33..8bf8e5d 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/common/SkylineThreadFragment.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/common/SkylineThreadFragment.kt @@ -17,7 +17,6 @@ import com.atproto.repo.StrongRef import com.morpho.app.model.bluesky.BskyPost import com.morpho.app.model.bluesky.BskyPostThread import com.morpho.app.model.bluesky.ThreadPost -import com.morpho.app.model.uidata.ContentHandling import com.morpho.app.ui.elements.MenuOptions import com.morpho.app.ui.post.PostFragment import com.morpho.app.ui.post.PostFragmentRole @@ -25,6 +24,7 @@ import com.morpho.app.ui.thread.ThreadItem import com.morpho.app.ui.thread.ThreadTree import com.morpho.butterfly.AtIdentifier import com.morpho.butterfly.AtUri +import com.morpho.butterfly.ContentHandling import com.morpho.butterfly.model.RecordType @Composable @@ -41,7 +41,7 @@ inline fun SkylineThreadFragment( crossinline getContentHandling: (BskyPost) -> List = { listOf() }, debuggable: Boolean = false, ) { - val threadPost = remember { ThreadPost.ViewablePost(thread.post, thread.replies) } + val threadPost = remember { ThreadPost.ViewablePost(thread.post, null, thread.replies) } val hasReplies = rememberSaveable { threadPost.replies.isNotEmpty() } var showReplies by remember { mutableStateOf(threadPost.replies.size <= 2)} var showFullThread by remember { mutableStateOf(thread.parents.size <= 3)} diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/common/TabbedScreenScaffold.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/common/TabbedScreenScaffold.kt index a24a0ad..a1a5ee9 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/common/TabbedScreenScaffold.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/common/TabbedScreenScaffold.kt @@ -36,7 +36,7 @@ expect fun TabbedScreenScaffold( @Composable expect fun TabbedProfileScreenScaffold( navBar: @Composable () -> Unit, - content: @Composable (PaddingValues,ContentCardState?) -> Unit, + content: @Composable (PaddingValues,ContentCardState?) -> Unit, topContent: @Composable (TopAppBarScrollBehavior) -> Unit, state: ContentCardState?, modifier: Modifier = Modifier, diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/common/TabbedSkylineFragment.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/common/TabbedSkylineFragment.kt index 01b94d9..24d4ac4 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/common/TabbedSkylineFragment.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/common/TabbedSkylineFragment.kt @@ -74,7 +74,12 @@ fun TabbedSkylineFragment( val clipboard = getKoin().get() if(uiState.value !is UIUpdate.Empty) { SkylineFragment( - onProfileClicked = { actor -> navigator.push(ProfileTab(actor)) }, + onProfileClicked = { actor -> + scope.launch { + val did = agent.resolveHandle(actor).getOrNull() + if(did != null) navigator.push(ProfileTab(did)) + } + }, onItemClicked = { uri -> navigator.push(ThreadTab(uri)) }, onUnClicked = { type, rkey -> agent.deleteRecord(type, rkey) }, onRepostClicked = { onRepostClicked(it) }, diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/notifications/ReasonIcon.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/notifications/ReasonIcon.kt index 1f2de5c..4d8517f 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/notifications/ReasonIcon.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/notifications/ReasonIcon.kt @@ -2,10 +2,7 @@ package com.morpho.app.ui.notifications import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.Reply -import androidx.compose.material.icons.filled.Favorite -import androidx.compose.material.icons.filled.Notifications -import androidx.compose.material.icons.filled.PersonAdd -import androidx.compose.material.icons.filled.Repeat +import androidx.compose.material.icons.filled.* import androidx.compose.material3.Icon import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier @@ -47,5 +44,10 @@ fun ReasonIcon( contentDescription = "Quote", modifier = modifier ) + ListNotificationsReason.PLACEHOLDER -> Icon( + imageVector = Icons.Default.Download, + contentDescription = "Placeholder", + modifier = modifier + ) } } \ No newline at end of file diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/post/FullPostFragment.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/post/FullPostFragment.kt index dcd9811..1a66c3c 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/post/FullPostFragment.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/post/FullPostFragment.kt @@ -24,18 +24,13 @@ import androidx.compose.ui.text.withStyle import androidx.compose.ui.unit.dp import androidx.compose.ui.util.fastAny import androidx.compose.ui.util.fastForEach +import com.atproto.label.Blurs import com.atproto.repo.StrongRef import com.morpho.app.model.bluesky.BskyPost import com.morpho.app.model.bluesky.FacetType -import com.morpho.app.model.bluesky.LabelAction -import com.morpho.app.model.bluesky.LabelScope -import com.morpho.app.model.uidata.ContentHandling -import com.morpho.app.model.uidata.LabelDescription -import com.morpho.app.model.uidata.LabelIcon import com.morpho.app.ui.elements.* import com.morpho.app.util.openBrowser -import com.morpho.butterfly.AtIdentifier -import com.morpho.butterfly.AtUri +import com.morpho.butterfly.* import com.morpho.butterfly.model.RecordType import kotlinx.collections.immutable.toImmutableList import kotlinx.datetime.TimeZone @@ -62,7 +57,7 @@ fun FullPostFragment( val contentHandling = remember { if (post.author.mutedByMe) { getContentHandling(post) + ContentHandling( - scope = LabelScope.Content, + scope = Blurs.CONTENT, id = "muted", icon = LabelIcon.EyeSlash(labelerAvatar = null), action = LabelAction.Blur, @@ -84,7 +79,7 @@ fun FullPostFragment( ) { ContentHider( reasons = contentHandling, - scope = LabelScope.Content, + scope = Blurs.CONTENT, ) { Row( modifier = Modifier diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/thread/ThreadFragment.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/thread/ThreadFragment.kt index 3244627..f6c2c89 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/thread/ThreadFragment.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/thread/ThreadFragment.kt @@ -17,7 +17,6 @@ import com.atproto.repo.StrongRef import com.morpho.app.model.bluesky.BskyPost import com.morpho.app.model.bluesky.BskyPostThread import com.morpho.app.model.bluesky.ThreadPost -import com.morpho.app.model.uidata.ContentHandling import com.morpho.app.ui.common.OnPostClicked import com.morpho.app.ui.elements.MenuOptions import com.morpho.app.ui.post.BlockedPostFragment @@ -26,6 +25,7 @@ import com.morpho.app.ui.post.NotFoundPostFragment import com.morpho.app.ui.post.PostFragmentRole import com.morpho.butterfly.AtIdentifier import com.morpho.butterfly.AtUri +import com.morpho.butterfly.ContentHandling import com.morpho.butterfly.model.RecordType @@ -51,7 +51,7 @@ fun ThreadFragment( listState: LazyListState = rememberLazyListState(), contentPadding: PaddingValues = PaddingValues(0.dp) ) { - val threadPost = remember { ThreadPost.ViewablePost(thread.post, thread.replies) } + val threadPost = remember { ThreadPost.ViewablePost(thread.post, null, thread.replies) } val hasReplies = rememberSaveable { threadPost.replies.isNotEmpty()} val rootIndex = remember { thread.parents.size } diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/thread/ThreadItem.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/thread/ThreadItem.kt index 5005444..ed126c2 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/thread/ThreadItem.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/thread/ThreadItem.kt @@ -6,12 +6,12 @@ import com.atproto.repo.StrongRef import com.morpho.app.model.bluesky.BskyPost import com.morpho.app.model.bluesky.BskyPostReason import com.morpho.app.model.bluesky.ThreadPost -import com.morpho.app.model.uidata.ContentHandling import com.morpho.app.ui.common.OnPostClicked import com.morpho.app.ui.elements.MenuOptions import com.morpho.app.ui.post.* import com.morpho.butterfly.AtIdentifier import com.morpho.butterfly.AtUri +import com.morpho.butterfly.ContentHandling import com.morpho.butterfly.model.RecordType @Composable diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/thread/ThreadReply.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/thread/ThreadReply.kt index 7e393e1..e7f9f1b 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/thread/ThreadReply.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/thread/ThreadReply.kt @@ -5,7 +5,6 @@ import androidx.compose.ui.Modifier import com.atproto.repo.StrongRef import com.morpho.app.model.bluesky.BskyPost import com.morpho.app.model.bluesky.ThreadPost -import com.morpho.app.model.uidata.ContentHandling import com.morpho.app.ui.common.OnPostClicked import com.morpho.app.ui.elements.MenuOptions import com.morpho.app.ui.post.BlockedPostFragment @@ -14,6 +13,7 @@ import com.morpho.app.ui.post.PostFragment import com.morpho.app.ui.post.PostFragmentRole import com.morpho.butterfly.AtIdentifier import com.morpho.butterfly.AtUri +import com.morpho.butterfly.ContentHandling import com.morpho.butterfly.model.RecordType @Composable diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/thread/ThreadTree.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/thread/ThreadTree.kt index 6c0d1f4..ed6d3ec 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/thread/ThreadTree.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/thread/ThreadTree.kt @@ -20,13 +20,13 @@ import androidx.compose.ui.util.fastForEachIndexed import com.atproto.repo.StrongRef import com.morpho.app.model.bluesky.BskyPost import com.morpho.app.model.bluesky.ThreadPost -import com.morpho.app.model.uidata.ContentHandling import com.morpho.app.ui.common.OnPostClicked import com.morpho.app.ui.elements.MenuOptions import com.morpho.app.ui.elements.WrappedColumn import com.morpho.app.ui.post.PostFragmentRole import com.morpho.butterfly.AtIdentifier import com.morpho.butterfly.AtUri +import com.morpho.butterfly.ContentHandling import com.morpho.butterfly.model.RecordType import morpho.app.ui.utils.indentLevel diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/util/LinkParsing.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/util/LinkParsing.kt index c1749fa..f255cf9 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/util/LinkParsing.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/util/LinkParsing.kt @@ -7,7 +7,6 @@ import com.morpho.app.screens.base.tabbed.ProfileTab import com.morpho.butterfly.AtUri import com.morpho.butterfly.Cid import com.morpho.butterfly.Did -import com.morpho.butterfly.Handle fun linkVisit(string: String, navigator: Navigator, uriHandler: UriHandler) { @@ -15,7 +14,7 @@ fun linkVisit(string: String, navigator: Navigator, uriHandler: UriHandler) { if(string.startsWith("@did")) { navigator.push(ProfileTab(Did(string.removePrefix("@")))) } else { - navigator.push(ProfileTab(Handle(string.removePrefix("@")))) + //navigator.push(ProfileTab())) } } else if(string.startsWith("https://bsky.app/") || string.startsWith("https://staging.bsky.app/") diff --git a/Morpho/composeApp/src/desktopMain/kotlin/com/morpho/app/ui/common/TabbedScreenScaffold.desktop.kt b/Morpho/composeApp/src/desktopMain/kotlin/com/morpho/app/ui/common/TabbedScreenScaffold.desktop.kt index f895f08..3218b28 100644 --- a/Morpho/composeApp/src/desktopMain/kotlin/com/morpho/app/ui/common/TabbedScreenScaffold.desktop.kt +++ b/Morpho/composeApp/src/desktopMain/kotlin/com/morpho/app/ui/common/TabbedScreenScaffold.desktop.kt @@ -41,7 +41,7 @@ actual fun TabbedScreenScaffold( @Composable actual fun TabbedProfileScreenScaffold( navBar: @Composable () -> Unit, - content: @Composable (PaddingValues, ContentCardState?) -> Unit, + content: @Composable (PaddingValues, ContentCardState?) -> Unit, topContent: @Composable (TopAppBarScrollBehavior) -> Unit, state: ContentCardState?, modifier: Modifier, diff --git a/Morpho/composeApp/src/desktopMain/kotlin/main.kt b/Morpho/composeApp/src/desktopMain/kotlin/main.kt index 35b853b..36a8c38 100644 --- a/Morpho/composeApp/src/desktopMain/kotlin/main.kt +++ b/Morpho/composeApp/src/desktopMain/kotlin/main.kt @@ -24,12 +24,12 @@ import cafe.adriel.voyager.jetpack.ProvideNavigatorLifecycleKMPSupport import ch.qos.logback.classic.LoggerContext import ch.qos.logback.core.util.StatusPrinter2 import com.morpho.app.App +import com.morpho.app.data.MorphoAgent import com.morpho.app.data.PreferencesRepository import com.morpho.app.di.appModule import com.morpho.app.di.dataModule import com.morpho.app.di.storageModule import com.morpho.app.ui.theme.MorphoTheme -import com.morpho.butterfly.Butterfly import com.morpho.butterfly.auth.SessionRepository import com.morpho.butterfly.auth.UserRepository import kotlinx.coroutines.flow.firstOrNull @@ -70,7 +70,7 @@ fun main() = application { cachePath.toNioPath().createDirectories() koin.get { parametersOf(storageDir) } koin.get { parametersOf(storageDir) } - val api = koin.get() + val agent = koin.get() val prefs = koin.get { parametersOf(storageDir) } val morphoPrefs = runBlocking { From 032642943c7d205089826ec78fe5546038d3b14b Mon Sep 17 00:00:00 2001 From: Orual Date: Mon, 16 Sep 2024 22:03:31 -0400 Subject: [PATCH 13/42] Some cleanup of unused code. --- .../com/morpho/app/MorphoApplication.kt | 16 +- .../com/morpho/app/ui/common/BackHandler.kt | 8 - .../src/appleMain/kotlin/Platform.apple.kt | 10 - .../kotlin/com/morpho/app/Platform.apple.kt | 10 + .../uidata => data}/ContentLabelService.kt | 3 +- .../com/morpho/app/data/MorphoDataSource.kt | 1 - .../bluesky => data}/NotificationsSource.kt | 5 +- .../kotlin/com/morpho/app/di/AppModule.kt | 6 +- .../app/model/bluesky/BskyPostFeature.kt | 7 +- .../app/model/bluesky/MorphoDataItem.kt | 2 +- .../com/morpho/app/model/bluesky/Reference.kt | 13 - .../com/morpho/app/model/uidata/MorphoData.kt | 565 ------------------ .../app/model/uistate/ContentCardState.kt | 18 +- .../morpho/app/model/uistate/LoadingState.kt | 21 - .../morpho/app/model/uistate/LoginState.kt | 5 +- .../app/model/uistate/NotificationsState.kt | 3 +- .../morpho/app/model/uistate/SkylineState.kt | 17 - .../com/morpho/app/model/uistate/UiState.kt | 10 +- .../app/screens/base/BaseScreenModel.kt | 8 +- .../notifications/NotificationsView.kt | 4 +- .../notifications/NotificationAvatarList.kt | 2 +- .../ui/notifications/NotificationsElement.kt | 2 +- .../com/morpho/app/ui/common/BackHandler.kt | 7 - 23 files changed, 64 insertions(+), 679 deletions(-) delete mode 100644 Morpho/composeApp/src/androidMain/kotlin/com/morpho/app/ui/common/BackHandler.kt delete mode 100644 Morpho/composeApp/src/appleMain/kotlin/Platform.apple.kt rename Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/{model/uidata => data}/ContentLabelService.kt (99%) rename Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/{model/bluesky => data}/NotificationsSource.kt (98%) delete mode 100644 Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/Reference.kt delete mode 100644 Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/MorphoData.kt delete mode 100644 Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uistate/LoadingState.kt delete mode 100644 Morpho/composeApp/src/desktopMain/kotlin/com/morpho/app/ui/common/BackHandler.kt diff --git a/Morpho/composeApp/src/androidMain/kotlin/com/morpho/app/MorphoApplication.kt b/Morpho/composeApp/src/androidMain/kotlin/com/morpho/app/MorphoApplication.kt index 4da1904..b5fe8af 100644 --- a/Morpho/composeApp/src/androidMain/kotlin/com/morpho/app/MorphoApplication.kt +++ b/Morpho/composeApp/src/androidMain/kotlin/com/morpho/app/MorphoApplication.kt @@ -4,12 +4,11 @@ import android.app.Application import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.DefaultLifecycleObserver import com.gu.toolargetool.TooLargeTool +import com.morpho.app.data.MorphoAgent import com.morpho.app.data.PreferencesRepository import com.morpho.app.di.appModule import com.morpho.app.di.dataModule import com.morpho.app.di.storageModule -import com.morpho.butterfly.AtIdentifier -import com.morpho.butterfly.Butterfly import com.morpho.butterfly.auth.SessionRepository import com.morpho.butterfly.auth.UserRepository import org.koin.android.annotation.KoinViewModel @@ -25,10 +24,8 @@ import org.koin.dsl.module @KoinViewModel class AndroidMainViewModel(app: Application): AndroidViewModel(app), DefaultLifecycleObserver { - val sessionRepository = app.getKoin().get() - val userRepository = app.getKoin().get() - val api = app.getKoin().get() + val agent = app.getKoin().get() } class MorphoApplication : Application() { @@ -43,14 +40,7 @@ class MorphoApplication : Application() { val sessionRepository = koin.get { parametersOf(cacheDir.path.toString()) } val userRepository = koin.get { parametersOf(cacheDir.path.toString()) } val prefs = koin.get { parametersOf(cacheDir.path.toString()) } - val id: AtIdentifier? = if(sessionRepository.auth?.did != null) { - sessionRepository.auth?.did - } else if (sessionRepository.auth?.handle != null) { - sessionRepository.auth?.handle - } else { - userRepository.firstUser()?.id - } - val api = koin.get { parametersOf(id) } + val agent = koin.get() super.onCreate() } } diff --git a/Morpho/composeApp/src/androidMain/kotlin/com/morpho/app/ui/common/BackHandler.kt b/Morpho/composeApp/src/androidMain/kotlin/com/morpho/app/ui/common/BackHandler.kt deleted file mode 100644 index ddebe6e..0000000 --- a/Morpho/composeApp/src/androidMain/kotlin/com/morpho/app/ui/common/BackHandler.kt +++ /dev/null @@ -1,8 +0,0 @@ -package com.morpho.app.ui.common - -import androidx.compose.runtime.Composable - -@Composable -actual fun BackHandler(content: () -> Unit) { - -} \ No newline at end of file diff --git a/Morpho/composeApp/src/appleMain/kotlin/Platform.apple.kt b/Morpho/composeApp/src/appleMain/kotlin/Platform.apple.kt deleted file mode 100644 index c42198b..0000000 --- a/Morpho/composeApp/src/appleMain/kotlin/Platform.apple.kt +++ /dev/null @@ -1,10 +0,0 @@ -@file:Suppress("EXPECT_ACTUAL_CLASSIFIERS_ARE_IN_BETA_WARNING") -package com.morpho.app -import kotlinx.datetime.LocalDateTime - -// Note: no need to define CommonParcelize here (bc its @OptionalExpectation) -actual interface CommonParcelable // not used on iOS - -// Note: no need to define CommonTypeParceler> here (bc its @OptionalExpectation) -actual interface CommonParceler // not used on iOS -actual object LocalDateTimeParceler : CommonParceler // not used on iOS diff --git a/Morpho/composeApp/src/appleMain/kotlin/com/morpho/app/Platform.apple.kt b/Morpho/composeApp/src/appleMain/kotlin/com/morpho/app/Platform.apple.kt index 6309ccc..6fb357c 100644 --- a/Morpho/composeApp/src/appleMain/kotlin/com/morpho/app/Platform.apple.kt +++ b/Morpho/composeApp/src/appleMain/kotlin/com/morpho/app/Platform.apple.kt @@ -1,4 +1,14 @@ +@file:Suppress("EXPECT_ACTUAL_CLASSIFIERS_ARE_IN_BETA_WARNING") package com.morpho.app +import kotlinx.datetime.LocalDateTime + +// Note: no need to define CommonParcelize here (bc its @OptionalExpectation) +actual interface CommonParcelable // not used on iOS + +// Note: no need to define CommonTypeParceler> here (bc its @OptionalExpectation) +actual interface CommonParceler // not used on iOS +actual object LocalDateTimeParceler : CommonParceler // not used on iOS + // For Android @Parcelize @Target(AnnotationTarget.PROPERTY) diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/ContentLabelService.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/data/ContentLabelService.kt similarity index 99% rename from Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/ContentLabelService.kt rename to Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/data/ContentLabelService.kt index b6bbf0c..9b52a8d 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/ContentLabelService.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/data/ContentLabelService.kt @@ -1,4 +1,4 @@ -package com.morpho.app.model.uidata +package com.morpho.app.data import app.bsky.actor.MuteTargetGroup import app.bsky.actor.MutedWord @@ -6,7 +6,6 @@ import app.bsky.actor.Visibility import app.bsky.labeler.LabelerViewDetailed import com.atproto.label.Blurs import com.atproto.label.Severity -import com.morpho.app.data.MorphoAgent import com.morpho.app.model.bluesky.BskyPost import com.morpho.app.model.bluesky.MorphoDataItem import com.morpho.app.model.bluesky.toAtProtoLabel diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/data/MorphoDataSource.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/data/MorphoDataSource.kt index cd9df84..4489b27 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/data/MorphoDataSource.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/data/MorphoDataSource.kt @@ -5,7 +5,6 @@ import app.cash.paging.PagingConfig import app.cash.paging.PagingSource import app.cash.paging.PagingState import com.morpho.app.model.bluesky.* -import com.morpho.app.model.uidata.ContentLabelService import com.morpho.app.model.uidata.Delta import com.morpho.app.model.uidata.Moment import com.morpho.butterfly.ButterflyAgent diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/NotificationsSource.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/data/NotificationsSource.kt similarity index 98% rename from Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/NotificationsSource.kt rename to Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/data/NotificationsSource.kt index 32be980..855f3d3 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/NotificationsSource.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/data/NotificationsSource.kt @@ -1,9 +1,10 @@ -package com.morpho.app.model.bluesky +package com.morpho.app.data import app.bsky.notification.ListNotificationsReason import app.cash.paging.PagingConfig import app.cash.paging.compose.LazyPagingItems -import com.morpho.app.data.MorphoDataSource +import com.morpho.app.model.bluesky.BskyNotification +import com.morpho.app.model.bluesky.toBskyNotification import com.morpho.app.model.uistate.NotificationsFilterState import com.morpho.butterfly.AtUri import com.morpho.butterfly.Cursor diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/di/AppModule.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/di/AppModule.kt index 5c52247..3588605 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/di/AppModule.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/di/AppModule.kt @@ -1,9 +1,13 @@ package com.morpho.app.di +import com.morpho.app.data.ContentLabelService import com.morpho.app.data.MorphoAgent import com.morpho.app.data.PollBlueService import com.morpho.app.data.PreferencesRepository -import com.morpho.app.model.uidata.* +import com.morpho.app.model.uidata.FeedEvent +import com.morpho.app.model.uidata.FeedPresenter +import com.morpho.app.model.uidata.UserFeedsPresenter +import com.morpho.app.model.uidata.UserListPresenter import com.morpho.app.screens.base.BaseScreenModel import com.morpho.app.screens.login.LoginScreenModel import com.morpho.app.screens.main.MainScreenModel diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/BskyPostFeature.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/BskyPostFeature.kt index ba86165..db6c411 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/BskyPostFeature.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/BskyPostFeature.kt @@ -98,7 +98,12 @@ data class EmbedImage( val aspectRatio: AspectRatio? = null, ): Parcelable - +@Immutable +@Serializable +data class Reference( + val uri: AtUri, + val cid: Cid, +) @Parcelize @Immutable diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/MorphoDataItem.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/MorphoDataItem.kt index 8c15fb8..754f72f 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/MorphoDataItem.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/MorphoDataItem.kt @@ -66,7 +66,7 @@ sealed interface MorphoDataItem: Parcelable { null } is ReplyRefParentUnion.PostView -> { - (parent as ReplyRefParentUnion.PostView).value + parent.value } } items.add(feedPost.post) diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/Reference.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/Reference.kt deleted file mode 100644 index b11cac8..0000000 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/Reference.kt +++ /dev/null @@ -1,13 +0,0 @@ -package com.morpho.app.model.bluesky - -import androidx.compose.runtime.Immutable -import com.morpho.butterfly.AtUri -import com.morpho.butterfly.Cid -import kotlinx.serialization.Serializable - -@Immutable -@Serializable -data class Reference( - val uri: AtUri, - val cid: Cid, -) \ No newline at end of file diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/MorphoData.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/MorphoData.kt deleted file mode 100644 index f3bdbcc..0000000 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/MorphoData.kt +++ /dev/null @@ -1,565 +0,0 @@ -package com.morpho.app.model.uidata - -//import com.rickclephas.kmp.nativecoroutines.NativeCoroutines -import androidx.compose.runtime.Immutable -import androidx.compose.ui.util.* -import app.bsky.feed.FeedViewPost -import com.morpho.app.data.FeedTuner -import com.morpho.app.model.bluesky.* -import com.morpho.app.model.uistate.FeedType -import com.morpho.butterfly.* -import dev.icerock.moko.parcelize.Parcelable -import dev.icerock.moko.parcelize.Parcelize -import kotlinx.collections.immutable.toPersistentList -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.flow -import kotlinx.coroutines.flow.flowOn -import kotlinx.coroutines.flow.single -import kotlinx.datetime.Instant -import kotlinx.serialization.Serializable -import kotlinx.serialization.json.JsonElement -import kotlinx.serialization.json.JsonObject -import kotlin.time.Duration - - -typealias TunerFunction = (List, FeedTuner) -> List - -@Parcelize -@Immutable -@Serializable -data class AtCursor(val cursor: String?, val scroll: Int): Parcelable { - companion object { - val EMPTY: AtCursor = AtCursor(null, 0) - } -} - - -@Immutable -@Serializable -data class MorphoData( - val title: String = "Home", - val uri: AtUri = AtUri.HOME_URI, - val cursor: AtCursor = AtCursor.EMPTY, - val items: List = listOf(), - //@TypeParceler() - val query: JsonElement = JsonObject(emptyMap()), -) { - companion object { - - fun EMPTY(): MorphoData { - return MorphoData( - title = "Home", - uri = AtUri.HOME_URI, - cursor = AtCursor.EMPTY, - items = listOf(), - query = JsonObject(emptyMap()), - ) - } - - fun fromList( - title: String = "Home", - uri: AtUri = AtUri.HOME_URI, - cursor: AtCursor = AtCursor.EMPTY, - items: List, - query: JsonElement = JsonObject(emptyMap()), - ): MorphoData { - return MorphoData( - title = title, - uri = uri, - cursor = cursor, - items = items, - query = query, - ) - } - - fun fromFeed( - feedPosts: List, - cursor: AtCursor = AtCursor.EMPTY, - title: String = "Home", - uri: AtUri = AtUri.HOME_URI, - query: JsonElement = JsonObject(emptyMap()), - ): MorphoData { - val items = feedPosts.map { item -> - MorphoDataItem.FeedItem.fromFeedViewPost(item) - } - return MorphoData( - title = title, - uri = uri, - cursor = cursor, - items = items, - query = query, - ) - } - - fun concatFeed( - query: JsonElement, - responseCursor: String?, - oldCursor: AtCursor, - feed: List, - data: MorphoData, - uri: AtUri = data.uri, - title: String = data.title, - api: Butterfly? = null, - ): Flow> = flow { - val newItems = fromFeed( - feed.toList(), AtCursor(responseCursor, 0), - uri = uri, title = title, query = query).collectThreads().single() - emit(if (oldCursor != AtCursor.EMPTY && data.items.isNotEmpty()) { - val newScroll = maxOf(data.items.size, oldCursor.scroll) - concat(data, newItems, AtCursor(responseCursor, newScroll), query = query) - } else if (oldCursor == AtCursor.EMPTY && data.items.isNotEmpty()) { - concat(newItems, data,AtCursor(responseCursor, 0), query = query) - } else { - newItems - }) - } - - fun concatNonThreadedFeed( - query: JsonElement, - responseCursor: String?, - oldCursor: AtCursor, - feed: List, - data: MorphoData, - uri: AtUri = data.uri, - title: String = data.title, - ): MorphoData { - val newItems = fromFeed( - feed.toList(), AtCursor(responseCursor, 0), - uri = uri, title = title, query = query) - return if (oldCursor != AtCursor.EMPTY && data.items.isNotEmpty()) { - val newScroll = if(oldCursor.scroll == 0) 0 else maxOf(data.items.size, oldCursor.scroll) - concat(data, newItems, AtCursor(responseCursor, newScroll), query = query) - } else if (oldCursor == AtCursor.EMPTY && data.items.isNotEmpty()) { - concat(newItems, data,AtCursor(responseCursor, 0), query = query) - } else { - newItems - } - } - - - fun concat( - first: MorphoData, - last: MorphoData, - cursor: AtCursor = last.cursor, - query: JsonElement = JsonObject(emptyMap()), - ): MorphoData { - return first.copy( - items = (first.items + last.items).toPersistentList(), -// .sortedByDescending { -// when (it) { -// is MorphoDataItem.Post -> it.post.createdAt -// is MorphoDataItem.Thread -> it.thread.post.createdAt -// is MorphoDataItem.FeedInfo -> it.feed.indexedAt -// is MorphoDataItem.ListInfo -> it.list.indexedAt -// is MorphoDataItem.ModLabel -> Moment(Instant.DISTANT_PAST) -// is MorphoDataItem.ProfileItem -> Moment(Instant.DISTANT_PAST) -// is MorphoDataItem.LabelService -> it.service.indexedAt -// else -> { -// Moment(Instant.DISTANT_PAST) -// } -// } -// }.toList(), - cursor = cursor, title = first.title, uri = first.uri - ) - } - - fun concat( - first: MorphoData, - last: List, - cursor: AtCursor = first.cursor, - query: JsonElement = JsonObject(emptyMap()), - ): MorphoData { - return first.copy( - items = (first.items + last), - cursor = cursor, title = first.title, uri = first.uri - ) - } - - fun concat( - first: List, - last: MorphoData, - cursor: AtCursor = last.cursor, - ): MorphoData { - return last.copy( - items = (first + last.items), - cursor = cursor, title = last.title, uri = last.uri - ) - } - - fun concat( - posts: List, - feed: MorphoData, - cursor: AtCursor = feed.cursor, - query: JsonElement = JsonObject(emptyMap()), - ): MorphoData { - val new = fromFeed( - feedPosts = posts, - cursor, - feed.title, - feed.uri, - query = feed.query, - ) - return concat(new, feed, cursor, query) - } - - fun fromFeedGenList( - title: String, - uri: AtUri, - feeds: List, - cursor: AtCursor = AtCursor.EMPTY, - ): MorphoData { - return MorphoData( - title = title, - uri = uri, - cursor = cursor, - items = feeds.map { MorphoDataItem.FeedInfo(it) }.toMutableList(), - ) - } - - fun fromProfileList( - title: String, - uri: AtUri, - list: List, - cursor: AtCursor = AtCursor.EMPTY, - ): MorphoData { - return MorphoData( - title = title, - uri = uri, - cursor = cursor, - items = list.map { MorphoDataItem.ProfileItem(it) }.toMutableList(), - ) - } - - fun fromBskyList( - title: String, - uri: AtUri, - lists: List, - cursor: AtCursor = AtCursor.EMPTY, - ): MorphoData { - return MorphoData( - title = title, - uri = uri, - cursor = cursor, - items = lists.map { MorphoDataItem.ListInfo(it) }.toMutableList(), - ) - } - - fun fromModLabelDefs( - title: String, - uri: AtUri, - labels: List, - cursor: AtCursor = AtCursor.EMPTY, - ): MorphoData { - return MorphoData( - title = title, - uri = uri, - cursor = cursor, - items = labels.map { MorphoDataItem.ModLabel(it) }.toMutableList(), - ) - } - - fun fromModServiceDefs( - title: String, - uri: AtUri, - services: List, - cursor: AtCursor = AtCursor.EMPTY, - ): MorphoData { - return MorphoData( - title = title, - uri = uri, - cursor = cursor, - items = services.map { MorphoDataItem.LabelService(it) }.toMutableList(), - ) - } - - } - - val isHome: Boolean - get() = uri == AtUri.HOME_URI - - val isProfileFeed: Boolean - get() = uri.atUri.matches(AtUri.ProfilePostsUriRegex) || - uri.atUri.matches(AtUri.ProfileRepliesUriRegex) || - uri.atUri.matches(AtUri.ProfileMediaUriRegex) || - uri.atUri.matches(AtUri.ProfileLikesUriRegex) || - uri.atUri.matches(AtUri.ProfileUserListsUriRegex) || - uri.atUri.matches(AtUri.ProfileModServiceUriRegex) || - uri.atUri.matches(AtUri.ProfileFeedsListUriRegex) - - - val isMyProfile: Boolean - get() = (isProfileFeed && uri.atUri.contains("me")) || (uri == AtUri.MY_PROFILE_URI) - - val feedType: FeedType - get() = when { - isHome -> FeedType.HOME - uri.atUri.matches(AtUri.ProfilePostsUriRegex) -> FeedType.PROFILE_POSTS - uri.atUri.matches(AtUri.ProfileRepliesUriRegex) -> FeedType.PROFILE_REPLIES - uri.atUri.matches(AtUri.ProfileMediaUriRegex) -> FeedType.PROFILE_MEDIA - uri.atUri.matches(AtUri.ProfileLikesUriRegex) -> FeedType.PROFILE_LIKES - uri.atUri.matches(AtUri.ProfileUserListsUriRegex) -> FeedType.PROFILE_USER_LISTS - uri.atUri.matches(AtUri.ProfileModServiceUriRegex) -> FeedType.PROFILE_MOD_SERVICE - uri.atUri.matches(AtUri.ProfileFeedsListUriRegex) -> FeedType.PROFILE_FEEDS_LIST - uri.atUri.matches(AtUri.ListFeedUriRegex) -> FeedType.LIST_FOLLOWING - else -> FeedType.OTHER - } - - operator fun contains(cid: Cid): Boolean { - return items.fastAny { - when(it) { - is MorphoDataItem.Post -> it.post.cid == cid - is MorphoDataItem.Thread -> it.thread.contains(cid) - is MorphoDataItem.FeedInfo -> it.feed.cid == cid - is MorphoDataItem.ListInfo -> it.list.cid == cid - is MorphoDataItem.ModLabel -> false - is MorphoDataItem.ProfileItem -> false - is MorphoDataItem.LabelService -> it.service.cid == cid - else -> {false} - } - } - } - - fun collectThreads( - depth: Int = 3, height: Int = 80, - timeRange: Delta = Delta(Duration.parse("4h")), - repliesBumpThreads: Boolean = !isProfileFeed, - api: Butterfly? = null, // allows to just use local data - ): Flow> = flow { - val threads = mutableListOf() - val replies = mutableListOf() - val posts = mutableListOf() - val threadCandidates = mutableListOf() - items.fastForEach { item -> - when(item) { - is MorphoDataItem.Post -> { - if (item.isReply) replies.add(item) - else if (item.isOrphan) posts.add(item) - else posts.add(item) - } - is MorphoDataItem.Thread -> { - if (!item.isIncompleteThread) threads.add(item) - else threadCandidates.add(item) - } - else -> return@fastForEach - } - } - replies.fastForEachIndexed { index, reply -> - if (reply == null) return@fastForEachIndexed - if (reply.isOrphan) { - val parent = reply.post.reply?.parentPost - ?: reply.post.reply?.replyRef?.parent?.uri?.let { - if (api != null) { - null // stubbed out before removing - //getPost(it, api).firstOrNull() - } else null - } - val root = reply.post.reply?.rootPost - ?: reply.post.reply?.replyRef?.root?.uri?.let { - if (api != null) { - null // stubbed out before removing - //getPost(it, api).firstOrNull() - } else null - } - replies[index] = MorphoDataItem.Post( - reply.post.copy(reply = reply.post.reply?.copy(parentPost = parent, rootPost = root)), - reply.reason, - isOrphan = root != null && parent != null, - ) - } - val newReply = replies[index] ?: return@fastForEachIndexed // Update in case we changed it above - val replyRef = newReply.post.reply?.replyRef ?: return@fastForEachIndexed - val parent = replyRef.parent.uri - val root = replyRef.root.uri - val inThread = threads.indexOfFirst { it?.containsUri(parent) ?: false || it?.containsUri(root) ?: false } - if (inThread != -1) { - val thread = threads.getOrNull(inThread) ?: return@fastForEachIndexed - threads[inThread] = thread.addReply(newReply.post) - replies[index] = null - } - val inCandidates = threadCandidates.indexOfFirst { it?.containsUri(parent) ?: false || it?.containsUri(root) ?: false } - if (inCandidates != -1) { - val thread = threadCandidates.getOrNull(inCandidates) ?: return@fastForEachIndexed - threadCandidates[inCandidates] = thread.addReply(newReply.post) - replies[index] = null - } - - } - threadCandidates.fastForEachIndexed { index, thread -> - if (thread == null) return@fastForEachIndexed - val rootInThreads = threads.indexOfFirst { t -> t?.containsUri(thread.rootUri) ?: false } - if (rootInThreads == - 1) { - val threadToSplice = threads.getOrNull(rootInThreads) ?: return@fastForEachIndexed - if( - thread.thread.parents.firstOrNull() is ThreadPost.ViewablePost - && threadToSplice.thread.parents.firstOrNull() is ThreadPost.ViewablePost - && thread.rootUri == threadToSplice.rootUri - ) { - if(thread.thread.parents.size == 1 && threadToSplice.thread.parents.size == 1) { - // Both threads have the same, viewable root post and are only one level deep in terms of parents - val newEntry = thread.thread.parents.first() as ThreadPost.ViewablePost - val oldEntry = threadToSplice.thread.parents.first() as ThreadPost.ViewablePost - - val newReplies = (newEntry.replies + oldEntry.replies).distinctBy { it.uri }.toMutableList() - newReplies.add(ThreadPost.ViewablePost(thread.thread.post, null, thread.thread.replies)) - if( thread.getUri() != threadToSplice.getUri() ) - newReplies.add(ThreadPost.ViewablePost(threadToSplice.thread.post, null, threadToSplice.thread.replies)) - val newThread = BskyPostThread( - post = newEntry.post, - parents = listOf(), - replies = newReplies.distinctBy { it.uri }, - ) - threads[rootInThreads] = threadToSplice.copy(thread = newThread, isIncompleteThread = false) - threadCandidates[index] = null - } else if(thread.thread.parents.size == 2 && threadToSplice.thread.parents.size == 2) { - // Both threads have the same, viewable root post and parent chains are both length 2 - val newEntry = thread.thread.parents.first() as ThreadPost.ViewablePost - - val newReplies = mutableListOf() - if(thread.thread.parents.lastOrNull() !is ThreadPost.ViewablePost) return@fastForEachIndexed - if(threadToSplice.thread.parents.lastOrNull() !is ThreadPost.ViewablePost) return@fastForEachIndexed - val newParent = thread.thread.parents.last() as ThreadPost.ViewablePost - val oldParent = threadToSplice.thread.parents.last() as ThreadPost.ViewablePost - val newReply = ThreadPost.ViewablePost(thread.thread.post, null, thread.thread.replies) - val oldReply = ThreadPost.ViewablePost(threadToSplice.thread.post, null, threadToSplice.thread.replies) - newParent.addReply(newReply) - oldParent.addReply(oldReply) - newReplies.add(newReply) - newReplies.add(oldReply) - val newThread = BskyPostThread( - post = newEntry.post, - parents = listOf(newParent), - replies = newReplies.distinctBy { it.uri }, - ) - threads[rootInThreads] = threadToSplice.copy(thread = newThread, isIncompleteThread = false) - threadCandidates[index] = null - } - - } - } else { - val inThreads = threads.indexOfFirst { t -> t?.containsUri(thread.thread.post.uri) ?: false } - if (inThreads == - 1) { - val threadToSplice = threads.getOrNull(index) ?: return@fastForEachIndexed - threads[index] = threadToSplice.addReply(ThreadPost.ViewablePost(thread.thread.post, null, thread.thread.replies)) - threadCandidates[index] = null - } - } - } - threadCandidates.fastFilterNotNull() - if (threadCandidates.isNotEmpty()) threads.addAll(threadCandidates) - val newReplies = replies.filterNotNull() - .distinctBy { it.getUri() } - .filterNot { reply -> - if(reply.isRepost) return@filterNot false - if(reply.isQuotePost) return@filterNot false - reply.getUris().any { uri -> threads.any { it?.containsUri(uri) ?: false } } - }.sortedByDescending { when(it.reason) { - is BskyPostReason.BskyPostRepost -> it.reason.indexedAt - else -> it.post.createdAt - } }.iterator() - var newPosts = posts.toList().filterNotNull() - newPosts = newPosts.distinctBy { it.getUri() } - newPosts = newPosts.filterNot { post -> - if(post.isRepost) return@filterNot false - if(post.isQuotePost) return@filterNot false - post.getUris().any { uri -> threads.any { it?.containsUri(uri) ?: false } } - }.sortedByDescending { when(it.reason) { - is BskyPostReason.BskyPostRepost -> it.reason.indexedAt - else -> it.post.createdAt - } } - val newPostsIter = newPosts.iterator() - var newThreads = threads.toList().filterNotNull() - newThreads = newThreads.sortedByDescending { if(!repliesBumpThreads) { - it.rootAccessiblePost.createdAt - } else { - maxOf(it.thread.post.createdAt, - it.thread.replies.fold(it.thread.post.createdAt) { acc, post -> - val postTime = when(post) { - is ThreadPost.ViewablePost -> post.post.createdAt - is ThreadPost.BlockedPost -> Moment(Instant.DISTANT_PAST) - is ThreadPost.NotFoundPost -> Moment(Instant.DISTANT_PAST) - } - maxOf(acc, postTime) - }) - } } - newThreads = newThreads.distinctBy { it.getUri() } - .filterNot { thread -> - thread.getUris().filterNot { uri -> - newThreads.fastAny { it.getUri() == uri } }.size > 1 - } - val newThreadsIter = newThreads.iterator() - val newFeed = mutableListOf() - while(newPostsIter.hasNext() || newThreadsIter.hasNext() || newReplies.hasNext() ) { - if(newPostsIter.hasNext()) newFeed.add(newPostsIter.next()) - if(newThreadsIter.hasNext()) newFeed.add(newThreadsIter.next()) - if(newReplies.hasNext()) newFeed.add(newReplies.next()) - } - val dedupedFeed = newFeed.distinctBy { it.getUri() } - //println("New feed:\n${newFeed.joinToString("\n")}") - val sortedFeed = dedupedFeed.sortedByDescending { - when(it) { - is MorphoDataItem.Post -> when(it.reason) { - is BskyPostReason.BskyPostFeedPost -> it.post.createdAt - is BskyPostReason.BskyPostRepost -> it.reason.indexedAt - is BskyPostReason.SourceFeed -> it.post.createdAt - null -> it.post.createdAt - } - is MorphoDataItem.Thread -> if(!repliesBumpThreads) { - it.rootAccessiblePost.createdAt - } else { - maxOf(it.thread.post.createdAt, - it.thread.replies.fold(it.thread.post.createdAt) { acc, post -> - val postTime = when(post) { - is ThreadPost.ViewablePost -> post.post.createdAt - is ThreadPost.BlockedPost -> Moment(Instant.DISTANT_PAST) - is ThreadPost.NotFoundPost -> Moment(Instant.DISTANT_PAST) - } - maxOf(acc, postTime) - }) - } - } - } - //println("sorted feed:\n${sortedFeed.joinToString("\n")}") - @Suppress("UNCHECKED_CAST") val newData = copy( items = sortedFeed as List) - emit(newData) - }.flowOn(Dispatchers.Default) - - fun dedup(): MorphoData { - val newList = items.fastDistinctBy { when(it) { - is MorphoDataItem.FeedItem -> it.key - is MorphoDataItem.Post -> it.key - is MorphoDataItem.Thread -> it.key - is MorphoDataItem.ListInfo -> it.list.uri - is MorphoDataItem.ModLabel -> it.label.identifier - is MorphoDataItem.ProfileItem -> it.profile.did - is MorphoDataItem.LabelService -> it.service.uri - else -> {it.hashCode()} - } } - return this.copy(items = newList) - } - - -} - - -fun AtUri.id(api:Butterfly): AtIdentifier { - val idString = atUri.substringAfter("at://").split("/")[0] - return if (idString == "me") api.atpUser!!.id else { - // TODO: make this resolve a handle to a DID - if (idString.contains("did:")) Did(idString) else Handle(idString) - } -} - -fun areSameAuthor(authors: AuthorContext): Boolean { - val authorDid = authors.author.did - if(authors.parentAuthor != null && authors.parentAuthor.did != authorDid) { - return false - } - if(authors.grandParentAuthor != null && authors.grandParentAuthor.did != authorDid) { - return false - } - if(authors.rootAuthor != null && authors.rootAuthor.did != authorDid) { - return false - } - return true -} diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uistate/ContentCardState.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uistate/ContentCardState.kt index 5268882..20d2f6d 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uistate/ContentCardState.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uistate/ContentCardState.kt @@ -1,5 +1,6 @@ package com.morpho.app.model.uistate +import androidx.compose.runtime.Immutable import com.morpho.app.model.bluesky.* import com.morpho.app.model.uidata.* import com.morpho.app.util.MutableSharedFlowSerializer @@ -145,4 +146,19 @@ sealed interface ContentCardState { enum class ListsOrFeeds { Lists, Feeds -} \ No newline at end of file +} + +@Immutable +@Serializable +enum class FeedType { + HOME, + PROFILE_POSTS, + PROFILE_REPLIES, + PROFILE_MEDIA, + PROFILE_LIKES, + PROFILE_USER_LISTS, + PROFILE_MOD_SERVICE, + PROFILE_FEEDS_LIST, + LIST_FOLLOWING, + OTHER, +} diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uistate/LoadingState.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uistate/LoadingState.kt deleted file mode 100644 index b360686..0000000 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uistate/LoadingState.kt +++ /dev/null @@ -1,21 +0,0 @@ -package com.morpho.app.model.uistate - -import androidx.compose.runtime.Immutable -import kotlinx.serialization.Serializable - -@Immutable -@Serializable -sealed interface UiLoadingState { - data object Loading : UiLoadingState - data object Idle : UiLoadingState - data class Error(val errorMessage: String) : UiLoadingState -} - - -@Immutable -@Serializable -sealed interface ContentLoadingState { - data object Loading : ContentLoadingState - data object Idle : ContentLoadingState - data class Error(val errorMessage: String) : ContentLoadingState -} \ No newline at end of file diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uistate/LoginState.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uistate/LoginState.kt index 16b2b3f..e784e86 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uistate/LoginState.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uistate/LoginState.kt @@ -27,8 +27,9 @@ sealed interface AuthState { @Serializable @Immutable data class LoginState( - override val loadingState: UiLoadingState = UiLoadingState.Idle, + val loadingState: UiLoadingState = UiLoadingState.Idle, val mode: LoginScreenMode = LoginScreenMode.SIGN_IN, val authState: AuthState = AuthState.NoAuth, val credentials: Credentials? = null, -) : UiState +) + diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uistate/NotificationsState.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uistate/NotificationsState.kt index 492f65e..879ee9d 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uistate/NotificationsState.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uistate/NotificationsState.kt @@ -10,8 +10,7 @@ import org.koin.core.component.KoinComponent data class NotificationsUIState( val filterState: MutableStateFlow = MutableStateFlow(NotificationsFilterState()), val showPosts: Boolean = true, - override val loadingState: UiLoadingState = UiLoadingState.Loading, -): KoinComponent, UiState +): KoinComponent @Immutable @Serializable data class NotificationsFilterState( diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uistate/SkylineState.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uistate/SkylineState.kt index d71b309..566b94c 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uistate/SkylineState.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uistate/SkylineState.kt @@ -1,20 +1,3 @@ package com.morpho.app.model.uistate -import androidx.compose.runtime.Immutable -import kotlinx.serialization.Serializable - -@Immutable -@Serializable -enum class FeedType { - HOME, - PROFILE_POSTS, - PROFILE_REPLIES, - PROFILE_MEDIA, - PROFILE_LIKES, - PROFILE_USER_LISTS, - PROFILE_MOD_SERVICE, - PROFILE_FEEDS_LIST, - LIST_FOLLOWING, - OTHER, -} diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uistate/UiState.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uistate/UiState.kt index 613d26b..58b2de6 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uistate/UiState.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uistate/UiState.kt @@ -1,10 +1,8 @@ package com.morpho.app.model.uistate - -interface UiState { - val loadingState: UiLoadingState - - val isLoading: Boolean - get() = loadingState == UiLoadingState.Loading +sealed interface UiLoadingState { + data object Loading : UiLoadingState + data object Idle : UiLoadingState + data class Error(val errorMessage: String) : UiLoadingState } \ No newline at end of file diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/base/BaseScreenModel.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/base/BaseScreenModel.kt index 0c0493f..5ca6284 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/base/BaseScreenModel.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/base/BaseScreenModel.kt @@ -7,11 +7,15 @@ import app.cash.paging.Pager import app.cash.paging.cachedIn import cafe.adriel.voyager.core.model.ScreenModel import cafe.adriel.voyager.core.model.screenModelScope +import com.morpho.app.data.ContentLabelService import com.morpho.app.data.MorphoAgent +import com.morpho.app.data.NotificationsSource import com.morpho.app.model.bluesky.BskyPost -import com.morpho.app.model.bluesky.NotificationsSource import com.morpho.app.model.bluesky.toPost -import com.morpho.app.model.uidata.* +import com.morpho.app.model.uidata.Event +import com.morpho.app.model.uidata.MyProfilePresenter +import com.morpho.app.model.uidata.ProfilePresenter +import com.morpho.app.model.uidata.UIUpdate import com.morpho.butterfly.AtUri import com.morpho.butterfly.Did import kotlinx.coroutines.channels.BufferOverflow diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/notifications/NotificationsView.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/notifications/NotificationsView.kt index 832cf6a..829bf18 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/notifications/NotificationsView.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/notifications/NotificationsView.kt @@ -25,10 +25,10 @@ import cafe.adriel.voyager.core.model.screenModelScope import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.Navigator import cafe.adriel.voyager.navigator.currentOrThrow +import com.morpho.app.data.NotificationsListItem +import com.morpho.app.data.collectNotifications import com.morpho.app.model.bluesky.BskyPost import com.morpho.app.model.bluesky.DraftPost -import com.morpho.app.model.bluesky.NotificationsListItem -import com.morpho.app.model.bluesky.collectNotifications import com.morpho.app.model.uistate.NotificationsUIState import com.morpho.app.screens.base.tabbed.ProfileTab import com.morpho.app.screens.base.tabbed.TabScreen diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/notifications/NotificationAvatarList.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/notifications/NotificationAvatarList.kt index fa4de6e..7380e24 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/notifications/NotificationAvatarList.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/notifications/NotificationAvatarList.kt @@ -18,7 +18,7 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.withStyle import androidx.compose.ui.unit.dp -import com.morpho.app.model.bluesky.NotificationsListItem +import com.morpho.app.data.NotificationsListItem import com.morpho.app.ui.elements.OutlinedAvatar import com.morpho.butterfly.Did import kotlin.math.min diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/notifications/NotificationsElement.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/notifications/NotificationsElement.kt index 231128e..e4ca873 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/notifications/NotificationsElement.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/notifications/NotificationsElement.kt @@ -13,9 +13,9 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import com.atproto.repo.StrongRef +import com.morpho.app.data.NotificationsListItem import com.morpho.app.model.bluesky.BskyNotification import com.morpho.app.model.bluesky.BskyPost -import com.morpho.app.model.bluesky.NotificationsListItem import com.morpho.app.ui.common.OnPostClicked import com.morpho.app.ui.elements.MenuOptions import com.morpho.app.ui.post.PostFragment diff --git a/Morpho/composeApp/src/desktopMain/kotlin/com/morpho/app/ui/common/BackHandler.kt b/Morpho/composeApp/src/desktopMain/kotlin/com/morpho/app/ui/common/BackHandler.kt deleted file mode 100644 index 27225c8..0000000 --- a/Morpho/composeApp/src/desktopMain/kotlin/com/morpho/app/ui/common/BackHandler.kt +++ /dev/null @@ -1,7 +0,0 @@ -package com.morpho.app.ui.common - -import androidx.compose.runtime.Composable - -@Composable -actual fun BackHandler(content: () -> Unit) { -} \ No newline at end of file From 7ad0e94efbf6a74eb60b0e61deb5970fc98ee888 Mon Sep 17 00:00:00 2001 From: Orual Date: Tue, 17 Sep 2024 17:40:22 -0400 Subject: [PATCH 14/42] Some cleanup of unused code. --- Morpho/composeApp/build.gradle.kts | 46 ++++--- .../androidMain/kotlin/Platform.android.kt | 36 ----- .../com/morpho/app/MorphoApplication.kt | 20 +-- .../kotlin/com/morpho/app/Platform.android.kt | 15 +++ .../DetailedProfileFragment.android.kt | 31 ++++- .../kotlin/com/morpho/app/Platform.kt | 25 ---- .../kotlin/com/morpho/app/data/FeedTuner.kt | 126 ++++-------------- .../app/model/bluesky/BskyLabelService.kt | 3 +- .../app/model/bluesky/MorphoDataItem.kt | 20 ++- .../morpho/app/model/uistate/LoginState.kt | 5 +- .../morpho/app/screens/login/LoginScreen.kt | 29 +++- .../app/screens/main/tabbed/TabbedHomeView.kt | 22 ++- .../app/screens/profile/TabbedProfileView.kt | 27 ++-- .../com/morpho/app/ui/common/PostComposer.kt | 46 +++++-- .../morpho/app/ui/common/SkylineTopAppBar.kt | 23 +++- .../app/ui/elements/HighlightIndication.kt | 41 ------ .../morpho/app/ui/elements/OutlinedAvatar.kt | 16 +-- .../morpho/app/ui/post/EmbedPostFragment.kt | 35 +++-- .../com/morpho/app/ui/post/PostFragment.kt | 48 +++++-- .../app/ui/profile/DetailedProfileFragment.kt | 4 - .../kotlin/com/morpho/app/Platform.desktop.kt | 8 -- .../DetailedProfileFragment.desktop.kt | 73 +++++----- Morpho/gradle/libs.versions.toml | 16 +-- gradle/libs.versions.toml | 16 +-- 24 files changed, 350 insertions(+), 381 deletions(-) delete mode 100644 Morpho/composeApp/src/androidMain/kotlin/Platform.android.kt delete mode 100644 Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/elements/HighlightIndication.kt diff --git a/Morpho/composeApp/build.gradle.kts b/Morpho/composeApp/build.gradle.kts index 327bf56..cba99af 100644 --- a/Morpho/composeApp/build.gradle.kts +++ b/Morpho/composeApp/build.gradle.kts @@ -1,5 +1,7 @@ import org.jetbrains.compose.desktop.application.dsl.TargetFormat +import org.jetbrains.kotlin.compose.compiler.gradle.ComposeFeatureFlag import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi +import org.jetbrains.kotlin.gradle.dsl.JvmTarget plugins { alias(libs.plugins.kotlinMultiplatform) @@ -20,30 +22,20 @@ kotlin { androidTarget { @OptIn(ExperimentalKotlinGradlePluginApi::class) compilerOptions { - freeCompilerArgs.addAll( - "-P", - "plugin:org.jetbrains.kotlin.parcelize:additionalAnnotation=com.morpho.app.CommonParcelize", - ) - } - compilations.all { - kotlinOptions { - jvmTarget = "11" - - } - //move from the deprecated above to this -// compileJavaTaskProvider.configure { -// jvm -// } - + jvmTarget.set(JvmTarget.JVM_11) } } jvm("desktop") - +// linuxX64() +// mingwX64() +// linuxArm64() + listOf( iosX64(), iosArm64(), + iosSimulatorArm64() ).forEach { iosTarget -> iosTarget.binaries.framework { @@ -88,6 +80,10 @@ kotlin { implementation("androidx.paging:paging-compose:3.3.0-alpha02") } + commonMain.languageSettings { + progressiveMode = true + } + commonMain.dependencies { implementation("com.morpho:shared") @@ -107,6 +103,7 @@ kotlin { implementation(compose.material) implementation(compose.material3) implementation(compose.ui) + implementation(compose.components.resources) implementation(compose.components.uiToolingPreview) implementation(compose.materialIconsExtended) @@ -177,7 +174,6 @@ kotlin { implementation(libs.voyager.navigator) // Screen Model implementation(libs.voyager.screenmodel) - implementation("org.jetbrains.androidx.lifecycle:lifecycle-viewmodel-compose:2.8.0") implementation("cafe.adriel.voyager:voyager-lifecycle-kmp:1.1.0-beta02") // BottomSheetNavigator implementation(libs.voyager.bottom.sheet.navigator) @@ -197,7 +193,7 @@ kotlin { api("dev.icerock.moko:parcelize:0.9.0") } - nativeMain.dependencies { + iosMain.dependencies { implementation("app.cash.paging:paging-runtime-uikit:3.3.0-alpha02-0.5.1") } desktopMain.dependencies { @@ -272,17 +268,26 @@ android { } buildFeatures { compose = true - viewBinding = true + //viewBinding = true } composeOptions { - kotlinCompilerExtensionVersion = "1.5.11" + kotlinCompilerExtensionVersion = "1.5.15" } task("testClasses") } +composeCompiler { + includeSourceInformation = true + includeTraceMarkers = true + + featureFlags = setOf( + ComposeFeatureFlag.StrongSkipping.disabled(), + ComposeFeatureFlag.OptimizeNonSkippingGroups, + ) +} compose.desktop { application { @@ -306,6 +311,7 @@ compose.desktop { dependencies { add("kspCommonMainMetadata", libs.koin.ksp.compiler) // Run KSP on [commonMain] code + //add("kspJvm", libs.koin.ksp.compiler) add("kspAndroid", libs.koin.ksp.compiler) //add("kspIosX64", libs.koin.ksp.compiler) //add("kspIosArm64", libs.koin.ksp.compiler) diff --git a/Morpho/composeApp/src/androidMain/kotlin/Platform.android.kt b/Morpho/composeApp/src/androidMain/kotlin/Platform.android.kt deleted file mode 100644 index 73f97d9..0000000 --- a/Morpho/composeApp/src/androidMain/kotlin/Platform.android.kt +++ /dev/null @@ -1,36 +0,0 @@ -@file:Suppress("EXPECT_ACTUAL_CLASSIFIERS_ARE_IN_BETA_WARNING") -package com.morpho.app - -import android.os.Build -import android.os.Parcel -import android.os.Parcelable -import kotlinx.datetime.LocalDateTime -import kotlinx.parcelize.Parceler -import kotlinx.parcelize.Parcelize -import kotlinx.parcelize.RawValue -import kotlinx.parcelize.TypeParceler - -actual typealias CommonParcelize = Parcelize -actual typealias CommonParcelable = Parcelable - -actual typealias CommonRawValue = RawValue -actual typealias CommonParceler = Parceler -actual typealias CommonTypeParceler = TypeParceler -actual object LocalDateTimeParceler : Parceler { - override fun create(parcel: Parcel): LocalDateTime { - val date = parcel.readString() - return date?.let { LocalDateTime.parse(it) } - ?: LocalDateTime(0, 0, 0, 0, 0) - } - - override fun LocalDateTime.write(parcel: Parcel, flags: Int) { - parcel.writeString(this.toString()) - } -} - -class AndroidPlatform : Platform { - override val name: String = "Android ${Build.VERSION.SDK_INT}" -} - -actual fun getPlatform(): Platform = AndroidPlatform() - diff --git a/Morpho/composeApp/src/androidMain/kotlin/com/morpho/app/MorphoApplication.kt b/Morpho/composeApp/src/androidMain/kotlin/com/morpho/app/MorphoApplication.kt index b5fe8af..07922ec 100644 --- a/Morpho/composeApp/src/androidMain/kotlin/com/morpho/app/MorphoApplication.kt +++ b/Morpho/composeApp/src/androidMain/kotlin/com/morpho/app/MorphoApplication.kt @@ -1,33 +1,19 @@ package com.morpho.app import android.app.Application -import androidx.lifecycle.AndroidViewModel -import androidx.lifecycle.DefaultLifecycleObserver import com.gu.toolargetool.TooLargeTool -import com.morpho.app.data.MorphoAgent import com.morpho.app.data.PreferencesRepository import com.morpho.app.di.appModule import com.morpho.app.di.dataModule import com.morpho.app.di.storageModule import com.morpho.butterfly.auth.SessionRepository import com.morpho.butterfly.auth.UserRepository -import org.koin.android.annotation.KoinViewModel -import org.koin.android.ext.android.getKoin -import org.koin.android.ext.koin.androidApplication import org.koin.android.ext.koin.androidContext import org.koin.android.ext.koin.androidLogger -import org.koin.androidx.viewmodel.dsl.viewModel import org.koin.core.context.startKoin import org.koin.core.parameter.parametersOf -import org.koin.dsl.module -@KoinViewModel -class AndroidMainViewModel(app: Application): AndroidViewModel(app), DefaultLifecycleObserver { - - val agent = app.getKoin().get() -} - class MorphoApplication : Application() { override fun onCreate() { TooLargeTool.startLogging(this); @@ -35,17 +21,13 @@ class MorphoApplication : Application() { val koin = startKoin { androidContext(this@MorphoApplication) androidLogger() - modules(androidModule, appModule, storageModule, dataModule) + modules(appModule, storageModule, dataModule) }.koin val sessionRepository = koin.get { parametersOf(cacheDir.path.toString()) } val userRepository = koin.get { parametersOf(cacheDir.path.toString()) } val prefs = koin.get { parametersOf(cacheDir.path.toString()) } - val agent = koin.get() super.onCreate() } } -val androidModule = module { - viewModel { AndroidMainViewModel(androidApplication()) } -} \ No newline at end of file diff --git a/Morpho/composeApp/src/androidMain/kotlin/com/morpho/app/Platform.android.kt b/Morpho/composeApp/src/androidMain/kotlin/com/morpho/app/Platform.android.kt index b1717d1..a98ef43 100644 --- a/Morpho/composeApp/src/androidMain/kotlin/com/morpho/app/Platform.android.kt +++ b/Morpho/composeApp/src/androidMain/kotlin/com/morpho/app/Platform.android.kt @@ -1,7 +1,22 @@ +@file:Suppress("EXPECT_ACTUAL_CLASSIFIERS_ARE_IN_BETA_WARNING") package com.morpho.app + +import android.os.Build +import kotlinx.parcelize.RawValue import java.util.Locale +actual typealias CommonRawValue = RawValue + +class AndroidPlatform : Platform { + override val name: String = "Android ${Build.VERSION.SDK_INT}" +} + +actual fun getPlatform(): Platform = AndroidPlatform() + + + + actual val myLang:String? get() = Locale.getDefault().language diff --git a/Morpho/composeApp/src/androidMain/kotlin/com/morpho/app/ui/profile/DetailedProfileFragment.android.kt b/Morpho/composeApp/src/androidMain/kotlin/com/morpho/app/ui/profile/DetailedProfileFragment.android.kt index 30d216c..ed8623f 100644 --- a/Morpho/composeApp/src/androidMain/kotlin/com/morpho/app/ui/profile/DetailedProfileFragment.android.kt +++ b/Morpho/composeApp/src/androidMain/kotlin/com/morpho/app/ui/profile/DetailedProfileFragment.android.kt @@ -4,13 +4,37 @@ import androidx.compose.animation.animateContentSize import androidx.compose.animation.core.Spring import androidx.compose.animation.core.spring import androidx.compose.foundation.background -import androidx.compose.foundation.layout.* +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.WindowInsetsSides +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.only +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.requiredHeight +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.systemBars +import androidx.compose.foundation.layout.widthIn +import androidx.compose.foundation.layout.windowInsetsPadding +import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.text.selection.SelectionContainer import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.ArrowBackIosNew -import androidx.compose.material3.* +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.IconButtonDefaults +import androidx.compose.material3.LargeTopAppBar +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.material3.TopAppBarScrollBehavior import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -31,13 +55,10 @@ import com.morpho.app.ui.elements.RichTextElement import kotlinx.collections.immutable.toImmutableList import morpho.composeapp.generated.resources.Res import morpho.composeapp.generated.resources.test_banner -import org.jetbrains.compose.resources.ExperimentalResourceApi import org.jetbrains.compose.resources.painterResource @OptIn( ExperimentalMaterial3Api::class, - ExperimentalLayoutApi::class, - ExperimentalResourceApi::class ) @Composable public actual fun DetailedProfileFragment( diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/Platform.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/Platform.kt index 8f6eb6b..a9081af 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/Platform.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/Platform.kt @@ -1,8 +1,6 @@ @file:Suppress("EXPECT_ACTUAL_CLASSIFIERS_ARE_IN_BETA_WARNING") package com.morpho.app -import kotlinx.datetime.LocalDateTime - interface Platform { val name: String } @@ -12,32 +10,9 @@ expect fun getPlatform(): Platform expect val myLang:String? expect val myCountry:String? -// For Android @Parcelize -@OptIn(ExperimentalMultiplatform::class) -@OptionalExpectation -@Target(AnnotationTarget.CLASS) -@Retention(AnnotationRetention.BINARY) -expect annotation class CommonParcelize() // For Android @Parcelize @OptIn(ExperimentalMultiplatform::class) @OptionalExpectation @Target(AnnotationTarget.TYPE) expect annotation class CommonRawValue() - -// For Android Parcelable -expect interface CommonParcelable - -// For Android @TypeParceler -@OptIn(ExperimentalMultiplatform::class) -@OptionalExpectation -@Retention(AnnotationRetention.SOURCE) -@Repeatable -@Target(AnnotationTarget.CLASS, AnnotationTarget.PROPERTY) -expect annotation class CommonTypeParceler>() - -// For Android Parceler -expect interface CommonParceler - -// For Android @TypeParceler to convert LocalDateTime to Parcel -expect object LocalDateTimeParceler: CommonParceler \ No newline at end of file diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/data/FeedTuner.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/data/FeedTuner.kt index 12720f0..6758e07 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/data/FeedTuner.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/data/FeedTuner.kt @@ -1,11 +1,16 @@ package com.morpho.app.data -import com.morpho.app.model.bluesky.* -import com.morpho.app.model.uidata.MorphoData -import com.morpho.app.model.uidata.areSameAuthor -import com.morpho.app.model.uistate.FeedType -import com.morpho.butterfly.* +import com.morpho.app.model.bluesky.AuthorContext +import com.morpho.app.model.bluesky.AuthorFilter +import com.morpho.app.model.bluesky.FeedDescriptor +import com.morpho.app.model.bluesky.MorphoDataItem +import com.morpho.app.model.bluesky.Profile +import com.morpho.app.model.bluesky.ThreadPost +import com.morpho.butterfly.AtUri import com.morpho.butterfly.BskyPreferences +import com.morpho.butterfly.Did +import com.morpho.butterfly.Language +import com.morpho.butterfly.PagedResponse import kotlinx.collections.immutable.persistentListOf import kotlinx.serialization.Serializable @@ -70,53 +75,6 @@ data class FeedTuner(val tuners: List useFeedTuners( - prefs: BskyUserPreferences, - feed: MorphoData - ): List> { - if(feed.isProfileFeed) { - when(feed.feedType) { - FeedType.PROFILE_POSTS -> return listOf( - FeedTuner(tuners = persistentListOf( - Companion::removeReplies - )) as FeedTuner - ) - FeedType.PROFILE_USER_LISTS -> return listOf() - FeedType.PROFILE_FEEDS_LIST -> return listOf() - FeedType.PROFILE_MOD_SERVICE -> return listOf() - else -> {} - } - } - val languages = prefs.preferences.languages.toList() - val languageTuner: TunerFunction = { f, t -> - preferredLanguageOnly(languages, f, t) - } - if(feed.feedType == FeedType.OTHER) { - return listOf(FeedTuner(tuners = persistentListOf(languageTuner))) as List> - } - if(feed.feedType == FeedType.LIST_FOLLOWING || feed.feedType == FeedType.HOME) { - val userDid = Did(prefs.user.userDid) - val tuners = mutableListOf(FeedTuner(tuners = persistentListOf(Companion::removeOrphans))) - val feedPrefs = prefs.preferences.feedViewPrefs[feed.uri.atUri] ?: - return tuners.toList() as List> - if(feedPrefs.hideReposts) tuners.add(FeedTuner(tuners = persistentListOf(Companion::removeReposts))) - if(feedPrefs.hideReplies) tuners.add(FeedTuner(tuners = persistentListOf(Companion::removeReplies))) - else { - val followedRepliesOnly: TunerFunction = { f, t -> - followedRepliesOnly(userDid, f, t) - } - tuners.add(FeedTuner(tuners = persistentListOf(followedRepliesOnly))) - } - if(feedPrefs.hideQuotePosts) tuners.add( - FeedTuner(tuners = persistentListOf( - Companion::removeQuotePosts - )) - ) - tuners.add(FeedTuner(tuners = persistentListOf(Companion::dedupThreads))) - return tuners.toList() as List> - } - return listOf() - } fun removeReplies( feed: List, @@ -229,55 +187,7 @@ data class FeedTuner(val tuners: List } } - fun tune( - feed: MorphoData - ): MorphoData { - var workingFeed = feed.items - tuners.forEach { tuner -> - workingFeed = tuner(workingFeed, this) - } - workingFeed = workingFeed.map { item -> - if(seenKeys.contains(item.key)) return@map null - else if(item is MorphoDataItem.Thread) { - val itemUris = item.getUris() - val seenInThisThread = itemUris.filter { seenUris.contains(it) } - if(seenInThisThread.isNotEmpty()) { - if(seenInThisThread.size == itemUris.size) { - return@map null - } else { - val newParents = item.thread.parents.filter { parent -> - when(parent) { - is ThreadPost.ViewablePost -> parent.post.uri in seenInThisThread - is ThreadPost.BlockedPost -> false - is ThreadPost.NotFoundPost -> false - } - } - val newThread = item.copy(thread = item.thread.filterReplies { reply -> - when(reply) { - is ThreadPost.ViewablePost -> reply.post.uri in seenInThisThread - is ThreadPost.BlockedPost -> false - is ThreadPost.NotFoundPost -> false - } - }.copy(parents = newParents)) - seenUris.addAll(itemUris) - if(newThread.thread.replies.isEmpty() && newThread.thread.parents.isEmpty()) { - return@map null - } else { - return@map newThread - } - } - } else { - seenUris.addAll(itemUris) - item - } - } else { - val disableDedub = item.isReply && item.isRepost - if(!disableDedub) seenKeys.add(item.key) - item - } - }.filterNotNull() as List - return feed.copy(items = workingFeed) - } + fun tune( feed: PagedResponse.Feed ): PagedResponse.Feed { @@ -385,4 +295,18 @@ fun shouldDisplayReplyInFollowing( fun isSelfOrFollowing(profile: Profile?, userDid: Did): Boolean { return profile?.did == userDid || profile?.followedByMe == true +} + +fun areSameAuthor(authors: AuthorContext): Boolean { + val authorDid = authors.author.did + if(authors.parentAuthor != null && authors.parentAuthor.did != authorDid) { + return false + } + if(authors.grandParentAuthor != null && authors.grandParentAuthor.did != authorDid) { + return false + } + if(authors.rootAuthor != null && authors.rootAuthor.did != authorDid) { + return false + } + return true } \ No newline at end of file diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/BskyLabelService.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/BskyLabelService.kt index bab0a28..1c6d29f 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/BskyLabelService.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/BskyLabelService.kt @@ -10,6 +10,7 @@ import com.morpho.butterfly.AtUri import com.morpho.butterfly.Cid import com.morpho.butterfly.Did import com.morpho.butterfly.Handle +import dev.icerock.moko.parcelize.Parcelable import dev.icerock.moko.parcelize.Parcelize import dev.icerock.moko.parcelize.TypeParceler import kotlinx.collections.immutable.persistentListOf @@ -30,7 +31,7 @@ open class BskyLabelService( val indexedAt: Moment, val policies: List, val labels: List, -) { +): Parcelable { val did: Did get() = creator?.did ?: Did("did:blank:did") val handle: Handle diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/MorphoDataItem.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/MorphoDataItem.kt index 754f72f..65e63e0 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/MorphoDataItem.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/MorphoDataItem.kt @@ -2,7 +2,6 @@ package com.morpho.app.model.bluesky import androidx.compose.runtime.Immutable import app.bsky.feed.* -import com.morpho.app.CommonParcelize import com.morpho.app.util.deserialize import com.morpho.butterfly.AtUri import dev.icerock.moko.parcelize.Parcelable @@ -20,12 +19,10 @@ import kotlinx.serialization.Serializable @Parcelize @Immutable @Serializable -@CommonParcelize sealed interface MorphoDataItem: Parcelable { @Immutable @Serializable - @CommonParcelize sealed interface FeedItem: MorphoDataItem { companion object { fun fromFeedViewPost(feedPost: FeedViewPost): FeedItem { @@ -252,7 +249,7 @@ sealed interface MorphoDataItem: Parcelable { @Immutable @Serializable - @CommonParcelize + @Parcelize data class Post( val post: BskyPost, val reason: BskyPostReason? = post.reason, @@ -261,7 +258,7 @@ sealed interface MorphoDataItem: Parcelable { @Immutable @Serializable - @CommonParcelize + @Parcelize data class Thread( val thread: BskyPostThread, val reason: BskyPostReason? = null, @@ -279,21 +276,21 @@ sealed interface MorphoDataItem: Parcelable { @Immutable @Serializable - @CommonParcelize + @Parcelize data class FeedInfo( val feed: FeedGenerator, ): MorphoDataItem @Immutable @Serializable - @CommonParcelize + @Parcelize data class ProfileItem( val profile:Profile, ): MorphoDataItem @Immutable @Serializable - @CommonParcelize + @Parcelize data class ListInfo( val list: BskyList, ): MorphoDataItem @@ -301,14 +298,14 @@ sealed interface MorphoDataItem: Parcelable { @Immutable @Serializable - @CommonParcelize + @Parcelize data class ModLabel( val label: BskyLabelDefinition, ): MorphoDataItem @Immutable @Serializable - @CommonParcelize + @Parcelize data class LabelService( val service: BskyLabelService, ): MorphoDataItem @@ -380,6 +377,7 @@ sealed interface MorphoDataItem: Parcelable { } +@Parcelize @Immutable @Serializable data class AuthorContext( @@ -387,4 +385,4 @@ data class AuthorContext( val parentAuthor: Profile? = null, val grandParentAuthor: Profile? = null, val rootAuthor: Profile? = null, -) \ No newline at end of file +): Parcelable \ No newline at end of file diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uistate/LoginState.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uistate/LoginState.kt index e784e86..deb1930 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uistate/LoginState.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uistate/LoginState.kt @@ -31,5 +31,8 @@ data class LoginState( val mode: LoginScreenMode = LoginScreenMode.SIGN_IN, val authState: AuthState = AuthState.NoAuth, val credentials: Credentials? = null, -) +) { + val isLoading: Boolean + get() = loadingState is UiLoadingState.Loading +} diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/login/LoginScreen.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/login/LoginScreen.kt index 9c61ea5..f442266 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/login/LoginScreen.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/login/LoginScreen.kt @@ -1,11 +1,26 @@ package com.morpho.app.screens.login -import androidx.compose.foundation.layout.* +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.material3.* -import androidx.compose.runtime.* +import androidx.compose.material3.Button +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Scaffold +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.SnackbarResult +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusManager @@ -21,17 +36,17 @@ import cafe.adriel.voyager.navigator.currentOrThrow import cafe.adriel.voyager.navigator.tab.LocalTabNavigator import cafe.adriel.voyager.navigator.tab.Tab import cafe.adriel.voyager.navigator.tab.TabOptions -import com.morpho.app.CommonParcelable -import com.morpho.app.CommonParcelize import com.morpho.app.model.uistate.AuthState import com.morpho.app.screens.base.tabbed.TabbedBaseScreen import com.morpho.app.ui.common.LoadingCircle +import dev.icerock.moko.parcelize.Parcelable +import dev.icerock.moko.parcelize.Parcelize import kotlinx.coroutines.launch import kotlinx.serialization.Serializable -@CommonParcelize +@Parcelize @Serializable -data object LoginScreen: Tab, CommonParcelable { +data object LoginScreen: Tab, Parcelable { override val key: ScreenKey = hashCode().toString() + "TabbedLoginScreen" diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/main/tabbed/TabbedHomeView.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/main/tabbed/TabbedHomeView.kt index 1413cf9..cccb12c 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/main/tabbed/TabbedHomeView.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/main/tabbed/TabbedHomeView.kt @@ -10,11 +10,25 @@ import androidx.compose.animation.core.spring import androidx.compose.animation.slideInHorizontally import androidx.compose.animation.slideOutHorizontally import androidx.compose.animation.togetherWith -import androidx.compose.foundation.layout.* -import androidx.compose.material3.* +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.statusBarsPadding +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.SecondaryScrollableTabRow +import androidx.compose.material3.Tab +import androidx.compose.material3.TabPosition +import androidx.compose.material3.TabRowDefaults import androidx.compose.material3.TabRowDefaults.tabIndicatorOffset -import androidx.compose.runtime.* +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.IntOffset @@ -200,7 +214,7 @@ fun HomeTabRow( selectedTabIndex = selectedTabIndex, modifier = modifier.fillMaxWidth(),//.zIndex(1f), edgePadding = 10.dp, - indicator = { tabPositions -> + indicator = { tabPositions: List -> if(tabPositions.isNotEmpty()) { TabRowDefaults.SecondaryIndicator( Modifier.tabIndicatorOffset(tabPositions[max(0, min(selectedTabIndex, tabs.lastIndex))]) diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/profile/TabbedProfileView.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/profile/TabbedProfileView.kt index 5916aa8..cb1fdee 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/profile/TabbedProfileView.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/profile/TabbedProfileView.kt @@ -5,9 +5,19 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding -import androidx.compose.material3.* -import androidx.compose.runtime.* +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.SecondaryScrollableTabRow +import androidx.compose.material3.Tab +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.material3.TopAppBarScrollBehavior +import androidx.compose.material3.rememberTopAppBarState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.unit.dp @@ -21,6 +31,7 @@ import cafe.adriel.voyager.navigator.tab.TabDisposable import cafe.adriel.voyager.navigator.tab.TabNavigator import cafe.adriel.voyager.navigator.tab.TabOptions import coil3.annotation.ExperimentalCoilApi +import com.morpho.app.model.bluesky.DetailedProfile import com.morpho.app.model.uidata.Event import com.morpho.app.model.uidata.FeedEvent import com.morpho.app.model.uistate.ContentCardState @@ -38,7 +49,7 @@ import cafe.adriel.voyager.navigator.tab.Tab as NavTab @OptIn(ExperimentalAnimationApi::class, ExperimentalMaterial3Api::class) @Composable fun MyTabbedProfileTopBar( - profile: ContentCardState.MyProfile, + profile: DetailedProfile, scrollBehavior: TopAppBarScrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior( rememberTopAppBarState()), tabs: List, @@ -53,7 +64,7 @@ fun MyTabbedProfileTopBar( .nestedScroll(scrollBehavior.nestedScrollConnection), ) { DetailedProfileFragment( - profile = profile.profile, + profile = profile, myProfile = true, isTopLevel = true, scrollBehavior = scrollBehavior, @@ -80,7 +91,7 @@ fun MyTabbedProfileTopBar( @OptIn(ExperimentalAnimationApi::class, ExperimentalMaterial3Api::class) @Composable fun TabbedProfileTopBar( - profile: ContentCardState.FullProfile, + profile: DetailedProfile, scrollBehavior: TopAppBarScrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior( rememberTopAppBarState()), tabs: List, @@ -95,7 +106,7 @@ fun TabbedProfileTopBar( .nestedScroll(scrollBehavior.nestedScrollConnection), ) { DetailedProfileFragment( - profile = profile.profile, + profile = profile, myProfile = true, isTopLevel = true, scrollBehavior = scrollBehavior, @@ -166,7 +177,7 @@ fun TabScreen.TabbedProfileContent( modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection), topContent = { if(ownProfile) MyTabbedProfileTopBar( - profile = myProfileState, + profile = myProfileState.profile, scrollBehavior = scrollBehavior, tabs = tabs, onBackClicked = { navigator.pop() }, @@ -185,7 +196,7 @@ fun TabScreen.TabbedProfileContent( }, tabIndex = selectedTabIndex, ) else if(profileState != null) TabbedProfileTopBar( - profile = profileState, + profile = profileState.profile, scrollBehavior = scrollBehavior, tabs = tabs, onBackClicked = { navigator.pop() }, diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/common/PostComposer.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/common/PostComposer.kt index 504795a..3b5fc57 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/common/PostComposer.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/common/PostComposer.kt @@ -1,14 +1,42 @@ package com.morpho.app.ui.common -import androidx.compose.foundation.layout.* +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.ime +import androidx.compose.foundation.layout.imePadding +import androidx.compose.foundation.layout.navigationBars +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.union import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Image -import androidx.compose.material3.* -import androidx.compose.runtime.* +import androidx.compose.material3.Button +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.ModalBottomSheet +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.SheetState +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.material3.rememberModalBottomSheetState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalFocusManager @@ -39,15 +67,15 @@ enum class ComposerRole { @OptIn(ExperimentalMaterial3Api::class) @Composable -inline fun BottomSheetPostComposer( +fun BottomSheetPostComposer( modifier: Modifier = Modifier, - crossinline onDismissRequest: ()-> Unit = {}, + onDismissRequest: ()-> Unit = {}, initialContent: BskyPost? = null, role: ComposerRole = ComposerRole.StandalonePost, draft: DraftPost = DraftPost(), - crossinline onSend: (DraftPost) -> Unit = {}, - crossinline onCancel: () -> Unit = {}, - crossinline onUpdate: (DraftPost) -> Unit = {}, + onSend: (DraftPost) -> Unit = {}, + onCancel: () -> Unit = {}, + onUpdate: (DraftPost) -> Unit = {}, sheetState:SheetState = rememberModalBottomSheetState(), scope: CoroutineScope = rememberCoroutineScope() ) { @@ -64,7 +92,7 @@ inline fun BottomSheetPostComposer( sheetState = sheetState, windowInsets = WindowInsets.navigationBars.union(WindowInsets.ime), - ){ + ){ PostComposer( role = role, modifier = modifier, diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/common/SkylineTopAppBar.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/common/SkylineTopAppBar.kt index ebfe567..6ce24e0 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/common/SkylineTopAppBar.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/common/SkylineTopAppBar.kt @@ -1,10 +1,27 @@ package com.morpho.app.ui.common -import androidx.compose.foundation.layout.* +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Menu -import androidx.compose.material3.* +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.SecondaryScrollableTabRow +import androidx.compose.material3.Surface +import androidx.compose.material3.Tab +import androidx.compose.material3.TabPosition +import androidx.compose.material3.TabRowDefaults import androidx.compose.material3.TabRowDefaults.tabIndicatorOffset +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf @@ -71,7 +88,7 @@ fun SkylineTopBar( selectedTabIndex = selectedTab, modifier = modifier.offset(y = (-8).dp, x = 4.dp ), edgePadding = 10.dp, - indicator = { tabPositions -> + indicator = @Composable { tabPositions: List -> if(tabPositions.isNotEmpty()) { TabRowDefaults.SecondaryIndicator( Modifier.tabIndicatorOffset(tabPositions[max(0, min(selectedTab, tabList.lastIndex))]) diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/elements/HighlightIndication.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/elements/HighlightIndication.kt deleted file mode 100644 index a76c6a0..0000000 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/elements/HighlightIndication.kt +++ /dev/null @@ -1,41 +0,0 @@ -package com.morpho.app.ui.elements - -import androidx.compose.foundation.Indication -import androidx.compose.foundation.IndicationInstance -import androidx.compose.foundation.interaction.InteractionSource -import androidx.compose.foundation.interaction.collectIsFocusedAsState -import androidx.compose.runtime.Composable -import androidx.compose.runtime.State -import androidx.compose.runtime.getValue -import androidx.compose.runtime.remember -import androidx.compose.ui.geometry.CornerRadius -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.drawscope.ContentDrawScope -import androidx.compose.ui.graphics.drawscope.Stroke -import androidx.compose.ui.unit.dp - -class MorphoHighlightIndicationInstance(isEnabledState: State) : - IndicationInstance { - private val isEnabled by isEnabledState - override fun ContentDrawScope.drawIndication() { - drawContent() - if (isEnabled) { - drawRoundRect(cornerRadius = CornerRadius(4.dp.toPx()), size = size, color = Color.Gray, alpha = 0.2f) - drawRoundRect(cornerRadius = CornerRadius(4.dp.toPx()), - style = Stroke(width = Stroke.HairlineWidth), - size = size, color = Color.White, alpha = 0.9f) - } - } - -} - -class MorphoHighlightIndication : Indication { - @Composable - override fun rememberUpdatedInstance(interactionSource: InteractionSource): - IndicationInstance { - val isFocusedState = interactionSource.collectIsFocusedAsState() - return remember(interactionSource) { - MorphoHighlightIndicationInstance(isEnabledState = isFocusedState) - } - } -} \ No newline at end of file diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/elements/OutlinedAvatar.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/elements/OutlinedAvatar.kt index 8c13037..f8e940e 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/elements/OutlinedAvatar.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/elements/OutlinedAvatar.kt @@ -17,12 +17,14 @@ package com.morpho.app.ui.elements */ import androidx.compose.foundation.clickable -import androidx.compose.foundation.interaction.MutableInteractionSource -import androidx.compose.foundation.layout.* +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.CircleShape import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.drawBehind @@ -74,8 +76,8 @@ fun OutlinedAvatar( AvatarShape.Rounded -> MaterialTheme.shapes.small AvatarShape.Corner -> roundedTopLBotR.small } - val interactionSource = remember { MutableInteractionSource() } - val indication = remember { MorphoHighlightIndication() } + //val interactionSource = remember { MutableInteractionSource() } + //val indication = remember { MorphoHighlightIndication() } val pxSize = LocalDensity.current.run { (size-outlineSize).toPx()*2 }.toInt() val sB = when(avatarShape) { AvatarShape.Circle -> CircleShape.createOutline( @@ -90,9 +92,7 @@ fun OutlinedAvatar( } val modClicked = if(onClicked != null) { modifier.clickable( - interactionSource = interactionSource, - indication = indication, - enabled = true, + onClick = { onClicked() } ) } else modifier diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/post/EmbedPostFragment.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/post/EmbedPostFragment.kt index 7197dd5..ac26364 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/post/EmbedPostFragment.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/post/EmbedPostFragment.kt @@ -4,13 +4,29 @@ package com.morpho.app.ui.post import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource -import androidx.compose.foundation.layout.* +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.foundation.layout.ExperimentalLayoutApi +import androidx.compose.foundation.layout.FlowRow +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.wrapContentWidth import androidx.compose.foundation.text.selection.SelectionContainer import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.material3.Text -import androidx.compose.runtime.* +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalUriHandler @@ -21,8 +37,11 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.withStyle import androidx.compose.ui.unit.dp import androidx.compose.ui.util.fastForEach -import com.morpho.app.model.bluesky.* -import com.morpho.app.ui.elements.MorphoHighlightIndication +import com.morpho.app.model.bluesky.BskyPost +import com.morpho.app.model.bluesky.BskyPostFeature +import com.morpho.app.model.bluesky.EmbedImage +import com.morpho.app.model.bluesky.EmbedRecord +import com.morpho.app.model.bluesky.FacetType import com.morpho.app.ui.elements.OutlinedAvatar import com.morpho.app.ui.elements.RichTextElement import com.morpho.app.ui.elements.WrappedColumn @@ -47,7 +66,7 @@ fun EmbedPostFragment( var hidePost by rememberSaveable { mutableStateOf(post.author.mutedByMe) } val muted = rememberSaveable { post.author.mutedByMe } val interactionSource = remember { MutableInteractionSource() } - val indication = remember { MorphoHighlightIndication() } + //val indication = remember { MorphoHighlightIndication() } val uriHandler = LocalUriHandler.current WrappedColumn( modifier @@ -63,9 +82,6 @@ fun EmbedPostFragment( .fillMaxWidth() .align(Alignment.End) .clickable( - interactionSource = interactionSource, - indication = indication, - enabled = true, onClick = { onItemClicked(post.uri) } ) @@ -119,9 +135,6 @@ fun EmbedPostFragment( .weight(10.0F) .alignByBaseline() .clickable( - interactionSource = interactionSource, - indication = indication, - enabled = true, onClick = { onProfileClicked(post.author.did) } ), ) diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/post/PostFragment.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/post/PostFragment.kt index 2b04281..1890506 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/post/PostFragment.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/post/PostFragment.kt @@ -4,11 +4,26 @@ package com.morpho.app.ui.post import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource -import androidx.compose.foundation.layout.* +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.foundation.layout.ExperimentalLayoutApi +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.offset +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.wrapContentWidth import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.Reply import androidx.compose.material.icons.filled.Repeat -import androidx.compose.material3.* +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.material3.surfaceColorAtElevation import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Alignment @@ -25,14 +40,29 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.util.fastAny import com.atproto.label.Blurs import com.atproto.repo.StrongRef -import com.morpho.app.model.bluesky.* +import com.morpho.app.model.bluesky.BskyPost +import com.morpho.app.model.bluesky.BskyPostFeature +import com.morpho.app.model.bluesky.BskyPostReason +import com.morpho.app.model.bluesky.EmbedRecord +import com.morpho.app.model.bluesky.FacetType +import com.morpho.app.model.bluesky.TimelinePostMedia import com.morpho.app.ui.common.OnPostClicked -import com.morpho.app.ui.elements.* +import com.morpho.app.ui.elements.AvatarShape +import com.morpho.app.ui.elements.ContentHider +import com.morpho.app.ui.elements.MenuOptions +import com.morpho.app.ui.elements.OutlinedAvatar +import com.morpho.app.ui.elements.RichTextElement +import com.morpho.app.ui.elements.WrappedColumn import com.morpho.app.ui.lists.FeedListEntryFragment import com.morpho.app.ui.lists.UserListEntryFragment import com.morpho.app.util.getFormattedDateTimeSince import com.morpho.app.util.openBrowser -import com.morpho.butterfly.* +import com.morpho.butterfly.AtIdentifier +import com.morpho.butterfly.AtUri +import com.morpho.butterfly.ContentHandling +import com.morpho.butterfly.LabelAction +import com.morpho.butterfly.LabelDescription +import com.morpho.butterfly.LabelIcon import com.morpho.butterfly.model.RecordType import kotlinx.collections.immutable.toImmutableList import morpho.app.ui.utils.indentLevel @@ -81,7 +111,7 @@ fun PostFragment( }} val interactionSource = remember { MutableInteractionSource() } - val indication = remember { MorphoHighlightIndication() } + //val indication = remember { MorphoHighlightIndication() } val bgColor = if (role == PostFragmentRole.PrimaryThreadRoot) { MaterialTheme.colorScheme.background } else { @@ -113,9 +143,6 @@ fun PostFragment( .fillMaxWidth(indentLevel(indent)) .align(Alignment.End) .clickable( - interactionSource = interactionSource, - indication = indication, - enabled = true, onClick = { onItemClicked(post.uri) } ) @@ -212,9 +239,6 @@ fun PostFragment( .alignByBaseline() .pointerHoverIcon(PointerIcon.Hand) .clickable( - interactionSource = interactionSource, - indication = indication, - enabled = true, onClick = { onProfileClicked(post.author.did) } ) ) diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/profile/DetailedProfileFragment.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/profile/DetailedProfileFragment.kt index 5f01233..7dbd70f 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/profile/DetailedProfileFragment.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/profile/DetailedProfileFragment.kt @@ -1,7 +1,6 @@ package com.morpho.app.ui.profile -import androidx.compose.foundation.layout.ExperimentalLayoutApi import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.TopAppBarScrollBehavior @@ -9,13 +8,10 @@ import androidx.compose.material3.rememberTopAppBarState import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import com.morpho.app.model.bluesky.DetailedProfile -import org.jetbrains.compose.resources.ExperimentalResourceApi @OptIn( ExperimentalMaterial3Api::class, - ExperimentalLayoutApi::class, - ExperimentalResourceApi::class ) @Composable expect fun DetailedProfileFragment( diff --git a/Morpho/composeApp/src/desktopMain/kotlin/com/morpho/app/Platform.desktop.kt b/Morpho/composeApp/src/desktopMain/kotlin/com/morpho/app/Platform.desktop.kt index 77fb745..6af8352 100644 --- a/Morpho/composeApp/src/desktopMain/kotlin/com/morpho/app/Platform.desktop.kt +++ b/Morpho/composeApp/src/desktopMain/kotlin/com/morpho/app/Platform.desktop.kt @@ -1,15 +1,7 @@ @file:Suppress("EXPECT_ACTUAL_CLASSIFIERS_ARE_IN_BETA_WARNING") package com.morpho.app -import kotlinx.datetime.LocalDateTime import java.util.Locale -// Note: no need to define CommonParcelize here (bc its @OptionalExpectation) -actual interface CommonParcelable // not used on iOS - -// Note: no need to define CommonTypeParceler> here (bc its @OptionalExpectation) -actual interface CommonParceler // not used on iOS -actual object LocalDateTimeParceler : CommonParceler // not used on iOS - // For Android @Parcelize @Target(AnnotationTarget.TYPE) @Retention(AnnotationRetention.SOURCE) diff --git a/Morpho/composeApp/src/desktopMain/kotlin/com/morpho/app/ui/profile/DetailedProfileFragment.desktop.kt b/Morpho/composeApp/src/desktopMain/kotlin/com/morpho/app/ui/profile/DetailedProfileFragment.desktop.kt index 9c92344..d331774 100644 --- a/Morpho/composeApp/src/desktopMain/kotlin/com/morpho/app/ui/profile/DetailedProfileFragment.desktop.kt +++ b/Morpho/composeApp/src/desktopMain/kotlin/com/morpho/app/ui/profile/DetailedProfileFragment.desktop.kt @@ -4,15 +4,38 @@ import androidx.compose.animation.animateContentSize import androidx.compose.animation.core.Spring import androidx.compose.animation.core.spring import androidx.compose.foundation.background -import androidx.compose.foundation.layout.* +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.WindowInsetsSides +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.only +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.requiredHeight +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.systemBars +import androidx.compose.foundation.layout.widthIn +import androidx.compose.foundation.layout.windowInsetsPadding +import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.text.selection.SelectionContainer import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.ArrowBackIosNew -import androidx.compose.material3.* +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.LargeTopAppBar +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.material3.TopAppBarScrollBehavior import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color @@ -33,14 +56,10 @@ import com.morpho.app.ui.elements.RichTextElement import kotlinx.collections.immutable.toImmutableList import morpho.composeapp.generated.resources.Res import morpho.composeapp.generated.resources.test_banner -import org.jetbrains.compose.resources.ExperimentalResourceApi import org.jetbrains.compose.resources.painterResource -@OptIn( - ExperimentalMaterial3Api::class, - ExperimentalLayoutApi::class, - ExperimentalResourceApi::class -) + +@OptIn(ExperimentalMaterial3Api::class) @Composable actual fun DetailedProfileFragment( profile: DetailedProfile, @@ -57,11 +76,7 @@ actual fun DetailedProfileFragment( } else { (135.dp - (60 * scrollBehavior.state.collapsedFraction).dp) } - val collapsed = scrollBehavior.state.collapsedFraction > 0.5 - LaunchedEffect(scrollState) { - println("Banner Height: $bannerHeight") - print("Collapsed: $collapsed") - } + val collapsed = scrollBehavior.state.collapsedFraction > 0.5f ConstraintLayout( modifier = Modifier @@ -242,25 +257,21 @@ actual fun DetailedProfileFragment( .padding(start = 20.dp, end = 20.dp, top = bannerHeight +40.dp)//.border(1.dp, Color.Yellow) ) { - SelectionContainer { - Text( - text = name, - style = MaterialTheme.typography.headlineMedium, - color = MaterialTheme.colorScheme.onSurface - ) - } - SelectionContainer { - Text( - text = " @${profile.handle}", - color = MaterialTheme.colorScheme.primary, - style = MaterialTheme.typography.labelMedium, - ) - } + Text( + text = name, + style = MaterialTheme.typography.headlineMedium, + color = MaterialTheme.colorScheme.onSurface + ) + + Text( + text = " @${profile.handle}", + color = MaterialTheme.colorScheme.primary, + style = MaterialTheme.typography.labelMedium, + ) Spacer(modifier = Modifier.height(10.dp)) - SelectionContainer { - RichTextElement(profile.description.orEmpty()) - } + RichTextElement(profile.description.orEmpty()) + } } diff --git a/Morpho/gradle/libs.versions.toml b/Morpho/gradle/libs.versions.toml index 374e77d..46b9001 100644 --- a/Morpho/gradle/libs.versions.toml +++ b/Morpho/gradle/libs.versions.toml @@ -3,8 +3,8 @@ agp = "8.2.2" android-compileSdk = "34" android-minSdk = "24" android-targetSdk = "34" -androidx-extIcons = "1.6.8" -androidx-activityCompose = "1.9.1" +androidx-extIcons = "1.7.1" +androidx-activityCompose = "1.9.2" androidx-appcompat = "1.7.0" androidx-constraintlayout = "2.1.4" androidx-core-ktx = "1.13.1" @@ -12,9 +12,9 @@ androidx-espresso-core = "3.6.1" androidx-material = "1.12.0" androidx-test-junit = "1.2.1" appdirs = "1.2.2" -compose = "1.6.8" +compose = "1.6.7" compose-plugin = "1.6.11" -constraintlayoutComposeMultiplatform = "0.3.0-alpha01" +constraintlayoutComposeMultiplatform = "0.4.0" datastorePreferencesCore = "1.1.1" imageLoader = "1.7.8" filekit = "0.8.2" @@ -35,20 +35,20 @@ kotlinx-datetime = "0.6.0" kotlinx-coroutines = "1.8.1" kotlinx-immutable = "0.3.7" kstore = "0.7.1" -ktor = "2.3.9" +ktor = "2.3.12" ktorClientAndroid = "[ktor-version]" logbackClassic = "1.5.7" logbackCore = "1.5.7" logging = "1.4.2" nativeparameterstoreaccess = "0.1.0" okio = "3.9.0" -slf4j-api = "2.0.13" +slf4j-api = "2.0.15" github-kotlin-logging-jvm = "5.1.0" kotlinx-abi-plugin = "0.13.2" window = "1.3.0" material3-android = "1.2.1" accompanist-permissions = "0.32.0" -coil = "3.0.0-alpha06" +coil = "3.0.0-alpha10" voyager = "1.1.0-beta02" kmpalette = "3.1.0" @@ -91,7 +91,7 @@ kmpalette-extensions-file = { module = "com.kmpalette:extensions-file", version. coil = { module = "io.coil-kt.coil3:coil", version.ref = "coil" } coil-compose = { module = "io.coil-kt.coil3:coil-compose", version.ref = "coil" } -coil-network-ktor = { module = "io.coil-kt.coil3:coil-network-ktor", version.ref = "coil" } +coil-network-ktor = { module = "io.coil-kt.coil3:coil-network-ktor2", version.ref = "coil" } kotlinx-coroutines = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinx-coroutines" } kotlinx-coroutines-swing = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-swing", version.ref = "kotlinx-coroutines" } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 61f0758..43622d9 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -3,8 +3,8 @@ agp = "8.2.2" android-compileSdk = "34" android-minSdk = "24" android-targetSdk = "34" -androidx-extIcons = "1.6.8" -androidx-activityCompose = "1.9.1" +androidx-extIcons = "1.7.1" +androidx-activityCompose = "1.9.2" androidx-appcompat = "1.7.0" androidx-constraintlayout = "2.1.4" androidx-core-ktx = "1.13.1" @@ -12,9 +12,9 @@ androidx-espresso-core = "3.6.1" androidx-material = "1.12.0" androidx-test-junit = "1.2.1" appdirs = "1.2.2" -compose = "1.6.8" +compose = "1.6.7" compose-plugin = "1.6.11" -constraintlayoutComposeMultiplatform = "0.3.0-alpha01" +constraintlayoutComposeMultiplatform = "0.4.0" datastorePreferencesCore = "1.1.1" filekit = "0.8.2" imageLoader = "1.7.8" @@ -35,19 +35,19 @@ kotlinx-datetime = "0.6.0" kotlinx-coroutines = "1.8.1" kotlinx-immutable = "0.3.7" kstore = "0.7.1" -ktor = "2.3.9" +ktor = "2.3.12" logbackClassic = "1.5.7" logbackCore = "1.5.7" logging = "1.4.2" nativeparameterstoreaccess = "0.1.0" okio = "3.9.0" -slf4j-api = "2.0.13" +slf4j-api = "2.0.15" github-kotlin-logging-jvm = "5.1.0" kotlinx-abi-plugin = "0.13.2" window = "1.3.0" material3-android = "1.2.1" accompanist-permissions = "0.32.0" -coil = "3.0.0-alpha06" +coil = "3.0.0-alpha10" voyager = "1.1.0-beta02" kmpalette = "3.1.0" @@ -95,7 +95,7 @@ kjwt = { module = "io.github.nefilim.kjwt:kjwt-core", version.ref = "kjwt" } coil = { module = "io.coil-kt.coil3:coil", version.ref = "coil" } coil-compose = { module = "io.coil-kt.coil3:coil-compose", version.ref = "coil" } -coil-network-ktor = { module = "io.coil-kt.coil3:coil-network-ktor", version.ref = "coil" } +coil-network-ktor = { module = "io.coil-kt.coil3:coil-network-ktor2", version.ref = "coil" } kotlinx-coroutines = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinx-coroutines" } kotlinx-coroutines-swing = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-swing", version.ref = "kotlinx-coroutines" } From a62686380bf78cfe4465f41c6d82c07329897d17 Mon Sep 17 00:00:00 2001 From: Orual Date: Tue, 17 Sep 2024 17:48:43 -0400 Subject: [PATCH 15/42] Reverting to a previous commit with some changes, because there were some unforeseen issues with the clean up results. Stuff stopped compiling for no clear reason. WTF, Kotlin. --- Morpho/composeApp/build.gradle.kts | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/Morpho/composeApp/build.gradle.kts b/Morpho/composeApp/build.gradle.kts index 327bf56..56f3608 100644 --- a/Morpho/composeApp/build.gradle.kts +++ b/Morpho/composeApp/build.gradle.kts @@ -1,5 +1,6 @@ import org.jetbrains.compose.desktop.application.dsl.TargetFormat import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi +import org.jetbrains.kotlin.gradle.dsl.JvmTarget plugins { alias(libs.plugins.kotlinMultiplatform) @@ -20,22 +21,13 @@ kotlin { androidTarget { @OptIn(ExperimentalKotlinGradlePluginApi::class) compilerOptions { + jvmTarget = JvmTarget.JVM_11 freeCompilerArgs.addAll( "-P", "plugin:org.jetbrains.kotlin.parcelize:additionalAnnotation=com.morpho.app.CommonParcelize", ) } - compilations.all { - kotlinOptions { - jvmTarget = "11" - } - //move from the deprecated above to this -// compileJavaTaskProvider.configure { -// jvm -// } - - } } @@ -177,7 +169,6 @@ kotlin { implementation(libs.voyager.navigator) // Screen Model implementation(libs.voyager.screenmodel) - implementation("org.jetbrains.androidx.lifecycle:lifecycle-viewmodel-compose:2.8.0") implementation("cafe.adriel.voyager:voyager-lifecycle-kmp:1.1.0-beta02") // BottomSheetNavigator implementation(libs.voyager.bottom.sheet.navigator) @@ -197,7 +188,7 @@ kotlin { api("dev.icerock.moko:parcelize:0.9.0") } - nativeMain.dependencies { + appleMain.dependencies { implementation("app.cash.paging:paging-runtime-uikit:3.3.0-alpha02-0.5.1") } desktopMain.dependencies { From 9fddb22537d636ad57f300e9740de9740e18f5fe Mon Sep 17 00:00:00 2001 From: Orual Date: Tue, 17 Sep 2024 19:07:40 -0400 Subject: [PATCH 16/42] nasty revert --- Butterfly | 1 + Morpho/composeApp/build.gradle.kts | 42 +- .../androidMain/kotlin/Platform.android.kt | 36 ++ .../com/morpho/app/MorphoApplication.kt | 30 +- .../kotlin/com/morpho/app/Platform.android.kt | 15 - .../com/morpho/app/ui/common/BackHandler.kt | 8 + .../DetailedProfileFragment.android.kt | 31 +- .../src/appleMain/kotlin/Platform.apple.kt | 10 + .../kotlin/com/morpho/app/Platform.apple.kt | 10 - .../kotlin/com/morpho/app/Platform.kt | 25 + .../kotlin/com/morpho/app/data/FeedTuner.kt | 126 +++- .../com/morpho/app/data/MorphoDataSource.kt | 1 + .../kotlin/com/morpho/app/di/AppModule.kt | 6 +- .../app/model/bluesky/BskyLabelService.kt | 3 +- .../app/model/bluesky/BskyPostFeature.kt | 7 +- .../app/model/bluesky/MorphoDataItem.kt | 22 +- .../app/model/bluesky/NotificationsSource.kt | 192 ++++++ .../com/morpho/app/model/bluesky/Reference.kt | 13 + .../app/model/uidata/ContentLabelService.kt | 194 ++++++ .../com/morpho/app/model/uidata/MorphoData.kt | 565 ++++++++++++++++++ .../app/model/uistate/ContentCardState.kt | 18 +- .../morpho/app/model/uistate/LoadingState.kt | 21 + .../morpho/app/model/uistate/LoginState.kt | 8 +- .../app/model/uistate/NotificationsState.kt | 3 +- .../morpho/app/model/uistate/SkylineState.kt | 17 + .../com/morpho/app/model/uistate/UiState.kt | 10 +- .../app/screens/base/BaseScreenModel.kt | 8 +- .../morpho/app/screens/login/LoginScreen.kt | 29 +- .../app/screens/main/tabbed/TabbedHomeView.kt | 22 +- .../notifications/NotificationsView.kt | 4 +- .../app/screens/profile/TabbedProfileView.kt | 27 +- .../com/morpho/app/ui/common/PostComposer.kt | 46 +- .../morpho/app/ui/common/SkylineTopAppBar.kt | 23 +- .../app/ui/elements/HighlightIndication.kt | 41 ++ .../morpho/app/ui/elements/OutlinedAvatar.kt | 16 +- .../notifications/NotificationAvatarList.kt | 2 +- .../ui/notifications/NotificationsElement.kt | 2 +- .../morpho/app/ui/post/EmbedPostFragment.kt | 35 +- .../com/morpho/app/ui/post/PostFragment.kt | 48 +- .../app/ui/profile/DetailedProfileFragment.kt | 4 + .../kotlin/com/morpho/app/Platform.desktop.kt | 8 + .../com/morpho/app/ui/common/BackHandler.kt | 7 + .../DetailedProfileFragment.desktop.kt | 73 +-- Morpho/gradle/libs.versions.toml | 16 +- gradle/libs.versions.toml | 16 +- 45 files changed, 1435 insertions(+), 406 deletions(-) create mode 160000 Butterfly create mode 100644 Morpho/composeApp/src/androidMain/kotlin/Platform.android.kt create mode 100644 Morpho/composeApp/src/androidMain/kotlin/com/morpho/app/ui/common/BackHandler.kt create mode 100644 Morpho/composeApp/src/appleMain/kotlin/Platform.apple.kt create mode 100644 Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/NotificationsSource.kt create mode 100644 Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/Reference.kt create mode 100644 Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/ContentLabelService.kt create mode 100644 Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/MorphoData.kt create mode 100644 Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uistate/LoadingState.kt create mode 100644 Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/elements/HighlightIndication.kt create mode 100644 Morpho/composeApp/src/desktopMain/kotlin/com/morpho/app/ui/common/BackHandler.kt diff --git a/Butterfly b/Butterfly new file mode 160000 index 0000000..aa6c8b5 --- /dev/null +++ b/Butterfly @@ -0,0 +1 @@ +Subproject commit aa6c8b51fce31591b80c36600ccf9c4a572395ee diff --git a/Morpho/composeApp/build.gradle.kts b/Morpho/composeApp/build.gradle.kts index 836d4a2..327bf56 100644 --- a/Morpho/composeApp/build.gradle.kts +++ b/Morpho/composeApp/build.gradle.kts @@ -1,7 +1,5 @@ import org.jetbrains.compose.desktop.application.dsl.TargetFormat -import org.jetbrains.kotlin.compose.compiler.gradle.ComposeFeatureFlag import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi -import org.jetbrains.kotlin.gradle.dsl.JvmTarget plugins { alias(libs.plugins.kotlinMultiplatform) @@ -22,24 +20,30 @@ kotlin { androidTarget { @OptIn(ExperimentalKotlinGradlePluginApi::class) compilerOptions { - jvmTarget = JvmTarget.JVM_11 freeCompilerArgs.addAll( "-P", "plugin:org.jetbrains.kotlin.parcelize:additionalAnnotation=com.morpho.app.CommonParcelize", ) } + compilations.all { + kotlinOptions { + jvmTarget = "11" + + } + //move from the deprecated above to this +// compileJavaTaskProvider.configure { +// jvm +// } + + } } jvm("desktop") -// linuxX64() -// mingwX64() -// linuxArm64() - + listOf( iosX64(), iosArm64(), - iosSimulatorArm64() ).forEach { iosTarget -> iosTarget.binaries.framework { @@ -84,10 +88,6 @@ kotlin { implementation("androidx.paging:paging-compose:3.3.0-alpha02") } - commonMain.languageSettings { - progressiveMode = true - } - commonMain.dependencies { implementation("com.morpho:shared") @@ -107,7 +107,6 @@ kotlin { implementation(compose.material) implementation(compose.material3) implementation(compose.ui) - implementation(compose.components.resources) implementation(compose.components.uiToolingPreview) implementation(compose.materialIconsExtended) @@ -178,6 +177,7 @@ kotlin { implementation(libs.voyager.navigator) // Screen Model implementation(libs.voyager.screenmodel) + implementation("org.jetbrains.androidx.lifecycle:lifecycle-viewmodel-compose:2.8.0") implementation("cafe.adriel.voyager:voyager-lifecycle-kmp:1.1.0-beta02") // BottomSheetNavigator implementation(libs.voyager.bottom.sheet.navigator) @@ -197,7 +197,7 @@ kotlin { api("dev.icerock.moko:parcelize:0.9.0") } - appleMain.dependencies { + nativeMain.dependencies { implementation("app.cash.paging:paging-runtime-uikit:3.3.0-alpha02-0.5.1") } desktopMain.dependencies { @@ -272,26 +272,17 @@ android { } buildFeatures { compose = true - //viewBinding = true + viewBinding = true } composeOptions { - kotlinCompilerExtensionVersion = "1.5.15" + kotlinCompilerExtensionVersion = "1.5.11" } task("testClasses") } -composeCompiler { - includeSourceInformation = true - includeTraceMarkers = true - - featureFlags = setOf( - ComposeFeatureFlag.StrongSkipping.disabled(), - ComposeFeatureFlag.OptimizeNonSkippingGroups, - ) -} compose.desktop { application { @@ -315,7 +306,6 @@ compose.desktop { dependencies { add("kspCommonMainMetadata", libs.koin.ksp.compiler) // Run KSP on [commonMain] code - //add("kspJvm", libs.koin.ksp.compiler) add("kspAndroid", libs.koin.ksp.compiler) //add("kspIosX64", libs.koin.ksp.compiler) //add("kspIosArm64", libs.koin.ksp.compiler) diff --git a/Morpho/composeApp/src/androidMain/kotlin/Platform.android.kt b/Morpho/composeApp/src/androidMain/kotlin/Platform.android.kt new file mode 100644 index 0000000..73f97d9 --- /dev/null +++ b/Morpho/composeApp/src/androidMain/kotlin/Platform.android.kt @@ -0,0 +1,36 @@ +@file:Suppress("EXPECT_ACTUAL_CLASSIFIERS_ARE_IN_BETA_WARNING") +package com.morpho.app + +import android.os.Build +import android.os.Parcel +import android.os.Parcelable +import kotlinx.datetime.LocalDateTime +import kotlinx.parcelize.Parceler +import kotlinx.parcelize.Parcelize +import kotlinx.parcelize.RawValue +import kotlinx.parcelize.TypeParceler + +actual typealias CommonParcelize = Parcelize +actual typealias CommonParcelable = Parcelable + +actual typealias CommonRawValue = RawValue +actual typealias CommonParceler = Parceler +actual typealias CommonTypeParceler = TypeParceler +actual object LocalDateTimeParceler : Parceler { + override fun create(parcel: Parcel): LocalDateTime { + val date = parcel.readString() + return date?.let { LocalDateTime.parse(it) } + ?: LocalDateTime(0, 0, 0, 0, 0) + } + + override fun LocalDateTime.write(parcel: Parcel, flags: Int) { + parcel.writeString(this.toString()) + } +} + +class AndroidPlatform : Platform { + override val name: String = "Android ${Build.VERSION.SDK_INT}" +} + +actual fun getPlatform(): Platform = AndroidPlatform() + diff --git a/Morpho/composeApp/src/androidMain/kotlin/com/morpho/app/MorphoApplication.kt b/Morpho/composeApp/src/androidMain/kotlin/com/morpho/app/MorphoApplication.kt index 07922ec..4da1904 100644 --- a/Morpho/composeApp/src/androidMain/kotlin/com/morpho/app/MorphoApplication.kt +++ b/Morpho/composeApp/src/androidMain/kotlin/com/morpho/app/MorphoApplication.kt @@ -1,19 +1,36 @@ package com.morpho.app import android.app.Application +import androidx.lifecycle.AndroidViewModel +import androidx.lifecycle.DefaultLifecycleObserver import com.gu.toolargetool.TooLargeTool import com.morpho.app.data.PreferencesRepository import com.morpho.app.di.appModule import com.morpho.app.di.dataModule import com.morpho.app.di.storageModule +import com.morpho.butterfly.AtIdentifier +import com.morpho.butterfly.Butterfly import com.morpho.butterfly.auth.SessionRepository import com.morpho.butterfly.auth.UserRepository +import org.koin.android.annotation.KoinViewModel +import org.koin.android.ext.android.getKoin +import org.koin.android.ext.koin.androidApplication import org.koin.android.ext.koin.androidContext import org.koin.android.ext.koin.androidLogger +import org.koin.androidx.viewmodel.dsl.viewModel import org.koin.core.context.startKoin import org.koin.core.parameter.parametersOf +import org.koin.dsl.module +@KoinViewModel +class AndroidMainViewModel(app: Application): AndroidViewModel(app), DefaultLifecycleObserver { + val sessionRepository = app.getKoin().get() + val userRepository = app.getKoin().get() + + val api = app.getKoin().get() +} + class MorphoApplication : Application() { override fun onCreate() { TooLargeTool.startLogging(this); @@ -21,13 +38,24 @@ class MorphoApplication : Application() { val koin = startKoin { androidContext(this@MorphoApplication) androidLogger() - modules(appModule, storageModule, dataModule) + modules(androidModule, appModule, storageModule, dataModule) }.koin val sessionRepository = koin.get { parametersOf(cacheDir.path.toString()) } val userRepository = koin.get { parametersOf(cacheDir.path.toString()) } val prefs = koin.get { parametersOf(cacheDir.path.toString()) } + val id: AtIdentifier? = if(sessionRepository.auth?.did != null) { + sessionRepository.auth?.did + } else if (sessionRepository.auth?.handle != null) { + sessionRepository.auth?.handle + } else { + userRepository.firstUser()?.id + } + val api = koin.get { parametersOf(id) } super.onCreate() } } +val androidModule = module { + viewModel { AndroidMainViewModel(androidApplication()) } +} \ No newline at end of file diff --git a/Morpho/composeApp/src/androidMain/kotlin/com/morpho/app/Platform.android.kt b/Morpho/composeApp/src/androidMain/kotlin/com/morpho/app/Platform.android.kt index a98ef43..b1717d1 100644 --- a/Morpho/composeApp/src/androidMain/kotlin/com/morpho/app/Platform.android.kt +++ b/Morpho/composeApp/src/androidMain/kotlin/com/morpho/app/Platform.android.kt @@ -1,22 +1,7 @@ -@file:Suppress("EXPECT_ACTUAL_CLASSIFIERS_ARE_IN_BETA_WARNING") package com.morpho.app - -import android.os.Build -import kotlinx.parcelize.RawValue import java.util.Locale -actual typealias CommonRawValue = RawValue - -class AndroidPlatform : Platform { - override val name: String = "Android ${Build.VERSION.SDK_INT}" -} - -actual fun getPlatform(): Platform = AndroidPlatform() - - - - actual val myLang:String? get() = Locale.getDefault().language diff --git a/Morpho/composeApp/src/androidMain/kotlin/com/morpho/app/ui/common/BackHandler.kt b/Morpho/composeApp/src/androidMain/kotlin/com/morpho/app/ui/common/BackHandler.kt new file mode 100644 index 0000000..ddebe6e --- /dev/null +++ b/Morpho/composeApp/src/androidMain/kotlin/com/morpho/app/ui/common/BackHandler.kt @@ -0,0 +1,8 @@ +package com.morpho.app.ui.common + +import androidx.compose.runtime.Composable + +@Composable +actual fun BackHandler(content: () -> Unit) { + +} \ No newline at end of file diff --git a/Morpho/composeApp/src/androidMain/kotlin/com/morpho/app/ui/profile/DetailedProfileFragment.android.kt b/Morpho/composeApp/src/androidMain/kotlin/com/morpho/app/ui/profile/DetailedProfileFragment.android.kt index ed8623f..30d216c 100644 --- a/Morpho/composeApp/src/androidMain/kotlin/com/morpho/app/ui/profile/DetailedProfileFragment.android.kt +++ b/Morpho/composeApp/src/androidMain/kotlin/com/morpho/app/ui/profile/DetailedProfileFragment.android.kt @@ -4,37 +4,13 @@ import androidx.compose.animation.animateContentSize import androidx.compose.animation.core.Spring import androidx.compose.animation.core.spring import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.WindowInsets -import androidx.compose.foundation.layout.WindowInsetsSides -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.only -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.requiredHeight -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.systemBars -import androidx.compose.foundation.layout.widthIn -import androidx.compose.foundation.layout.windowInsetsPadding -import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.layout.* import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.text.selection.SelectionContainer import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.ArrowBackIosNew -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.IconButtonDefaults -import androidx.compose.material3.LargeTopAppBar -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Surface -import androidx.compose.material3.Text -import androidx.compose.material3.TopAppBarDefaults -import androidx.compose.material3.TopAppBarScrollBehavior +import androidx.compose.material3.* import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -55,10 +31,13 @@ import com.morpho.app.ui.elements.RichTextElement import kotlinx.collections.immutable.toImmutableList import morpho.composeapp.generated.resources.Res import morpho.composeapp.generated.resources.test_banner +import org.jetbrains.compose.resources.ExperimentalResourceApi import org.jetbrains.compose.resources.painterResource @OptIn( ExperimentalMaterial3Api::class, + ExperimentalLayoutApi::class, + ExperimentalResourceApi::class ) @Composable public actual fun DetailedProfileFragment( diff --git a/Morpho/composeApp/src/appleMain/kotlin/Platform.apple.kt b/Morpho/composeApp/src/appleMain/kotlin/Platform.apple.kt new file mode 100644 index 0000000..c42198b --- /dev/null +++ b/Morpho/composeApp/src/appleMain/kotlin/Platform.apple.kt @@ -0,0 +1,10 @@ +@file:Suppress("EXPECT_ACTUAL_CLASSIFIERS_ARE_IN_BETA_WARNING") +package com.morpho.app +import kotlinx.datetime.LocalDateTime + +// Note: no need to define CommonParcelize here (bc its @OptionalExpectation) +actual interface CommonParcelable // not used on iOS + +// Note: no need to define CommonTypeParceler> here (bc its @OptionalExpectation) +actual interface CommonParceler // not used on iOS +actual object LocalDateTimeParceler : CommonParceler // not used on iOS diff --git a/Morpho/composeApp/src/appleMain/kotlin/com/morpho/app/Platform.apple.kt b/Morpho/composeApp/src/appleMain/kotlin/com/morpho/app/Platform.apple.kt index 6fb357c..6309ccc 100644 --- a/Morpho/composeApp/src/appleMain/kotlin/com/morpho/app/Platform.apple.kt +++ b/Morpho/composeApp/src/appleMain/kotlin/com/morpho/app/Platform.apple.kt @@ -1,14 +1,4 @@ -@file:Suppress("EXPECT_ACTUAL_CLASSIFIERS_ARE_IN_BETA_WARNING") package com.morpho.app -import kotlinx.datetime.LocalDateTime - -// Note: no need to define CommonParcelize here (bc its @OptionalExpectation) -actual interface CommonParcelable // not used on iOS - -// Note: no need to define CommonTypeParceler> here (bc its @OptionalExpectation) -actual interface CommonParceler // not used on iOS -actual object LocalDateTimeParceler : CommonParceler // not used on iOS - // For Android @Parcelize @Target(AnnotationTarget.PROPERTY) diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/Platform.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/Platform.kt index a9081af..8f6eb6b 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/Platform.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/Platform.kt @@ -1,6 +1,8 @@ @file:Suppress("EXPECT_ACTUAL_CLASSIFIERS_ARE_IN_BETA_WARNING") package com.morpho.app +import kotlinx.datetime.LocalDateTime + interface Platform { val name: String } @@ -10,9 +12,32 @@ expect fun getPlatform(): Platform expect val myLang:String? expect val myCountry:String? +// For Android @Parcelize +@OptIn(ExperimentalMultiplatform::class) +@OptionalExpectation +@Target(AnnotationTarget.CLASS) +@Retention(AnnotationRetention.BINARY) +expect annotation class CommonParcelize() // For Android @Parcelize @OptIn(ExperimentalMultiplatform::class) @OptionalExpectation @Target(AnnotationTarget.TYPE) expect annotation class CommonRawValue() + +// For Android Parcelable +expect interface CommonParcelable + +// For Android @TypeParceler +@OptIn(ExperimentalMultiplatform::class) +@OptionalExpectation +@Retention(AnnotationRetention.SOURCE) +@Repeatable +@Target(AnnotationTarget.CLASS, AnnotationTarget.PROPERTY) +expect annotation class CommonTypeParceler>() + +// For Android Parceler +expect interface CommonParceler + +// For Android @TypeParceler to convert LocalDateTime to Parcel +expect object LocalDateTimeParceler: CommonParceler \ No newline at end of file diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/data/FeedTuner.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/data/FeedTuner.kt index 6758e07..12720f0 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/data/FeedTuner.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/data/FeedTuner.kt @@ -1,16 +1,11 @@ package com.morpho.app.data -import com.morpho.app.model.bluesky.AuthorContext -import com.morpho.app.model.bluesky.AuthorFilter -import com.morpho.app.model.bluesky.FeedDescriptor -import com.morpho.app.model.bluesky.MorphoDataItem -import com.morpho.app.model.bluesky.Profile -import com.morpho.app.model.bluesky.ThreadPost -import com.morpho.butterfly.AtUri +import com.morpho.app.model.bluesky.* +import com.morpho.app.model.uidata.MorphoData +import com.morpho.app.model.uidata.areSameAuthor +import com.morpho.app.model.uistate.FeedType +import com.morpho.butterfly.* import com.morpho.butterfly.BskyPreferences -import com.morpho.butterfly.Did -import com.morpho.butterfly.Language -import com.morpho.butterfly.PagedResponse import kotlinx.collections.immutable.persistentListOf import kotlinx.serialization.Serializable @@ -75,6 +70,53 @@ data class FeedTuner(val tuners: List useFeedTuners( + prefs: BskyUserPreferences, + feed: MorphoData + ): List> { + if(feed.isProfileFeed) { + when(feed.feedType) { + FeedType.PROFILE_POSTS -> return listOf( + FeedTuner(tuners = persistentListOf( + Companion::removeReplies + )) as FeedTuner + ) + FeedType.PROFILE_USER_LISTS -> return listOf() + FeedType.PROFILE_FEEDS_LIST -> return listOf() + FeedType.PROFILE_MOD_SERVICE -> return listOf() + else -> {} + } + } + val languages = prefs.preferences.languages.toList() + val languageTuner: TunerFunction = { f, t -> + preferredLanguageOnly(languages, f, t) + } + if(feed.feedType == FeedType.OTHER) { + return listOf(FeedTuner(tuners = persistentListOf(languageTuner))) as List> + } + if(feed.feedType == FeedType.LIST_FOLLOWING || feed.feedType == FeedType.HOME) { + val userDid = Did(prefs.user.userDid) + val tuners = mutableListOf(FeedTuner(tuners = persistentListOf(Companion::removeOrphans))) + val feedPrefs = prefs.preferences.feedViewPrefs[feed.uri.atUri] ?: + return tuners.toList() as List> + if(feedPrefs.hideReposts) tuners.add(FeedTuner(tuners = persistentListOf(Companion::removeReposts))) + if(feedPrefs.hideReplies) tuners.add(FeedTuner(tuners = persistentListOf(Companion::removeReplies))) + else { + val followedRepliesOnly: TunerFunction = { f, t -> + followedRepliesOnly(userDid, f, t) + } + tuners.add(FeedTuner(tuners = persistentListOf(followedRepliesOnly))) + } + if(feedPrefs.hideQuotePosts) tuners.add( + FeedTuner(tuners = persistentListOf( + Companion::removeQuotePosts + )) + ) + tuners.add(FeedTuner(tuners = persistentListOf(Companion::dedupThreads))) + return tuners.toList() as List> + } + return listOf() + } fun removeReplies( feed: List, @@ -187,7 +229,55 @@ data class FeedTuner(val tuners: List } } - + fun tune( + feed: MorphoData + ): MorphoData { + var workingFeed = feed.items + tuners.forEach { tuner -> + workingFeed = tuner(workingFeed, this) + } + workingFeed = workingFeed.map { item -> + if(seenKeys.contains(item.key)) return@map null + else if(item is MorphoDataItem.Thread) { + val itemUris = item.getUris() + val seenInThisThread = itemUris.filter { seenUris.contains(it) } + if(seenInThisThread.isNotEmpty()) { + if(seenInThisThread.size == itemUris.size) { + return@map null + } else { + val newParents = item.thread.parents.filter { parent -> + when(parent) { + is ThreadPost.ViewablePost -> parent.post.uri in seenInThisThread + is ThreadPost.BlockedPost -> false + is ThreadPost.NotFoundPost -> false + } + } + val newThread = item.copy(thread = item.thread.filterReplies { reply -> + when(reply) { + is ThreadPost.ViewablePost -> reply.post.uri in seenInThisThread + is ThreadPost.BlockedPost -> false + is ThreadPost.NotFoundPost -> false + } + }.copy(parents = newParents)) + seenUris.addAll(itemUris) + if(newThread.thread.replies.isEmpty() && newThread.thread.parents.isEmpty()) { + return@map null + } else { + return@map newThread + } + } + } else { + seenUris.addAll(itemUris) + item + } + } else { + val disableDedub = item.isReply && item.isRepost + if(!disableDedub) seenKeys.add(item.key) + item + } + }.filterNotNull() as List + return feed.copy(items = workingFeed) + } fun tune( feed: PagedResponse.Feed ): PagedResponse.Feed { @@ -295,18 +385,4 @@ fun shouldDisplayReplyInFollowing( fun isSelfOrFollowing(profile: Profile?, userDid: Did): Boolean { return profile?.did == userDid || profile?.followedByMe == true -} - -fun areSameAuthor(authors: AuthorContext): Boolean { - val authorDid = authors.author.did - if(authors.parentAuthor != null && authors.parentAuthor.did != authorDid) { - return false - } - if(authors.grandParentAuthor != null && authors.grandParentAuthor.did != authorDid) { - return false - } - if(authors.rootAuthor != null && authors.rootAuthor.did != authorDid) { - return false - } - return true } \ No newline at end of file diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/data/MorphoDataSource.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/data/MorphoDataSource.kt index 4489b27..cd9df84 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/data/MorphoDataSource.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/data/MorphoDataSource.kt @@ -5,6 +5,7 @@ import app.cash.paging.PagingConfig import app.cash.paging.PagingSource import app.cash.paging.PagingState import com.morpho.app.model.bluesky.* +import com.morpho.app.model.uidata.ContentLabelService import com.morpho.app.model.uidata.Delta import com.morpho.app.model.uidata.Moment import com.morpho.butterfly.ButterflyAgent diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/di/AppModule.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/di/AppModule.kt index 3588605..5c52247 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/di/AppModule.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/di/AppModule.kt @@ -1,13 +1,9 @@ package com.morpho.app.di -import com.morpho.app.data.ContentLabelService import com.morpho.app.data.MorphoAgent import com.morpho.app.data.PollBlueService import com.morpho.app.data.PreferencesRepository -import com.morpho.app.model.uidata.FeedEvent -import com.morpho.app.model.uidata.FeedPresenter -import com.morpho.app.model.uidata.UserFeedsPresenter -import com.morpho.app.model.uidata.UserListPresenter +import com.morpho.app.model.uidata.* import com.morpho.app.screens.base.BaseScreenModel import com.morpho.app.screens.login.LoginScreenModel import com.morpho.app.screens.main.MainScreenModel diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/BskyLabelService.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/BskyLabelService.kt index 1c6d29f..bab0a28 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/BskyLabelService.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/BskyLabelService.kt @@ -10,7 +10,6 @@ import com.morpho.butterfly.AtUri import com.morpho.butterfly.Cid import com.morpho.butterfly.Did import com.morpho.butterfly.Handle -import dev.icerock.moko.parcelize.Parcelable import dev.icerock.moko.parcelize.Parcelize import dev.icerock.moko.parcelize.TypeParceler import kotlinx.collections.immutable.persistentListOf @@ -31,7 +30,7 @@ open class BskyLabelService( val indexedAt: Moment, val policies: List, val labels: List, -): Parcelable { +) { val did: Did get() = creator?.did ?: Did("did:blank:did") val handle: Handle diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/BskyPostFeature.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/BskyPostFeature.kt index db6c411..ba86165 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/BskyPostFeature.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/BskyPostFeature.kt @@ -98,12 +98,7 @@ data class EmbedImage( val aspectRatio: AspectRatio? = null, ): Parcelable -@Immutable -@Serializable -data class Reference( - val uri: AtUri, - val cid: Cid, -) + @Parcelize @Immutable diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/MorphoDataItem.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/MorphoDataItem.kt index 65e63e0..8c15fb8 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/MorphoDataItem.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/MorphoDataItem.kt @@ -2,6 +2,7 @@ package com.morpho.app.model.bluesky import androidx.compose.runtime.Immutable import app.bsky.feed.* +import com.morpho.app.CommonParcelize import com.morpho.app.util.deserialize import com.morpho.butterfly.AtUri import dev.icerock.moko.parcelize.Parcelable @@ -19,10 +20,12 @@ import kotlinx.serialization.Serializable @Parcelize @Immutable @Serializable +@CommonParcelize sealed interface MorphoDataItem: Parcelable { @Immutable @Serializable + @CommonParcelize sealed interface FeedItem: MorphoDataItem { companion object { fun fromFeedViewPost(feedPost: FeedViewPost): FeedItem { @@ -63,7 +66,7 @@ sealed interface MorphoDataItem: Parcelable { null } is ReplyRefParentUnion.PostView -> { - parent.value + (parent as ReplyRefParentUnion.PostView).value } } items.add(feedPost.post) @@ -249,7 +252,7 @@ sealed interface MorphoDataItem: Parcelable { @Immutable @Serializable - @Parcelize + @CommonParcelize data class Post( val post: BskyPost, val reason: BskyPostReason? = post.reason, @@ -258,7 +261,7 @@ sealed interface MorphoDataItem: Parcelable { @Immutable @Serializable - @Parcelize + @CommonParcelize data class Thread( val thread: BskyPostThread, val reason: BskyPostReason? = null, @@ -276,21 +279,21 @@ sealed interface MorphoDataItem: Parcelable { @Immutable @Serializable - @Parcelize + @CommonParcelize data class FeedInfo( val feed: FeedGenerator, ): MorphoDataItem @Immutable @Serializable - @Parcelize + @CommonParcelize data class ProfileItem( val profile:Profile, ): MorphoDataItem @Immutable @Serializable - @Parcelize + @CommonParcelize data class ListInfo( val list: BskyList, ): MorphoDataItem @@ -298,14 +301,14 @@ sealed interface MorphoDataItem: Parcelable { @Immutable @Serializable - @Parcelize + @CommonParcelize data class ModLabel( val label: BskyLabelDefinition, ): MorphoDataItem @Immutable @Serializable - @Parcelize + @CommonParcelize data class LabelService( val service: BskyLabelService, ): MorphoDataItem @@ -377,7 +380,6 @@ sealed interface MorphoDataItem: Parcelable { } -@Parcelize @Immutable @Serializable data class AuthorContext( @@ -385,4 +387,4 @@ data class AuthorContext( val parentAuthor: Profile? = null, val grandParentAuthor: Profile? = null, val rootAuthor: Profile? = null, -): Parcelable \ No newline at end of file +) \ No newline at end of file diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/NotificationsSource.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/NotificationsSource.kt new file mode 100644 index 0000000..32be980 --- /dev/null +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/NotificationsSource.kt @@ -0,0 +1,192 @@ +package com.morpho.app.model.bluesky + +import app.bsky.notification.ListNotificationsReason +import app.cash.paging.PagingConfig +import app.cash.paging.compose.LazyPagingItems +import com.morpho.app.data.MorphoDataSource +import com.morpho.app.model.uistate.NotificationsFilterState +import com.morpho.butterfly.AtUri +import com.morpho.butterfly.Cursor +import kotlinx.serialization.Serializable +import org.lighthousegames.logging.logging + +class NotificationsSource: MorphoDataSource() { + companion object { + val log = logging() + val defaultConfig = PagingConfig( + pageSize = 20, + prefetchDistance = 20, + initialLoadSize = 50, + enablePlaceholders = false, + ) + } + + override suspend fun load(params: LoadParams): LoadResult { + try { + val limit = params.loadSize + val loadCursor = when(params) { + is LoadParams.Append -> params.key + is LoadParams.Prepend -> Cursor.Empty + is LoadParams.Refresh -> Cursor.Empty + } + return agent.listNotifications(limit.toLong(), loadCursor.value).map { response -> + val newCursor = response.cursor + val items = response.items.map { it.toBskyNotification()} + LoadResult.Page( + data = items, + prevKey = when(params) { + is LoadParams.Append -> loadCursor + is LoadParams.Prepend -> Cursor.Empty + is LoadParams.Refresh -> Cursor.Empty + }, + nextKey = newCursor, + ) + }.onFailure { + return LoadResult.Error(it) + }.getOrDefault(LoadResult.Error(Exception("Load failed"))) + } catch (e: Exception) { + return LoadResult.Error(e) + } + } +} + +fun LazyPagingItems.collectNotifications( + toMark: List = listOf() +) : List { + val seen = mutableListOf() + val workList = mutableListOf() + this.itemSnapshotList.map { notif -> + if (notif == null) return@map NotificationsListItem( + notifications = listOf(), + reason = ListNotificationsReason.PLACEHOLDER, + isRead = false, + reasonSubject = null, + ) + if(notif.reasonSubject != null && seen.contains(notif.reasonSubject)) { + val index = workList.indexOfFirst { + it.reasonSubject == notif.reasonSubject + } + if (index >= 0 && notif.reason == workList[index].reason) { + workList[index].notifications.add(notif) + workList[index].isRead = if (notif.isRead) true else workList[index].isRead + } else { + workList.add( + MutableNotificationsListItem( + notifications = mutableListOf(notif), + reason = notif.reason, + isRead = notif.isRead, + reasonSubject = notif.reasonSubject + ) + ) + } + } else if (notif.reasonSubject != null) { + seen.add(notif.reasonSubject!!) + workList.add( + MutableNotificationsListItem( + notifications = mutableListOf(notif), + reason = notif.reason, + isRead = notif.isRead, + reasonSubject = notif.reasonSubject + ) + ) + } else { + val index = workList.indexOfFirst { item-> + item.reason == notif.reason + } + if (index >= 0) { + workList[index].notifications.add(notif) + workList[index].isRead = if (notif.isRead) true else workList[index].isRead + } else { + workList.add( + MutableNotificationsListItem( + notifications = mutableListOf(notif), + reason = notif.reason, + isRead = notif.isRead, + reasonSubject = notif.reasonSubject + ) + ) + } + } + } + return workList.map { it.toImmutable() } +} + +fun List.filterNotifications( + filter: NotificationsFilterState, +): List { + return this.filter { + (if(it.isRead) filter.showAlreadyRead else true) && + when(it.reason) { + ListNotificationsReason.LIKE -> filter.showLikes + ListNotificationsReason.REPOST -> filter.showReposts + ListNotificationsReason.FOLLOW -> filter.showFollows + ListNotificationsReason.MENTION -> filter.showMentions + ListNotificationsReason.REPLY -> filter.showReplies + ListNotificationsReason.QUOTE -> filter.showQuotes + else -> true + } + }.toList() +} + +@Serializable +data class MutableNotificationsListItem( + val notifications: MutableList = mutableListOf(), + val reason: ListNotificationsReason, + var isRead: Boolean = false, + val reasonSubject: AtUri? = null, +) { + companion object { + fun fromImmutable(item: NotificationsListItem): MutableNotificationsListItem { + return MutableNotificationsListItem( + notifications = item.notifications.distinctBy { it.author.did }.toMutableList(), + reason = item.reason, + isRead = item.isRead, + reasonSubject = item.reasonSubject + ) + } + } + fun toImmutable(): NotificationsListItem { + return NotificationsListItem( + notifications = notifications.distinctBy { it.author.did }, + reason = reason, + isRead = isRead, + reasonSubject = reasonSubject + ) + } +} + +@Serializable +data class NotificationsListItem( + val notifications: List, + val reason: ListNotificationsReason, + val isRead: Boolean, + val reasonSubject: AtUri?, +) { + companion object { + fun fromMutable(item: MutableNotificationsListItem) { + NotificationsListItem( + notifications = item.notifications.distinctBy { it.author.did }, + reason = item.reason, + isRead = item.isRead, + reasonSubject = item.reasonSubject + ) + } + } + + override fun hashCode(): Int { + return notifications.hashCode() + reason.hashCode() + reasonSubject.hashCode() + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other == null || this::class != other::class) return false + + other as NotificationsListItem + + if (reason != other.reason) return false + if (reasonSubject != other.reasonSubject) return false + if (notifications != other.notifications) return false + + return true + } +} \ No newline at end of file diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/Reference.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/Reference.kt new file mode 100644 index 0000000..b11cac8 --- /dev/null +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/Reference.kt @@ -0,0 +1,13 @@ +package com.morpho.app.model.bluesky + +import androidx.compose.runtime.Immutable +import com.morpho.butterfly.AtUri +import com.morpho.butterfly.Cid +import kotlinx.serialization.Serializable + +@Immutable +@Serializable +data class Reference( + val uri: AtUri, + val cid: Cid, +) \ No newline at end of file diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/ContentLabelService.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/ContentLabelService.kt new file mode 100644 index 0000000..b6bbf0c --- /dev/null +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/ContentLabelService.kt @@ -0,0 +1,194 @@ +package com.morpho.app.model.uidata + +import app.bsky.actor.MuteTargetGroup +import app.bsky.actor.MutedWord +import app.bsky.actor.Visibility +import app.bsky.labeler.LabelerViewDetailed +import com.atproto.label.Blurs +import com.atproto.label.Severity +import com.morpho.app.data.MorphoAgent +import com.morpho.app.model.bluesky.BskyPost +import com.morpho.app.model.bluesky.MorphoDataItem +import com.morpho.app.model.bluesky.toAtProtoLabel +import com.morpho.app.model.bluesky.toListVewBasic +import com.morpho.butterfly.* +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.launch +import org.koin.core.component.KoinComponent +import org.koin.core.component.inject +import org.lighthousegames.logging.logging + +class ContentLabelService: KoinComponent { + val agent: MorphoAgent by inject() + val serviceScope = CoroutineScope(SupervisorJob() + Dispatchers.Default) + companion object { + val log = logging("ContentLabelService") + } + + val modPrefs: ModerationPreferences + get() = agent.prefs.modPrefs + + val hiddenPosts: List + get() = modPrefs.hiddenPosts + + val mutedWords: List + get() = modPrefs.mutedWords + + + val labelers: Map> + get() = modPrefs.labelers + + val labels: Map + get() = modPrefs.labels + + var labelDefinitions: Map> = emptyMap() + private set + + var labelerDetails: Map = emptyMap() + private set + + init { + serviceScope.launch { + agent.getLabelDefinitions(modPrefs) + agent.getLabelersDetailed(labelers.keys.map { Did(it) }) + } + + } + + fun shouldHideItem(item: MorphoDataItem.FeedItem): Boolean { + return when (item) { + is MorphoDataItem.Post -> { + item.post.author.mutedByMe + || item.post.author.blocking + || item.post.author.blockedBy + || hiddenPosts.any { uri -> item.containsUri(uri) } + || mutedWords.any { + item.post.text.contains(it.value, ignoreCase = true) + } || if(!modPrefs.adultContentEnabled) { + val adultLabels = item.post.labels.filter { label -> + labelDefinitions[label.creator.did]?.get(label.value)?.flags + ?.contains(LabelValueDefFlag.Adult) == true + } + adultLabels.isNotEmpty() + } else { + item.post.labels.any { label -> + labels[label.value] == Visibility.HIDE + } + } + } + is MorphoDataItem.Thread -> { + item.thread.anyMutedOrBlocked() + || hiddenPosts.any { uri -> item.containsUri(uri) } + || mutedWords.any { + item.thread.containsWord(it.value) + } || if(!modPrefs.adultContentEnabled) { + val adultLabels = item.thread.getLabels().filter { label -> + labelDefinitions[label.creator.did]?.get(label.value)?.flags + ?.contains(LabelValueDefFlag.Adult) == true + } + adultLabels.isNotEmpty() + } else { + item.thread.getLabels().any { label -> + labels[label.value] == Visibility.HIDE + } + } + } + } + } + + fun getContentHandlingForPost(post: BskyPost): List> { + val result = mutableListOf>() + val postLabels = post.labels + + if(post.author.mutedByMe) { + result.add(ContentHandling( + scope = Blurs.CONTENT, + action = LabelAction.Blur, + source = LabelDescription.YouMuted, + id = "muted", + icon = LabelIcon.EyeSlash(labelerAvatar = null), + ) to LabelCause.Muted(LabelSource.User, false)) + } + if(post.author.mutedByList != null) { + val list = post.author.mutedByList!! + result.add(ContentHandling( + scope = Blurs.CONTENT, + action = LabelAction.Blur, + source = LabelDescription.MuteList( + list.name, + list.uri, + ), + id = "muted-word", + icon = LabelIcon.EyeSlash( labelerAvatar = list.avatar), + ) to LabelCause.Muted(LabelSource.List(list.toListVewBasic()), false)) + } + val anyMutedWords = mutedWords.filter { post.text.contains(it.value, ignoreCase = true) } + if(anyMutedWords.isNotEmpty()) anyMutedWords.forEach { word -> + if(!word.targets.contains(MutedWordTarget("content"))) return@forEach + if(word.actorTarget == MuteTargetGroup.EXCLUDE_FOLLOWING && post.author.followedByMe) return@forEach + result.add(ContentHandling( + scope = Blurs.CONTENT, + action = LabelAction.Blur, + source = LabelDescription.MutedWord(word.value), + id = "muted-word", + icon = LabelIcon.EyeSlash(), + ) to LabelCause.MutedWord(LabelSource.User, false)) + } + + + if (postLabels.isNotEmpty()) { + log.verbose { "Post ${post.uri} has labels: ${postLabels.joinToString { it.value }}" } + // Adult content hiding if someone doesn't have it enabled is handled earlier, + // before rendering starts, as is Visibility.HIDE + // so we don't need to worry about it here + val relevantLabels = labels.filter { prefLabel -> + (prefLabel.value == Visibility.WARN || prefLabel.value == Visibility.HIDE) + && postLabels.any { it.value == it.value } }.toList() + .sortedBy { it.second.ordering } + val filteredPostLabels = postLabels.filter { label -> + relevantLabels.any { label.value == it.first } + } + + val possibleCauses = filteredPostLabels.mapNotNull { label -> + labelDefinitions[label.creator.did]?.get(label.value)?.let { labelDef -> + val localizedDefString = labelDef.allDescriptions.firstOrNull { + it.lang == agent.myLanguage + } ?: labelDef.allDescriptions.firstOrNull { it.lang.tag == "en" } + val localLabelDef = labelDef.copy( + localizedName = localizedDefString?.name ?: labelDef.localizedName, + localizedDescription = localizedDefString?.description + ?: labelDef.localizedDescription, + ) + + LabelCause.Label( + LabelSource.Labeler(labelerDetails[label.creator.did]!!), + label.toAtProtoLabel(), + localLabelDef, + localLabelDef.whatToHide, + labels[label.value] ?: labelDef.defaultSetting ?: Visibility.IGNORE, + localLabelDef.behaviours.content, + noOverride = !localLabelDef.configurable, + priority = when (localLabelDef.severity) { + Severity.INFORM -> 5 + Severity.ALERT -> 1 + Severity.NONE -> 8 + }, + downgraded = false, + ) to localLabelDef.toContentHandling( + LabelTarget.Content, + avatar = labelerDetails[label.creator.did]?.creator?.avatar + ) + } + }.sortedBy{ it.first.priority } + possibleCauses.forEach { (cause, handling) -> + result.add(handling to cause) + } + } + + log.verbose { "Post ${post.uri} has handling: \n$result" } + return result.toList() + } + +} \ No newline at end of file diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/MorphoData.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/MorphoData.kt new file mode 100644 index 0000000..f3bdbcc --- /dev/null +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/MorphoData.kt @@ -0,0 +1,565 @@ +package com.morpho.app.model.uidata + +//import com.rickclephas.kmp.nativecoroutines.NativeCoroutines +import androidx.compose.runtime.Immutable +import androidx.compose.ui.util.* +import app.bsky.feed.FeedViewPost +import com.morpho.app.data.FeedTuner +import com.morpho.app.model.bluesky.* +import com.morpho.app.model.uistate.FeedType +import com.morpho.butterfly.* +import dev.icerock.moko.parcelize.Parcelable +import dev.icerock.moko.parcelize.Parcelize +import kotlinx.collections.immutable.toPersistentList +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.single +import kotlinx.datetime.Instant +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.JsonObject +import kotlin.time.Duration + + +typealias TunerFunction = (List, FeedTuner) -> List + +@Parcelize +@Immutable +@Serializable +data class AtCursor(val cursor: String?, val scroll: Int): Parcelable { + companion object { + val EMPTY: AtCursor = AtCursor(null, 0) + } +} + + +@Immutable +@Serializable +data class MorphoData( + val title: String = "Home", + val uri: AtUri = AtUri.HOME_URI, + val cursor: AtCursor = AtCursor.EMPTY, + val items: List = listOf(), + //@TypeParceler() + val query: JsonElement = JsonObject(emptyMap()), +) { + companion object { + + fun EMPTY(): MorphoData { + return MorphoData( + title = "Home", + uri = AtUri.HOME_URI, + cursor = AtCursor.EMPTY, + items = listOf(), + query = JsonObject(emptyMap()), + ) + } + + fun fromList( + title: String = "Home", + uri: AtUri = AtUri.HOME_URI, + cursor: AtCursor = AtCursor.EMPTY, + items: List, + query: JsonElement = JsonObject(emptyMap()), + ): MorphoData { + return MorphoData( + title = title, + uri = uri, + cursor = cursor, + items = items, + query = query, + ) + } + + fun fromFeed( + feedPosts: List, + cursor: AtCursor = AtCursor.EMPTY, + title: String = "Home", + uri: AtUri = AtUri.HOME_URI, + query: JsonElement = JsonObject(emptyMap()), + ): MorphoData { + val items = feedPosts.map { item -> + MorphoDataItem.FeedItem.fromFeedViewPost(item) + } + return MorphoData( + title = title, + uri = uri, + cursor = cursor, + items = items, + query = query, + ) + } + + fun concatFeed( + query: JsonElement, + responseCursor: String?, + oldCursor: AtCursor, + feed: List, + data: MorphoData, + uri: AtUri = data.uri, + title: String = data.title, + api: Butterfly? = null, + ): Flow> = flow { + val newItems = fromFeed( + feed.toList(), AtCursor(responseCursor, 0), + uri = uri, title = title, query = query).collectThreads().single() + emit(if (oldCursor != AtCursor.EMPTY && data.items.isNotEmpty()) { + val newScroll = maxOf(data.items.size, oldCursor.scroll) + concat(data, newItems, AtCursor(responseCursor, newScroll), query = query) + } else if (oldCursor == AtCursor.EMPTY && data.items.isNotEmpty()) { + concat(newItems, data,AtCursor(responseCursor, 0), query = query) + } else { + newItems + }) + } + + fun concatNonThreadedFeed( + query: JsonElement, + responseCursor: String?, + oldCursor: AtCursor, + feed: List, + data: MorphoData, + uri: AtUri = data.uri, + title: String = data.title, + ): MorphoData { + val newItems = fromFeed( + feed.toList(), AtCursor(responseCursor, 0), + uri = uri, title = title, query = query) + return if (oldCursor != AtCursor.EMPTY && data.items.isNotEmpty()) { + val newScroll = if(oldCursor.scroll == 0) 0 else maxOf(data.items.size, oldCursor.scroll) + concat(data, newItems, AtCursor(responseCursor, newScroll), query = query) + } else if (oldCursor == AtCursor.EMPTY && data.items.isNotEmpty()) { + concat(newItems, data,AtCursor(responseCursor, 0), query = query) + } else { + newItems + } + } + + + fun concat( + first: MorphoData, + last: MorphoData, + cursor: AtCursor = last.cursor, + query: JsonElement = JsonObject(emptyMap()), + ): MorphoData { + return first.copy( + items = (first.items + last.items).toPersistentList(), +// .sortedByDescending { +// when (it) { +// is MorphoDataItem.Post -> it.post.createdAt +// is MorphoDataItem.Thread -> it.thread.post.createdAt +// is MorphoDataItem.FeedInfo -> it.feed.indexedAt +// is MorphoDataItem.ListInfo -> it.list.indexedAt +// is MorphoDataItem.ModLabel -> Moment(Instant.DISTANT_PAST) +// is MorphoDataItem.ProfileItem -> Moment(Instant.DISTANT_PAST) +// is MorphoDataItem.LabelService -> it.service.indexedAt +// else -> { +// Moment(Instant.DISTANT_PAST) +// } +// } +// }.toList(), + cursor = cursor, title = first.title, uri = first.uri + ) + } + + fun concat( + first: MorphoData, + last: List, + cursor: AtCursor = first.cursor, + query: JsonElement = JsonObject(emptyMap()), + ): MorphoData { + return first.copy( + items = (first.items + last), + cursor = cursor, title = first.title, uri = first.uri + ) + } + + fun concat( + first: List, + last: MorphoData, + cursor: AtCursor = last.cursor, + ): MorphoData { + return last.copy( + items = (first + last.items), + cursor = cursor, title = last.title, uri = last.uri + ) + } + + fun concat( + posts: List, + feed: MorphoData, + cursor: AtCursor = feed.cursor, + query: JsonElement = JsonObject(emptyMap()), + ): MorphoData { + val new = fromFeed( + feedPosts = posts, + cursor, + feed.title, + feed.uri, + query = feed.query, + ) + return concat(new, feed, cursor, query) + } + + fun fromFeedGenList( + title: String, + uri: AtUri, + feeds: List, + cursor: AtCursor = AtCursor.EMPTY, + ): MorphoData { + return MorphoData( + title = title, + uri = uri, + cursor = cursor, + items = feeds.map { MorphoDataItem.FeedInfo(it) }.toMutableList(), + ) + } + + fun fromProfileList( + title: String, + uri: AtUri, + list: List, + cursor: AtCursor = AtCursor.EMPTY, + ): MorphoData { + return MorphoData( + title = title, + uri = uri, + cursor = cursor, + items = list.map { MorphoDataItem.ProfileItem(it) }.toMutableList(), + ) + } + + fun fromBskyList( + title: String, + uri: AtUri, + lists: List, + cursor: AtCursor = AtCursor.EMPTY, + ): MorphoData { + return MorphoData( + title = title, + uri = uri, + cursor = cursor, + items = lists.map { MorphoDataItem.ListInfo(it) }.toMutableList(), + ) + } + + fun fromModLabelDefs( + title: String, + uri: AtUri, + labels: List, + cursor: AtCursor = AtCursor.EMPTY, + ): MorphoData { + return MorphoData( + title = title, + uri = uri, + cursor = cursor, + items = labels.map { MorphoDataItem.ModLabel(it) }.toMutableList(), + ) + } + + fun fromModServiceDefs( + title: String, + uri: AtUri, + services: List, + cursor: AtCursor = AtCursor.EMPTY, + ): MorphoData { + return MorphoData( + title = title, + uri = uri, + cursor = cursor, + items = services.map { MorphoDataItem.LabelService(it) }.toMutableList(), + ) + } + + } + + val isHome: Boolean + get() = uri == AtUri.HOME_URI + + val isProfileFeed: Boolean + get() = uri.atUri.matches(AtUri.ProfilePostsUriRegex) || + uri.atUri.matches(AtUri.ProfileRepliesUriRegex) || + uri.atUri.matches(AtUri.ProfileMediaUriRegex) || + uri.atUri.matches(AtUri.ProfileLikesUriRegex) || + uri.atUri.matches(AtUri.ProfileUserListsUriRegex) || + uri.atUri.matches(AtUri.ProfileModServiceUriRegex) || + uri.atUri.matches(AtUri.ProfileFeedsListUriRegex) + + + val isMyProfile: Boolean + get() = (isProfileFeed && uri.atUri.contains("me")) || (uri == AtUri.MY_PROFILE_URI) + + val feedType: FeedType + get() = when { + isHome -> FeedType.HOME + uri.atUri.matches(AtUri.ProfilePostsUriRegex) -> FeedType.PROFILE_POSTS + uri.atUri.matches(AtUri.ProfileRepliesUriRegex) -> FeedType.PROFILE_REPLIES + uri.atUri.matches(AtUri.ProfileMediaUriRegex) -> FeedType.PROFILE_MEDIA + uri.atUri.matches(AtUri.ProfileLikesUriRegex) -> FeedType.PROFILE_LIKES + uri.atUri.matches(AtUri.ProfileUserListsUriRegex) -> FeedType.PROFILE_USER_LISTS + uri.atUri.matches(AtUri.ProfileModServiceUriRegex) -> FeedType.PROFILE_MOD_SERVICE + uri.atUri.matches(AtUri.ProfileFeedsListUriRegex) -> FeedType.PROFILE_FEEDS_LIST + uri.atUri.matches(AtUri.ListFeedUriRegex) -> FeedType.LIST_FOLLOWING + else -> FeedType.OTHER + } + + operator fun contains(cid: Cid): Boolean { + return items.fastAny { + when(it) { + is MorphoDataItem.Post -> it.post.cid == cid + is MorphoDataItem.Thread -> it.thread.contains(cid) + is MorphoDataItem.FeedInfo -> it.feed.cid == cid + is MorphoDataItem.ListInfo -> it.list.cid == cid + is MorphoDataItem.ModLabel -> false + is MorphoDataItem.ProfileItem -> false + is MorphoDataItem.LabelService -> it.service.cid == cid + else -> {false} + } + } + } + + fun collectThreads( + depth: Int = 3, height: Int = 80, + timeRange: Delta = Delta(Duration.parse("4h")), + repliesBumpThreads: Boolean = !isProfileFeed, + api: Butterfly? = null, // allows to just use local data + ): Flow> = flow { + val threads = mutableListOf() + val replies = mutableListOf() + val posts = mutableListOf() + val threadCandidates = mutableListOf() + items.fastForEach { item -> + when(item) { + is MorphoDataItem.Post -> { + if (item.isReply) replies.add(item) + else if (item.isOrphan) posts.add(item) + else posts.add(item) + } + is MorphoDataItem.Thread -> { + if (!item.isIncompleteThread) threads.add(item) + else threadCandidates.add(item) + } + else -> return@fastForEach + } + } + replies.fastForEachIndexed { index, reply -> + if (reply == null) return@fastForEachIndexed + if (reply.isOrphan) { + val parent = reply.post.reply?.parentPost + ?: reply.post.reply?.replyRef?.parent?.uri?.let { + if (api != null) { + null // stubbed out before removing + //getPost(it, api).firstOrNull() + } else null + } + val root = reply.post.reply?.rootPost + ?: reply.post.reply?.replyRef?.root?.uri?.let { + if (api != null) { + null // stubbed out before removing + //getPost(it, api).firstOrNull() + } else null + } + replies[index] = MorphoDataItem.Post( + reply.post.copy(reply = reply.post.reply?.copy(parentPost = parent, rootPost = root)), + reply.reason, + isOrphan = root != null && parent != null, + ) + } + val newReply = replies[index] ?: return@fastForEachIndexed // Update in case we changed it above + val replyRef = newReply.post.reply?.replyRef ?: return@fastForEachIndexed + val parent = replyRef.parent.uri + val root = replyRef.root.uri + val inThread = threads.indexOfFirst { it?.containsUri(parent) ?: false || it?.containsUri(root) ?: false } + if (inThread != -1) { + val thread = threads.getOrNull(inThread) ?: return@fastForEachIndexed + threads[inThread] = thread.addReply(newReply.post) + replies[index] = null + } + val inCandidates = threadCandidates.indexOfFirst { it?.containsUri(parent) ?: false || it?.containsUri(root) ?: false } + if (inCandidates != -1) { + val thread = threadCandidates.getOrNull(inCandidates) ?: return@fastForEachIndexed + threadCandidates[inCandidates] = thread.addReply(newReply.post) + replies[index] = null + } + + } + threadCandidates.fastForEachIndexed { index, thread -> + if (thread == null) return@fastForEachIndexed + val rootInThreads = threads.indexOfFirst { t -> t?.containsUri(thread.rootUri) ?: false } + if (rootInThreads == - 1) { + val threadToSplice = threads.getOrNull(rootInThreads) ?: return@fastForEachIndexed + if( + thread.thread.parents.firstOrNull() is ThreadPost.ViewablePost + && threadToSplice.thread.parents.firstOrNull() is ThreadPost.ViewablePost + && thread.rootUri == threadToSplice.rootUri + ) { + if(thread.thread.parents.size == 1 && threadToSplice.thread.parents.size == 1) { + // Both threads have the same, viewable root post and are only one level deep in terms of parents + val newEntry = thread.thread.parents.first() as ThreadPost.ViewablePost + val oldEntry = threadToSplice.thread.parents.first() as ThreadPost.ViewablePost + + val newReplies = (newEntry.replies + oldEntry.replies).distinctBy { it.uri }.toMutableList() + newReplies.add(ThreadPost.ViewablePost(thread.thread.post, null, thread.thread.replies)) + if( thread.getUri() != threadToSplice.getUri() ) + newReplies.add(ThreadPost.ViewablePost(threadToSplice.thread.post, null, threadToSplice.thread.replies)) + val newThread = BskyPostThread( + post = newEntry.post, + parents = listOf(), + replies = newReplies.distinctBy { it.uri }, + ) + threads[rootInThreads] = threadToSplice.copy(thread = newThread, isIncompleteThread = false) + threadCandidates[index] = null + } else if(thread.thread.parents.size == 2 && threadToSplice.thread.parents.size == 2) { + // Both threads have the same, viewable root post and parent chains are both length 2 + val newEntry = thread.thread.parents.first() as ThreadPost.ViewablePost + + val newReplies = mutableListOf() + if(thread.thread.parents.lastOrNull() !is ThreadPost.ViewablePost) return@fastForEachIndexed + if(threadToSplice.thread.parents.lastOrNull() !is ThreadPost.ViewablePost) return@fastForEachIndexed + val newParent = thread.thread.parents.last() as ThreadPost.ViewablePost + val oldParent = threadToSplice.thread.parents.last() as ThreadPost.ViewablePost + val newReply = ThreadPost.ViewablePost(thread.thread.post, null, thread.thread.replies) + val oldReply = ThreadPost.ViewablePost(threadToSplice.thread.post, null, threadToSplice.thread.replies) + newParent.addReply(newReply) + oldParent.addReply(oldReply) + newReplies.add(newReply) + newReplies.add(oldReply) + val newThread = BskyPostThread( + post = newEntry.post, + parents = listOf(newParent), + replies = newReplies.distinctBy { it.uri }, + ) + threads[rootInThreads] = threadToSplice.copy(thread = newThread, isIncompleteThread = false) + threadCandidates[index] = null + } + + } + } else { + val inThreads = threads.indexOfFirst { t -> t?.containsUri(thread.thread.post.uri) ?: false } + if (inThreads == - 1) { + val threadToSplice = threads.getOrNull(index) ?: return@fastForEachIndexed + threads[index] = threadToSplice.addReply(ThreadPost.ViewablePost(thread.thread.post, null, thread.thread.replies)) + threadCandidates[index] = null + } + } + } + threadCandidates.fastFilterNotNull() + if (threadCandidates.isNotEmpty()) threads.addAll(threadCandidates) + val newReplies = replies.filterNotNull() + .distinctBy { it.getUri() } + .filterNot { reply -> + if(reply.isRepost) return@filterNot false + if(reply.isQuotePost) return@filterNot false + reply.getUris().any { uri -> threads.any { it?.containsUri(uri) ?: false } } + }.sortedByDescending { when(it.reason) { + is BskyPostReason.BskyPostRepost -> it.reason.indexedAt + else -> it.post.createdAt + } }.iterator() + var newPosts = posts.toList().filterNotNull() + newPosts = newPosts.distinctBy { it.getUri() } + newPosts = newPosts.filterNot { post -> + if(post.isRepost) return@filterNot false + if(post.isQuotePost) return@filterNot false + post.getUris().any { uri -> threads.any { it?.containsUri(uri) ?: false } } + }.sortedByDescending { when(it.reason) { + is BskyPostReason.BskyPostRepost -> it.reason.indexedAt + else -> it.post.createdAt + } } + val newPostsIter = newPosts.iterator() + var newThreads = threads.toList().filterNotNull() + newThreads = newThreads.sortedByDescending { if(!repliesBumpThreads) { + it.rootAccessiblePost.createdAt + } else { + maxOf(it.thread.post.createdAt, + it.thread.replies.fold(it.thread.post.createdAt) { acc, post -> + val postTime = when(post) { + is ThreadPost.ViewablePost -> post.post.createdAt + is ThreadPost.BlockedPost -> Moment(Instant.DISTANT_PAST) + is ThreadPost.NotFoundPost -> Moment(Instant.DISTANT_PAST) + } + maxOf(acc, postTime) + }) + } } + newThreads = newThreads.distinctBy { it.getUri() } + .filterNot { thread -> + thread.getUris().filterNot { uri -> + newThreads.fastAny { it.getUri() == uri } }.size > 1 + } + val newThreadsIter = newThreads.iterator() + val newFeed = mutableListOf() + while(newPostsIter.hasNext() || newThreadsIter.hasNext() || newReplies.hasNext() ) { + if(newPostsIter.hasNext()) newFeed.add(newPostsIter.next()) + if(newThreadsIter.hasNext()) newFeed.add(newThreadsIter.next()) + if(newReplies.hasNext()) newFeed.add(newReplies.next()) + } + val dedupedFeed = newFeed.distinctBy { it.getUri() } + //println("New feed:\n${newFeed.joinToString("\n")}") + val sortedFeed = dedupedFeed.sortedByDescending { + when(it) { + is MorphoDataItem.Post -> when(it.reason) { + is BskyPostReason.BskyPostFeedPost -> it.post.createdAt + is BskyPostReason.BskyPostRepost -> it.reason.indexedAt + is BskyPostReason.SourceFeed -> it.post.createdAt + null -> it.post.createdAt + } + is MorphoDataItem.Thread -> if(!repliesBumpThreads) { + it.rootAccessiblePost.createdAt + } else { + maxOf(it.thread.post.createdAt, + it.thread.replies.fold(it.thread.post.createdAt) { acc, post -> + val postTime = when(post) { + is ThreadPost.ViewablePost -> post.post.createdAt + is ThreadPost.BlockedPost -> Moment(Instant.DISTANT_PAST) + is ThreadPost.NotFoundPost -> Moment(Instant.DISTANT_PAST) + } + maxOf(acc, postTime) + }) + } + } + } + //println("sorted feed:\n${sortedFeed.joinToString("\n")}") + @Suppress("UNCHECKED_CAST") val newData = copy( items = sortedFeed as List) + emit(newData) + }.flowOn(Dispatchers.Default) + + fun dedup(): MorphoData { + val newList = items.fastDistinctBy { when(it) { + is MorphoDataItem.FeedItem -> it.key + is MorphoDataItem.Post -> it.key + is MorphoDataItem.Thread -> it.key + is MorphoDataItem.ListInfo -> it.list.uri + is MorphoDataItem.ModLabel -> it.label.identifier + is MorphoDataItem.ProfileItem -> it.profile.did + is MorphoDataItem.LabelService -> it.service.uri + else -> {it.hashCode()} + } } + return this.copy(items = newList) + } + + +} + + +fun AtUri.id(api:Butterfly): AtIdentifier { + val idString = atUri.substringAfter("at://").split("/")[0] + return if (idString == "me") api.atpUser!!.id else { + // TODO: make this resolve a handle to a DID + if (idString.contains("did:")) Did(idString) else Handle(idString) + } +} + +fun areSameAuthor(authors: AuthorContext): Boolean { + val authorDid = authors.author.did + if(authors.parentAuthor != null && authors.parentAuthor.did != authorDid) { + return false + } + if(authors.grandParentAuthor != null && authors.grandParentAuthor.did != authorDid) { + return false + } + if(authors.rootAuthor != null && authors.rootAuthor.did != authorDid) { + return false + } + return true +} diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uistate/ContentCardState.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uistate/ContentCardState.kt index 20d2f6d..5268882 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uistate/ContentCardState.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uistate/ContentCardState.kt @@ -1,6 +1,5 @@ package com.morpho.app.model.uistate -import androidx.compose.runtime.Immutable import com.morpho.app.model.bluesky.* import com.morpho.app.model.uidata.* import com.morpho.app.util.MutableSharedFlowSerializer @@ -146,19 +145,4 @@ sealed interface ContentCardState { enum class ListsOrFeeds { Lists, Feeds -} - -@Immutable -@Serializable -enum class FeedType { - HOME, - PROFILE_POSTS, - PROFILE_REPLIES, - PROFILE_MEDIA, - PROFILE_LIKES, - PROFILE_USER_LISTS, - PROFILE_MOD_SERVICE, - PROFILE_FEEDS_LIST, - LIST_FOLLOWING, - OTHER, -} +} \ No newline at end of file diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uistate/LoadingState.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uistate/LoadingState.kt new file mode 100644 index 0000000..b360686 --- /dev/null +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uistate/LoadingState.kt @@ -0,0 +1,21 @@ +package com.morpho.app.model.uistate + +import androidx.compose.runtime.Immutable +import kotlinx.serialization.Serializable + +@Immutable +@Serializable +sealed interface UiLoadingState { + data object Loading : UiLoadingState + data object Idle : UiLoadingState + data class Error(val errorMessage: String) : UiLoadingState +} + + +@Immutable +@Serializable +sealed interface ContentLoadingState { + data object Loading : ContentLoadingState + data object Idle : ContentLoadingState + data class Error(val errorMessage: String) : ContentLoadingState +} \ No newline at end of file diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uistate/LoginState.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uistate/LoginState.kt index deb1930..16b2b3f 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uistate/LoginState.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uistate/LoginState.kt @@ -27,12 +27,8 @@ sealed interface AuthState { @Serializable @Immutable data class LoginState( - val loadingState: UiLoadingState = UiLoadingState.Idle, + override val loadingState: UiLoadingState = UiLoadingState.Idle, val mode: LoginScreenMode = LoginScreenMode.SIGN_IN, val authState: AuthState = AuthState.NoAuth, val credentials: Credentials? = null, -) { - val isLoading: Boolean - get() = loadingState is UiLoadingState.Loading -} - +) : UiState diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uistate/NotificationsState.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uistate/NotificationsState.kt index 879ee9d..492f65e 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uistate/NotificationsState.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uistate/NotificationsState.kt @@ -10,7 +10,8 @@ import org.koin.core.component.KoinComponent data class NotificationsUIState( val filterState: MutableStateFlow = MutableStateFlow(NotificationsFilterState()), val showPosts: Boolean = true, -): KoinComponent + override val loadingState: UiLoadingState = UiLoadingState.Loading, +): KoinComponent, UiState @Immutable @Serializable data class NotificationsFilterState( diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uistate/SkylineState.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uistate/SkylineState.kt index 566b94c..d71b309 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uistate/SkylineState.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uistate/SkylineState.kt @@ -1,3 +1,20 @@ package com.morpho.app.model.uistate +import androidx.compose.runtime.Immutable +import kotlinx.serialization.Serializable + +@Immutable +@Serializable +enum class FeedType { + HOME, + PROFILE_POSTS, + PROFILE_REPLIES, + PROFILE_MEDIA, + PROFILE_LIKES, + PROFILE_USER_LISTS, + PROFILE_MOD_SERVICE, + PROFILE_FEEDS_LIST, + LIST_FOLLOWING, + OTHER, +} diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uistate/UiState.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uistate/UiState.kt index 58b2de6..613d26b 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uistate/UiState.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uistate/UiState.kt @@ -1,8 +1,10 @@ package com.morpho.app.model.uistate -sealed interface UiLoadingState { - data object Loading : UiLoadingState - data object Idle : UiLoadingState - data class Error(val errorMessage: String) : UiLoadingState + +interface UiState { + val loadingState: UiLoadingState + + val isLoading: Boolean + get() = loadingState == UiLoadingState.Loading } \ No newline at end of file diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/base/BaseScreenModel.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/base/BaseScreenModel.kt index 5ca6284..0c0493f 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/base/BaseScreenModel.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/base/BaseScreenModel.kt @@ -7,15 +7,11 @@ import app.cash.paging.Pager import app.cash.paging.cachedIn import cafe.adriel.voyager.core.model.ScreenModel import cafe.adriel.voyager.core.model.screenModelScope -import com.morpho.app.data.ContentLabelService import com.morpho.app.data.MorphoAgent -import com.morpho.app.data.NotificationsSource import com.morpho.app.model.bluesky.BskyPost +import com.morpho.app.model.bluesky.NotificationsSource import com.morpho.app.model.bluesky.toPost -import com.morpho.app.model.uidata.Event -import com.morpho.app.model.uidata.MyProfilePresenter -import com.morpho.app.model.uidata.ProfilePresenter -import com.morpho.app.model.uidata.UIUpdate +import com.morpho.app.model.uidata.* import com.morpho.butterfly.AtUri import com.morpho.butterfly.Did import kotlinx.coroutines.channels.BufferOverflow diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/login/LoginScreen.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/login/LoginScreen.kt index f442266..9c61ea5 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/login/LoginScreen.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/login/LoginScreen.kt @@ -1,26 +1,11 @@ package com.morpho.app.screens.login -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.* import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.material3.Button -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.OutlinedTextField -import androidx.compose.material3.Scaffold -import androidx.compose.material3.SnackbarHost -import androidx.compose.material3.SnackbarHostState -import androidx.compose.material3.SnackbarResult -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember +import androidx.compose.material3.* +import androidx.compose.runtime.* import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusManager @@ -36,17 +21,17 @@ import cafe.adriel.voyager.navigator.currentOrThrow import cafe.adriel.voyager.navigator.tab.LocalTabNavigator import cafe.adriel.voyager.navigator.tab.Tab import cafe.adriel.voyager.navigator.tab.TabOptions +import com.morpho.app.CommonParcelable +import com.morpho.app.CommonParcelize import com.morpho.app.model.uistate.AuthState import com.morpho.app.screens.base.tabbed.TabbedBaseScreen import com.morpho.app.ui.common.LoadingCircle -import dev.icerock.moko.parcelize.Parcelable -import dev.icerock.moko.parcelize.Parcelize import kotlinx.coroutines.launch import kotlinx.serialization.Serializable -@Parcelize +@CommonParcelize @Serializable -data object LoginScreen: Tab, Parcelable { +data object LoginScreen: Tab, CommonParcelable { override val key: ScreenKey = hashCode().toString() + "TabbedLoginScreen" diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/main/tabbed/TabbedHomeView.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/main/tabbed/TabbedHomeView.kt index cccb12c..1413cf9 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/main/tabbed/TabbedHomeView.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/main/tabbed/TabbedHomeView.kt @@ -10,25 +10,11 @@ import androidx.compose.animation.core.spring import androidx.compose.animation.slideInHorizontally import androidx.compose.animation.slideOutHorizontally import androidx.compose.animation.togetherWith -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.statusBarsPadding -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.SecondaryScrollableTabRow -import androidx.compose.material3.Tab -import androidx.compose.material3.TabPosition -import androidx.compose.material3.TabRowDefaults +import androidx.compose.foundation.layout.* +import androidx.compose.material3.* import androidx.compose.material3.TabRowDefaults.tabIndicatorOffset -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.derivedStateOf -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableIntStateOf -import androidx.compose.runtime.remember +import androidx.compose.runtime.* import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.IntOffset @@ -214,7 +200,7 @@ fun HomeTabRow( selectedTabIndex = selectedTabIndex, modifier = modifier.fillMaxWidth(),//.zIndex(1f), edgePadding = 10.dp, - indicator = { tabPositions: List -> + indicator = { tabPositions -> if(tabPositions.isNotEmpty()) { TabRowDefaults.SecondaryIndicator( Modifier.tabIndicatorOffset(tabPositions[max(0, min(selectedTabIndex, tabs.lastIndex))]) diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/notifications/NotificationsView.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/notifications/NotificationsView.kt index 829bf18..832cf6a 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/notifications/NotificationsView.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/notifications/NotificationsView.kt @@ -25,10 +25,10 @@ import cafe.adriel.voyager.core.model.screenModelScope import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.Navigator import cafe.adriel.voyager.navigator.currentOrThrow -import com.morpho.app.data.NotificationsListItem -import com.morpho.app.data.collectNotifications import com.morpho.app.model.bluesky.BskyPost import com.morpho.app.model.bluesky.DraftPost +import com.morpho.app.model.bluesky.NotificationsListItem +import com.morpho.app.model.bluesky.collectNotifications import com.morpho.app.model.uistate.NotificationsUIState import com.morpho.app.screens.base.tabbed.ProfileTab import com.morpho.app.screens.base.tabbed.TabScreen diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/profile/TabbedProfileView.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/profile/TabbedProfileView.kt index cb1fdee..5916aa8 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/profile/TabbedProfileView.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/profile/TabbedProfileView.kt @@ -5,19 +5,9 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.SecondaryScrollableTabRow -import androidx.compose.material3.Tab -import androidx.compose.material3.Text -import androidx.compose.material3.TopAppBarDefaults -import androidx.compose.material3.TopAppBarScrollBehavior -import androidx.compose.material3.rememberTopAppBarState -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableIntStateOf -import androidx.compose.runtime.remember +import androidx.compose.material3.* +import androidx.compose.runtime.* import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.unit.dp @@ -31,7 +21,6 @@ import cafe.adriel.voyager.navigator.tab.TabDisposable import cafe.adriel.voyager.navigator.tab.TabNavigator import cafe.adriel.voyager.navigator.tab.TabOptions import coil3.annotation.ExperimentalCoilApi -import com.morpho.app.model.bluesky.DetailedProfile import com.morpho.app.model.uidata.Event import com.morpho.app.model.uidata.FeedEvent import com.morpho.app.model.uistate.ContentCardState @@ -49,7 +38,7 @@ import cafe.adriel.voyager.navigator.tab.Tab as NavTab @OptIn(ExperimentalAnimationApi::class, ExperimentalMaterial3Api::class) @Composable fun MyTabbedProfileTopBar( - profile: DetailedProfile, + profile: ContentCardState.MyProfile, scrollBehavior: TopAppBarScrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior( rememberTopAppBarState()), tabs: List, @@ -64,7 +53,7 @@ fun MyTabbedProfileTopBar( .nestedScroll(scrollBehavior.nestedScrollConnection), ) { DetailedProfileFragment( - profile = profile, + profile = profile.profile, myProfile = true, isTopLevel = true, scrollBehavior = scrollBehavior, @@ -91,7 +80,7 @@ fun MyTabbedProfileTopBar( @OptIn(ExperimentalAnimationApi::class, ExperimentalMaterial3Api::class) @Composable fun TabbedProfileTopBar( - profile: DetailedProfile, + profile: ContentCardState.FullProfile, scrollBehavior: TopAppBarScrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior( rememberTopAppBarState()), tabs: List, @@ -106,7 +95,7 @@ fun TabbedProfileTopBar( .nestedScroll(scrollBehavior.nestedScrollConnection), ) { DetailedProfileFragment( - profile = profile, + profile = profile.profile, myProfile = true, isTopLevel = true, scrollBehavior = scrollBehavior, @@ -177,7 +166,7 @@ fun TabScreen.TabbedProfileContent( modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection), topContent = { if(ownProfile) MyTabbedProfileTopBar( - profile = myProfileState.profile, + profile = myProfileState, scrollBehavior = scrollBehavior, tabs = tabs, onBackClicked = { navigator.pop() }, @@ -196,7 +185,7 @@ fun TabScreen.TabbedProfileContent( }, tabIndex = selectedTabIndex, ) else if(profileState != null) TabbedProfileTopBar( - profile = profileState.profile, + profile = profileState, scrollBehavior = scrollBehavior, tabs = tabs, onBackClicked = { navigator.pop() }, diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/common/PostComposer.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/common/PostComposer.kt index 3b5fc57..504795a 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/common/PostComposer.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/common/PostComposer.kt @@ -1,42 +1,14 @@ package com.morpho.app.ui.common -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.WindowInsets -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.ime -import androidx.compose.foundation.layout.imePadding -import androidx.compose.foundation.layout.navigationBars -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.union +import androidx.compose.foundation.layout.* import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Image -import androidx.compose.material3.Button -import androidx.compose.material3.CircularProgressIndicator -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.ModalBottomSheet -import androidx.compose.material3.OutlinedTextField -import androidx.compose.material3.SheetState -import androidx.compose.material3.Surface -import androidx.compose.material3.Text -import androidx.compose.material3.TextButton -import androidx.compose.material3.rememberModalBottomSheetState -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.material3.* +import androidx.compose.runtime.* import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalFocusManager @@ -67,15 +39,15 @@ enum class ComposerRole { @OptIn(ExperimentalMaterial3Api::class) @Composable -fun BottomSheetPostComposer( +inline fun BottomSheetPostComposer( modifier: Modifier = Modifier, - onDismissRequest: ()-> Unit = {}, + crossinline onDismissRequest: ()-> Unit = {}, initialContent: BskyPost? = null, role: ComposerRole = ComposerRole.StandalonePost, draft: DraftPost = DraftPost(), - onSend: (DraftPost) -> Unit = {}, - onCancel: () -> Unit = {}, - onUpdate: (DraftPost) -> Unit = {}, + crossinline onSend: (DraftPost) -> Unit = {}, + crossinline onCancel: () -> Unit = {}, + crossinline onUpdate: (DraftPost) -> Unit = {}, sheetState:SheetState = rememberModalBottomSheetState(), scope: CoroutineScope = rememberCoroutineScope() ) { @@ -92,7 +64,7 @@ fun BottomSheetPostComposer( sheetState = sheetState, windowInsets = WindowInsets.navigationBars.union(WindowInsets.ime), - ){ + ){ PostComposer( role = role, modifier = modifier, diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/common/SkylineTopAppBar.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/common/SkylineTopAppBar.kt index 6ce24e0..ebfe567 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/common/SkylineTopAppBar.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/common/SkylineTopAppBar.kt @@ -1,27 +1,10 @@ package com.morpho.app.ui.common -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxHeight -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.offset -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.* import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Menu -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.HorizontalDivider -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.SecondaryScrollableTabRow -import androidx.compose.material3.Surface -import androidx.compose.material3.Tab -import androidx.compose.material3.TabPosition -import androidx.compose.material3.TabRowDefaults +import androidx.compose.material3.* import androidx.compose.material3.TabRowDefaults.tabIndicatorOffset -import androidx.compose.material3.Text -import androidx.compose.material3.TopAppBar -import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf @@ -88,7 +71,7 @@ fun SkylineTopBar( selectedTabIndex = selectedTab, modifier = modifier.offset(y = (-8).dp, x = 4.dp ), edgePadding = 10.dp, - indicator = @Composable { tabPositions: List -> + indicator = { tabPositions -> if(tabPositions.isNotEmpty()) { TabRowDefaults.SecondaryIndicator( Modifier.tabIndicatorOffset(tabPositions[max(0, min(selectedTab, tabList.lastIndex))]) diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/elements/HighlightIndication.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/elements/HighlightIndication.kt new file mode 100644 index 0000000..a76c6a0 --- /dev/null +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/elements/HighlightIndication.kt @@ -0,0 +1,41 @@ +package com.morpho.app.ui.elements + +import androidx.compose.foundation.Indication +import androidx.compose.foundation.IndicationInstance +import androidx.compose.foundation.interaction.InteractionSource +import androidx.compose.foundation.interaction.collectIsFocusedAsState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.State +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.geometry.CornerRadius +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.drawscope.ContentDrawScope +import androidx.compose.ui.graphics.drawscope.Stroke +import androidx.compose.ui.unit.dp + +class MorphoHighlightIndicationInstance(isEnabledState: State) : + IndicationInstance { + private val isEnabled by isEnabledState + override fun ContentDrawScope.drawIndication() { + drawContent() + if (isEnabled) { + drawRoundRect(cornerRadius = CornerRadius(4.dp.toPx()), size = size, color = Color.Gray, alpha = 0.2f) + drawRoundRect(cornerRadius = CornerRadius(4.dp.toPx()), + style = Stroke(width = Stroke.HairlineWidth), + size = size, color = Color.White, alpha = 0.9f) + } + } + +} + +class MorphoHighlightIndication : Indication { + @Composable + override fun rememberUpdatedInstance(interactionSource: InteractionSource): + IndicationInstance { + val isFocusedState = interactionSource.collectIsFocusedAsState() + return remember(interactionSource) { + MorphoHighlightIndicationInstance(isEnabledState = isFocusedState) + } + } +} \ No newline at end of file diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/elements/OutlinedAvatar.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/elements/OutlinedAvatar.kt index f8e940e..8c13037 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/elements/OutlinedAvatar.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/elements/OutlinedAvatar.kt @@ -17,14 +17,12 @@ package com.morpho.app.ui.elements */ import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.CircleShape import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.drawBehind @@ -76,8 +74,8 @@ fun OutlinedAvatar( AvatarShape.Rounded -> MaterialTheme.shapes.small AvatarShape.Corner -> roundedTopLBotR.small } - //val interactionSource = remember { MutableInteractionSource() } - //val indication = remember { MorphoHighlightIndication() } + val interactionSource = remember { MutableInteractionSource() } + val indication = remember { MorphoHighlightIndication() } val pxSize = LocalDensity.current.run { (size-outlineSize).toPx()*2 }.toInt() val sB = when(avatarShape) { AvatarShape.Circle -> CircleShape.createOutline( @@ -92,7 +90,9 @@ fun OutlinedAvatar( } val modClicked = if(onClicked != null) { modifier.clickable( - + interactionSource = interactionSource, + indication = indication, + enabled = true, onClick = { onClicked() } ) } else modifier diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/notifications/NotificationAvatarList.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/notifications/NotificationAvatarList.kt index 7380e24..fa4de6e 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/notifications/NotificationAvatarList.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/notifications/NotificationAvatarList.kt @@ -18,7 +18,7 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.withStyle import androidx.compose.ui.unit.dp -import com.morpho.app.data.NotificationsListItem +import com.morpho.app.model.bluesky.NotificationsListItem import com.morpho.app.ui.elements.OutlinedAvatar import com.morpho.butterfly.Did import kotlin.math.min diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/notifications/NotificationsElement.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/notifications/NotificationsElement.kt index e4ca873..231128e 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/notifications/NotificationsElement.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/notifications/NotificationsElement.kt @@ -13,9 +13,9 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import com.atproto.repo.StrongRef -import com.morpho.app.data.NotificationsListItem import com.morpho.app.model.bluesky.BskyNotification import com.morpho.app.model.bluesky.BskyPost +import com.morpho.app.model.bluesky.NotificationsListItem import com.morpho.app.ui.common.OnPostClicked import com.morpho.app.ui.elements.MenuOptions import com.morpho.app.ui.post.PostFragment diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/post/EmbedPostFragment.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/post/EmbedPostFragment.kt index ac26364..7197dd5 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/post/EmbedPostFragment.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/post/EmbedPostFragment.kt @@ -4,29 +4,13 @@ package com.morpho.app.ui.post import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.ColumnScope -import androidx.compose.foundation.layout.ExperimentalLayoutApi -import androidx.compose.foundation.layout.FlowRow -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.heightIn -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.width -import androidx.compose.foundation.layout.wrapContentWidth +import androidx.compose.foundation.layout.* import androidx.compose.foundation.text.selection.SelectionContainer import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember +import androidx.compose.runtime.* import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalUriHandler @@ -37,11 +21,8 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.withStyle import androidx.compose.ui.unit.dp import androidx.compose.ui.util.fastForEach -import com.morpho.app.model.bluesky.BskyPost -import com.morpho.app.model.bluesky.BskyPostFeature -import com.morpho.app.model.bluesky.EmbedImage -import com.morpho.app.model.bluesky.EmbedRecord -import com.morpho.app.model.bluesky.FacetType +import com.morpho.app.model.bluesky.* +import com.morpho.app.ui.elements.MorphoHighlightIndication import com.morpho.app.ui.elements.OutlinedAvatar import com.morpho.app.ui.elements.RichTextElement import com.morpho.app.ui.elements.WrappedColumn @@ -66,7 +47,7 @@ fun EmbedPostFragment( var hidePost by rememberSaveable { mutableStateOf(post.author.mutedByMe) } val muted = rememberSaveable { post.author.mutedByMe } val interactionSource = remember { MutableInteractionSource() } - //val indication = remember { MorphoHighlightIndication() } + val indication = remember { MorphoHighlightIndication() } val uriHandler = LocalUriHandler.current WrappedColumn( modifier @@ -82,6 +63,9 @@ fun EmbedPostFragment( .fillMaxWidth() .align(Alignment.End) .clickable( + interactionSource = interactionSource, + indication = indication, + enabled = true, onClick = { onItemClicked(post.uri) } ) @@ -135,6 +119,9 @@ fun EmbedPostFragment( .weight(10.0F) .alignByBaseline() .clickable( + interactionSource = interactionSource, + indication = indication, + enabled = true, onClick = { onProfileClicked(post.author.did) } ), ) diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/post/PostFragment.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/post/PostFragment.kt index 1890506..2b04281 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/post/PostFragment.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/post/PostFragment.kt @@ -4,26 +4,11 @@ package com.morpho.app.ui.post import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.ColumnScope -import androidx.compose.foundation.layout.ExperimentalLayoutApi -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.offset -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.width -import androidx.compose.foundation.layout.wrapContentWidth +import androidx.compose.foundation.layout.* import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.Reply import androidx.compose.material.icons.filled.Repeat -import androidx.compose.material3.Icon -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Surface -import androidx.compose.material3.Text -import androidx.compose.material3.surfaceColorAtElevation +import androidx.compose.material3.* import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Alignment @@ -40,29 +25,14 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.util.fastAny import com.atproto.label.Blurs import com.atproto.repo.StrongRef -import com.morpho.app.model.bluesky.BskyPost -import com.morpho.app.model.bluesky.BskyPostFeature -import com.morpho.app.model.bluesky.BskyPostReason -import com.morpho.app.model.bluesky.EmbedRecord -import com.morpho.app.model.bluesky.FacetType -import com.morpho.app.model.bluesky.TimelinePostMedia +import com.morpho.app.model.bluesky.* import com.morpho.app.ui.common.OnPostClicked -import com.morpho.app.ui.elements.AvatarShape -import com.morpho.app.ui.elements.ContentHider -import com.morpho.app.ui.elements.MenuOptions -import com.morpho.app.ui.elements.OutlinedAvatar -import com.morpho.app.ui.elements.RichTextElement -import com.morpho.app.ui.elements.WrappedColumn +import com.morpho.app.ui.elements.* import com.morpho.app.ui.lists.FeedListEntryFragment import com.morpho.app.ui.lists.UserListEntryFragment import com.morpho.app.util.getFormattedDateTimeSince import com.morpho.app.util.openBrowser -import com.morpho.butterfly.AtIdentifier -import com.morpho.butterfly.AtUri -import com.morpho.butterfly.ContentHandling -import com.morpho.butterfly.LabelAction -import com.morpho.butterfly.LabelDescription -import com.morpho.butterfly.LabelIcon +import com.morpho.butterfly.* import com.morpho.butterfly.model.RecordType import kotlinx.collections.immutable.toImmutableList import morpho.app.ui.utils.indentLevel @@ -111,7 +81,7 @@ fun PostFragment( }} val interactionSource = remember { MutableInteractionSource() } - //val indication = remember { MorphoHighlightIndication() } + val indication = remember { MorphoHighlightIndication() } val bgColor = if (role == PostFragmentRole.PrimaryThreadRoot) { MaterialTheme.colorScheme.background } else { @@ -143,6 +113,9 @@ fun PostFragment( .fillMaxWidth(indentLevel(indent)) .align(Alignment.End) .clickable( + interactionSource = interactionSource, + indication = indication, + enabled = true, onClick = { onItemClicked(post.uri) } ) @@ -239,6 +212,9 @@ fun PostFragment( .alignByBaseline() .pointerHoverIcon(PointerIcon.Hand) .clickable( + interactionSource = interactionSource, + indication = indication, + enabled = true, onClick = { onProfileClicked(post.author.did) } ) ) diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/profile/DetailedProfileFragment.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/profile/DetailedProfileFragment.kt index 7dbd70f..5f01233 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/profile/DetailedProfileFragment.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/profile/DetailedProfileFragment.kt @@ -1,6 +1,7 @@ package com.morpho.app.ui.profile +import androidx.compose.foundation.layout.ExperimentalLayoutApi import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.TopAppBarScrollBehavior @@ -8,10 +9,13 @@ import androidx.compose.material3.rememberTopAppBarState import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import com.morpho.app.model.bluesky.DetailedProfile +import org.jetbrains.compose.resources.ExperimentalResourceApi @OptIn( ExperimentalMaterial3Api::class, + ExperimentalLayoutApi::class, + ExperimentalResourceApi::class ) @Composable expect fun DetailedProfileFragment( diff --git a/Morpho/composeApp/src/desktopMain/kotlin/com/morpho/app/Platform.desktop.kt b/Morpho/composeApp/src/desktopMain/kotlin/com/morpho/app/Platform.desktop.kt index 6af8352..77fb745 100644 --- a/Morpho/composeApp/src/desktopMain/kotlin/com/morpho/app/Platform.desktop.kt +++ b/Morpho/composeApp/src/desktopMain/kotlin/com/morpho/app/Platform.desktop.kt @@ -1,7 +1,15 @@ @file:Suppress("EXPECT_ACTUAL_CLASSIFIERS_ARE_IN_BETA_WARNING") package com.morpho.app +import kotlinx.datetime.LocalDateTime import java.util.Locale +// Note: no need to define CommonParcelize here (bc its @OptionalExpectation) +actual interface CommonParcelable // not used on iOS + +// Note: no need to define CommonTypeParceler> here (bc its @OptionalExpectation) +actual interface CommonParceler // not used on iOS +actual object LocalDateTimeParceler : CommonParceler // not used on iOS + // For Android @Parcelize @Target(AnnotationTarget.TYPE) @Retention(AnnotationRetention.SOURCE) diff --git a/Morpho/composeApp/src/desktopMain/kotlin/com/morpho/app/ui/common/BackHandler.kt b/Morpho/composeApp/src/desktopMain/kotlin/com/morpho/app/ui/common/BackHandler.kt new file mode 100644 index 0000000..27225c8 --- /dev/null +++ b/Morpho/composeApp/src/desktopMain/kotlin/com/morpho/app/ui/common/BackHandler.kt @@ -0,0 +1,7 @@ +package com.morpho.app.ui.common + +import androidx.compose.runtime.Composable + +@Composable +actual fun BackHandler(content: () -> Unit) { +} \ No newline at end of file diff --git a/Morpho/composeApp/src/desktopMain/kotlin/com/morpho/app/ui/profile/DetailedProfileFragment.desktop.kt b/Morpho/composeApp/src/desktopMain/kotlin/com/morpho/app/ui/profile/DetailedProfileFragment.desktop.kt index d331774..9c92344 100644 --- a/Morpho/composeApp/src/desktopMain/kotlin/com/morpho/app/ui/profile/DetailedProfileFragment.desktop.kt +++ b/Morpho/composeApp/src/desktopMain/kotlin/com/morpho/app/ui/profile/DetailedProfileFragment.desktop.kt @@ -4,38 +4,15 @@ import androidx.compose.animation.animateContentSize import androidx.compose.animation.core.Spring import androidx.compose.animation.core.spring import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.WindowInsets -import androidx.compose.foundation.layout.WindowInsetsSides -import androidx.compose.foundation.layout.fillMaxHeight -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.only -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.requiredHeight -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.systemBars -import androidx.compose.foundation.layout.widthIn -import androidx.compose.foundation.layout.windowInsetsPadding -import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.layout.* import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.text.selection.SelectionContainer import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.ArrowBackIosNew -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.LargeTopAppBar -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Surface -import androidx.compose.material3.Text -import androidx.compose.material3.TopAppBarDefaults -import androidx.compose.material3.TopAppBarScrollBehavior +import androidx.compose.material3.* import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color @@ -56,10 +33,14 @@ import com.morpho.app.ui.elements.RichTextElement import kotlinx.collections.immutable.toImmutableList import morpho.composeapp.generated.resources.Res import morpho.composeapp.generated.resources.test_banner +import org.jetbrains.compose.resources.ExperimentalResourceApi import org.jetbrains.compose.resources.painterResource - -@OptIn(ExperimentalMaterial3Api::class) +@OptIn( + ExperimentalMaterial3Api::class, + ExperimentalLayoutApi::class, + ExperimentalResourceApi::class +) @Composable actual fun DetailedProfileFragment( profile: DetailedProfile, @@ -76,7 +57,11 @@ actual fun DetailedProfileFragment( } else { (135.dp - (60 * scrollBehavior.state.collapsedFraction).dp) } - val collapsed = scrollBehavior.state.collapsedFraction > 0.5f + val collapsed = scrollBehavior.state.collapsedFraction > 0.5 + LaunchedEffect(scrollState) { + println("Banner Height: $bannerHeight") + print("Collapsed: $collapsed") + } ConstraintLayout( modifier = Modifier @@ -257,21 +242,25 @@ actual fun DetailedProfileFragment( .padding(start = 20.dp, end = 20.dp, top = bannerHeight +40.dp)//.border(1.dp, Color.Yellow) ) { - Text( - text = name, - style = MaterialTheme.typography.headlineMedium, - color = MaterialTheme.colorScheme.onSurface - ) - - Text( - text = " @${profile.handle}", - color = MaterialTheme.colorScheme.primary, - style = MaterialTheme.typography.labelMedium, - ) + SelectionContainer { + Text( + text = name, + style = MaterialTheme.typography.headlineMedium, + color = MaterialTheme.colorScheme.onSurface + ) + } + SelectionContainer { + Text( + text = " @${profile.handle}", + color = MaterialTheme.colorScheme.primary, + style = MaterialTheme.typography.labelMedium, + ) + } Spacer(modifier = Modifier.height(10.dp)) - RichTextElement(profile.description.orEmpty()) - + SelectionContainer { + RichTextElement(profile.description.orEmpty()) + } } } diff --git a/Morpho/gradle/libs.versions.toml b/Morpho/gradle/libs.versions.toml index 46b9001..374e77d 100644 --- a/Morpho/gradle/libs.versions.toml +++ b/Morpho/gradle/libs.versions.toml @@ -3,8 +3,8 @@ agp = "8.2.2" android-compileSdk = "34" android-minSdk = "24" android-targetSdk = "34" -androidx-extIcons = "1.7.1" -androidx-activityCompose = "1.9.2" +androidx-extIcons = "1.6.8" +androidx-activityCompose = "1.9.1" androidx-appcompat = "1.7.0" androidx-constraintlayout = "2.1.4" androidx-core-ktx = "1.13.1" @@ -12,9 +12,9 @@ androidx-espresso-core = "3.6.1" androidx-material = "1.12.0" androidx-test-junit = "1.2.1" appdirs = "1.2.2" -compose = "1.6.7" +compose = "1.6.8" compose-plugin = "1.6.11" -constraintlayoutComposeMultiplatform = "0.4.0" +constraintlayoutComposeMultiplatform = "0.3.0-alpha01" datastorePreferencesCore = "1.1.1" imageLoader = "1.7.8" filekit = "0.8.2" @@ -35,20 +35,20 @@ kotlinx-datetime = "0.6.0" kotlinx-coroutines = "1.8.1" kotlinx-immutable = "0.3.7" kstore = "0.7.1" -ktor = "2.3.12" +ktor = "2.3.9" ktorClientAndroid = "[ktor-version]" logbackClassic = "1.5.7" logbackCore = "1.5.7" logging = "1.4.2" nativeparameterstoreaccess = "0.1.0" okio = "3.9.0" -slf4j-api = "2.0.15" +slf4j-api = "2.0.13" github-kotlin-logging-jvm = "5.1.0" kotlinx-abi-plugin = "0.13.2" window = "1.3.0" material3-android = "1.2.1" accompanist-permissions = "0.32.0" -coil = "3.0.0-alpha10" +coil = "3.0.0-alpha06" voyager = "1.1.0-beta02" kmpalette = "3.1.0" @@ -91,7 +91,7 @@ kmpalette-extensions-file = { module = "com.kmpalette:extensions-file", version. coil = { module = "io.coil-kt.coil3:coil", version.ref = "coil" } coil-compose = { module = "io.coil-kt.coil3:coil-compose", version.ref = "coil" } -coil-network-ktor = { module = "io.coil-kt.coil3:coil-network-ktor2", version.ref = "coil" } +coil-network-ktor = { module = "io.coil-kt.coil3:coil-network-ktor", version.ref = "coil" } kotlinx-coroutines = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinx-coroutines" } kotlinx-coroutines-swing = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-swing", version.ref = "kotlinx-coroutines" } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 43622d9..61f0758 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -3,8 +3,8 @@ agp = "8.2.2" android-compileSdk = "34" android-minSdk = "24" android-targetSdk = "34" -androidx-extIcons = "1.7.1" -androidx-activityCompose = "1.9.2" +androidx-extIcons = "1.6.8" +androidx-activityCompose = "1.9.1" androidx-appcompat = "1.7.0" androidx-constraintlayout = "2.1.4" androidx-core-ktx = "1.13.1" @@ -12,9 +12,9 @@ androidx-espresso-core = "3.6.1" androidx-material = "1.12.0" androidx-test-junit = "1.2.1" appdirs = "1.2.2" -compose = "1.6.7" +compose = "1.6.8" compose-plugin = "1.6.11" -constraintlayoutComposeMultiplatform = "0.4.0" +constraintlayoutComposeMultiplatform = "0.3.0-alpha01" datastorePreferencesCore = "1.1.1" filekit = "0.8.2" imageLoader = "1.7.8" @@ -35,19 +35,19 @@ kotlinx-datetime = "0.6.0" kotlinx-coroutines = "1.8.1" kotlinx-immutable = "0.3.7" kstore = "0.7.1" -ktor = "2.3.12" +ktor = "2.3.9" logbackClassic = "1.5.7" logbackCore = "1.5.7" logging = "1.4.2" nativeparameterstoreaccess = "0.1.0" okio = "3.9.0" -slf4j-api = "2.0.15" +slf4j-api = "2.0.13" github-kotlin-logging-jvm = "5.1.0" kotlinx-abi-plugin = "0.13.2" window = "1.3.0" material3-android = "1.2.1" accompanist-permissions = "0.32.0" -coil = "3.0.0-alpha10" +coil = "3.0.0-alpha06" voyager = "1.1.0-beta02" kmpalette = "3.1.0" @@ -95,7 +95,7 @@ kjwt = { module = "io.github.nefilim.kjwt:kjwt-core", version.ref = "kjwt" } coil = { module = "io.coil-kt.coil3:coil", version.ref = "coil" } coil-compose = { module = "io.coil-kt.coil3:coil-compose", version.ref = "coil" } -coil-network-ktor = { module = "io.coil-kt.coil3:coil-network-ktor2", version.ref = "coil" } +coil-network-ktor = { module = "io.coil-kt.coil3:coil-network-ktor", version.ref = "coil" } kotlinx-coroutines = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinx-coroutines" } kotlinx-coroutines-swing = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-swing", version.ref = "kotlinx-coroutines" } From 0d708c7b4a923361638382e2cc45e8c1dffe7a19 Mon Sep 17 00:00:00 2001 From: Orual Date: Tue, 17 Sep 2024 19:08:34 -0400 Subject: [PATCH 17/42] gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 6d7023e..f4311b2 100644 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,4 @@ Test Results* Morpho/.kotlin .kotlin /.kotlin/ +Butterfly From 27f1741adbb9a7efd4d9591eec22490e07efb437 Mon Sep 17 00:00:00 2001 From: Orual Date: Tue, 17 Sep 2024 20:27:13 -0400 Subject: [PATCH 18/42] gradle update and stuff --- Butterfly | 2 +- Morpho/gradle/libs.versions.toml | 2 +- .../gradle/wrapper/gradle-wrapper.properties | 2 +- gradle/libs.versions.toml | 2 +- gradle/wrapper/gradle-wrapper.jar | Bin 63375 -> 43504 bytes gradle/wrapper/gradle-wrapper.properties | 2 +- gradlew | 22 +++++++++++------- gradlew.bat | 22 ++++++++++-------- 8 files changed, 30 insertions(+), 24 deletions(-) diff --git a/Butterfly b/Butterfly index aa6c8b5..9e114b9 160000 --- a/Butterfly +++ b/Butterfly @@ -1 +1 @@ -Subproject commit aa6c8b51fce31591b80c36600ccf9c4a572395ee +Subproject commit 9e114b96100c953e9de2fd8d1bd21807f48023be diff --git a/Morpho/gradle/libs.versions.toml b/Morpho/gradle/libs.versions.toml index 374e77d..8fd5960 100644 --- a/Morpho/gradle/libs.versions.toml +++ b/Morpho/gradle/libs.versions.toml @@ -1,5 +1,5 @@ [versions] -agp = "8.2.2" +agp = "8.5.2" android-compileSdk = "34" android-minSdk = "24" android-targetSdk = "34" diff --git a/Morpho/gradle/wrapper/gradle-wrapper.properties b/Morpho/gradle/wrapper/gradle-wrapper.properties index 3fa8f86..09523c0 100644 --- a/Morpho/gradle/wrapper/gradle-wrapper.properties +++ b/Morpho/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 61f0758..40a247f 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,5 +1,5 @@ [versions] -agp = "8.2.2" +agp = "8.5.2" android-compileSdk = "34" android-minSdk = "24" android-targetSdk = "34" diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 033e24c4cdf41af1ab109bc7f253b2b887023340..2c3521197d7c4586c843d1d3e9090525f1898cde 100755 GIT binary patch literal 43504 zcma&N1CXTcmMvW9vTb(Rwr$&4wr$(C?dmSu>@vG-+vuvg^_??!{yS%8zW-#zn-LkA z5&1^$^{lnmUON?}LBF8_K|(?T0Ra(xUH{($5eN!MR#ZihR#HxkUPe+_R8Cn`RRs(P z_^*#_XlXmGv7!4;*Y%p4nw?{bNp@UZHv1?Um8r6)Fei3p@ClJn0ECfg1hkeuUU@Or zDaPa;U3fE=3L}DooL;8f;P0ipPt0Z~9P0)lbStMS)ag54=uL9ia-Lm3nh|@(Y?B`; zx_#arJIpXH!U{fbCbI^17}6Ri*H<>OLR%c|^mh8+)*h~K8Z!9)DPf zR2h?lbDZQ`p9P;&DQ4F0sur@TMa!Y}S8irn(%d-gi0*WxxCSk*A?3lGh=gcYN?FGl z7D=Js!i~0=u3rox^eO3i@$0=n{K1lPNU zwmfjRVmLOCRfe=seV&P*1Iq=^i`502keY8Uy-WNPwVNNtJFx?IwAyRPZo2Wo1+S(xF37LJZ~%i)kpFQ3Fw=mXfd@>%+)RpYQLnr}B~~zoof(JVm^^&f zxKV^+3D3$A1G;qh4gPVjhrC8e(VYUHv#dy^)(RoUFM?o%W-EHxufuWf(l*@-l+7vt z=l`qmR56K~F|v<^Pd*p~1_y^P0P^aPC##d8+HqX4IR1gu+7w#~TBFphJxF)T$2WEa zxa?H&6=Qe7d(#tha?_1uQys2KtHQ{)Qco)qwGjrdNL7thd^G5i8Os)CHqc>iOidS} z%nFEDdm=GXBw=yXe1W-ShHHFb?Cc70+$W~z_+}nAoHFYI1MV1wZegw*0y^tC*s%3h zhD3tN8b=Gv&rj}!SUM6|ajSPp*58KR7MPpI{oAJCtY~JECm)*m_x>AZEu>DFgUcby z1Qaw8lU4jZpQ_$;*7RME+gq1KySGG#Wql>aL~k9tLrSO()LWn*q&YxHEuzmwd1?aAtI zBJ>P=&$=l1efe1CDU;`Fd+_;&wI07?V0aAIgc(!{a z0Jg6Y=inXc3^n!U0Atk`iCFIQooHqcWhO(qrieUOW8X(x?(RD}iYDLMjSwffH2~tB z)oDgNBLB^AJBM1M^c5HdRx6fBfka`(LD-qrlh5jqH~);#nw|iyp)()xVYak3;Ybik z0j`(+69aK*B>)e_p%=wu8XC&9e{AO4c~O1U`5X9}?0mrd*m$_EUek{R?DNSh(=br# z#Q61gBzEpmy`$pA*6!87 zSDD+=@fTY7<4A?GLqpA?Pb2z$pbCc4B4zL{BeZ?F-8`s$?>*lXXtn*NC61>|*w7J* z$?!iB{6R-0=KFmyp1nnEmLsA-H0a6l+1uaH^g%c(p{iT&YFrbQ$&PRb8Up#X3@Zsk zD^^&LK~111%cqlP%!_gFNa^dTYT?rhkGl}5=fL{a`UViaXWI$k-UcHJwmaH1s=S$4 z%4)PdWJX;hh5UoK?6aWoyLxX&NhNRqKam7tcOkLh{%j3K^4Mgx1@i|Pi&}<^5>hs5 zm8?uOS>%)NzT(%PjVPGa?X%`N2TQCKbeH2l;cTnHiHppPSJ<7y-yEIiC!P*ikl&!B z%+?>VttCOQM@ShFguHVjxX^?mHX^hSaO_;pnyh^v9EumqSZTi+#f&_Vaija0Q-e*| z7ulQj6Fs*bbmsWp{`auM04gGwsYYdNNZcg|ph0OgD>7O}Asn7^Z=eI>`$2*v78;sj-}oMoEj&@)9+ycEOo92xSyY344^ z11Hb8^kdOvbf^GNAK++bYioknrpdN>+u8R?JxG=!2Kd9r=YWCOJYXYuM0cOq^FhEd zBg2puKy__7VT3-r*dG4c62Wgxi52EMCQ`bKgf*#*ou(D4-ZN$+mg&7$u!! z-^+Z%;-3IDwqZ|K=ah85OLwkO zKxNBh+4QHh)u9D?MFtpbl)us}9+V!D%w9jfAMYEb>%$A;u)rrI zuBudh;5PN}_6J_}l55P3l_)&RMlH{m!)ai-i$g)&*M`eN$XQMw{v^r@-125^RRCF0 z^2>|DxhQw(mtNEI2Kj(;KblC7x=JlK$@78`O~>V!`|1Lm-^JR$-5pUANAnb(5}B}JGjBsliK4& zk6y(;$e&h)lh2)L=bvZKbvh@>vLlreBdH8No2>$#%_Wp1U0N7Ank!6$dFSi#xzh|( zRi{Uw%-4W!{IXZ)fWx@XX6;&(m_F%c6~X8hx=BN1&q}*( zoaNjWabE{oUPb!Bt$eyd#$5j9rItB-h*5JiNi(v^e|XKAj*8(k<5-2$&ZBR5fF|JA z9&m4fbzNQnAU}r8ab>fFV%J0z5awe#UZ|bz?Ur)U9bCIKWEzi2%A+5CLqh?}K4JHi z4vtM;+uPsVz{Lfr;78W78gC;z*yTch~4YkLr&m-7%-xc ztw6Mh2d>_iO*$Rd8(-Cr1_V8EO1f*^@wRoSozS) zy1UoC@pruAaC8Z_7~_w4Q6n*&B0AjOmMWa;sIav&gu z|J5&|{=a@vR!~k-OjKEgPFCzcJ>#A1uL&7xTDn;{XBdeM}V=l3B8fE1--DHjSaxoSjNKEM9|U9#m2<3>n{Iuo`r3UZp;>GkT2YBNAh|b z^jTq-hJp(ebZh#Lk8hVBP%qXwv-@vbvoREX$TqRGTgEi$%_F9tZES@z8Bx}$#5eeG zk^UsLBH{bc2VBW)*EdS({yw=?qmevwi?BL6*=12k9zM5gJv1>y#ML4!)iiPzVaH9% zgSImetD@dam~e>{LvVh!phhzpW+iFvWpGT#CVE5TQ40n%F|p(sP5mXxna+Ev7PDwA zamaV4m*^~*xV+&p;W749xhb_X=$|LD;FHuB&JL5?*Y2-oIT(wYY2;73<^#46S~Gx| z^cez%V7x$81}UWqS13Gz80379Rj;6~WdiXWOSsdmzY39L;Hg3MH43o*y8ibNBBH`(av4|u;YPq%{R;IuYow<+GEsf@R?=@tT@!}?#>zIIn0CoyV!hq3mw zHj>OOjfJM3F{RG#6ujzo?y32m^tgSXf@v=J$ELdJ+=5j|=F-~hP$G&}tDZsZE?5rX ztGj`!S>)CFmdkccxM9eGIcGnS2AfK#gXwj%esuIBNJQP1WV~b~+D7PJTmWGTSDrR` zEAu4B8l>NPuhsk5a`rReSya2nfV1EK01+G!x8aBdTs3Io$u5!6n6KX%uv@DxAp3F@{4UYg4SWJtQ-W~0MDb|j-$lwVn znAm*Pl!?Ps&3wO=R115RWKb*JKoexo*)uhhHBncEDMSVa_PyA>k{Zm2(wMQ(5NM3# z)jkza|GoWEQo4^s*wE(gHz?Xsg4`}HUAcs42cM1-qq_=+=!Gk^y710j=66(cSWqUe zklbm8+zB_syQv5A2rj!Vbw8;|$@C!vfNmNV!yJIWDQ>{+2x zKjuFX`~~HKG~^6h5FntRpnnHt=D&rq0>IJ9#F0eM)Y-)GpRjiN7gkA8wvnG#K=q{q z9dBn8_~wm4J<3J_vl|9H{7q6u2A!cW{bp#r*-f{gOV^e=8S{nc1DxMHFwuM$;aVI^ zz6A*}m8N-&x8;aunp1w7_vtB*pa+OYBw=TMc6QK=mbA-|Cf* zvyh8D4LRJImooUaSb7t*fVfih<97Gf@VE0|z>NcBwBQze);Rh!k3K_sfunToZY;f2 z^HmC4KjHRVg+eKYj;PRN^|E0>Gj_zagfRbrki68I^#~6-HaHg3BUW%+clM1xQEdPYt_g<2K+z!$>*$9nQ>; zf9Bei{?zY^-e{q_*|W#2rJG`2fy@{%6u0i_VEWTq$*(ZN37|8lFFFt)nCG({r!q#9 z5VK_kkSJ3?zOH)OezMT{!YkCuSSn!K#-Rhl$uUM(bq*jY? zi1xbMVthJ`E>d>(f3)~fozjg^@eheMF6<)I`oeJYx4*+M&%c9VArn(OM-wp%M<-`x z7sLP1&3^%Nld9Dhm@$3f2}87!quhI@nwd@3~fZl_3LYW-B?Ia>ui`ELg z&Qfe!7m6ze=mZ`Ia9$z|ARSw|IdMpooY4YiPN8K z4B(ts3p%2i(Td=tgEHX z0UQ_>URBtG+-?0E;E7Ld^dyZ;jjw0}XZ(}-QzC6+NN=40oDb2^v!L1g9xRvE#@IBR zO!b-2N7wVfLV;mhEaXQ9XAU+>=XVA6f&T4Z-@AX!leJ8obP^P^wP0aICND?~w&NykJ#54x3_@r7IDMdRNy4Hh;h*!u(Ol(#0bJdwEo$5437-UBjQ+j=Ic>Q2z` zJNDf0yO6@mr6y1#n3)s(W|$iE_i8r@Gd@!DWDqZ7J&~gAm1#~maIGJ1sls^gxL9LLG_NhU!pTGty!TbhzQnu)I*S^54U6Yu%ZeCg`R>Q zhBv$n5j0v%O_j{QYWG!R9W?5_b&67KB$t}&e2LdMvd(PxN6Ir!H4>PNlerpBL>Zvyy!yw z-SOo8caEpDt(}|gKPBd$qND5#a5nju^O>V&;f890?yEOfkSG^HQVmEbM3Ugzu+UtH zC(INPDdraBN?P%kE;*Ae%Wto&sgw(crfZ#Qy(<4nk;S|hD3j{IQRI6Yq|f^basLY; z-HB&Je%Gg}Jt@={_C{L$!RM;$$|iD6vu#3w?v?*;&()uB|I-XqEKqZPS!reW9JkLewLb!70T7n`i!gNtb1%vN- zySZj{8-1>6E%H&=V}LM#xmt`J3XQoaD|@XygXjdZ1+P77-=;=eYpoEQ01B@L*a(uW zrZeZz?HJsw_4g0vhUgkg@VF8<-X$B8pOqCuWAl28uB|@r`19DTUQQsb^pfqB6QtiT z*`_UZ`fT}vtUY#%sq2{rchyfu*pCg;uec2$-$N_xgjZcoumE5vSI{+s@iLWoz^Mf; zuI8kDP{!XY6OP~q5}%1&L}CtfH^N<3o4L@J@zg1-mt{9L`s^z$Vgb|mr{@WiwAqKg zp#t-lhrU>F8o0s1q_9y`gQNf~Vb!F%70f}$>i7o4ho$`uciNf=xgJ>&!gSt0g;M>*x4-`U)ysFW&Vs^Vk6m%?iuWU+o&m(2Jm26Y(3%TL; zA7T)BP{WS!&xmxNw%J=$MPfn(9*^*TV;$JwRy8Zl*yUZi8jWYF>==j~&S|Xinsb%c z2?B+kpet*muEW7@AzjBA^wAJBY8i|#C{WtO_or&Nj2{=6JTTX05}|H>N2B|Wf!*3_ z7hW*j6p3TvpghEc6-wufFiY!%-GvOx*bZrhZu+7?iSrZL5q9}igiF^*R3%DE4aCHZ zqu>xS8LkW+Auv%z-<1Xs92u23R$nk@Pk}MU5!gT|c7vGlEA%G^2th&Q*zfg%-D^=f z&J_}jskj|Q;73NP4<4k*Y%pXPU2Thoqr+5uH1yEYM|VtBPW6lXaetokD0u z9qVek6Q&wk)tFbQ8(^HGf3Wp16gKmr>G;#G(HRBx?F`9AIRboK+;OfHaLJ(P>IP0w zyTbTkx_THEOs%Q&aPrxbZrJlio+hCC_HK<4%f3ZoSAyG7Dn`=X=&h@m*|UYO-4Hq0 z-Bq&+Ie!S##4A6OGoC~>ZW`Y5J)*ouaFl_e9GA*VSL!O_@xGiBw!AF}1{tB)z(w%c zS1Hmrb9OC8>0a_$BzeiN?rkPLc9%&;1CZW*4}CDDNr2gcl_3z+WC15&H1Zc2{o~i) z)LLW=WQ{?ricmC`G1GfJ0Yp4Dy~Ba;j6ZV4r{8xRs`13{dD!xXmr^Aga|C=iSmor% z8hi|pTXH)5Yf&v~exp3o+sY4B^^b*eYkkCYl*T{*=-0HniSA_1F53eCb{x~1k3*`W zr~};p1A`k{1DV9=UPnLDgz{aJH=-LQo<5%+Em!DNN252xwIf*wF_zS^!(XSm(9eoj z=*dXG&n0>)_)N5oc6v!>-bd(2ragD8O=M|wGW z!xJQS<)u70m&6OmrF0WSsr@I%T*c#Qo#Ha4d3COcX+9}hM5!7JIGF>7<~C(Ear^Sn zm^ZFkV6~Ula6+8S?oOROOA6$C&q&dp`>oR-2Ym3(HT@O7Sd5c~+kjrmM)YmgPH*tL zX+znN>`tv;5eOfX?h{AuX^LK~V#gPCu=)Tigtq9&?7Xh$qN|%A$?V*v=&-2F$zTUv z`C#WyIrChS5|Kgm_GeudCFf;)!WH7FI60j^0o#65o6`w*S7R@)88n$1nrgU(oU0M9 zx+EuMkC>(4j1;m6NoGqEkpJYJ?vc|B zOlwT3t&UgL!pX_P*6g36`ZXQ; z9~Cv}ANFnJGp(;ZhS(@FT;3e)0)Kp;h^x;$*xZn*k0U6-&FwI=uOGaODdrsp-!K$Ac32^c{+FhI-HkYd5v=`PGsg%6I`4d9Jy)uW0y%) zm&j^9WBAp*P8#kGJUhB!L?a%h$hJgQrx!6KCB_TRo%9{t0J7KW8!o1B!NC)VGLM5! zpZy5Jc{`r{1e(jd%jsG7k%I+m#CGS*BPA65ZVW~fLYw0dA-H_}O zrkGFL&P1PG9p2(%QiEWm6x;U-U&I#;Em$nx-_I^wtgw3xUPVVu zqSuKnx&dIT-XT+T10p;yjo1Y)z(x1fb8Dzfn8e yu?e%!_ptzGB|8GrCfu%p?(_ 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 TZiJi*F`$tkWLn) literal 63375 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≫L+crb^Bz zc+Nw%zgpZ6>!A3%lau!Pw6`Y#WPVBtAfKSsqwYDWQK-~ zz(mx=nJ6-8t`YXB{6gaZ%G}Dmn&o500Y}2Rd?e&@=hBEmB1C=$OMBfxX__2c2O4K2#(0ksclP$SHp*8jq-1&(<6(#=6&H`Nlc2RVC4->r6U}sTY<1? zn@tv7XwUs-c>Lcmrm5AE0jHI5={WgHIow6cX=UK)>602(=arbuAPZ37;{HTJSIO%9EL`Et5%J7$u_NaC(55x zH^qX^H}*RPDx)^c46x>js=%&?y?=iFs^#_rUl@*MgLD92E5y4B7#EDe9yyn*f-|pQ zi>(!bIg6zY5fLSn@;$*sN|D2A{}we*7+2(4&EhUV%Qqo5=uuN^xt_hll7=`*mJq6s zCWUB|s$)AuS&=)T&_$w>QXHqCWB&ndQ$y4-9fezybZb0bYD^zeuZ>WZF{rc>c4s`` zgKdppTB|o>L1I1hAbnW%H%EkFt%yWC|0~+o7mIyFCTyb?@*Ho)eu(x`PuO8pLikN> z6YeI`V?AUWD(~3=8>}a6nZTu~#QCK(H0+4!ql3yS`>JX;j4+YkeG$ZTm33~PLa3L} zksw7@%e-mBM*cGfz$tS4LC^SYVdBLsR}nAprwg8h2~+Cv*W0%izK+WPVK}^SsL5R_ zpA}~G?VNhJhqx2he2;2$>7>DUB$wN9_-adL@TqVLe=*F8Vsw-yho@#mTD6*2WAr6B zjtLUh`E(;#p0-&$FVw(r$hn+5^Z~9J0}k;j$jL1;?2GN9s?}LASm?*Rvo@?E+(}F& z+=&M-n`5EIz%%F^e)nnWjkQUdG|W^~O|YeY4Fz}>qH2juEere}vN$oJN~9_Th^&b{ z%IBbET*E8%C@jLTxV~h#mxoRrJCF{!CJOghjuKOyl_!Jr?@4Upo7u>fTGtfm|CH2v z&9F+>;6aFbYXLj3{yZ~Yn1J2%!)A3~j2$`jOy{XavW@t)g}}KUVjCWG0OUc7aBc=2 zR3^u=dT47=5SmT{K1aGaVZkOx|24T-J0O$b9dfB25J|7yb6frwS6wZ1^y%EWOm}S< zc1SdYhfsdLG*FB-;!QLV3D!d~hnXTGVQVck9x%=B(Kk8c3y%f0nR95_TbY;l=obSl zEE@fp0|8Q$b3(+DXh?d0FEloGhO0#11CLQT5qtEckBLe-VN-I>9ys}PVK0r;0!jIG zH_q$;a`3Xv9P_V2ekV1SMzd#SKo<1~Dq2?M{(V;AwhH_2x@mN$=|=cG0<3o^j_0OF z7|WJ-f2G=7sA4NVGU2X5`o*D2T7(MbmZ2(oipooE{R?9!{WxX!%ofhsrPAxoIk!Kr z>I$a{Zq=%KaLrDCIL^gmA3z{2z%Wkr)b$QHcNUA^QwydWMJmxymO0QS22?mo%4(Md zgME(zE}ub--3*wGjV`3eBMCQG-@Gel1NKZDGuqobN|mAt0{@ZC9goI|BSmGBTUZ(`Xt z^e2LiMg?6E?G*yw(~K8lO(c4)RY7UWxrXzW^iCg-P41dUiE(i+gDmmAoB?XOB}+Ln z_}rApiR$sqNaT4frw69Wh4W?v(27IlK$Toy<1o)GeF+sGzYVeJ`F)3`&2WDi^_v67 zg;@ehwl3=t+}(DJtOYO!s`jHyo-}t@X|U*9^sIfaZfh;YLqEFmZ^E;$_XK}%eq;>0 zl?+}*kh)5jGA}3daJ*v1knbW0GusR1+_xD`MFPZc3qqYMXd>6*5?%O5pC7UVs!E-` zuMHc6igdeFQ`plm+3HhP)+3I&?5bt|V8;#1epCsKnz0%7m9AyBmz06r90n~9o;K30 z=fo|*`Qq%dG#23bVV9Jar*zRcV~6fat9_w;x-quAwv@BkX0{9e@y0NB(>l3#>82H6 z^US2<`=M@6zX=Pz>kb8Yt4wmeEo%TZ=?h+KP2e3U9?^Nm+OTx5+mVGDvgFee%}~~M zK+uHmj44TVs}!A}0W-A92LWE%2=wIma(>jYx;eVB*%a>^WqC7IVN9{o?iw{e4c=CG zC#i=cRJZ#v3 zF^9V+7u?W=xCY%2dvV_0dCP%5)SH*Xm|c#rXhwEl*^{Ar{NVoK*H6f5qCSy`+|85e zjGaKqB)p7zKNKI)iWe6A9qkl=rTjs@W1Crh(3G57qdT0w2ig^{*xerzm&U>YY{+fZbkQ#;^<$JniUifmAuEd^_M(&?sTrd(a*cD! zF*;`m80MrZ^> zaF{}rDhEFLeH#`~rM`o903FLO?qw#_Wyb5}13|0agjSTVkSI6Uls)xAFZifu@N~PM zQ%o?$k)jbY0u|45WTLAirUg3Zi1E&=G#LnSa89F3t3>R?RPcmkF}EL-R!OF_r1ZN` z?x-uHH+4FEy>KrOD-$KHg3$-Xl{Cf0;UD4*@eb~G{CK-DXe3xpEEls?SCj^p z$Uix(-j|9f^{z0iUKXcZQen}*`Vhqq$T?^)Ab2i|joV;V-qw5reCqbh(8N)c%!aB< zVs+l#_)*qH_iSZ_32E~}>=wUO$G_~k0h@ch`a6Wa zsk;<)^y=)cPpHt@%~bwLBy;>TNrTf50BAHUOtt#9JRq1ro{w80^sm-~fT>a$QC;<| zZIN%&Uq>8`Js_E((_1sewXz3VlX|-n8XCfScO`eL|H&2|BPZhDn}UAf_6s}|!XpmUr90v|nCutzMjb9|&}#Y7fj_)$alC zM~~D6!dYxhQof{R;-Vp>XCh1AL@d-+)KOI&5uKupy8PryjMhTpCZnSIQ9^Aq+7=Mb zCYCRvm4;H=Q8nZWkiWdGspC_Wvggg|7N`iED~Eap)Th$~wsxc(>(KI>{i#-~Dd8iQ zzonqc9DW1w4a*}k`;rxykUk+~N)|*I?@0901R`xy zN{20p@Ls<%`1G1Bx87Vm6Z#CA`QR(x@t8Wc?tpaunyV^A*-9K9@P>hAWW9Ev)E$gb z<(t?Te6GcJX2&0% z403pe>e)>m-^qlJU^kYIH)AutgOnq!J>FoMXhA-aEx-((7|(*snUyxa+5$wx8FNxS zKuVAVWArlK#kDzEM zqR?&aXIdyvxq~wF?iYPho*(h?k zD(SBpRDZ}z$A})*Qh!9&pZZRyNixD!8)B5{SK$PkVET(yd<8kImQ3ILe%jhx8Ga-1 zE}^k+Eo^?c4Y-t2_qXiVwW6i9o2qosBDj%DRPNT*UXI0=D9q{jB*22t4HHcd$T&Xi zT=Vte*Gz2E^qg%b7ev04Z&(;=I4IUtVJkg<`N6i7tjUn-lPE(Y4HPyJKcSjFnEzCH zPO(w%LmJ_=D~}PyfA91H4gCaf-qur3_KK}}>#9A}c5w@N;-#cHph=x}^mQ3`oo`Y$ope#)H9(kQK zGyt<7eNPuSAs$S%O>2ElZ{qtDIHJ!_THqTwcc-xfv<@1>IJ;YTv@!g-zDKBKAH<

Zet1e^8c}8fE97XH}+lF{qbF<`Y%dU|I!~Y`ZrVfKX82i z)(%!Tcf~eE^%2_`{WBPGPU@1NB5SCXe1sAI<4&n1IwO{&S$ThWn37heGOSW%nW7*L zxh0WK!E7zh%6yF-7%~l@I~b`2=*$;RYbi(I#zp$gL_d39U4A)KuB( zcS0bt48&%G_I~( zL(}w&2NA6#$=|g)J+-?ehHflD^lr77ngdz=dszFI;?~ZxeJv=gsm?4$$6#V==H{fa zqO!EkT>1-OQSJoX)cN}XsB;shvrHRwTH(I2^Ah4|rizn!V7T7fLh~Z<`Q+?zEMVxh z$=-x^RR*PlhkV_8mshTvs+zmZWY&Jk{9LX0Nx|+NAEq-^+Rh|ZlinVZ=e8=`WQt;e@= zPU}^1cG*O;G7l{Y#nl znp`y%CO_SC7gk0i0gY&phM04Y)~vU0!3$V$2T+h(1ZS+cCgc zaC?3M;B48^faGo>h~--#FNFauH?0BJJ6_nG5qOlr>k~%DCSJaOfl%KWHusw>tGrTxAhlEVDxc8R2C-)LCt&$Rt9IKor=ml7jirX@?WW+M z^I{b}MD5r$s>^^sN@&g`cXD~S_u09xo;{;noKZatIuzqd zW1e7oTl9>g8opPBT(p+&fo0F#!c{NFYYpIZ6u8hOB{F#{nP)@})X20$3iJtG$cO zJ$Oxl_qH{sL5d?=D$2M4C3Ajc;GN0(B-HVT;@pJ-LvIrN%|SY?t}g!J>ufQrR%hoY z!nr$tq~N%)9}^tEip93XW=MQ1@XovSvn`PTqXeT9@_7hGv4%LK1M**Q%UKi|(v@1_ zKGe*@+1%Y4v&`;5vUL`C&{tc+_7HFs7*OtjY8@Gg`C4O&#An{0xOvgNSehTHS~_1V z=daxCMzI5b_ydM5$z zZl`a{mM}i@x;=QyaqJY&{Q^R*^1Yzq!dHH~UwCCga+Us~2wk59ArIYtSw9}tEmjbo z5!JA=`=HP*Ae~Z4Pf7sC^A3@Wfa0Ax!8@H_&?WVe*)9B2y!8#nBrP!t1fqhI9jNMd zM_5I)M5z6Ss5t*f$Eh{aH&HBeh310Q~tRl3wCEcZ>WCEq%3tnoHE)eD=)XFQ7NVG5kM zaUtbnq2LQomJSWK)>Zz1GBCIHL#2E>T8INWuN4O$fFOKe$L|msB3yTUlXES68nXRX zP6n*zB+kXqqkpQ3OaMc9GqepmV?Ny!T)R@DLd`|p5ToEvBn(~aZ%+0q&vK1)w4v0* zgW44F2ixZj0!oB~^3k|vni)wBh$F|xQN>~jNf-wFstgiAgB!=lWzM&7&&OYS=C{ce zRJw|)PDQ@3koZfm`RQ$^_hEN$GuTIwoTQIDb?W&wEo@c75$dW(ER6q)qhF`{#7UTuPH&)w`F!w z0EKs}=33m}_(cIkA2rBWvApydi0HSOgc>6tu&+hmRSB%)s`v_NujJNhKLS3r6hv~- z)Hm@?PU{zd0Tga)cJWb2_!!9p3sP%Z zAFT|jy;k>4X)E>4fh^6=SxV5w6oo`mus&nWo*gJL zZH{SR!x)V)y=Qc7WEv-xLR zhD4OcBwjW5r+}pays`o)i$rcJb2MHLGPmeOmt5XJDg@(O3PCbxdDn{6qqb09X44T zh6I|s=lM6Nr#cGaA5-eq*T=LQ6SlRq*`~`b+dVi5^>el1p;#si6}kK}>w;1 z6B1dz{q_;PY{>DBQ+v@1pfXTd5a*^H9U*;qdj@XBF}MoSSQxVXeUpEM5Z0909&8$pRfR|B(t0ox&xl8{8mUNd#(zWONW{oycv$VjP1>q;jU@ z@+8E~fjz*I54OFFaQ{A5jn1w>r;l!NRlI(8q3*%&+tM?lov_G3wB`<}bQ>1=&xUht zmti5VZzV1Cx006Yzt|%Vwid>QPX8Nfa8|sue7^un@C+!3h!?-YK>lSfNIHh|0kL8v zbv_BklQ4HOqje|@Fyxn%IvL$N&?m(KN;%`I$N|muStjSsgG;gP4Smgz$2u(mG;DXP zf~uQ z212x^l6!MW>V@ORUGSFLAAjz3i5zO$=UmD_zhIk2OXUz^LkDLWjla*PW?l;`LLos> z7FBvCr)#)XBByDm(=n%{D>BcUq>0GOV9`i-(ZSI;RH1rdrAJ--f0uuAQ4odl z_^$^U_)0BBJwl@6R#&ZtJN+@a(4~@oYF)yG+G#3=)ll8O#Zv3SjV#zSXTW3h9kqn* z@AHL=vf~KMas}6{+u=}QFumr-!c=(BFP_dwvrdehzTyqco)m@xRc=6b#Dy+KD*-Bq zK=y*1VAPJ;d(b?$2cz{CUeG(0`k9_BIuUki@iRS5lp3=1#g)A5??1@|p=LOE|FNd; z-?5MLKd-5>yQ7n__5W^3C!_`hP(o%_E3BKEmo1h=H(7;{6$XRRW6{u+=oQX<((xAJ zNRY`Egtn#B1EBGHLy^eM5y}Jy0h!GAGhb7gZJoZI-9WuSRw)GVQAAcKd4Qm)pH`^3 zq6EIM}Q zxZGx%aLnNP1an=;o8p9+U^>_Bi`e23E^X|}MB&IkS+R``plrRzTE%ncmfvEW#AHJ~ znmJ`x&ez6eT21aLnoI`%pYYj zzQ?f^ob&Il;>6Fe>HPhAtTZa*B*!;;foxS%NGYmg!#X%)RBFe-acahHs3nkV61(E= zhekiPp1d@ACtA=cntbjuv+r-Zd`+lwKFdqZuYba_ey`&H<Psu;Tzwt;-LQxvv<_D5;ik7 zwETZe`+voUhk%$s2-7Rqfl`Ti_{(fydI(DAHKr<66;rYa6p8AD+NEc@Fd@%m`tiK% z=Mebzrtp=*Q%a}2UdK4J&5#tCN5PX>W=(9rUEXZ8yjRu+7)mFpKh{6;n%!bI(qA9kfyOtstGtOl zX!@*O0fly*L4k##fsm&V0j9Lj<_vu1)i?!#xTB7@2H&)$Kzt@r(GH=xRZlIimTDd_o(%9xO388LwC#;vQ?7OvRU_s< zDS@6@g}VnvQ+tn(C#sx0`J^T4WvFxYI17;uPs-Ub{R`J-NTdtBGl+Q>e81Z3#tDUr ztnVc*p{o|RNnMYts4pdw=P!uJkF@8~h)oV4dXu5F7-j0AW|=mt!QhP&ZV!!82*c7t zuOm>B*2gFtq;A8ynZ~Ms?!gEi5<{R_8tRN%aGM!saR4LJQ|?9w>Ff_61(+|ol_vL4 z-+N>fushRbkB4(e{{SQ}>6@m}s1L!-#20N&h%srA=L50?W9skMF9NGfQ5wU*+0<@> zLww8%f+E0Rc81H3e_5^DB@Dn~TWYk}3tqhO{7GDY;K7b*WIJ-tXnYM@z4rn(LGi?z z8%$wivs)fC#FiJh?(SbH-1bgdmHw&--rn7zBWe1xAhDdv#IRB@DGy}}zS%M0(F_3_ zLb-pWsdJ@xXE;=tpRAw?yj(Gz=i$;bsh&o2XN%24b6+?_gJDBeY zws3PE2u!#Cec>aFMk#ECxDlAs;|M7@LT8)Y4(`M}N6IQ{0YtcA*8e42!n^>`0$LFU zUCq2IR2(L`f++=85M;}~*E($nE&j;p{l%xchiTau*tB9bI= zn~Ygd@<+9DrXxoGPq}@vI1Q3iEfKRleuy*)_$+hg?+GOgf1r?d@Or42|s|D>XMa;ebr1uiTNUq@heusd6%WwJqyCCv!L*qou9l!B22H$bQ z)<)IA>Yo77S;|`fqBk!_PhLJEQb0wd1Z|`pCF;hol!34iQYtqu3K=$QxLW7(HFx~v>`vVRr zyqk^B4~!3F8t8Q_D|GLRrAbbQDf??D&Jd|mgw*t1YCd)CM2$76#Cqj1bD*vADwavp zS<`n@gLU4pwCqNPsIfHKl{5}gu9t-o+O< z??!fMqMrt$s}02pdBbOScUrc1T*{*-ideR6(1q4@oC6mxg8v8Y^h^^hfx6| z|Mld6Ax1CuSlmSJmHwdOix?$8emihK#&8&}u8m!#T1+c5u!H)>QW<7&R$eih)xkov zHvvEIJHbkt+2KQ<-bMR;2SYX?8SI=_<-J!GD5@P2FJ}K z5u82YFotCJF(dUeJFRX_3u8%iIYbRS??A?;iVO?84c}4Du9&jG<#urlZ_Unrcg8dR z!5I3%9F*`qwk#joKG_Q%5_xpU7|jm4h0+l$p;g%Tr>i74#3QnMXdz|1l2MQN$yw|5 zThMw15BxjWf2{KM)XtZ+e#N)ihlkxPe=5ymT9>@Ym%_LF}o z1XhCP`3E1A{iVoHA#|O|&5=w;=j*Qf`;{mBAK3={y-YS$`!0UmtrvzHBfR*s{z<0m zW>4C=%N98hZlUhwAl1X`rR)oL0&A`gv5X79??p_==g*n4$$8o5g9V<)F^u7v0Vv^n z1sp8{W@g6eWv2;A31Rhf5j?KJhITYfXWZsl^`7z`CFtnFrHUWiD?$pwU6|PQjs|7RA0o9ARk^9$f`u3&C|#Z3iYdh<0R`l2`)6+ z6tiDj@xO;Q5PDTYSxsx6n>bj+$JK8IPJ=U5#dIOS-zwyK?+t^V`zChdW|jpZuReE_ z)e~ywgFe!0q|jzsBn&(H*N`%AKpR@qM^|@qFai0};6mG_TvXjJ`;qZ{lGDZHScZk( z>pO+%icp)SaPJUwtIPo1BvGyP8E@~w2y}=^PnFJ$iHod^JH%j1>nXl<3f!nY9K$e` zq-?XYl)K`u*cVXM=`ym{N?z=dHQNR23M8uA-(vsA$6(xn+#B-yY!CB2@`Uz({}}w+ z0sni*39>rMC!Ay|1B@;al%T&xE(wCf+`3w>N)*LxZZZYi{5sqiVWgbNd>W*X?V}C- zjQ4F7e_uCUOHbtewQkq?m$*#@ZvWbu{4i$`aeKM8tc^ zL5!GL8gX}c+qNUtUIcps1S)%Gsx*MQLlQeoZz2y2OQb(A73Jc3`LmlQf0N{RTt;wa`6h|ljX1V7UugML=W5-STDbeWTiEMjPQ$({hn_s&NDXzs6?PLySp$?L`0ilH3vCUO{JS0Dp`z;Ry$6}R@1NdY7rxccbm$+;ApSe=2q!0 z()3$vYN0S$Cs)#-OBs{_2uFf}L4h$;7^2w20=l%5r9ui&pTEgg4U!FoCqyA6r2 zC5s72l}i*9y|KTjDE5gVlYe4I2gGZD)e`Py2gq7cK4at{bT~DSbQQ4Z4sl)kqXbbr zqvXtSqMrDdT2qt-%-HMoqeFEMsv~u)-NJ%Z*ipSJUm$)EJ+we|4*-Mi900K{K|e0; z1_j{X5)a%$+vM7;3j>skgrji92K1*Ip{SfM)=ob^E374JaF!C(cZ$R_E>Wv+?Iy9M z?@`#XDy#=z%3d9&)M=F8Xq5Zif%ldIT#wrlw(D_qOKo4wD(fyDHM5(wm1%7hy6euJ z%Edg!>Egs;ZC6%ktLFtyN0VvxN?*4C=*tOEw`{KQvS7;c514!FP98Nf#d#)+Y-wsl zP3N^-Pnk*{o(3~m=3DX$b76Clu=jMf9E?c^cbUk_h;zMF&EiVz*4I(rFoaHK7#5h0 zW7CQx+xhp}Ev+jw;SQ6P$QHINCxeF8_VX=F3&BWUd(|PVViKJl@-sYiUp@xLS2NuF z8W3JgUSQ&lUp@2E(7MG`sh4X!LQFa6;lInWqx}f#Q z4xhgK1%}b(Z*rZn=W{wBOe7YQ@1l|jQ|9ELiXx+}aZ(>{c7Ltv4d>PJf7f+qjRU8i%XZZFJkj&6D^s;!>`u%OwLa*V5Js9Y$b-mc!t@{C415$K38iVu zP7!{3Ff%i_e!^LzJWhBgQo=j5k<<($$b&%%Xm_f8RFC_(97&nk83KOy@I4k?(k<(6 zthO$3yl&0x!Pz#!79bv^?^85K5e7uS$ zJ33yka2VzOGUhQXeD{;?%?NTYmN3{b0|AMtr(@bCx+c=F)&_>PXgAG}4gwi>g82n> zL3DlhdL|*^WTmn;XPo62HhH-e*XIPSTF_h{#u=NY8$BUW=5@PD{P5n~g5XDg?Fzvb_u ziK&CJqod4srfY2T?+4x@)g9%3%*(Q2%YdCA3yM{s=+QD0&IM`8k8N&-6%iIL3kon> z0>p3BUe!lrz&_ZX2FiP%MeuQY-xVV%K?=bGPOM&XM0XRd7or< zy}jn_eEzuQ>t2fM9ict#ZNxD7HUycsq76IavfoNl$G1|t*qpUSX;YgpmJrr_8yOJ2 z(AwL;Ugi{gJ29@!G-mD82Z)46T`E+s86Qw|YSPO*OoooraA!8x_jQXYq5vUw!5f_x zubF$}lHjIWxFar8)tTg8z-FEz)a=xa`xL~^)jIdezZsg4%ePL$^`VN#c!c6`NHQ9QU zkC^<0f|Ksp45+YoX!Sv>+57q}Rwk*2)f{j8`d8Ctz^S~me>RSakEvxUa^Pd~qe#fb zN7rnAQc4u$*Y9p~li!Itp#iU=*D4>dvJ{Z~}kqAOBcL8ln3YjR{Sp!O`s=5yM zWRNP#;2K#+?I&?ZSLu)^z-|*$C}=0yi7&~vZE$s``IE^PY|dj^HcWI$9ZRm>3w(u` z-1%;;MJbzHFNd^!Ob!^PLO-xhhj@XrI81Y)x4@FdsI( za`o4Gy(`T$P?PB?s>o+eIOtuirMykbuAi65Y_UN1(?jTCy@J8Px`%;bcNmPm#Fr!= z5V!YViFJ!FBfEq>nJFk0^RAV1(7w+X`HRgP;nJHJdMa!}&vvduCMoslwHTes_I76|h>;(-9lbfGnt zoZomakOt759AuTX4b$)G8TzJ&m*BV8!vMs9#=e0tWa z%)84R=3?tfh72~=Rc;fXwj+x z+25xapYK@2@;}6)@8IL+F6iuJ_B{&A-0=U=U6WMbY>~ykVFp$XkH)f**b>TE5)shN z39E2L@JPCSl!?pkvFeh@6dCv9oE}|{GbbVM!XIgByN#md&tXy@>QscU0#z!I&X4;d z&B&ZA4lbrHJ!x4lCN4KC-)u#gT^cE{Xnhu`0RXVKn|j$vz8m}v^%*cQ{(h%FW8_8a zFM{$PirSI8@#*xg2T){A+EKX(eTC66Fb})w{vg%Vw)hvV-$tttI^V5wvU?a{(G}{G z@ob7Urk1@hDN&C$N!Nio9YrkiUC{5qA`KH*7CriaB;2~2Od>2l=WytBRl#~j`EYsj}jqK2xD*3 ztEUiPZzEJC??#Tj^?f)=sRXOJ_>5aO(|V#Yqro05p6)F$j5*wYr1zz|T4qz$0K(5! zr`6Pqd+)%a9Xq3aNKrY9843)O56F%=j_Yy_;|w8l&RU1+B4;pP*O_}X8!qD?IMiyT zLXBOOPg<*BZtT4LJ7DfyghK|_*mMP7a1>zS{8>?}#_XXaLoUBAz(Wi>$Q!L;oQ&cL z6O|T6%Dxq3E35$0g5areq9$2+R(911!Z9=wRPq-pju7DnN9LAfOu3%&onnfx^Px5( zT2^sU>Y)88F5#ATiVoS$jzC-M`vY8!{8#9O#3c&{7J1lo-rcNK7rlF0Zt*AKE(WN* z*o?Tv?Sdz<1v6gfCok8MG6Pzecx9?C zrQG5j^2{V556Hj=xTiU-seOCr2ni@b<&!j>GyHbv!&uBbHjH-U5Ai-UuXx0lcz$D7%=! z&zXD#Jqzro@R=hy8bv>D_CaOdqo6)vFjZldma5D+R;-)y1NGOFYqEr?h zd_mTwQ@K2veZTxh1aaV4F;YnaWA~|<8$p}-eFHashbWW6Dzj=3L=j-C5Ta`w-=QTw zA*k9!Ua~-?eC{Jc)xa;PzkUJ#$NfGJOfbiV^1au;`_Y8|{eJ(~W9pP9q?gLl5E6|e{xkT@s|Ac;yk01+twk_3nuk|lRu{7-zOjLAGe!)j?g+@-;wC_=NPIhk(W zfEpQrdRy z^Q$YBs%>$=So>PAMkrm%yc28YPi%&%=c!<}a=)sVCM51j+x#<2wz?2l&UGHhOv-iu z64x*^E1$55$wZou`E=qjP1MYz0xErcpMiNYM4+Qnb+V4MbM;*7vM_Yp^uXUuf`}-* z_2CnbQ);j5;Rz?7q)@cGmwE^P>4_u9;K|BFlOz_|c^1n~%>!uO#nA?5o4A>XLO{X2 z=8M%*n=IdnXQ}^+`DXRKM;3juVrXdgv79;E=ovQa^?d7wuw~nbu%%lsjUugE8HJ9zvZIM^nWvjLc-HKc2 zbj{paA}ub~4N4Vw5oY{wyop9SqPbWRq=i@Tbce`r?6e`?`iOoOF;~pRyJlKcIJf~G z)=BF$B>YF9>qV#dK^Ie#{0X(QPnOuu((_-u?(mxB7c9;LSS-DYJ8Wm4gz1&DPQ8;0 z=Wao(zb1RHXjwbu_Zv<=9njK28sS}WssjOL!3-E5>d17Lfnq0V$+IU84N z-4i$~!$V-%Ik;`Z3MOqYZdiZ^3nqqzIjLE+zpfQC+LlomQu-uNCStj%MsH(hsimN# z%l4vpJBs_2t7C)x@6*-k_2v0FOk<1nIRO3F{E?2DnS}w> z#%9Oa{`RB5FL5pKLkg59#x~)&I7GzfhiVC@LVFSmxZuiRUPVW*&2ToCGST0K`kRK) z02#c8W{o)w1|*YmjGSUO?`}ukX*rHIqGtFH#!5d1Jd}&%4Kc~Vz`S7_M;wtM|6PgI zNb-Dy-GI%dr3G3J?_yBX#NevuYzZgzZ!vN>$-aWOGXqX!3qzCIOzvA5PLC6GLIo|8 zQP^c)?NS29hPmk5WEP>cHV!6>u-2rR!tit#F6`_;%4{q^6){_CHGhvAs=1X8Fok+l zt&mk>{4ARXVvE-{^tCO?inl{)o}8(48az1o=+Y^r*AIe%0|{D_5_e>nUu`S%zR6|1 zu0$ov7c`pQEKr0sIIdm7hm{4K_s0V%M-_Mh;^A0*=$V9G1&lzvN9(98PEo=Zh$`Vj zXh?fZ;9$d!6sJRSjTkOhb7@jgSV^2MOgU^s2Z|w*e*@;4h?A8?;v8JaLPCoKP_1l- z=Jp0PYDf(d2Z`;O7mb6(_X_~z0O2yq?H`^c=h|8%gfywg#}wIyv&_uW{-e8e)YmGR zI0NNSDoJWa%0ztGzkwl>IYW*DesPRY?oH+ow^(>(47XUm^F`fAa0B~ja-ae$e>4-A z64lb_;|W0ppKI+ zxu2VLZzv4?Mr~mi?WlS-1L4a^5k+qb5#C)ktAYGUE1H?Vbg9qsRDHAvwJUN=w~AuT zUXYioFg2Dx-W)}w9VdFK#vpjoSc!WcvRZ_;TgHu;LSY*i7K_>Px{%C4-IL?6q?Qa_ zL7l=EEo|@X&$gX;fYP02qJF~LN9?E-OL2G(Fo4hW)G{`qnW zTIuc+-1VJvKgph0jAc(LzM);Pg$MPln?U|ek{_5nNJHfm-Y#ec+n#Yf_e>XfbLbN)eqHEDr0#?<;TskL5-0JGv|Ut{=$Xk8hlwbaMXdcI3GL zY-hykR{zX9liy$Z2F3!z346uu%9@-y6Gda`X2*ixlD_P@<}K?AoV?(%lM%* z(xNk=|A()443aGj)-~IDf3J+UA2p2lh6ei^pG*HL#SiThnIr5WZDXebI)F7X zGmP-3bH$i$+(IwqgbM7h%G5oJ@4{Z~qZ#Zs*k7eXJIqg;@0kAGV|b=F#hZs)2BYu1 zr8sj#Zd+Iu^G}|@-dR5S*U-;DqzkX3V0@q-k8&VHW?h0b0?tJ-Atqmg^J8iF7DP6k z)W{g?5~F*$5x?6W)3YKcrNu8%%(DglnzMx5rsU{#AD+WPpRBf``*<8F-x75D$$13U zcaNXYC0|;r&(F@!+E=%+;bFKwKAB$?6R%E_QG5Yn5xX#h+zeI-=mdXD5+D+lEuM`M ze+*G!zX^xbnA?~LnPI=D2`825Ax8rM()i*{G0gcV5MATV?<7mh+HDA7-f6nc@95st zzC_si${|&=$MUj@nLxl_HwEXb2PDH+V?vg zA^DJ%dn069O9TNK-jV}cQKh|$L4&Uh`?(z$}#d+{X zm&=KTJ$+KvLZv-1GaHJm{>v=zXW%NSDr8$0kSQx(DQ)6S?%sWSHUazXSEg_g3agt2@0nyD?A?B%9NYr(~CYX^&U#B4XwCg{%YMYo%e68HVJ7`9KR`mE*Wl7&5t71*R3F>*&hVIaZXaI;2a$?;{Ew{e3Hr1* zbf$&Fyhnrq7^hNC+0#%}n^U2{ma&eS)7cWH$bA@)m59rXlh96piJu@lcKl<>+!1#s zW#6L5Ov%lS(?d66-(n`A%UuiIqs|J|Ulq0RYq-m&RR0>wfA1?<34tI?MBI#a8lY{m z{F2m|A@=`DpZpwdIH#4)9$#H3zr4kn2OX!UE=r8FEUFAwq6VB?DJ8h59z$GXud$#+ zjneIq8uSi&rnG0IR8}UEn5OcZC?@-;$&Ry9hG{-1ta`8aAcOe1|82R7EH`$Qd3sf* zbrOk@G%H7R`j;hOosRVIP_2_-TuyB@rdj?(+k-qQwnhV3niH+CMl>ELX(;X3VzZVJ ztRais0C^L*lmaE(nmhvep+peCqr!#|F?iVagZcL>NKvMS_=*Yl%*OASDl3(mMOY9! z=_J$@nWpA-@><43m4olSQV8(PwhsO@+7#qs@0*1fDj70^UfQ(ORV0N?H{ceLX4<43 zEn)3CGoF&b{t2hbIz;Og+$+WiGf+x5mdWASEWIA*HQ9K9a?-Pf9f1gO6LanVTls)t z^f6_SD|>2Kx8mdQuiJwc_SmZOZP|wD7(_ti#0u=io|w~gq*Odv>@8JBblRCzMKK_4 zM-uO0Ud9>VD>J;zZzueo#+jbS7k#?W%`AF1@ZPI&q%}beZ|ThISf-ly)}HsCS~b^g zktgqOZ@~}1h&x50UQD~!xsW-$K~whDQNntLW=$oZDClUJeSr2$r3}94Wk1>co3beS zoY-7t{rGv|6T?5PNkY zj*XjF()ybvnVz5=BFnLO=+1*jG>E7F%&vm6up*QgyNcJJPD|pHoZ!H6?o3Eig0>-! zt^i-H@bJ;^!$6ZSH}@quF#RO)j>7A5kq4e+7gK=@g;POXcGV28Zv$jybL1J`g@wC# z_DW1ck}3+n@h2LFQhwVfaV@D+-kff4celZC0;0ef?pA#*PPd8Kk8sO1wza&BHQFblVU8P1=-qScHff^^fR zycH!hlHQs7iejITpc4UaBxzqTJ}Z#^lk{W(cr`qtW~Ap;HvuUf#MxgEG?tEU+B?G% znub0I(s@XvI(lva}$Z7<}Qg=rWd5n)}rX{nb+Aw;}?l9LZI-`N-*hts=c6XgjfJs ztp>-686v6ug{glEZ}K=jVG|N1WSWrU*&ue|4Q|O@;s0#L5P*U%Vx;)w7S0ZmLuvwA z@zs2Kut)n1K7qaywO#TbBR`Q~%mdr`V)D`|gN0!07C1!r3{+!PYf9*;h?;dE@#z(k z;o`g~<>P|Sy$ldHTUR3v=_X0Iw6F>3GllrFXVW?gU0q6|ocjd!glA)#f0G7i20ly>qxRljgfO2)RVpvmg#BSrN)GbGsrIb}9 z1t+r;Q>?MGLk#LI5*vR*C8?McB|=AoAjuDk&Pn`KQo z`!|mi{Cz@BGJ!TwMUUTkKXKNtS#OVNxfFI_Gfq3Kpw0`2AsJv9PZPq9x?~kNNR9BR zw#2jp%;FJNoOzW>tE#zskPICp>XSs?|B0E%DaJH)rtLA}$Y>?P+vEOvr#8=pylh zch;H3J`RE1{97O+1(1msdshZx$it^VfM$`-Gw>%NN`K|Tr$0}U`J?EBgR%bg=;et0 z_en)!x`~3so^V9-jffh3G*8Iy6sUq=uFq%=OkYvHaL~#3jHtr4sGM?&uY&U8N1G}QTMdqBM)#oLTLdKYOdOY%{5#Tgy$7QA! zWQmP!Wny$3YEm#Lt8TA^CUlTa{Cpp=x<{9W$A9fyKD0ApHfl__Dz4!HVVt(kseNzV z5Fb`|7Mo>YDTJ>g;7_MOpRi?kl>n(ydAf7~`Y6wBVEaxqK;l;}6x8(SD7}Tdhe2SR zncsdn&`eI}u}@^~_9(0^r!^wuKTKbs-MYjXy#-_#?F=@T*vUG@p4X+l^SgwF>TM}d zr2Ree{TP5x@ZtVcWd3++o|1`BCFK(ja-QP?zj6=ZOq)xf$CfSv{v;jCcNt4{r8f+m zz#dP|-~weHla%rsyYhB_&LHkwuj83RuCO0p;wyXsxW5o6{)zFAC~2%&NL? z=mA}szjHKsVSSnH#hM|C%;r0D$7)T`HQ1K5vZGOyUbgXjxD%4xbs$DAEz)-;iO?3& zXcyU*Z8zm?pP}w&9ot_5I;x#jIn^Joi5jBDOBP1)+p@G1U)pL6;SIO>Nhw?9St2UN zMedM(m(T6bNcPPD`%|9dvXAB&IS=W4?*7-tqldqALH=*UapL!4`2TM_{`W&pm*{?| z0DcsaTdGA%RN={Ikvaa&6p=Ux5ycM){F1OgOh(^Yk-T}a5zHH|=%Jk)S^vv9dY~`x zG+!=lsDjp!D}7o94RSQ-o_g#^CnBJlJ@?saH&+j0P+o=eKqrIApyR7ttQu*0 z1f;xPyH2--)F9uP2#Mw}OQhOFqXF#)W#BAxGP8?an<=JBiokg;21gKG_G8X!&Hv;7 zP9Vpzm#@;^-lf=6POs>UrGm-F>-! zm;3qp!Uw?VuXW~*Fw@LC)M%cvbe9!F(Oa^Y6~mb=8%$lg=?a0KcGtC$5y?`L5}*-j z7KcU8WT>2PpKx<58`m((l9^aYa3uP{PMb)nvu zgt;ia9=ZofxkrW7TfSrQf4(2juZRBgcE1m;WF{v1Fbm}zqsK^>sj=yN(x}v9#_{+C zR4r7abT2cS%Wz$RVt!wp;9U7FEW&>T>YAjpIm6ZSM4Q<{Gy+aN`Vb2_#Q5g@62uR_>II@eiHaay+JU$J=#>DY9jX*2A=&y8G%b zIY6gcJ@q)uWU^mSK$Q}?#Arq;HfChnkAOZ6^002J>fjPyPGz^D5p}o;h2VLNTI{HGg!obo3K!*I~a7)p-2Z3hCV_hnY?|6i`29b zoszLpkmch$mJeupLbt4_u-<3k;VivU+ww)a^ekoIRj4IW4S z{z%4_dfc&HAtm(o`d{CZ^AAIE5XCMvwQSlkzx3cLi?`4q8;iFTzuBAddTSWjfcZp* zn{@Am!pl&fv#k|kj86e$2%NK1G4kU=E~z9L^`@%2<%Dx%1TKk_hb-K>tq8A9bCDfW z@;Dc3KqLafkhN6414^46Hl8Tcv1+$q_sYjj%oHz)bsoGLEY1)ia5p=#eii(5AM|TW zA8=;pt?+U~>`|J(B85BKE0cB4n> zWrgZ)Rbu}^A=_oz65LfebZ(1xMjcj_g~eeoj74-Ex@v-q9`Q{J;M!mITVEfk6cn!u zn;Mj8C&3^8Kn%<`Di^~Y%Z$0pb`Q3TA}$TiOnRd`P1XM=>5)JN9tyf4O_z}-cN|i> zwpp9g`n%~CEa!;)nW@WUkF&<|wcWqfL35A}<`YRxV~$IpHnPQs2?+Fg3)wOHqqAA* zPv<6F6s)c^o%@YqS%P{tB%(Lxm`hsKv-Hb}MM3=U|HFgh8R-|-K(3m(eU$L@sg=uW zB$vAK`@>E`iM_rSo;Cr*?&wss@UXi19B9*0m3t3q^<)>L%4j(F85Ql$i^;{3UIP0c z*BFId*_mb>SC)d#(WM1%I}YiKoleKqQswkdhRt9%_dAnDaKM4IEJ|QK&BnQ@D;i-ame%MR5XbAfE0K1pcxt z{B5_&OhL2cx9@Sso@u2T56tE0KC`f4IXd_R3ymMZ%-!e^d}v`J?XC{nv1mAbaNJX| zXau+s`-`vAuf+&yi2bsd5%xdqyi&9o;h&fcO+W|XsKRFOD+pQw-p^pnwwYGu=hF7& z{cZj$O5I)4B1-dEuG*tU7wgYxNEhqAxH?p4Y1Naiu8Lt>FD%AxJ811`W5bveUp%*e z9H+S}!nLI;j$<*Dn~I*_H`zM^j;!rYf!Xf#X;UJW<0gic?y>NoFw}lBB6f#rl%t?k zm~}eCw{NR_%aosL*t$bmlf$u|U2hJ*_rTcTwgoi_N=wDhpimYnf5j!bj0lQ*Go`F& z6Wg+xRv55a(|?sCjOIshTEgM}2`dN-yV>)Wf$J58>lNVhjRagGZw?U9#2p!B5C3~Nc%S>p`H4PK z7vX@|Uo^*F4GXiFnMf4gwHB;Uk8X4TaLX4A>B&L?mw4&`XBnLCBrK2FYJLrA{*))0 z$*~X?2^Q0KS?Yp##T#ohH1B)y4P+rR7Ut^7(kCwS8QqgjP!aJ89dbv^XBbLhTO|=A z|3FNkH1{2Nh*j{p-58N=KA#6ZS}Ir&QWV0CU)a~{P%yhd-!ehF&~gkMh&Slo9gAT+ zM_&3ms;1Um8Uy0S|0r{{8xCB&Tg{@xotF!nU=YOpug~QlZRKR{DHGDuk(l{)d$1VD zj)3zgPeP%wb@6%$zYbD;Uhvy4(D|u{Q_R=fC+9z#sJ|I<$&j$|kkJiY?AY$ik9_|% z?Z;gOQG5I%{2{-*)Bk|Tia8n>TbrmjnK+8u*_cS%*;%>R|K|?urtIdgTM{&}Yn1;| zk`xq*Bn5HP5a`ANv`B$IKaqA4e-XC`sRn3Z{h!hN0=?x(kTP+fE1}-<3eL+QDFXN- z1JmcDt0|7lZN8sh^=$e;P*8;^33pN>?S7C0BqS)ow4{6ODm~%3018M6P^b~(Gos!k z2AYScAdQf36C)D`w&p}V89Lh1s88Dw@zd27Rv0iE7k#|U4jWDqoUP;-He5cd4V7Ql)4S+t>u9W;R-8#aee-Ct1{fPD+jv&zV(L&k z)!65@R->DB?K6Aml57?psj5r;%w9Vc3?zzGs&kTA>J9CmtMp^Wm#1a@cCG!L46h-j z8ZUL4#HSfW;2DHyGD|cXHNARk*{ql-J2W`9DMxzI0V*($9{tr|O3c;^)V4jwp^RvW z2wzIi`B8cYISb;V5lK}@xtm3NB;88)Kn}2fCH(WRH1l@3XaO7{R*Lc7{ZN1m+#&diI7_qzE z?BS+v<)xVMwt{IJ4yS2Q4(77II<>kqm$Jc3yWL42^gG6^Idg+y3)q$-(m2>E49-fV zyvsCzJ5EM4hyz1r#cOh5vgrzNGCBS}(Bupe`v6z{e z)cP*a8VCbRuhPp%BUwIRvj-$`3vrbp;V3wmAUt{?F z0OO?Mw`AS?y@>w%(pBO=0lohnxFWx`>Hs}V$j{XI2?}BtlvIl7!ZMZukDF7 z^6Rq2H*36KHxJ1xWm5uTy@%7;N0+|<>Up>MmxKhb;WbH1+=S94nOS-qN(IKDIw-yr zi`Ll^h%+%k`Yw?o3Z|ObJWtfO|AvPOc96m5AIw;4;USG|6jQKr#QP}+BLy*5%pnG2 zyN@VMHkD`(66oJ!GvsiA`UP;0kTmUST4|P>jTRfbf&Wii8~a`wMwVZoJ@waA{(t(V zwoc9l*4F>YUM8!aE1{?%{P4IM=;NUF|8YkmG0^Y_jTJtKClDV3D3~P7NSm7BO^r7& zWn!YrNc-ryEvhN$$!P%l$Y_P$s8E>cdAe3=@!Igo^0diL6`y}enr`+mQD;RC?w zb8}gXT!aC`%rdxx2_!`Qps&&w4i0F95>;6;NQ-ys;?j#Gt~HXzG^6j=Pv{3l1x{0( z4~&GNUEbH=9_^f@%o&BADqxb54EAq=8rKA~4~A!iDp9%eFHeA1L!Bb8Lz#kF(p#)X zn`CglEJ(+tr=h4bIIHlLkxP>exGw~{Oe3@L^zA)|Vx~2yNuPKtF^cV6X^5lw8hU*b zK-w6x4l&YWVB%0SmN{O|!`Sh6H45!7}oYPOc+a#a|n3f%G@eO)N>W!C|!FNXV3taFdpEK*A1TFGcRK zV$>xN%??ii7jx5D69O>W6O`$M)iQU7o!TPG*+>v6{TWI@p)Yg$;8+WyE9DVBMB=vnONSQ6k1v z;u&C4wZ_C`J-M0MV&MpOHuVWbq)2LZGR0&@A!4fZwTM^i;GaN?xA%0)q*g(F0PIB( zwGrCC#}vtILC_irDXI5{vuVO-(`&lf2Q4MvmXuU8G0+oVvzZp0Y)zf}Co0D+mUEZz zgwR+5y!d(V>s1} zji+mrd_6KG;$@Le2Ic&am6O+Rk1+QS?urB4$FQNyg2%9t%!*S5Ts{8j*&(H1+W;0~ z$frd%jJjlV;>bXD7!a-&!n52H^6Yp}2h3&v=}xyi>EXXZDtOIq@@&ljEJG{D`7Bjr zaibxip6B6Mf3t#-*Tn7p z96yx1Qv-&r3)4vg`)V~f8>>1_?E4&$bR~uR;$Nz=@U(-vyap|Jx zZ;6Ed+b#GXN+gN@ICTHx{=c@J|97TIPWs(_kjEIwZFHfc!rl8Ep-ZALBEZEr3^R-( z7ER1YXOgZ)&_=`WeHfWsWyzzF&a;AwTqzg~m1lOEJ0Su=C2<{pjK;{d#;E zr2~LgXN?ol2ua5Y*1)`(be0tpiFpKbRG+IK(`N?mIgdd9&e6vxzqxzaa`e7zKa3D_ zHi+c1`|720|dn(z4Qos^e7sn(PU%NYLv$&!|4kEse%DK;YAD06@XO3!EpKpz!^*?(?-Ip zC_Zlb(-_as+-D?0Ag9`|4?)bN)5o(J=&udAY|YgV(YuK9k=E>0z`$dSaL(wmxd!1f zME&3wwv@#{dgeMlZ4}GL!I`VZxtdQY$lmauCN_|mGXqEEj@i~du$|>5UvLjsbq!{; z@jEf;21iC1jFEmIPE^4gykHQzCMLj=2Ek4&FvlpqTlS(0YT%*W<>XgH$4ww`D`aihBGkPM(&EG};Cl&wzg8!jL z`rkqPzvH(0Kd{2n=?Bt8aAU&0IyiA+V-qnXVId^qG!SWZ7%_f&i!D{R#7Jo$%tICxY%j)ebORE>3H_c|to}c#HX;HAC?~B;2mmQrMp2;8T zmzde!k7BYg^Z1r|DUvSD3@{6S<1kndb%Qt%GA# z+sB2&F5L`R&fLRdAlpU_pVsJsYDEz{^ zKGaAz#%W+MPGT+D$+xowMY0=ipM)0p?zym&Aoi)qL(pO_weO(k?s|ELHl^W zviJiFUXRL&?`;3_;mvc02A@sbsW9}#{anvGafZ#ST;}za?XS3}ZG3B4m(SW{>w}Fh z)T5Yi*``Tstmi9SHXmuWSND@cj}qtY!`tuD29Dpu+-D3$h<5FY>jE>YJvqBmhw?oll`x7Ono(}R~P zle_eBwYy0Rr7kmf_SEt_gn4)AO-r`}^Z5Y%Rm8)K-?X>rvDL+QT?#)QwDsQ2c$tc* z&#hbgkL6}GnBDH;+lREM6MGIskRa@r>5Iq(ll2IepuhW86w@14=E{6$cz*cBDQ)CT>}v-DLM-v8)xaPBnmGBKM63RgDGqh!<*j90tSE4|G^+r@#-7g2 zs8KE8eZPZhQuN>wBU%8CmkE9LH1%O;-*ty0&K~01>F3XB>6sAm*m3535)9T&Fz}A4 zwGjZYVea@Fesd=Rv?ROE#q=}yfvQEP8*4zoEw4@^Qvw54utUfaR1T6gLmq?c9sON> z>Np6|0hdP_VURy81;`8{ZYS)EpU9-3;huFq)N3r{yP1ZBCHH7=b?Ig6OFK~%!GwtQ z3`RLKe8O&%^V`x=J4%^Oqg4ZN9rW`UQN^rslcr_Utzd-@u-Sm{rphS-y}{k41)Y4E zfzu}IC=J0JmRCV6a3E38nWl1G495grsDDc^H0Fn%^E0FZ=CSHB4iG<6jW1dY`2gUr zF>nB!y@2%rouAUe9m0VQIg$KtA~k^(f{C*Af_tOl=>vz>$>7qh+fPrSD0YVUnTt)? z;@1E0a*#AT{?oUs#bol@SPm0U5g<`AEF^=b-~&4Er)MsNnPsLb^;fL2kwp|$dwiE3 zNc5VDOQ%Q8j*d5vY##)PGXx51s8`0}2_X9u&r(k?s7|AgtW0LYbtlh!KJ;C9QZuz< zq>??uxAI1YP|JpN$+{X=97Cdu^mkwlB={`aUp+Uyu1P139=t%pSVKo7ZGi_v(0z>l zHLGxV%0w&#xvev)KCQ{7GC$nc3H?1VOsYGgjTK;Px(;o0`lerxB<+EJX9G9f8b+)VJdm(Ia)xjD&5ZL45Np?9 zB%oU;z05XN7zt{Q!#R~gcV^5~Y^gn+Lbad7C{UDX2Nznj8e{)TLH|zEc|{a#idm@z z6(zon+{a>FopmQsCXIs*4-dLGgTc)iOhO3r=l?imNUR-pWl!ktO0r_a0Nqo@bu8MzyjSq9zkqPe*`Sxz75rZ zr9X%(=PVqCRB=zfX+_u&*k4#s1k4OV11YgkCrlr6V;vz<{99HKC@qQ+H8xv5)sc63 z69;U4O&{fb5(fN``jJH#3=GHsV56@{d@7`VhA$K^;GU+R-V%%cnmjYs?>c5^6Ugv} zn<}L&i;2`zzW@(kxf$$gVH@7nh}2%G%ciQ_B?r{13?Q@=Q+6msQGtnyY%Gkjeor?g z7F*tMqLdhcq+LCCo^D;CtOACCBhXgK-M&w{*dcUdmtv@XFTofmmpcWKtCn^`#?oZC zUOm52 z7sK$hR|Vh6y&pfIUK&!`8HH*>12$nWA)Ynp+XwOj=jNLD z{QA4gezbe>wiP?`jJO;c&EId;=2u80s_r97;TX!6@*(<%WL+^bmxheMB3pKx0OpH^ zPs}knV+jpJ4TaD@r^V`mTsjf`7!z^H}eHQ#Rp z72(>Dm#QO!ZYR*O@yHic`3*T^t7jc=d`Jz6Lk@Y-bL%cOp_~=#xzIJl?`{Qu;$uC~NkePE+7wSW_FM`&V{gFN zl;lq@;FtAsl!h;tnOvj z#gYx!q$5MdZ0Jxjy=t*q)HFeeyI-vgaGdh1QNhqGRy8qS)|6S0QK7Gj9R?Co{Knh> za>xkQZ0}bBx!9@EUxRBYGm25^G}&j-`0VWX04E|J!kJ8^WoZ(jbhU_twFwWIH32fv zi=pg~(b#ajW=`)Vikwwe39lpML?|sY$?*6*kYBxku_<=#$gfTqQ_F!9F0=OkHnzBo zEwR!H_h|MNjuG$Tj6zaaouO}HYWCF8vN4C%EX-%Iu%ho;q$G#ErnafhXR*4J2Rp5* zhsi0;wlSwE*inVFO>{(8?N~82zijpt+9Y_-^>xnE%T*zk9gi|j7b@s<5{|qEquUD( zS;-%RySZOCOEh*>!kvbsQ265* z>X8*_Wy&~FB@aDHz%glyiAujXq-|2kDUjFTn9Rafsl+XNyFP%PG|l&ZGWBcEXxy=9 zeDn2PIoVuL$gX0RgVK1O$x3%pOzS7x^U5Pi;mtT)%cY;&e&M7GLM}zP+IPbqLt=^5 z7qLfri8myf;~2psc@^cA6mG&{C%e_(M$$!wC^5p^T1QzrS%I?(U{qcd+oJJkQxe10 zON{Q*?iz%F4MbEsoEc+x3E?&2wVR^v3|Q0lDaMvgS7mNjI{2w! z9|~=!83T%GW*iaChSS!`Xd^beFp9N4%K+k*j#jFumk}U?=WKL_kJAltxnxp~+lZzT zp@&&kSPTg3oSGos`rVBhK0|4NdHM_hnKuw1#0JV{gi_dKDJLB+ix~~HpU9%jD)@YY zOK)L7kgbLyN2%Dx#fuY}8swh4ACk7%BpP-n5(RhDq{gEHP*Fo4IviX{C49|B5h~SC zFr`=0)=h2^F5UpCAgt?R5u{6VvpUf#*nC zCQ`$!|C;L2lpjlG?(>T$(_$O3_YNNbPT~(?!j3aD8k=yu^ogw4bkjvgF|3BOq(hB& zG;^cPXmcUP$ox8zElCJ-zMbK9q^8{rri#8Cek5Ydr0YT-KTh@J z6^AcB9ejew8BY5kzZUZX(7Po==eW<(;uV~E7(BY5c0^xr`cuRwn)47bN?zOb!0?cw z#v}R$z66&m#+AHfo@(^V2#S~bhoUkkTArg+6w>JzZ52r96^({1W!?>4$h0l|-jDfj z>7(<+%67#(A|4hZ3>Y;hd&S?}F;`Vtqz|pK&B>NJ=Faci;gkf-+GmfQR8^zo_vul2 zB!)kfu4Dq_g)8TBBo52*sB6F`qa&JCR=_A$QWgX_K}fZm{Cb2#1q`^S3+WaS>sS#@ z-4k*G=#?z6d_e7JJ+Z8^(t0tNdL{K5F;2nfQbXgld}a(X)Gr;WojOy`^?es~AClT$ z5^lD{WJek0!p-QEH5E7n6DKQ0%_ZBZ=|jfV_MM{VmL8y-Wd|>OmeemP=C@xI@@M~1 zW2S*im@Rc=O>V886_UJ@oh1!2H$Ku&U*Hh_oxd{32)vf1$cRiepv28ricM;}#p!+k zaK{z1I=9Y%3m4|Pj*BD*Fn5Vh?O@oD^1UcjyeNh0fbhh~V6xb#4njlGW8OehUe!MnoR(wn#nsoyL1m!Rov)Nv4~&JEVl7L z#^qYdTpNI#u`N0UbVMiDmD>g2VQcG3>4D6gErgddZnSQTs){BExxRJRB?bIxTdZa z;!S8FHJPPiIDQ*FAUiWSYnjILFjDvxvSC zk z=j4Kx@Pg~&2Z?cmMDa;)#xVeorJrxDBqy{+`kG+ZPQqC@#ku-c3ucU+69$#q_*se` z-H#PFW^>-C0>++|6r=<$Z8)ZFaK=ZjwsNYXqRpl9G|yme@Eld5B-*I69Nx_TResHi z!5nm+>6zaJYQO#%D{~o-oOJ;q`fa5}l!8G*U-E$OM&7@dqciBCWtd}|SrDXz$TB($&m*=Epuolu2k`KUwO7maP3P0ok zmF57lSh0Ba@&sO1iZ5^+3s8{B8t|M;Pg&O+{tZJCiLWd6H@{b~9{CLF9s3Kn zt5)Rs9ejne?o{%f>B$Dl%X7fd~KY)I|(pxUeHj;gNsK6;ZR>`ciu;GxvhDUt!+31Knss2U(%ts8K z18)8;<2ax9RG?!|Lwdt^i5L^&O788roKmVAB)=EdK~HqR2Q=)H_VW}xY=95MP_Ov< zPEz3%DRK}+(aUBwsr83H8>`H^v~|A_t}0vPmRwKPt1{|qOY|PZu}j9+{ZhF&-H_TB zU9xWLpNTc`enI|)h9jQeqf5RfGLFk_vfX`40iMpd%KZF!lKbZTdBw$<^G6nuS+$fT zrbK)xo&;buPJcpOZ=x>n+bRXVFDs(23Xr=rDE&!)pVXZ;;A07NXGl_0m`{Z)DQIu$ zFDvY4xu-ifTe_$|n2B83eI;KUg6pVbw+N!nyLj~wnRi{4mNy{WDV)G1!6$y=+x6U{ z%4_9=Q^L!x_gAYp?J3+u5hA5cO8aHeI=6AC8^S{mzhqCBvBLYEutUC(X0>hKg|AvN zvkmJCQNA45_KjW{aEcyrBppcO6G0zTy%v1&@~+2!n?kA9?>0>AjFN|JdCnHQ8$hEU zw#mwGifHppLP?89LMb(Y3Li9iCPx7W%ek}2FgD2YSzjsR4Xj<=zN{Yo@7s7(k%mP4 znT2p&4EQ@q_chd-E z78uvD*C@oba`U3W2Iw`M#`5C8jOHv8^Li<|j^SI>>>`77Dp71Vtz=J?4Zck4SdRbd zfF}C_>Y(#)r@y!Q0`tMlG#b9>5`fAI$B&tWJfbGlYW$J4V+-s=HH!`+;1XeL@USdx zR0$G&&XBf9lQtkH5)p=U!8J!1{oc4E!N-~Abxl6E;;=3-hMYZ+44?u}zabmCE)yB?*_w91m$n1Yskp&@ z;kxeJX-#ioX^{elyLu~gzx|_KxLpX62MF%Axq3$!Z_P`pBWR?zP8OI`PV~6Aa0Oi0 zv_Ot1m&plf-ZF{e(z(Ms3*S5q$e|j;gOwGrmWsCHfLi(h8y?gc$(2H{884C1FvHQQ12tX=qFUsK~zM!W=K>;zaRsu4Xmcc@8nSs!vK+{ z?}bq}-m&p5jRSam67n>yG9ez=I^|J1O;Np8s=P~9MXYLxD+cFQK7PhG=bkjo{Naae zjp3NWWrlFWDb3Z5D07Q|WjZ=wOQ=aKA%en=O@hL$QCKpIXNZE=InFk|Fhq-&H!6&X z*MVy8=hL7Aw&pQjHrFf27C%3B<>FX{@fOLNhUoxL4*@nY}&M3G*T-p67a zo}~_&yGOB)#vbU|Q3FA8S^X)c-yBlmN(_%}`7Ha3uWFe?>9f=3hlO{^gv~$p`v?vk z_P*r43|(S{%ihs;)YH|jAMpP=-Ms7Ne75_YZZiL3CHVjSU`X1|?Ehh&gA=Xn7W7d@ zf8bM9Y>lG!`PWFDDA9G;x*{1Eh^55u66*9D+-4^dYZ{xXP@?sQLVrY%(azM;C^4FuN7CQ%$!3sr1JL=!Be& zuOZL^bLp$Qo2rL=WDzQIls%s!Go z{s}Q0b#+#8bKga|01t%^9Z=wEsevvXM_{$dCR97ed3@1kX)mtSS!JN^rtqKOj}p~> zfpCI@DX*DqcB6ZnBcl~}sGO~1s$AtfkX6fy3N8*ebvZc*KBW;dA=)?#BE&}-or74i zZUt5;{FBPnkZD8YUXDsx&2LvSziAlec3oc>&Lf1Doc3g?H9{OO_$M4B0qTat0UsWP zTlxUeQ3B;oJ%en4n?zQB6*Fb#wH7`$SQN5GI|=DnJKiYm{?-?#-H;#sIjz7kQ4&VW zN9d1(1$_W~S=<%qDD!mwRytas=eqX^iW}YSx3;wJ#)Xp_`Qk1DFiXac$-3;jQbCif zLA-T_s~5yP@Q@W>pXKl^gipQ>gp@HlBB>WDVpW199;V%?N1`U$ovLE;NI2?|_q2~5 zlg>xT9NADWkv5-*FjS~nP^7$k!N2z?dr!)&l0+4xDK7=-6Rkd$+_^`{bVx!5LgC#N z-dv-k@OlYCEvBfcr1*RsNwcV?QT0bm(q-IyJJ$hm2~mq{6zIn!D20k5)fe(+iM6DJ ze-w_*F|c%@)HREgpRrl@W5;_J5vB4c?UW8~%o0)(A4`%-yNk1(H z5CGuzH(uHQ`&j+IRmTOKoJ?#Ct$+1grR|IitpDGt!~ZdqSJ?cOtw-R=EQ+q4UvclH zdX=xlK-fhQKoKCPBoFAZ*(~11O6-tXo>i0w!T$u{lg!#itEUX3V{$S*naW!C@%rll zS{L(1t%xz(*B`{1NL!*aMc<~fE=g;gXi&Gb$HpD!P)8?JzfN;4F&wv(5HH<=c>>)n z({271)xREH89=C(5YKL{mmJJ_d>qHz;;gTvTlgM*vz9@YTTYZ#%_2A zS0G-t9oMQEpvfv(UjfQ8T$vAHi)zOj3>D*{xSRiu3acc=7cvLyD?_ZObdu$5@b*!y zaZ#u?7uF}SrHVQa=sTOhGW{6WUlq#RhPPm^GsRH#qlX8{Kq-i~98l;eq>KdCnWyKl zUu&UWBqu#Tt9jQ97U4}3)&(p2-eCLznXMEm!>i^EMpeVzPg%p;?@O;dJBQQY(vV;d z3v+-3oTPC!2LTUAx^S2t{v;S_h(EZ^0_dS5g^F*m{TEIy^Qal~%mu3h7*o`jWOH}i ztv8M)3X3a*+ry_KkYXYE4dB0?M|t}#Tp+(}6CQ zBbq;xhoHj}b@j-@koDB#XcCY~>_x&Y;i%MH|3tF^X2h{36UCVfQ-;oEA+4ZkJ`^Qi zQf^8}6eFO$Z+Dj-F1wkG##tTx>FjR2oOXFmbKFj6K3+=kePQ<4d7%z5R5cOB;zO6| zm9^m#U4lcA;7t&*=q|a-!`!)}SgYXT#i8hnxtx@kaoBF$QAS-hT7N5kH^l zB^i+})V>L;9_0Qqf-dyF%ky8Mp-dp#%!Nls3vCt}q3QLM3M-(Zs1k}1bqQ9PVU)U` ztE=?;^6=x}_VD%N@${>qhpkU*)AuUBu_cqYiY&@;O$HV*z@~#Tzh?#=CK`=KwBv+o zh%zu%0xPKYtyC)DaQ zpDW}*86g%>BH3IcWMq`g$j()0kWE(qkIL8A&A0mf&+BzxpKF}=`#jG% z&*wa!&pGFLs5_b#QTZE4Bp+})qzyPQ7B4Z7Y*&?0PSX&|FIR;WBP1|coF9ZeP*$9w z!6aJ_3%Sh=HY3FAt8V144|yfu}IAyYHr1OYKIZ51F>_uY^%N#!k~eU53at-_E-Gh?ahmM5y* z+BTIbeH;%v1}Cjo{8d%UeSMWg(nphxEU`sL< zQR~LrTq>Da(FqSP2%&^1ZL#DTo5Sbl9;&57tQ-@U&I#lj)aNSkcfEJwQD!33?anVU z?pw2q7WtMvfji493`rSFnyp7{w87cW`ak=UEYlk5PCB1K6UDVKXyozOChH4yHh~Q< zv>yvKw6WLfi!PZUx60JZcTNM7jo{ww9b8Q+S7C3WA5&llSwdwh$=Q(*(f3ofqcz=nwOmOy z(J!K=*wNoRU*${{Mbwapi9pTB(&VVKefqd-qrUb9*Eyr2E@oZ9Cgf}Mc;QP<0D)R4 zz=!*^VIG4T*7Xl=sJxrWv9hW^eJ%qYp5(d0?E6LZzJ}=7E+1{?GQA;z+!^VBD81}O z0kJ^dKy&WMw+1+aGVYY-v@i28@Gm+sX5=@U%F=Z?W)oar}2~Rc&F|+3A)n-U2GF10+QdxDb^iA@7eL$c7yhBtL z>lABrh^qy9XZ${E1}Ss5!N4;ig0-pUh6@|RPCHOWvgG{|l}2enRgJftsN%D|ck0YO zuAQd2aMPSyGuJ~jm)aY=+p~mGudw4erwE%P^)5f<*$$2C-4^I=e8-}7##ZQ!8!Tep z+Z_!}CAI~sry$|XK$ktXaxP*x<_ijCPp`2=6sNLZU<@9Sz-rz7^BCE9yh0jV4(I!Z zxmA4d;>B-!vD}Xp*&*N%`b^e&R;D97WS}{~{O-EtXeZNfdf51tw!WR6Noo4hjHPv5 z?heYYRSBPjMc}tFEU^|U8a1CxxK%)WTcn9P%`wR^I$QSeMn6=w>Z9OoVvcrl`zYlZ z2y`mAu0bV(Scc>G_EmIo_4 zm*~h`mxYZC&+U>C5G1FZH5L^U>Cq-9UDRQa35jz&NBj*0{uJKfZs5=Fn@&)Xh6aX(H3w9m9BGLePqVotxTeSPh5-mc7$# z-80t6yB0$Nx<54ohdO*QL7m_(&+#*=eoNiYDB4rE4Cag@qfyZS};Fx;Vf1;oync2k z9v#-w?d6R& zOI`CCS_d=tf3|?g3Z}b6-_Rdg3y~enQhmgkni0Cvf9m6%Ft8r;NC5|b%t&?lkl*4{ z8Ui^;Ds^gq6ti(1xB7y_$zA!i-M~#!!tl$ErTR>P~>T=Yky)8(uvPbvLmB=UfoD zrfl}8<1OQrm?8#j1!?s*T>AoectQl&m!o&*^JcIW`_&bk3tN}k^0rjl=HL$z*uIYt z?7l?^Dqr?q1210Sp$xoAy!&{2^{^Anl460 zI&7urrc&|Y{rjv04VOl{y7c82N6xzg5ueYmQ(q(zC3w_C#x*~%yf5j7MI{W`tsoxzA*PrmK)cTskU| zf2C}Bq$>S$-1JgIh0aW@LxI|-8(OGuD#^M01ghh}&#ObO>tZgSw_LW`zdf&IN$YO# z)|X_9m#JwLW5pErZB3ScggKcNzxA9(hyKkK9I#pR&79&*+SV_eu={00{HF=Bb+AEe znaSof+r1jZ!EL5XgqXWkckaFSSyEk}o!%p8XsD}O>borZ6x%X2b&q!s&1-O(>`kZ$ zB2l^5Cx9xQx9)PXN1xPM)@+LxACH_iZ8zGc(>wnFS_O|@hKsxpMjXOzLEa7OvSlM&&G9ioQw9~RsD4F zK7Q+_&|Q6{eZ^8Rx@pKL`le6kH+(fLc{=V&{b%I5=n}VHV4)X_2Y!pYxgC8wU)yP! zPF3t$?(jsC>Ge=&{kmPGUEETpaw(QTAl)m#{qR3_aq9!wK%6XHfV4C>Y^>Z|%ns7j z{Ja?^IA{+@;kR#IjHxkar%3$eJT4?xNBKUVmoO z`A8Zo-{~_;vcikZ(p}EZzU4kO6WPqkMyE{VvS?;44Z@lj zz^fKX9UL!8Wc(9VgI?P4*zpis8dzl};I>yr1>dtXU=FTAlx}Eht4-*7RACL^AflGh zyZb1hTf(~CkMo%#Q%NMgM9tE2D+)joqbtHYA89Ql1nqVTt+MxZ^*FRd&n5YlIi!8m z>$Ysd!l{+C)y;Wa(ZV-=<+NZKV;v4mt}v2m>`v$-$3b;GsLxf= zd~f(rmfpl``{0aVwN7y!>eGyJFP`L+TxHjHTOS{K^$L2`@6(Rli`{EFwpH@R%eZ6g zwf7rc43Yk!=k;{ z-Rn%~B3amGr}}SxfE$vS8FIPL=Qt57$|R#sSoFgdNUT?fYOYjPl%ZBFpi=jq=DWby7Zxm@y;B<89!9= zbgEH*Uy)~iq5kJLX$+ps$kV`#6jW#|9BGz^`ivNeid(wVbk4jl)VBpW&~;eXNi{#` zwx?{DXR~*sqQcFhY0XCfQ4-*2aN1BGX>$_swtKEqnd>j6vcZ!#0)pXRi?<{!P?tGw z2x_`RD$W)qD{?z}VDPt?+)8*rqLWFIPQ(9-VbBdf{7ff?w9CZ{sIi_gnuC$I0(+P8 zms9XB%}VQ>>pve##}jog6+cD?v~n4Pa9Vmc zg#K$|+`adO=B7`uj35Y}6EZ z{dY`x@w8;R-7zrsr1O_~Jvl*|o-x%jF=Rr1C}GXP^|IYN`1sqmG-oI@R#%X66c#5W z$$tQB)sqwiVm;Y^`Dw3mo|firP{*HsOQJre5%Dm^H@we0FN88VWJ0dja?_U38z73f zrCV!b3qNP0kM#%9T!W5`ynGcg%BL28FW1J-J1_S`BJGCaReQ!am(2%qZ3lLgzq|ns z!!fF@`0=*z)J2BwZ*hO|Yu^cI_nF$9l-Pb3jE7=P8gZ#!xiuZ7-cSa`gb`6mxGTgg z-DLdID?M!Z%+hHB#{?&0$GFRpf+_}q<_wbzX6K?w;%6szz1RbySDSr2r^h_qi$khs zXdZ9A0!_Bf)TR2-^-K~q`FQ!#1x(U4VbV%AA@Ei{%cA(EwC{XfjRi?`&9rav5;Q5% zO1`Rn@OA_ZB@N*mC#)?d3P!}Eh;=NgpIKsy{(yr`hv=aouwt@r&P&}Z3DNWo9ro30 zX52~(aTV$*HHlgB66-4GQru!_AZ|)V*I5X=WG)`N@U&D>e@@C#V@JwEL*L`7#$yes z62C^5%Qniaow2$3HrAc7U{qzpb&FA*xLI1JSWR@`RF=JCcvTI)%dH7;sWInt9JLu# z|Ao|Q?K)cDg_JKsym=joo5gR80wtv01N`um1nQ@Ms0Y*bVzxL34} zo?gizp?`=Y{*W>^Hy2%Jl)y?A+&7s1UVHFixuIy~sawXjcDCL`129cK7|ZQS0u;A} zTJC#WNmqkIrnHpAhHVcM(U^vJA~dl@jf_bs*3?i+=&vuC?Aiy_pcB~=1syDni4 zw+FLuz>F773u#$;NUQ9WDtUPY@+rA3WBhQdKFKOyzkA(URa7;4tW>3jQIfi8v0h3g zJC_HVDXS#>DWb|&se7FHnr=q&l#xg9o02}}u=b-R>@sw={Z zHF*?t2FmhqZ=|qa>x=A!*$S+0T zhO*D*M?NTf-eX`eO)9TIQu{7Dm77Acnj4b1jI9@c*ZL8wL%8kLEhd$KM8=Y!fbN@9 zC7B5#y>JM1n5M)!&im==EgHs2j+xCZG~+~QWCi?s!QyFo2kqx{%jE2n3^N*Ayz6Lp zhg5g^3# z+5FoJ@$u@9WJgPKpUWEd4}4AK9TJKU8W%ms!d0p%OIOX+bY+55zl!vIaz$XFI9Ep+ z;bL_}7PDI2Y`Ng*XY(65 zh0%`@Lve%fc;)N4_g12bNrt6gH=N#OHtxO`$lpWlw=Z6MF+E@;>GkZ#lAZTn`aHwf z&I1|aV#b_VHMIgBN*RzU9i@Z@m}0i>o?({&%fpEfaOpFeaJ7V37;m0?kzd}}Lk@9$ zL}8TEo7WZAcRi%zFZxkr6<0k#X-;lTD`Oc~cDb@olwgWCewvk{GJ}hCXbF!AdiLpd z|Cck$ZTKI?Ack{34Lva7+k=H8K2HTZiurox6F+>dy+@R9T^awxj590D$|kXUg+Ygc z(f)jlRwN(4z$#%PnOVc;#Fv{nAi{#UcXPNcmP#5O{zh_*`=q^JCeia{sN4zHjk2*y zqUVh{Ya{j>SPmP^i#Qfcq_MTqo8g52Fi^F zKBc$$HVI!xFx*4Y9l+nt)$AoZORD}%5I10oI3kx`-N30QueiwIw#0VV2E*Fb-nKW% z=+r^hos`Y-7~{cA1FVbK$_=~*z53+Q8KGjg;>ztg((H12%QTf4OYU8y)C}h5yo#$% z&Q$`vMM*g?ZcatAn2j!hFv8KuN(dw)T*}sF#THDHxo8xC^?vJ zc`U6bVo~hOr6I!8*GTZ<^D~;unKjK=!IR|GB4E>Mcvt*2GK);93jIDd<(nNjHO z4Hi@2^%Uyx=^Z~5eZ!5rO5%4H|eFoNjD#+Kcu%_57zZb4Z@Ak#X6txD^{U3wBl^r+W- zLorkK;uc;NgTj7dGxHQS+@T*T>Q*j4^Ll$ejQqWrwcHyG9y%Mk%m8nBVG5hvSaYm5 zJN^#-Q46kZG)@T8n2^QCjxIwxUVi%s>EY`E?#@_(A~njFrTiDq;8v|W-1jT|ROlNI zU$h|YoD4PVTE^&NC6_m{EAFBVqsM`P*`-AcDGWQygURzM32Xeq2xng~XQsYeTZ5v$ zQLaa2M_Iplw}4eL6fLPu`6`PYcVMysO>`{8CB~glD=TX7?JZcHfHNmykBM?QD)#D) zGp>R*<^D?WhFQKRc^}22l6F=D2RPrxaX2ZF!b1X0XF*d4%=!sbNcS1q2WOUE(7e4$ z^L8f;F)__d3>&KQFE8%$I4h^y5FYBfB&fWzn71_OSrPe-DHV{O#Q;GP z+Tw!J?eVjX19RKH?*hKQWQt8r7B#lYX8xoSHFGCW-*DSQ4EM4M3Mw%gkSYNK18@(e zfzMF}WWaCyS@1y%-~Xg0ry~tkQkUmKuI5lGAua{{vn22V!2T()AU5FpKh@Nv)s^Js zv~@VuUG;=CnLmQR{PeUBQf2;lAV!vG>^Z0N zL88rrjL-*J!43;7C=w9xhcw`yjRKq7o4L9=0SmR9PA-nX12@#h(iIu-0N_xm2OV)( zU_raT0y>$wm^oMi2|U3N;OhF9uy}`<-xVka#DV*l{O0yHzi9vUxa1Qtpi$buR*8cU zd4~lS1pT$L^!0=6qUKOpM+XPsy{f7W#1bjrEwaeN!Ik9(zySIT^pEHvHgJUneFN4) zk=k|$55(g8slmS|@+*4fr2urd3LwjIIZA**g+%l(SZNn4HwQ}y6o`vw>2&mR1X+&q zDa1Af0B;4rAMZMOlHbAqK|R_xuwJ7ANARtFE({-P2o{tJJR<>2KVp)ZK-M;)ejx zd*E~Mka<{OL7%CAhk4n|1qg?97-I!l0rOinjVi#arbgg4bi5;nY5oFL`UWtPk5&L#grSxv zE3!}=1px!ZTLT90aYc^s`~{VojjJml&<`@e41dFP+XU6D0AOkbn2rlI3>^LcqauG& zc$m3Z{!u8LvUrm^fT{qX5yD9{?r(CCiUdck%!T`KIZd2oQJz1joB&M(Teg_>;yS<2-5>BWfSPpG`Rt{!j6>kqMAvl^zk0JUEfy$HVJMkxP-GkwZuxL62me2#pj_5*ZIU zP~#C^OZLfl$HO)v;~~c&JHivn|1I9H5y_CDkt0JLLGKm(4*KLVhJ2jh2#vJuM6`b& zE==-lvME^Oj022xF&IV*? /dev/null && printf '%s +' "$PWD" ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum @@ -144,7 +148,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then case $MAX_FD in #( max*) # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. - # shellcheck disable=SC3045 + # shellcheck disable=SC2039,SC3045 MAX_FD=$( ulimit -H -n ) || warn "Could not query maximum file descriptor limit" esac @@ -152,7 +156,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then '' | soft) :;; #( *) # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. - # shellcheck disable=SC3045 + # shellcheck disable=SC2039,SC3045 ulimit -n "$MAX_FD" || warn "Could not set maximum file descriptor limit to $MAX_FD" esac @@ -201,11 +205,11 @@ fi # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' -# Collect all arguments for the java command; -# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of -# shell script including quotes and variable substitutions, so put them in -# double quotes to make sure that they get re-expanded; and -# * put everything else in single quotes, so that it's not re-expanded. +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. set -- \ "-Dorg.gradle.appname=$APP_BASE_NAME" \ diff --git a/gradlew.bat b/gradlew.bat index 93e3f59..9d21a21 100755 --- a/gradlew.bat +++ b/gradlew.bat @@ -13,6 +13,8 @@ @rem See the License for the specific language governing permissions and @rem limitations under the License. @rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem @if "%DEBUG%"=="" @echo off @rem ########################################################################## @@ -43,11 +45,11 @@ set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 if %ERRORLEVEL% equ 0 goto execute -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 goto fail @@ -57,11 +59,11 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe if exist "%JAVA_EXE%" goto execute -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 goto fail From cd71704d523424a08535a6ffcda6bb7dbc966080 Mon Sep 17 00:00:00 2001 From: Orual Date: Tue, 17 Sep 2024 23:24:39 -0400 Subject: [PATCH 19/42] FONTS! ACTUALLY GOOD FONTS! --- .../com/morpho/app/ui/theme/Theme.android.kt | 2 +- .../drawable/BlueSkyKawaii.png | Bin 0 -> 118701 bytes .../font/IBMPlexSans-Bold.ttf | Bin 0 -> 175712 bytes .../font/IBMPlexSans-BoldItalic.ttf | Bin 0 -> 184656 bytes .../font/IBMPlexSans-ExtraLight.ttf | Bin 0 -> 179500 bytes .../font/IBMPlexSans-ExtraLightItalic.ttf | Bin 0 -> 187448 bytes .../font/IBMPlexSans-Italic.ttf | Bin 0 -> 184020 bytes .../font/IBMPlexSans-Light.ttf | Bin 0 -> 178140 bytes .../font/IBMPlexSans-LightItalic.ttf | Bin 0 -> 186768 bytes .../font/IBMPlexSans-Medium.ttf | Bin 0 -> 177104 bytes .../font/IBMPlexSans-MediumItalic.ttf | Bin 0 -> 185720 bytes .../font/IBMPlexSans-Regular.ttf | Bin 0 -> 175748 bytes .../font/IBMPlexSans-SemiBold.ttf | Bin 0 -> 177272 bytes .../font/IBMPlexSans-SemiBoldItalic.ttf | Bin 0 -> 186284 bytes .../font/IBMPlexSans-Thin.ttf | Bin 0 -> 178896 bytes .../font/IBMPlexSans-ThinItalic.ttf | Bin 0 -> 188304 bytes .../morpho/app/screens/login/LoginScreen.kt | 41 ++++++++++- .../kotlin/com/morpho/app/ui/theme/Type.kt | 67 +++++++++++------- .../com/morpho/app/ui/theme/Theme.desktop.kt | 2 +- .../composeApp/src/desktopMain/kotlin/main.kt | 36 ++++++++-- .../com/morpho/app/ui/theme/Theme.ios.kt | 10 +++ .../src/main/res/font/ibm_plex_sans.ttf | Bin 0 -> 120340 bytes 22 files changed, 121 insertions(+), 37 deletions(-) create mode 100644 Morpho/composeApp/src/commonMain/composeResources/drawable/BlueSkyKawaii.png create mode 100644 Morpho/composeApp/src/commonMain/composeResources/font/IBMPlexSans-Bold.ttf create mode 100644 Morpho/composeApp/src/commonMain/composeResources/font/IBMPlexSans-BoldItalic.ttf create mode 100644 Morpho/composeApp/src/commonMain/composeResources/font/IBMPlexSans-ExtraLight.ttf create mode 100644 Morpho/composeApp/src/commonMain/composeResources/font/IBMPlexSans-ExtraLightItalic.ttf create mode 100644 Morpho/composeApp/src/commonMain/composeResources/font/IBMPlexSans-Italic.ttf create mode 100644 Morpho/composeApp/src/commonMain/composeResources/font/IBMPlexSans-Light.ttf create mode 100644 Morpho/composeApp/src/commonMain/composeResources/font/IBMPlexSans-LightItalic.ttf create mode 100644 Morpho/composeApp/src/commonMain/composeResources/font/IBMPlexSans-Medium.ttf create mode 100644 Morpho/composeApp/src/commonMain/composeResources/font/IBMPlexSans-MediumItalic.ttf create mode 100644 Morpho/composeApp/src/commonMain/composeResources/font/IBMPlexSans-Regular.ttf create mode 100644 Morpho/composeApp/src/commonMain/composeResources/font/IBMPlexSans-SemiBold.ttf create mode 100644 Morpho/composeApp/src/commonMain/composeResources/font/IBMPlexSans-SemiBoldItalic.ttf create mode 100644 Morpho/composeApp/src/commonMain/composeResources/font/IBMPlexSans-Thin.ttf create mode 100644 Morpho/composeApp/src/commonMain/composeResources/font/IBMPlexSans-ThinItalic.ttf create mode 100644 Morpho/composeApp/src/main/res/font/ibm_plex_sans.ttf diff --git a/Morpho/composeApp/src/androidMain/kotlin/com/morpho/app/ui/theme/Theme.android.kt b/Morpho/composeApp/src/androidMain/kotlin/com/morpho/app/ui/theme/Theme.android.kt index d1c2cb5..b76c121 100644 --- a/Morpho/composeApp/src/androidMain/kotlin/com/morpho/app/ui/theme/Theme.android.kt +++ b/Morpho/composeApp/src/androidMain/kotlin/com/morpho/app/ui/theme/Theme.android.kt @@ -40,7 +40,7 @@ actual fun MorphoTheme( MaterialTheme( colorScheme = colorScheme, - typography = Typography, + typography = MorphoTypography(), content = content ) } \ No newline at end of file diff --git a/Morpho/composeApp/src/commonMain/composeResources/drawable/BlueSkyKawaii.png b/Morpho/composeApp/src/commonMain/composeResources/drawable/BlueSkyKawaii.png new file mode 100644 index 0000000000000000000000000000000000000000..79cab8e52577e5cf673571da04a5620c1f4a8154 GIT binary patch literal 118701 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

~&cTVgwzZ;@bfOAgiw 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 OE*Nb}?`#^~uKhnq-os1) literal 0 HcmV?d00001 diff --git a/Morpho/composeApp/src/commonMain/composeResources/font/IBMPlexSans-BoldItalic.ttf b/Morpho/composeApp/src/commonMain/composeResources/font/IBMPlexSans-BoldItalic.ttf new file mode 100644 index 0000000000000000000000000000000000000000..31cce79bbcfbe987eb879b720f124d545f7d2c53 GIT binary patch literal 184656 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%0DaRr+TaWE&dj7wSR?wg}2849shS&ZAxqPtn)wV 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 _c8|71J42C@V6HcM8W^TY%%*gHu!GS& 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 literal 0 HcmV?d00001 diff --git a/Morpho/composeApp/src/commonMain/composeResources/font/IBMPlexSans-ExtraLightItalic.ttf b/Morpho/composeApp/src/commonMain/composeResources/font/IBMPlexSans-ExtraLightItalic.ttf new file mode 100644 index 0000000000000000000000000000000000000000..5bbda2cc0ec9caba290363017aeaa9b78af50e10 GIT binary patch literal 187448 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 literal 0 HcmV?d00001 diff --git a/Morpho/composeApp/src/commonMain/composeResources/font/IBMPlexSans-Light.ttf b/Morpho/composeApp/src/commonMain/composeResources/font/IBMPlexSans-Light.ttf new file mode 100644 index 0000000000000000000000000000000000000000..b3d035d5da3772f0f17f53463df8660e2d8cbb1f GIT binary patch literal 178140 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¿sF-kjnF+pO z<`7EF=P2Ph3vSmQ;9FLzpMg~yI@v~W{#14jktAQ&@%j+ z8gX>j63h_HVDeO+6PSUG{>{lUuqXZ)i{-JxV3 zC%b6jn8pErKc+@MR*THzK7~zrxVkbS&#zJ2P_Ln6UoW$wtOR7OD|Y!VtFuEvTnTfK z{jP-gTy>;E{8fvsVEgVm?(Kk7j*_2IZZDDAJoT1#5=5b3G;)gbi!z!{td(|c%c!>LheAih%j(voTGOdM}l^z+rJ`RYb z6`>YY`i2!K>sq?J?S#tFNK#_WmyDf#9&6DRQc@Bj2U2x?59IX^V_%p}Bk1TGmg& z6KOZ%{85RX1_x9vQ8h~UM1s=hp<7X>A{z{)cA9le~ zZw3Jb7yFO2H)wT}eZs37v`e2yy+s5~8j` zFBC~1$%h9Y-8W++jKo!6#YevVw>tHc@%<~W#3>K!(@*?zQktK@GkbY|#Rf$JXnBz~ z!|xE~I;!JKq{l=yM_?AEaD zW1`AOb>PSB=KHG|`BOb!QbUyVZLmiqlpj{y6ok#Y;yM?g$(_?6S{aC1oIe`3_9|obdN4AV=`oiz`B{uj{uEbp2PrQSb0> zCAKKxmx%=Dt5B^!rfyO2c|XDDJz7jK`YNbuvOai(@&7ns-XVvr?ATw#{!iL(hm-~U zw~{Z?K6`0Z=pH7Cb%ju|UzHLPGe$-cwns>DD>RMI=;?J?9d$&G#lKa%5i%=szE*D1 zj-0WTJF?||?BiV@w8O7(au)f1&sJ<%%F6H*zT7+-EjcPOd4LrK;h$_ctd zIbL@tO>~FSOLr*kb%)YccPPi{4yC#7P)^hxN|NqSy66t2lkQL&>kg%n?oj&c4yBFm zP_lG~(u|VUVdpLse&KWbGp)=>gyx1}&2Hm%NgWtU+X#A*bvUD2=OsmSCku3H1*kkC)3Ri%4_GYW*N_YAN+7 z#QLK;>r+|JSC#TWzWujS9^hJ$=>8o{87b_)W0JD)#7A_2{>!HV+SR~I?~&uTtae$`^Hi3F zkpFd7l7--_FAE{zWS1p>gH<~-%K=_imdu8gTVFr_fz}huxdRFiGAm$0Y17|6-SMw) zy3+<}@EK14!`9b8J)z(j)00`EF0Fl7*P|^dkwGStSzKOBnQL0;9#i(2t)Mz}*Vw<& zfNLPXAT1QkC2PUuAL9C&Hu8B$zm;py{;n%kp&S0HZFEH1h}@-(0$Av5rfZGgWc^!@ zX3QX8a{58qAE@BkO|2SH1Nl344Y+bUS$S`v&NZcPQNt#LZw53!fhXIJ)mMFH9f`SJ zpu>Ncb`~@k^1TvcH50AxywIbkyThtR&D;sc8ng8YzR@GIL>AG`;WTX zDw2~(IM-ynu_s_`3N1sVo>L@2LJL*bZ@rqV{@GjkbWGCn)A3<-9HHQZXh|jFpFGmW zTFZ(sQi^EBCep_npv0%DUf~v-Pk#{GfBg7s=SVdA%05}Avm>qaC~u(&zS0+i=XWql zbmZHAW3nEHUZbmDp;d6p0CesB@Ej80JIlI?9jA;`dW*KBtO8}M;xBtD!7BA5b)lR} zF`_PeSOLkOA}zZ_-FsCoo2;`m4CB6vzi@i@FcOYXo}=K4XmS%7fSQC8TwRl#>r3nN zMb=CE;FYk;rmV6Dk*{?$MUh~VQu1g@<(Wt<|MAy!5!bP1z)AU<_`gP0%~1V!>mC$e zdBmTdCSfXMLTC%>_IX7J+&}wDzWDzCnGXDP4j{hZCf#A^93ZR7s&jzudI`f$LGxff zjY!cDR^|Z3FTwaI$F)GO94>!Rw%U|soAif2cqC2fkN(|PViQax%HMrTiC)Ad>ws$X zHh+Yb<2wCvtoBE_HKT7m4pxq3HkZEgBUtPZ9G;)4X)OA7YSo=}dUt3Kd&sE^d&E8X z?58i+M+#*Rr$^Qj6FC(WN z!dHF){WDVk+D`}A(<&;b$XFu%DEdQ4LyCJJz3XSNb(B(@boWG?X}iey|MR|6WgLY_ zTh;u!0QBg==&}kqj9uhD zcOHZL{Lp_7uEGs%pJ=FOP-NX$$!W+pL-;~0Y^QR0F|9YkC}MMOko-$a&% zu6qB^b8dB2cUL2t%=>*+zguz-8FoT<1rqX_UJ662vo#1X-RymQE3ZKDS9_6tS(gXK8oS@}=qZ3|ZAJUCvFhjh8N zhEfd9!yqxI#v=$_reAMjRs%eHl=8)YdKO(N5v{kx&p;PO%D;-E?L+xjK+b$N&9~Yp zWt&VZx5n_6>m_u+VWorgjMQ?NJB9i{K4j@da-lC-ndX0f3p^i}zoc4Bo~1SV68uw= zl;)OXuA5IIi^2opFMg+x2`~gZ{BlyQ;2ylh)BKW|b>g!b%O~+OjZUN8&hY;S(BgF9 z-k}nGk|`)JW7*vEPi85NwKJuN)+3XR1O75E)v>~h0+)CO`VYT{+93CZG%RrL2TBtO zlo@0ZUlH&kesjoXi1vI-Y$4w)a*me7O^2d}*f?Tkt_N}q%@gqz^ajKK9Eu%sk#>h^Ciq1ogQ2#| zb4n>}s-BCUcv{YezruA&h}OgJ8TBigktgvMk8#Lcj5sKBi1X8&i9aWjAlh@AXjk}I zd#-uWg-NWOn0P7oJr{U@6(=6IswDm` zSApem;Z)A{gJFqV2&T10$}*ZGjy#p!Vr4`=`2Pd>)V8@fQu><@=zNkk`H40jhe8q! z6@C$GAjcTFQ^BW!`LH>cr}Ty989H|_g>!*>DfpE_tA<{{qLM%f1N0}rz5JzcXdWBq zlG%M$IZ6Hzsd^~*xX7Bk&QY7WVT@1FP9lxL65t$0`&8eFeIy5%UxFKA`YN?D=UHai zOAkl15aX!&^l^dMy!h`;pgpQQ_MUiz!(TipSl%aB)r$-MEm~XRD#4fdZH|GCOJV~V zOQ&vDc0u=u+{P(XQ7W?{;XCwE@y?Bv6uh|QfvMogz20=rk8JGNgVH0X=eiY3`7Cx` z;sDZf# zZt&o~6p{>lM!CYFGQda|zr3y<^vSJA?=kRFAfN8I)^U+bw zv0fR_{!M(PDDn1?kO6d+tRff<4@pl1Jc9Rk&^%dumeF@F1D{^*dmZ1Yh-=1gVZ9*n zJfW}n59DbQfBZ7MjHk=RlyZkF_=Ex|CWJ$=V$4f|1QdpmmkqzKv zw1iJaO5mXaxL^=|c!*jVA)z)h1$p`r&bBc^qE?9!5gCtp7I@wi8v#y_ph3m1`|whB zQ0q=&Yr56}cQ7`?KkSw~T5-NgLp-R$kl%xHTdGo#b&#kBbjmG_CXBAT+L_t#FlW9cVLth^DJ;yIUWk2;N* zH-QH)*CqQ#);UY&ep#2YgT7!Y@JLj*9ekxTKV33!`oLr+CCJSA3pAsqk#^;9U2=4$ zg5^AFmYMUDz;gkulJ>{Y-XhvAGv{Ni65iy$^On7t%UM57Fw#rgYuQtp8Pz~1Gv|AC z=6tWtoNv~d^NGOHPNY|6&Zj88yuj53q%w0pNoUR{>nK-?j&e=bQ7$Jev(TfXT+?-w z%cG-Q)5GSOM(HS5t!5TR=_psNW)^yMl&eNZxnw`+&(JrMd8Sj?>G`wNEMr|0bgZjK z^9$QFzp%~CFC3X!XzM)uI-Q3OB03IuE}==ixW%Jp2Zohu@_0@Ech#E}mKO z33h_j$9~g9!F5i4C;LO^a$RQT_p!ovvDIal*yWs8+Le6D{QL%;pWmnR^QY?k{5p1d zp2eLqOFvp?=}*#G`Y}37KUQby$LTEnc%7x+tF!cbb(Vg;&eETzv-A^mmVU3!(pO%z z65&OUHBD#fdv%t6lFrgk){&~}Pnb(LE-aU>>b!I5TElYb#v>J5rYP;N_8Zi`ZGbKti9SdN3dz3`mq#{h4ya$byb zUYv4XqHc(O3y+tYSV(;J-xpuZ7ZD?Jg^c zoyR9~J_(MDQjSbkj!aQ5Ol6nxd92MipS5e#Sf%>2R=TcWo2Go3p?v96zRZL#|K7@y zJ-@A7<<4yT9{YP%4(r?g8&@8%AFztq$NT42p7LwH@@s|iYk~bccIz&b74X>ue!ab( z&kd}zT+H6vTdfk-EPR{u?W{bJ%bwfsTBXX_WxD=EIja_)wkqraJHQNkStqwbxx7lb zyjr=OQJSucz@atqp>ChGoDov-yJZY1fGyF8g~oGshYZI5H)la_et7M8lc!w2Yxp1k zw|SbXZ^=qJl0Wky-sFc^ds)fwRmQ6f=E~A6n@Ok$_cNsg`3-ssZ#eQu0dIPh^BD@0sgb$yB*R_fHUYq$|!z_k3fMX*w z&K_Z1DA7@)Uik2*nVa?(cm?}pLMu}o-lKX@kqZYSta}7igT2~Ox1H zr7tD35kG=Ltfash8D%<76pi=(C)yAJa_z^B3GG+_M7kbWL(AVsE3-oYqqw`cCj~7$ z_RDyvS@_Npt&k{%_D``|<|Xqk&-MJ+C5QdPIAMer+KZW{*i!MPJ?^)9+1i^*g2UbT z!0?Cl-{F@&)vv(Aksc$bN=6Gko%E2Gapj^*LcO4&z|QW+V3S^2EAmq;0Kg?j`dKbzY0D+ zK$8}t$9SvPAy|}A4qnh$^8NqZmugXT&oM0T$F@`Z;;waux3Jj?NdQ<9BE^$GHE?B- zo>Z>Z7#Xs6T0Jm{C<+YmNWiOT-H0|paTTe+Pj!tNg@Rz7nTG>9LMr3OMDn;I5xt=1 z4l7+bE5dcTh5uNQM`97QkJT8VLIP59n$q7f@uaYV#yC|zkgR87N0Mc&<5&Yjnc+b8 z@h_n*&`A8_sA5{;BXR|sug6E&vwf7llenJo+3?%sj!3V_qYmKCoqIuGv3G0xUvP);^$*7~w@PKyen~ zuTOL-ver`$+eH1sY3}Hf(dIfDL|%_-wox^K;qchisQ=IZ0yRinroff-hO=!@M%J9jx+#*C8`4q^$kO?n~72ocOEeHu*-oxW9tM zxc{+I1>E4H7oLMv*^I|4Wp%X!sLj{K}K-;-Q)`7AXTyO-G_BD_=9KPKZ7M z4*8T31mhvbSaskdb&PWLvPNCimPMxRxt{&_|02s)r02kkyk#yDFZvF9BtpyCudp3L z!4pBRZ}_+#d~du$xmjjV$%w4bLo$@K`WN`KzaOg_z-K0V@tUW<>q zQzep!o_mKlPKNkUdSHhL8Q1CUz*#{|I z)@BwSkvk1nB@uavrly#1(7=+8fWuwb5&Bn07`OO4|mmWR*Nw2|(5%_X3e*0~q_9n&d`Qk@TtjL`uLO4R8Nox=D#!35J|Q?J z$A82_*kDF=@E)Xh^EwneL|n^^5&a`jy%4x7aMy@O_^DVwH}yw96m})3e}x;MovcSE z@6$-{b9g}LE3W+t2@-w99L6DO3oCyOve&&dRu!u{iLq z%p)@Elti{TzsED%gT3*g2cJjN$vBD3EqxwoIcBBP{#>;V;>8SQh6r$!%t|PHpTbjS z1WSL5ZWOB{y5?Q!pAd7#lT?e71rKCVs=`65-CP*-Vhh?UTlgbn+rJ4B-LIC^$ z`fRjPq(WjUS}M9}8^;!`+D?vx2IKgp?{i3r*&Hbm^orK$%6f3ekYLU@kDT*WH}=0; zr>jALMdmp8M1Kx{0z8Zxe&v(@lKvz3koD#NfAn&pTK_)_)!1H?WO|3c4CO^O;FrsJ=oRV`rkzj^K@qe1)#a81u;2XgCYFg@dv((PkoztRrr&xev>) zpnZ2Qg}$H`*$CLa-K?l~h`cVDog$y^%g|YPP<=>rD`zLfN*XK8w`AK7y+r5Bi#rbR zHB>LTV8dS2M^q>L6+xpKX=gVg%46?`@Vn3dTx|64S7dn|ZYf4Q5!S-cKVnTr%Aoo8 z|B?*Kx)99!1`ZoYc4MIro4z2Q%13ay%p;W7NbCi#8-NAFN-w2?tlxE*=O|TPl=`xk zzLhhHIvv2S$?DPCJ2dx%K3SP+fUGB3nIMXhrg!n*i?qM`pycY{4Y1l7- z`@#5LG;(gF3gg1>BI*hL+7{@DelUKq&G3Z_xqY{rm^eb!{}YPE_aAFl_&tcbSVj$mX)Lb1O)H%^mfY1#2XcBAd)1j z)V=1`NQpoXue#5&UzI|#HCRjdH+V8@JI8TVcCQiN*kCG#65ixj#Bpaso$!)A-8s4- z!VCP#j2-iGBQInF2J__;#)^OPR5(>5lzhII&_@#MliWiu5w<6Ze>}{yUOr{bd9kg* z+1%kQvQjuo-~mnY830i5&5NhPWX==7d?wz z_}0XC(7zj1lc)0(R^PlL(g)7fmz$TV=lDL#?HT+++fi0NF(~6RxIHhfsuXgEWYDo% zL}XpVeZS#Qf54n;4S#_TDOX-9fAT%72d96H{Lm;X6qNmln5~Nyc}H?EIe&ycn4zFq z=gVlL@aJVudtOS3EXAbsmQ2hi9=))Q5@eo|4+>_}15Tjy=ea|$CU;7NE#eF8$~?RT zvYm3UUeX85QR}o@WzLPaMtpQA-XySVO+ikP*kI7^<^c6>D7cddeH4d4Bz8Q+SF#UK zis?It>cKJ=p=)-)pBd=3M`+I`v{MG3&#Nx_4d2hp_(>>3U7kkkJdNkn#n}hM@ly3) zsk4xH0$HqFe}(^TS?$DeL5LN|4DZ629cR4>qATYfRcLm3=4aOpjvJA^>|Cnm@Z?K_}m-D&Ty(j4Pns1do$%Ouh zLv(uts6IjA!CR$gZgPi%S0a8BzmR%0zePT=!xF_&4@tSog(ebILl!$?$$!qpAcp`C z+9qU2RboG;;7>|H>K`ZwL&B)KPiu# z8G1C(M|hjM-{7dL$eL%Z!|55&U=LH)WqA%R=rpeN{ZpC&YuXeQLc&I_&XJ ze5!envQy)jc^jc3p`Sb2pdv@9F3&QCF#BWvryTVwHnloCnbhe{uPrWQZf@HeXVmp8#xbotY^^2rFg%xCv0)#djdP;IxEw^ z0A6H&*=;(al*lvr=u-C1KvQi+$D1CZcoiE$C1IQN75GW$+IgK-V)6HrT~KkZyP?%_ z;FEr;=&LQ%y$yfieGZ~9@+3Zl#0BIPL0_3a&i~WIEwH*zp6eNOsMs0VB|>=14+P@3 z4}inB@U)D+7o9KTalyy|1)j@~7`r}Nx{VqK@ZQFV#s%eEp_1`IR=4N#UF_$RJpDV# zX8=lpTA(<{L4ME3*C+EC;459XkzRhu$db%&;|ah~FVTbBPw#C%T=WukZ^I^NBnR4> z9Ur7exWgR5vVv3%tPfr}&+kIYk)6fH;w$wBsG0Srg{QciRiyc3XD6<{1(f{acR=SS zN6?`9uZjGhCrEf0sp?^ljjQ*xREa9@Lfgvd=?C=XB@*f5AMgcNg>WVQ4jL`^G7`)+ zEhTvMJigBlSFGNI{R`qsY>nXR0{SP4KB3wVwecd�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 z zjk2${`6r8i3c~&=hZ7!!6CUOs@yW$rjvn_FZ&Wo4enj}#t>oOF|JbcSR|9mSBWi%E zhPq#-?w1*v%)){S28#rX@b*m2tHW-?Z7a~CX0-KHys89XDHtj#g;LVk7d&IA)M85g zBc=Wk`;>!y%E3NKwpkALNxTHfnjxzgE6a1(rv$uS8UNYn$>cjL@FX7B@9AZ|K!0!p zp6^yX-$(I$*Q*D(kCOK(Hcd;U{!rc0qW|KKpJDH^Xh{K@qzEsB2sM0hhE|0~X1(ap zt+XhW7KudufhYd})PLD1tN5+QDfu|FI>_#U{(`MkXn?Q4e+T&QfO1Y`FRg@@9Dz2+ zXbD~}@?*wSz5!h%CYnMkc4!M|E7UEbh9YVxqJ|=F$wzcEklPdJ zG~xWvi0UeI9iyqxW8}yv{d2sXu|`JoO=&|ux#aBxpt58WNzYWGrV^8o%pmgUEQtV> z!ILtwPwoTXCN3qpH{0RK0dNzZdlQV~$r#fC{Nrx)W*i3|zJt%{;Bz|ooK6x4KBpB6 z)rBs3suS>zUD}8Qvi>-JVhJnpRq3-{-&Ap5}kt<2`%4-g%T7TH(Nx_(jF^ zc^_M6C0h+vqt=kjv(-a2!up%{Rm2q-`Epdk~gu@V*`WT=`A75w5@-MksUM|%Ay@B~A1l|vU##G*4_OpcNI`3@WX{N#Ki$%gXloh7ex6JRq;G*8I)z=h_?^(LQ|8h%Z zYGG~OmCf}z;~RuF^fc^u!CX2q=dZ6r5ms7N3Sfp{e+Y94f~bTr%%#YMl&}j?av>@V zmFaRJ-CT&Gjf^yaO4cK{610K@uCcZb*NB^1QCw?kt{iGTP0ns@Z%r$&)<(BAIKg)R zyT>s~ys*CY+qd8T?biB*eycjQxIX`d+`6K) zt1j-p_$q1ZGVE>~xsnmv>K$sU%L9c&Oc5h3Ay7JWA-rkIM_##uK#<%KL%Z^{T^_!@ z+E(vS`{|8a8QRv2*0febT3-wluP)Cl?`-LqXt!0BmnA!pxX4-7?a7#D+InH8r~3t( zIM;2iv&;YdP;0BTiD^Z3xi93`7qbwRjURv9-UwautiE*!RlZd~r9nv>)u3>;7xd2zZn{Qb!>if*(*eYQ4BbW1|bIgRJ@kwi8 zKlm@e_xSQUM7_?cS5_!Qt`dfLZB(w?}t=t&P zS%RJ=a+a*k%qVYfRaKc;p4pmNo>snU$&%{Yr7Ksi?6x>V4Pz+ zQ>=EYkLbjqb-1Got&0_rIZ$NErnQ!QUob?O904LH3`~z)@K_m>F8iJ6Ej;q5dU@zE zYYwoiBVsVe@@RqGVi2Zq;UW%aC1kpt6MNZXR;!i7oe6w*>z#Gm`19BBhW%w8na|0U z9Que157^75Ch=^Y^&O5Mb3DYco`XK06|<5@mUHqY4u`@oP%u*YuG7LjfHpi-*b8DC!jo^$NqtIZA(o=o{p- zbP!mqNX2`yinSV{cb#6#=UW&Zt7Q~;XAy=P1wPV-c}7~WcW=0E@}I9Zn!=)s7tXJ13tF|2dM3!%Ua&FAx=1 ztg-nzPcV}ghM5o|410HJbEo0J8up%M*n1l6%}O1Hy{9RAH@NKGU`N$V6i zxA7LztjXeKFhNI2lP262+vqyk$pTe!7=xwB1t#=}evd_LEu8y|vMOAk4qOa8pO{#T z@!$9|GJG6O-QCW&e5nbkKK=NXtTFDe5@(mz9Y=d-tt0GcaJANor@%O=KQ&mti?@*b z=DI&K=8LJf+gpc&v#h(SZqlLx7a+$P{0A(Hr!)kVcw9oe!_Y3t zTCt8|ldY66+C@u~qr-48ikL}q2bdgkGUMe!yj)mEd)cE&QR@I{;wD46$#!dOM!6?8 zQz&-P&u9GUof%Iq5Nc&FdT5h>lkHt7{x)#2ZqF{dIqhcIPjSD4Jmoq~A=?}BKg8t~ znQF!N!#QfrTvJ@qJ-@8Ld0=oJYqLqY9*ITQSpTnVAYQ9hCs?j6z}sQ%bz2i6f@|bM z7eY0=d{ZoC6o+9XQ!X%BRZtX<%$F%QNAYcRb1K(z^ctDQRwm7qPGf6k>m-kMC)|z{ zoGus>nN8!SCc+S!E@cxAOJ*jR>>A{D!%Jd6Ef;&QP0X&k_ zO3&aJbj!el(Y`^MRj#b%8?q7S@JdaO02dahScOIV1|e5doQUB{)y`;L*i%x{v#>Sz zJ?GL(=Wv8v%jqj7PriD3MeAoSpR)8y{ea(>1-j^~jnOX+_0`7e$%Jqlj!NyTxrQI< z|MMNgA~YX20@s<$o{n>EfF9|pxJzcHkhg0@0DK;sqa z`p-PP4i6>Aid7)8aiNVP*xpvvXg7F_qaExx<4zab;)ui?>btVMuH?ej2~(Q8>dJd+ zv-2CKR8Cn~Smo<$@0{4#SvzmeXT2>=rFAtejkS$&vC+P==JJY;iu7^ic2?ZRLDm;D_Uk)SFOXcvXQ%KQ)18tj*=7C+=Q4Z)KDW9(FpfiE6G(psASI4 zRN&Ph(D4%I3Av!#)tdR6;*!gHGE>@ay6FpLh4$SoX<66a-QvGm>;zf=_UlkR$NH@J zG`Ut@7`{ibu&d)kaJdu-*+sW0T#x{ii763tR_qf$@<{Xyd*#b&#qXB0a{Gr-u{jmZ zr3h1O9soza@n>M76c{bAIap2wy-mVcuC6jj-1U*={}Q;M}(>?i%*FuZ(x zO>XEiRCTzor^4^XP+VAW8*m+BHkqhv(hzJ0K2N!)ytL~U=WpBp&bhhsJ2SrS+&&#x zTkZeW!?J(vUjWQ`0R9RvCyAY7>gzBcXH-vr6w`(Cn1bk%9}uND3I+v&j2M@mtr^v= z*{z-8=2Vx*<~{z~zI#0Xd&;A~>-#6?hqmK?`Q-usz}BsT%`9~2pA@nsQowM?Mse9F z^tzx9xrE|TGMoT7#-sFtFT2%~Y>RW)>4B#9-8~1rWz{{4n;(a^_=I07$!Ynflj5HU zG-OfO{{l4e)=c4|1a-HATr`SlLTCx<7+c9C;RX@H7%FdVw@cegGYRDs+a3N_?3Mlv z_OxdEhb`m%SGG_^KHPMvBaUaHwQ3!F%cM?)J`So!4_S8C56vI)GD8-^^}0g97`Iab zvxH%!&$pg>HgP!1D3W61fl}f#33U62URRgKmcvbUxu?|0xzNAm!rqf}Px*I7-C-xZ z@rJW*aE9}B|FN4< zld~-Cf=anr3V5+Z$&OHwEgjYM<|LXPXE8la675VcvzT6{{E&dZ7!~Hjt51oaT}td7 zuO>FL$fF^W(oR);(pNMpP|jA^X$zNS6jf*BG*#u~HcqYazuvoj^0aA_x4+cg+uQw; zvu@eN=D_$ENRYOHtnh}e(|aqpUG}6{%h}V9{$gD7fmnh?)&yP=70Tf zXZ9BMUjmtwR+mGoI68N81TiF74M|1p*c2KzvZAI#Ae14MbYPYKi{O>MmK*b~x6@W^ z|K`2-eslXfpa1fgKmQYF-LHQA>tD6sx$25lcMd)z5)RkcXMiNp`dm*1Ci)Iu+%uZ?iKdk{$JX?{-@n~5fkN^&%!er zVV1=RCBhuUh}-K9VPuq~*GhM))^w+2fTlsAOO&Q?FI>ZS+7Tpj9ba%ur3x z^>_Un9k)=!OBhT>Z=70B#&rv(UOTI%X7=^dmfl>_oOktfdIH6NbA8Rsj^dwe@A|8b zsSQ`$b=i_{Uf#H{udruY#RXSQT6EjDGV7+)U()9Psf%4_sZ#zH(MiK8?=~ycPM7mS z7e*OIH^8MBU`;J-6s4YL15Q|rB3_q z&o7?6Jh|OFwQKSeSbXs8vTH7ymekOD;cRi|7D1oQ5R=KAIA0mLXC4- zFLc3`O7+Rq0}6Sw#z7O~Cv9~Y_?FmM6u%|rBV1sYC)$j0q~U!Ks49298Fln%RL{b< zJ#WwNasHQok^PIod!_yM-x*#MJ8R?y&pY}P%T0(7u4@o3NHTOUuo|ZleXfAnP$sSX z$DW=!_6>YE*ACtX;_l;{d9A9iZbn}vj*L8v>d%J|7A_$OKHMtfli5n0AuER=2^%JM z2T$!~cing2U3cDh-<>TBBm%LZrNu-dyuW?;;oq+P&;MN8QI4)zGU=+mii$oxxu6^< z1#3-ESoZn3O4Kr`x<)P2%EgF_rC6|yKd>_*ZKe3zP7KeskKirxq{s>Heifvt!3+|D(FA? zfH85%Dvx46R{<&erEd%A9CQG*6x}0G3st3$E8s<=4kp?%UL>(u5FMM@8UfdPt|>0Q z_3s7UcYfviZ;e3s56kiw#Xb6SLHf^rleqkQ5zy}9eHwUAg1Rq=56|G6BF;;jl84&l z_A){o;u0bxQsHbW?x}GJNtm^4%KIvv+QuA6aX&TvnuClD4;dRQ*d-ofKu+WellaXQ z@L;Z7X|6gW)3~jaY8m|~`<=u#X2pi$@605Y#0tKx1i_eoR zaROjkXO)8t6R$vqg4$l=74E>`=y-))u=vK>@s-P_FSvAe-=wRTmQ1*~W!@Lc#}{5Y zdCsNtrcS!{lAFC#J1UzyNIdCHPV#+bR>Sm$oUYc=y75z|PU=ZY%3j{T{Ce7hJg}dQ z^i(E_1|8nR9%Uh2aw~O1w$T!8h$a-9X!rE)b5fl<2XAriY*&jUc^;FgA=jEA5$`-J zpGF6@zMBIS!k8N%3|+_&zlasnB*0)ye@2>)pPO)kj+5f|r2QbhZbobC^!oJPyzXUv zeOGklpnMnfEb7e9?_AVFkNGomzc{n3Y~~l|+W$(sb7*&uwwuU;XGFV4X_HZi2oZLz z<)Qhd(g`KBU_1M!Zj9PA`2dRc4(F>z^HY}x57opLl19W9KEAF|MtW2bbvQKWMPxKq zRfxzcOmCnY3NAeN(3Jn}`L|g=@%;0oM`!%Z`452oj`JlwW;kCN{5$YNY=Qj`ME;2_ zI3xUskGHJhs!c6xNG}Mw6IJ*{E*bGrxhzE*J>?lL8l8@QHVvEp!H$_vIDeeJ({A^_ zK(Fpn{~LC-pS1u%A$?$=i?^1Vn4=7!1$k9_d6Pl*E z(4^V+(F>mUJb%G4wzRt6|6_Z(|7!nE@h>ZZBUj-dvfvrf(-_4xAsk4Wn`m}Ni4^P) z#Zqal9Skivxqh$x6aS6&M*kJ|Bc0B?_RhhFA;lErDF+P{iz(C*gDf`%MaS6`*`xft zP=kXtOBVyBDmJAoEw!~JEp^05!MFpwmMmJd*KZdoA_1mnkNb6WgS%C_+F8zF3Ckz$Dy2J#e z$1U1FO7j&&D+JGS;0j|Aa^15~3q6`=n2qf>$sg6ktxd}=yClKx_RZ>EvItw{n>Exj z=SR?KLan{DqigW{dg}kT_Aa+q#BNa1cPG#{vR5=pw!?YH^ooopEXq7eMtMfww8!kE0 zF&4=Yfg|r&36cR`CGI7uT?q=4N>b+}+hp)b-X^1tMUDiGQtVPE)j#EU^Q8X%8MD(m z63W{9o0{jg=ld$!inrKbo4Bv(n)ZxI^JaAUN_xiS6}HV9U)48pLg9;oyLNha>~BKO z&~IgIvCV1^gWDZN4Z&S5#;e?P>Sa7se1LW2Ot4>|!rd&~y`Bro-CyG5b`IyuWMcf7 zlZQCu-%T7lIF4|{Nb-i9sDmVTaS}`RM>FnhGwy7&<-wgLDojmS9;;bvDyQ~3u3@Q% z(kwzq!H;wUYC{f)AdaA#?d@MMZ{oyRS>wGG%~NXYXLl6O>mj8z2aluml50!H7cQ83 z;o?i?EwTT0W_J?9Cv_!RwI%5}hW{h3a2)gjc=YaEp^(QHdu~* zGud%Wy!vG8X3@cB^vUSnkau|woe{j%7$@V?6BhN5%EFRa;yW%6p4=`cq^y|kwZz z@bUF(A#=4AqI-DGTs4?Caw8m&t8^*higBk)1m=e*QQh1|IWajr?v>m1itu`g4$uXi zAiLC)En}dDrOV429M^FSrmV(RXO!Ce{m*5^Y^))AL7V|8n-B19e*~jtdgJtO+XYMew*--*H1!aYvsb zQDv=_0$)llq&yT1JXUfgj7IChvofY2pZqb=Ps20BcJTWM2l^=*EmWywMU#oL(q|>i zZxYMgFu$8jhO!!`Xkdb=dJi%bVa|f|baNz!`n)E0Y&cCbhSZwo;zjm`pa5JmDNF#6j6W#* zt02IMGK22BU}PLwk8`Z&*3sF%(3-29m<>+G!;SJiZ3s8nqA#<<^yR4T!Z^8OoH3l) zAd78;FlUy0Yq`?-WPv5FwNa4dH?Cs?Mf0z?RBhG_y;eGORyK5B4FRt*Eu{vxrBX|& zBW=mJUenz(5=mwB+EpjP6kVfkMYD8bHDY43%WX3OBzuwv&D-F3WTd+T+zHI?_)=Vn#6my~xk)@1y?dRa@$m2>{V`TnHw z$>q~4Q%b99bCR+vbCdiTy)&BXUrj2gXe<$a?F)R<)5fgc>DI;8N0NeeiM2>uFRt1Q zp@|*LO9(}%V8#2n)n;r_qGuN{BWA5tunk9pjcy3H6p%k!V0ldbXaP*h4$Y#uzr%@Y zZgx0ANpqDFGGTxMN|~XhNHnWk&#I)nVn#4zi{!~1lo!jqohrnjiu(0}Y@}Kxx<^Jl ztFSXwl&R4Kof`8P3AQn?K`q}hsbZ~FMYHOuQN|ye_zxu#BxA^2GQlHUI?U`AC7n`n zXwJt3d$^Wy9@uyB-(TI;^|^mo)PL9KCZ<S@6nFQlRGB$_Wn)x7k;q(Ge7uWz2mR^&WiSBGin|*oee#6rcIfQ>anfacDmhX z&qwYuMIzWzk7ip!Sgb-lvETOA%-N^y^obKKW}>-wGuM6=|J%&-Wj_s@+;}TH?CIa} z59>FQLod&lW>z>+b{63qbN`+Q_e<6RqcubKKkeRM6u6qX;NLgp%W#fD_dCjtcJI%o z{383zniggPu&*_1D?QAf)k#*Fz%zWjBHUccoHNpSAdi>1cESQDb#eX?ctQ5daUKg;bBkt+(xi1ROPv$xt_|8dIWIlF`Bst|Q) z;U(6;^KF9(Kf6foqSvG?9Lh^lzs-#@ncP!am?R0fitkAd-jp3%WDC;2nv?C#F80yL zSYKMU?a$3j@-;NnrIk%B@nofXGjpk@2~0og90HSdqcQ2yRO^YO9`REWrLIxfyeo*! zjI?a$&_^D!re`)7pq(H7yzkLJsSFx&yT))(k{*c6O@5HtIE8r>|4j1|_ z9F)<7y*&(+zls0}CEM3ga+R*L5zD8kv7ED0_HJx^nvD6xYET1>>uH32s@prWW3zWZ z^;G(rHA{cd@r&yF?-wXrpymdIOO`O#u?b58Gp)7AKs3D2ART$b3uds(Gi0a3)6!y~l_N6P zP8c!MGfWb%O)E?-a;5RHeRiII_daJ?m{e+wC#g}8kRjRW!)uHls&TB?okpCKgk41N zJg7KN>@X1iCUsI;vJ1j*?EHQE{O>@{|jc+vCey zXJoiQZn%0;!_1bVnA?8lxu`Frd-?_48D%|n|A)Ibk8i6w^Tu_qY|HTyTef9+lPy`c z6D8><-xo1d_6Zr7Qs&LMf0?TA+m%rtP$)ow9U=&=%U6PI;#b z=|)S3wPj{Hr5$KW_e6fb-*e8@m7>t;%=`KM^9vl`t4CM&-t(O2Jm=X@nQ=ktTURao zK|BW8if=774D+oSNmQA@Tbf_FlHI^YA`C66#p_p~m7~$XNe^>2#2#QWdX|MmN7tm{ z_Qc$d$)S+ z+eL@^mh>IEXnSqV)=Q7_?imYL*iM;u&$aIE#ND%acP$air9BX-wx6mFFRAMZjE1YK zIz}6N>X(E)qG$QhOSjh4Zr69;R_EP%>CL?RPH^o%;O-7v6UjH>A(2uOo}l7cC?AnL zGNr`frcD8D=ljk(?^gSbt*3v|iaQPCPCt})s;A8(aVO?x7!XC_631N_Osnov437#U z4StBb7=Ju{U8}vZm2c!;I3)6M9}nNC65>At?rqTHa332Q^*Pi$({F?dQkr0yQxXk# zMV(A%#{1EI2vrL2`8iK@G#HFldp7NiwzNcd-dbBB@`Ei)T7n|KqP8{u%g{(>Vdh9E z{!8{_2+xno=c6A**7H4hb|LzK>Xdl42X9d&pAFXTVg#s*fxs%G1Fs?FYsT4HySrP_#KKtsqD~CW@7y&%zia1w z!|-^=_^|vFKXwiv<|%NRaUun$%rgBIsm07PrS3y~@<@F0NUI5t&HC7ArTlQcml#12?$Z}~q`ri#nY7mFcf0tRN zf8$_@2mhZ&ui;!HDr|?qIX?l8ldAJ@@^R*3Ifejyr~{n#^mULmtgDg7Y;js;GM1J~ z{w{+n1Ab?hzwPJ{?-RS?_n-M0|58R;y|5dZIgIL9!I4eD=Ib|8Ls|joE6y}8Kr$M)x`oOp326OifE{`hDkv$NigiwvWR@}*51IHE8dcz+#kPK~9SI$z*u(a4JRy}2ctBd3F-sC4WV%?Z1SN& zJX{4;2eBFsO4tyN4Cj5^Hbg7!0<2ZAa^+&p9>Uc_FklVI|5o8|CJfr$@>daj>XyH1 z0SQEt$yWPq(qp#*Oy!Z0U2Y^kpQd1^estF{5UEb!}Nu zxVF8m%j@kD%{AT2TU(cR*CdV=t^K}IUw>;w;$-v(zUm53=G?k+C~wQj@v6ipj40-& z19JK&=*=**cvG_$bR?oB@uUT)XuVNh!E|usT!6@J%s*cMmgT^`!4nnir6Et5UtKRx zlplodI!;Ww+zZk2abAQY6N_EMXcP!zy$PpGO@Tb=cc6I7wLAWO`+e;z>?>Mp?d#Xu z#V3AKTl*VPeAiv^U%&8?kG#O%Hi~VC8-E(oO$&KJUc!#g=$VEDRqBi<$=k6t9;ng* z{v4FU>L4+!20$Pl!G^b@>|P@;V#QV}IlQq`XT;{U!@TJQAcQuIW~b-&I8Gi!dsG5Q zz<7+jJ@pCH6a(s5v8DlMSQ2Wjk_N0vgz_|IQ6dlWzZ60Fk@ba|M4iEm+-Q5W=eC)a zrrM5N@8Y3I|5(#kOX@w&*)8>AS7UCezq5H)pt-TCqO7WEAoi7}nw+A_qK@v|Dp+o9 zt-#PaUec=+}(D7}r8__c}Y=?S)u zaJU0aY}-CNyPYjC9O?`W%RhLhQE>^PtGOx%xPsWA7B|VKrRB@Y5rcb0zEicWQNNR7 z-bpJ+(o|#}7MY9;E%ZO@`diD(TRFwlv9h_ku&}zhQXj=76>Wocb%Sjc=CQ5B*IH5D z<|`@jwUt-2`Vd+tYy;vt+oz?*02`h?N#97-9YP(INN5^}MPuSR<`uxEgI|g@_+^9b zYIM_qzsic|5%-9jjMrf*#T{WUP08ET;~{mQ;y^d7MS+NeW+q4A>c%a1797h6XWUVE z=a$Cx;vUacO&yVY?um3XUFEr!@5;>AKR`FiWy}{g{E{J)rk`8i?V%trW+!K<5 z-INqkES5xP+6!o_?cef7S#lj#wQTs7b})eg%xH!R6oJpU$!C(@mpljWEZ-XM{aTy2 zg>N3evJG`bA=M#euuvs2$L=1V9p>ACYygq_#NWGkJq=?-E!Rf z?mO$+>por=s(<&LH8>9iy&w1f;0HelemRK0^04sbFOxQ7B5c&iwW;>6$Eh;UQy`Ww z05hfd?Kd*M5&Yrc4@Fqa#vd*d2jja3Z&u@Cgj&8#?0}e2+FphpBXC9*7#| zE-#HXC4cx`aZzEhv)Ef+nK)rzM{Gle+o;1l3?$9NCqY-4WFdGzQl|s>CUxcz-=t18 zlU%}i@?_V^lS4o4_$mIE_Y>uYTTJ1%sXiaeCqtjlh0)J&+5g&0X5xYRMdN=K&%OIc9Y4bV@5bZN{~fkn;t%jupq_tH z|3Syl*aTwAz||y66TwgVKWY9i7JthBDf55EM<^`%afbDmHs_4QEM$Zymunso!)JM? z)X3-J<;_xuMK~{~djc<){7zA2SC;TVl>!p zfSo@Je?NwyBQ>qYXqJGwN9Y;XeDq0NL5BqIZqXGZ`)KIqoaS6Vn+sy@%1Mp1OFf}| zj=TErcxW{ee)>D*N&Hv+t1sxdqvHbk2hX2F6|E;vY0Ydm%r_eeF#`w$AJ*Uy9IU?Dh zpETFjNsohC&s6866(Et1pRC%c@@Dk!MSx}}-wY4YFBiwLqr)iuT-N7$zcGWUb;Di=)4hp5c&=mMP?x(hDkG$y*iu6zm)iJct$-jJLV7ejLXuGod? zCB44dPs-`xkX~PCfb>9W*XW0}lVQPfCV)b=h>uDCqv`V`hNo@+EE>cI|Hk{C=Sdn3Az8wqgijy&enVgpub$oX4Z!mFi z@?uf=(o3-yU#xiY$*dD6#8$C2{?YhHyF>uN;LpdOS2R9nIKkmE6^GNAWfonXi24%2N$@j&t!X&ub_a)>CbzmWoEu_EA2SO#^hQ>cl&pwE1_#Ca!b3yh z$jaj4l~+`Dj5RlpbyO~&oLZKA-mzrqvf+WQVAp{B!%v`CR(Ss?w1_I$@we07-e3#J zF8J{3R?K=@K99BRfj44hD8=PI$O%=rh0@uo+R17V8Y6F4&k1fy0*PoM8HVDkN8LXv zZ?5-w_D^j&R99ErHL;;{>E-L%+t*$`F>(3Y_A|c`AN5=~wd_LA9UV(%mK4_ww3c^n zKQc3OWP2x%^`8k34~IX4{xl#e`76eoSm0V2hhLDWxLd#%OvsyQbv%h!H?_+-}ffBrH?YlDwiU@T6RgV$rhkf|`P| zl3>Y1-}U>qwzh`{-A(zvU{9N~(v>?p6Uf^>U6SX@Y0URH#pLewAKLq6I2DJ7+R7_C z>Pn4o=ht*L#9to&@T!9Dc3PWuVzGY?V~O3?Vb^Q5{R_jM&5FZi=s*1@JO=d9vhk&P zO?4#$YmTR-FDFO#O0I5IL?IcJNe}-u5b~w%sSqG)zcwjB^CjqPpfR%oN9N!ACD~z@QaYGkvo^2d%b-P=K8)rAik)C?y9%CO$LF_tnzj)tpTRW1& zTHD)(?+n3?XV}ItOKjkej4(Y)4rxX@vfP9f>1bIJw6&sZ+qkx*q zVF09t@rpX3Ripq`kpfsnut_&{51ahP49_dVJY^seP$JFWW>jOTySvR!kM3C&F>N)m z*>LBY-kO@&s*bjCY;@eSY_5Gv^X4@@FD=`+aoOa?jUsbw&zQe%$$WqR{F1ub8Y8f5 zI966Ry63&|uWnn{ziylS11yx-#*7K$5Qe7Jc0CtdleWB^SB04G6p-8Ge5d!7QQiUB zYXjdXwn8~Q6C=*~q?`fR0o^QEjqh^7M~f1WB0yY#R40B<)?5jWT3;K0{44W6DFtJq zQ4*FFjHu!x?>5DRz#J|FWxAcH zo_cEY&YhdX1FK8hiefEo!zIn`bwi&U9U2J()HSFv9ZPRZd2(4q`3iTzCUh zL(YZmJfTVZDf0vtL`3r; z#Lwhkuzq5@yZv}=tl53ykw;Llq$)PLe@CcjswlK$|75?%CKk>tT#d@t4C!9YXnmEd z28_8(sn{|eKM64fn>I)Vm%*D#%wcK4JUQsp*60JGQc{S?;EojgyCcPeqE9&d_6Qma z0vR6;+?Vyiii}|M9ho2ZJdyQ;AE!636-PRS0dXk)VrTq*NJJP*u((o;Wean*%_wej zEOqTRYay^!$|osxL1v^L&YHB$=uq}(T0v+kuf>rwopo@iMhwzUOT)eyMVyq+>xNxK z&0i=0v(1f}_AS%-Wi`Dsk;y%^ZB?s#CfAqcJ^yWcMq8b@W~qB!q{vqgZkTFqU(r?F z7xT750hKm< zWVT>%0*(xtM^x2~?KmDlyB_Tr+5>2hpgo25EZWOxZ=fxp6*4}z04FtQ#wDD=aI(x} z3=g6mMmvs{!3xU!SBU0C!!pBhfDIihlkaci2^#NRf_um0 zd|AY5(UQ&4%jbFlT_^xK*Pykd4WX?<+m41VECR}0K$#0Da{*;2)v^`+;>jauPoX`F z_A=TVXbWhvUj;a+K~v9Wxh2kAQWgiR5ue)S+tk1*!_5>Q*0t1hT-sa@Bf7oBV?@pLV34UX^d z1}m0FozcnG;se{?ziiwwu`2$VVt?41GlsJA8N(g*!LCeEZ7lM&_xqcMf@SvEnau7s zUwvz4{7?2pwW0pHxt)$#;T#^$5%K@bniRL!1id9$2K4KG&`1D77zAH%)4Iz7q4buU zvoDR4rlqfFT@nihr=!r*kpdOWo@9foaYF8g3`O9O#vaa0Iv;7*p!Z1CSl~(vPUwQ) z2o^Lno%2ikZp0;)_C4{$6Zbze_Eg4~mppU-6Hk2bgC7jNBP3R#`0ld!V>qP^Z6{`2 z6f=(2UP_?dBFf=0V|d!|uu43X$0~jF>Z6oJX(6z4EqD{tSBBhE6ddNeD;FnDl&4Ie zJj<^LdB8l`u1=)=5;TmABL4D0LV0pfo+#5213Ae`=}}XKhPa|qfmKNsLC_V8Fd}`3 zHsYrt^_@hMNjogiSjZ1d@;Q!^2hDHhadJ?8Q0p;ez8dW&Bf};F=m=-WDK0eO*otEt zjzJu|F$$R&1gA7(>e?16C$UA?qkb3bp2Na{oJ2bu{?;ck6@2o^AgT!twpXD(Y(?9W z##r$1r~R?6u9&~&@TY@AqoYH?*}cZ`y?ZyV99fc^nNeEb6KEO@SBi^%{p+4Ke_Pj? z7er?KFa41Q?ux*?=7)`bjAtX}T$h0e61uT_Bubj_s*QMM9wU@^RLmVXPyu*Q4H|S3 z@C3E--gX=hpwXdu4DA85N6?-^dlv0wv^UTe&@=~CHz60G+9Hh!FMxj>2bn#HMjQXr zXeZHLL3<6= zWO;lwKF^~aL_3Uj9PL50N70@}JBjuR+M8&akE)xn7gRv=&=Vf>*^>t_+aE!D3hh}m zmhN~1Z2?Vl!h9eS^*q><0-W$eJK7N1Dzxorn$zY3wfR78zPv4a!sl?7(@#wA1bMc_ zONq}0@KW3oG*2~#gGtR-)vV(zV3w>}znPZCJ8)+3jMo3KJfoqT8FQdnFkJXLr0B^C z&|P85hXo0 zXa3Kk(P;JXZ1$}2lQT~j8B^Ku&wOCe@ReM%8?13y%Q<1&kEzN zsz?j4skF9yaubqOKFCT|Tp?ZMgF1Bil{axB8Iw#8A?GnLyiu6tE~}X{9+$%A%}= zaBYXnXJp5xnj3d*%`92e*PfLkZDrWcoXRfA{NA@ko1YTb`bb{L6Ril9*CdHti_7yu z6;V$pH@2n14vqnd#P~2)Gp+!B+aG8NFe@Qc;dQzE@aRwINdJ$XKNj3m7N$iUneABr zB%gnSCqyqcidQE!=2yohgpNSHK4wY$W0eH{=_RC~4Syd#afLL!d>nyud-)iZKr;Y% zJC0tiqS!RYTt=e^$IuQbVqMnC%B1b$z_Y#U@o(bz_vLZ&lV7ll3++$-{`bEN9Sr?W zA5cpSPVi?1t1Ds0RaPj05IbS}@Cmpxmf9vIFCM{TvrgLJn`@?TPAZaVb@u*BK{JvV zlhSCQ$i-1r=*&gX&wzxvaHK_8Eso_<2VtO=+WNqUBZuGw)`n&&y#N8SUR%4!szpU1 zEM+>7N9t0HEkFh0b5``R*C(o28V@aB8e3Iyxg$Gsb?olV>$`goxH_8~Lz#K*qRODZ zz|-QX440M{_+4I~r?$M*Q{*`F+qvH1;oiARdM4Vs-VpH<#)%zs6PpYFwJFe4mydcE zh3>#WeQ9I0bFs52Z*gsvr@7iyQsb#P)!W_L-9sB1q%r%oz%i*eU~3UgC(tL;+S3d? zQ&{5Br;iQ)14=GobYw|Vwr5%?dCSX9V|)r}KV{Nyf{+uWognrE`Ts9yoL*~6gVfo` zWy;aA@R{oZZFH2rJvefOr0hZ&CV(UNC}@#1??9YZG$NY}L6Q}6LG#MnU0>`kmJMZx zjqa19qXXAoQnh{xsen(9E*<&Df&Ss;8$6fYFfS`fYWJ)Uuu9YFyPe2Uk`)0g=W$P&rIwfarU&3Pf zEYXPcCy6zu41A|iB?ru$)iuFZa-PqSOU}zTamXD*V z%Si(-nI?&(3`dIF6_Qw(bgDz3z#l<3ibtkztVWZrxZ7U5tb4=Gog2EB72EH-uX|#m z`$$!&&gH5LRgJD(d5ieOH~nn~FTC(zyZ^q{B}-c4x%CB}K-mptjh=!PBLgEV{~oSk znb|38T@~B@p5bVTNE$iZRs@$O2rB?r11yn112jt-5jj4^Izghq!F+eI= zl!EFZ0AV|!s3^5qYAhKbCpCdVX6gs@nXqCY3Syv91)!kFb&6sPg+&UDT)#LA*+a5; zl3P2D)W>;__-uAS#bqDE@d30)&~yhvN}93(g6bE!=t&4Y385z;`QI@9GVqto(j;It zz3KaO`AbV&RYn#p3P?K#cl5R4FT*(8aDw_=AL(C*fB!bQzHrm55VM;K*H1nqyt)0M zheUn+Umxli$ceu!c7^tbZk-wqWjADJ6gUg)q2Xm>O?~98g9mTwXh5Q?(p#sYw|ZgI z>@*N{U4;$Sy5!`f^vNZSM_Co53N{DCfKhL)0tTyq!75;|3L2J5=Rbhs^=QY?9zc5p z?J2Zp(OyP-18o6K>#gc0)cE*_x3m5g0C7-i(Y$Cdo+PY)^Eik4_dm1%@>#UH3Z=VR z{5b?3J&d**Z656)+F`WgXb+-2iuN?xNwinc-bB;7tGWq$l7qWDaCaVamF}7gJmdlo zxxhm%@Q@2UCFZ>}UX~E`Gzw@_ zmsUZKOU)BsNfi`x6LF5pK_b_4@)eu_u6VPhf)eqn(smh+X7z1kTY(g)j8gpLP;ov{ zbZo(q=6))pOyF1~OKzEI8(lI662T|Z3y_G`W+4%5tOYR<&K9|`AIpYt2hYB?=IwXfkd>R4A6y(;Rn}3|7o5*6 z$X<$#2?~O}Q*AC+!QG=H^Euh}^Ddg*T>0rc9)3*BY%EwBt!n8hIPu;*Ti!1`J5k5`fJaKP*oF)6w*QgJ()EP`_YC)cALBY;18 z3MWzv;mNaTFPq;ervYrG6afm+yl5e`VYJm~S>SO>ZKH9NLN>VC>+knSX_Z*LVF$R;>5p$F`yJm`Mk#Vv-FCOX>m4|jey3PY%>ik7b+PA z!Cb85SZ0C6JP3juA`0z=Qpy$x$l@Zy{j8Kh*Z>4dhpdIK%vPCzFuYa;Z81)f;oc5N z@U-|)3pCFV&n&MZ-KaZIRyXg?MOGE`EYmLujgLXCrm!B-f;tLyQ(2BkG+uu({`LO| z|E_483|I7dhqexUNi2?rqZf2VqAz`YZ)ESjz2UvTuOHpoGkjo8J048zMS#6h4DZ(& zEMnHMVz5YALS9evoF(fyQVaH?-ytbPgR16nB2BuMXbmO}ilAdinFgh3e-NEJjCLID zL9|EFAQ`jlWHxS+4f!e?glp0?C2$%LX<(-@o^(b%m_ZOqLhg1YEQPurH?rJ(F3S$4 zL}H-|uVV*+KR!1wI5_a!U3c9TJ`fgb_ifs=??Ctmqmgju5)7^Bf%8G~jS6^dY{;us zsdrokX!dU|mH_&~YZ zV5mizRPASjMMs~BHLJo04un^QC#DBN^s=#%S@;Xz?Ttozzx!i4*naHVD?My`&v&e! zOmVX96+h}}t!?W*^TJ@bK0F9Ah7-buI3n&k?zO#-7C)1gNhdP;vI65I*8?3(WZ8B! zJzKFvn&K@$+E&8JyF9$z2G)gg2rBUWVwkp)H?rnFBlr)YcuDcqdSh!*8hbH@th=Hj z7j`2x_qdMRP_j9kyh|!#uhD0t0)1`(YhlcGkw9^Dz$1v<`~!FsOh=?7`)Ue4CfxX0 z0=_l{{+9%NO$z+21bj9H{*45DIsxB}T(3M0ci1ns{n~nN()-LLuE%>CrxMriv;D=o z-uPnT{wsJr#yGMNL0!x|(Bvrdf6I|@Judk%t0WF2_*ETc{ko)`Nv(44ngaCr%Fr-L z653ddNa&GbJnGzQ-aOz3(GH_6!aS$trAtn(LL61p8Vgu(%VNHrN9)PhEh-ZWZ#fp; zay7LM;Di9I_?Zxf%duAT1Fb38z*=^RY)wgDTu5kQpdT@8%fRv;#oTAy+JFq`%rxzP z-2vDEtxQhTV~JuLR5O#4C0S5FP{q$;(jeO#Uoz6uShT3wTi@B;De^qEna=6{nYkSY z4&4+PbJe*vOl+DTuW2gJDJv?f%#QyFReIf5qW=Fw1+Ln9|4^+_?U_{M^qLdYUS>ceIqXwwd&2FH4N;`eZt=e<}grkPJ7zn}AOx z!|ew&++lx5f^JBECVe;9N#B|ZExJnj-6e_NalY(K-0#v9_<0HVWhrp$`Bx_3e7-qf z8tj}e$csC-pa0Gj_@2b`uhsA_+~-v}pX5qmU;UqIeoMIQzk;t#fpdN< z_?i?r-&etBQ{bH63O=2H@8&M18jhX`q@!Y7&wiho#PxVjIlp-yhkal2^>Tiz`>){j znBQFs5$ylP+~!`}cevj6+4|*VXiLoPPR{Mlq`4g1W-9~B@!(m-uSTo`XHS6uuS^E2z+1!KFJ8E zGgZi$>4Cu4f*aBuPA6WjbOpfFD(9Qb1eQ}SGcgmPonzJ_ff=(mD(gpDru>K~P1-JA zlzAFT7rBdzDi(P-VI!R(k;~2HcOE>rQ*JJQ*52`f^17z}$3LH!#+GPYRO9BK5 z(6wrliB>LIgP^x=Nvc7daewm@SQ)xysh7uXVYS?ZvD{$WNJ?@CaB@9AsO2V%<%S`ciM^ zzI%i_Ogoe;DXuqgWxcTEQUj>bCU^0!Hax=TLKxw~%JHGDny$t&ai|nI2=)Cf?oS*D zHka)l4-9ws92uF_9V3m?o6fuV(2}9(XerjJrA@tol8g+8FEm&mn(nTSA91$~{!Yl{ zhYQhsFuQTIt)enC+L-y5J(aOBS8cG&-CFBfH8C*@cXxAhb+E70A1rkTQM`T0)}G4p z#?eqEIGh||$#Gx9h(n^f0tdjk)An~IoUS~;|2+wAkmD-&8p5IAb%Miv7nBmQVV}D; zTW3n8e{CYLq|Ovprbn6~e~fG4;Iv`5=n~|yoXSQBVg$=Z1R)^ELb#7WHhc>0Sv0Cr za5=$tBVe>u^?bPeD~pXN5Dfiil@RbFUH4sXaI6fy!nt(L9V!}$AX0NWI$4S^_u)LF zq|g)SZLVv}smiHwmwGA#HH&xbz0}AX43{?tcg;-B>}qSN3iaE&jZ9xneUp1}OxY|)RICamQw?IYO+CwK0MQROGPZ*e_M>V zaMIj!80*_{G)_QO7cJGb%a|8)QJ{JKuex-_CzDrz`|gX4jKtoXv}hz4{)yIAt6I<8 zd)5*`ype|AFn)uH-N!m{xrxYb3Z9J#ywws3wKhv@3J?%T0$5VD5?yHvu9Y=1;P-Y( zzc-_i1>D>o{E!=1BMt(&E=YNho1oBaZ=MN2RqbVZA0ogwQ?Mf1lv1O=1uZ+YmYI|n z2#QrHEy&`5dD5P`_*NU@Lz{eqA&;+jrh^+*)?n-U@UoJOqN(;x?zL6J-i;--uK7#r zdRyG?=Dxb68}Zlp{;+q=uFFP7_m0)okIwh?ZX2x&292S8ts^!4?qE?~dGYv}&hoY; zP5n(v+REo=ROyc^ta6NT2b^Sg81u60V)X~)aSMYjCO%dK7tZjro z^FEZV7?lIv2WqnFsp$9}i`VH{XiYlJ4NaBo?LB-4@!Z2`RLMB;RKB4DN0mX!Sj#?1 zKb1H#O9jCmjBuOBL?$u%aU__8X#R-4mo_QVb%2*v;?oFjA(IlNtftm7vkbmou_9RH zvK+~xU9q(S(ytnAwddEmvEf5aP2X&!V|7oBT%rO8TbfJ!uAtuy>8>&x(p|5);(e#C zp<%EpBQv{heAm#>&hdI{S=w9G<|%G$Y%bLjp7<|ob+bl@+LIcgDQiTHTq!N75xp`o zqNEIy!XPc-dQBg4#4EukUQhbn)rsG|C4MEPC3U}dCc$5q(vpH-OStAYq!16{1}umMjc&YQHU<^FDZnMwJZz>HvwS2Be{bI$1KfLFuDf-;OMg426tFu5H3d$ zLI(@-Xtmhd>SO>_@Tv_ORAH(X50|zKHg>HlZ7SQdEV8!O%kc`Yid7GmHud=H`=7RIW)9uyiSe|c0@q|pU`MiCgw@oLJ)iVQ3j?lJ0Q#X{QhJ8`^JG_8d-LA!KWaaa3E;v%3vy z5i;o${sYjeiC^uxvhZu6lf8{Nx5f-xT++=f?(=Wt-$L2~?W-41sUH3KCEh-V^rzu# zHT+e;i%AC>z9t0@y3z1i4gUk~f5yN%qv6vEZkt)yZTmapMc|1Z)GYhOSK$Eq=BJM& zz0Zukz6tOjay^mv$+GWL*DtVmfrzW)kd4`Rq(h}b@Fya>TIV8hNRELp8;bJ4CU z_m8J{S&~+o{G6NtDzDH%W>WK~&CpOi5IE?=Bt+ua(gs&`R$ugtE_Y4SEix>`hTRY_4%3GBQa z!ixGTcb-eD>h<;gJ|orEyYM{lQ%T)h<7Z0aa~2gJ?gD-k{vGJaZ-FtYSfgLfNupSeBIMv8hBPb}s-LCFCtgr*SN z)`c?2k(A5fR|5QisFZI43J07)6+1auQ+H#~vbIIxI+oM|SaH01n62n*x_r;xJ-S%$ z9?iUoV*h{4#A5cDhbO-C(u7#F=8M4eAJ={E`|S=Ie2COntD!@XAPd%wOJz{cD%~#(gjYHhvoi!WYVH|CWFQAGVJst{($O zmCps9RxZ35zZpYRhM%1VywG+bLvRthm&6G)S}f_?3ZSHm5w9axK8B_-7c285<9Fa0 z!Z8<(CJ1s{dWV?qO0Rc;Tn;0oSSLJ-7;c3BIU~PVR^y$VjP2aHszXb42{eTtN)mj9gTfG_<0()UZxTtWi@< z)d5aP*+9XQb%0Y6n+k5)Yvp~Ugr?xhI>0INW@uNuzoi4P+A!_4gj1F)me@c0eUfGU zQ&QGf*C)&Rr;LTf{a0C%B<@cgKs{IK0Crp5!i(_J1i-UvA>2Jc2eTRX&*CN(AV3~1 z-7;;`>l7kL2~@l}D)pdoRRWSZCG?C?9BGsrcay=@>q;WI`a1ej%x@Qi?WiKjzQlD6 z8fHeauoA9W_h2#P!+@h(dMX3<(g4z{SQ67!H|DOuc5aakeIwq9GI_eOckp}5HrG`W03&f40|G0N#(Gvym0I5-;!DI zSOvyG!J7UnC0H{g5E3(NLM_-DRK@nS>CL}rm`JUVA%0Wh@dbj!5Y%ey$37b=2{^ZZ zdijP8%h8Pe+ufD5`&SYrrZ$t6wg1eMK+dM^@{iejiIs@Lik441W##PsSak^3R^)SB zZ~7k8T%nwyt~Y%T3jQq(&$2Ja!m6>ZwIW6P$y(8=#GJFVBFrTDJ8DIzq*kPUm#h_? zl3J01Cu>Egq*kQhN-MIHN7yd{hocR7%=S+VX))_aGm6@dG2+p(A!`eCC;5Wz9*6@OWwW zJpm`CL+*dZT&iCA13EpVt6zH=lZ?TTjncNt8kGA^N=4eVNQ#oFxw)z3b92U?(#BYA z0}Q|qE#Fz1;o3bOSws3-6PeiQ&M4inv||-D>A9J%m8H$4y93)d4R78i3F^<5N*w>e z#NfbElki4|2gj8vJ-Z7UTy)Z0mDWi<&9xXa@-a2WV_1R?Nq(&0YczZmtJ^wMvuTiP zcb5IYJJt0~pox#k`;@Eud?g7k@1x*r2nX+Kf>roq82d5qfmx`>-CW(8Ijl$qYL9}W>vwgDl!%E>=mB zb6DADypFp8U@1J8Svc8BtV#pWUa2h0R;74tSQcAj2}5yJ1q{V~>%Gn16_LK-j-Fg-P1DQgR!9qT!}Y0_-=$mlFHo^y@f}m=5hYGYtyx!FIjPcoU}A57Lk^AZL*Id_B{x z=hu@Y>XS%9D>@d9q)mNGil!37ac2O5GOHHis1iTP3KaLR#~Hc*3Hny0?tuB6Mgtsl zY!V;~MpM;h?h$s~Vb+$1(FK|ajBQ(H1~+;etH;}W`T}ir)!{nV-gPThtXLkKo2!bh z40lXNtNasdTU-63OKzK7-O}o7s13I_26M9=CEj-5IE44PO`GP<{1!M{)VaRj=j&hJ z8E)xlZph7c4k`K2D8Q(BZSQBUi;Dd*@uzhR3x9e*Ep%CGb?O|GiXtukia&BdYa~)~ z;CtW(Icg%I#w!rz!FRL(Qcn}YeUmr4~b1* zzxdLdu84GQ-M)Qq$&>M)8^;^k!k=wluw~BAU$i$oK2yAWEb^x~qffn{(+1-hB;sS# ziW3rXp{WhOT?~`eYfFVN`8f+tDj^G1$TOkuIKWFB(kx@uGFIe62I7*0L_LEUkBmlR zon9X_3Yj>~Too<>vKTYdR9q2Jy{E>zWXZ-}S3&1xmu=s^zt;1O)(?f9rRN_Df2j5Q zn6I3FGsf$fmlc?g(5C(iwjaX5YyHMC4d;BNnk4{_h(6$<)piKuU1|1S3y@Z4bx@F8 z3>zuvpg9;O`v^Mt6xy?BbdlL4f2>1i3eYL0j;n4`V)X#RsCFi8@(Lde0PeHAWm2C? z?m09Ezr*yc{n+%Ckp&iJ)Oa;2_V+JV&b03&`N&qI<6c$lF_jpqdM4d#sr?PdOM`+>CGh-~(P$h5&qk&*1EA>vGV!eKgchE4`j7=k)}bVx zlXD7~LB^C~DpN?d#M+RJqf@R8{H~gr&eW-}V4edp(TLIL^3=qLi?GH8EDakXMowvE zJa7&K9Ml*v8cCUA3n(dhrtD@zgDQ{O1PBl6nr)X z&Ka%X)5-9p`^;!~DQrvsN5ZMj;QrrG_m^;ae+8dSf%Ee3it{+4rAG zxgL1d*Y9Iq67Z>>ONv$ZpVHUkllVLNo?i8ypI7fG;Zmzm@HHuLdUOb2T^ zqG;tGs~g=p$$o{fC|nW$@rr&?opF=M zSoIJfjbTu+8-S)gt%+E)XQ^fS3afGkjhd(pAQug}wH(NzP$sI;FUp0O7`a30IaVrf3+4-Eh<%)>kiO_SFn!K2xGUdv z$Bk<*^>(S~t0I2z@q-BAl z(g0A|li7d*m<6?Q=aUD~B(=S%pmuOI`86}Sgl!(}Aev5Q#5c4?+0-TEfC3y4caW}s z8YZYqGQWt)gw!`GB*`*;Mi)P75n8`MdKYvn=bo9FmyX_b*U^_=-h0U%E zKbivPh$;BZDR8cX3Vve>oFl2=H<)l(xc?&8N5aXa+LGRf>!Z5 zT8E1scZ+eIYjyPP6!*qI#-c`IOHET57srZ4+UXt<@ed^USt3aV>!QEjKkPNE(dP=M zuy>t>ZKFZ1R0`YgH?hr8vzw!j6$aPqv6G|M#ZgN7-96^-Fh3n~g;V#t*Mj4Et}qJz zLBcVg$wRLOuVhFiqNa*$W}R$WXk~s|vI1{$&17paXH%ve0$Ojd+Yp8g$q&+phV_Rt z`v6G=QBz38w6P=tB_oO=#lGZNZ5<^X6)IYa(b_}e^r79$FRloCZkfO3y?5Vo%ln0= zW46y1U*hYV6&F_AaOE~uR7M6IWp#Re*de6^tRvuIx4^M( z^1>Yww+eo90$yh9FetSUPOtLDr0Y4e)b%&;df<*y&L=_n#RxV-?P9ZzkBTmmF(l{4 zU0MQh+H#$oS;j2PESlek5SFkCjTxT@(4_hODV#{o=j93J0=MIamyHp@FXO%5K*L;N z0=OH&`feEC+~k?e4|C&|_?CS0O`OQvtmX-BgGx-ejhP9`^^bmj@=zyiaYAMq%8F1v z77uWM|{01=@f81Eyb{+n;ojE10z4J~z7mQ63|Bb+7mi=bB z(l}nAk)@JzGMWeL{5=Yeuf-ZUjtYK@hBpJgMXpf_el!Knxuf7WYq+{UIj4f(m;&ec zD)+?SIP~yBPwP~J{&&tDJykPam4|@aNv{>KA--SIF({FAd=dw)^b2LiN=Y{@ z0FaL?mGDjI?GGe<2POPn3+vVOgv*E?!eN{KdE!2RQ?pgq9}t)?yuZBOg0HkKkADjE zBmjR7Sj%TLOP(z?A}XzRnq{VEc>ypFsS>d86nBd;Kr}q36Q2aM2#rCL^gl2+oI*bp zg-}P6u)=&?K5X@MeE07DuYOf@ib`*&R$LVS#9(CLPx`$v?wbcdZj$p@ z!H=47;Bk}eKjFmVHnab@o@=SP{yJWd-tUC{8rNeHyN3NQmf@SGB%-@(k%f*awE&im zq?M*L_l@>_NLhvTDd;_CLM;)YgiA6<$dk}}RFoU*bPK<8=W=nt-WzBhTJMbCX@~ZF zuW|nA=Nd!NyNvVCFfLTB!xA^`=nHU@Sd*|mOWvp8$!plv5@!m2H065oJ_WbdHr|K4 zPr+|YT)&&^zlKx7*eY=><$|R5Ig+@Z_aTqteH`}NQ?CC^;{G?KTuHRd!1r8OG*QmgfQt-;ShWhS{^)6vM!CZ1sRK zybEicJj+YKYV|LuVdv>UgR;b#U;l#P`Aqy!8I+9Xp`d~kC$~^xT)xf#DkjtXEVf=W zG1xpldGE~^Ov+>YL!~8Ryk}z=kt^cK=J=D{o5M4`-n_w0mFI2!^k=^`u#v~d9v1_h z>%8LoU3H`94W2&|Z|m}poi`}y0(jy$2r`$m>4I}W;q^L+S0&F~XKw++3^JN0H<7JG z&S}XcX%z!z4zW+-dg(J{~|)Xy)$9r2+=+EmOZ7Fq5CgKJ&E68PDzm>OqNiJVErfT+F3i}+EqJNwpa=dQg~t&_$Q z$-}rnVD4n$kbqO;R`8=H9P7;zx!w@Y^=3d`-wbI)$ooJN<$Zpw?xWzmkAfd19P3as zG-84A8KVz<@T|BSoug|FL<8-_2ySK|4l{6-7}tO^R&r-7@}S=1paEE9lr4`)`D0!t zs{6naRfjoQtt?U1utZhE5>+iNQ7HL|J3v>oHGuO;n^al=%{enO6CpThg`h>n@(t4? zgXgckWS5GfIV=2T#lD8t`mN(L8;a{tM{d_9Z{M2m^nQPnMd~}cR*rkyyd~NBCEJ#@jklHcM10My95sb)u51e1Hz!7+jM&z2jv7`0DGj`3 zTn}lW+IEm4L28Cc+CE@u(;?a8NrGm00#&^-o&HsfD!gigr!ss?bC&XxS3v?uiD*$9 zK`)RB z4W)%tDmP1sXy|I3$i$MT%@ePAqG}O=^r+t}(+ky2o1WXsAZ$j3=TlSE;XA4jEOsdw z=zsWOmVkYjTVsl?ZGVnN|IGM*a2cCXCncIr+#MFj-{3$vW@Vk6m4tJAt8m07+n8m5 z!|t@n#Di9wt+|z4A6kZ?Vxk~AgY}hFWQNLCrwK^ayX7cS$t}bbE5qw&TDk#uqUmNV z67?V}@3DT^L8A(KGL~qAeiB8zH=Z9{4zF#phs%xoIK9Ydm{#LiS0M zTa)=T?U^VC#Z-J%gAJof&0ZClm6|=PvT4m8Ctwy7txzG91*2far}BS*CoI@RBR>Z< zUs5xvp2i$0(V`?6g&vqjtpKB){m*TlpWpo4bNxd@{ZKsK&q`Mh{BX3hty9{Zsl^RR zz8RGx`YtaH7*PeMB~-z0Nr97}D)`Y9IA^|s-<$%cOsn8GCg8h`AxSZWa|K?R^jzv6 z>iQ#z>-nCPY1Q?&3%GC-?@5_fU4K)`^^|E9{4n95$gQAAJQw)CpKM#zt2Ak|oov26 zsZwa#irmC2k_(D>3Gk|dB25?!Wb;WDlqFM@q%!XXC-BPuSl@RTZ8h3F+Cj9#Xf$+s z@#AtCUQtCur&A{+%CL#Wh9kM8r;^>e0}$d6k?s6IQS(G3GTu_upHtE7>kdh)C(DG? zb#`M{+sNpao-!!MGrjA&D=WL#^@>O0+aj}LvGSH^Wr+^2sH+>SHS%khUc6F#B!N}( zYlXdAQ?N>jR>7}JVD&BIc6&3rvYuE?jlt6>vSxf5R;?s^g+S#yTMR}(A6g0Kcrrd< z%#r~V=FW{40yacko~nx2*3tAXAB4ObKeF=TrM0S)ChgoVWz#={HXt)3dbyjuflNtw zDs3iTpG=##OW9OipG=##OW9Q2|9YCz64?E*#-hXifn-{iGAps`u)p_h;O|R;yA$`h zOT){cyls(lx?A4oUAEiha}}I(RKagffpd;3_>C!W&P4@3qTz+$itY0LG29<`drS38 z!FhiLzuAPt6N^fX8cuwA<@>1X`CN7Vjpp@d-Tz1moIHTnQ?IQ_x(}&R-RC-eJ*?

n2c*ogjQvsjR4YR=G}y;gsCn9&X6?eN%qTQnsy(B*{B&b$ zYFL&Knb7=;>0g}rnJCP7R(u4G<1_D!8aJHzJn|TT_*2i=S4N|!KWATg`g44~6VX23!t-S| zqWy2Qyh^WR_4UK++E;h1UDB4j0RzW+Et_N=fKF!iF2Zu6>dLcXqqP%oS~6c+u)CJ3 zEhK2Nl>nv5ayn%}!%(&LSh160BntvC_^(;pR7(Np=vD!);+WfoEX9HqR))1B=ZSfx z?nowVNJ9eqAtsPxYzkL<9hDiCTUP~_MLnMAvf!%o%Ip;lgS!TL;~%YT@mDV{FVC;8 zYpJXXHmUffYh^u5p(Re(Qti0cK8 z>F+7iQ8?V*fI4JIcD|UFGgUqUx*`fIGHVcdO-_HYj_hXwgek%)<#Ow;-AxnWDwn^l z+}9th@N`Xg^iG%9WA7Or8X6y*94~8(dIAGYuC-HRM%G2wmbEQuu8%cUWM*VmwoimY zt9yMxfBbjGHfZ|}gjPofqgIIf4Eb2&G zqqGucp-XdLYhoo=J*EE&`)*RHlvD`qLsoi5*?;U-giTgl{+Ax?YAdhF-rShcdTIAp znDA0m9{=6ZqoONrdj{jGa`9e+i&^e4pARk52ipsnmCU-L|2BNQXIYDr zH9#V>eKmDM>%-0uiSghd=7)n9p*TV4u#7HG8W^!5Sya;$h~!R%3)4C8DZ9u&Un8 zBFqkkFjFO_Rgd$t-kh1;Y8z`EyrhkGeZr;B82TiB?%BI`*Lg$zk={L7xu%B3 z@#*TT`$s0nmaK7ajdZOVjgI7IEQ*Detc(PF-o~&$uRap$2o|)4nw!wC$ifbBAMz3c zwsowHttvH`{nEr_jbid-r1erwMp@Ne_JdQ6IqIBrG-qSHV3t{Iml*{$G9NXo06Wv_ zU1^6z)Zrf$b6>A+EYEQ^^aqz#lo^J%%RkjW(@~mR(dvJ!?_Z7AT7OeN*cB>WToVd- z@`@Z)#aa1|D;B@2xFFCu+FIV(Ki=|FJL*^4YA}~x!x&ZC4zP|@s=TL%#2TZtxs*0_ zl1oo0!G&uN3S4VX2raoxG@%mD?2%HBNH1ZsYka82tMbibD*YUDsfmNq^q6`AmMsvw|MVW|-iMRARc9BIdR zOJh_GPiUoBrA{)DL)3^gn?_Y^dOM*E3R>U^D-u47lb885Lco}Q3S*RJtmd?|Bl<)M zBCcX9?tQH8{Yafzz33dvP7wD4<_(nkqy^4;#xeQlP|u08f-1&aJn}ZXrib^5+!oZ32=~hI6c|bF45UKUIGV zbEdb1J6hj!3?q|`uKpCgTueQRn_O{c0&`^kXZ*=zM`?}fKCzb+eJ9-60w?o4A>%l1o=6Beh*^nIR%$tnuXzdCiJ~gz+*G`90xTv+)<~89 z75c3bOCUEbXI^H~deR-0m3)xIh)wevN0%+>Tf1iMDraD@ZK}d;$GWm&a80Dt6^PYe z9_e_uxX}3QivGpDBjJ^U1=Vd0Re8l&Tk`X{v=lakmjr#iZPl3>)mL>)EzZLBA+SIw z=_k*~!n7hLA)&x3N$G71={8k*(;|<>d(s#}^3>vpT0&8tL^6XzIJN2}@kMFwSK6}p z@z|iZ=2x-pM*aDqu}X1gJd8mG*D`D)SOtHDRnQ431Nvx@jv8g!D??07dQ0Ps$*Y~5 z_?DNNd<1otL9K8%LZYURTKQ*aNGO;5zgTHo+T(@yOe7RipQtuQs=8Nq?!D;XuGp$# zq!Z4}uAZ8jDQoIPvBym#{&(MS(|cNd@!!9(V{UkE$Ii{iDJ2ubouJh(qc=6S<&3UR zE8$qkyI?cg164IEv3wnFZa4K z=_D-yH5tdWoUj6qieVz2>B_^J%Dh1{s!e3qcpniTeeyDy8XHDhES||6QSw5kZGvu^ zBAJG5J~K_8P8(<~yn!h(B9D*kmQ``3*iLT!5qjgJ{czZf8oZB!xv;O!^ zvDof+bVs=LXI|adc|+&l>}#q#f4XIU{+CA%U2*fV+rIq9wmkzoRy&&~Ix!Bp;1>Ui zad0!IvJlgR<`#1t((2Nr5rdW%Ed5gt3;?Q-0^{G@dW%ceeWbk12dUfyNDX03JMQnP z$ zNHi&W>M<^b4bEjdN{y0W-O+Cq~?f|gKc&esaOr>e;0!;zJipjfnzEV5mQW#gS_ z??Jl{jXP=Pp_+Ibc(1B6(1v}D>ZO}Y){0>`gUR(1$}0j`CEZHNKO&BBJKE0fXt#HC zV*QH#@!X8uvHlh7CEa#+mIXWou@^cgnw_h6L}o6kX!722>CIOx9UNSG#m(<{H^_J2 z+K3|98?Xk;{5N-G3! zk3B8}-%?|}geMwhuVwtU>@}i=H5M3Wn+1ypDUw)U4jcR;+a#y7@;#XIOD|uR4?uHC zIX9#%VFgVvP>B(5SkQ{O8$A9m78_3uUb!Q4#})m*VfLdqPt?R;i9dkH={NbBe3RD1 zo2l_(gsPp^ckyRjYKP>QYuig55HX{2d>nWZ4gEY?l8+t|>uoJThr_8tyj60H+Va&>V>h5{R!D*@Ho0KP{e# zUvnn@XK{PCXco2I(fEt;A4GxY5{xioL9&q4$2XN;KKVH&@!YrYb9;gDBUt`gY%!6? zO*49JeG<nQ;iK7LWt8!shN<*t+cV&A~5ZnvH;2Z@llgi6?JTwAu6ziI3-K;5G@d5SW1R# z1=m29X;!<$fg! zvEWK;RQG=sS4-WI_m?uTg1^SQFcTpG$Nd}Z)4xuG%j>tw_cX7U(t_+4c?kLyQTLH6 zCE;8t;dj4n!6V=Qm%w8&?_;Y6f&h;{n}A;;;f-g*#dQ+ign)v?^>WRalyUDW!{o+$ z<#QK)3H)CpuWyHBnYf;Hjg1>5JT{vISDZ%PhnyzMe&g@-eK_8-Ul?!eK5DNt)B!`9 zmc$pv8}+gInPvsU)`QP~G+xK#ZL#fR8A`K~WjzQJlN*1==g6R7>Bvp8bXeR}O?jn+ zb493@Qfn!$u9gh16!cy%VMRC+W{T|eG1B7gFzf3qHUcR%^+Z|MnpkyZ|IW#KKGZed z?8>y4t?A!&c}?G%(A4^kE2d{R=G85`VAG+G?TuvR#qu+KBYTz&bl&#P{`s-`$c{BV zo40RRKDBu}S3c6`zXQWgtj_hc_EjY$6Pi!B6!2J{=&|mgB{^zERH{+bGKnlujqGDR z`lxCNIRI6nA90}yOu}JCb%Y_8(v9Y`@_Vhbt$*kn4YkI4ifY5&_?fp_T0?DL_)Jq% zW5Z*HZLGheCp#l+C{oiEaEIFKE9%<6_GU|Mxv%L_bW8T_9`r5K2AdpHXdHMcbL7(c zksRZoR&Yw-GV9d8726~)pyqZMMqR_^Sh!4T=!-E6chEXwWu;Z$ZC z#!=8_i%O+f7#Pk#V~7)21byLVE^xqZPytHQvz!!`V1K>SPnm6See(dwCHy!}9z^5L zlT>DD4lC5Vl@m*9n=XcamH<;U!7AFSNnFLKY|g32Z6RD0l-TPY6!e3^r4QRO^hB<% z-PnW(mhi=bvNY*;iNTYolNuymOYaMGW z*_JoSMqcoWSG*9s#5M-A2h1AyAmC6iBs9M`foA)FkcN~78WNI_w53TP2?-SVnzm`1 zJ#0-ViO1jnf9`$rW|M7_^y`Ot($gL3y>sq8_ndS8=iGBEi;Dw!lcVA8!1`U0UF!qg z;nB&wKyh(p-t_2*zrNr+w4PT`?;n}{$%-=DljSeJYj*D=ufF<7@9ewE{aGHntm2ci z%W9r}oJM`6{bZ0v`KV3+tniq&E$I5t1V?QW|fMN@yJ*eyKlkg zMd;JS!elD=46dDeoZb-2ZMt(V`#+YQSJPQOHMC&q*zn$6{WunNWKnsjV&k$+n+C%} zqkHoQ!;Nj710BU(z2R`Eu(hYNwZ3~~>3IK~S)QD+!KLTwS@v^CRgrXsd_iQpZ z;JnP`@7Wall(xJ-XQeRfJ&?O>d+po!rs;hH^wt_5B4$%3ojN1EX{b?J*%&wVCauiN zF(|?lxWv!gjvFTLs^gQ1EUJ`sVtzbg3$aeLvC0~Gg^cQOO}db7i5xiMt1jyu-BDOt z?5ix^F&ZhYD>`xj3hLF*R~64bUS16GE3N*-?2@W)yg{C^fdc1G8BkPfI7TXy+2J{j zMVBLODlt5wj)|iT#+v{c6m3{dM|# zsZj;m_5Xuj^%_%Dqd5YfIh3Y#qDzM!@uV;V#+d*usE|Dp=qdBMNbGevgM!D?^8Knd zA#IKFt4R+|Jj2gknRUd6pB8%Na#@^Ak6zik+AonFH(tmEKO%%Mg1+GMkDH%-oT()= z$#}o2o@L;D^?08R|82y27>G+FR&8^Q@6jn4Gsb0_DGv_&7a<6vaciyBj=M_HLhlk) zMq^f%P;{@64f;SyQC(UBTv<%4yv!AKlh`K4J=<|dkg-W}#W^X}ovH)HR3nz0ROkIM zB{VF?n!9Dx9T$uaQU;YS$2K_bBC%@+_7%;>UAB% zC3Ym~hduqn!~NqUBjyyojN^yVy7n7yx#h<8I@64ua_(a%e?8D-W;YFN+`M%2M)@+( z6nm#>0HeXt{vN6*GWSmAI6Nl$@p;rgD6=oviX)9_CLv86T-=l(3cH3;Ev}>U;5f+^ z=YFJqVu6T7;XARN5wkF|dJr=h0WJ7Mq+keN9tQ<}=13J_7!xDfX|a?vgx&>e`PVr* zP+368%|QnQcpx-j%9(KZEl8W9H$GUhcEwIqR!7A-(AQ-7myWI67@0RdZ)GHMvbKGC z^`{?<4)u2a4T7xVzhV{6UfV=yD!JFvNPIWy$FV|8o`nu{{xxoiyo7%zA8@)3-RF#- zoexTisH1aIY<${8`9lYk``HPHa$pfGbOzR;4ufm`@Z`-xqC2{Jgk^8BRSz1uUk5vD z;gjp^K2~sbz4A2Sa=mic3A~xcjj2ERhT4@?#z8jFz<>jT%Sr<2kg9Y@CM^K4QQL3a zh07;yf9B7p*R7j=;oCzaBSZHC~Fp;g05x-XH3-vhxDHB@2gFt$xj{&RxB;ySt`;j1_Xn z`YPMHcdkY(TUFlpCTQk)_xI8qsQor9%E|?#)30ga9BoluYo>eNwN-l7qQPA(W8x@Z z1gUWZS&u>ZPvGJTO~tCnD}7d#hspJrL>fX^OF+T~* zc2Ayr?j)}K8?S_8{^~`S-*(+~xAkw?Ja6+B`9i)!7_i46Pi3(0S8_3=P)MddvV5Y$ zxAw4HHuaQo*-Xr3rGZsTQXWvLfcknYuGHc)3*QwqN$i%(EH1`}QTSW}|D)JCsh5i_ zgHA?eC^F=$U$kW1^baC;PhoL5WUje~(|`7%)@(a##k{5G#*BaXaBbPi=ffR!9li8i z1Yr}jl?a<9v8Vx{t<+=NICC65#+g$P=TwiM>#rp0F@;|cY*uQDks=SCr)IwgEOJ9E z9lEkcJ<)@W`rHOU%R9|`VxN3@)3$A!Vm~zl(Xp}U%LqvPeE<39?;l(e3C~#^dybUv z1iA638RipyX4LGoc#BJKI^N)vb$w)!AlQ*D3`uw;`=mP8#CUngFnfzE6Z; z5)qfYM8u^YPNdA|aonuHl?id7&~t^VrWg?y#(NdQcNT~XcH=t9eDp6$^=ekoK8?5_ zP|xL;O~j=$dR<|SuQqU5w67vqc+=6*qc;@>EBd0B1!{dYh1W$Rf%>Am(0W%GzwE;4(1EJo3?hlP90&VN#(q{?j#20VmOM-L*1$6> z1iO7Of{w<}$cx+Qu3{we!kLqbyii^g^1@GD8F7B_s2D+iC}*J60__ zx4`46XrI&CvuvxlDMAIEw>v|F})E5i$#;*FRYg2EKRj#1M( zW0I!Z(kOm6D9kv7mIpKHn7D(&h&~V*=P0J4FrteR?JKAFZHUD!xp3q%mr;6G_^9OgK=H9XQ>@g3Vys@Q{UjGnc;Io_r4b`AYy5?aCRYu+tS# z5$F+ouHJFFxbv{j4Lqxgk4P9{;Q9LYMf;b9+LrBK)D~^SZJW9^+XvT-qCn7^!4C|q z#m(s2f%dsw%lC}U-MhT&yz{!2@0~lg2X`MZUuudhYa45uiZnGvrf{AsIJdA1K^_pkOj;U>!dBdel4=-8u|`yNUw$EG2vl z5>$!zmog`$0Ba*2UhZfqtu@`(X(-Zda8srfYM_pZ0cv0)U4cM>$_Y^=pP6oggcgPB zI6wi9XAGLJX4S6kzxLATw$1zIMx$nZUg43ezxK7A8!OEVPA=9n(Ox9|lmRJ7)213J zaOq6r#04syt+pB|Ajl%u&L|v1{C~2}=`eJfqUF?H`vIACsB)Kz6`Xm@^a|D`F7qI| zSOLmPD2P<$Uma3{sA6uaY z2|hO5AYH*Y(Z~M&SAPF*;$i=j*+>uj-%x)f_M;>h+ZGS|P0*aph(}Sg}?N%Pd)a9FFZE%{-Ya?zF)qO$5yR<0{}U&=1<;*;O<~cV=1vTl!$m@jED)u z%9=Mq1qVdT8^K(O3}!aItIcgLbFlFYAqQ2uzeKrY$%6I2PM&X#?k^}Su{UklzR4_! zK6Z0qL)jej@gHzn4}PbzJS)rEwRX#Hc*TRU-`A9x7n-#b{b5yurym&9Z{-YELJ4uiDq@_CCl-r^I299)#x-m%Epz ziC8=2?^p7!0EnWWwwT!ik~VSsQwY-F%r3m>6Tol^ z==G%5KTU(L^{@(Zqp}f+xuNibq;cS#G^5`^1Kc4NSh7$c3n2&oP=uR$`N4@*(qD_) zet_P?P|8ebECZ-q7Th;IZH~rb4?i4vz3X3Rf36Fatz9feD#(NR~$ZC$n>1ap~Lz<0~V~S@HNM$5yXfFc3XnGCz9pRHU~PIOyy* zYTH+ByMFry$b;mF$mudznLwTr&jkLt$fas`&lYA*3FS<+ul%4wlMIcuWU( zG`iL?Zg06dMpe#0KBd-}Dx1Lv-<_w7VZ#firh=s7`OEsG$y%mupjsYxF(XynEPL-I zm%MlBMr^lPFtB>nYp<@HAa_Z2lpt*0)LESnzg-!8;LHJmU;XOU&&!@w%gmy)OUekEqv z_+(=ftvmCJl$Gz6{t5J8Q+gfpYb4voJ?IhhG%fE2TXMfWZ@LPpH^U2-F!g32I(K|{ zaami%c~hG(vSp%J?%f4B#I{P}MxsBu4iHWV)j1nR`mB6u{XzpHJchN0A z9pX6=>&GW+(4({fGUIi=%=FvnQKwxjhRUN^mDq0KBs=~4%M;q=)N^Z(jLb@{A+})l zWMlT!8xOFSNpcpkUVOV6B5gCxY3Bwr+!$`f3{Z0;8*?MunH$)Q3q9&&kEwVCx5Q*? z$e+^A+AlB>xWnN&@OR_V&L~EPR2=j93YBw{nv1hykV!YGnK%-IMi0XhvmERf=&`9C zAdZgIBjxr$S}3E1`lBTw-Ok;j^MnBeMCvI2wGmW35JV3IogTmrJ)KH|hz@MEf-e}1 zb2cxt&Wl>P5aneZ2|Nih&XcM5okw0WC@9%;GiD|YLOpDW40d%Nl>DSEh5o{l@}h0b zPbxgr-8IORm8-6j9Hl@pu(6V=PccX7$}1&HiL3unM?z{+BqS|vn=pQ5K!h?g~lnBYVN7)ku{H>#MZB*RG9H-l(sHHrA| z3~5>j1!v6DvY4l3{RBNwi+C6Ip!r}f+U2!A3_&O!`_W##D=TSmsGF1vLJs@ zwe!@fg4(U%tQGEt6U`%#s0v1L2PrFa#399aJec{X8d;1-jLLMjSC<$$Cs=9EZ`+s*;iCD0x?!MhN{ zrc%2ZwXcbfR@%%?b9LQCrIuVbNJ-f%(Q!NGeKJtbJi|gQkJV}*tMO|{dNF~dT+{rA3zm2_v$tj~{89)B<-ytBo`e#3z-GgL>?(Zop zWBm}k78sXv<(_9fbP;Iiqo zM}+WKUqv2RVkR6{NZenK6_VH=TRh#j;GveHV_Fzlb@$*gxJ85R?!ikFG0h@YBg!9T zD@Fk*xT;Q1iq4s@+0Z^;prv^nJR zlj?!%edFj31tW7iF)bqe8(lSCo%F8uW{U3(cjmvgzUSh-OSsv6e*g69)zgDZ%R9T6b48HiH zX~mv@{+;hXMY&NKeq0e;D)KZ91fOy+1lvL_UBMc1MH)YKI7#bh4cWV#B$|>UF0()` zD_B5Lt-#}C-Cl&7dR!JH&BpIAM&xK>vZPKD=Us(n4(I=(h(69s$6hukACNqLZ*5{q z;Htr-oWSHv#wz0&Q!Ui_ehxorPGj2k`$VUadmz=OoMM_0o(0XwzRqMU26NOT_$}c_ z6;+|=GaQ@^H9qu>ib9f)$KmT$5Ea9O_!NF*0YkYxX>3I|f>_ zwr^gs>eCNKhU|NfXZ5r)=@H{dFL|e|%CA)8h^?e-XSUXZf@C3#hH-_@AQ{eijP5Z$ zmN>uDyu~yH=#H!7?s5AjbM81!DzCJWh$aIHaBEHb6A`m0@(Cxc)VLiqVGEjZ!g>`E zQGl}8)c0Hbe3G=A$h5<5v*}ZS4)A-Z(5HW|811G}BiPc9;d7th7&eRR?u?MgBdKkKnW8y zy0Cl2itaDA5A5C=ThJ=!1z`Uq@U$FMh5BAVv2qT5Bn!v zGDSa&8^#b-F~rSOmwOe=7iN9n3Ita#Ka^K1tLXtSwujS$mVCAn{l~uPLkw4 zIxgaqAWko{0pRn+A+lK_cfPQbj@uUIOtn@&ib7j z3i4uK_H3&*T#|DOoKd8ObMhx+j9(YAT6g-obn)Vl)HR z>Go2h-@v*JYL7FcC>0WNNX0rCZA-^GIeaeGxtejJ^>WPYqTL7J20wFP_vE%fxc>5d z28!fV+1szr9`wukvrE^Id&x!P4q59au;I0@$2hp{%+OKow;GSgyy!$6_v4U+F6~5c8;TA1umV#lZtjy?iAXo-2X?zxP7YS>{hE`~mqbs=;9(9YlW;v(q%zdB&VJdtY5*p4#bi+BOU) z=Bb?_r|nscpyaX12;QvZlzW_^1A34p!Th!`v-1m$K4Ui6a;(W&<+tIWR_O_~vylCj zp5LaHNIlP~?_u;()>$H+$ah29U(hrb?+KnZ-Xi{M!6ja>pcAt zr!6@89jDGwzE!4~bAlRXy*kxSw>zgwsrFs*-Wz=hh+n$Wo|X&Z3b&SaSNs`pOTsUmlSH+&JNPBXg;idO(4^y+{FG;r)+fnt<`gz- zZmr5yOU+^1Frre`(sS4(ABKH)5Pfhu1k>#U?Toq&?*_pfhd;9dm+k`x!6cXI2qycZ z1a>eeQmhb6{$@IY=@SIAG!ue}3D4jLQyM0Eqxbq6k=u7GbNecb{|vc(^~HY??VXV9 zzK?y(oK;&s_wrt=X6L47IU-GErZh+7tIU+=XhEJ08eKwIj&$2?uiRqlG#>KFCROr?n=~2EhKCkrt{OQmV!U_;4AbRPFFf6bGpv|#Y_PBk3^NO= zkBd*lWElIynf4mc^)5}%e)b_PTfn5nim!c8%_!LpOw{2DjnByu4NiMDYR6k8lG zR{@(80aor%N84uuf2#)1648?UFP*7G#^Ye84=HPVqp!&L{v>uhYCXmL3~(F!k~tB3 zaqqUR`^*Kgrv?{}gd_NwBMKS561`Di+?0-arm?L3JhxL^H(qP}88J_eXde1NS0q9N zN=7uOf4Ag;(8sfOM zUNXB3xt^?&F&L{@K} z#osv%0%{H%IeN$SYs4vEu;aji9gDXH`fB$~j^yP&_UX7!zCt|m1*^Qb(}$^mt?1{; z)7D+$YaC^=PrRMJjB39=sU0U?>Gm6v+Udiq-@nyX&jH#Xr|)FT={QtM@-uW4c1W_g zM&dq!G>O(Oi`!wY3$G&0Di_+u&@;ZIU`M7P5PszL8e;0)YTu&ZN0w+Lb`UhphaP%; z;j{MNEqwE#hem(VYkoBLQSn zrFfsmWh99)P$I{`&1K*wU|d1dkX#!x%?PAz_(^mUQ_#md@i^*5b?O*nhiW)z0hIg_ z&FTNO-a~t$E2ewH%c~_%I^VkcZtta+eC=zS zHhLd^#4N~T!Zb#Ue(akvS|3)UbuGT3UCywS9R)0ZrEEGY7UuD6vl%c(m)SIh>5iUC z^rm@sFh-Swo`f)tzQ`2x7oBwF#m7NW>MBQmCgoi95CyweQO>i@@08V0hfwYZn)4k{ zUT+-cX6AUdMZJUOq%BYq%@tRF+%ZSF#hI}(E&*aV^Uy${QwK?MF@o*ynE9o^4E^{l zBYq?C)Utk7pH(?(ma(R`Y0HNGHMLP5$Cx#D_Z3H0O${6@UOsgTz?*!OZNy=WzpgxT z)h(Cq<~Y!&eo1B#q7mMdS){REcO264KizODQy-kofLWj!M{nvJP${}G4-I6`Ne0ao zKLwUIiG`p}=k*0+iu?<&xh%SM>)_hjsHXesr=Hp+d{Bl!JF}ksJm)mEmVTPk;1(Ky z`XuV66`Z8wB0WG?9zdE42LmfZ0%}L)MvVodHA)qT!{Fd%F3mfmob;f0+ZLhtiP%3o z`w7tVAEwOFd*CcEKYPFi2VET}XlI1j2cew=?sU!jbMbiR=f5usmDiaU^% zREg9nPQ-ZPQzhUZMk7xZb!oHQ59yaZ;OTH#Vd zw5~kpE)-f&v<0-V9t?Gi2ga$CbyaYBI8|cgNY;f_hZgNMQHnFPt$I)Fu@=0o1#jyI z|CRJAY|XucuV}R1eVd;2^0o@Rjei1+Pv#I8C8U7!N`;cJ)6EnsGI2_M z>^CAb$(-!mUprWnQ`g#3Ti84_x2IzwxE@s2W0cxKWhZBR zYJI6R>{bJKpi&{WD0qr~Azs!b^tEG}H3`k_y%J4mhazZ)9%(o1!?gEg0@G6RQ_zX= z;vy~ws;-+9BT34O`$%_NTXSPaQG;)^cVwcop}A*rynk*=)hQsL~j*NiPw;Ta((&8+kdsZ(&SIitxS0!yW zK4ZX+YxyeqlsksvbpliRS~3rZ`^~z?^X#IIhM@&Z<_|=Djp*mDuJ*3l=E6|n%ho-2 z`77oQ4o^t;b_8>R9i@d?|A4*;gPXbFrpySkK7yg6RE!TfvY+N4y1mQd804ZDrh=tb zfLwu#T4`gB0(YWoO)zE+_rt6QGVdJt!q}3Uj{d;-{OMx5FdQ73m>g^E>uYTdgT5sb z?UQqRi#j%pnSnYmv7os%l+)JQ+LjY))$_I+9OM{t`6XB2UWaz=C%W_2rTwp^3Ln%8 z#=`^usampLaXi$8_kF(2%()w)xBQ&Lq1azxW>YtwBf24kUEYZ97`m>WDhW#uxGmma z+QephCo_ZDUqGw%8BbN{uU={MqP15>9ityVDvPCuTy5$e^(0`cl+#Y$f)R*MQEJ++ zELz&#(iEJSJ9i=&Z0%mUsH3f|gI9Mr+>Ohe80~58&hgCZsq5bZdtZY6p#- zti(2hwR34qtiweF>9p1^i&jmnot44T=>k8Qvbq^GdRLb2yJs|Xv~(Da1sM6Wvve-c zt~{Q-rR$%`(k0tD^nDl*jLe_Nb$gPH)|0*8ic_E&H^_ zjn)lv(G3nxXWcTdRCn}OjL)0)c?x@jsNFg~zWDvxC;DaFuvOI3A%bBG<%U}2 z3s!cpBK913M6EXSlS0OR^Dw@9?J^4qL;U>IJ9bZo&&w?+MSR|Z&ud`gTkSmk_uJ*~ z%S{v$*Uul#@b>}pzvT0JyuU^Mo=G}BXNuQ0J? z51-eAzS<1WuP{*r9iRL0d80g^$wPeqc6q<^_vAlQ|5TbM3kqqyzHh&@S=lHbC=gc!Bf6IivYV(yi zee~&5`?rSlL8n{?`ircu!&M%pCmTPZ!*)=kG>_l4{?IE}O*aPZVX21^Fh4i<&AyvA z+8bW$H(#oV{pQg}%~wnDj%y9S?Xx}sPo|cNKAyM7)0uN{S$r3dhsiZVx#n=L8OSxQ zU&LOCz0zVf%NMh`CH9Kh>`(p?ZLwGIz&7JH>sMA7PCLho&fufP*;F?L&bTKiScCp-~}{AlA7QFJ@x4VYd$7N?5)%+KAo(3&z2 z2V-|Ev^-&Jf7oL$jNKVD4^Ns4KV){KJ~Lr7v1tR=M4pMoc?eGg^Iq67>&~#pS{SPXZp?W^u=CKZ#F+9Z#G{(`Ifx- zyWqLaxEZr|Ki-VlYdSnX35|pZxEgPnQ%Hwv)U(R`R?1`eT+HZq`pix}u_)~~L)!Io z<9+!gBs~k7LF7oyRfs#}NoMP>Q#wXJH}-L~+SkV*(|>24sx)dV%w zIh4T9e7#QsiHYk>^)m>%hv z^A}#WvA<{4!FiFb3tGCDUl7@@iAbTrOr`-ldO?TGT}^{NEIO!(sfGTgEi|tdBI?Gr z%^Ti4(c0Iw;@|`bYE>hkiP#>wV0m}Tf~}Ex2Uqp_n;>r3GtqI zl3VPOB?0FX$&uBA&PmouK8CrZZXi@%9vY}?UpiPs_s6@Qco?9a79CiiAZ%K}F9* zSLec>iav)F)klzqY0Tg6WB!I211d_%G7U5RH=L0E>)HgK#i8~ss@epsNZ@z^bw~}3 zpGL|m4nnc+M7mCJ60FDXTWF5fFC1wOuf@q#m#q(PnXXzq-7&d48r`+Dt-5zC7`3y2 zyN9#va8+b-^W5=6=k|ry9A2>X-mcd6tJlpre@Uoq$+n^XW#c1t1J(o`izI%=Ii|^3 zv%Tb}H-Vo9HZ*YawR84HFSzPayod)<2wreVl@KpDkjokm^bzO8ycn?!v>f};gf&n%GQO;TXxoyu(311#th>6swd>xk z3l6Ud_nmuaeD3DSNL3h&lb749z2>*9Z$n`quOl%+#W3MYDl>)&pHnwp9C>CKM{ym) z#l0cfI3zh2=9QrQ`Hto_^H+hXqROJqn#3Qh_ZH^mi7+KdeFKFRf`!XfQ%*7 zW{@ntN73>XT(XxVj2juGV%{+C?U=L>FS8jlh5zuNSsZ)K`l~+lmg%t$n`^LAD=$VqmZkWORb?W|v?6XC3wz&nouI7hoNqwk|e5i*v&YkWmMk z^Pu7s4A1Jr>h$3m-atV?Eb%!NS>?$^i1Q}Au|Mgj=gLY-%KW9J590FU-n!T&!@YyE zVtffJgkM?Q&m%i@EJku5gDOmyC9xG!#4K&$x!1X&bW6*lx)&tc8-l3_4F`S{+%|hQ zE^Dgffr^jdl0KbcZ6v&DkbUY_(#ZUL^;j7xz6d>x=bG&;+XnE^2dmI#JPq2{I8RI- ze^RW6yw|y*L5cH{Wq?9(8V5z?)Sg831Q^(|!f?QwUWi5hV@wGK6rrx#H4>E3Rm`)Ozc-z!g^n zZj-lf#M^&|w{J~)yHuzwW+zi_4>{c)ACc|OM=3qVQ!=SwEJXw>Ie{t&4m%Q@o4J~V zjhY0|@aNVBoaOy5@WC$N82RHB897FA8se{x6O;DpS@P;^zWNk6LdSVAA`H5+yeZWO zg_+Z)#{xN`+2BhQ+GbaOBmdxNyZr%b%x;a5EOe)Vi`#_)iWLm6Ju^G~_2+b*pTd&e=08v!do&|oCeC$S zYwpH8DWcyDt*EjrVKfaIRuYgc`k2a5Lf>^f=-BKE)dxDt0$ILdYi&zj!+0Rj;kQZ) za|!gDrB<)GQ>y&bsc=qWl^>k}g&m_9m>?qlb3%m+tllp`246naZ{81WUdj0a_+71G zxPcj`sJ${j59!r@>?-tQSD_!t*M3X}6ad&VKNr;d(Zhc9upd3_H$0pWpxw(8PCp9W z^I2S9#`PUs9wbdcV?r>H^s9ql$dZDMt(OvbU_TvKs4KklJP%4}kI&cyEA|^MH)RQ^oc2(A}Ny=2p+gW=rXH z1OA!;r_?~Alp$Wwc6{gLI*Kb7x`v*OPo%<+0Z~EPVMqgAIqu4)C-eK3f#H#?ww(I* zidlh@5=0_%JH8ccX`a_yWce#wYAf>nB?WmkMJP65n$^(vM{zo(O{)ypCtiyw!z`oT z!*Jns=EC-_h&7e{Ie_xE@ew@ZNj!rVxZYUFNe4%dhMy$3m^ci>Qh%>FX655d%X7Yc-u?XVWS-$i`PCU-gb?3v2i234eXSTx7}q; z8DE3F_u4DsZ7-p1KkTU2UKwvYj<$AbTNQ7+&{}PL81~U?uTE&&h_>IM?c8|VMb-x6 zgV4-gdriFUUTcxD4|>vTuTyP?dFoMXp|Qz&1G+N^E1RQwJ4fl4LO3EcSb5Ni7&nN% zt>m28QE=tuHG$Idrr?5Wtv3R`;`Z9G?;`fhKU!;zIYP|_=go#`^%|=%x4rhp*ss`w zc=KcCOGd;xgm*NgWUReK{O&d?7`Dd#VC|`4n7Dp-5$gQM;*m z#&txls0Tc5HuPDxH#juRMBRZl=(SL;uV?|Z+Pw0PPEl&0?>(Te6jR%4Z;E|g__j_R zw9YX;Xdgs-z_^BbA378ICUq8j=xMBAPbr-*H#Fefj*Gv+WwWB~guI|9XZb@lai zb;W^>M*n+D9;h__NBIeWOU+LpBR@8LtPK3zF?(BcIzMJ^P<(Ro` zbz$GdgE$p`+5{hY2OILy$9UH|- z^y4msyC-n6c%u$h=NX8+ynkEsx)ciSbh&`o8Eph)B*8eZ4o)`JJ=;rcEFD<_rl<&3{8~57>AQ4r@ z)uf!O55C)jck}owP=BW*uX@FCnl;H${wn__?A#Q~P2xJ78+o|gBL_aYE1+!`O2^X{ zp^aBoUVnY%m5mvuto7`^s_omW_SL3ZB=D7k5xMVwYD8EWiFgG@#HRM*W_6AT%@F=4 z=|{Gl79i z@CCKF^}r<*Q*&LOW^t!uU)E?o9c_sud(oEj)Qq;FgS2uy=G+{T8>J;}PzrSb=}Fa< zHef@nYi5MuZZNZB?*vmPb$j1E?nHL)logzW6}$lJ$p#RGaj4UCy3U!?a>F@|3Us)7 zamhFw#Ep!@3g_ldb;ItjL-*G?b6P*@HTf8(4%{{98G)Tfab8)WopXjTZ`>~cc1@fb zTTzvr1Wv8iOEr`<9F!V|sF|%92d|k20{L4yqq)0957`BA+ZMmMP7fXye7D&Z=C|yt z(L31lo{GcMheZcuL30>bK%+M)mGjuj`lI}h9;(GU&V;2{1;YH(gV-}8^LB+VuRuU- zSa5v<;*7iPYV+&%72xXn8M(5BBN^X$U?LMoUg5}r=oLJ##tmZhr-(Vs4A0vxD(}Mc z;Os%W-+av8g`pZFXYutq@Odx7CARS+T(9902|j@v8Ne`Z=HX(!O#>2$>F&Vu_O3qQ z0H$4Iu1Aj*7-3qXWOSZGFCF!qbE71Kx?g~`gxP9#1^oU1F1sdBl2;OtFOU)AQ+9*- zsC^0EP;D%q38zmHH7$TnAc*gSaTJAxdwf4LXkS|Dg7&4Qc0(Cr2W2T=K;4(Dx6NPK z52K5`Ml12E%$PlwX(rTXMqvJJ2bf2Hdhon^?MLvuD)wi5oCJARPtj0{!0&Gpekc0= zHqdwdSIMJG%#(wOk%Q?W#BSeBh#i0RXJ~)M{x~MzvLuvSF!o;2x|prc;pRuUWDe6O zARpFqN&^e3uB1h&C=i#7s@osGEpYke3RSnW@xD*veIkQ#Oka50u0(|IGw?pGGfv=! z>(@lTQ}GXG9Ms&-zMa7R?d0jt@s7W-k3lWs9jU1Kc6plcl;ItTYh+62*`VQeb|y3& zG<+E}_!JGvX!z-A_yd4&J39pkhY)_%dJ}jvs~=Pv=dmABk?=$%36(HbdIR(_#WUR|;t0Aws&2Ai zh!s+puYieqn6wc&(kT}!-V;;DD^~~5(IuC*^rSSl4=|La* zOml$>uo%n_U?lR@NaSNA@-Y(m&Pd1^cpQm*%))&5j`C)JWuGByfs1 zy{r?s;YgH=A6Wt{1=|uOpHlnuJV7Q0xye{qCWsG!2bjewx{e8A6i8Kq=f**#bA#zT zdwg2fF8)R8;F0I;kiHY1V(6r9H^Q13Cm_YM=9-cTsglQva_EKv<2lwblxI{z|-JorutP0FTQkkm5 zkN>-%rUde@a-gPU;)NHYcuBp!BxA3ixwHRp$jPPD&vzpbcoLqK%&< zx}2E+B<<3l2}DZQS@so)AQHm(2;_^op~lKVAK&-i)W>wW-wjM8(%`^Edi-DV(1Mg; zmJkG*ZtBln^pAe&m^UxCC{ol+K(&mJm(jGSSBs5I1VJhK-A6?g5b`VYh?wW7p zyu}3vzhd74%z6)6hvjD!PeyAkzEVDG4JL@No`AZ;Szi(&;-_t(V!jZ_eV8lp7g*Ry z?y6)w1YySqC4;(L9ko_ynO}o7&Uv}a6_=U^bOBy??dKRthTYt$yXewjgskb~4>~?5E-02=xZJjU{3gZz}N^-#$`$Zmi4XkX`m9WO?MyF3wZM;~XQ-&Nft?rP3Kn;NS&xf{Xn zjp2?9YE&{nWsy4c>~NXtNT>75TC-$S(%aen9)I`h1wFaXKcCyPV0CwCNp@M@)b%?e zksa4h<&|ZZ{QicvMOy~ano9?^ENZhh*9}c}9_XALs{ElA P7q_K0wk_U*dHR0=O+c&= literal 0 HcmV?d00001 diff --git a/Morpho/composeApp/src/commonMain/composeResources/font/IBMPlexSans-LightItalic.ttf b/Morpho/composeApp/src/commonMain/composeResources/font/IBMPlexSans-LightItalic.ttf new file mode 100644 index 0000000000000000000000000000000000000000..20bb6cfc6adefa7f1f0bb1642f2a5ebb592d7ca9 GIT binary patch literal 186768 zcmd?ScYGE_*YH0xdnF--KgYbI!~vt(1zx z3aIkk(lat2NPRGPgg2xU%x@E zl5XCf7f;;%q%t-;KWlo@Q@3|ks&Xx*$_&h&SrjxaqL&l?6ycXln4X&-KJ1~kN}2BX z73O9YPFK~Gd{2B;K6lFO3FFJZv|j1dkCeXe?TI;A<7*7-@EYOdr`k-!qf9|mW8Qo7 z-e6*W(X1!7-8WdN#zU2goi$}zcGjkw#~x9t>^7w$F3HcDH9c&tIZyoN#1Br*%FkI} zxP2pGd>_7e`n1BLfxQ-;AnDJPs`J|Pf}H8|K7MqNQeot$zg1c}s*H+KZw}l7S&Lt z;L}d^Ct8f#PQ~aRDqLT#!uc@9tiyb-Vh9VE_Fy3lXcw8G?qStSo+@3BR?T!WzR>|E z%s9$8%al3PUj;k*`KlgieS#XO&lo4hZJ?V6w5}Lb`Vk4If)6$FDBdFdF^2B7Y!qY zNq!1EtyimaL<(!(E4b2W)PLxJp)2R7*Id~peOS%-_+ZA$H~SA=`DS{}_+i6{6$Xz0o=HCL)(ixx?mgND|RUpar#qMC~+hyT7tz4<#2t$y#( z#rFUcqyZ)}*68{DNsLGR_?qHTKfZoEa1Ki+@5tu81`W*s()eL5Xrka=>Q*kJGHHX6 z_(j{68mBtoep$Uj$ZF*(t;=Xyu#VFeu`BDU*fn$=?4WLs-BD*^_oQcO-A_|JdYyg) z`z`$q_ExQ9J zI+=^HJDbkfsU{V>t4YVsG~{l&Ll?B^Z~9{oF(a`@n=#nq%sA}vCKo%;DUFP z0K3Q(Vb3%(vFDgM*jJmYv9B=;u&*;WV&7zL!oJzuihaAe6Z>v+H}<{eUhD_WgV>Ll z$FZL>&tSi1Uc-LNY{dS^e1!dp`2_o)W()Q zWw1v(la=G-JM*w_bskg>{iK;%N6nN~rkP%+V)+Kg8#pdkBUN{Lb6sk)OtUwr#cVY| zbyh7@P2Z=y%24f9bNX9F>!YY3HB*hf;4`C=RAbdZRrMpiK@Nq#OFE!#Q$=dHN-fST z><`(8s~gqS3sR0y$*L*jx*EWGAeK=*RVN5jJypr~iBylN>s2oP3=_5(QR-dwsG3J^ zZG4|-;0uyUoDfeNCq~_)=BP0$z4)uLYLdFFRPNv`4!RrB3x&&7H?tn@OEb!f7s!Cvr?+#Zj1DE*j z2({d~-*=Z$Rh&@#NHxK!<@-mgB=eK+j!_NFd%n9Y{d~Faj#VkTz;~BdHS|#59jB`4 zF21`W4df%w4Vh_5wc2tq5;Ra(`EH{cDgRp~|<2epjwwg@uoMfS#uJV94mpBtFl(|3{Bt{m#l79{zHHT*q z|2#E|r+jfa-+E=uRkMLtFjYwT3+ZOP3D2dJMa1oaE%?kM#U3h6^}(;dO;u7FWOEUW z2=1ms`CD-p61RXarsG$rT4M6}qNRmCm+*d!7`@eCzRDvk$lEmXm)r*7ntzXB!u7I zgh*==to7#$=`kMW1rHK)AUPG1pR^Rge3}nIkoF?j^m6DzkES1iMz03QuWAV2kS167D z1W9SALf8s@`{(%!E~kSd!A_{ZCo$8PS^Xa|-V(o5>Q`Dw$rpk}sR!v*1(wo`2Lp;U zy!$;}q|casX16oZc`xuz*!b}5@P9^J6j2!QLB#1Y%OcB1PKi7cH79Cgbj#>#qkoHO z9J3(i)3TMy_AC2jY`fUoW4D*Pq}<$cr^=5h|4{kw;+n@@7WYEjsR~0YyjZbG#SN7% zsx-XPvz2yLzPR$OmA|MGTjlbqs_J!BH&ts=?c?grs{f-#QjO^~FRuB1t;MzPsQq;9 z7i({>y{GosI!)@duai+{Zk>g7?ymD$GY9>&Z&Dt-DP#x)qSt-7s2wu zOM`3U>&5qrpBMi`{N{R9>t)xws@}u(Ua$9F{o5Km+2FMXI~(57@bgBuUo`omJ&hMN zKH8*PlkrWaH+i_p4^4YEJ=UyNv#HJQZ}wudX;*&Uj7 zG#zJjYTD`1i<2*D*g3CD!_>*CtI}q6t=9FG^oKIqWu#~H&ls68F{3DBe#R{s_h&qj zu`+XE=G~dgGM~#_lesDL%gmjb2Qp7~E7Pq~w_vyC-P(0a@7BND$Ziw66?L26?Uru$ zcfYCoJ>4Je{(Sef-9PC5Rrg)pf9ZazM`Vx6J>q+`=vlpI!=8yfFYftK&#!xa-}BdA zD|^rBeM9d%doS(%Oz+jb-|qcM?{9nW>-}4wH~M_oXG@ibc@jygNK%;-v^8;njE-D!09(St{iA3c5a+|diim@%*$Ff&suN&WSe7EtBjDL3g ztK;7p|LORx_3^2@rZt#0W7@CN7ffGt zdHm&9UhWpmF8H#rYvI>LWs0(j))u`rqs)wjGZxR-HM8%`r>-b_Mei$?%xXAm;q1Wd zduK15y?pkH*{f&2KKs4dpUmky=h8W&=iEPM*_@~59G`Rk%J3^=uk3T>kSj;cZ9g}4 z?#j7q=5Cz(+1zb&_s=~#PtA*&S8ZN{c?t76&FemI@Vv3}rp%i)@49)5=RGvzyAJ@AE@`hRp0EG>rO)qQF?_|O6?0ZBTJhkDXI89Rv3bR} zD}GvW`h}<$YP`_&g$^(Dd|~(tlU{h}h4)@Kw6fO9?kg`}xn$+LD-XR`{l&g7Ui;z; zFK%5Gu_}4h#8nTidS=y{RlmK|?xk5Tt$peA%N<{S{FTU8Ccm=zm9wjxub#Jh)#|OU zIr4eSvPat{B?`g-Mwzj$sDZvAcRPiz>w;p^9mUi)H;-kVpwRrak} zZ|#1&;@dsnUjFtcZ~ypC;GItIOnv8-caCoCvvI@5Z{98Q?&Nnrc`x(5?eBMaf5oQQ zO%pb4`XKnhO&=WGoWD8$!ww(rV)PWQig4=`%Uf19_8BO13D5m%z6xjF2dj~hx;;zK z;f&pL)K&UweY?I>KdM*iZQ3<8O7!xQAJREp3@ND3v!1}=3fo}pk0zU?h1y03wu5*2zrFBjP zD+Ox=>jdis8wC@B$-xf6^x*X1?BGqoM}toUmj|B>t_ZFQz7kv)d@cA!e8u=GjGv9- zTgDHFA09tC{(<;s;$Mn?HGX~k#`yQ@nR?;%%GIk-uX4Q__3G4XRyl1|K4-#Hn z(ngf@I!bz*bBFV|^Nh2~*&I-T@{}|&&_7TBpL1E@nZSy`>cE?UPXpTmy8{OUCn@QC zO1dmqF<32FD;TsTO$xTRB`pfh_e=Uz@VVfN!Iy(;epk}|@q^+=#4nD2I(}vR>iBh~ zN*Z@TNyqyojkP7+az;~9?H;pbRqjruTzJ=9Ro!U&wAT5Z=a;L8;kKfrz0 zPQAn3ael`+rFNX&acakj9mjSY-ErNH1v>`s{B?)4KDCP+cRh@`jeFj%Iot2sS#~E} z-t9xT&)$B;_Pp&~wl~?{_}lll)!e!V_QY!y;dF<+WZSY*-@~q=g==_-x}I(<>6zW; zd$W(WDl8s*(Q2zWb&B8W(IFaOWOiw7r-PH`g*wBXSt@o&0z&IL|qLt zXR7MPno?F5bJYUstcU8$s-z=psex({%&wbRm2_v7Gnt+|U-fYYz)uZ_=Tru+Y7|`6 ziOkaT)HTi$7)eXiQuUx(#`?X$>C0N?3N>2Y3Dawa)0>(6bxt%`>u)MBAHNN(M6uo< z;{=`BPP`hY=P(z)LeJN;^i}$1`p`rA9(})lK)poN9;{%aKAdH4ywcIH#ob|I#!oczv%k9xo)JJ=p@}# z-Jlcn5IDL6^&mY-_tjJMT=lx1p^ip*~ z->VMk2h|Zc{U`N(>R0`g*7_d^IiAsuep(y-oQ~Ab!c|@aH+i+Ls8{G1{i?2{SLrCd zR#$<4Turan)!{O4&^7hzy0(5z*V1q3I{Hmr7rt^(zeB5gTgU4L`d!^nf2v#Q_w_~k z6Wvm8*O%xWy0hM;Q}s^WMeo*WdausV-|KX}M|agf>puEN-3yL%Pkl;{(8u*qeNgw; zC-rcBOkbu?=wT*I=bA`8*+iRi`f?Me3r%@lpu6dR>STRdkJK~O8faHpy-7FLhx7oW z^f;q+wsCZhF?zhdTD_%jQG500I#KV_nffQ)+ZpWiaV~X6I6a+y&Uh!=$$`T=#>sWY zI$2I0YpZe21ZNuj-s#RrXOtPDZa0^j%gj(S!i-||HO!1OJxnjt+w?Jm;Gp+41L1ZL zFoWSJe~0+xYqJB|X&lE1m|x5xC*1sKelRB;;YvSkJ~vO8XW>SFVm>vWnYCuU`NF(n zUNRfa$L2Tly7|UDX}&ef%~taddhrVS@k;ZPS#5qcubKm9jX7x6nO_-;j+!^&SHEjc zL#xj)GMzIYnDb_{69IR7ui0guH!qrhnPuiHIO1E(<7T^g#wlaIG&{|6W{-Kn>^Co) zeP$J$@7K%`^M*MFpZvIa+ng}(n6qY+am|M&Lg$%)o?ycDL_J%r*H^07^bKm0UZ^(f zo79JTk@`sAs6NowsCV?W>Ro-EdQV@k-q#D%M!iHG)eo!VdYL+o3 zs`L6Wbwmq`f1}$YpXkVV+ev??FV^4c4*E--qW`7a=q3maGPcyN4IyC(6+G*>waoRbTIPIOz zrn0)$x!CFGbb?CGaISzN$t)sDrv#EzAU(79V0AuwcEJ?&MQ%aPWVL)sR?$>d0~%%e z^y?i|9R~L66NFu@7?&y_yNiYgURBkGY2OejLZWJ`E>>NkWBs6TBcOAPIW}ehdR1Ok zK|WXqu543gC~aWVr$OENLIH;o5X7!3Iw zXdoA<#xWNv1LMCE@YREX-x3~g2i3*$5C?*((Lh}iS3@_;B7mq4AM+x3q{+;YIwQU4 z1>L<&je^pa#6=W~6srOPfgt?k=J0LWsY?(A^n@~Bir`|LnwXU}rN|urYdbqTf4VvN z*Y^0S)AG&!zqTh7WM!M3e{E06%gr+1{s((n_7w9aGU+Milc_WE3(Tg!wx<=0pK9Lz zYr9bJu;D-0c~d8t)rEg;7bYZ{=L-Lmon#&(&R^Tf=6>S*wVh&aBhFvjt<81B`73+I zxI%MP;eW8F7n)gxGyck+lT$Fw6#TVq`zf;`daFosM5Y>!9fwoq1X}te5)W~e!)2^1 z7MHfJvbgvRO9Wh+wk@`_cPbt*cQSb0=& zsyfx2>P`*TAhn#yNF*mQFTb5t$Q{m|&Rxvh?s4vQ?sM*U9za%UfHj(1QO5EVH`1wq z6mxL_(WO)#0h4IDlgAJ%?MSC5qK2zP-54!UkH{wh>R_dvL#`H z167SDPJL)+LnzWkP`f5j(Pq%57SO#`mN%OOO-q4Zwt)(_gSvIlv-LbXpSz10+!FnW zUd}A(Sw^Z?nFYPhoaY^8IUh2c`zNE(c4ju;Gpj(N&RpgIW77%dBxg;O-!h~;X+C@c zQTbB-O-uQg^Zk*nTO5SSGuoNLd|PTpVrXb%}wN-6X->L0thuW!jsoiRi4(Ko)t|N30 z*0ep%81txkjP#?(M|!aI2+b(j5x>-?+?NlSaZ62xN+*3g?7vpB3(6Djmq*GV+8kBs zCjxhM3wI3*cg^Db1a8Bdl)MCnPSpONP?l0LY7isRiL&dVvQDg1&MEK2K}S#=vN%L5 zj2VQaE>y1(Se7zM&9<~9_1d#07ts)tF!W6bFX_7;U6OZ=Or!+1S}OWabDlyz&INf- zVij1r-9!=N3RY}Fp99e0k~Cvc@59;@O8(=NchhF?M>4j>S7*L0%)OZbvL0jp_ zT46A)cZjplxrt{l_#s!5awn(^x&X|QO46UhjOxEwr=fOIy0tpMmo42IK&68wfVAjZwq?||Eu)TY8Fg*T2s*u;L8=~Wp-WX$);c3qGfP98 zTbd)PNnfiVSV3*jGbUBFHI?WUt}kv+g!lBHg*+2gjA^XK zk;WjCsV;{eO*LKJy(U>rHqG3Fj4|CxnHIR~VH#m-V>)1ZW3IuZV2aG6;B10QW#!Xb z4CQHHe#10&H#5Ju$ca%G>1rxb2i)Dd8s)*T(!W;Uj0vb)F_Tdpx)n3o##u+ahN=Ok zGPrGksjR~p`NLIH{i*w#{#0G2d#Ithhx@v2t19WX?opkt>gaU$kRIu-(IeGDb+=nZ z3>0}_At~vuYaRGQOEjw_a&1C&XcIGZmKh|CYm9HiQjTn)qF`EPr`kO zZ&P^hK|PI7Q_MKk*jz)M=b$??glBKcIY@OiMd~6moc1=3bZWWlOe2i6PlZOZKQu+U=mbw+t*1)`gkeR+`7-R z4!pqMJ;!@1scWGPA^M=tc{IY0TUn<8m*dk5p&7n$V(F{ReHtQkgb8QRfY+F4`bbygP(4Z|d3g3tm-=-GeSwB(yVGrvy@8Yy%#WGt-| zdiiGt8Y*QreJ*9a{v3|7;xMi1|Bli{m)|Aij)QAtPhg@6B4mn?QGE zObeMl^hX)r{+Eo5nWc>#J7xU*Gb3Ya$g~Cr+1sd~CBaWwtwYsL-I&^vLjgT70D?xRvoUH50bg|Q)l{`#)!Vj3`B zZD3yUnY+fkg!?nn@#hbEFZdD|r0oe@emzP%k-38z?fzUmcj%yEZJj-Ye@{#U<`;rv z%IUNccH!!!kf?l!CMp6->L57Z3Ev-RJqKfTR0N`1C{M`;yD5{T2*!y@tnjQGBiH> zW2qhYz=PmIKhO9d#8IeU9Hu^|8)ZKL?$Vg~hV%Xo zrqozr@%IOs3Z37oYJ#I*1dpt_(v86g4jCgPFFW^@agQ<1v;sf=ILSCy*Wwl23au2p zqLbf(a$IC_U79YAQk?|X)-3jM4&H5h5xj+r;C~k82Fy-y`GNZlZL5XzBjzgNm8LyX zAJoMK#_PKS%vjnq^)kmUtEP0QamO;noX6inU`3bWcGaGX! z<}S?3n9nf3V0L3xV(!MgjaiQAi;;9f=62lolroZ!q=!C)d!_GxC9s6b1H8Hh@2_Ll z;;zJVmmj{K=LR@*@9_K%vrXV9jSu~FLiq@sQihPZ3b&X}{!{#~mNjOeikcsI03M*h z=a>y`#>(>#7dx<6oND0#q-zI>L?+UX?#NJj%1RpfL~mpteUYg2M^Z5mDac@CDf*vSC4qOc9&+7ANYtn3i_|3KtW9)N)_2WRJ`(B{$az~KolZnfovc%k(6vEY*A8i3 z2c&tOkmOy01h@+l;4~z@=}3Pwk=}OKJ&^hK(!J3?>4QX2u~zMie7irq_n*{sJwOjs z1*#Bf_h5B7lJ85A1`pN4kPDAM<~s^HaSGYrSmb`=bT(4n96dqj(r=28ROX>Mdj%`c z8G15u@_ap2Pt()&EpjYdq@R66HP4t+4TtA_ogs1!uUqzC?2I=ZrW<2ksaj;%Jj|~1b{W>)D4YdMZ^jpZ?-$BpdUGxmzM*{wVTC6v- zhH}+-c+?*uSN%l&sB)0Wf5r~R&B(yNKx+F>mGSKWe4^&8}@TlF?{1HOZsy+iM0 z=KB$H`aPB>d7AO>QzZ8L^bhd3fAqQBzaXPOq;`ug1M9d6`iS}riR>}>vbp-W`X@Z_ zllqjpL!VYJAf-Kvl=i%KVHIj)jAH_f+bd1Di7;hMBz%W6CJNbpj45kkO*vCu{i41! zap+upr6!t+>R+akscfpizp7@cn;K|h)iSl&saBVLYVoF?sc#yXhU`|mNd0UYGpemO zO%1ak#^8OdqwYg~@t}G@Jz`q1i!IS4nPii~PPR5^Ikl6$Y^I~^WiywkCG2MFVp2_- zVJDl(U?*EQ_ObPVbL`3a`y%%j8UH{e{)3T6c=`atL?(eYz$kcXW6W5SWyYCoGv4Hw z35GG0U2c=iWHZI&o2h1+nQks;uUnxhGBeCfbA_2@X0y}nN;B8YGgl!^nJ;_Y%(d)w zyWZSjZZr##tt?`<+b!l+a~u2J7MnZFo#rm~yxqf&xBJZf<^lG+Ein(7hs{#q=LwG+ zN&gdQ5G+S~K(q~>LD%3pbOBZ%@n4BXz$!EiUPj+wHQEGg&^}m){=o(`0A5D};7v3D z-bVVr5v_vv&?wl1M!{yZ06s!9!P8dw99@NfqC@Z%ngv_XbNEIiKBD!2cEJub3w9y( z*&{L^Gz)$}!{A3GKR=_j@C#ZChtOI$j11@qnhVF!G&o^SA`3c=?!sA-1{oJ=koL4D zoG>)cBG`NPCp_mr<5Z)OBhvh!=+CGlPW%OO{l-ocr>WD-X^zalrPInuP)CvUC!ue1 z9C^P;`r9DuZ-<<}gO%`KjLg5Y)5S@(ngHoehLh=ZbGkb{-~ji6U)sm%i`GbgXMi)% z8H6Uu5V)t8IYXUc&T#dOGXhz}Hn!#MP`5G*{2E!rR^~I`Iip0w1o^~Ht79_G$wo4g zgN!5>ZIe9Ycaxnd=$lMM7B?NRPMQx+nx9^p4`!O5UYeg?nx9^p zpH5n4c-FXroS8Y2d_rPZn`~l+2OiJlh^+jq?1E`iBeJIDPMexDIV!6lZ)$E<_KYI? z2~li`5;M};N)kLRL_BTrd0I$_7kb*9dH3U|TF7`$i^;OddKD4lf#toF^?~!=;y z3bJS9PneQ3%kwFhH9jw=Ag3^|up~YyCVFo%S-AySGjlEoO7`FU(uLl(wxpiB zd`YQ2kC?2YyeZ=^Of%C@#Y02e3*LNCq#%Nt3&L8LeoMXZE&YP93(`z&e?eH9_ZE?r zW3d>WU4o>j?D5lzva+*trWS>d&(5MY!*gt{^Do>-DscwVX2ou1%<-_>@e#Eh=(Bl7&><$1*m&$HEP@+L{l zgrwxQ(UVFNi@~33YD2kZo!5=-Xa7tF; zM4L(2w29LSrg|k!_e+}LS4g^_PP$)l8GePOhn{}Lr-$PEoi@E~IQm8O`S1d-1O=r^ z5MF45q6JdFKxvGG6kMhM=0T&9MewrEgGKYpq$ zGViGosTp2byOtCus%r_L>9*p+(-|JJio!E2*x?!0Gb*Dvnpa|P93uDBhz!2~85d9# zABr>|`mR1ST|-?q)DbMy35o48qq~(r71OO$bBpR$oOhtRkb~|e5yN|wMsA3D*)fq$ z)-Ixl4_gnP9Q5$WL64r%Jxdaa=~XJX@ZJ_j;k~_n(YsVopf`Yo_w~a2df|Obg*zFv z(SD@>3iZOSJ~FyylUvPE82+_^9FI`^=WK(DNsDdEFPcW%GwX0{6-Zj*}|UfdwV zvZhR*m=$epf5aM>QC1gTMlApgf9ZN__PxM+v zdZ`73_?B)XK6OiJ?ZejIhq-;|-G{NYU#8Z6xK9mJTKi@3riuwEUH!7Q_sil<9TQU8 z`(^3s!|2V;5>i_GW$>n)y!&Nq@0Yc`AMPzN3HQ@ssVSpGN^2iC-g+@1CBx6Rtsm}9 z;Sy3({q+1PTZ%WOOGwEKrQ_G3w?0is@z$k0{qRg558k>Je;c{3>R7)m!(KYo4*q5ARh-rj%rrkn{W38DH4#ShhwpI(NaZ(Bdy z8-=MKKb}A8rg)<=^%F`bR6qXHo2OC_et4#zzBiA>-!D(5|4i`fC&90ugb;rG`bh}I z3*{H8A0IAn@{^R%*5a9`&5x%|m#0mSr%jKiO^>I=M-rzlNWJj1^~2NV!_(F)Pd~kO zetPZv^xFC9we!x+sX_FJYMwje&u4KP)CMS4}FWK)@$^Jl=>>ZM{^IHJ7z$j&OvDPO8;T8c-s_lb4K7EhDyYcYuSEw1#E!1_d4P(7!G z$~sGB?giQ6dQQJBkZn2NNwXLuVA!@{BgGcC67L(^7_2i!$S^f$U%UiLAn@Kxz=$ob zkW$f0@K7Oyv62*uy-ENs_9{uYQVHfuJ}u>0Qp8f8C7)L;`CKZ_!d5W^OR1_T^>GQ% zb{tCZ#)XVbZ;_hOH6uZ!)uIg^g_L=jzdtbD?hkBZ_Xjri_Xo;81lf~kkYYz54Gz7@ zKBSU2**Q@1hKO91nVeHFRbBE&J3p&nvP$}+O~RaIX!DVj1h%m9RU)Ip>!`U%pFKa> zgE0;nw?5RyruyXoobvu5Q8L0AtgGB1Zh9g zrEN%_K0_+C5sB4CBv`AEZY@Xh^&nEQ+mM(oK$Q4Qw6NB{i6Dfh-L|R=e^mi9SJ|0+m6FL{k z?gnJQD+Ox(jMGU!0mKLOQZP~hoTLHOHl)pWhEhT@eFKu~Ii$4Q;^1{NUf*DP%RY7F zoJXAoCWSIABmGH&FC@2GpD>(Zsb^dJDQk}t8%eA9FEZ@X(DkjIV(n+FUCr8W*f^`L zJhWm)STy#YW>r(#f)Rd$Ey|O8R$NdzRS9ImPD^8z;irC#`+4wNu3gSCUSe z*y<(gj9#dWD74wG-=pV!P3w6bt&|nv2ECQX zIP2nG^j{X?^Y`x+Xu15&I~?_pzq*0p&+f;7?62;Jfcx*>_fo>Yx))RaKi-SL$zRX5Bl*Rxeuj z8f&9NM;*7YbU;A|XX&R`tLM;|xlTQg2Fzn>g+2Lsl|ASA712{stL-_@Yv`kU z)q2q{;p7$CeuT_-v@^Y=t%siu6eu0Oj~#4_0%HTi1A_y71Kk5@+?@h#IO(V%By+00Jv*Eu(5mGhjl9F2zUSccve6m93-C zsHY3>&D5FPtL*(8b@%t~F4VBpvD_VOP2Was|Nc%v*T}m=Aq!~_a?dTfX&Z8n4K%Q= zYImTGg>_?bTe_RL3nUa>rpD+weZ~m@RcKSvUIph3q}3L;%H4eTFWUwW0@niD>S$54 z7_WXBgz!$yb;7Tj#ri-WhoAA~T*@R^+U#UPYtYxlM#GoB^MQ4DwYHpW&i@3g|1Rs^ zYVDx)IcnW2tot`>ql-*@8M(Cn!TMZo-7~FwueE=)KG$2jlJ(hbZFHddit`$PVW4%_ zuy#Xh54HY#tUJuQ@3rr>CVIpvFQPh0!2^{;JhW9@F%{~haYW8E99eXF%MT07I) z-;1sDt$Uiaf425?Yag-pC)W1Lc-*>WC#^>NOz84Nn!21!5er2QM{Bq;l(!l?kIS-a zu7hd>6<)_V1skBnDbV6~IHTZQ^f)`J&FEToW)J*VXgzO13q8H2QOpoiQ&~|+W{nuX@30HG& zkc<$gIDKjzX9uD6%85a5u$nw4I4bl{YR{*!qI2_1}*2Utg$MJ{wn&+ zd%0^`z13QbG>0kY5l)z@&v1*s#G&HPvdku8Fh4L`R9S|B6CQcDt|lBT}4Aw zXUZ&6cbC5%=$_0aQ*|$yPik3JX0i_EKL@%W6}i)e-88*=9fi!tc+W%h^{f$a9S0km(G^I8tCz8dEUW!S9hDc^(4l#$Ms|x&-8Tp zlL04R@h1aTbNtCb&yo?2l^cIDV9m#$4D{79!s!L_Cj)&gnw6*Yb<9bXzMe5Jpl^^r z8E|HkV_TcFI5CWaT$(wc)jgy|vR1!u`M`Fq$2Uo-(15YuM31M9s<@}3Z-Jk22jPyp z(>;gT<({UsmT|Y?-o~y@+`OH2cVZtP#%b0W;WqSV_gBg0|CNisvc!Yicvaaw#OkUB zSd7QS;*wkOCVtY>W8C$)I0wN!;vRQ@17or7S^Oj14Sc;GH#_*<4PbRS@oN!MO-1qk z6CpMEo6Q00hEpcUvAVk)-`~Kv%QuzWlhg>uHmn)9bk0^A_{~ zy^!(NJwdtX!{YCU2tP0ej|hwnp~cB4%`cy-X~`y*^3Ylwa+C#eC`h;QlCWOx1F~BIGD@k&00H zaQ8DX5(P#28VnzCzoNwNP{NZIJ5eeQ>>LMsC*6I7oFUdRo?jDF<~9o7vgCb0MZ2HS zkHX3O7;%pamj7@Wa*vW5n4+LrT&~QPd>@{2ajpw+%6-7g!}|VDSf$%`NezY5{=;nx zDDk@>H|oFC<-uLb<8L|Ts|(Zlv(LO99SVU?{hcd}QQ?2)s^&gNy*%f>MvaPDk9$2n z`?z-k|8r2NHM~D#ThV&LzoBJ)?0)aACG=zWQ^2U`-h!K1w7VC#JU@2#y8k4s9PNz| z_d70+-I+_)dQZxZ{o`xdRZqV#q5D6Q{2d7O85(66N>KS_W5*xsLD z|6+GZKFK%Y$Nby<81u9Hy`)!Ny4>NAw;@&{}e+PS!yV#xYZgijKc^%JF8s7fQbp^0(g$GZZ`R|yRR>aS#8#8`jK-3C26?eYUD zca4;cp0keU)A*N%ezGiiE90zYZNyT3D=`;AB1+w2l{jG$u{yuTpc}i^NB*3?yoPMK( zrLawVsSey#D7i?3>M_f#Lf)rvaZ~0hJYy)!FSMyw2{qKqXVmxy;O8*Uquh^C%5}7= zGo=0)xjgN@MV`NbnE>yHfV>(s=vQ|uwovth?(NuHN%aIZc+7nte6wJnUQaMv`ol%} zez%;; zJ@>DczMiDS;p|-PO_aXu`V4TY=LB#vk-MszqVg%(9OQI0*g)qoM}|o&B92h0+Zj6| zINMET-@|Axk+_AP4u|6(%|2I|FAQTnSr&IJ_wAfZR-QEDxNo;}&&5_Euu$3$-&Wzi zS!Ry7WwyDH9j?{YNOG?M7g2bIGnj|gVy|s&?v_Xn>HvFP?p7+uoxtoio-4= z-bL8LSDb`Au_>!s`J+)X;fdICPiAGC0=&X?oXLLqHbBFH-u~>f8o=F*b?+eDvd^~w3Ff7$klntQai;T7?m}=d47cq3ZHAOn z+}vzmRYW!!G4~V@g#PIu7#&6 z>*FNW!Q1iQ4R?4l>)oF@nf4dBsWVN$M5q+@S;lh0L^%`3*%1{?1$CpT$j-_pB0W(< zko8qpH?gy_hKgW!Wlhc#k-q_kv&*u!s%ifT*qGgxbyXFlEJ1ZSyDsBNp`NM7$xro7 zecTN=-6Dcrm;hFmq)R!f9(zG2s2tf3s;09eG>_0pW)eJmc8224H~GYzYNq0z zW~QlIgj;~m<>qpgCOiWb&z{jj#W_}{NY!QU=nT@4|0E8QJ*2p2o7t+4>?9=xB+zOB z`$^~FzRFz1Ne;pxug|X1YdKZqdUL(Hh`psZsKH2F7paEqER_>^7MsP`cbGf)zv!Lj zP8G&3)4S9q!iP|f{kviV_L@Gx8CnmShg4^Fn=VydWXCCGf0X|%)|Ne|`qnF&ab7sylNmk6*a}m5hikSX@IjP*sZFN{5fSf&n(i3vllvT4iIWbnv;`GFF>MBl9EYGYSuf!>OVAar$db)f4`2EhwUap34{#W+`KYrHK)i z8j3#0rHl?j4I?czl#xQl3ZaG((DsoOX%u&WGYCfG9#cFD$-glsGRnv(WGt=J%EWDu zHDl$MD-^6tR?vsIBdDE?ovWz=P5@}-@bD))0{ z6erz-l!CPp31QCW z;BrWTah(*-65C%>{Dej%jj(%muo3gmg#mgy`tah^DFgj53C2}CXfjKZn#);IEBXM2GKIGTJHx@e~%=f8V0 z6Z{z5u5&-3jEXt=VW36oz^s1{kjNFu`dGriHa-rJus*h>6aFbB!Y5XInJ>#Jp~6#* z16GWk0VBxrdH}8Iq(I2!klF#u5v3RT7k^i#9f?HX-(K{oeL(Uh^Trc)y&2AW8<(t# z@jn0#ZTg}4I)3Nz{fv1t-+33X#4+=a!0!+>Df2-gXZEz>P|Zqx$wghol7euO%3><| zYjffD$hy_Fw#R3Z=)b$9^-<;x*uMhB%kFo;`5h%+2UTE&0^B|p1Q)ysoe8eU4ID<|!H?e>iOxs88-+CEIJ2MDRg7g4IUgGvSvBMe(ls{_}Sa*4nRvv6sN5)W+lP z7w)_63Q}1Ir-qAO5J}4x&fjjo*60`1_(}pR*>&_6s;2u5IY-dP;rY1#q9v5Ibn!>l z@s=+LC6Kgsv9hW}57{I5b6>)ox9k1`tkG=k({5mvwp<)q?4S-dk8!{qB*kOJEkR_N z|MmjH9ell&-g^kQ+(&@*7&ZM3=?Go~J5qnAEIp944={2BY+Vc7Pz`dGz8y=Pb3o$J zRGTMh3p~Gq^`nHIk`JWL2o!wA5?|Kr!aWiS=EcVK=a>5i?K6UY^ggXcyGx-8&$y2Q z&)e=l>Awd_t1h*xa2+H4D3SJ%_I~#%;0uT65$k?{*$NG)>OPBKnEO8K=1P2B4SrWu zN*e{=b(<|U91eQqm$X$bPV|zolD;=E7^qjrCA3!Dor=JuK zNZ#}iP6y=6A1yw=;M-%=P&vxK&thC~e@^OwxH59e70I)hmkRDhR&d)A(NY)tEtb@ll%AR(44S2&uTu9Bc9f7ARvGIG6X_#wwp|osDi#>MnvNq(zNbal zvUm~>X&wHEVZ%KuL>VIJql$Tn_g<0OsYqF5tTv2nFJhzzeu--bz3&}n!iU_~Ij1d- zdRR@#UnKXJnZK=t+J-~BU&4MKcX>=z_ZdpRn>pELDEekbp0BC@7{<-l!AWiRX>yUj zwUmV;M!SW^ucLQ=;_;!}*FBulMrCgBx(Bw`D?PWhJs!I6;p0IGhp*(aE!kqg^S3R5 zV4i2dj8}4*bNOjeuTl?O#1~BbU(9D}qOSSyi9^_sNu~;aTx=$Cqn7_BZRA}Q+E^?9<#bVIXUZE&k zgkr)c{1ul_GikLyu)=r~*LERB9`5Ns`$>VZfi}I*(qB>`C7Ex@__vidz{tW4g_Cs^ zq0r@4RS zDL}Die*gy(TgLP+cnaN^dY>M?p)Ao zBwpzYxdhUAD;JTG{5kOUDfTIR_gZWV1+%j+@Xaa^cz=_=<33L+(cqVr1UeBpVNGL!|6-Uz-HuLy7`_!6q@U4II#UnW`Qz6~Ze@RVyE zb^j^U<{YEjPWhY~^;Zd@6rt>BhalOJdC51_@@^>AZq}HUk?XMLVa8aQ@ozsNG9n9B zV<`=N)V8jwq<>O;p-Gj2mK7WIN&B<8xjsFS7rBJGmaY$1#y>j-%197HesyT6-nXm4 z{AQ>nvTs@*Ya(*`8anX}Evqs?5?#)0iKKPyfR( zuHnCr_&?J&Wc@BY5oy2DDvp99S^Y>H7oP}fi8&-Df_F;EyUfId@}2W!3#2Tw7Fpj2 zeL5>S*?59=+ivkUHioo(4^y55TWEq8L!Jh1n{X_Z84H(Eqp36%j-T)w%UMoidAN&L z!$(~Jhp-l>&)ozUPB?`1EQheZN$419aT8JOL;>mYaN&gq`Q=@PCNC&5~NS7$2;7-YF#ovkFmJMr_9QoOI) zrFdVpE$^$d<$cw(ysw^?_XYQxyC)}WW|5-E33^*OL0A5umczN8x!i+Q9`|4*8k3ME z6mfS!CNcxQ$xQBO%O$(SV!6EK3&(-$v2ZZ6!1zGR7p`pi!c~p%g*#cka9_(89%%W( z6)azPpydl!wtV4%mM=WS)>CE67f!LYRmJj!TU)+xCCeA?Yx%+*Enm2=i0)_M(F>oxws7fvt57uKct!ZDUFoMBr- zdCM1$vwY!!mM`4H@`VRlzHnvB7p`jCM<>e{zSQ!C`&z#6K+6}dX!*kIx`8h|(DH>V zTfT4=%NK5I`NHiiU$~Ox3wN}9;enPf+}-kp`&zzmAIldWz<(FN;6E0^9lq3Zhbvp| za5c*vE@!#JH7s{H!g7bhEqD0hQrzL{mOEUd6nD6+s#({yyXrzvE1RtmOEV6a)%qz(?@e6Eub-9jJESN(f^UPt>bgjBt&L;!h7(> z5}9Mme+7lZR)$cSy$dHu!Ts>|5mmQuLaXKSsJP6;tJ!xuzanN8-l`BP;TAnS1HjeDgae<|7V%*uvRws!UbTHZohSvWcVZIi_E$|G+gJC#eMq^#b{utEj&J0sih zRWIoBQRa3+ug~K;${Z|)ISkxeMuUBfgshhEd5;+qa{@a$oFkTmpB1d|6fM59GTJkS zo@aKl(}u$Vw*LDlTa3*Cj)sNfZ&{>tO6uAliB2$1e94>SyBioie#&R0?Mbym)5dD@~aMwlG<1{4~&a!t2-^KfLTogIfq~%KADRtRZjsFgWky1W5 zUWb|Y{KV?~M||FswrM%klDF5F1GI2@0kh~ZTSGF#2(2eXe@mnq%z}xPK#0szL$k_I z-sNp?l^!7dTTH2o?@RgyG{RyOxdnNI)83`6d?zLKKY?QMh zxM|cd}r_zCOBZeXL-n!&0eww!dEohL>BZNGsZ0Pxe%)Feg~Dn{%250FR@U`ywHb=fBcn# zDnzvWiH46P=Au^Nr_AUJxCirZ%?#;nVI%d_bI-8n~;xr9(PyS_2G7~1>a4$ zZp@0$xUaBc`HF8(TKaGt?%hG^?tRva@Zj7>X<=1azX;duXFI$1+MOc2maXB6?z483 zfy-&v1{Q2`6D=))N;t$K?USo?`?nqzdr}Lc_gI=waRSJMqvEY+;f^r3fI}Wa(cC63N2p64IHtT78&)M0G#IpI(&Y3Z> zQWfsSVPHnC5W1Kldm#z10%n`mICF!g3XIY`_qQAQx%6SmiGq$BK2i{j{ z)34H&zreiQ^z|C(1<6ipIdMI-+gi?2mvhuRqLI@X?VGRQdI zFsnJ$)@n{ATg|Bmt2x!sYEE5bnxHv_-jr#Ic9m#OwXm8~Wvu2@1FJbz-D*zpM=dm` z!mZ|1SF1VI)oM;vvYJ!upJfMaJF7X>)oM{*<=*Q^x8~IaYrvVD+aWto~FPt3MTPwWZow4Jr9!Xe5946}n^`u6A15)sCDDQcY#p)lRcg zs~!0paxE1E(h98Gy`6R5d_nrE^g*E&V3}0sD-!6j|0ivM%xb zqWo2mL(yabXnKP=?m=SP^QXGp&d1*RPE2M>K^ntW^jyH zGKUL5f#L3uLOJ9Z@6r3?py8sEUyg4?hR6&KTgHg;*xsD&xDTa3^gnVjPl{rscoD7{ z6rJ%=_^?91Hz1!6FsdE~LXoIDLjCah2^Vx0*tcM|K?TbIok-(FUsqN(A|pa(zzn4H zY~jBEtxf-PJ0p^OgsSszda(8{(q>J>_$Hb;<$+jcuqUYvtC#bCn0piWx~l8m|D1Cr zUFm9`Crj2~NtR{Fwq#54B+pZfO$cTVggFUmLma@#ODGvgd2K^W6H?mr(IjmtO=(J! zmImV{K&T-J5Ze%g84MU14;XpCgRo^w_kDlsoIA*p%nXKnKe z)KbYcYO6%)Zbk_k#nhtJ_Z$N}f!BbBKFwp(1EVgT=xEIYQ-deL+{m^Lzft{0i_xZM zamP~hU6}E*d_CdJXC2S|KKGOb56$8FbYOxboW^nRU_Wc&@1m>WbJ-nFM z5quHBKGO^ApAq(fD}{LPMtbmeLoeT!N@gazJj8N`Mg1V z7kI&(k7V)T@vc#omZ_6pipTMJl*R${A=wpq7mOG2eO)_~LQUmA?82h+%?!Rd#v z;FaLfsf`~ja1Z?b8s2&d8pXbvG(7I5o?*BshqFX>3`Y~;cUsg&WBJ|CntL<&m-do= z^S@^5W8coi3W;rzlo5-SMxTaV@Jvoh)b|X2G4v3a|*2z$BGq}1o;T4l23pGYKA3Bob>laVXVMm zYMf&V_sT}7g{Se5nHKAj1SZ>7t z6T#B}^kYOQ8j>@~U**IhIBkcws-O73(afU!n?N93hxr2LVM=)#sMMdy7nC->s*iW3 z(vz$&zlB3a`x!=t(BH>7Rv)!&wMR?Df0LC5{q>HZr#b<`=-MKX$^wH(`d%IyEicNF z&Gvn|tk_$q^&pr^fy+;#$y@CyU#@&@md^WfX?e;f=^jNZHM*dJXA;sEd);VSZMI#^ z5l(qR-#aC7lwA{N)BHzMxoe-@mGoy^n{>>s>YR&5o*auV z;Tz42-+?kT4qOnS=aCGb%O={H#GS#Oaq6(M=*GQh zIK{JAkUK_(>POG?W5D$rYMlm$YaE#$&AJabe+5KELHz0f9)j>HNt4d_FR;Q`GsRiJ z^8x-3Acy_LHC|yK4?ec|5iQ{zxg>rbU!*I9?cs|?{~h68eSOI2QZANr4=pT?~jubrFbhs;BI?AXQds2L8F&jG(uP0~UZHYq^uidCmm{tqk2AuN2Sp*Xs z@z03%h3?WFu{+t*4-VHe;QX{Dj-&W_&P1pc^VG&qX(yFP(M!C6Q82*F7nb0opH)3f z`ieBnIeoJ#{w1~)TaZ%qQ0Y#epJNx;`Y(8R5>OncA8tZ*fSl#6Z$X|lhZ}awzx)1xW5!ySDSospe7L7xr zeBt5kWN#B4mW!MZmwt`I-fgkYUU(I`;aHqNTo#`oHtx$JvAi_VS`8Tv6-^GXIShpN zexOM4(c!n_G3k2-aST?#wiY1&Ad1q>U6@34>c0=oTRkREI9Bn|?+ud)~8{8zbOBZ>7M zar}47NLLpSpUt8nf(#mTxNP?2VD3PR`wA7jWiz?4Uc1!@%2Qu<@neeIa1_A zUq8GAFK>kE=rrgl@3Pu_?@hFuo<$NL(T{rUL3dSi&jWEV)I+(0F=)NW$R?cyE5Eb2 zfcC+2uwPEu(tS|S`@nww!<8&ZPKmPJSh$(82H<(BTby(@wmEa$wo*G(fRlG{S2@>*u=B`0X;ke?57~Q&&D9$3sm&T z&<~?tn2qfFeK2n^{(CbVAv!BFZw^;7=&#A6EWXXb8azu`FHmY3@7m&PXYN&hxkl^1 z%BaiTf>M1&7QbU;I9|V6t+p7A^lmLamG!-%%PLQ~TK`Cm^tyg^+~wNnh#2STt0g@r ziWSMDz@`v8;Rw!7T99xpvZ+Ct{s|9GNihwl3;|_CI%}a|Js2Iw7`Y~no56OCWdfK$ z9)KBK(HH-d_|ktC(Xt2JD!zsv&sXw)&6n}_IgjwY`@?RH`zSws>Ast6!hGXS9^w7u zZo3d~;2V^eLL^M1g{A?!Ml$Kwj`m6Cys{#+%W`o=k@#+OOBUxfLZ};B=5VjR(wqra z@_<3(d&1PBfJm$2I7?}@74rt=Mpy>#ma?zWK})GuxicT^S5OD#Mp#50$5CI^Jl$~J zMt#Rqehojh2x}?-^xO!I)O`{Vot_(^iFojIU{r2|#g-c(&oDI+sJnqtxe;=JcNVZ~ zbmVmFOIu)lX`c0^)mUFz$okUeSzlVn`qJhleHol$eQBxImzH9EX{pwi7P7vydh1J@ zV0~%z)|WQHxt!l}8zVW#dejzLk6MrQsLi!8l3g}Nvdenc##z5wi}kBj2j38DF7!0Z zw=mW6Eljg~3+%wBox{#HWb)m_!E@Z=z%tvoo7baTP1KVmGy)bTTfV_^@Qrr2R1o9hmugbMs4{-(CKa?g`DL0^giR{wCE%A~QV}t!$TX?Q zGO2J)Dik+YwmC^fu1Q6nNkW0iK%vP%k;y=@$v_E_kqJDh?89ZI8_LlQ%{(c2s6-dE zAWf6`Rhev5Bi+-Gs&;-gmOZ4_B&5zHq~2ts!K9+mq@szu9uH9C|Kt~e&vsIyH^}ah zV%+AzZEvA9`}jrRIc4)YKz8F4vVBC!0jzu7=Je!e>XYa+`GY>v#6lwI<&xQEP_TrDt4)_ zCgr0OA;qth_qmhft-h)F|K}gr^pw#y~8c9Qjr_rDJ(9Ys1(vnLV&6i=uMFD>+Can3`7KIGQqU3_e)WAG(PQk#AVz-8nI9hAqRq|6x}?-~m1?-{WXBA6QZ}lZBDp8m)&XZ!rctH|n3?QUWFy zxb=h<{+SQ>uXmgtml*ace~g$#)1mSfuE}5az4DO}z@*6Ru(y1`1GK^~#(oqT zRIFqlTfStn`?6{KhqQM(ZsP6vv0wN8uEc3N`eP;!>F5Y_GnV2w<(GrGQKkArhfVz^ zlxXp_QNTlgJV$$P3)|PY>lAUUSGoT+i@rZR`~lwdVMu4*Y{(yl-b94ezaF38~ec^F%6)48>t}MVsM`y zO&IN=_j`^$Q{JooM_E&icRohBu_*YMZ}V+^lD3V+Bth}94{1?UBq^$J`diT+%Ky5D zZ4iNz+?b_%!?fM$5ByIZi))~a(exB?mI1}2IaUvzwkJ4FL?s0UXXA@JD8=1li@u@U z1Lrcb+akLA$*)25ntw(8-uDuFhIjNKm-~qs#^aal_33!@g(v7^-XJ) z;oniOcz?|B_rYm9O-}Y8Bd-xd?bmo9e;HYy_vVqtp8wFc#UJoZBc4-@MC*57z{C(% zXd`_xV=P=B@-IPKbTMs(;CPrM*e%22cw@Zp{hf~Uj{DPxxj=Gwl0AOPK`fZcSQEO#-_z)+JyuR^3~mZfDB6-k)PymyyiXbX zH3rOYO`ec1iZ0M-gGrRCa>riuJ)r!ERN8~I2+;=NBztOY#H8ccd(djZGqE$Vtv%tt zD|X7>nmF+9NF0x{7pJNGB{0MPUhuGyi2DN^*+>t2tjLb#DXZVW7Q>|WEQgk;X%w~C zGW^-gy?!gHh<3+Du5E;-XeFK~#%mGB=v#|1mw=`|btcNsX02KY?mxT%QSK`X)pY<+m3maF_7mFFxMX)3u8` zB%yYVIw}gNxoxV6^66R00wW*6*&*x8JIMZvz@0-MvSeX|pD!y21z$qPCG?m^+c9zh zyzh>IFv>PdwliswmI)T>6QgYG)Cbz4NlPTj*j-6Ad}$hLh_N^1&L8`g)CDe&w_ro! z27fUN|KYS8^zEfa=8U?aKZ$+f!>P|Y4eatYDe)xJ9P*Nrmk-_2{{^+oi2h0RuH;e@ z`(SeqkiE|KMfmxL@XBWRd%N_`h?v8%#B(2HFHVD`hvSx4HcS4OG88Iv;Z|%^8q&u1 z*`|q3n9YKcKHk_$)3flL{{@)7>;S92L|}Pkv*gzu~vdH=_H=kt{utyzpkpfkv$C2A(Q3`0nTrz}f-oudmGy zaP9}(f$cU9wTI)fT0~pTKLhu2ww@=p^A62;qNR%LFL=$Ic~mVA%F0H6WTSgzg~nWh zHgK8scS;j5wihhOp7zo+D~VG!dX1G*Ig*0_Z|wOHVgF6aWf$xkb*zLxkJ&u$>@@c-Nr{g%OYcl2-mp975yU|?;JzB3;SHYDD15ONl8gio2RuL4t3JT_=%1nqPV=KoZ^a^5v6A~fz{@{`XThGvH66#A zzY1LFFl@^Unuo<-qC2#W9oxuQ9m`_zoecPjx43L%|2DmkYx+08K>J!4>*sIH*4de7 zIIE1;ELiTNr;S6kyNCawzfa)p#q^?GM6VA!9{$Aeqtm1!9E+sz*5S)A$cJCTo~u`e z?^A}KPaJVQ#$Tt)X_6N8?>`Nq#7jQ#UNByaRsm2_FL+-4FB|cTp5mM$%+_Au+*4eY zRLAfa0^|S#wDsHDch?yh`tWj7^aO}S39c@+o!vTzH9GHfCsi=FJ3q; zDMn z7e;^CsJ&jsMjdA4sJyI3YF`wzMz{rR$lEo`js8uO8!O zx1Lj^+k=yd{BYg(;Pm~K+Vz{zX7WnkC02vw;>Y{dGc-g=O>plx&b?@!Cb@DsHx7u+ z$M)+71p01Daewp-*Zf$RU9|Tdj2lG$zS97 zSJ@k4yUDyi$-n{)JQb#gRjc4e0%X|vW%vF7P>NdIFcOoh1imysegxX z?O!XBZGxucn72F2uu=Njff2t((^N2JUvAT%Bx;PY1ntuzqn&6v9?0J0D2J!?9jd-e z(>FeEa;!Nrk{7>TqN!v!dC5b!>8tTFv>W+ijc#iDil%&jNzG58m+=T_*K5pbW%9%E zebXP&d!qNCtzP1L;up}^8H}ab#GG~akQwS_T_ekN2FJVEI^1aswW{XLigzEjaa6+F zJ@lsi6}7u3dNa7X2hMtm`ej6KAWv(AxBiUl1Ll(YS|4Sl8@2nj+SEv}5JAKK9Awn9H(?h?F7|cC$kS&&lLarGalsFJTQ^WX z@_`4ii~n6{PoIJo7v{|AUiz?ip8l2Au#3OPL4s07LxaL_X3#+GD7ZK1M=wNQAaeh2 z$8Oh;{v2mB#=H_-K!-@*92I4u?@RKQ@EpkI4^;9ElrxH)kYl|E`8wm6#<8AzFXt(l zD62H4{?VF>b?-FTkODhL+1eXpp;h9K;~L}o2R=>DI5RKskA?z6JLA1O^g+RU#JGX| zWm-V_V;1o)<7&owZnSYV!|!Dzj%M3^ggeO#uQ563Jk9N02+V1me;)0(gJ;kY!+k3} z}DUOoPu6_We>D!yUMDbK^C}^z8_^)Izu6&Um{!1=oeyVc{4r;JP(YL z27Hi{*sQ03t&d}WE}b*zH?@lBdCa$VM(m~UX-j#}oasiWMhp?VshRDWQ5s3u4LrR- z#%w0QD{qXLxp}jrzQ}q`b}?4>iBT_~>7@H^o+=O=p!~Oi<1oLSjH@|CNl$UCxqvhu zjm;P|W+7(P@bF85949@kU!>r#hb z{!ffz=e^=LIEp>#jpx{V2Cq=uCn=HVH0o?MG3eNa(2?BtK9@7rnNo!{TeQYD z?tK!zSL7&|m4{xzO}YZKaNh6*h_>39X0Ga+%%5|3K$%0E&MH zI{CpzshcFk`p09i#opl`TU>><`_&4u2VaZpS@A{~frxi@NStI5 zKjr-(Cjp^{%tMC;1&!my%lN;~wfYIgsYVTMj9n zX4X<-1#hD}G)@+&KYfhTNN;EKcfOyq0XHgEYk41#s6C9sQor+qW>tc3D>(OH08y-E z8jOFVw1|x=Qq0=FrhegL>ZMT6B1=7%#E+BzyB#YxgW_ZworZ=RYYA*mL^5t;xtOno z{$6Hq0b?3rUdF%V|0Q4kxl6XtU;I7wmdKK899Qg`3r9#h(f@_EeF;BKx!sC@Sl?Ql z;Qlmx5{;)DYs_}3qGI&-po2$Su!k=pBb(R~Utw&mbiMM6u#CusM6~TgTwK1$9!DI$ zjxn}V$s*BBrikTahq#>F4Ofws_&Vpa&h=zKzsXrgW`q^)9=DIo=9OeIA5X^eIT993(XkLpJ6tKQeVJD4--PG*U^ zmpPyQ)w`b=o_@xxO;0de)6>kx^o;kC_cAjsonZc?lYE4g!W>B%WJky(D?&Ee5OT?a zkWco5Lb4tdlkK3CEC=OeHz2<^eHBmOYb!6FvVGLBO(Wlh`f&Z6+se4D**(Q~%C(UR zP1I-O&j+O0XmYq;**=zFDV5V#c@0+3jwoQ>FJ=2cAM;bzn^OLj(V$*q9?4XoJRcdf zANc0qdQ0FLepx_K!*^`T?UBvB_3()DoJ_J@24ryO*J$|*CRzT1NtU^w#WEM>X7}**mJUxkPK0#JiI8SF5ms1Ej(W?W5-S~iYJmbswCG8g1nHjc@btsvL3735jAf>z7t-(>mx zOD&&&rRDR_w@m(i4*vqn-d|{W`^znFf0JeGZ?cU26_%^N*fR4MTTcEG%fMe`8Td;r z1AnRI-XCYV_a|EJ{Zh-iKh84kmswu@O3SOCZ+Z0#EU$i{<<&2@y!yqKSHHya>KA$E zdlxv1EW7?h%dTH$Gt9Nw40CgBZn>29iT+x@?xXHk&1GuFVp+(&mR-Y4gKP zv-#ns+x&1HHb2~Qn;-5hn;&kax87UtOttypdTf5Ul{P=zG@Bo;-R6XwXLG_$u{q&Z z+H7!V*=%r&Z63H)HV@o#n+I;1%>y^XW`J90Gr-NU8Q^By3~=2x16-H4-`nrZw;A9T z+6-{BY}U60-YIX$>9qOYx^1SnnW1!MEL@15x*DxJ4w=)KsLRm+jkHmx@(WugpB&5K z4DmR+aKvg^#Y z%r@2N@D*hB*yECo%sl zCbDd9iFBJ=!n3(0GT_&%m<#fH?|SDWHml)Eo6qnpU-Q03Mx8snJDerP zPfM{YcRI_ApO)Lag6A6FoQIwHSLXM--@Bjr{eJ5G)VU0s^E2lHn=SAnn=SAX%j#2US$$?$R-b8>)u+s|`qW!ipDQh^&jicrGsm*}%(SdN zJ~n_@Un$#7CgIP{et@! z+_Ru!!9DYj&p$AK{`|&yKcDxv3)at@+VgNv%G@FQ_08>@``FyuZPl6E`2E4$Yv<0N z+csy*oG0fzIeW+KCujd~_Fde^Z}vB4UpxEbvzJ;~ls7wX)=RVQnYC`#)w9}WrO*6} z+1Jio-u*=PA9VM0PwsA9xJJM38lYG|^4IMxSU>-O!FUgMoblK7Rq9~Bt^(@vetwPf z?KkgllYc%moj>d>neox-`=&4M_T-R{*gbl3I z>;Jv}NBnZ?zEgKg-52XV%kNXQ_tm^m^JL9g~7dS?&!FEb8j2BSahiW5%0b__N%Il#8X+t$BPf^-^#s3rPY0klI>SKq3GJe9~HJ2A1*#zaI|1)-KX+z&fAnXG56~^ zkLDC)FV0$>`PInJA~hM;rf*5_41XtFopyWbW2qBTzJwmT0B=WaAZ1JQ%x4OjW=k>6 zmWl^ehj*n+PGQq>>G(a9@RO$C?PTKR%);Z6$61ViI~$+s9DWseFdxT{x{ROliCvBV zbS=M0&Zqdbn68_Qep`nQly@@Kd4yjFzQ}%Ox_Kd8ZkC(n%yx6!TxX73NKUXGw~Wk8 z3oRSdBFn?HSpF=TzE-*`on`Jt?nTaW_mA8^a#mR8q>s4Ycfaqfa+Nu0HU7i~=WNT0 zbgAV-y3XB0=C12abN;Pq&i`YY^KR3ee<#n_`JU;_AD}Z^oWDmmPI2y)Z|wXFvm$jm z|7x1>-)zR6`_YHXoF99uy|bO4Vav~V)_a$DA9o(IxncjyyVASH`MKHYN3qH`IluJ2 z=zY=o4Lb1a&Xe9Zygzn+YqRlgHv788^x-q;!|yoHdUrDe@AJ&Pd%yF7>Ag3wk?WmZ z*u@Rbo8HfvJ$0Y=sP`-9E$=tpZ=55h{f=S_f9D)y?$ci9r0kpD=(Aoc z+qURy;3CKCiS~O7qsP5P(GA`bY{gRUT^4=XJCElsiEi{RjShNiqtAJ_a(pNIce8$v z^5Pjz%zPAmPM2AA<(Nm!c)=Kur(Nb#yQnFgH z*BN~zfHDi3;>X|hR+Lu z_TuOe5dR#APXci!5Wni(Ky7Y~9>jjS;I|L_9s>F_@Y@gc{ouAA=$`_&+kw76MBd?0 zArO~<*V5?G5aSo9$)TVo#|_UDsL9p<&%eT_$)N^m;JLu?+!Eai#JhpG9^B>zxIN}A z<-TRnUxD8&@GEHhfp!DX9s|P>d7b;f<-(PR1N`Q+H8wJo9MRITZmF(~$~Mac4lqz5t3maPm9wQ2->* z8$K?E>o18uX;ApQCd#FFH^X;6UQPkcUZ6SXJq_iz@$8A{F9V6lf#)jVxs*@|D|uG& zBvL|^XzoTJlyKqB4%!XogLVN{c>`jm>{=oEbA2Gq&~Pf?TE zdz}!_y#jQShBP3%8^~g~lB}MBk7Dv_5`pd?M^uOikst$q5unQgf0Ea{0J<#jmj(U= zT^3Y)Gk`A5Bw#7eEQ@YV;wN#xC^-a_IR@noNWra6A&{O5w98OH**>7{1KKj6y%%WT zl-x2`lW{hlRwum?-QWW0MMmE;erkXM!do-vTA&=Wk(z8yi5>tlaeIY9wgj16%CpO& z{|R(C;Bari?LPs!0q=64S&Izc0H58;JnMJDV|OD3-vghIM_&iBH-W4Q$bJB1j{#X0 zGC33~GMN-EC6m&9G3|27@EPO99B>&4a2eBm5pWp+YU#cRP)qkEBw`8YeEF3A%MT=D z0DMaS9S5HgAbt*>i~w;S5KBvE1`@IdXzPLYL7?3Qv^B9_NzAZub0l^aZZ7KJkhZebz z+43%Cy@d5r*0u284Lo-X@4XdmV|ouM{2tPDFXta;{|VNoq06@DFMzrasQb`+Z-y8n z8_GiWWuwLEg$(6_-@NFtP(Crn4*Z`Y+G6R{;wZH!H5^y*#_>?S4o%y@zSV#?H?y|D z0j+G?s7WWK_dxlD+~d~(d;|S5pj}N3&t@I1HkV^>tVX=&W@>$l^Z`7yoxLj}-fF5j^4h1ZqU8GA&{kU-~}=qYMe z3w;hkpI+#*8~XH9yS>!zFuXqieR`o!FPwB7`t-s{N1=~2TQ243mRf{%S_FN3n3^64Wke5#GNBtY9zefc(666bAEef&sP#c=eZpuMa3eh) z-O+v2wi-Hysj2ALKuw>crdVP0>qO2sv$jBkR<>=N>!hYVK(!DIE#jFaTw7`uSe%$e z4YR0W7B$R5X0oAO7?}w{yR?9Io1k4G(td)P4Z)2S(5?d7<$GJXe;ehKX%G6nO|7;; zzvI;CC^b4ojZRRTJ=A81+U&DDsj=Fef_@=tQ$bBmQIk4qa)4Uo8yB|mX7j_a9KNK7 z-i7N5fjlP(a-<&b=T2&Kw`jsP0eLZy=LvEkejSKU0dWjkaoh=@J%Tqs4*lwDf?S{t z2e{7+AZ`q}4383ynTT#_W+fLaxX<)u2z)Pu+kE-Shr`0B$!9HOYeprO+UU zn!HF&im1tPYEcdTtHJ*`YEeinUZWPf0!=U!%68Jh`4QuuW@@C#=gg)@!< z;dUS#FwV#!JuCBR0p$sB-A_AgoC8-Upajno1A9;nw0%IE4SoZA06xqfpr59&E`++m zuMc@7fczwQ9j;|BM-~LR?7_`E=j$cmRW*=}#Wuuon+{&zOoIM30NR5dn%~sQWxnWI^3S zKq60Fycz=#3NL@@?Z;U&;o+=R4r@Mer%g?~>F)6l@lh5_c-S&JlE@} z)r}nA6n)fLhbMkH{>NH;xEt8s%z6v!t(0^pdOy)N1E18)*T+#x`z^TKfgT@RL*_JqEc&zmHu$FVK0-avz_quc;*35SZ|{1VnuG(VX!@QC_p6C9+K!Yq6|O&#gszX^U> zi|pJPyg`1}yT#TUoCJah z^#BT1wMx_$RXeoJS%$~3oY4oX$PRynlLNMo(<7%C$1LYYuHD4Cj*>42qU+(%wX`&D zVE-2M(5+50knV-XW&_8fZ(vcrh5y2kId(9s%Pi#BY}I#n5ubmaJmb=~eWSU4w6M9aOl9 z=*~K1@^WZ@6|&{q?mnR20k0f|LPwCj-oSQ8kv&O0Ep||g*QmvAcr8|o zUTSfGS{$Gj+vBy^imYvm{scKoJ*_rn)F$r?wRw=*JVhVO>!ql#x+8u;0VYn%s8l?tuJ7jXJ=!D;nhytB5U9)w!gM}bgY-6^2ll%z$TMvLr3`gS9I zuY$*CkiHk-il+lDvKi^y48(a!S|lHc^ME)n&>}fNod-|nIn#jrIUqj>-tL1NvICy} zH*l8*Pp5&uLAaq8$hQLdYe4>6Ab%0axBK=ax(%)C0Xde9>yLBq6Rc0Ozu9SlIpBdneFZsZAX}}HW5L)KBta;#?y!zn15Z-W6cm{5G25xv9Zg>iA z_%+xZJawnOpAG< z%u~*=R?ZP{dji}Z2DedgyC2;4fm>O;UEp>%To8f_LTKX<5NIyi>-b+sJJ^4tS{VJ* zFBI22c^s2>6~AZ=a(Eu=`Ec(A=sx^nW|%++qLHt0YMf7U{At!3Id>E1*3lpSCGP(+ z>&;GwcP*IyV|=mOuxa1IC;v8j@Gd;r@6s~4oAa`LKjxmFm~7N}J2>|`D|2RhJ9*{} z);{h%#&i9w@30SgGl}iteM7^)BwdyI4}w%if6n7GzXEJx3ORuNV{Sh#51r~vS3@x zI|*UEa)59*5bg%T9H2V|mP0@@2o%{!b1|GZ5zeavnkuBZ3Te(m4h92h4iV#(9IT@C zD$9N&9*oaJA-q8s2>S7tJfwEOS}WE@27;}4fA|fQuC~=7=q}&r9cT^|#>HvSJPl|{ z11Zgc<}Nf(1N&)6=@F##9i(&+Tn!+luYs!pq;x;fMZwh}aK-#W0d$W5-Kk;FtsMs4 zp;4fFCkZ;~koODSA)u2tbR>Z8$Z60Wp>41Xy!kr30X;GZC3B%||Mv1TbI`0^qR~f*Y1-#V(ykX!y1iXr- ziTe(tu?24i+$VVDfgA0E~{^R(ePq2~)4hUXE z1~R}}LWjyG=kR9cD+P*!a9=u*5Rc~`z7hwTY@o>o8u`oFK$D52W&%w%mM(&SxeTsb z4j-<Bj1zElXHS zsV!gPBR?-g?KhF`?NEC!QNwRh%6H(c@3P&>aUWY|C&PxMVMEeP!rq1gy-m&1uy;X$q?UT_0C@MionpIeK-{HtJo$oGuUT#s|@3D(Wj&HQCBPsUGpB}`ok zs7n!bDFXZZsEhn%;*=a)WQgm9@C=z8zsmIhPQ1q_21x*3&S?7)<&;@Y&@n6v4aWfNulbkPZfmj2o^3 z#_Q=1@V&H01264G;C%%w_62R@Y@}_c_hYQ8-)FKHK7NW4Cj$Q?_yPI&0oZ=@k$Sr_ zus9LC-Ap(li~VfuU&8Kp!X?#k$waWa5iZ#Tmu!MdHo_(U0d_Zm-Hl*(6WHAdmu$2) zR4+A>pMD=T*aU`UrBbLx4z&nVf5krg;q3^#a0rVAmI(YEfhSUdJ_7U+px+AguLAw6K))C0Hvs)h zK>rBP_W}K5K>sRM3Q2BAS`{WszLUUNcDHhqmbY3N8J2ABzYA3;2|P0@KPZ*-N#ZS2}==^yRbpPjIqdIO5C^D zf`rM_#kat^i?@A`=MZ>Oi%*iK-na}T&2RH>1Al&>{>#9lRzQq`gHVvzihK*s`#dde zodX3U@Q^&6aKO_U0Z%J3lxZ-f6aPLR8+`%e7cOLb5nFi}>#%3vLbAV2oA|qUjf(wj z#&+`!0qvb$+B?0pcY0~>^a9t*;HnQ?4baZnN;_w30HgZc)K_p2UvCZ4a~|vY_%IjH z`k@z^Hl#yoS#XXt>0;+gT>mob%|ziFkZ$!nEoNPYPx^6mc7`(nO0=Wbr=uCCq8YQ` zjEP85CYn)topE{eke82HnqN})u#y(hrddoY(l3Pw93|n)P)Zx6Orn%DN~xn1EFz`m zV~-1fvJojbgf^fR75xqr{S_2d^hUNVhaR^9;>mr)lMfONrw<%Yp#hJf32B(f`DWG@ zIItB7Xv6QA!u_4-*M+Q$tXIb)u8c1WcPSRAev3l5tw^2#+O34OH2MTw{{~$DY@j7x zhwBfI?Xx&{P--Q#twNX90Z9YUH38K`_|5MFJOV^}yf)6O4_v(mOW5LtdaIFFdNsk% zhvT!XqpP!X6;^`|VyK z4zJNj2Z8huTsMG3?1l3V2l~uI8d8u351grucLGU}hn>OOR^y4B4L#eT=X>&7fN=-( zeH%^exLpCsKGXBa1d;tqz1231IB-`-;7t* z0_3f1+o;tP^i(H&&|_SBgg%@hYVsmj9{}sesEtEy(n1+fKyd)y)*TOJ!=X9&v%c+B ze6kP95bbAcQ3|g4;~!+}UO{*7gNt8c%-}M-?lHVB=MH4bm$8G?;0XM}cqJQkAb!c9 z1_!CZVR$B+8XSXX3V~i6JrMNS9D)XFt^GdU*010td0VnLzPF_|kho|&xSxo2Q+pu~ zyzc|&+kyBvT(cdBcY*W6$Y3fsXPgAk`Z(VX&ffsyR|2lt?sE-XwglP`F#^x&;5i38 z9|g}R!SfK-Tw@$|fajy&S+;2(IM%o?wb12H!2R6k`!{Ow?g3&&gVgK149~*nmO3CN zGAM{Gl3E1R`+@o-Q11iky+C~&s8a%79{|VufcO{??*rm~xa1wL6Iv`pMwjrc-&-oJ zp`L+ibeVi|c|YpYpjVD#KMq%othzwU*Fvl%tfj1c!-Z8X!uQApT3OPwky^#B6#?Fg z4jDB94BWecdoOTr46wZi{?MogVS6js{wc8k6xg=_`%i)W1z>*+`78kL9l%{dNj1Rz zUZcJbpqteP@F190AHV}}^-tlC{{Y($g6*FH|3moLr+L^wdKf5=0p$sxI|+2s#m9kc z0LTV`>?DwByv#|s_at(+69|WpJB__0ru2Jy-laga9X+h*7SVI?#@wgiYQUhGX!cfmBN`?Ln$Dz|EUTbq2UO7CFH zcNFTypeuvpo)Jh+|6 zf!{{*+uQ27NktBFtyRKEUF?=-D4~AfmBd>y(zfyYze@NIB-D4^s~D0vM0h2eUQ z#X1_`PrcbW;7m}y0hD2&JQ8p`Z524*kFV5%yDV_0=$~*$3=1dMXa`u{jk@x*P5Doxg5qhN*+?@itlR%dX?qW7x<8som?;)Vd0;&O^+5uF> zKy?&dodRTK=nzFq3V^HtpQt*3tOUr80$CoANxLaSm+-TS^*Va9gd4^3?=X)+_1(go z(|GeK-kQxDb9rMnZ!G7Hxzsg3sB1PHJw#pABYlwi$$L0J-42b(6Wgg*UceJ41D-gX z#1pF5d*zAVfG1vgPduU4p?dSu18hr+GYS(-`eTwcqSy6x476^BYg@S9ip*&|(IU7R z9snQG)XDU304D|Dq%6QmRzUyb;6$zXBVc?0oE(7O{l=N<*HSAvWZuU$^g3Us{vmk4 zT0G=fx&sOIeJG7edkedwQGNV2%okL@1<()~c%#|S3h^6fL6U4izy1_&+g3w>`Qf9SgC zw}JF7AUy^icLJ%#&+P(_1wgtBNM8riAt2oeq(2AJH-J>$-itu0akmUzQD}r7Bs+?PlDFKc(wj~9~j{ZOrS_*gytn!jo(c#Hr~=BabIZK@gn)GDVTbfeU%PYLD#!7bRF1 zVvV#rZ$tyE!(X@q`|f+ZFM*3bsEB1j3am{AulZEmV)E*HyhGq)yHT;naKadG`XHC^ ztc?T)!qxbcXVbSxHeMiAuW%vMB<_VK$pxxBDCUpXP{cxbN`so}e_%{ETE!nhEwA^D zKzjC$s#Vk~dT|u3!WiXYmPXn#W@!|W`*18x1$Z4YDwb98dA2hnWgR->LZR;3b};UnVZb&`>zn=vcPIbMYfUWbQ5 zzoh%kXs>%au^-=S*SLo|uuj~Zwqp>_iU_8m21OkhvK`o8^tzoq634dH2 z5sGeNo~Au5`uG<0+)Igvoi8w2?2FO8E;0s24%r4$9Bm9cgD@)8#|9ikq9+N{9u14n`E|shzkr|eMcO49Xb!&@yN?nzrc9CIKCq|}3yRIgTJHJ+B9D^Q8mOS; zYD)H71I*w?=|x6IefM9)SN}!MO4hSjFMxwCik@_laX3ZMoH{tA0#2C#r&PcxdDJlv z`HY~$u<-D8HT@RN)M0?H_^ZLtF?z$R-Mvn;`zGr?*0&hN#n*Bkv$uKN?}b^@St-GT zYdu;v9%G(7yb>>mQvGPvL`qg1sVd;n36xw-$xA7@l#**{&2&PSndp^tRB#a(GH#Ci-PXcq$w`6%L+?@Kl7SB0Lr0 zsR&O+cq+pCr$;xrds*LP-N*VC670cAiq4*5RC1Qnh6$d@1|4(qqD?H~OB)U_lR?M)9Ap z8297sKS2#)-=JN0JZ zZS-NY6*{C~qLbb2IP1g;v+C^sD(J&W_TPv7ZRE-^2YhaBXH(?-p$3HoTb=jK86F zMC^~^Q*MTOr9mAdzlV&E1D;Rt>;Smq`yI~S$$B^IHY6eg{4xrN)>$VbT^I4q&-JvVHD*SmQ%*!50ygGL zp-)D6L&hOv)u#V;aWDPgP))O1D>4u7L6baaQUOgWphq6`$iv$FCD!II!6T6^>T?yI z-6rB2o9S=dN~Xt%o5dFgSO6Cb;1)-Z@xbTtyUVb$%kfuMkykU( z?;aZ+xy7M12z*oGZ_MRw^r%t8*xSo_`y;&lk-&PX7epRb4c1F7V_7fS+HCW&qXk?m zq(_w*d_u*nC9I{4<1E91`w^5Q__e>Gt@SLP%QvHM^5!=UyMB#SexhvEOlAKoc8d|< za2QtB^l%D1c8J;yj|TgFIXUif{H1S3ALYsWf%?OZ<_hXQ-rQ&KA7XE}BZ`dfvDE-~ z9~3zR+`E7m|CQra(SHs`KQJ;&eysEcS|1+)Z6obp4Q;ETt$YS?jp7lCy)bTw_^l!u zg~Y}Eo}WWtS>su@fn~+1)ZeMT{ujXVZi8|cQ0~I#leG9UDqmV~4C~p)4|_j7_G+;V z@3AjK7W=7Xa*uclJZ3%O)a3W??I!aI7Q7P+BfnB@b-xZLDWy($q?B^-rFK9dW%S$m zeRbt{07G!8MtEwxIjt}7Rm|4M-wWWc5B$C0x21`lDL6+?$7VJXuacZ=a{Q0Y+bhD?Ng_9WF|HO`nm&#ljK>+9OQ8Pj{bj`ot$ zj;@YSN4TNAv#W>PQJrmGeO+2ExgUy)sy+8d6-rM~^U*mCvztrC&%3Orb5(XIzw6_x zBSQykrk7PVrRL^K!erbM|7s{@hTwau!GpVO%T>G-gJzu_JZpYLMhNewxEGsQr z-1>#Gsg0$dIB!nQxcsX222#yK3%>Du0ZdJVmmb=HYMJTGGN9AI)YP#tHC~9}Bk(w; zlBrchg&ACQR5W{t=7Z3o6lKl+o2M*Db*|^cS6IDEIhexEyx4)x-l2oF8}K{nWwwXL z^P+L~qDEfm<2sFVQ=K&MS}MGD&UU*xXH0Jdd*P-Yx1+tJ+%CpmP zbF&t7pLa=Teo<@Z#Fp``-6c&ORi&jvJEl!eNlz`D)IK$@YEs1~rc5f#4^NmV+_u1l z4x_Y7oHplcs>~E;s#PWr+?K|18(^*~j>{Tf7U~T%A+~uz z0T01{LOWj|83w0LD}>2nSjmCK8k_3F^ z3id?tg#3g>a$jIdI*_8$9CuI4!q(Edc^{v>U`=*g`K1dl&du>cJ@eaE)J(3sI;*<# zthU8pymJvU+JwdG@?D9o@zAmWyiA9kZaCbjSaG|GFbV#^frP?no$;4@qx` zr0+{_%y^RVaAUX!^@5;VZu zTop*KO8Rp=U(f@tC={FYgwPR(F2161@#VAYW>2n|aNg~o^4vv>I?u0eZ2e5e(kt>) zFI%^;bZ*wCKeg}+7tP48ZLg`HGr7Wzl&3HMNM~#H{7!f8HRqIt3Ts*_T99*>dDFVx z8{rtnKFSo7IHgIPogoZhkK-69(20Vi6FEA;NZt4yDLRqj6!u*7kXq63eFm6Hx4j(p zm|2u_1?=$+lVl;sZE?Fg3hTQ%upqPDaD5o1cX|56$sdVadh;b2msF)UwVoIG^n_1F zu3gvEGR?bo+PB&K$22EI4$#SD37k)KXNhxx8{R;|#0#B^i~viarIDOw@m zNZAyJHdQB51?_(!)Tu<+Qd-tK6+m~k!I{e5LiRQ|b2zimUOi_Dwz$4kb zY_jv0tUqA=3G1V*TUgW9QsPJ1xt#TLtRV_nYVZ}X6%$XLmI~*fAw09{2I8dDkqv`s&UOx4CnwTi-CgYgKD? zM`3D8Mfo`ymqn_^cdu$IEo_}q-Ei}oBtm;Ke8C6Uz7jLtbrGE!4! zOrFrwT0W<{Wo}+tervQztY} zp5-o|+g?$U-rQlEN$u#lEGVCi1lJLf`Kt{$IkN){N1*qVIK4+`=3LP`H;Dm~McVXS z;8wvA>mBLtDTdu_wvLfd?t%OvsSW1Lv~zM4d@EMNR({iP9WNhAUQ=UJS2d{r5X%1ZK>29I8KO>2F9P2E}N-q_RA zo-kwy;}*z{qHgVs(fgQNw!!Ie&UMyUbvnRd6SAZ2N8`90Av;yVWmOU`6T;++&>YaT z-jN7Nm^L^okczXdgcZIBt%!-x3Yd8XT)!eFLMsrV6(&Mc0uh?xtguI$d=Y9wgqmU^ z)Px9e;EPaGOoW;cp(aGA2@z^SgqjecrkDsdAwo@*%FsPTsKekZVB2JcQ~`4h=gurl z=&2EM)HFhJ3WrG+ay0+i6_;Hb$WcSR2A|INc9o4T;nT^fPIz))F3tCE8)wXMy9(vU`Mzy9RR2iV z!jy4m&tAANl+w96V`WK&*AWU`THRG%G9i6-7jD~s&YHPmVNQMi(BWxCrK#bt>*kG* zTv=LCy0~Fz8~ULW*&Og*LDVaqRU2p(jdQAuvgPn}NIacBC1^I4sxEv-8@CC`{%Jb5 zJ2cz!V2E-be$%X7owR*agR~IGf?wXu(}lUNm)2nae^OEIUEFo)>WKHv{LD~#W{v+p zcvE?CYI@jxn0AfhMSmRaHaOGKCQF!LgF-3{&Md*0AA>QVNCL)yB4%a7z{R~lnnA-7 zKmv<;8ld0m`bv7k#3hk64;Ki^M*H`i!CNQRPYcy0!|3yedH)5@r`5)#3+qh$5eR+) zyueLJG_W#-N&FKOnZQQiBPC8G zUL zSyN+jn5+eohp!n9wOrd>oKm@J*37vnsTJqWo=;NM&ef5Rlva3Ndnk1AxEZA-HQ~8k z^Ft};&z@X{ERwUjd-j4EGv_W}m|K@W^!Bvk;&68QiM*Q3%S$WD7EBo0>dwon$@;{! z^2PNCrjJWB550EMJXNP@o_ELP2-M{ZQzQzP_zF{e1!I`1a`H`(@;R5|(>acnB-L{| zIt&AG)f2CJO!rXVgysqAnxuH(ZM7}lqJFtlZJFB5je$*3yEzdbIz!tc)?QQ#LwYob z5SRlew+$1M)a(tKj(g6zV#1;;=Ggy?MVIGig}l6)skQ#z{H*IPTlBv!?ehPpU4G%* zoXT7UQT)BxtAX329S{ZX_txG|@bU=T5VZHrfijOOS)ac5_P)=+emh8>Q+;TR_WrlJ zN>6X^&+Hl+y}j>zF1fis^!kB>OO$Yv{C0G=#oz*L_Ty4VUjWk znnMXqn2eqjDC|c%T(&vJuaa!vm&|9I!nvu_8b*n^)%%{th>+VKpN@?DuD|HIdQ_*; zkP#2G4R#+$;2?pQn9P2W=@$!}H5&+g4z~s)WOhWOFEvROp*V>Q2@n*VlD#Yrq-z6FI%b&RVZDZtJO&amxu!0~V`u|SaocEh%ix!W z+Z2Bz_%`0H4oMb&JNK&bOa>K4tt6jzv2&$jj`6m5 zcFYeLp*7Cfo{<9aor7HV3ar{{s*p+`FniOx)b5r(!C-{zaba#AR~A~mCel%wmr^{v z>hqDc=Ux{nFUreuZ<|(7^2s}<4P7}+*k1(pkHZlYoX@EBHQav;yu*Z!7)wkNQW3Nz zI1vN65I_|-Q@AbcOfaex;@W_G1w?uRmkZSp4@#@C+g13>`5~`mYIRpxX+6H;$5&;n zC@%}8;ZDw6xG*I&-*bk3nOhT?+Fn{wv8di{#t$nm4(HbfYTca|(BU0w7j_maCgMl< zW4twj-$!^q31*Y8iGvY>!wWmy@2=@wn)1%7MOB7h>$2))g^+uKc7AHor3!6 z)sc%@li5X4*^c)SU@atS+T#43!l#p+Rx76f zXp2CAwl#661DFy}2iDeCBS0_-2-Z8&9OKQzD84)1URbSw16{a|t02Ln{!S_Zf@%T; z3?W%dy)I|xb9V3$@W@1Eu&w5KwN2zi=Xkj;{kjUB)%rbNn1>cOR2XaYv%mhx>T~jH zr%!OF&ZwPr&a|OjZsojHS*a;AXa08PP|xbj)X?m} z^g)3UDSf_phB~S7o(JESsYY>~N+{N?Yd>#pUCViYbk09do{@jnvevWbwPhBhhAvsN z{E`XN+P;ugxA2O2Ywo@EoWCiVHNSqr$Ga~no0NaiXO=D~J70>-XnGwq1R}9_{F2!-nfG$%}f7101YbgeLeRN#9 zK#~&2C0`f79qF9$yMEKfUBBi8+{Nq@?3~SN@hB3o`)B`#ogc=|9=EfKJgagYg|*?@ zkiWLx?*2vQ(7$!Mvm--~ce$5FybaTb`a6c+njXME7x*(2(>PtHyeIg5sQpN0`jn9X zQ2aMChkiE0oe>#&YGF|JgE2n55k72lKEDAj^m_wid>FX+3EYj)CvCC{ZGJONZg0Jz z&}Ewl5S7}wNz_bz5cG8epagN41Dq1~!;*N{Iz|by*1JbSRoAU-KX*=TVZ+R`u9$Vn z+M?-2=bkn7+y!k}`Kh7HE^JsnyCoy{2U**ebbY3CLG8jT=67BH$%_^?p0liE=7I?e zFPn8^cirdU&I%;k+pN<-O;L2Gbd<8)|x*9s%!g{yv&)Xo!s?W$yQZE>%%156Xs<@N)*)7AyHm1P%zd-ahMV zZmVklrRkzKZw;mK)*Occ2^lzJz}I` zwkaGdo+qww>s|Si=K6{=A4whh@4rsDdFJhB{b|Up|Jdy_y+0T_&%JN(ufW4i?l)o3 zWLdl7gUDKfB!Op{EPQf!1+VV_V^ad)BsOoPe7XDfCHw8)`{dwvLEv}UA~T*@;IE1+zV1le zw>krdNzQ$XEX!GD-Z@y5E2!k1pJVmbN;CpC^gSiHMSergEOqPc18Y7ye)hT3pXph# zdCo^N!zt~vpYS$ZvZP~8PuLJ-bQ`{}WNxV^@Wk33`pC=Zm6>iTa(M52 zz?%tXP3VmEP8oC-z00@zo{x@vuW-Gi2urg8TdL@v8uy7V_gq@Sv?=35OOojEmgnWS zET3K#qsXL>-TblQPi9?GUB2Kn!mLfrOIwxJIRB#V7c^~% zUJ7BybA6vUsvVut1dQe-^ChQ$<`e#zRA)0gud&*bUx{5l+ji6|s^Z8JYt2Ed0;2Ep z0_`@7-qC9J-L_B85<65(TQTMOp3nT}OEG@v7DKe2J*P2c=-2Lyj=4Td4E0Krkzbwp z&Ytk&JrhuD$?=|`4<~_}WXr`D57>`V5F*)ANVYm?VNM|GCN*mjR=;nyhkk8%#|}R zJkl`29tSL76i--UN4ge0Td&69SH3puYda&Jo9W#)_$BYQ>1GAa1$Tc!DV5Gr73K3+ z%nDS>t}%0F+^z*|lsFM|iBqf^r;_q`Ag0Gt&sR>!!Y~A1_MaC{U9oKHvKbB8Yi6C7 zH)YYKJs@NPC6^I2@-KT(!NTvAVqxQx()Bgz`#7-R{7 zSxJt;aA=4fAje@8lG@-oa+fT8pb$cbi(t5S)Xpjoxz)=WLvG33p+^_P{;vY>x4qBW zy41UI@OC_?UEt_{0k=h6LZ|sJ?{3S3iX_xULVqZzOYR^fK~4f8Lz9VGU<0}!c;z0L zdt1uj5_jTVbH9;tV)4*RZu`(C_Zs)Sq2IZ!L;nB`KLdPU2EGjE;*qVB5!x&8BoahT zU`xqvO>uJ;4L#b;%%*=o^mp!6L!THT7aHIC zI36qqB-rcJ(_$|p2%5lydL*SQ6edf^SD0d(-B|F&-8xi}D_}&mFzK^nOQc>Qy~9_JcU?1;{$mLH705{IFx~%E>wYbnbS}cfRxO$EJKhB^jlz z+!WrIcI_zkCnh%csCrARd7>v$P-eG}Otp-gt%9r1>pK5!XRL`@b+T!E&%l=IPVeIG zY>RrC+^-nW-fYZx5X<(Zx7FXVjqJ-nIA^nh)n#Ipo|x$U5F{%5r= z*s%YSZ}!kH78^>klh(W<@t2Ij#bI5ydA#!9zid2&tJYRvFZQW_myb>Wo987o8pO4P z9GymUbXfcTve9XjV}h)q(xLP?H2Er_|W_BPWFlqE8`Yn}MWwa&{bG{2leT7?kljZsGq5cytm?T27) z;G$EG#u@)?7_007@xAoALQyw7?L4`|KAqa<{?J*xasZ z)*;0@t|d^@CHb>jf~)Fj0Kmf)Lb;iM_ftXwPN04fO22htqNyj%_`JzpP~VBz_@*M;XkXuaU5o#m z^^sMv>Z<4ksYapgO#!Jb(1rj3iHM%7Tw~e!7;Bp+1nNmbPiQE_XWeF34fl= zIXjfxG!z(1HpCKZ#_Ef@N0Xqwb-;v5U?lQvYFBe4m9$IUy@d}er2H&iDtVy@*r4Nj zwfR8ZZNRlmSN!l^t8tVKc#SE=gmJyh_xy5}O&ORsq zKz;fL!nf79vsb*D&QIL=B>{Zb`hf$~>@{gO9tV56+@2QXlm~M-v|O<~2pmwl8Ok-t zJ&cKhnp>-o*dX=ZpCmvr`=$c?s0EP?s0IuGXn)R8DOsK)(wWqBTvQA^-LxW}fSyc0 zJ8Q37-7~*C2*ugl!HQV<`q9~K#Wvf>WMa~vJX7q|kEOrg8rrp?|BR^!rYjy_nDO`u z`^H0lhu|>I;=8Gz@IoTH_c#`Ds}_|n9+0%unzt27wd}lEl4$@dK>5K0IBQ37FZs=~ zL8bpE6~qqY4gk3WlwuIJj7ABHA0>`rgTz^Ynh0Piob0qQg40dHMaFCT(?~ar>~JU_ z*@C@$d-q&5wRrQVn{0vYsSRg^#&;%X_7oZVi$=!`oP6v4Vfej)me{bcq8N3k9*y7V|08Bqx)E z{YXj*$HZTy|FN#8Sv>AeU!dP~*E78*k<}BC*RSKeRMOKV?9tyo4h`&-wpz|fRL;p~ zHP&63HMK%_m-{}{+701-z&4X?X%c&u@FQ*!GW6)k;DqNg7_WNPN@Lc>@$t*YC`x43nEazs@H zXU>Htdz$hb#p9jrGpYJbQ-Q(GkTb7nwkNr1Fpyj`T3Y7n88a)fcU6B;v9q(^tbhg= z0xLQ&Gh8coQ4FxwN+ISW%WtgM^Kx}|+}{EcKYZAE19Dn@oQ2!LTFCYClxo;P_k0L+ z@~LHlJVDqIec*U}uy6R}GC}4hNo6e6kAPjw5yVajuW~65%F$ivO0IXTSR_4r_x^F& zIuR$=-F?b3Ih6k8DGLSI+6ru?uBZjRc@x@K=}|VRP07!y!>F{L_^igdFN3Y7$uPU0 zu231aklMDW4VI=v6%ePPRtjDNuvOAAAqkb>Dv4fw=C5Rjl3JgH)n!`snSM=pKN=K2 zt2KeyLBIisbp)iB)aE!oK(%1+$c$jhLQ^v$1oQ`XaQFi`yg|bX1p^RT3#i2%^i84G zO+4Lx;rOOqHOV#YHMP~9)9nr25pR8JF8(fWzKI zE@Smb|DNC($^MONB4fRY%BIO+XsX9w>W|fj2a}*7k=^k`Y+LAE`aMIjf)aWYbi4X;EKa55gFImCY_i?Fbo25o(CuLEYR)+lvhw>c*S}&_Mk}X+8t;BYZ-LW0P+ne?m*oiBktDA zySULBm91vpuK{lqLm5HA{TztnYX3pC?YI|- z;&#)r%+G4a6X|`~h2KmH1JT4#VLT8{pdJ}gag)+TMQ$N7aFW{wrYq~tml5gU45ef} zE25G!E6%;`ukDVq`FGs4Y2xsF)Mj@zCdOJf*~-==`)6$S@^j{5{rR>c2mnS$VzxBj zrqd^I`kH;KOA2hp^_Tx!{y$d5!d3Qao4cmDs&8FEk*)Kx58PeZ@85R!+Xh=^k4%-c z*LCfjh-@eb_pV!8684_+_E__5`YX;-XY>9oiBf-Y|E{rMa{JXYedUqTt8Z)j4^N=g zceFfITRJne##7PJjooWjc*I)4JYf~ez(sX}#wO&aD3nJ);Ms!TxrL{{U&HwkW`8{o z9{@EZj*NiIGSAI4Y%+nljarwdYUW1w&K_975ZZ?b;Z&@ zFYOm!TDn_3!+;1P-|=&}zummT0c~23OT6N`KZei`p+%7G@EA@;FV-4Kdr#}9Bbjoi z6kwMNr}Hg3vej10fAjtJ(!Ano^)J64ch+yiox@l&8py$%u%UKnbI8ZS-xGN9FZF!> zjub0^YiEd$L(B2grHkbo;j}PnIWpd2(w>#eBGQ4~jkiiEQQ440+Qh_NEkd_@YQx@m zb4Ou4#N+avetU6Vd3!8c+Bn`|tIjL7m*DJ_rT@Uw)LSfM-{P&{7_aNabJ~BvD;u08 zd2w@uvA!7{ZHi%tFo-8=z$jnelk5@CiGuX&_+DHo?!uK}WJBWjZqz=4T4=Y5v`7Fh zzgIuydrcjLidn$-7SR=6sR#_oxJe)ABJmk~udO`ydqtWb?CDtc!F=8!-6@Lo-{5(z zr0dJa*JN_!^W1nIZ%#AFoNIpLFXgvymfyZv`;5c^zRN%1UBWWY zHZtiDE^**S;J_igW8HEb0FBLkt-oyQ51sz4fN3|1K|Cc4s>d1GuAUBQWg9ea$ey5; za*p9C$CVBhYz)X284+HG_Y3cLzuQQ%tZs!wZ;eL^{*uVGeb!T9*FT(*Alf&weX?s3ajpL?|v;l1jsIil%FSOUGbrI+pV?e=n+E zemVV%Cr^2orQa>}iz_e=)nHHH$w+r573WHiwz|x_nDQmYK@mVC=0Ol-5PfR!BKasz z6gPwirI^#{E(&HN*Kz375qoH1PkrB}-m0F!j<>sXeQ03StT$12Fq-q$&um&7T{qP1 zy!v7L?69-SnH-$#^Gt8n&t3KzIoWBLUl6b66Be}Zq2?{xRp%9kz{+i-7()q=Pv&B+tf(4s|D3xTKef z=zsi9*Pe&=uUu%w?#8;Y zSgUXiwWt3*HeoBVwT7G}&h)>x<11gmn+@X4@Jci|W%N45cq@tkpjj9%>hX}t7~|zg zA{MY?N#MAw|mKY&-5SYo%-3OgW?{!JNq;>dn@*32dq=m;$zA5rhAMl-n@}J;>}A3 zwIz8KcpyDzPsi7+xZVpx&9g8CT30Vs;uep9G8!0$<^8kLSXOrce^ki-)--eMx9uDLOrcIb32@ynr&uE|GD9>u^F#jA@S_4&GW7bi#UX!64rui=W z4kmMw1vFD4Y(A}lPq`5)ZJ`bGiEN;l&jQS6Jz5MF6H;zKIueXgGvK|lM;xs&IpZ_} zdX!GWw3eUN7v#U~ajm|q|KtZc`bR661ekTPS#xgqcduEm zH|w|mPv&TrlQ#SRWYRF1c-xJ5%UMk0PC20)(c1ribwW)=)t^C%|KXI2cm6-chtQwL z7RY&^jZV-;DJN!RFbW5xNOJEmnjViojy>Z|oX?lj-=S5J-&A`BHa6l3ud?APh!c=3 zlg1s>@#*)Wi1&!I)9*X^Q~pcdFtoG}kzLOqVZeIr^5akq%xeo0Ge})y0Y>ltf?QpSHG@ zR>B5YKHlEiUZPsuCC*qnq#ppC_h^Hp#$a;r?U*5tHLh8Ev6T*l5-J0j3$dm=sEfgC zdMsCU=&R}7Vg%FvzGziw)#gr7l|S~k$p!AzUgK`+<8WbDMMFaBsHr#0ft>f4U2DAx@7E@*`*FWF#3xwSPJPHku(5`|F zsMk}Oy}*n|Q*2_glevS$=2eGgTLuo?w!y6Dhu*fS&s{>}+VCOO+CDqDy0&wwH972V z&2I_DSGn_zwwB?xpm(Mqy8gV0oqv5~IJ*AuIP2Ed=`&KZq5eG+p=|4l+3DoARiU0> zXbI|9jB-aU&5P1t%w5z&}cx3p^TucN3r8h1M;dA{u+>1#qcG4 zz*%A8teL=vDm*D%nDfU~Tdzkle4Q8ZOk_T(zl{n9^7xN*RM+(__Ly~Na4^>3KqB^V zH}d#cO?!Rod`+$27x34`dP~X;U9agX7OmAC!=a8X<1JQQ?``P|l-4^Ny2BM_)87@Y zSpA(^Z++OyH z^B6VdIz|e!{yavN5;`V{!z&xiqo!eX;Q53?%Z8tojL!HpYEu$J1Wjad ze_LC8c3}Z&Gp77KrM2Z#9m#&7H#OfDkLB40DkA<$!zIqH$#c3YgVA8ma44@ZZ?Lx7 zTi2XO0k2YiEtU6vw7456g(Z~gN|R@tj!phSFEWNW>RS7ntVTmezYF?@=2o*7 zyVvxW_)7wPZB=XfO8q7N{NfTt^tf6KbW`IXJRftJZ+ zU1f7B&@z>*i&ciZ>f47ZBls5ysI*PuDnTzCsb$UKp>m}$V6W1$Zn~`JGV~uxe+jxX zUio*pGK`$^)Tv!=YFW<`cZx6TFGC)M-c|<%%#9c@O^H%(>xrbGt-~@*LCxog+#kO9 zrpoJU5`p*R-#gJVBJT8G(Gu&r|Ndl0+hw&!F~i!PrCz-r(xaPEqj2b57JX$}wUz#s zDq@q=WFuWyq}4EGMVV+oks9vQ)EMB*5UG0rk-+;=1M&VFzvTL2N4x|7-uPAh{*ISk z>NvR{FX1%b$)jDrj8`!%Ud1GE+4oxU8YX6EU-YftD^GF&L~2U=_(l*L3PFsRD5H9f zWQ|h4`PL+H@8>(uW!_%4zWP#!xS8LOzO>^dOi~9Bma!u>*sNY7-GDcP#6oJw>N9I_ zg`~S8Y|9*UsWOu&l+~eeQ$W^Jd0RV|?I!_{1aL1JM^G#RNLHC&N|I?^0g_2_?*h3e zrIfpYq@M=%R5X<1IUCH%qBt%(%Vx=bJ=zs0j5cXNU5P?Ju_}yPh{+&1Z069Z;p4w$?<1LD{IlhOowJ8U!;#091xSIQdRK1wl zTOm{Yx!1Mhv44tw)AfyLS1Vq*HT|=E@lDq)--~|_|Gx!q0!yZL7UpaTZ&FIyo}pOO z(ZW6}=^u%oh(A;RmFptcJyZA0brDhcNcitQ+W66rh97w(K#$QGZ52i}h*kx*ZrB8> zpbt?IM9fA-o`E%F&#e7XVM8H4!AcrjiFN~xT6k@#8c@!!rdi9{9<5|QMM92f`gh(k z*z|lpRdp@BOnDRADAOoifBA;J9SwHrI#G`K?Rj&-BcyX8Mma^XJCz zh@UI}!8`0hcK-pr56^>c-pcL_=~c&$;K00lQXz*O|Lt*!?-ZmD2{G&zj_#8q&*eSe z%WvR$XAZr0#r1S#=k=)&{=*GY;%<@tqFcP(osLaCZ-1`u&2I3;I`(5tAJpeD-Z&9g z5nZlAWj`{A{cdY-$|_6g0?0H0v^^&=>>j=$9u#(o2V95J7l6-mr*A0~UyH|e&tvw- zd%iRHZ9Jp>_PmWy$7TT=I z9Qbpy_@J~!TefhH`#S9z5fb+!v{c^Tj4$P*5Sa;|0Vy>vTQeW&XftfiRIjR@3H$BU z{iw$;(cQqLcZ+PSSQ7(2@rCh=>P~6`mLF5;eVd zrGH-b+GTmKT?Q6*1r0N^*Gex3-Y z_hF62w|p`FJ6y+9z>E~hXBG^24=&3+@m7pgR;bJU0L54lxA+Nm`3WHTGIr%(8qamz z5wMBAzVt`MciYoR{U%0CilrYR3&&@`=@pXN<%8MD%@4AI(=$aznad8l0v<&Uf*E5? zdx&Y6iHe#F{}y>+_qtSdj}d8ZOT78RW}vYF5&shX+c?jMUim}hFzdBpZA7k|dc1iE zlfZhA6f1%eMfAQ&v2dbJlr9|+x$9yPp<5}s^MkA458MF1MH;C;tI@GPj$b0EDeh-9 zG4*;T(Hhly9T4%01bqn^KB(-Z0aMJEkjf8FudVG~*IC=uT4y)v_x175xrFs+XL4Jh z;9&P$p~IFG*Q+{C*d>z_otsCRN*a43GgG4%OY37+VXy{6bj(vQ>CrWWG#TWwQl-oU#gG#SZ;ID442p84!(vEu zg+>0F$>N4$UwK>k)W9`w+tnWH9Pu_62HJW%N^2_I;|r03J#!TW75U-9x>7N{Z}a=l z`P`=1^yo+j^4~U9>fbE%CnM<>CjVxAaVk!?8Afp9{1m)P4KO>Y{WG1VRg3_G8nN-6 zP&J*<_;)t%{R)ft3fIsTe$~0T+9ci2CL-kID;Mi4KdaSBwOa{5O0h7|^8%IlUSt%ROsN8;eI^MJq6(Bx99N4OfiKTuyV<;ZiaY^yM}v$ z5c^wVR(*R11u2=q?+sATI*j*C6k_6L)B|%1M>5#9!jm+MZB~YK7dRQE0Fo-Bqn4!e zqH0j%!@Ikn@xhT>lz}w)3spSi_br-4#|vw1ReDn$1`PKy{g~8&NE0@PiALgIMN9WfJ`zl7Z z9GTj9!_GwK?)kpj^08^N!ieCst1q&nbKzBop5pdt8 z2OGI?EJ-ia5GFHBbwu9j)I*|TQ`^P)73wr^l*+KRrvgiSOtaV7-R5bC}gAVw!-LXEQR_ zURG~lp5LK_Z;TT6A@>Q(L{fz@%cgKsndiB}{Boc4DytD4bL!!C6^x?QFLfw?4qqb#yRJ@ z2HGp8i_^1uS5L+Gn7&`Mbj`WNqj_7`;M4)~T+B_h6SS#adzLw26jcU?Dk(=+pKk)P z>q99?aY`R>yb2tfqQ+I!Dm)4Rv;hwA;Z;x=R6$`-r5Q?LfM3{nMRj3*{VHhXtLULl zr80k};!xnOMYt=SYA9M!Ij6slF2y8Q>^pDLEjSNL#>PrV(+mQ|@UdJwjG6Qg#uxCo z*aKjVl9V5ceQyz>cH@i8mvrUArUR8-rRyeJ)(mzO2`xW;(wT1)y6eqd3r+p8W>3`D zIj~x0PuldM>lbFHYX(Q^dS+v>x}Lh2YsbOTU^3uu>5t%yfThQlF4xOJe=4+j2CHZM z4l68PJgap792g&nUSMEULPNzqsT_-Bq-;PT+SDi3gCf%r_&frZL|M=4EI>dWIwh z%bIU9N<7O$Was;Hplalz`I;(!IwL~dc&Q*>j$}2AnpA<=zA3^C^PGcjysKpm%}fIZ z--{QoEUzene^kIxR}|=|EfhOk>umO>(e2&SyQ>owt5eNm@oKyAUDq>0FAua;32~u& zx~Q$Yzs^zUNHh%B^=umr4)r1+sj722UN=yea1=R0W2x3c8zSZnWE=Q|9>OU<5qxW( z`1Nrl8SXc2Jw13`+9g=Ga5tcDRVcP90Q?F8fkOC*5b!btl}`w090D4LfW{%XnTOzJ z9s-VsU<>wK?n#P0{Vuu5}Ff=(P%)?FTjYLPyzS2H`WMcCM@?->=>okF-n2P zr3hpyh4QTwfitBrCX~XMPzudlDdJ{IL6}PsH&cpm8C=VIYiywK-t>S=$rb2CtyUm+ z?y~1s72+j$ltmf7M4e!<9`zm+oIj9{&nd&_l;LyA@HsfiYS*e_*?1IxJcaTs$_ps3 zpe&&{)OV>{s%KM;t};eo?C>Vc$3X_8+*FK^V~9MD`<^a^`-_;CS5f4B=`me`f~cW< zjE2hTeiSvY#H-O~T=npyj-!mC%%kMN#L^5ZO|?EJxYW!A0i2^<-s^#GjUhr7Q3cq5 zf=#A>a|AsGEGj_Kn4o!#}lwhxxHl@85$+dV^x?!v{{hT&a9 zrO~T;+sE6zJ^c@M_7o*IZgLEE2HTT%(O~n;t%^)_`^CaWXRvhMn^=(_&K z4dQ+2f2)r6HN^XiJ`)>o8^X}xC@z3vntmzhgqM4=21d3jXEkQ+EhhC{5lCvvtx|u6 z9A%KxxIDR~K$S~!IgbV@D&liVC4utXk!ZN3zJ;bZN=Y;j(j*tks$k8G0aU*x9ipwF z-~YJlG4Erp_g*t}iS5nt^c(MUea8D4*Zue3AG;t19qAKdb^4QN;&}??g@nEGsM?NOi2@D8QdRy94k__sXuf%*#S?ghEcJMdL<#~9@|aO+rNgFC<;tMRPW zc))7JR6}ZIfmw*77G>P+Gc?D5&6%b1!Cx0NE={J%+zIt~w2@2bTncjq%4q>c%b$G8gWKQpDz`OHX zSa*`bHeyB#6oqX>zedodMxcKqC~PC9vJoVy5hSS*B&iXUs1X#l5tOJAl&BGus1cN? z5p&;YQCM{oQX+DcFiw+lXc%h^bW{W0K=4wO1Dg{5@S{K&1Le@LDxiotUs5IwQ>B60 zYM5EF!5XNo2Fe6;nwnU3we_4Pe3Ce^inJUDlcK<~iQ5+7wgtFt0p6hi?@)kuD8M@u z;O+&udjalVfV&sq?gh9zu2pweH{sn2@DU6Mw{CKcmRcNE_@E{R@}KuP*V&qJ3$jS_jSdkTR9v_ z$ph#^#e7))-h@`f?5QX}?_g!`q^Ub0ex+;C0c?5WxJUbU%UH55DG%CbJ)82(-tgf>v z=0*a5t*e?OvsRm&YIT>pW7XYNvB7PX$ejqB)AUbc7o_boNPi<7Xlh#Oi}2pHcyHFh zjNJY|w1P6#g|dR?bC&jC-;|S6%m%lXSFA>Z_0t^^!jacs=kCf;7kDosI82`SiyNxt z-_nTo=?$G~W=QR8N|+ZHo9;5hr`NO_E5oN7qMsf7zIglhJG(k(^Jl%Yu9?Kdj%9xx zy3808Q^wenfBfU`u>=3!tVE-jO+Ws9K^3^x4k_;|7~vH3(SO5pf~*Kzo)0v3LW7+W z>L8QNFq!_W6p*3|O)AF^H%>sDfaCDG(af_gyWuY}Aveu@)XacE|1iszSBsWPieHX8 zE*Z8KY_7^OLcw8NiouzLW8l-EoRjR?DG~&gB&I(lf|O%Sz~sqjo3IT&anIy*S!AGj zwWliQ!uH0>u(zQ))i5;MKlv|Uc`ohkX?8;m7)`$l zjsjG}co<_$9FE|`a6ctKD|--CBP5}$DJZdS{Xf9x6_VMC^Dnb@f9mW1M+lA;@BnEH z4EcW!(<|{EWFd*|{3h};q%)G6Lz|}kI@IKIC?t{>h@e*At}vgZM;7S>wjBFIo}dSr zH#`woT6Etx&tFw=#C3Mwf@`v%*Y)HD6UARTZ{Yk1*L?SW*9C`1(;vL|;R12{-5)C- zs|`m(63%<8TEnVDIk zf6WT+=4x4?Zp}^BP6>?~YjP-RhlKSLe^o>4&g3HWEoAaF5!DsmBRV7~16qK_V!Ze4 zM=DyHY8>bG9`KyK?w22V_fLmc>&DvNS$m@Sk?Zv4biTVb)}$9BPwF=P_}FMcby4~@ zaYa>Gd(+8FCxHmJ=iuj)|Pe!z2SJ(K3_|H>6-CidtuMu2Vf7!>o;Pgk+d4x;tz5CWjv;( z9&JUUv=xfAEE8d(m5F$g#3GK$9F%bQ2jxCu8)cgU z6fQ(+_vxCA%#Ii^;2wilu*1_i`)&Ob8(kZR`-kU6WHSA|7hKQ(!`Ne+#m0o_PJf*x zk^Y!KK(El=hOa$_uk~u*p>}<_+?wk@DnCCfrqT^m9Ug)=L0->Fxt)6D1`51#VBT#Y zpf>PHU<{z4((njk5dCdbOVK2Cwgx@-X=c=cF%AqpO?-4d;(pA$4+;x$+`F0No48&A zbu@DqGwnB3)#@s$>MAwfPl>^f5gq?$l9iw%Rjj8RVzQ&Z`9Al(75BRCiz7Ru=K5Ld zeXjc}?sxy<7hjv~o;Wa(ocwiH*Rzw|lLscDf3g9AHT?#RQ4J_Si`F3)>HK4zz)M35 z_XF0HlUQf>gB3QNtPhtG?-fnSdmfDBVIYTfWHG7sqbOh`EfNm7h*swk6gaajynW-Bkl;aagjG-yMrSB15i4H>*AMm;7d9n0d`2>qp0yE{D2O8 zKnFgc10T>KKcEVj4Nnd(db#jP0%T4N`7a!?v3GPpG3p3F4;k=wVkWOxbS+kHbS>6? zd2S(ZhigH1IM=y0)}FQILv8(!h)R2R=Y0v$01th8`la*th0`yUinC+q#cr7$j@9;7 z$41uvNRNd(fA91+HaK42vW4_@5EAfCNWipM_lX}eI>gd*^x^wB9;~H~Q7(lQGh0jg zTB~@g26Weybm_zgJMqCze6SPJr4#evgmme|(sp7>oR|_P*1Qwar4tmv3F*=a>Cy@5 z64z3~(AYrnqr_20QS6X3op>P^p2XU%G=*-=rW>E&#%#JFJG#LWyDiz#jY)B1Qrwsn z=zo=_&<#AJ5aY%!;D!+62G8t<5aYIFQ*}#9wv;dnPgwbr$p%sY;evCT8wR!FRDJ{~ z_nw-~X%vHNrF(+hOLbxZA0EJm2k_wme0TsK9>9kO)WbD2N>EI7VgN%Hz>o#xl|)rC zn;|JK(a~E;v{af=gGn}pye(VIw9|(cEaVmkO2L)Z87dMHK+UA75vbrPuZ9~|2_yyz=Rt=$|gsw!%TH77d0?JmjRanuWm_2vps z7%3m~OFmUnbK%ZS4_!TLEbjG8c2qZ|O4Bc0JhrXJtLq!Z=*Z?ueO$ln!tIsOuN&np zBhmF(fSus)dBQ|DCDs@ep*(Hg((0S1erEaktX7|)%hjgl7}=l=j6RZxa{2FJLn+Hl z$Q{;CN99k7?xJTg=22cU&jx4CQk}Y(!?XpJIaZ&eAC{llFhEm zzm@(LY{{OI;lAd%W`F$bcx0r#s;p?U>(XzDcz*gll~of%t()Tw@xui>)&zPyu_Ljv zm^U`q5DK?9!H2sMdI!L8qEh#0Q(Ghsj}_(>|Af9B>p!A(i+?A5S0k_JxzuI`f!nYg z)3Ol6D&wgOL9DVMR#^~(90b)1Vu1y*z=Bv{L9DAF)>RPeDu{Iz#JUP%UEz!cr5sQ< z;kpW9s5o>c#c%=VU0Cc{YxF$Ydr^*{97VYU! zsxl}dCh$~}@cDts*2OCqJ(czGVCP^-(%lyRPC;csQ=c*`_OEWQul2_Q9nll{Zs(3` zNe?`=hqo_2bacJ3xVvO5S<{>rQKNeht+){Wu3J^B|GJ>S+JcXOuuo6k-1zgK08mMR|s*`m< zAcjcKK_zF}0imn0azEp^&{^IAo>axwLSJpR;QuXqQ5UH0K(5`oJGbaExuf{ZmjuP7 zW3OV{5zhjay;zrno8(Zw|Kj{hDlTzf($*h&DgWn{KX?5+etyT?P}JR5*tzzsp}!Zd z?(XgbU0vNj`QkZU=bU?>>zvm^~*R*xiL2>I+nHfSz0$T%#H}1;^h7s6-bU>zv$ZFgJ|twzjhELjc8 zLFi)#LMqWmn(K}b%GEt88%kthS{#Zl7Dr6+g$uO^HKR7vEu~>y?fC(!aqPE=<-A9({Jidb9H6XJxXeYmDNwOoYGxU z8UBi)@XVQ^+_orRTal_AiJ3=6=V|oo`o!U{n+Crxl<$`o zzjMxYPV`zvznT7!*e|{pi<>q-%igCi9Y>^<589*#?1K+;E-kM*&S8*q*o^s;6y9Vp zvz}3=-Yg4P;X}ME-5JBjOrci>hgt5#v;=8Dm5)?m59Ay+`9%V1e z5tO4S`H%xu?rodoG(e1iI$d&E)}v<5S*FQl=1DHq4%8|jkEf9EEN^V!MeyJ>J&0Ah z)Sgs?7}hnSQblGRKB1a4l6FZ}EyX0g`4~zrUXOIvyS=5Q^#wHr?fzhLctTWG`wZvG zQ-F8QrGI|fV}O03dS5|#Sy4elQAHr!H0-xkmAi_fJSjPIIB+`QKHcHK);t=%%sg(+ zopjPo;iLVUCEIcyN0O{~k}k#RV4-36KJxIWIxz~7Mr zugrm8m;=ASxG2;A(hQu>x7J74;QIJ;&%Zo#-Rt_=oabDT1NR#HGS9ing!^$m+H-O} z+kt~v&pIlMuY}9*SMZHF@L%d&&kDYn1Ak7wuYzw_2IqQK@VN|ppLh;msNnpv|HyeR z*R$%skm-;2l*&}$j5=9LfKn)r|4OD*FQPlG&dr^*{97Q2DAXDknNDXShlp-izCLunK z98b8hqy`c6n3F4ConV;>46(l@A%G7&jOUUjAUWB}H2V%C`BZ&hdqZB?6*F^Ao8BW< zt)vDOUN}D*w(1Q76LUu{yvAiq4famd1#9>Z2jw+O6ppx_rzK~rH-cNrj^9N zS8j9KJ*}OqdN=LpDJ^gBX^u9u_f|%dbzX1!MQi3I1yD1;ITuqT1yJxUIq(-G1yJy{ zIq>Hs1t6Rhz``tIC@BEppaAG^;%OH1Bn42{U68qsD~c3A!QYVse=P@oVHU2hmlQzt zzcd5q^GN~J^SPq_-19HbT=%*j$$8EdS-5yzQULXws|W|Hij{8E%9TSWB=3aEl9i3xF_%bo%j@G3>C0I}^90Q2tQj+*g|x`2^}>}yK9xq1sJUM9xL(K^ zkDxHDyAPds7`I|;LvoE&!WPnjOG0=;q+UrMqMh6B)P0DE0O3MC{#OrzO zV-jzBK=DsVxC3xd`~p;1PlW$gQg#JjBpl*v4=Cvg;72b{3iq1ca+Wl{f@jX!UcuO4 zD)Qv?B{CD4z%_1Fp09LAqX;ns3puYGYwX)vmGa#YMU?Jle6w-{#*rT(0L8=T`^%IvP9N_0Iar z>UwW|L)FZDn)MFSE&cmB0 zF5TW`joJ_7DpjMlA!jsC=-l@de4Pm=pZsP0CBU5+U5BwvETSg=`MQ2k^4r6d*OA0k z8{q6(JG=JGm6bJqXCr2pi#3w}q#Vr{9|+(Yo>7A0N1@fyi67MA9W)^% zWDgK`4ltGq2?JfD~0u!&^!V7`5M! zaMYpJE11lWRr`dsRU=)pcl)VZ-Lah=@sbuheGlNtu!k!rBQ;T~m-HvK$ zfU2-2hC9UN9C}gn9T=I~WS)vm1u@Y8YfA-mWn(BA=OtvA6SK4;x(Np`9JpC=GiKmA@vVzGg;M@Rxpj)#$oiof~i5nUL{c z7i@~fqx#@^9jpAq@kV!T*;M1nEi&+HWP0~H83x9+#X0%0q*T%%Y|Q&G9wz+Pa%WcX zjXCg>l7cDtVh;Rga*ZnZh8*~hKWW1P??CHBz5)RUnBWkk8Mob5J? za-UtM$pDKgB}*(v+~1D^w-0R;WgcZO$`O>KDC8xXt+oe9L0u9_FSpQIK-8HjwQ6s! z1zkc|6BR~FXh?L03H#zT_;F9O{jw)6$~kqx{FATC(LQK*JXRH zC%a3(671^g6LweL)-%Erdj}c^+Nzs2-FiS56XQLb>%+C^Xp?IUi zzWd@a-_SP$;l`nALx^+F8M|a#cWIy#iOt%og{#Ury%yVMOM7{szsZzF^^exX$D4hI z#~-bZHd`3?d&xVh8C#p3VSS(E-P8@ z${}BD9Biv<+Vt*&LbsTrf$e98C(h`?BnF)JJr|F8hYHMTtJ1A$8|^uGfb$uzj*qwa z^zw#ibz3vESHE9s(_NB6Fb*frAOnbbG~usr{Wl zX|?Hv^8Vb`asROFZ^Fs5EBJIwsPep^B@q3Te*;1IW0Zp`KjH_KY?9~N0yhM z_)!eZHdfLcC_&hOsO{%VBI-`nYyKL>cgx)Ox=No)Sn*D;gR!L5!fmKlY%= zDCleW3%e9s4Kkw=a+D2Hy?NP)C)7Okp!yo*GB;08e~Z%yPrvGD?|?@eh#^(GP_#J? zLTkG{1@&!J1r@VTLPbli9N#j@Jy1{&3vT^3R3`i^VxS4%m;-+Y_dpZAXu^q6+qegs z@C`Zr7q|zS@HrFSxwKFFFa5_DO-4R4dKs2sNW>X3MCDx0RP8YEREIH6GPj?WZ;E`gW;{FD_Lod5$_X zJIFK-?WH-2Ypqd7#*;TNifWb|Sf^=>u2_0>saN|d@X|@VJoO|u)5Mp>OVgx^;Zxcy z8L%roe8uXGH>9p{hMFhu(eF8VeRB{r6!$hU37)#Z3=5N@OFoz#DcK;h8oS+L?Pb0T z*QH*DcPY~j(vs?tJnUP_kEV$w`*J3?lb!6WfWuF-Yh_@OID_O|NW2(u%mu~xNt&hU zZ4dL)ovA+ip>Iw6*ZITw9o04Nv(NRXbS?ewXME+yDf%Lcdg^CK}bWvlz1z(O=R$oXgDANA~_~LT-l25z_4kiM8 z`2)JjE7W~(LL>F{yhEO`7FY59hj5?o=A1}F;Ynf{u=1z4Pe41YQs*$B-oi4|;*v$G z6=L)XL!K$Z=n0gE*RmhX)RWjwW@{)&(7vkDg{!_W9;pB;&jgN8eDKWJ>vz-m3 z_V-p;o84AtxMjk1kNAGhR)(aw)=oFk98B<=DF?kQ<)8{V7W0*AEF@fVAqu`R2mU81 z2Pybs4*X>)2Pyc5WpHvK3O<*C?-MUed53WFuh1k}?<3_M)qf$=AMYvU9o7G!7_|E9 z0eDqd_g}xPKjk2GpIO3bJYV`TY>z=Gn-FxPeSl633DfP8KF4%3Snhjlu7vFTX8Bt> zaaf1x;glRx(@5dOCufwLm&(9n2Nl(&pj?!J@nBHu^6AJ?ByDNT3u5()m@sm=W@Zqc zJ!qx`kw zfP$WNHH)#a@ym9l*ov=WE3tR$P*F{BeRcXPZSSnB5q;%83E}L>(UTklr9E&AHK@#4 zdrht=HQH_Bp1#jZx2*g`CmNP*30=TDHwiB0I*W)R_h34mycW0k%`t z^rGf~S4$GD(({naNJ&r*7=s|VQk1ViCWQ*KS6gX#ez|Z4hb!dYTbc0Ky>*RMHTc^c zv=VkL(>VH=n;#{;<*$XO-TIB`3ZcYgRR4uc|JMO0=R-IxQ$yBx1OD|){}~Rung{(XDKn~7a%pCjaJ5l(ARH13=Er=x zbseYxEspBC%QM%#t`A6=QNgdsfjgzlsNh!-j@1RKVXOnCtibA8uYH-RcrDvl7_;TT zY_YD*%=~|^?{6iC)|w-4G!f9G0Iit%tj-@1qu8gZ*jH{4jLKE&`f)(0u7?_*{Q44< zAoPJ#8IB{k1vU88_!r15&RP9Fwfbh>rd*84(d`?J)YWrWQ%A49rB?qp3X5{^NxVTV zg3~6o2$$UExB1VUQm8nxsmSi%H`O!{^))WudPo-#TsPN2n;%kGmWyxVA96JcY2UGt z)rWQwHz$XB*Lni6+R#vYjmY=8re;%Z(BTtLxRc&5slE>%k9GVrR3`kZlDaGSqJrb3 zJfFA}99S5Xjr|W#ss2USJw7V$Q>E_nuUYsr@;(Z_NH}IQE^eN}R? zI+8kLU`-?*t&Igedo~Zv)kHjlU4tW$j%Z!H(Nh~A3zzHmlKg6a?>BSw{*wq{3oP{Y zG)CO6JWqYJVSGz#u(3LniUq@flmikTXsr}IrpbV_#nJzmT-9onH)KZN$5F;?5l(LT z%N%|B-G~dp@iEN`$R;VTImB5$Z-rlysQ{1+Jd&bffzi)MvBVs0+R!kLk{MBno|Svi zUdu%&-*O{Z-hL5rON)ik!e4S?^`KzGUBPg>#GWe0k9ZOz|m zc(2?yw>{4lnBP?&Z1bYe-QmHO%0w)%(A(n=78ERRdpspiW3()yzQ!7V#az9ybc z6rFeB);WKmv$HD@EGTlqn#Ylinto|b)Z3lvYrI0Cvtskj-D@S1m|~LTSD6#UA%}{a ztqNdv0UgK^D?P7A4qpJjc_b_VS(|bJ{fYee4Ke_@S|-4RYo)<$8Rvf~$6wROazq4- z=nc+D^gXT*hs1pTuQs3gSGNr2_e7^tA*Ws6Iald=DgB#W`d#5@{P9@&P5b=ZC08U0 z{oRpjpL;OX^&zxV$U|nL))_Kvw8c{&dvC z6iUkoRsogPw*&A1iWJ$NMdJne2iFIEE>v(I@rpJRuN*c;2s7Uc#iJTfASn>hK}tn# zaknv5T+B7|;u*C*Bb;)k0vlaPZ;1`z78khA5A1gCj-FdpUR3l{<;bki>6(@( zxwcyWGD^&l1*Pa`NHHzyN}a2=X8NYpV-%=S37nP=^CUa^gInW4JzwUi`n)T0scucR z*O<#4pT%=s!#-(>T~1`z3txFjfjFjyEPZBT%%Y}LQ)1OX#{f>DL%WvxZYwsC@!8%C z$6~IIEM+p|1+rLUs*vPzk;9oA5=|dkwQ3C1$~l||U=TT|<@RcMlIT#&kaZ8B5gGxh z%Xty7wMdXyuRoQVIMt5Xmr76XDlO2B!n(Hl94n^&Qu<@GTuw)auQ+2U8BhP~Z}w~& zEUGOfLy@&!0%OLpx4a>DcCz^35b9A`a48om_{J<;H{_15;EP!}v`FMU6@0@oIOReG zpUc4ap+7kd!pWn)mh)WN!Bqc+On=%|!6BRd4~hw^zuqF{Le+o$vi_6{6?~R(tb}oJ zCU`Ew6`!Kjb3hx!G+4V)KX;P;94+ArHF=Ufk~v&z3jyYl>kEwl;;bhh8~UAr+nU^b0D>IUH(eXD{oDukp~S!-`JSV7=@1O3&nm{yc6Qk zcu=%JtGv?Js=C}!8ZfmEV#LBA>06~{U~|sQ{7K4?3cfZoGq3AAAVWsAD`+%S86>Qk zF~h>Mi_gz-_vcL2irp-0(R>2@5mz0~UX!W2sd+&d9_SnE<`bZX7~>YXXGvd!Wy3`! zX6`tfawh1MxTngOGfb)6ab2uTl-skgl^PnXM16;pC3#XX zR-zA;9LIee7s!BwbEW()gZVq8EUEf0WcqU)DNCyUmc>}!U&@lI|4idj?PKu%6 zbIahQ3<|!W;7h*(xBEwV{|0q`)9xnW^8O0GVHuqFSMa%Ia8e|~G0tK&$64K9YN}NK zg=PIokyQVK0(GW8wT`O)TC@L2ke?^zd-~OT{u&kf6aF1ZQ5AeK2mYk=EKu-uCj4cL zr_Xp4_YcVX+^|F4hwv`rJsJ4TIq-4gqZ#;3Iq+8Fp$z<34!ps*AOpY7g5&f-pMKng zb9_KX@&4+4K9T8vqt#!&=YyI4w~A{s{UgTNnf^!FAJ&|g;XRN01KS$3FB}I*#cVNl z%%TG38qY@buJDV`O&B1_u}L^2IVLlrE7JSlXQU+QRYxt;tWyZmOChU9@;6BL%9MCj z&}=b<>t{hfREQfhw${tYY2?a39W@R(f{0=Xh+XT>gBNP9_j~V1ApOGm!{QFx7lp5x(?IWX)DdZ@5q7A=fM9e2mWZzeeSm4czzZ$!v-<))aOTR zb(!bfV_k>mUzhWodvo9mHczJi{e%PKegak2yMb}V;Kv?f))r+OQ89aE8LTrUsVvrI zQx0L~bEIpq&CKY63?33oAl6Xh7o92mlQDluj`XXfU2! zMW527!gRwbZ;(Kkw_z3!&trK1OZSb5--*w=(;x2<1FrNxrNk~*`oN{>*S6g3occ!lsR|9wCHA zN&~b!=PWfq^ZYP@6P^p2)P`#TmY zAVavIsEJwboY3(!RJG4^*GD&9weXR)?$TNP&fuz@y*qrZrP~fot@16->6V5jN4Ya$ z50C8_*!bb=7w)a>AI*#^X{;K(W6MVM{0#hB6W$KAJXg}{CX7*@anpK^wmMnm?Z8yn z54103ZBr_@RN-4=YOYC&5>#A`?YdF_Ks28f7e>fL|e~kBa3pGmt*5erC9( z(DtGnK{11n(S^Pan(9KDC_oc&2TYR+24>(d-9YHsNh^6kqO!`tL%-W1#{Wtm9#bhG z-8nB>pFcnN%=uSDH(#^y-+$G)aaEhU%x3J`a%bnAt~>k1_I8oH?yAKHu3Xm@9NpHl zxw55n+Zjvk>HkHHh5L(+vC{LzT@r&irmH1phvb;vKt~?*Q*iE=3V!o4I5A$qZ(0WD zTq^jnWpM773Vz)(IOkZwueIQyZmZ=!N;v461_mMXKHNuD{~N9T^8LAws{Xf%%X9j3 zA65O2vOmVY9V-*}$4Fz(f+w9+*}UZrx+2ufOy#bmT1=RM2~$D{=@FS|!#q;NK^6NO z07$2n2Gt-fbAXm1g(I=(s}}TLG#-{vW@l2jglR|Z##Io6s(^=85O1m=2vq^ss(@=% zz_lvvC<=FDZm!6yg}13-d}Edpo_3>sLfp2DCPq5tb}PO58iHL2HcQ{1A&<-VTE>xO zXN3Ev8sV#RX5?YHZ>br%+nN#1tmLQ&$G(OB=KN-7OzvCix_hkaFvC~KeM`ab&4I6# z`<8;=PdL^UHLBNw_BKLDff9vM!H|@WDB|&i&f3zaoFSb^e=qqrcGV9Utif# zx@EsfLobwkEe$w%#?9xW%D}lE75rER?$a;S$sG|69hiW;km=9$sQO>a{uoVIIQ8#g zy-+8!#34~xz4+-2Ah%Y|irkx~J~cbME7$@v-GkJoQ4NT^Rry#Rc~~BK@EFUJIjm&T zsi*LVq@;c}u=m@gUeU`?@Tc%za)sT_27N|eK;sn@tT3jA@WP423l*i8RGGmqUOWW9 zk}~_yFn!M0po1RQULM|tk>B*;V9Yt=tNynNc-sKG`2J5wsYdlb%KjK_ zH}p-oKZx4{jE@LN&t?m!R&bW4RwHZMFr6+XHKx0WVw|{@E4oaslEMd5)=-&Y5>y}7 zuMf}k0lR%5s6G%>9|)=s1l0$E>XQVO9+!%smV=-&C>-<7Sc=nmt5uwJ$(gPt7ynhH z)L#w%XhliC*{N^t>^%8M=L&~3b7cNmN*`)uu9hRCYimHoE-}~LP97Lzt-C^eM+MiN z*V0q3$eXevEvPw?7&zu#C5o#&c;uS}6gcu=D;3^Db#pLj1ocbKti171`*gBSblcZW z%%4-b`D54>kIJ*g!+5+18;sc+|>DA{lLk4@m+E!m*WHPOybBzs4{T!rV4)ZGC0Rg z!Eagy=U%Ab$Ckm#n=1Hq8TdYO5GEW{PgLhTmsC&nzcJGv9EYTOggcB|#b4+2|9GbV z^~?H`H&ypJN;ojqSo#5G_h(?}!q8*GD~st_t&GX!U6?u9%&8$KWJONR+!>H0@ovk6^h8YBb!6JN;*$B3s7b7hU-L)`_*#X z7039L5rsGqr+-#m#utufFl6ONQ=OJwAS+Vnne!nxDEfAhV6bKwCZ|JXV`n%A)8vEp z=J;gg28bn7r#mN2f2!Q>t(^$Bap;npM}n-q&E>aMrXRqtmD-KnTg}>t?)TRC4@`2@ z0;~28v$i|VfAi$KFI&W5ZQXCx=BPX+N9((4v@%$_PsgZW6bWCx3ZCNLFX4Gc?kaeS ztHA7^y9%B%F3-TP(XP+T#@D2rpjOS@nN@?;CwCIUAt&H{%%^9ryH?5x>biR}*S)U4 zPs#}jes2!^S}7+e`2B<{Il=fx$O-kB_0Lfjv^*dgYT20KY)vYdo%I#F>k7^`cNHp= zH_0K_p3*SXfbC>BAxz&&y;TFAw?>JC4QP-P^~r6TCyrpH;%fC$G#;z?Pb$lrB@#id zFmcC5al@OH{227<=|DD0^9VWd!Mfq9?(M6ZN`k%fhbD@Hp{Al!|N8Q8#cq#m^J6+Bc$GD=(k+7*d5+>Ht>kM*%nEyI~5*n zLzRJ(Dl7O+796|T7P*@dPR^iM_AdbE(<<+iRQKVghWjWu@1x*15sqE101|yG=23c6 zB~S6D3RATP8%;*nmb`D{16sgk(8I*{zoc~g=2nWqY6T;!l_ z%Xr90s+r?Y^}}4tzHe$@U3>lZiNtiw+dVLY+m6_Mu?Bzp%uAW6Ro)Tc&ea~igV@MhNVdNUObB*5tz=J5J@(}skD2yHk=9Zq@W{JQJ&w`ob_+5xGjWpUPQ3 z6RVCR(}*igZg7PhlI!tN$1M=$zwunOlQD$GW;sUY>D#QRij%;cIyA=@V2W*O|wA7U2!DwvH7%b1tPPyNm2}oK=-K z5_3&`0|Qs!g2Cx~77bHbfd(PW4Jl-5QP@AI&lmx&$u7^b!7hi{xB!i<)=v-0hBAEM znOGLwS0tnDury9c*+;h_<(wB4ZMXK-JS!-@FV?>a5^5nLL-e52=ZQA6wR+ytjg1 zmx1roZS;8rTay0>|7plPRkH*&#yP-<*ivApY943 zZtUMw+??oZSl#a}ZS*PSME8)eyYtRI+u_&%v3{e!#j|QnRo{*=@qzTVnZ1L-;=rh{ zyi5hq1sL4DcUx6YQ2ccU54ansalB>O_>#v~@T)W9`?~(J(F%I8nezDZU80eL)tIwO zuNdE~0 zLB40dde0&Go+g~MO~G&a-^G0ifMZpacJ6yws*>7LTdGo3siabs)SgtOs#5!2y?0l4 z(@pOyn=C@Zu!Gp3A|NBEfXnzhfWrLFj03K0qKM86KkCp5=rE$<49<)@%A%s3`oHhq z`|^@Xb))0_^P}m!+@#*S_uO;OIrp6JoSWB9uc>OkmhBGso+O6S0RY`ijGO0IIo3MF z#%gITX*X!4qGC)8pxd>bw5HX`b<&6mK|V0mSrw*ddLcm~#nOdE^{lR-L!}2M7pg@p zX-l>$A)uAWLcB%8w1xu~{B2FY%V{>P85$h)m?h0m7*FIww19R?lm==?$ARYThR zbnv-pgIG34Rm80M_y(Ub?py!y(T``I`uG#(CRkCQGB^GHQ<(jw$Iy!o^rF;p8Y>NqG;W(<(@+T_KBaK};)|lw!WM|}r)8ccXTdqi2q)k{ukt&afh)Xi-ePX}( zl23frmwEdaJUHLzq9=yL%`Wkdp@GcX;+dO3C;TTPWC_PP%=}Z?XcnC`Fwmw`g2FkG z1M18zb&x=3l-FGqC{P<+KouaFdGZs>RL}`U2YS{|q+(*_JYWX)Nx^yW0Z-KXQL{JQ zz5SZ4sqN#DVEU91=iT0C{PSm2RJx2k+YenIs4zvkv+|jN^6-((8}7JlU3hBG=*-n; zZ>cLE*wJ`o%bx0ZV9UXy7jG?#l&{(y*nt7d`cD@k>xOka-^Kc))|p4FKWf`}1+`za zm&ab8Hgr<6mS$$T964Z>-Og$+D)ojQHdYR0f=8DG;Hx?25lN^Q8f`(kyH-?$VPFr> z)M_(;(x5Eg5!YLIsp*aA&>*V*HKzyD&1<(rruU4s4aaK?vv*|j{WTMrPgJ+YI)j~F zm%pL4ZfZZl&!dCyn>i)nKYicK)dxpw`?ri8I6JfSPd;CGq<78Q&f!QIvCmBvYi6Kn zRE^mZ1INLTNu(t6BRi!L67KChsX-xv6=K~+p6SJFqrr7riY>x&3kq;qGib_lQQ~^< zJc3AxwPRK zJarO zcW33bj+Waqoa^c&^nlxT8e>*PY!hB=9%xIRhk%e*b^1zqv6%3HI#>a#K9=%}q!TQ7 zZw~Yt@v^LO%WaI9TqDkF9}Jg9XAezoIn=q~+*#3hWr^77J;zlZtRCDp(eCxQre|gj zR26xRPdkk_cg#&rMkg-aH*)U2`Ac{9dzz0<_GR8v6OVV*n{8v8y245S*rwg5M4`NK zXc6L9KZkS};0fsMS~gfRQFE_7ceb|TF=%AvHY|=l$+m4^<#rUm=Qgrkr+lcRIJw%} zxX^oXzBHf$mfze;t@AKliYY8A73mo#Q5~s_O{LS(OPeHYqD{CW0C0{h($&^>snh|ni`X>B^A;3L{F!$ zv?$)&f*_r6^e*i|J;E2U(OBf3CqMeC)uTP*x$%1XGU1)Tj59XIO}olEtpY3hCHw?+ zj9Jv9Szxi2?8>mBnG z>4v_E>KbpPqW<#gw^s*u&PQezHYJ`mQ7GRrhE?_}%oGkup+r(=f?6|Gu*wQ@6!f@k z2UKpNicPWoZ(?bJ z;W|{>8N=$UuPG{T(U}0pe`|Ph3KLM`wqQM0LgJ>75s2}X=!`&xx}46gM_nS!Q)7Ie6sIL>E)aALTZM-jDUU5Ncp}WdcURyD?Ys=Q!_)xfIP4AXdJVj-b z-oF`Nf1%5L;mBH_X^5Lp=c3rXs-ds;%Ep;DZCI>rPe&@-J7N`M`*%$J#cfxITN?In z4J2Y~`)(QR39W9wYI3LoXbri?_!gwCmlaq%FrIjRgvAFQ8ZF3Z$ohS49j(k|%%&WO zObz)O^UHRo_aT`KiBS(^WM*a;rlVL&^EcC)%$MdmqHf>6K$;gs1+;hvp1e2c2i%RTix3@eY0Ao+%-=+>gwYR}bD^ z;_bUH^8zi;@8F-m#(J)FpvR77J=V@in_~(L9qrGwSB6H1%LW7eQ|g8uM^$r_N12pC zipynz{RIS9!ib{kl=$f`?_#fe>#(=C*WKuMo%X&l5jQ^F*LD2zzOLs!|7hm8Az*Fj zeG;mWRHD1Gj*WN?6tAHD4qvM*JY&sh!OAY6c<#!!7xD?TqvsCGt{y?d7u;&!tw#7ZDZ!x-{ zrRtt7b;(r;v1@WtTw0o!ouTx|7lo68LD9@`Ue;iw+W zj;eh^Td+pX&J5PWVa!gI19%iuqrokDXmh|`4+TbY?(EP^Pzr-^3YNy9Lh(!Xk7(Bw zY&yN;g8TXo)+ycm{k_Bc4)}~(?>XDnA#Vs$L;QKB5A)WCBu4B#e*(?$BWQ*yXyY@v z0Rxy#ia7d;3anMTuer92G`zH`ljbY?S}iA&cB?wo)Q^@XIa#(ns_jsjoTw`t%e=%5 zzV@+A{ag1nPwk%?+CZyw2}y}Q_tez*fvS3!H#8_3)4L{H&fYtI`QZs#m`Fw(ZH$gA z?%01-eX$@$Le=E*(dGuz4%DD%6s!tI?{BiXC8x~#Q-wtM9K9fPdsd+PdT6pWp( z(K?%&jF#n?)<8)gTdEr(+ZhsTc>zq0Nuzt1pcX66q1iw##zlX%J%T+=gk>7Mkt z`=&45HHfk=eJJ}fw&~!`-qOahHO) z$;tN6J!Eo1tr*2fdAmgy3n-r!ipr#DK@v({a~ z=CnX6+Gd*)+t`U~+tP9&D}@e4c$3S8H2c1IQ^`nrv@|xfrf2VcrZ5v$lS!utZw48uk6P8P_wYTG0*%T?J~F0i0aN;bI&!xXV$ zhg^iTpH)GEVwil2jYv(pyGU1+MooRB)2PYTbctgL(&dHygEnAzf({u(Em4#lUpjnd zegC>--S|3xk$d$ief0-6kI!}BShiUDtV7pT7Z8&vv%_ch_9lk9D?8_CIV?RW z#k<|-YhG&*G%0$ozEmfzi6pW4(<*ZoMBIJ zG!VE$H7liVv!ap~J&Oq*snqTA7IxgEvl z)`k(DBXQKz*;W&ehI~b)6HtzxuHeANfs0IMV0~(QF4(_ba=XXnY;WscTh);MPOKxH zsO(xZ6ff(jOs#2Xi|m^nz4G+wx38Mqv}O9z(>2ovdg_O|7Z1JD%CUoo-VKdJTY=b* zb@Fpd_HzS28`tr(T+B` zZK5)uSyKYsb4XJGCjfKS-lssX`$h-HjCQ81^DPePm``0p8XAv zyZEbf#|}6%pPdvJFz*ts_P63est=}H_5rh>H)p+a&dSiIE%>R44*BQ{=gIDL1SvN* zg|;`VAN2Itebh2f^${~gKsSemb1da8Jg}furVK3{Cf{HmVnd%I9}*AvGHX3zuP^hl z)8%zGodtc>`~>;|6Q=uq8^>b2JNuc~BsbI5M+?BvH zkjLi0NjLR##NWR>cNM|ka~z}8du199441J&co;5fXkxv#735B+ybgq%etQ@Ay5<;bHC)!MHmQ#{-0%R(?*o$d$o4QAJm%h|E3@r8SUJF8yW8A^-M;nv!#Tg-w{)p&m-}G0 z!wQl2=TOj3X_w32t4xKx5YpLpd4JWusWqoP=lA>Es$JR}vYoO9{l{|0eRSmjK9>XX zm^Ux2miGs*7-yNN|LSN9t9Ha{&>yxxF5BCew~J?Gd&gAn@2OKhm75C$B`oiQdHD0PW*??B7&H(;1x%NM5)suHG1hm`8v=ukZJ-S%r}0@xO}?Z2~p*>O-< zA&=vr0lfj#sSVmqLR!Vp22Cn=OK=4%bCXHPI1Ez)Ej>Ax@BPNe^h8Ru_XN5o;=lM& zGF{}3cl=fS>#?ySXT0?2CDe``1fuy1}Dp7a=kmS-&2 ziG5kiLrcw-_{a)V7D1sZM-u&ZD&FJ6yJ}Mh*^_F3DO&E76ljHz2O~LZ@2SIFxdtN- z#-e@y?^_PFOz#;PTjMk9>w_~Lb&(20XRAlHhWtT)QE2+i*@b;o9;b6^HawbaDfYT& zz)e$^>>e8LPHxy23zZv~;Y@TsN@QYPTUezeV z-(pepL!%8c4ytxdI&9pq_d=Nd{gyA-ODd8>>H^g*LyJ+e~Ue4g3(4EN6OHl#q|-A zDU!3tv?ZHWCEJgMqRt)lgCi>>JtCk53jjY1X@LSRxS`}oRj^SJLRJ`53z?i5*Ip5~ z6-$3?-R{cHmUG9~!bdhVw7+bs6PJy8V#SJDYkI z*e%-S-_iF(j-ylr1L>d)T}vU+ObiRl--CJHlkPtFVSjUN!aI6HSxvbpuk@}S_Qo1R zzRib{5KG^CxVob6Zhsj{URDP_-WP28{L5H{-v9xAjImccwlUgQBk>RxClm}-=bqR* zw_GF%E85(D*3qI{d#&v`th+BTc1EV&JUwz9%Z1Rz<7h!DwI2*rwB_ zZpk>rQ+wWhceJ`x$!6q=LxocNm|!X%1Yq^8U|tGBV!3Dqx&HNVWbX;>_5mbh&;8=Sia5&8NT5kqGmS#|t(YcdNK z7YX#%M`@4xF>67t*9J^BoLB|>$uU7A zK7VJg_wBY&w)@NsRV$yJI5F*Oj{f{UuejZN-(CVipeSKys!Cv7rOkMN=@^FJ?=krO z;t=Lr98a?j;8w>ea`3H~pk7QA-#2D8XhDd*z!#{|BX&RSgk-H6;*@!D zRZ-x)EKofKt3q(1gwo;Mcq@%V3yH!(ZNX`8t8F==npi17xIx1rTB*qnG`8iyl^F{B z2stV~({$pBf$6>5W2uwX3e5C9%JI(QpKoKmG!TPTxhz_uM`QD$hBDd>HqM-X?b8*_ zDrt_CqoElu=?YoIg@EqnwwewpI-@8Q>@jlZY9a~`qWqtpD5Uq_bs%4cW z-RK(;+gfRgaDND)AB9ew2Ftz#ZY{%38(GvQM1raZXhK_*XSGFv&eG~bQqe}45PzlW zk~(p3MHwfT;UZSr0lZj5s}5w6s*P?viBHPgss6+D-bdrtZJ4d;UETlWlOyBBp00tn zcSG_0{q$ILVW_3AzpJ6Ht@Sx-J?LfQD0*K4ykdjn29*k?3`d$O1Yq;MAv=x&`{yLG zL@zhJslrIDO2>GhJ`>PTV>2)|NN7+gJ$o~8KjXoF5M?rV7Qp>SFdJy}gsAC6X-uW26 z0<4K$=h6ZZ197TB#18no3E@zPCCeK`!fKF)+65^`4|TsPg92ssp%_4h@%#$a|Z@z5AYBi1=oZj3r@$gZ?d zFvA5~iQFmYo`;z(0Ucdxfr-x0F_Ta#^;xN80VOSvhmeKdjv-GL`+n|{m!}?@l<(z z*k4=I(e_QtcPl;n^xYcZ0SB-!N~j#*j^D~JWzM*bh0#-MyY%zMjUlMAMoffOl?0r4 zSNjqt2HgO!J~$guuxykg8p+9#HXjn#c{2|tAL*Xzs0xp58_c{WDyKI1T*xzcYy$DI zpVfCyv<>Z^iVjQx7k zAs-j~@%g255F;R+f)3EB3PV!vN&<^L*;S)$ANv`$4zcz;+)pxjw*q@iW=3;MT&06V zj437itgSIDPj2DbK({ipTr{q!>*?;UZ4+;~Zg0n6==iJ6gGTXXnb#U?I@;T-#X#NM zIUhRzq2pIRdB+`38e4BX`;N00>g;-Wa?aZ@=VIv>P%HOfENRCJv?YffBRSTa7OD0K zwBczt6l|h&*-+cs&<2X#kfecSs&w6T5(+-&$&uOuZ+yTM-W)`^e~$o+fvfQvj|iNd2qO)fgm z$fx~H)usNTg%NLeps{GvmD8isSAMRtHPq!Bx!d317p0ZOTgJQze7EPy+0of6pQwyA zbq^d5)EZ7_py8(jfyT_MVlwl%C=b>Qyi{LfI-NC*zZs})6oJeGsJ-^XAhy|3pBN$R zX_Ly|rYe0{GFZb-Y|`5jR-v~zeJ#?)g&4&;L(~$3CoV;K(A&|%-H-OSsX{l(TW;rI z=Q-Rg$NI~gE^g@!?VVgWRO+6aKhwK!<1S?EEKE=B4z;&lw7Bbx9c#tFSljHLl9BO9 zDlt19svZ~{m>n&t_4beScDJNPwyxWFhTrL1l^#5GJgnCyYd)28q=G2g9Ih*nF9n6L z3bajbc?KFm#4DFb9}PkzNXvtl1``Nh|IDXS5;^)U# ziRW&*BvWZglDkcuz+t=-Ys78dcs1V8O+?C!o6zef$E_#y0NV3Yu>AiR58xCgO;ua5 zGW?X&k1Juh(@zR2>`j62mq={I$lz`|{UjqlI6C#tZ`OnZ{oc_pmDg2>vdW?hM!bod z2HzDo%(7P6Ls1OKHL2s8X6h0>-L-M?)|*GK%e)+_?da^Nt?LiR1}dk{ z{?Pef0lMS*+s=LeIdir3)q1v0fbak+2YMk7MWJ~QC<+)L>!=*^eN_d#r-x{ZuRO#& zM&!aWqdEhN;q{RC4txDUDhqb>_?22SWpNt1N2t3DUshE1@GxkcMNXiHoALQvxjJ@I z?&Yrz{d}9x3PSGoVxXs$9D@`&SqFyhHkBs|`vMFTNMWZdDC73*pLplkV$`|);qlB3 zRnhRq)SHav%$Gm>0Wov@sxU&zx%iDpJu zK;32$6TPTYY>Ktfp&b*Eo?X{%YpWcs%>B@Gplx{fTvxHrDT2vWJ#S6#u|KpNn23y@ z(zoWV2hzKDXRqERo(~VLZckMOOJkEg_11OUR8M`T6&BVHF_XF2i5^%EX(6Es%S*Tg!qK5t&Zdi4Q<&l=VxQ5#^;ax-$ZdD9*ZW^t#<0gJ8xh$&62e zyUs$N4x$f1$63UED%-{CgZ2&BeK=7$F)RpLK0ZfKazGU)`v4kvR38yf6ze$T7hf)S zH(3xsI$5IP88dQ9&sm3yiW^3oJw@vm`?o|_kKQ~g!X@Q8NtFOPg>=c2Uie<~WaW&h*^%H3qXzju9lSDs;S${8O(zi~b*_7QKT=C$@E?0GH7 z!gu?e$^tGZD57k;3Ls|~Maj*Jp}47&Bf}O@pKEVmWZ1Y-(4S~rVCIKLM;_J#!D%0~i`gwqS6Qh4j9wO^0fb-X>|ml_8jb@7gY?Gr8CDDJ=W+`7T4P3w9Vr+Z6^GS7&s1FY~rIa#-UW6SKJsZ;O0 zZu6#{wX0@ZX7-K%FpFxhubAgyEDi7~13le?Xuqmo*#kZ*@Os#zD!{T8*rA&Bq;T{g zMyu*?69o!ErANy>v2t{%%6OKe0|o?mi<#pVo`*x0&;VPX)f%BQuQZJh&4Q(-KUg-r zw ze&TagZB0F57pg|NTz%tzKRWPCAYiz*&9ZJ34E({&Z-dpw^^@s9Ye{!nG}Ag%I4|Jr zpsi@@Z{oH%+fEdKYTwSVBia=XjK6oO-&06r+xdIdzWsQ%-E5SR(ii3L=-qrs;^<)Y z;dz^1#v91P_GgWou_|UxXqDPglsu~xQ)6g@iO7A*C;&(pkbbo>6_a42n;%u#qD^?p z-=oK+jz8ln&ae7W5E{2L&S*jc3@fJzK48`eGicYbjF^GqaLO61GA|W^n9e*Jn^^+| zS~U%=Kqm%|hwwztT35WThiItQdy4K&rhnP}Z+)NY{-vY^qRm8G#T}P1AC z1ErxTgx+q3~xwJFL;PINj8kU-@jq%?24w6mUyr| zS#vgVU%%;f2ekRHO8$GUl_M>2FB$vH#y!;dgykHTebt2bB!GL(rZf>ON7{@cGpxfM{y-#*SK>)3QpKOy1+JV-B4QHWMf@rH;7bWyt9YezlcxmKLiQ`WBL9GKi zw-qXkS??-VNoym9It&jwI*+k%vdXzp5$Z9_gwhm~Iq|g|3|JZr2}wy5-y49(jMGb& zVZpp_3?MrgPvs!O-T*xID0x;P#yEFpVsao&e-FSF zvPzix3Q49sTck@ak_`2q$RBxkst(0jX#B0dYHXR8HQ^etOD4-XkjqbFa4ASC^eldMp+IaDJ zBWbMgX_N~JpJt!N$C+W ziF&|Ah#L!pA1q&=`gY_N=CS+8`sj-x* z*q0rAhX}}VkmjNy8H1N>66p1gzO$^>!bP?z?Meq$OOO$gI9NJ z*rg8GTiDAiFo89tE;bBMp`7@w<^Fu_#UmGP=f}P2_N&&<*Y>XI|5D;7iL`ieWE@#vor7=bG8`|y_Fw%%)J2i2BJ&lu$N-0X}45d5?Dd3gT6ts;ZmThEZf@pI->zkED38GJ~ z+0ZhzcWj2X{J#0!D0E=aLwo{_qDXw z6=Iim2e-~{bHnR+%eHe~ReR3ay@m@VsIB(Jyx()bqkg~r72F5ZS->vw*Wh6vV~E8N z2f4mgQ%gYe81K?K5Dl{6gt2mDQGg_ zI0^D+a!BhHJBK=(oM+vFPF_)bMRU6O3d3>yOJ6#kaddz9!|3VK&%jr#0O)rdD(vZr z2xXJafhql3ea57G|KuXtB5Qf*1tBMsB!3@ek9BUK9KL?gxcUy$P`QS*mvMbcfcM(F^o z(eDB9ornT`6_v$O0aQ{$4`7Cw$icYIMXu!Ls;;Vu83iQnjD@;F@B!31n7&=}oIggMq!!u*8kG!Z<=q zo(HHn^!Ro;yRa@65BYoA^mXu|_%-SD6E-l-$ zy{12wxcpr37Uz7mJi)R}@H=!LgBL?O)nJoq1a0F|d z8QRtgFNi_s3PaYPQFR!U-AF3b;TEI<0I^uID&lL`oY8e+nKw*~XRb*p+6GRYXzAL1 z%_%$IREq<-*Cw`1_xRkR$D4T?w%?u|>v5KM?`0*7+VKSLjq7m)H_z-24S4HtJ~U75 zK6N7`m_#iyo8Nc`#^*7wb6_)`Z6|U|wcqIY8OOi$3q)^cIgh8gDa-TbNw}kUy;fryZ(afMxT9l8!5UWpJU&UT zBW3Oc1=<{c%NT6F?^g>&n*s$;DcjYn?I7D3yv+AY84!gGuw1?zW1-8JEQ)eC8eF4X zzWimO%h!}P&MuG+;Xwav5Z}$uNG#}G zcL{&6YT$R(6+nYlT4>Na?<5{nqKICXT!QF;xr|E+utNoG^9T5SIqYSA-zhXKvJr6t zRZdH}d->A}w5!0hb+&^AKROXTusZ<=>6PglP6CMq7D;1bB^7cpu?8`*CXrM)FTrsQ z&uFl!ymf(3?P`+R1hYA&VDPyqF72(UtZ2T5ewcf}`)mFKqm57HdT} zufoyhxCu>QYI}QnfT=ylMkrnTYa3c?d~b52&+Q~yi749-p8xk2TFJ)R=44xd4~S$` z2yUFX;L(EAk`@KP#R3!KBtC?EkFV61+@ixXgxO~aH5D9HfEo)B7l8pL0W=o=adiJI zk2?oSMqDy2F_7|%b3&abiG!5cF6O5&iKfC;#%xg25yRer3W@;UQFq4m=C|u@IF_-!0z3bn6jhN4Teqem0zN4&X zIP)#aJy@CMXF!Qkp1^zpZ?Mh91>?8309x+pRbSu@mUHRDuu-WEbE!_QvGxRdh%}?+ zTyjJQrgi4cZ00VFQ=dc%4rh{@Pa?ZzO()dHqxEnJHCb*QMy^nJh*z$|*q)u^0`{~m zU<;-c!A+FS8__neGS@5&*9sLEwe4$0ROp%egcU0;x-%P1oQk_vd|JrEao-*+EV+2o z6;0)VE*yy~wz=f{P4W!kEVkYF6-G`w{~QCza%_qgY3Jsj>7+HZJ(JpyQby}B%WF(t zq>V%<7Ozl9L0Lx1R~(QBg3`Lrwe0D7e0j^6T|ndgRW~gA)jf+-y}nA9xqJH>*z`RY zY%XpbDk{JB2+jHrXsdoteTRSdISaFOTQ?c+!hA~HyGe`Xy>dQ}(i+UR)7n?<(n`#> zw=~jeP*AwMBi#(nluna-4kun-`GJ-ULu&Ikkz; zAeE|bVuEzE@A*#0EgkT5*%Af?^92rD<+AXE%e|Cm7{ zem-%%^Z0^jzhmO<&fm^w{^cE~b_|OC%s+`U#P-b7q9=2A=AWM=bCjvHB#Er zcvRaas)&#rTfVb4%f5tNVxYg-KWeUkMJIfIMKn5;2zW@2T8<8QN#`4lt{{KTJ~WNA zvC@HSz(Z;SBk-;UZuz}*c!ao>UW+z%6bm;#BSH3|^0UBOqH$c(|$J08w;pV49R&v9t zJJ_$~yC#1Aq|^}|iYw_5fT!U1@Y+2uWkILx|6zEQXbrSEF5*mNZT~jQtE9EO&8hbC z$F>~Yro2i)EKjx94^rPxLyRh@A(t5eV9J?N1FvQ+zT-{@1$*H*%EAqmS5v|yEsv&l zCw**txUa0zU9@KXt~IMh_2IEe=}$W7E~*Ng4u4YR`gQx^Pnw+yW=mIVd4%;h-t#I^ie5#WJ z1&=NpSX375Q$94Ypw7d^keCxvTofD)m9Aw<$g0uoSiq-myL~#wl#t9vxB|hU2GR3D zp6pzMMbBJZOTH9XZ}y%{8!NfS8f#sud=`Y`ISMz8HPdlaUNA6BA!FbZ*QB&qFmuxO zmeCLm)}}x3p;1Vatvzo#R8+*w7S|e;*#blCh2@zonGb?*vzlDoi%uAhZDLMbW;_Pn z6miTwj3yQR#ZEbEwG=2MJ=hu**De9WF@|05D53qxh(mr1TRYN~BL6=l5qM&`eQP>301F`cH_pVm;{ZBXW6p)!`FqC)r0YdIrxl z7C8l*6N0P?Uf`%FE&&`9MHh`ZFAiKYKl`R4v9?Wg?r7YX`Ai$0*oP+`0Bsr_r!bvG zWrJ8VUa(HJXpv8Bz!Rw@Dy@<^pbw7ZK^vB$gCRi((+HPhmMyLFbF_F;Vjy$8=sg`N zw;7AH^_0f^GsFGUHLX2$mmU*qyU%J$giDJXBJrk**g&ecp)1zn--`YYV=fP(zrB<# zsIcHj=5VMMBeq=8?nv}kxdtIjK_$8Yqq3*n5U;92Xm>(JR=Tl`=*CXG+hJZrF~s2z zGQoq*KvE0(h-y&QSac~pH(i95y9)*Ma6ozb>Atv2h)86zwV`IJXJB|B7K&_~9U3o* z_`2ggT^^$?mhc!jw$D{$2AUG}!AL3=?{^!nSa@(Q77PR$S|i~9h7cUkh9ilY+6Uh7 zVw#YzMSu`X{?QJy=BvQYvbk6RB~|4BX5f@cO3~N$rD}Fr)CRF-s&zUg-t@^%v$C(LXRxw$xNTsx zw8hsRPj-6DaI~r2-#+}Tar^t~s#A%E{#dP~Z)c>mw4tS53?{oqq`mRP$aw4U zT=UGD(*tJ9RA0I#KGfVgP-MoV(X_`9?P6|tplj>uA%Fbfh)4}~hx%Kqqpk5uPpG9W z?)AEwS}4s=1IJv4@f5L=9CCBzrpp?Mwq)(uv3W&d4j_K8R-7;w8d&TQ%Er9nnNOFu z*9lJ%R;bvp@uGI@9U%cox2ugWK{|GzdJ*yrJi5}75=O0HE()d2EENkZRr}d=#wZz5 zs>9DgP%$OjJdiA^0$i280`r4=TJWu=tSw3&f%3QXQS?B4<%sePTKhYf1TV z**rM^t#Nm;tFCd}<#pCIeL5a;7dac6W6iE&vmp%5ItY551*yd(AZS(>7)+Ie;$|U| zrAvXl)wZ2nF*JBWk`5(Mn(Dwz;N(510i&St_v77!`K}g|7w@V>fxxi^o+!j{oI(m_ zEtIUsmSma);w*C9G&s)LGL=kko*fy#wRI4u47Nw3e2W{~y0@%OS7?r#SU-GAw4>Mu zR%<24g}`yxPA&aT42sL7rbXTS`mCm{$2g0!0OuIv4AMHU5QHT&T3Ia~bMMYg@aIdM_yZSg36obG5Xs zGe_Iz=-MU3)kF)ZBXzBBWnF7OFV~F!Lv(HGB)ays%<*DN)%KR;s@fAMT2SYgORtN1 z^j2!x;$@mvhb-*g=4O2|0P$baw6$4H%aYSs3ou93N{^kbY2{C#Y5zP)O&gAclT{71 zP_^k)Y;prMZE2*Ws~wtFh*+%G^G&U3>jGI->zgjrv~r%p;EMuHTbAWut!eG?+Z_Cd zYuZ%4roD%ncDNs!)?W)vo2zFH%tbGHB=xLonVyX>WEXMReaUqqMlcsNW0a(McdLU6|Fm8 z&rYl_)Up_on8#>Nre*E9vH4`VmZkiVT6XN|PgTSNqNw<(932|~&8D#$>*0l!I<`Bj zW4l4M(qj)g_--S0H507#WcnkiW4mDmsx4YKsHZGq4_avvvmk-GQk*k@zp!9f>Kt3G zV^v`rMb|u?nr1y!ZJV(y=52X8R)nIP7jiUf=bFtWLbOFYdR%6kyI(8WV6UxY2j-)( zkQ)lMxvg2geQMlIZQI;bQdEa2H(<6qU>aQ}`3hRr$~aMVE^Gvw*0MHbw3d~=+y8f3 zmNXFUdAtTCJMo^@A!yf*Xmp^+*)Du1)Ul`w--5mE1Prb&*mNSNQvY_?7(oVSI1Mzc0~$ zzf=BwK>Ty|`6!oA(7!rEA-?$h{uQ2IXE@dU@qOb8_pcK_m)|F|-FEyd%Di)Yl|V|?_mE~jtMXw2S!YK89`ghZ^Nmh$Un+Y0?} z5I>ZE&*#&_5;OCkZ+*WF-}~hEjCOH;C*}AW1hy!8{#n6cdH;|Xjo%5!wdjAj@fGOb zG$f*p))XFwvsZvek_T7t9#;*%3E_g%ls-1$;UJXe=_?f(14cTtrD zIm*?h-?$fclb^f3ELz;n^UI1wyjY}*MW9$1Kg~R!dA?1w%6~*_TjqJuTAlw9ZJFnB z6J-1tKR1%t+Xjf3&?hcF0;!5TY=^T8dMQvY0_jLiB<^Vzp7`%t&P$2Y4d3xsPZz24 zTAntNE$8(hf!JyG;1li$y=Ux4{HDwiqGv#3Id2B2_3GBuEQGYtlbE}R0 zxi=Co%jX<_)$pn3@XkQm?8h9ecxT%ko9vrHx%hP4pYnSGQzGDbDCfrC{KsYe$9+3M zgI`!Q0B}d0I{65!eHXS=q(NJJ3J0i11|tEUnjSD3mv!!4xlh0yZ*ZJ}6aCIpRL!{~ zGD*zRG|o#p&H2nNz#y*`^B)wQ%kP;p zQ^+BLK9To~4yNO{BgXED4n~Yyl1^hj^KRT^oiYCb(II9(IFGm^8ZMkSok@Am{pjZz zs-LCy$boO1H}IBhxJBM_`8_RfiQ^G03KzbW z)ErC&(FDn6Lz)yae?#Xe98q$ffs-Wt7nO%{*(-O7{0$^mmKu zZ_a(~{#K>svO|BtGhBG>^0r1vrcDu8OfH)O(xJm~F8ZtQn?r}3{>uBJw-LRd#X5Q8 zOq)@n=&5gPQylmG40z^47SCkwtL0EG&v2O+E5gWS4vz8bg)Wq|u<0uz%bv^;d-9duCp(yO41Ct@u6XBFZIW&LSu2k=q3#Lc*&$jh!ICJEn?!dC=3;X$~-GM&H z6{(-E9$ORD9kHG-u9h7LO&*v|@12YFbuV5pht9+l<#h)Rj-0unr)_rs$n*tUdQ-bD zn?Gbzpxv>6wR?5}1&|Ds&jF}2H^{*cTBVEfw7ys!8i?1{#s@+jYX(E1!8IM3htxyF zk7W=2k-iB2@9(SYo$q2P=PW-wy&C>ntaHer=MgH@VjEUFw>+kSF^=Ex-X^2rmRqm$Uns~k|m zoSF4%>F_N0N*WI*U1l;kvUcyp)R9yAlG`qt-T(gXSjW|;jviVaZ(n_2D7AhH zAaY}l=0M(frpVR#<4JHliJb9-*&q|DthAp5#?q_CBJGAUjDGmW%nc*=ZO^sVWIA;umhNsr2h7PQ5 zkFP#7dg|33vF`WppS@^XvhS25QxkjFjs%k!ILC6AaSl$*`Z@$fPCa|h4MUiK)Tj{V zF?Hc_9Kpp=yl%ydnKp5>gX`#>Ix{>H0Dtp4MZxdLuLMuzox3EDffeMPDM$Xx4f*erXhR5F?`%O54CH) z3IQv}penxqabqa5$53RA0lsBDhDtdGxRRuQ8Dpr<9fJ;x$sv#v#g)~Km6i2nO_92) zo`#%jdFEjQLjiH633j0|UUpeNp(lnOBX^CGiYVZ(JnK#vZBM5zoge@fjE3uk6zf*Y~L=T_9+08yw6y-_1GSaLnx2j;EOL{rk zL!`bbS`ZT`i9$}77x&8x9chk3ZN$@o{|sN&6AWM0YpkpMSn$Jmf!5{NO+0`zA(3mI zH=lAv72aRnR!$)w1j^&SqCa|h(pXpZF%)SLj_=@(n~jXRBe7z-`ita{ayj$(`-F8t z#?W2o1t^*hc)rs&A2I&D?qdLuIzD1-6<;-80n4MT2u>rE$?I4zID5jx3U{i%F1Ie8 zvM%noE^d_fA$zJz`}$BkdPR; zsM;GjaBJumW0&p%`tYE!6B6U!A)HXfltmoqQnmU}@kjK*EBoMPAC@p8N%nGHa12Le z3$VHD474lyBgGsHn=(@jhPxUo+|iQqXj7=Wsk*haF_tuT*0%Nr${V9C&GClvKu;@9 z6%~%h(A(#XAHd4(V{h#}SRThJMk2_WWMS4VYgl6))5xtw4I^n!2{GE$Usj%|t!eXz z%KE#Sn#PTtt+B4^4V6XS4R!HYD~xx?bH;-B8?z1zxxulP&urBAoRaKH$u5xWOsNiF z+{Z{ja6*a0iJrKhCkoa{&27cv07B07N=rXss;6frjaPqDtIOHYApbQM23v<``D$%$ zs`dtg>R+znZyQtMi{{_piqLChI-?YZmO11|0fEAIiFc8F{gTDJSuV~pc*6Q>(YiRM zE?m%cPVgH}V!3dp6!hmqoUm4$K=(MI9-WSZc)0;b#&;O27gTrP#k{vVHD*k@d9rcu zY}^}v_X=Y-+iT8m%6xKV0tiuOY!}yK72)`R91ezRa!`GhpbuFo&deWdl-XSK1tqQvD7O$F-OtIdGv#d3rc^B4(np{zL7%KK}XgZm5KU!ol} z;tRRGm|8>wMQ=PlXCyugj(vD3C9a~c-Elf0@)}hI1EHLMUGw~m&+$b>YoEgF`*{5V zuO+-(`(}`;i;HHw5_p+Nax24FJUkcsFCi=L!|RK9J%yJOEQX$ev0T~s7z`+|J4yvu zz$#JGlJJI#LN&qWKyxS@YckyB#li6K*K5i`zViBNf0b$OT{MIdY-*!l=3?}mXG_*P z&ZK3NU$I<=GVCe~HdcM63Vnet!z+we60d2zHsf^=uOoOJ#j6;yiB^$cvP%^dtG^sj);bj~clqnuLUkq8mBHq+8qCuGbi=2m zwyB{nBdZ%&Z_A(_$Bo6#)(t6Ro%a)1s$4ni#82g4RDjNiSzFm!td*@+4&PPvA$b=r zE=UE4Na_g16;W8`$DwfP)R%isVlhMH=ynN-`_ejwMa4{zwbuh}dLH=E9?hs!EV zcMJY;R>^+7wb4};ZFH6TtIA7pg@14foaNQVIgr&~$DVp!<~!&Ev6r_R*E{Zkz2q@B zWZPabE^^Gn{_vO^vuz(h+ec*Erfl0sjdhNHhZW#47qe|YM%#Nq50AMy+x9uMbwT5L z%q`irD~zpKL$`R$ZP~WZ7z>W;Ax}N#sj3ap z=3~aZ<4Pd!eTX_?wNjRU5yD%lkVWevAuotwwQM_87#T=_cskm7Nn=$o*xkJKEyk~E z%PQlcl>Z`7=x4@u#~#_MotbAf)&A8;I5uECd(2(g9^5aUb8I$_;1OZ!J|>=OjEpP? z!JIOpH9TOWDrO`zS$^CVXx!%6QrmC%0uAvac-n1CDaUrKbq`KN_!mA6V|#q*3`f<{ z1K9ujGAlhBnR;C?_H{MR4;u2(ydWyWhK#y zpNH8S#;Q!>Wx%lUFrNj?C9xi{!Eu%|EX$W}vlS)oihSXQK5s&w=`Qk^dvu=-@YP1g zC(I*wX25Zc1F{N|6jEpTN_JASFLIQ$yDv}|lv=DUg1^DbWNFi*xVRNBS#7KW7t$nn zi5HMtmfb+(fEgxU2dmXVyvm(L)pBEjD!eq@)l}h%ddu2Ep=7YCwWKN5R~fxLVQj38 zcGs3SL|Yo;Nb~4!sXedyqp%yT5nb^AV?+UaL;>!V3Pxl?T&y&r3;O3<3P&V-fsY~E zNzc(~7!m5YyulplsmV^(pEyU2_J|s(#G0j9BX?WNXDfG!t~AE&mo!$(WqZvEBdx71 z^T$I2l?ZYgOSgb0ziM*4J&wOV>39o9m*veY0sa`C1S4Yg2eG?vf)Xcm0t=#v<7Osl zM4Fg9i#Ludo--Jc2Ze)wJ6D|T^a;27E74~rchjHm0Vv@0&nQnogXI#yg~MFB-8jv0 z57x2?%0(TskRh-Ei5qh|?!@bUys$`fHanZq!hHh`CxI8$TQ$@hHDMz+rBc1+Z;6+0 zOd~CqC4Qc>ABdg}l$TDu|i9sMTykcWpe8?hPv_oM%5xSE&krI!!kP|N8zhTi$^6Aj|)*9R`U ztnMA>#ZgR7rnY)n2*xy;`aoXs7ja=&_B{ec6)bDOBRN*g2L#K1@QO-z(>u!AqFPN*|4 z(^Gjyp{6{lCl$8vU1pv5u6Y%xgPH`^xwe>F>9wQ{3<9_y^&HrS!!n`Fm}V<^lmF4e zl_hED3S)&1apC`9WWcP;1_ZJf=$zx@W`lUvyb>1ubsSZGWI&I~lp~V!JB>lCG$M~2 zkyT&S#K-{clJf>O`hOQMFdM8mf%VQYJ!lSyubHP~IM};qJu{S8SoX)z`~qGt;U#$& zi%qfjoxI>2CUG&17xP~6tqvDFF1*E=SIyIt@F@h%W^s;rAr`_wVYGnLFNvkdhw*b) zMs@g?cyU*@+{h5)ISbZo&Rdr7HGaVii?5g$;c3mRRJwBPU>WOWr|y{SjqC*Hk*XLx z3kmnfL=;}g>0hqVX6gV(ZjaS9c-`@k@f-0w^An(?#}Ok0dgX`$H-zkzOiJV^ z9ve_00f>4iuOIX+w3v6)-CIE(x(mGUlzBIn>H5M504-!W!?Y44%N)bS3wX(8e^9m- z8V2Z`_WA!Aeg2_a&AZ=Kdzte2KVrJYf0`f1^CnN=^CxF=GGD+;KITqbaHpJW*Q)Vj zFrs!(_~0#}TTJ)Ofh(@4dl$#_c|7Ae^EL=jxi0d3`s!}WpK(w>!`h8Qbe-I1KeB?) z-gIl9eKefov;Q)B^SpTrfXGO1Ak(<1K)J#JMaz1ca4|Deda%4 zF*P`L^ZCpD_qjqFqgL#N3$>mk9Y9kG_i6m~X1rK)CZF7&&;C0>T-RVAiT(Ks z<1)Wp^QOkk=T;tEo|iVX%uAak{)+BevGi4V2rn`3#7rikjir}Ri8=h?0V(e=N6on4 z&V=RMEkh4BLym!$xTL94c?!)tYs;$I;9xico9K4q_l{3Go3UmBj*FDG_kglWorm#m zau=kzqcqD42l34jypG~^D_(9g3R&j`yz*ABwX*&QKbN0t%+=y6Yi$lCl%FeEMTz%& zY!?FM$PjI4r=KfY`{t^T2Jt*SiqQYmC|F~R#av)(pV+sx(kL$MUx*ft!t!mYxj@}x zOy`Z53z_MLL>6#4J@h~$sJW0wVsb7@z~CjYK1(ncC76p6%motV)LiI;WN4?|iB?L| zT4{+^!lzS&T|=$CTj0h+X-7Q$w{4&rqLucLU~ikBX5 z5WE+(c#nU=li+E2hhcd$B==uKs^ITIh<0w5a*b}REZZFSEGNrFC$z<{S;2>E-dAXt ze`dK$7oL?5n109qc4A8cK94%KfnxA!aoOeQ=Byl~bcFckoCRuFBiw)!Ss35^d)d1%@2fWtTD(I7v`R_oWJ`+ z{a*Cj{Q*uS^W=^%^Qt#;eChf5h_gx04?TE)()|2ad|@NWg~*{37_&}S-M!^I3QbvU z&>BnE7_W(5$zNW?KcueVs48Jb}L?3b<_*e^+jDw|AY2;9h0pmc61^3TaLf5 zf+bV9Uc}3VIko&P(%Y(x$-jxkdPnO7%w}XbT)#Qa2Bni zHv4Ob<#V;Z?yp`8Tpbl$3mgR(5Dj$Wm~u+cML`TpzM~6KYYKrAIJ*`OH=c8I;GJ

x*!Wr#*OyUZ)j3m!ZeJg=!H+ErWH6baSGno1C*L5&4tY20|taj)c=|8AP+yRszB zP0*Z^#{D^i0%jh~b9`K1!B*iR(!A*gr(62CY?{A@y2R8`m5x6L&1F@(W1yv6uykli zw?q%E3mKU4xx04N=94xkk=O zy;=J&X{@Yo=xW~b7I?GdV4#pHUz^`DzKlTBX2%08cDBW_6&8-F0^fEF^pNrGR)TR`bI`fH zZ0OBtSjUESx*RY=;<}!*%#~b+kk^c(=oM~-_%HH0ASZg!nwFny<-bk%mqu2TgFro2 zhf!Ckg(pL1{I(svn2{Z9M<(iK`~^0*_DP6Q*c92*Rp{$0Gla(<@kDFn8LzC8ZL-k< z4lB1H%(JMGs%}XHTZa8pO*;m<)(?ln!|S^Sb~H`-hg*V)mg>yyYeZt=uW#^79K34P zt~>gtDwAV#twU#QN+dR&G1NLYmaLrWzhl>`s}4?hZV>w#lhNvMu%{!z+wgXY4j>+@ zqsh#p1IB|JelmB-j=s$cz4hIZ%8vDKnwomk`i{y-SFm?sbKj1O=ON=9(@U>7N01HD zf;immEdM>{n3n^ugJso~ota_MeK=<#bp0s1T!)@2IT3O)a|@uVL$5rT4W7seGy-Vs z0^L5v#Fn%|QE6^Q;^rUK{AsD;q8v0jl;@8Ana#jl4Nq%Q_&(_)A z;%|JTxOaAIPgSM6#<%Xe!y_YyuUqG{cYGGb_V8zBpClxZ5IRy5I#NudBao1U4xv|p5JHhaAOS^eV3(_iU9mS*RODI^ zv2pEsxpu{ZfDKW=0*HXX^M23SeUhMD^xpUPd;fWNKT~#gcIM2PnKLuzoS8M&7*h=^ zWD@&i=j7h}$;~Ckc+VPRSNF*qGJHr}-8YOG*~*wLAN3hNGV7*8m$WvfVI^bYwhtM8 zO4=2xW-TD@4pNz1Fgt&4+5=bhG^XYg##9xF?;^P8vAbl+?bX>8I!QEsJI}1&XoAJ#>CGzCZ@;i{DpI)o_6v*VJ2x#{_Mgh zHhq9Urn528UFQ~;&L4L1-%gUGjcN4U+>*k%&m=4>Fea*qZ{9T4c&3txGl?cD95z*X zHs0-Kgo!cJOq4uv#Zek=@Mk3?WRLFV`))^f9NiH`A{wX6;%f=<%!THnewR+F+Wi+3 z>k016r>oLW6L<5sFJ8g7N25##7)-1rtydj0h@6L;Q_T=F-sGAhGr**oR^|%R!%Q`^ z2{WcjxU;EZ`GCGaLqY`2aoNJ`3?nZ3qN=E1ck;N)a{UfZPHHD2@FVPl$(OPEN{MvmtNHnhn|YlgExFR&*Jp zKz#1Zx&h2Fz#M%t?<@JK3?H+mSN%0+?9!!@=I}91lh-U>y0rdM$`L%TGaG&vV9oCX zdIbSsf;7NH&N{m|kHm1ACfApMrpZl{fpcs&dB?ULFnml7kS339=eI7YTe+Ocp$$qK zt!BDnKVhCEY^@0!Yb#m8Z8cj1yOynkUC%bcPO@FFyV+dq{`4tphgd4cuD35>|HFQQ z{h8g4y~BQwz1vkY*41%!u^YGs*o_?NxFkn|a4lR5?6$5gc8W{K?%+CMcX7buy1P@c zd%B+3nJyE%x68)Pb>!~)LJ_RXb9vY!-3083ZW8tsHwAmDn~pux&BUJV=3z4D3a25%&4+eC#Fe0_=<2rP!Cd%d!9FuEf5|U4wm{yAJyXcLVkcw*vbPcMtXh zZWZ=(?m6s#xR<+yh_*; zy;;WdW_#yiU+Jxo2=tLQW<7P2U|bu!p2~u9v;o&uO}aT5I#`D{5Y9F)Kygl`cegYR zs7(nmREX(ox|#N-si_r2awf}kq)Ve zvGdoPxq*9U;PwjKL~$3aH~Ne_8RXj1^`=-{zDo?;s^ap#XW;64p6>{r^T3?3UQ)L?NDA29&MV1+6Mj@ zv(mdH@K-Xmy}5xu)=cx_0)J(b<~|PmRZKJYMBtC7XI~fi6HJDk9QYGWJ^H#2b2Uba zu7SUXnQvb8{R~gd%-wc;@#^f^|%{PUngeH)$z9d45&6yfj zKv*fhBuuGUggu{oD(}TD#FxZ(3mC2j`A#Q| zKZ}!32NQ^s$T=SfigBmlD=@R@E~hJeb4g{6nNFN(3ZJB#M2vhwCFeqO2BrwV|iekrG$vw&1^R7%N9>GOm1%}hq&y z@k?#YSNzM@{YW>7^yg~aiYX5~CWW@d8l_(g9whf9lL~}lk}5!UY`zg_^1)(0-$~t1 zQCuVe@l1syi7?4G(+tIxw$>H*zroM3-<(MfX9C?!a-5A@0MxU8mu@7^6y6nqo!MHl zsX%d7*%yOIVHPD%0>UZW(q3j6!ABnP7Vuq?mQFAd!S6ucNDJ~~&E#vpFO|m}Moy*V zC;d!nDJy^=iS{Dc^ zrt8-}JbufN5+x}tb0~L`_6%u_rKBsZXg;>UBrWeuO-*nm^(Wd$|oh8uI(U+yi;hw zoher&l?b+^RcDfe)Nc6~f&syy^r;f13{xqATq(i*CbO@H?Gabxe)6(Hv7zNrW206_ z?TOBezB2mhn9P{#V|G*;Tj}-K%CSpg-;Qe*H$Lu}%CVJ4R$f;5t16?b+*jqt_|*8K z_%9RsBrHjIv1-+-ovU75b#r2u#AS)$YByAmuim5jMb)3J(YVH(8h@{`vu5v_|Ekra z*5X=QYM)m7;W};W+*aq4x-;s2a?)M(X4Si(-qLz^*IQrjy?VdZuUx-Q{r>ew)t^%T zy!uP)KVN@e1Jj^NgKiD_G{|poc7sbBT-)HShE*FDHTz23Zb^YrFDn-?`-+5FcQOIxgIXXlaCw{F;aXzS9}=eEAT_4bqP$yq0#)yA}$(dN6hSGF_l zE^Z&){+jk%P8pVRcFN||_372qAIlh;akRs~I%afyqf=_9`JI!xjO-fUt#h|CyB+90 zp!*Z2UeY6`M~xnhd$j4%sYmY~LwbzwF}+7=kMny@>N%_DnLRJ;c~#FldOpe*{RuhG2^<2>bK-Jp=QPP_ zpBt51Ew^EAyWB3hIk`h~-^l$i_lw-!xd;11^{LjUVV~B0?&|YlpM!m)`c~`Puy5>l^<=FJGJ;x3hJ9_Mqak1lSjY}Ta zZd{jfIpcjo3a%-*yO8rq-U? zWNQ1VU8m+w9X561)RzlwVXwkvg$JfxH0{XrYo~udWBiQIXP!FqmeZS`KJ4@dX4zTA zvmP&MSJa{C{G#o%d(OUX_NF;?=B$`=w0LUquf^fHF>|ZVtuwdr+?I1snfuh-7v{b; z&zo0yUX6MA^JdPQH}9-@kI#E%-b*D{m)u-(sMM7vl-4V4QJPlTqqKkNsM0B=b4nML zUQ&8Z>FuSfO4pXYSo&7!r={CV_mm!;A3Z-|{sjx{f_4j*EV%rP>@zl=S?A2AXWn$? z_Jv&+Ubb-g!q*nQy>Qc6F=tgh>y5MCT~uvR$)cyujy-$o*&m$K;+%)hZF%mE=iYJd z-gAFDujP4*&%5mW;pe}#xc=gniyv6LfAP^J(MuAR1hVLK635T*S>P?yVrhx-PG%TyZ)l< z58p8BhW$5AzOm@W4{qFiQ^%XKZW?^kxSOWkH0P!bH`l+p<;@q}y#3}qHy>KjZAI@D zLspDl@$xOLZdrcIO}Bh;Yu#Jt-1_}(8Mi%nd))2aZ~w;~E$^6n$I3f?yYq}YU$`s% zuIui4CrD9-MJ=yO`SCvYff7; zea$<6@ACH-9vlDIgO9t%2R**(iJDK$dt&pGZJ%7Xw$9qK*514J$EUhKb%Te}4W8 znJ*}}Ou1sLs9Aclno&1YsR!;3vzX;9+&mM{2-}6_-#A9^MdmzvzP-v`WACzS>}DHw z^<5`7);;T9bDw)5uZmaQtL4@AntH9g^Sq_r72b01F7H9_@7^2UTcN6F5E>E5K3Ne?BhPI@fq$)xp3&n3N(TqC)5a+Bm1$?cPeCXY*=n0#~cs^rI$ zpGtlv`Q_wSo46*?O{zAj-lSHOdQBQNY15={lgUjAo4?tzM$3V1s_okL4~Iuzbk28^6=#G$;*=;OkR_`HhF!8l2$vWq*H^ECTK}F z{bnht4gaELHQ{Z>gjY(zO`ULMb=5k$?dVE#8~jlJ8pHZyC_GYg_(NlkzJG&DwqCGx#I~QdO6xOUkmDD(VU}^9`^BOyH*SmH2KRN#m@Q{*IdjX* zExoq1-qPyR*EZMx>?>Fa&((MbuJPmAmW}xuW|G6e+p|qArT`X{*8^(u-DG z+Y?5M|I`Gwax;w1FU9t{dRf7{ao$32k$0VUyLTrw{e-v9+vvULZS#I0mYDsphgV8% z2Z{Z`39a};5CNg3A+Q~~3G=|Q??ZPU`|Ll3mWdl$u1_Ab@sZ|y7%g*5A6A62HaN#z zK%Mn7gIQB_W4$yC_R*#0Z>%Z$vPPLjPhM;Wc|&1pMZ*uO1aCRcRHn_#8heR%3rwC{ z%-*xbM*V-C(wXI^GvNi2vHqJh6Ys1~GYoDc%O*wOY^ zJJvH`)lU`Mf=+$yh<`@n5;54*42qi%U*KZzcQ3kM-AnGUd)_pgwt3E8VqUkGnK$j_=AU+{ z*=R2{Z`dW~C3~TH#a?7ywHKS$>;>j!dyCm;Z!`Pto#vpu%N(-zn8Ws7bHv_fj@rA; zZ}v%B-9By;?LTcBMxJ)|6WaypK{v+R?)Gzgs{Pb)(Y4(VnWRKd(Hf;0lVLQ=Pv2$FUo$canu}eS-ILeG>L>~joc)U3m-sEy~ zt9j6@V%%=$HS!vJO&G(QdCk2RUP~{*DovwakUy zsa`j)J5+LkcP11`W)X2VBa~)B*|`Hpn4<+}l@yuxrk51XGAoPn=g%?qpiyqnkbz02 z>#!k%l3*bl#--}W)GEWDu45X)c5e>zJrx%GsW9UE!6zFDocYi#KV}Gem1t_idT#^= z^<-u!9butoLEQ#J0mnhVrfJM-%xYqh3eE!>aJ$QrYurg_xE1bACB^7D)4yZtA&3kqh>bw8Zgo;s&^ zw%c)Hds<0;f!lUsyJ+V0eD~>}*u@1!?gM1SMeeOR3uc$N*H3I0mrR}GUOcf~DtLJI zPwbgvdQr*L)e`cq-yNPpRJKfzxoDkr zmI$9LZCh+<@zO8EmNkXd)rD2jm!o$_Ygi?0M=ulM8*?*a)rr~`b$8Ta?rBkNLtlp; zW6msQdFY%_6>od!9PeZAWv{)LfHZO?eO-QeBm-Q?YjB+&tDWo`q7NTj&2UUg)Q)4XXW zp0RGGNnoTaHdVcO$Q~0FdYKEXU|utsS;_;fhHC4osXEZyt1vMECDnMOr&Ch9rU4`HY0yK7-9*0-Ecp;QEsxQXFu{SUCKIPA zw6i%BsU_5|HB__>w5c6*?-X#H3YAKS0(OA1b^?YjrmH>6o~!e@Ynj2_V(+jknI%2M zNc9x6py!$Myu>W$pUmdoXEfTv%;sxm70A1p%j{xo`jt7!VJ8wdflJDh6~HGDB~}PO zxk7l=ARNiJeiJFrM6ZbXw$zNou+Y{>E0h?dNX<#QmH*A#X}k;3hN?sD+XgKcX)|@| zLF?N?uS1M3wSgCjTfP?QZ^M z-ZF2Scg(xyJ@dZ#z z<r$0LxNFsoC~gQolW0auLlfd51O#?*$8ycS4us*&-DwhpmCBeB7Li$j3V-@6%ZY zR%|zM#5j``o6zSFbhtdt1k|o_?LhkN_k@O6iwvR{?3c$X@t z>B}i+6;5^O?9;E>bmny zEjQCtc0){a#+Xwon0ENPV3IIRF}*OIF+DM9m=5kG)6-jQda~;7A;t|f%{EVO-G|Ez-|okBFwHUPt|su*H0kzaGf>S| zlMF6u*gW#f3m>pKlsm_Cwo^<^I~DgX6N71KZVWdS<2`P=MbR^DzPUgQdR-YXgViWcv~Q=m>T366JT)T?6pc%(QhYaebV0#qC|ejH=*|yqio|nE|FL zTJ{wtmvF(C;Ioc>l5`K5>h2wAYRJ?JRij=8nG9ECs=8{tD>a?$*AS6GCe@9moy=jx zzk)c$)TM-dY7)KL)bU5Axz~g6OzPwkQ^`#-E#1}B=Uv1*o9|}fO4xMjFrT!~=bfZ8 zmbNtyBklNq)P#4IrRVn#=_CJBCLxlRmv35$k-qnP)8~tsKS0NnmO&qZv)^AbIThlG|9IwiX$g(2XoOb&J0mnSVw9E&J^f!W z-hIsRFiNL|R#!Aaw;yHAAV%o-e`gw&eJ8YC=zm4?o~aQT4;T|#Vv+;X4!85aF^%0) z#)Asuhm0X*CN6wX#}XM&DjLcEf6;hbNMFXRh>@}D_vQ`W4`Q||V_L+>cqilA|B{h0 zv!Zb_cK$mfV`{{}rDuF@@BYa;uqt!wcPQU7Xw_JyS%Y0PeLao-8OQjv#+*#Kk{oKC zLJwREOst7=!*DB`So0XXs)2puv8Mi-InFh5tv^n!)_R z9t1Z6gS0(?OX~BVj5jiWkh#O9=uJlE4ns|Mt>0$|7n}|NCc!afWyX{u^9HXCV`9Z} z0K34gG^}HIr|lU2OyLxmA|?XQW#QeP4ez%5F?TXHZVvBuA7KQSU*bmQS)C}0(8Q-G zdoehIu9I&|`g%Ru!(cU?ar^PztHHD2rVsvLj%L5`=Tcr2WtVx?G~Tr_X}N0(M`C^$v-ka2V;&W;~scW^9JQ_t#Fo) zUvbP~!Ht^K07u}-GB%arN^mCl>WP_!Sz>B3kEqOi$j61?p?o~hG4>C1)zY@aGzV|s z(e=lqWB69a2f;mg%G{UnkNIyO86(vQUfV=yuh2@ttKzo0sfOnHv2^h=(@AixW_N&d z@UHDOf_rfV|En->GcO9E9~?3X%&|s=5_rCbaH)@110(f8T^wT~{dWa(;cMv&FJW#q zowZJc=1`Xb?QAD>(yI>*XilBdE+lOvFwK}_*0$qJ13T7K#jRl{o5prJ^ml+c2|j!? zJB{)2FUIqR(H^E_`kMyc5ZqJYPR+y}%JXRarg%wEhxm~#Vj5$s|2Dx;2cm*>Q|2T}K5phQ$b z9P<~-W8t4FJR@);M)DjRq#toB<|$!$vc?S6Hj6_yn_jG`^$I_N4C6Q#V-Tga!UM== zm74?ip$~GAex|>L?_&m_cRa`pMxv63q+%FSkP*mML>eTrpm9i9CLq1|kNASDkz}o3 zLwHKc*R5^fwICU(ht#Eki8W14qOD{eGmo1rQ_VaCU#BWN_+hV_NyvmRF_$6FUW%5&ZAhvww3V%J z8ZJO;9Bnd@6eXA+&9&wywu&T~`^{BIl#x2bJrHkf~ynaFipAW<)} zEzRl3SzEIvonzaW*+{6{A?G~>>2xY`>U89F9gx>`LRQzsc14=k9Z6mfB*49p0B0fb z%|`m0i}bdy?T5^FfE|eb$si-Um2|+)A0GGt#P-LhGFKT!^{Z8W-(i*bNwq=EH9;4%vPc7w-~WRhMXXn-5$y<|!YV8H_d`xtgw) zs}29Et~<%qLldikYsfyb#_S?Xc1>JU*UU9%FIh{o)3sto@QgdzF(Yv8psDT6jmR%n zn48TV?iBWzr8>rGm%%=>j%Yb`mYrsp3iNBH^xV;wRHv;j_or#8t=cKL3KD{xa?p_}HWyBX|3JKfE4MeIVG zDk%ZlODieQ0O9bKJS^Jfta$Wha`ukez53yGz`q?lNR6OWBKd zg}c%%V>jAzceT65UCWNN>)DTXqr1u7%#O5M+^z05cf0WOgvX7f|6Vi*R-!#1+6Jr8 zHFy|ZfYnI+*Ps#b7@7u8pl`4iZGv@ZAFN0J;8`>Po<{><0~!D?BK?0Et%6t4D0m%> zf;Z6u*obC=udVPdx(e^3L+~M*1)I=w_(UW=qV<4w!B#X2zCh~pmB@V1Ecg};gYS_1 z>_lr}H(ConqP6fdGN8R^F8qR~!LRNBvY)3f{*2k{B_AW#Z{@Z2m=}6&k@>gxPVrLAJ|z8V=-cc^-Y=5=j>!5uBj@j` z68=+>`SWD%RqGUO0fBGdmES;S}F1oOFfnrN6HpV+24CR31^Ohpbd4LQmTIIgE7#VbPJ zWR6#i&dEHl#4GjYqiJx4cP6@6XQ7RCws($quJD}yz!ioc4QKdo=;ob(H|*Ww-Rj+j zM%NwablnxRV9v~xl$7k4{PswqQ&MB5ALFLQiCEPSD)*-OUu9;io8Bo$+_bdR4(g_) z>-)5n^z_*LImPn}iwbAv>zmXxKW1uby8oQoF_g2Qq*xwOa?*4C&ofg4HzRO625#rT z?Gm_|ftwY$3Qk%|rk_blR*+6s08UnrURD5RR*+s+kX}}hURID!R&I3ul#;?T3MKiJ z)ZUtGYK{*cZhB1q?EHd~;yE$-#nX%D6wZpvFPS-KdVaxz`T7Y_v_z>nS)C*aT!jc% zi;t_25HI3t&O8V4GZiwPv#R84vVKKW@xk&R;sfCPr)v2HGfN5<%$`%+m7sq?qbIRmq=Tl7B|wF|X2tr=WC^C%=sTQ)Mlw?@uf*wI5I=fBwv(smG?7 z8>Hf+q0=!>0Vq-sLCrDmI#hhhJoYL3n0Lpdnc3x-cUk^ZOn#wav2sBNvx2(s9n@v-pzeDI zb>Ca-JSD9|m&()2!KpI6pm_FdDq8@Gn_iZ8mFWtUUyf=OKJY`TSNzNmN-Y1_4+zbe zl3!AJMtNS*Gb^<2>=YmT-rAW`b9#4)nHdysreD11nOd!G=IIhMB`v*E<nFY~BiuC9r#b=eG3h_ck6fk;@|9+1Deolq=mFMKkNed;#b7vHK zg>$Ay7gvN>yETvo2=AR6Up!+0{jX%f?4tYy^J9vA_$wEe7Uh@D&`hF=XB3yr@k^Q= zlr$%(knA9x?4aUuf(pxyxIx8dN8$&aHoH?a`bG5l=n}sKB^62#U8=7tmsY@3X@&QS z%G6G|iPH;9W`nLNML{l=xYK&SSL@IiLR%Xixbzo z9MEj7xae$#hy3}`ISO`kjt0i%ltuGP?2kj_o*9!96d>mqiV{GP6+quRfTnk(%SJka zLYMP`+Z+XP%eie}$psE7Gb z532xm^l-mX46lHY;T7Kd_1z^WG(tM}2u(o3QqoctJUVw;^vH^(4ronIkb6$Y_>sqy zdSp;3BZJO8QakrZzjKc)FMdwYa&v=X=0wmDpfg9&Kxg(xrPOTgg**pDC%a4ZXuoWu z%gYu!x~y}L_B;33@|Qtx&WIjcv2zDaH@9>2I882YTv>yR$}gHbBfqlR!H6}baDIN& z^n3=ysHufT^Ydd0=a$YaDxMQs#0yVe&YyuDn#m9zH7kGa+Fb z+`^fR1T$rx=9SDSjxL=!eRh6olsex0`~@+n7D}4l+!-^y0t}B)#j^{i=SO7k$e@KLKNaf-z9RaDx`O!3*4&nK46r2wdJwZ`h3DsNn*H zH-g+p2D#^vb8k#u=}bSH%r0??iuuKJic1sAy*_HH1enmbl>kFMv4RNoRS{pLcB)l; z@LvZQ@gD*V=tI@uA@Xg_-#u%jIK_|eRV4Z81ZdHRN}xqOv4R!JYpSs9{Ejpe+7WF(spSP}VL%S^TMEN=BEUEWHC5{kd66Mu(sb{*;sF zpln@&vUUmH`%6sT2kEfXlu;t1Lx3B9y_k}b6Xe?|c<)c)QZh1w^nxi{hCiiC$;geQ z6V#!l_kbg>~egeGuYcA>|c<&GPJO}XkGbr#F zNjFkIL4GNb`U&vfC3yCyoGBS8k@|_mkJL|)UQUp2r{KLm3R6EpykOML@JD6pCz4L2 zegfB@r&15W``jRXe;!MCP@detO$q8JC8(d22!4Y4Nr}XZla|s+@rNAg)yb#5X{mlY z#P#2&`t*Wlzg($)`{LP$Kh^L5JO}Xj^_P|!aH6t&TAh~V*H2oOPs4c*;0QQZseXS< zOZ8{`xIy`ug3#_s-^X97-wsGW$lvE1F{up7A8@u(v$87d%+RNaaXQg#FT0guk69?C zqy-%{Evt8w4!rS^!MJ=FP7gY1deG_8Q#A9G^q_I2r}&L3J?NzADSoF-Pw^XFdeFJj zgT|Sj;y1qZpi`v>16g`7aHR(WQhG3Oqz3~`dN81*r*-xV2QtTL{!I3A~hgAL~I|s_25RscovkFV*m>z$$XXls9 zGHHLbNtlxcH6KYyV2euZ9$P!Q3VWz!4~T@x9*il-xY2$itTNi~{=Sc9xW2D0@@`J^ zM8Ap?AyJmM=79KJ53%_o*k^(iS!BoqkySpAoKkX&7F%OkZ&rDJQL^jWhF&JWr`cQC zkv*6vvmdh&yPK-B^E%oy$cOeJJKBL74m7Kc{L!vLr5DX zKgn@3urwma=eZNfF^+pBcgU@uuI|QWrC_{Jm zCMQ%}5lP8jLVnlT#iVqd;@~ki(JmsbhwXgioL_p?DRCkYuLLuKFIt3sk@Hxs?X30? zwSQFm9CW~~jZr&U?Y3%Pul79}XQA57)xJ&b*=nDp_L&+}bjhvxoBB^tyOG+p)NUa$ zk;w`SGu57~b`K4orv8sLrhj^<4?~)Sn2R+$LG6`lx7GN))P6_pE^3cdJ6Y}4YNx7Q zp!SVov%gzl7^-$%4S8Grg=$Y#J4NjdYIjw8tlE9l?kBd{r;x;|y( zhMc5!XSM69-CXTw#HMu8qb|{ut7>>-wd2*Ur*;jsTd3`cjlM?+S`mXTis+QYqa9M6 zU5T;>F`}y?nko(1&DGc>{Wp8#%roe&yk|Z_YQMwmGY3pqb|s=WB3dI&Y%{b(+Mo%N zjt)o<^gXiC^caE8#soVNEsPWGt3#&&J%;1&t1H`6r#b))ko_ki?{8Zs&-bPIBFh&^ zeotihBEioslhKQWUZnF=%H;Ah%jEGT$l}jH27kUv-Xn7N%LAEvMB;uO()Ja|)i<**vVcmXO z&-0vGA*WYt;QWUt&`)^)-Ie$LgL@-i|JD0d@;TAH0VsZd-wIqOzMlZ@zk1(*?#_ws zBacE~1nh9uKont10gQaMaUC*AQGwhw51F;y*r?s4HvJNe$<(!SO-(0Pl zL${zSOpo2XhY^J6+7Z(zDHdjH+YJ{nE41qMYicqGKtiIX{f{ z%thuAv|;WxtI>d2WggQLnV%G$6|mh&CnGF zML%|+{VlX8G(R*aG$S-6G$AyK(~kOwvO_&Wo$;lG+J;($l0x-DHA4xZN+B0&;T`t& zdp~*KdtZB7*wOtSXXm`?Z3yLg>p4khwfBH`H<}nXuvdF2`})p9yQ75k|H?fdDE{0I|LyHfrVLV^pn0YEbegs5hy( zzj}A1hNX_>PSKiPPHq4BeG%3fQHB}dvQqH%}Uq{a9xB&`D0`;*#a#d+TVha35FA!QOQ zz3Q!ow#Zmy?LxKDK*m2??G|FQn?pios@+5FpTtHpO2R)D8*O;;C#v08?Uic#;c`;6 zwY}7SM{S=Hj#PiL+O5^*+%96Ks$HP=RJCtZ8|`B9{Xy-wHKb7O32L`gyMx+Y)ka&H z)cUB6_B8&R)s9trx!PESHxwIvFR}NjeTjzGRa+@Ay1o)qv4P&L_?wGu?oxk!wNFwT z&1v3tR=a`N=y3|go&jBw6DSg()iLN6*Mb_?Wq)!4rx$c(&*xgskV=7)Jr7OZV9?%2 z%lLIxa&MxM*~`2S-R=wB{uHg~&0v2BI%(gc5&fOnizf6hQ1U`3`5|&SOn#?x5>!=m zd*$^1GnAg6CG$ITHY>VDXtgHUWb|1@FL;Sl|9oGn}M#4(A4)$0(9h^T@8uk-W{gC? zq3L{tv!~jkpIXg!k@3j(lK&RiOwlE^S@OpMn=N{zwvWs{*}csl3+w=w=`!s=nTOgz z%tUjo=#J*uJVvewtTGt0IM<3X%Z_)`-E=!a=A-sBSwGrIXo!~B$?~rPn=d08r&%p= zOKgFRYj!F#(MD1&oxwWqI?l!7Zv?hTMmkQ%;%@}3_V^otUBGO0qdiN;K30MJ zjliBQV;}27#y)$Y{Efg~B!45Y7c-YN_7X%ZZT~-WuK){y6+)__y!}xF7Iz2zN(# zAMcKs7(zZZ(c%4JnXmkRaD|_x|3>SKiys%@R^mxcWub3}*I_=8knmgh_JwzaKV(i8 z9o}VX5>g8s;Sc}J(=P6}OkMD{QvBe)rl}NuE4+m`GM5uB4R5hM6Gx0+i4z~htwt(a zNQD_6-yftFWad{};rRcc2L7WWy6_LQbk@@0?ND8rG5Bj1sF*(Kb-;0n?g0&$!R zcs$yfN37$%Kh7urMNoJzG-DU$UFZn=gm8Z%^hfaUDDTb*xCaQK?BR{jn-J7Ij<6pBT#5fMF@7fG zYvSzJ6!$`@tA=;u-z(_|o#K+XLZ7}i3FNg?+fXBEk9_~R#;9QFDXj8bg+AnkKc{}a zBqfKvi@bKw!>U(^{(tQ~4#tJXa*~%^8M$q?hSc=Kgy8Pfi?K zp}ia%5gZ>I@E<=Vhu2e2PlPv8FB@<-@N^IE%cR^0sA~|qfi}8Z>(JAkg8Rbn6Y`8w zAQ`LQrk?i^vMUJr0Xpn3;&4!b>ecuLs;hbGJ-vU<1tcs;WhLd z341-fDg2)JfaNFJ+ivv>{n-zcyXoIM!>?#qxdojH9}OO(_~tW4vb8`PMO!_j?f-RJ z`%9S5!jA*hTj4h`ua_qf@sV3)%AFVv)0!)rM0p}*-+cra))HDdd@c0m`|$hp6^|U= z2ht6UnLB~-{qWng`1iRtn1+O`RexA&3S91|C&-CX4drIUXvhh~M~Jb9G&p}a{E@o9 z5bqmm-2>Wvd=W1<)W-?i40R>+q!-cLz9rkNVnAD_Y0>uEO{; zbm4bb_*G)OK>dFK?b`vZdr=}tYZ2@b9<&=$_zt}46@HgDfj13tH=4m+#}x4gle%UR^0Iow`<1sLP7RLYZUbeDL0VRcyA9MZ z7Qgi05@}EDHm$7;QO!{?*BQv)P*-?5Da=3cj<8*db z3eUa)yIG}WwS{-z2pL%Cb)e%ecQN9`PB zg`@cM$!PAm?5!Pxe=PT0-DBIDa!g@IYXSG!in}?w3$}z6>qfBmFL;S=NA zPc}Q*{nv(-dYDs*z$kyDstb}OF5e(rj%o@vUi#rmcjI~N<6ChT5p$hivgC%iH2Vr*>2BhTPpQ0!(* zCWR)h2`5i9bxrX%;}oW*>~3ssYRRgT6DwM>&#?#l99x-Qt~I+IGuiEUGU07Fhad}? zNLw@8wPWw&sUq7nUD@?`idlrDB*kzl6@1hd$N)32J94H(7j{5)W_Q2vRP)&f*^O}d z-(D;BKxUa~tX_L_rcAcWHZkmn%;DSs`S)HN`y%^rUIIHKDdPY)fD#RK11ZZOH-u8i zKlz$-7V>Z)8R15d??^Y&6v}={(^6!iob@yo&TT$BC&!tN$Y#cK+Q0-ifn4Nd#}2HH zCz@HvYbKdG?5LbR7Cc0^yWHFi*G^cqZ^F7_vmA#hu zXS>E8q!v3v7oPRqHKvlbHBzj-?+WyvXnv-G6G zI>?*u!Ovb!@?FVUT>aV6c|ZOKI0LOdGUnA_fIXdH;4$|U;p^NwV1C*?4Ib9>7t zR(R$O^=!|Y?DYJU9l3J0XJdAHeo3j>=}D=Dmp<9;a=R$YZqD+MKOOEP&M%yY5hWZ3 zPWwWt&e=S_aq_3fZqK8pkuE8ED+hiu=YNJww(R*dNYuPaoZ=Ac#hS&OHyCHm;>^Ly z=6sRToAbPQFW#KV`GX1O49*~|YR=^x!bEcpXAxH8%=YSDbxwV#>D4sDI3=|fr(e|O ztgw-smRiT;p;J)T4B^z&lgtQCPOWE#b9QP2=$T_aBa|*mDP4@xxEQ5wq6skyS}D{m zR;gPhrEalG-D1oHZjW;WPGc;X$n7y|M8*yItHcb(6B#$0(mwPr5_b{P)O?`YB~W)c zlVJ~I?`~!T2U)>?g?~Nsn)QrfKZk!~=CPg7LyWaDYdIV~N(_&&^@#dn8O3WdH#^D< zA(3=s#1SnEnXkXjc>21eNg8GI1AhekZ|v}Q!0-*w9>mD%6kaT|EaB|zBJY1OM>xow zVGmpi;9zc2nVJ1}#QBcU{ZRD=%oRRUe>?K8!g{efv!EB40d1t5(Y!e*J1$1oQDmzU1B-VcBL??0FF9RDhRqw8O+4wuY_nE`$1ryo9y z8_Sz`YO@A7IRbV>JIEux$819Qz%q;Fv?Q<_<$p!H*+g87eiLHHwLpG3VS=$nrT zOM<`mHuZcXbF~kEc?<9Mke-<5G#0C8V39R7KU0ptC~y-;LS4$3!I5Y69Zh&#LDT z^J+=6lHU##mv9W|f5%)vMehPD!TOT4k8^3ARtRTJa1fkGKVr44IcQCgi>w6H4}6C- zSO3ZA%DtT11J@?}iEpr}NpQ4Rarr$N$$D4#MADypTprOD@hda>{R;73go9EEO>vtW{*5wu>Q)m0CvRk(;B)VP-6S*`Kcf*Rkf*z+_$ zxYg7+{-DNLBaqA=YFt~@OXT&wmMT)?vc9TNVzK!h_52Jv?|yX(GYu`q=h^z(kz*be*>YD)fW!rr?7gVB~2) ziZwB{=nswP*HtNvW3_nziu0wZM{834ICf>q_O+}lwXJ-GE9Pfn)l}Tm)}-Ze9#4f! z`_wM#+9RdIim{`tKKE1Ghbs1KX&;PcyR_}f+qb~NKe=h|)NOgGWu@qs_OsNj$E z!MAZ=!@K}xe~bQIE&Lp#0=z+1(s4k31Wr*r5Nse_nU%Z(780r1C$T>S&qo-6j!?%R zv!eeND0eVsB?=v7gitu}!G&R*|E^*?_FyP>rSgg|u&AN`!X z5~#cH6zT*YcHoj475NAkEnH~4KT?T@tGrf%IKqdP*wMt6aX@5b9(EL1_4~1>?7Q1yM0XQ2DtXalqgWm#f)pc#)tGd=`+tEsQo_yg8a zW{s6h?eO#A%c!r1spALf0oT(~9;6Md;jIxnd=Is74gI?&*jYoY`LvSPY0sobpWjSN z+8n-Bzg-7r5AgP3GK7c3_rm3UoZfINp+~{XFYv$q5&k=22Y}`6@YDFDZNKjG>B8TV z;y%9f@f7E`Y$-Gv;}VY;;_IJ^yni#CF5Df7p`N| z;|m!zwDe3lnY-eDnhO@W$VbM~D)8J6L-jBkdW2T6L&r8+u9oo-F@94_>*&b)!_>LS%F#hAAk=R| zT3?1-%hUUYv_zh|U#Tg;;XCd{+4U=4l4f~tIVQ4$7c~(MoKfCeHRZZhhtqaGe4q>9 zmNnvRv&-Rb3b(A8a?6@4pR5IEn_Um5kh9I;MBd_VnUTfe)J_b*V z^LgQ24NJGx;rX2C|OMn z)@z2=Yld<`D{JjmQ7&kA<$}g37c@q>ps~sYJyp4&LzD~JN4cQs$_4GCT+lq_g61k0 zG*7vpy_5@@sa(*$$_33+E~uwmP^VnbJmrG+QZ8teazWcE7xWb6f`*g}YLyF`r(DoM z+Me>13)&OCsrNZcTX>u#3 zDE~cx3))_}pmE9tje#v;;CxE!?5AANmdXXKtX$9x<$|Uu7qp&oL7OWVw25*-YbzJD zrgA|WD;Kn(azWvN!Ue6%DS0(as`5flQeJ2?<%K3IFSM5OLTe~5w2|^c8}KKo`kc@# z9ML*(a3^s-sr)BS_J+y+Gx@Wd;}&j|-11i{?fvkisTU)ll0Aw}6WkHk<>wWF!~6Hg;{^`tM~HEi-75waF#YHh-2U|ML{Nnjb^piorya zx4!>O>w_O~WOmriRv5SQ8b} zT9iX|u7a{iRKGmps}_7K624l-Rk~fuQn_!X1ANsF>iIMC1DP+v@rCjq!Y8(@^x-vN z4)gT0Up5`xqUD*HDjY<7d*vN8`w(O1PJH|IS?6_<`G0fCx|S3Vb03uzJWnz=Kg_({ zr~0hiBo3EwSA;_;&r!ra$`}a5`cCHIGMC!VbF`$Tv5!(a(cnS;zJ_i}5c5wRDXm1p zm02w1WB&DZ_>I6{HiOqV-xDhHtONdhT;^^(`7;(~1o#CmF^VU_K7Qe42@iS??g?l4 zngY-$e#HF%Bf3eO8R6fi{yyf7%*Kw=rc_s#IhW{JXbJS|ukhE}d_ZT(UlFG^zJ`Q+5zKE{ zne+B*%87m#Yk=)Q=9m9hQV;R&W1%j(&f9}jWIr*b7JZ5%|F3L7Z>&k5t!bJt`xE|; z^3w$%N=J(n-|;WyRLERdYV8Hum3&K^;1Z(D%{)|AKaoo`F{Q;H5Q@ai(vuXVw&aSe z=OSOwn!W;0+AgJ@>8HM5RtNNM|Epu|V<37qfXMH0{|+KxCda-19wOkkei;;^NRK!c zBH?9;=4f!0V@)`f{ssKP#g!|VMN%ed`_gmDapbRrf@cYb&tGwX&{3f-wAHADjWVOpBkR7aBkCyrO&=24+iTFDH@PJi+TW~pg}Q}Uy?=n&3Uz!3oxL@1(w?BFuYv3KCiS_6 z?_Q=Azs8%bz`q9Cw+ddJA?}Z~b)jX_chSxTO24mF3vc83CDxuB;NQ|(;SmT-o5<-G z@{WayTK-K@gWd0P);4>Lb*HVIuN~QKD<@~msoCAw@!u28kQ1Me&HlW|>DaWKDZ1uL z&^1@0uDRlL&DBcRT-|leRY})e-F3}XN!MJly5_2^YpzbZ<`RvnvFIzw8UEdM&6TTb zu54X%744Iy_OT!_!oCc%oH@r@rd&R8t+EhN{COe?v`0t4DNrx~dLOPu1b+ zraC-Xs>9Qjy6J(QiD>e4Q%#;8s>u^yL6hfH)#Pcfnmkpg#lDnR^m(l6^ElP#$y9wF zPxX1CRG%kW^?5?n>R5D*RAWb1C><-TP&()eB|}#zX}Us5)fGy*u27P6g_2TXg>rI* z6-rB8p)}VON^M=C)Y2776J4P+))h+4vK2}VR_@FoxMUUJ(=Azt$VdW#W6Y$ll>ch3bM+O{q_m6PO5#5z*8@w z_5pO|a_M7%HV2eLI46~u%ODkn%6@rV{SMLd_=9!_wC{gLm&2EhZx-6G6zo}O)!Wc` zp$(>lQ4@$Xhpv0fGP zDfctejOZ^gMw5a_x!(rQ+rW)KFOSSn(P$vd*JqHCTX>#A+usBdk!4dyJjuL3#-p-2 z3T7weS^j%pBfkF<-Cen4{n1$0=Z@L@SMc0-A!Gf_w4lbsUxQYWq%-kdjD7rFo;iJO z)N=P_Dz|=ORB6Lfr-@_vnH8+vX11E^>(B3*{Fs^EXv0$PvQz!poY#+BK<;o zDm--7Nqf|V%MDCN@YkY+RMEA49Qc<@dUG}P$qb$~zqA)dU72G9dO2_a3+TS)>|GT=XlzgRDpux~_Wu7Onh(5^ox=!DQzie)c|9gz& zCwPg>H${$d6eHisyi{P+%byWPp8l#E(3j1$>+&(+P2ge-APu4SG6qCez&?GEwMRwz zB4dEk7p1koqc49I65`dS=6|4P$UmBo!6z^Xx6n7fKLHw1CkTtu(cfdFe3|bm(aWm_ z{p-&&_xs@=<27O?yaxI#^M+XX6#KDvDV5oQPRSnAo_TV6m7U41%)_*#uXz4}_Slm5 zyWm2skuUU2n3H{a#uKB7wm{LeiQ)P8mvBA)aM7Y=Y~4)_{R%fJtlBg0krJz2e}$+c zp3`UJYu!7Ekr?i>|P6Afy)Q zTa4C;^yKfUmk<6Ab8iA*MRhG|SJmlxKHcZ^Jf73@Ja+?4GdCCo91uZKK_H1-i6IIv zHyRV;OK$SV8;$1PO#VsUO|B*;Hxi$5z^EiDCJu;50xF^cGB$&NOw#APZ>_2`^n_q? z|MwSESDjr|d#}Cbz4pW}87*boD!Y(;(qx?9A&u17G#^yFO2<%Ma>i!aFXH`fB^Mi( z5k_mlp(BjdQpf}rA3Q9UL~*7vv5T39hVbCfL-nTU$7q@-P^VN|7I{Ajyx6fmFaTFF zrU{(MTA}#u&|mDW?Q_|hKt`de!-|Ytl62Aj!^bVj+o4>Dxyp;7V`%;PUSm(gV6?|m zsAl4wy+mY>(?2c|aIttmrExXeZb~%mh-$#yP*P$s60Jcq>fy2V8()M+p~-y+qI13u zl0fBgc*=?LM2+uJZgKbx>)%|ebj zLb-&lVjG=&qw)+rm8;@RG(glLev*Ae>cg8N0pckM&p1|9w1NKK?^qi7{Y2>N&^6*c z^Ay~k>j6GrL2~4`U*^v-9^Hn~xNcYG)_7Re1`9=Su zaO?=ju=I3^bc>Zm%WcKm^}y#slbGOI`0qvK+#}p)a=PKtVy;H;zof^8XJR>5BNflV z%Uj^?59lSbF|`RR`wY)Eg6FUtDL{A^aOE1x-AY7#4Zmb=_ZAyi!6OHDAj)Rik4E_` z9!}^0o>32nj-%jZ*k>Z`yF&30>}QsGn=&G8!1hd(U${pP#!zSRneeu_OQKC8ea657 zMWJ*kmq;0oS1?$&il;jAA?re@kEt9XbIxLrp#bH+N%`Br`)2NC?DbpFXeZia3+eCk zGy{6Q#q;&RvPIi;c&ZN1NUnB&_dBFHw%XyzB=V7`GL9eKlhffZS@C8qp5tNa%B*sM zM1gk6x&x~9?&DqiK3RK9R>QB4@gzoiOL*rozLV#hDC2-V(t+CS(EgZp^a;MFbF+?9 zg6(U1(AjSg{oJO#GjKjVEt|w^#s}MiUvBs7*^$10%8HCWx8ToM&d7Qe|$q%o^{Ybs}@XYnb~zmic;*Fb})lIKb*=OIQu7j}^T>#r)`} zm>>N#tAMTLpUjNj#g4zsi8e1~PPE&cOD*o8#2EHSmQgxQ)Fh6aWyV7zoSHz2jJ0Wm zYm#X3NW{Duu1q0aM&C4pl{DU$wOKmBNe1x9*qII;i_;Cyco-)qBZC@=8Dtw%saFo) z%E+53_~Uu(hb(jEW2s{S`zgzudB4^_4%!qmTVCeO#~Y>ms{tQnKqQga9>quxX*JYb z{{<`aRXgT0+2bLNv6#n;Vzb(u4t4CcmC8yQ*HroCJfw3o}$ zUM`>Za=EmZYnt|QxwMySTGTw#813b%(q66@?d7V{UM`pRa#db1wK#F>RE`%qWw4E*sr1HVaU;E&fC_>DRPze#7{ zH|h-hW}SiG#Hw)AU+3XZVHfIbqf=+&cd=`99_cbCe+uhey`5YkJI`2@j5@hM(5`z==}U1ouA*M^Yd$Ue*R>g zpWmYM^LuoDeyh&UPlOj;^iIkA{3M;9pRDuqQ?#e5LVK!mbSzz&_EZ&XPgR!oRJpaM zsz7_H^0cQaU3;p$I+m_nd#bW^EM2MgR26AYRi^e-WoS-BxI;0xK#$Sl>0r({a)q%Y~}tOB)^<5WzE+-m4tkigaVa>LU_B5 z@9X(jtWr^;Qc;3Lw33U+hEHX~ud-olkxG?^DwT(7bjc&s`S<)wQf_lAx5X&8S#X=Q zP7m-e1)jr_u<}EYHCJWrhdAZD1m(PB<-7zq&uhf9UwJMoG33E>iSS&3k;M9ag=`*N zgvLrjW0ml?)T}U);lEaLYcsozRQ4ku&wa1iYs4r=rYT3JD;H+4_c$xj!;4oLZq}<_ zWMt~fwQl9hEagj&@}(EP{C|vW*$LdpQ|`=R7548LxvXpZpQPMx-fxt!yZ3`ezVd5< z@@u*BYoYlhdw3Vgy7-3bQU0!DNA41KS;HiN9MAnW6nE0-QdvZ+1NeDSjY%x>3ftIn~e21fj34ph`X&3 zgYkdkF5&@4mkx}_{x3p?MIC-H1}qjF9s+O2@U3O7BO+mV<;Sq&{rHU^4)bG&XYz?t z7-Mo8A4BNY&m4fYADa=da>&@VEkL^isQ2)9Hy#XL{Xgbl*4PL~@IRTs5zi=sF~ROgf&tQR1Z8l+QRo|hPO3G&27X7vFzrV8N?^604}bV~r=WSH=Ss#NOC(oXXM6a)(}qjq)+3amEy_rA zdW1L5ElFB*!w|Hr}6qI z^9`tV>TH${Oi7EyKExxGBf3toJ*4GOmgZ+!eS-zbhbk@5pSvurVi*)c^k~i z8Z=H6;cVZZ9PA&Y+qH^5ka&1 z?HtG}2hT_4keUq;sSyqki)v#v4LQvnRzNsGeF5!-iS)&UAO77pqe;}zjG>#%VLO6c zVpn8*d+=f2iQ%t$+enz>cMrpIv_CE@jM#leAAxCEL&$@SSSsz(XXC|V4v_msXm?a_ z9D08!JUm&?w*g`q%-MtD>)-{6K%XFTEaRO7qm1s1&_6ndP`ak+3Lzn$B8O9W)Wv+~ z<^EY{_Zl|-6uC1Og&2y^4xdys3}3tPx>!#mk~dfia*FLjUsIbs^h2a^x5nYD!(1c0 z9QjaUMfM?n9A$IZ?U-C5DU1?<`U&JJI{Y}lUIB(z#G@7-2M?BNc>QefAN&G|Am>U1 ziRkuvYLKRw-K4k?Ygj+{i_ovAo?U?i>{6P_5S$PM%kL|X=L69u@U)%&$Wy>7D~Oj; z8&>TAm$uC1fDP&Y2_1s57~qzZ$9D zID8(JGjf*#f8?N_+^i7s3Xxv89yp&yLS)1iJ2e2YJ@VyL)Q2(-$sTziKCrBAC#$O2 za>r`t+`psaSs8;&USeA^;r>i=9iY!e;Fl3K$R4-iedK7l=qx$3kEtJDYZto9q9$8d z@drJof2H-0M&T_i(-6AYx=0?%oy=Vgaaj)eNL{3_Py8A;cV%c!*4b8X!|t6Hj+B|s z(1^RJ1C2xTasi}s(1sVj_%fHkw4=iw?pr=JUdtM z!aUT_Bf64)r$L9v_uq3!dk}ocn(+U>IsU(Z=`6nQi-QrNl(5IW{x|!+qtg5ZM_yGKc1AdG zKRmh>Nr|j35&3-hop@24qS$nq2WeapKC?a^EciBT~~1A zK1_ol>Gn^-Za=52Hz>A6yaCbLdf)~8l@|X-tf{QrbPS8#Po%V8ZSoL#(bnmEe9yQ^ zM)qwYE|^aYZWFEG404dQ^-gmo0>pE>g=iNdV}MxmkkljOEuO<^TA9=WnBsZ1ohTLa zjDc+*dF5bNi}7rVX(bI9o$@yC@=x+q=yP?=HRYz@yG{{9F4X1_Z40R})=#2A$c)%U zX*C$My-M^t{Leo?Z;c#_0E#pA3S+|y<(Z6I9ZE-iQN5v&J~_`c=z4;<=yuvZGP+0n zr^n%^SY4e=FuN<1VMxM5zXx6IkwxOKNDM=cOdV7IK6k_$@ut@DY%Oz&oEV018u!u) zKPaz9YN_pD*4gG?J~0oO%juH#GP~N8jbwUPM zxu*c*^D!MY=nb#NWw?-eK_$G z-h}c3L+BPcLRb6Ou)i7`g?98Cf)xvidrL-6Vgpv&lp)=rQ3w-G9x_ADrkzkAQ=f{a z7tJsI2(lWZ^n|e5EVL`oG4jOefas|DQu3}u$c7!@Rj8jvOiOy2HgYH9BiXf3X)W4M zJ)~hDhIIf>q-_@AC#4-Ul39j2=90W5{v>fNnE_-#JFx;0ekwqU8No%Gb6`2Mb+qRP z^cQYoH1T>`o(H&p44mylL;a5Hi?Z5H@PW`baz$4>hc;~I?k!fcOwgyI3twdwQW+h= zs@S~o0kQ1#;AAw^BZ7O@%4JlmT4I4Nj&UH*2UkS$jaZFH+Q;2t1;zINHpC-md0%7W z;%8z#bfr%C{$cdFa5n4G*k8(ZsAd`J6P~3E`;lNGdNlgSe!%-Do$_r@L^((y!ylD4 zQ0VzNWHs6=9(3l>kM{tOG8;#N37KK}0oaj|LbmOU!q3pWhQ>5T1LXYELnQ$ZmPzfU z-G}B-ZS)+YEZOKUMLi*Ih|H*G6O}sBEqD^UZTAQWj@|`JZ=)N;^6DJAkRA{%8-0vC zPAvM3PkL{ch(97@&ZO@^=9VNWJbCn#0&2b=-pCvM z70Q$qA!Ox#Sy6fbE@b=(b3-=6VY+vTd>ci(A+#C|*3n9&lyqe29q={c5zS}X2GV&~ z@~X4b;7jrbkevW(iMjkE4%%^8R1=TpEC=#$+cwdf_KaE=Ep8W-gTsd&(e)Vm>|B1^ z(gY>KQZ@Qls=j-y9tD`OLZ->G(^!Ml`M4lkYhrrZX4tPjxzO3x8V>#qi z{IXNDf>}kIdz~XU7_vxqqoqgQ7<{L7m-QG1)pYPXp+R_9}iF zNqrHYT=%9>sn6kh>Gu7ds9$%169&63b!lQZ=A` z<(u#c-sHr0u|Gl|0FNG@5a-Uv_t();;n1_l72cLT7~W865bO3geyu@!KT11T zw7afy8Roacp)WrPXQAoVQQi@3{+~nn48DfsSYg86V|hi+-oZ{jOkEygR0Ri6I{Rp- zT^ypbf@=rodG=rMQv!NV`uyA@zsVaMM6dqBDp_w4E#1N0v!tw3D=e{FutMq^{FRoV zTwe|475X~#`y8(Yu6i>Ek51|KF@jL zN#IP+WjAS8Z!-p9Fo zh2Gc%4$cNsl>dZu*cUR9AXm&ji=MpA%CEN(C%$& zbO7qT%|BVUlD3%8Ol5b3{wL}d_jmdIHb0ogLi){$85df~Xq!$(ZA@ow@Jv##W7h5x z=IDNo9kW*&tIQe9iQR4Xv*K(4tIL+KlB|zaW2;yvwubd#>sS%Ck(FRutv1$u9cxWu zW!EX}%G}2qt~auy$4%_v@m1?~>n`@%xR>2E?q`>bU$fiAI(E5unY}DtWB1;-*tOym zdr*ADu-O>)nMhH45 zRC)#J$&+zVrSwK6axdd*WNv#hT2bb)mSR&^xn2xKL+h7wN3* zLYOLg9LiOzm?>Fn21o%ia~8Q0Tv#&wy_wf5;;>kD+QwNK|-`*f~#iOy{G z>C9HY&b795uJr{v%QZ&lT9@mb)>xg>8mDtwD|D`Pyv}B=)VbD`I(Kzq)LiQXooijG zGg52MI@h{RS5vOi)s$0pHDz5>$*FR3xm5+0D<>P8y`8ZYAJkHT|j?;8i<7{2kI9*pYPSI73 zGjvsBx2|gJ(N>x~g%O?oU2W*EX)!wT;ts2cCM}fu}+D-kG7h?lkMJJL7fNomSm- zXNK;&)2O@d%+OtTnswKm8M@z2v+lIhqI>N0=pH*WbdQ}Wy2s88-D9Uo_t=@CyX(x* zeRbM&N1X|}qs~~}QKv(9)S09^>P*rdb-Hy&oiVzXPM7Yb)2@5zbn5;&^}2sfgYKVG zulwiB(4BK;=*~IKx^vEW-8rXKcg~q%$^PL@x^qsW?wm72cg|_nopV}rw;b7V=|$Fx zmOaB~=sr0!&@3C-w_J9}Y1SQb+H`N6NtWyoK0)`!8LNBa^y=O?ow_$phwhCtN%zJX zqx<1>>3%rvx)V;X?t#;-d*Dn$Vy@M-9Iixaidpl!l7B9AX*0S&R$@p)r*@-fWZea~ zuDg(_D=v&j$1X&-T*tq9T_d3meY*rbBKt0Npm#qMTEx3N=vr^VVA==ZVgt3Jt^#2N;Zt;t5RuHc=5zMp0! z=-SQj!HuDdB+ zqC9aKw&K^uOx+#na@`&23Z?ga-4SV)?u9fPO25Y5@v;m2#n_U!*cVCmf}fy!8_m(Z zjpiyX=jpVJiMpTBBxo6H^y=yh*XZgC<+}PpkFLHjMps`b)YTXAb@hdIU45ZhS6|4{ z)fara`oh(^`hs6qUl^yWFLdkb3kACR0(-i%t9zTSzR;wrFJ$ZL3uSOmE*8aL74Bl9 zNpdHndc| zaJ0&dz`DoY9%qv8Urlz3V!xW$1&UUI<75A|-T<8XuQikJM)}v85cwB+hnm^{lEy{-vmt8vQH!Vf%4VbK zM~!)c&f;Cg?-u)tGm0K8 z$|&4eSYGgrf_8bopwT{kzmYHU@60=rcV}L9?&_TNIX}p`l5c-d@`LQVy+^p?pZAxN zm+v>;|B%0OU-AR;&9MJ@-u3vhZphr`{SWVdWd1BO+x^*$AElp6?@Rk$>YJ(MDYeOW zC#^{;P5eT_ri9M;yW_UU&4~S4%+{DD7aopqkF2#3s~Rj0 z-=z)>)`Y*3gr_nVe@Q$;kLs>$a+;5SBpR#;{k4Q#KFz-}G)GgHhIquxw2(~Sl*%gi#GOfT!TG@H3*uFs4*Z_+wDq6X1IBacy5AZvSiiTPFkZ5rw4O9Jp!Z%d zUQ=DS$y$$=`zyP=Z8zRfyY)W%2=*Jhti#q}W1n@*`pDR?`t2yTD%LoLW-ByKLdQaS z8j3kP8MWLU{Lm^uQx_Q?3;z{d#90;S@~Yrrs~QiehG%ubtyTlq#^5%v5oh%VPg|3@ zP6@8H`uKi2>GOgotSdRM3O;RJ&2=H~uL%Cty2*&Q?jXPWf&uG(!)-kuJY=ood^tE^ zZRVuE-lB!$qCdsONDUXOv$?#%Gp_95fGa0>+Lg;Sk83{H0$MsXK)(~{&j9_?Kz|zO zT|n;!@-zqXqd=Yt`Ou-wfo5VW_9^T_5N4;Jeo4hBXZO#b9Gaa0`$Z0JR6G z_X2glwV8KM1s?(8ok08%5W9f*h(enm{16C_y9&|SEkJo1D31ZI>I0>5Xh$(&Py8-a2^P@WkE;X-WhV(_;DIth+%$R`4j02f(%9c`tbW2s|Hf<+EH%0nY^f zN@^i{RSB+N1o{+<5k=rR!{Lr3<&4JQTf=ZW9m9e4ZgTKzIrz0XAwT#5koBXFY#tWKgogv*mz_@V+kJ3C1@)~0Qk#D*pgaVA zE%18?NE6@!ycBBG$9L1=kjtUPJaBgvQCPv|LjiUzy!X%}-@ae7K{5DQovP2*B zt3KEaU2I+OHc};2w{<{1vM&;4L&wT>K)4<+IteIap?C}w&jrHcjx@11P4EB^#zJj@ z&;x`Xo7#M1%lk-B#sg)}ASi7-itG!7f=3HHdVq2@c>J4-k*Xm`l^@>&GBgsU6rov4 z(2D|D1^2ctO$4&nfoiMOL|P}M*^sTZR2wODPmG8|o5EX_viHKHKY=FC!?&tO6)HDS z{R*g_Rfufa-i=(JQ3~fF+xh5Nn;-Y9T!;8ks4d!GBqs)HCjjk-;4;z&#`d>EqeL2#D=l(z09M&nTUBcK$C5k8bXN3w~(7ScNE#MfXi z6a0V{3-~Q&l!4cNbmva13My3dyN0t49oN9Mk+de>>jbxApQd2DrlDQ>&;zsBqjxs@ zMa`q#cO~aloL6%$MswW=FRp-2H-XpN;G#RwT=xb~S@&~34~^DwzKq^r45js|QE6>} z=Qm^Hh_$mrDjX1e#g&B3N(R@gBkW3rUzvC7O6Quv)s1AKNAZ@FcePf>sg;k~d_Zm9 zp*F?T!bc4X9Oyqn`%4Xyfc`kp*8_bE&{qR}HPBZBeKi&qo&_5#48ILES}z3gdTR74 zYj15pe{2rE43sYd2m zKd4_#S`B9%_YGVdsnHbZ*GEmJ^RCTVoj_d+)U`le3)Hnh9S2Re0d+BSI0fXdA<5_* z_Q_tyX+!OIpnf(=7S;jvakwl9&dF$AIr@ zASK=aw4H)mEY3921zKBzMFvWNv=m57fwUAzQ-O55%D}Bi<{jMM3zz%^F1%mmAs>kI zRd?G^i%f_FZvxk6!1Wn$y&kAfxKiPuG`J=MdzgvT3dA0u%>dfd;Mwco`GoB!qc2nz zluJT*_C?`2&4K>3gXeK0;5iBCg*!^XGaeDLWC$(NE8ezu@WNCr2yS~K$e2VNy6gbQF7nFwGrxd zBKwn({V8z6G=BH-oz1t2NL!+HIhJZ3C11&T73bBQwpQE=q-l;vMEgT@I(e!mk7ZlO zX+s-rHT8$Rj(>p~PO50{7z zIZVxVI(!|iM~dMP(IaK(kO4=}r$zC&SgCqyR8NiSsZo6tkJnI>W7H(h(IFmc;&G%g z#MdtZx#)V~jE{g^BvE+!G;Q(4j03+J4{9ZSv8(X&7E|{X=!-kZQQ8bDUmk1Ln^JkES0 z-rltvy_IgvWL5iR@Y7<|E=RRKn}fT6{RAzN)2?jJ97+?vAdjnffwuQ@((oI!hm0AV zGih6q3-$SkoTM%EAs7nP<2dy=PCfQQ)5FkI+Ltb9dWO23aO#58qisc3u}&LxIYV8} zP?uBG1s~Liqb_mO#YJ7>s7pL`iK8wq>Jmq5+JzmN0d8l)0o27<3O_6(ZhJEtZzW?> zS5d!(K(rWHT!DvtE1Y&4*uDdMaW5M7Cs5#io<9$-2(&MwWjEjlY=%Rbzhm11BN3?g z1GRV=V#7}X^){eB0@UvT^(mm<57e~kj2xiO2kLyFt^;ZxP!|C;d#3|&0oOt(&;$-Q z0{KR8_!trAL>qGMZ>Dvp%ZEW+~=&HBC^BY=&`@|oG z+wTwljas}%Ep}p~iB0hBt7se6F0)>zW}AbLK!f+8@FrK1L0=xWI2Fj#If+5B54?*u z=}x@Dop8WTIAA9numKL(2?uO*{MVhd1$GlpKR4)i0R0nD&_4_Ge*yX@fc{yaUk}%Z zpnu7M{ym_7bp+^t3-s>+Jz6&mJz4>%2iM>cYOtCbJV*^zQ-k}c!D?!-ni@O@4?jW; z9wCMfZs=v4L2vm?#zZXRx)R>F7x`96#H(U`EU&TXE7hLxjxb80w*Tq4%zp8xS3k~jr8-IeV*mAxPS{;BkXDH!WO4w}cJ#tJ`pLQGE zwij;ehfbT|w)d&&dbsUHxNQU6whnGfh1*7IBkuxTs>()6nT9pnV%ITL+!iL#LOZ)1Se~`#>!oxzK1kP;ZAu>!Hyh zaMTEg9kMp@-R9uWu{rVBoMdVuQI7TC%mckVE_S?x<2=*^e~K$}is3qs%T0PFnkoyr zdN{qD*};v_?MdkN5_EeEy6u2&e+JJlp}AH~Rdkg2SU`Kqpuj8|Q18({*p4B}@ zjrH$Fq4h)V*V9IN6));F-r30cF7Lg^xt((d@9*S%AHS!ccR%F2kN4faMEgXSwQUKj5R7;ah!>qtn3f=tEgckHS|)$Dr%Td4Lhh|1vQi&^(%-$&qpf0 z%haZO@UMT!^;Mu_M6|`4krv}gtP`9kIZvTcSOcQ3q>Yrc5l%V-ZBIhmerPNH z?J4N%=q}P|<3Zp3$U`hz*Fp}&PQ=3d7ImA6*Rzz>ua_}C;AX7WD&cpqZrg!e`1^>q z=*13bdkvew-Z{!qF-TLE#B1PFFSCy=63)N?1Ly+>*9*%1xwxGgpDU^D}a=0LXs z=#~RUbHHd47^T09Gb?xysX2tyNCfl{QnM4OIfB%@hs5+FF-P!8r^ER(89TZR2-W*= zAX|v7TMR9SL$^H&y6t}lbp1fL7wGzdt{>>O0A0UhO~jwr3xxe}ovar{Zw8ic88lb{ ze042Rt67pI_C|?ttpT3C}}$v7ziOONsYW;xmrlB5{UI@ZVV@P@t%x7>rR`W`&{L+)9D0L^d) z&2R?Ia0bmFcI-6xV$=|_X&^bV)H#m2igX;HuG^u82d!yB1+hIDP~ix&C|>YpB;=4I zA+&gv8r#qe$FV)9sjp~z=>zz{mKvm+v14e4BevFO5qOONQDSQFUFvE=nSSb;0A(cB zat4YdL6I1zUJ}*Zk9L?YJOTxlF}n6xhwwMoQ{PwdS;hZ*AB=3J?A2hz zg@(d&Ww#~UBA)_=KVXqx2Qx&LgU^7O7oh%gz_kHBeb@FQ)FQtFR^96vBd1Q0=!9Z10%ZdkUYV)aKrO(!=I_c8o1#_ z>aiBAt%n=dg0;2kJtY!VpAJQB4{9ZKNCf_;(0eJ$y^FEYH{wmLK#p!g*V%e+9h5u; ztjB=y*WgoR>_hPRA#mDl>O><2I0df)r$~AVa3;aINx&NGz?uN8>w$FxuxzX9t5 z!1@ZX{tj6Cf%PR|-Jr1M>0d8Vu%80e&p3aDkNF_yL)<^5{$@XzljzP&X8JFKTURoU za~0ZA+BLV)l9qOjZKq=4#8cp0boVjf-wOQO!SQi8@q~+gkKx4A=m?2@N%ZJ6oG7tK ziTa&{6OW>;L>JJ{gMBKc#23*}Cn)uxa^Onz^(uJXZczy*2DD_mM@b^j2PpjzrAzB- zJEiZT^i!06h|-Tc7VR3>WIAUCH5RGJQaN;5bi5}$ zHF=!*a3fJPD0T#|d?xK<+dtTa{}hi5oFtau!Xgl1b#QO{%x_b(Bh;#2ahj)im3FSJ zV{56?E^_-N`gH*O{xwR!#*=e0INsst*ORmjUDQpYZK7W#`fTgh%}CoR>(?!C=?=K`UAS}yT)Lh*Z>7##sPjJRyoEZ;jzBY+NwExm zpe+sc4*;E5XR(GS9KMsfhuYMyDl~R``Y=>K0|X}2mUgtn{r13X0r2uZcu4?)GgwAf zlpPY@6CAP22-bH1-)s(g@N&+%wC}Ef3iG(m=emlP^p{A#g;V^O?_wYCLErok8!54y zey$(#n>}Q)1Mgr57&#g|2z&=!vH0Y1>X#3IEdv;(^<=0l&|1bW%puBmIpmtU7E~Pi(GicB2IahMpo^`!(3-8}bT!t9~^qWX;d5R_dOk;qi7n*6W4iUYn zr1z1IhG%?0qO|}`l8En?WVE1NTF@>XK$HkXEznIg;WT)(8citsn9Lz2ProFP?ExRB zfou=>I1N-qKotX2F+ddqRBgtWxZc8fD>}CVj837{sX#ZKmc|0=%le^|T1KhtB*V!V zVoJ@W)EG)_qEyDyQo|DXbq6vip5zMn_4mfj_yu=C^*nMaAW~F>UMPVheDJOGY_V&) zRm0i9*@y*hLPn=iU+G()g&#E=8P9^pJaB{;EtO4-Cnxw6Joqj=_%b|*|H{4eGgndi zh*la1HamS0hk#%|5bU>mtI*w5=)h_;R1J2s4%#=MnPi+o6W3|@wbKO>a4}o(fR;-7 zgqAuNoIC(dL^I<%1a||`M?iBDT=avBk0>=@N!;4*y+1|Nawm8Y2#A_Vv_r6se$?m^ zx+ez?%|l0OTn5To!084~xB6zIT4%7ag|Zn@K+C=ePPgfC2u$n(6VHH&XTZb`F!2m9 zZ3hz@lq04A&n)^cW*ZG~#6Qj&mqIszb}!uWKG-=8PsRY{TR?dND0cwmyFhsWOfl{O z%vAw>HMFbYSshR{aJ6YB@!pT1+q+laS{-xK|6`n?Lxmud+UVilzgm-XzWHH z7~K;9$3W2!6gz-o04QDu3h)KSt4UYRL5>@^ZzRp`McfS?0}4$aR(KX%G+XV;Kh`@C z3n94Mf?OwoyRG2uMeueEh<5|=9w6T3@OnRZ+XKYXBefH}y$-~?!P^_k>)Pi@zu7dn zV>(So?D&nqeh9clDvkj+y=YkF3NSAHg*8Y@9W-xHN$CgX(`ZT; z@E!oxRC0F1l@{_TzL$l5iUr02$C_E_CktD`_#ku{>#YD|1~4*$0p7?XEgzmJ0PaHU zgh*1d!YZ60k*8OY-caP}9{BVG@TLLpdT_lPm|p?reqg50o@*68Rdw(uz%i6;n4skY2PxipKGM)SMaVx2v?&GA43cL8`|(!Xo81VuPQ+^UDA?5<~wZS`C>+d;5@-MrbkxjnM}!r=y*; zCt;BGV&sTM<E2K(q@^IRjL00Tt00C~te`5>b+vSr%3- z4KB%mlQ)Cm?O=E}7~TuCZ-C+5V0bGSJ_d&00K;3=+I>X*UqtTI=K)IO4ent!5OhDS z*zSa1d(m5y!Oavjs*RUjU|eE-;`c}dSbUy-4-UCm$aAh2LItHGk|L8FIISGDTj>zS-qX}#~ zdx$#YeTH+FQ82dE5W6)R{Y6(H3-G~U`u|}B`s4QkO~~&(A08;v`q?~ioH|M$fEP@= zjJxm`ZETB13h}@Im>vMr!UF?}?I~#XL@+3Q66~`FE~G8MD0OWMQXf)Zj%6ywGI@YlER&noPK?HT4g>K3l#T;JvGd1) zZa+{R0gpmq!66dP{S<7p#3Pd7<`g(Pl^97H_vxf*UnSQ}sOjYtn{Bsb&k$?2Bi$R| zDb?(%e{Icn(vnep4NyfaS-1}xoY*5xi6E#2j-$jlN~GsgrII!>@C&c>1OJEcN*cUU z1;#yK{6k|Ey`Fa=r3HArHnwRc5b= z5wSn4Yzb7yfa(zVItgTA`DJ97v@E2RC1cz6!KLU}jk;iqpp^7u+Yv`v6y~>s@0)j` zKeLG}v0^HIPba^+SqWtZGCni-C1BYIA0Fq;O}xq2DTn&067M2Hz}kRt_))9|<1x{= zIh2=&Hf3)Fa2p_gWRGl+udLGFAFj_?7Nym-5t1IYLG*JT$av(Sj3x)>5p5=&byz%j$Y5XioJfn#Jjg} z-pW3k35M59RAD zMBVHl>NblxOE@n;!$jmIUoAj}F&_F!FJcY#kuko-$Z)5|!ApTMNu(EANX*WO7!s|q z+o-3(e+KwZM7yLx`H;6Qqd5}5eJXJX$+43N!AwTvUI(@oiv`1HSOum32U_`y@QF=r z={M_#+R~RS<1PB(Q=#@jcvU1w;%B*(mPTnFN=rkIi=np1)}TN^FBck=elRee2(}Zc zbq?HGMXklV@S!E@(O0e1yc`W#IIrZqiZho{ z9BItH&0+Sf+lpbtXDnwNXFMl6+ggd7M6oT__^?tq;R!2ETQ2nu_Qn6o2aX)zs0R)o za7=~@Rlv~#B<;|klesWmoMYIDbq{m*_Hypy+|QT`X5LV0Ii;3UYB{CmQfe-x=2B`d zrRGv$9= zEaS^EzAWR*GQKS1%d$bxmhg4f2+)=QZ3)np0Bs4mWszGJxn+@C7P)1STNb%xX>Msw z-34mNEe%-GfF%uB(ni4e=()ur^)DmkFC*nIBjqn6<&5c~z8*Nc40*978-6DmHU;e| z5rxgfv;=EO=u;Upbb{z>A`#aUL|YT-%}V5rwe(%Ejs}|bG*20;!*}=MD?HD=?8Vc; z_ZeumWyo(WvRj4h)*`pL$ZR$8+Jc<6qSMcy)Ayj$583__c2UQHlUBni7N>!0Bhg+N zH>=()BNeCN3)t;puQd;TU@j>m!HTV`ITzCVv^dC&B5djkJeZqklio@z>bBsYfbtdk zW%fAzMf-^$Zz5iMkVySOBIKKhmDVy+;xOYq?7pPPnAt-_%-^AYcJ!%)Hw#q~WGrx! z>L}4oNFMfF=2d*CuuF`w4?NC-S7tL-_;UQZc}6$zb^~uW@OIO)RtB6h(?iBi7tp&s z4%~X^7m{6}g$h&1aRqVGJHYjQisKY;oWk4bP+|{n$MSY8+PjceQYW;TLZ9dc_ zC;2?7;>m|RDOZemNy+C|BV&&6E`VYU^2?~u;GXNfomhrnu33{I)Pvca|osd9{__AA6^0W;+Q$HgBd7KGXv$*%$(Q{wKsvm zUh786SP}ds)Rxg#JHh5eDE)mXy(K(u;CW_ItpkTI(>~t7$Y^`~*keS&AERgMRj{-P z-&4nUgQ0HXqdxdjBDS6IR4;zUWUf<)d-U;4;zySw6GX#-Mn>t$xcEbqNRJDUhWbD@ z!?*Sri7k{OBX0I!{R*)Hozz7}cou<0ADj^$-AN8Qx)bT^B=^a1hs<)a$N5Uc?-;m{ z`B;Z2`2~6*(!fC~^*?UiM1Hq&-bO3)4$@>qr`;nab2OYDF}^)Tf131&$@(t|@Io%U zkOeP@d}hE4GPlqV)qe}se~U#g#DWctJkvMKTOO=eBw~C%{kn0KTTQv5530$jn)3Q7 zZv!4PBPA3^#eyezdphg;Xx}tEwb+n|n)Jl}nHGXX)vCie5qU$)Bsp3}a?K;xC&~3m z>|h4=C&Ot;NxxSHc1OlHNzZB$Uh<2y|JLKhz39qjB(Hkd2MkL?80-OzqIycOz?mA=?jK>O|BuX+2| zK=_Z1oQiL|gPeB&Gd+CZnOQ@m?ILX#zen@*{6M`2{LB28w}5yX5bse>SbC6`g8yZ~ z=M{3|C{bFZm=cP?c`;?YMHwpyEB9%I>VH@~<8+^Y3zGXbw z8{qo|@GZT=o51%rh4>wEdq?^31Tcvoy%%gsAIzYhV0iKTdv`@s9jBI&p4F9b7jyyo z!9Aqs+B=%>!JK8(P znH}wweqUUCRj=9Bn(cMBc`I5w$Mdwv^kkba)iuV(re8efk}Fm$zoxybaja)j{xugZ zy`d|xyKVA<6-_ObEv<8=&ze==++JNWu71ol^8z2upWaY;VPB@-FEnWaXBIf48piEw za0|-}pTd(RSP$bYNl=s&g|l=?NRLX0k%X8iBxXti4K%@O43*1Lgqd8Ew16bi=xqZD zsa#`7Yi_9s*PFWLv~@t`DyglaKr8%jvF@4ei*LALar;bn?DFNSTBlBJ9W!<6+{X61 zZomDmwx+oQCogTOez>~jf}SgTE|40|qK0LRo*c1;Dd8GAG%pG>L7c!J7gfXP3Mt>D z$Qv{~*NL1e<*2@Q*lDQn=b66MH{G!3I%G;<3L&^5GaCJv9;xO7xrA;b750e{Y2^dcDT%@!IwwpGKZ-mG#Cc_Fd9VI?h|pC`}{e~ zY#k_!W-&|fZ<*k~fH4gJCKO=ulrlhJn7f8mWGEFFec@1nIS$B#55M25nfRK}8V`|x z`iM2&;pPyhY8a*?X#yH1)@b6#1bK3`&{UL5uEa=`>moEQwu;O$Zg1y*ScfAPs9fBQ=WdN5YqpKoth%;1;lah_QiFN&qb2aB$ z&JCQgLe_=ce4f)<@q{r^v(4lx{Dr8B4iRbX?CKC>(bdt2x#(V6 z&TP$&Z?CK~-5=4`p-Qg>-%ab0s_a@P3jy2P05*&P>OmyzbLn&-{Apkel+ zg!nvLi)X_fhnx&}`}o&K@)LcLm_B24vnr7>4;G$>Arp@Az*gB(G5P81Ca zC801X!7T}loEd>iPvA+4L}B0^cf$>Zpd4w8qc)1Qz#P(;sYzNsSGS_K!dB5;qWrM9 zA(QNwrXkp~Zdo<$+T!tTmrR;ESb;khD(A*qYf7hgPM>^XWBYf$`ql4XssgiGs(&k% zYOcsnh&Mfq2ptFKyt;;@8O8+)o(p_5!kcoP7RJjk-pm&?_ zqr;e{B}}wgbfU6ot;3?V#zcMS+p3)CgA-Nt2*3K^S0DW9gI|5{E8`1bZu&yOVJ27G zjmQVDtj8yIWmINGVzY;^sroo!tDhCcv{7u@RoT>X!{X+u2QDbQVM%$Z!=r%{4sX65 zV%9-ST2t#VY0B*KwlIGlituINPd2ZLMq}KD`4Jk;Yb+IJ$u@F8M@XZ6yvN`ba(JV- z($RTgk3qE88nhxYXEH{Qg}no}-`(y5oNtmWbf)OKILDDbE` z{-2Yxec6GZlLKbfc;4EBFXc8a5*tLIRY~U#b4!{)lNQAs!xV0`6s5sw2Htj-uM+j)sg^V-4{FO&v{@TCI5E_oX`d|MN_~Xf04~F>61zZV6#~Mru&A}+R z92X~onuzZp4yxF{HrfifGHu?cy1SOo7}I8babP7gc!YmlHFzd9#*f9~)EaeB`I-W} z8C74WHp7S#Nr;r}G@VK%kJ2cNM6-M%vhP<8kKt-JlQKwS)+St^%GKt20lcl(rFzwA z<-q;4dX)V`asd7ka>^QtJ1)&~rCmH>*4*Wdt=`I_%;nd5s){qMgt?ybg7o&z`d(W! zddAzb5fPHGJkygS@|-?Io`>PXk&m-fKe`1VehGd)HH+aYEdXh%O3mzsTUek6%QJvG z(%kbUAkGQ=0!|W-G7-8-b4-1DpW!FN#|=+dg3jVmK0#DL1V+4jX2uTk>5DC|C(Rnt zsLJo0-|O@B&hHFeyQfa=<_tT1A=m!OY18ISgrw zKWVU?a`9_racI$AVdfX7+!yC{gZ|{c-lDom)2xIg)jikO)&$so$TUi6z5Wp{sx$sc zoHW}53_+1C`XxI`M@4I(Z1`J3CSsb42pEZgNIO={g{|J>Nn3;NVK$iF<@25hQgcfV zZSSsPB(syYPm?Krb99pz2f4%)=Q+(f`l%%qeMQronw!dMnlHYpux)x>-+ccR&!pN0 z)+lVAJG-d!qLlW=iTI}v(8fHdhHqd>z*gaFx~ zS=8p;HfHQ4^#$JRmM>pAuG8$DpIdO%cjgBk6kBltbT|$-igg#^2mwuUm8yQA1A&f}8!7BNrSy{d%a?eo zOR|ezAZgaJjm~9wp z*E7GnVRlDc$0eWr-&VqbM;|?)r9m@SETu6POdQ)0eZUABh7r)wZIL&eAV&mI(I(Ti z;c+7!_aTvM9M?>BBW&;-ApK3IKi%}_FY9P>bxyc$VwbD2bLpM8Os%zQrhU;$c=P$^ z-;{IUwpfW+2kX~+KF}sn+;4CNjmJIgN!5(BybBVXu5(a{@=qy8`c`4E{g3} zK5mqesZ?FAIboO`h$1jYGrXv)8f|S#ZlVq!H%gksrxat7$hAb%yu8Oq6yVG%BMgpS zj`tS3uW#NS+c0H&R5rXOs`bqPb8|XmB$$NC77oid2VMS;F;^o{nLdALM8OC8LRe zQsUGOZrfUI2eHm{qSAGyd+v!T%V*Zs&Rjm_#EDr87tZ>IwWj`xJFeh={gUgh{rt7p z1w@8v2@dS7vHjG1N$cRC6mw7|~=9WJ2FvXyh9RnC_gwRlz*A;P;5{uhut7={PWcj? zQf1t_26m}7YGBz=&7D?vq~^mUwo0^EmEDFCrNN?15GBnakC;t66fBMm9jhu1e$Z2{ zC}VhaEzs$ShUluf7_NmU6@yix{VbL;oSxZ^xqQIRFm4)+?jUS1A8BArTYLU7Q z6O-rwh&mQdfC}>>6=e>iR?n1R)K5v!F0-KANGDplof2zY%gv3PPT<=lm@7Y|IV_A~ zmJvRYZbGTBEAZTk6=n}-b6|tDCh(?NInc-J-H`6HS`#E_D8CNVvl020HnR{Y*5O(S zYoPWJyhPRWl!>AjXRG_#Yt80`3l~_{f&~MC1$@VB0(@QR0GS}|{ILJ0sK^pN$TA|> zi}s*ckAyO_C^OTpqFYp7#EGR)^N+<{vzxEFKJBxKZH*0`JE?0-VDU9qyZb`bqAcZFGq zv3hWcQ&-BsF~&-{@QS1RBSx!Qq^{B34@w}1o7ELc)f$SfCn!tjw1l_fHrN4LiT#-V z_++z+HmJJ|n<{*GLu}U%f7lg!!#AFcd-AI{SlKHAd(E5`1N%V2|7$LWB^`)Gx>Cgl zXOo>l)EUN;j%yZyco=n}b&Xhj3kU`B1cA_jE(iMHd}q#d-EiAZ`D1?VdkWQU-?Op- z%ToRJUG!TP7FwebA$vb88W8~{T3#4Q3hzp*wnQlv-5X^`BSby5H^R@-8|5js%q3HA zoLyHp`^KrEYxR_-{QRaV)ismlIyps@)VzPWsws3`TG(+>^XHl`>L?7~ICgR-_+-@~ z<1=C|y+*bwm=s0AIdGRqwls8XZv^e*hKYU!(bBb`}}l@bSq2xXo$FQxNZ z+>!c4%a$#A@{zjc=DJ6$HD6e`@C$)`=07*oHrF;FlHm9H^Jknc zd0NU%GpAUd9EC#S-7=m7m%;B=M>US{uPfBYUuSw>SYb9?bHlVL-@on7Z-=;H#i9!r zdoWD-*D613LlekYmU8$(qasBkM@1qyib&hzh8-0VlnjlENSXw_M~{l&rJiF{#H^Zk zOZ}{h`D3O`Z64qI=}-G7T-iPQ=9<|Bv&QyJ?i@o9);H;zl!=`eG&J_K)ikFhXDyu5 zcwt*%S9_nowz;vsE;-e+a8Ap`V@gEt<-!{^@J75bUc3r>Tu?|lhv_|sMU*2VR@e}o zhQ1Z+YtCP>!E#%32OhNME>X|$Qt&C<=Qa9|H!##^PSgP~g(}S6(^a{B2;#%+Fs?UVg#nXPWm>+e~VE9k84& zei4Dii9$ybqKWOqFQj3c)#f_!{>mGt#7w>M3wtR0ful#w`ReI_T^CL}bsr&uG0d_K zE6=ei5del6#=#IhZe$?QNvNNAu33*f8I2`#npY;ZI{wCzu?B- zd}V&S)o~S&uimrAyg0CW_ippzfj;y4O`8JW2Wwuiwv1Y69D@eIAlo<$Xh%szfR0v_ zYRw~hMVtdhH6yk{lXxRm1-J~cb@>Tqi2Zh1%f9|A*8^YP{^r+zYW=S-Z#A>g-b>BP z182;bz;A#q8R(={oM>D*JSH_vr#YTo1iAwjR3g=tuw}?QDBy0; z_#;fyaJyvIPg>YpT->{G(${Xe?{i3fRt!>+l)J;ue9Cha3Cg#0?$@{63ExkZ_m0WWP}N7zy!Il4-Iw;W^L$Z zwcLn>$fB;9ymqxwf)iDE~twL!7J*BqQ0Np*ghOI!t>+^QYa~ z6IB+oK^ko_>So~Wha(h&+J5m>UDE3}@41BknD+2*9=yg%`mf$U4*WveUSoiv2uUfE z2#J>g6VeqCvU0<0P_%SPG)$yZgxc%$q)J3gB6n->s#!;ctW;XNdQEXT1#gTQrBl@o z=%kxvR#BkxosKc%+l%T-y>W|^E62|6=$JF6IKOec?-%C68(wI>qB*r~th=~6r?PB( zb74u>MGdtxCXOxsS74`?hOxElr~@sN47ME_7X`ax5F+ZVLYlG+qmeMB^!U=00nRS2 z6-xOoaA#;5(RHIs(<*pRhDKq4Bx%Bd~M%59ob zTh>{V9~T=}P&ckpw1wB-kUPoi&r2KM8Q;{lfJ71q7k8M#N`9}5b~Zl~_=8#Sy=$)x z{BCFbHy4?&1*+zM^F8wm&}JgEDFwP(<7;c^;IcSNKq3NRfrx!Sp&{({?6o% z9dV#i0x3lbUxEaUFD8NQgVe{hBTg=UNyJIU&avI5-c>nw5U(s=`|@qJ6Tc{mSUQk7 ztIRwTcri0RCEr(H+%R)YY1zc9$2HC9s>!%CwXnRQsCUYg-iFSstMkn}t$8QBWx1)v z-Iuhr&zrzlSs{!sKW%DHO3zfEH`V+)gkgOCG4_BIIcP8zitcJOnzRC?=ccplShLJ1 zD%H=jrFEAf?Fsz)Dn$xY2K8Y?S#`Cgx#Dvo0yd;ey>aYS6MT&`=GXoExNG{FQsa}0 zs#?qHFYGGuPrRyY{M4!AIq6|O@iBAV}c^d2dj#$Y!2ht89fxy0ueZ#K-mcl z%>-fWabeByz8&{ZBvk^YVj1ka#|;^w*TokhbUPy(C4TNSSna;q!AQ>v>kSF6oAu+b zo=`S(Lg%ECv2!~+=Z-Cz)Hz{h*@UadPnb4sf}CT=j~~mKQquXUy1vWZ30ar+RNLW& z>Yhup65N;f)zK|*)!24V`&j*BPH$~?H@E5^u-XAtO2Lls-L-45_F=vQF9l(|6o>?} zH*L7jCKA`bOm|?_9dOZBSuC6NSISmDQi>>%Ub+$@JX1Oc7 zg&mJOd}lri)6FZ4X1dv}?elv5@Z%4o_|9cafd0&7qxm`3m}dN0`g;0|=}Ld;<7t2g z^Mzpr!6g3W)19bKb4kSyYinv z61ww+->Zhr$~2d1P-qY5tR+pNVM67%8jEZ+GUS?Cq)b@ zqbf(^47AF z+MfE3+KIUjRyIwZo?4WbQQSVgrea)EMTY;x#*xdZj<8Ju=f(1XivA5~k?=0-PdTM1$ zbxm_Y$0ePemvj^~luiw7E6cy)lB!A9Ur^n0`O0aneK%d!_(Eg(7fb898fG?hp__Y+ zQ|4vnXT>uUK4#lI>TgJI1{$=kxupx;+G~1G-FTzyR_?rQ#Tg9w<(+cVw5rW(`8p@+ z>nC}Fm%zw!0u6j$ZpE19HR40c`+G)sA8(4Mk@uf--Y*VbY-KUCQuCJ%7KQFf@{V)f z&msR}^TX9-Ps$8t5x!}Bn^v|@;2GS?jwp0{CKiKepiXFP=yxm1%PTljs*2K!s`QW4 zm>gO6_uDYcEHbPzW-%wsz!RKh)$9K4L}m<`y~1;vPoy(X@moMr6dxV@m3VY-ywM-3<$L+Q7Kg{@I(MNILuJEVlmuJr` zsY%Mu^_zb!FLdX&w>D;$j4O}z=VTU^Q%;Tf4*OCbq#VWO;Bp+gYB@=iBX(WOQf#W> z38S;pmquB$N~@Fe^L*C9@*;O`o0Qe#kM-qb7L`+1>SVsGbtUFYUFRBaD5ZhH{2ee< zvTB5+Utw%f9Fzy|G&UJO2DYNXI6#+0gJTnS5IA=Z0_VpgK|}85Z^*sUxKc~K(kYc_ zg=w+UL7d^1PN#SyLFASmoF>?)s-r6>K4<$=PnF(&`;?zw_w&B*d`GC&#y1(EZ_L5p zOfajua=h_vx8Gj+)KlO3&UgCwO!!~Qd5>=@+4o&)y?`k@k}i22()6n}{c1@^$6pkj zWo$zpV!=miq?eG!0aUTn(uQ<=^x&{K=w`tmIUU5Syskl#c~x1fzo*jgujG9A1+y;j z*B7joiX!*_q3uoJ8@tN0aosE1^1jRRCfl-X$&xHvvTV!yzRuVl+cV>3_BE4PoLR_5 zmdQRzNJ2sggg{_u+4=?k31ulE&?YSuQfSFQn|}RDAkz>6G(bX|LLiL%f1Y#hrqP79 zzkc5&$M@>h)xGDw=RNOv_v3C9f8P^ zcO-S;2Jh9L3atyzDxq(FGx7XCB=^9xKBD{s-n$xl%n<27&)#$=y>kQQhXxsV=Eeha z&`2bCSYa4U!q8WS_sNwcj-JRx8|oqw*zC!Oc{4iSRZ|-qs=s_;xvRCUFK2g_w{t_C z*3ZAFj!id*C);W>cAQP?@5^tA^@M%3J>e>~-_mu}Rsyf?Hy|ak=dk{R{=oVA4TCz@lNp$UA1R6sG-?AoXMDe%=#)3 z{RLy%JAFU-P0Tm^iRG0*>4WQ6D=X@oP=c_kVcP)_#Sbb-2kjMOhLj+fg({G6*SWkc z_Q9&iub>vj{eD|p%yBq6vU3mucX#Gvxy-2^i z9=(FQ7vb(1xVtXxinZS#tFbFZZ+rX3-P-MU`NZwv^54a-jf?qh4E=2H$?;Yk}UVh#*EQ_wPT zq}Q%kJcB*0l|2>n%IED6lE2J()54*U$>;prrq*~c7;kObwYi|G!RbU${N{H>1LvE= zJHw&#f#|}keY{Y_E!F50wfE0?UYhROmU3f&8#d(D^V9+>RQZfgZl4PT>^TW->2jEN?+pDU5ZoQ zKv{?FN1;#N4o;r@L{rN#1*1sRi(C9a`Rz&~lRX%OPESUbA4V~fFwHCe;#!yhoRYu(ne!@zH(i$YqV(m==9;Rd5f z8?6&(RAnnQD`y?B%p5MCxS|ecs0@m6j^QoAOGUP!v|3}XillVqlF%O4?vaDb8zq8gKVRRx(?z0#5ETIFr$IHbXyYcdZ7*doRpz!rZmTLHQ zKY(Ih7KFYe+9t^28X=yr0voO1{}TYL@y&i zd7nUj2#{eKZ2p_57omp~(uH$DAEy6gK$PeH+oLmj@n|WwTfsk31s;@c)r~L&g6|2g zTXgGA@Y~;GR|U(}4oZSLzm=v_A%pODXM@#uw@_?ME*(E2`P zW7ASSW4n$K+q1Sgf$3u4Z<6E9HB4}hdX87Uw9L@%fFg^S2K6`#2nz&DwNYPsN+ee^ zrROOWkket6fDQ}T2EgREaXfDWV6ZVH$7ktD1{O<}o?dU{8Xyj%M)AulWyWr;QU>#b za=s=$(-PU`>#i8^MZ2S|~A@NGnQ`Fkq*n<72Y$oS@oc&9fo+!7zlEX=H^ z#<8HNqB_&xA9A_tCtMYQhI(IqscWINWy+P~8VURQgJsp;@v=%}F8K0G%I703(`DJN z(eHVq1tlt~J4b80k;39MM`;w(E3|%8T>u{}vGozFh6YihW7C{LG6Mlf#u7#uh_jLP zBp8ofqU-pKkrV&D5uA?K*2brU%FdD1!>dZAr+Z^4w6WW>6W<=pRxOedFXR>vkmZD49xV#p}QvQr~kaY-dnpX}dOtU`$$jxXB zm9E8Q5!yskkI~bxrvj zN@jD~cAnY1`OMBX{n#Iv+}AzSy>BvLob0(`=guoSoBfrwQU9br>Z$YxVQ)h9J0+-m z0dt9XkHcJamY5$BRBvLQT#6&mb%_z6(2|1%B?Nk*)9(hDf?CU;!l#gyT3kwFdo2Jo zC)vcs&HRjupR_e&Z5j};K^&R9_@r$Ru(*IRhhO@R$QnDW#HD$?E`eVr#E=uhz~@I; z4%i`ra(*71?yRltoDS+oPglU@3Uqn2qdm6U+u$r}?ee>Q6%{@uSlhL+xp`w(t$qx- z0-bf0wQ>F`URzn$8E|!fx6xfu>zJSNl@*lrk~6uLvuXr0$N`B8oC#@7+8R)X?wc|G zT1ch2vj4$QMV1>-fety_=se0eC&zvFEmpWk$1!(jTrL{EC!{Crxva`q?f?LSefuDdSr;}a_nKfH1Rj|wUC%Ju3q$e<6{ zK0(n{j|QahYI=#aDm!=!im-X0flM5OVhDv}={Nv4p{X!HQqf`5e51G!JJyL%O-3-G zzNPp{e(SIUky!ay2J#WomCpbyI@n?LLU1R`=;Vv*DBHVn7a2ee&nRRS*cC6Ts3ZJP zA}O zgR4vQnoCAn?)-6Ba{Ms|O+XI+$7YV4bwA?R?Rcc-qeo_TDPtYCw=DO4;uC$#Ew^{v z%f2=ti|Gw$7&*+vuyLR;=D4#RDx z@QlSK3NSOtRG5h7BIB_1icI83U&2514ZSIVD59(?H*`CHYRDqj+lLwu; zs&G5AGLDv#Vsg8bU;;D5TtlViP!nQyI?yd+&MDQAk5G`~o6uXYf0xbag-yfcD*uEy zVak})dgzyzssdSL4bJHhp}DOV$1*gk3>CmYqu1e$0|}zV9N?Y~AVD;53Am_o=SMuV zo-1#0UwMmX#$!1T*L8GEbery_l#X-q@>@f^nt5T;$KGt&_4GV*nW(00jJYGRW znx83JfB&iUPcDA@z_*n;8uLQR@x*)Z;~v{fYK?LWc+WJ|Rmpp7UjdC|fGk<1hRbxu z3ye|&1Vjp7a29>RGHM%CooWN-iQh6$z5-g|zJTl>Db4|Wlj5@R!f{fZ zoCD6co_uoOlTQwQWAz*OpLbKzRj0BGzfAU=n4S%KPDYjQDe0@sup0$EI#J&=2ZXSJ z&qrmUi+Hn{9%bzm)1!zSpbtXhAL?Q{9w@t+ct!c%r=DE>@|Rbi{1l$fyo=4qBu9;X z={GQVgms*eQ3!-1fC%hI@E)c4{X-8wJoJ9?_m>|UeDu-54~f6@&5xpntW)j68TX1p zJ_+m7N&d}H6C2RTWAQmqfQ}PM0W_KF1@($?UO~44x}I!DOtzE)k3Vbjc$zGYc>R98 z%jK#Uf0R4SbD$di`yj^WZ_qzz?U&HzzJd;Cz#5VkGW#b)K?GQFZLh(1%1+rcA{ISJ zv|uz(tgikePzp}pW4jjg{&Ur5yTx`C_qxUQe!MB^46zhy2y~CC7btirv#iY_{R*xH zhrpA?^Hlk~7tv?2`o6{!_MjGh7bhsltAC|I&f^B+t0A5+ko-Jus6lW7nT8Q#IT!@; z5%QOdx#=JivcO1?zNj$~kK@|daIecR|AOQ5ClZSZj`AMd?oD+W2+Faw(_0}aTfVl?*^QORRKpl_#Q8*z zT?|88#BSm#?CB4c`o!hR2Ub^=U*42)CWsPjj^UwlMobDu#6F z{as_MuArbU*2v?)CyHx>6}1(?+G26iHbTs|;g1jNYdY>R$-qV(Cpo~$AMn_i!yx%A zvw*oSFl)(U$LeAS1M)gtO4|3nH~+owHQs-J#{KszJC&V@k0m~K`G4Ra-zW_KEx9jh zvhy`Cohb8|a}12E0}*UR%2SDVC_kP1(rV&${K|zs524RFwndXxbQw%r=gqw86urmV zbty6lu8huSpz|?1CKQt7843Td@7(su_JNF{!~4F40&vg#;up#(*kaKG%1w7eZptIa zq?6s6|CS=7X%04#V2+mr$x^zI?il1v$%=EL8d3ALLt}f{j;rT-r&cdSdZ#caifxy{ zTmL%j{w)YUr!Yk8T1Ivkh*D9bG~+3PXw#ixh1ZMYx=lg%vcZxxntc;E~ zZ`fUtp0m^$n&3x-K*^7-BLzi6t^U?leZ&X9SgLae)5naX@GXuzDSP!(CAErcTF3aGeSg)uqdmzPB1Ftl@ zL7aq_A_JeLB-qFaugfV((Na!NBTD2^nsNSB&0^=as~g%%yQjBzOkT4RjjmiXIdSbu zG*P0=;JePnngbPei=)-lUt+uN*t~dVS1h*c%wqeqEO_-S-Us_vDgUanvTKB~aYeSM ztxa@?HZPy?KAPVzMe+xemhL^Dki61>4msBJ^~ky=*x5h}u&mI&Wt69c(VB$t3q%2+ zG;F*`CYQ^^c)h$Myq!=8rPolYpWTsNozvoKD=rWA)X&aWb!=+))%)5iw&XWP2ijcq z?Y%X*JLfVpOB@^XJ^4yad}E7q$JWTo)-B;sXSlt+g~kx|!-4pV9mfaqLk(EJ;orCa z6eCJ~*AJ5p>M0g!xk}RVM?VI!K}zSb$-qiVR*4puY7To!l7y8G#42SL5@5z0HV6Us zZ-BACwVb9T3inPtQ@4RI44s0mM<@pYTp`O11fa(@0S5WnqEahJ+?1)2NLDE_Lz1ar zJFsTi$EUY#n@0Qnvp0TYv;D~F?`*WIXBvXR2DIafPdT!E^XBbriT5L?<^E5qF1R?} z`N)M|DvyU6+Z#jT&o`!~$jjR?Gs?gh8N7QLS-M^akx8312F|p4eWJGLC}o6 z&J8=;KjB~55dBrp*jNvmQZRLNqP}r#Z(sMW(FT9L>YE%Hs4kyAba&#ViSeQF3Gs(- zSzwE+J5ep3@sc-j)iB~E`G)i~QYdc`Q;6O@`WO9TZrJgSWEGU6q=AbbM}`hEp-&nl zXz0oYZ$xEz4ML-H86c<*r2XCvNF}%=^%_XpFt$?K7N#J|+^`H%=g+?=%1r4Dj92a* z*k4z-e_(Ircp&*x=fFTG+Nsjwz5V;U=H|Ng_wOw(P5!i{zpszAa`6Y4XkLFe?S~SQVCWfexjM(1c1U3RTfI3^d{54C#(JH1+1yr zO3L9?G}emX30X#zu@f~4?a=-ra2QnobvW&%Wv(I=6Mc32fdku@2j-oNxuIH5v|z4m z>)`(iHHVr{9Vxi?_=$V-k6fNP6bZYV9ElHR1nVNvVaMglblX&}vM+ZshJ}|k6K26V ze6~+<*k#3BT@o-o33vgE%|g7Dd<#YsyRcqLj_Hy{WEX=I({0b;n-|fv>Hsd7iTQ!n zDg{5o7BQN6nenIPHuVjN@0>TBCRMnEt1Pj?XLtZ%g;1;QUU%G3Y^Qm~SZb8*qsL|f zfth2YJFf6-ac+!{F6Y1T+uy#CzdYJK=h{*b>Yd4L-Faqt`OJ>UTz_|@bAEJoX?A?B zC)Vrk@IyFK5E*ox@?CHOr){3R)TlP()FpFMDEbNn45p({ab|3#6i`@nPx`T_{n7Kn zDN_iNcg0G)@mPF7=~W!CSvb7*d?lo0-R`+5`*dl>)Yz5T@2Yq>>)~pgu7#EHozL`c zSpE6V#49};(61;aa1i4aWOh&k*c2mN^zlkz-KL=S6-rC6HwazJh7UqVvH`*%7iJw) z0MF7mO>0XgI$Mt;N1#bWfX5K_0&Ail!kr$gs}BIoi!R__nDZgoZwxf3>i)5;LRK-J zTWi={IojGgoS%8_bM{uZvilvylffKk_I7u#w`De7)7RbWYZ@G=ja6XtRd~o@F?C?fL1QI~O=!E(j-%a# zc0bx#v?tJ>MtctJMYPw@*3k;7Ko;OcImH7SY{Dv> zc5=7?QMfkSN6|iu_6*uNwDV}Mqd90+2NEf)4(H)!1-Kc1Qh=Wn;3oz63F;N{(Pwdj zaY@II3-DuP_0b15gp+ys!GRmXV2W0UX2-1=nbIowZx&x$Ac%#l3SU5Hd1)a#shcny1fP?whWijvU)_ zWc${k-QEpl{h{8uXxZVtdz*WsorzyWW0Cg7rHp~_h_5B%yldyo{I2Zf<&5@lzuWKl zyS#>QuYciK?ut_Bix!;A>{1Q{L-~K2Vk${z3F08$U^cQ~SQ=f@8%kpX4VhFIC_gy4+&R6%l!i&9rI1SArJOkVna_OY z#xLA_HtmDgeg1~eeCFPJ@15N;s|+RnQ3)oV!6}51rS(s%%a{|i&tk#Zo+EK`R)XnYW(63{HaoQ{NrqaI%(jcwbF@6W^olKQ4P3JIQmTH_IM|vWgaSY-}k7PRVUL;JI((9-dtqLjj4zCjh zxrP5xJJ<5rl(nsY^;cWnTX&Ddt2zP|^qKU`Z~tWJ!b0iB?Vt37+S)>%tw+=c4jtMt zJ35?+Jx;t`O->ws75rUv}mL`~&g_H57;BTkE1&44#mbM4&1lrAL51@eyUW&z`HU&CQ zK9tAKpT+cj0_|zE=g?k6dkt+JO?18hCwLBdT^qNyFE!b^110Mim zgbRZUX(uWkI$~`um>)GM&n~4r6c;S9m${gHYcF+`K-yCV^ISRE~k&VRF*`29fgaPduFo6%G%2-+cQfG{v_HQj@Xqnb;mmz&R+ZAXUcYLu`eGg z>I=kO4Y>(lPR!RG+myAeKAyes;!BsWX=EThJAJm)@0@KJFUsJ2 zFT`r|Evz==)n428DO)#aNw?(9oQPgSpZp*B-e^Tvb;)iwu|=CXWCEN>;=@b^+LUtu z?-l}|_+CnW6jQUs_gtQk3*#=l!``u{H5f`7Mt{1&@1V!!)mKl=%mwjclGCqKsFCraBde|h#d)KHh8 zmHYxK{EBb5EdwCebj{rb<@Kv7lnmkFgLB|tF7aI%<;=T1>uNFmsI8RXf8qf}eE zV0jo7uuM8F#KIE8by9w3Y9KcG!!MXCP8swmNIz9(X=yEvMW`PgD4!(D*fP>j3m%e@ zWwJ_QJ*wP#i?kF;guE<0yYoP6>z1-Muct9Br?{Xb=-o8x?yB_X*Oa*2m6b)#nvzUs zcrrS+t7iMs488U<>Qk5P99(fd77Dm4vx`uHti(U;@0-dk+K`=H?ke+C7C7tcyw2JV ze_8wR*AsuQyeklF4hFu5Wx5f>`KQ2aEzU5ll}yIe>5_FTuuCfqj~?w9_-|0DR*crP zi=?=WYdJL7nU$M%{NE*8x`#=+RM}XKk$O7Ps>JMN>TV&}8iNlY+yVxckxhG~ zoXOb8SxbPXHI%H57^Y>Cva2vmU9`^#NJDbAi?_m}5mGskRx;8tw`0d#$4E)q-FF`y z8ajF)9FK>iU0shVU;JT1^JRw)UDn)i*K}|1bRs_0?AhgM?g+PsJ6_zf1uu{ZOr6BI zm)P!4is=;GE5-hrVwOpr~r?ZIGxs%JwC;QLc^v};e{mPja29>UDN>k#W*tR8}Rl4v#e!Nc? z-p3{DNG0o9DHdl_wUX4BD=ZN-l~7%Bbi_py)f}C1rvpv7{N&B}huTu-aCrXtv;A}Z z3uw>n-u(zq2aw{J1{KeyvX(DujiX0l+dq+S>$c7&yd+m*9?3I!^c$4;2T@GRmses8 zmEItv2=xy#3pMUEu(veu)HKMKun9qa0gB~U(v#f^z#v*0R%b3}TuW&lq9{YLlGB1j zNG*m;jiW&(Mux`ou#A*t{TeBe>VqT}VF;2KC1&Mgj}VGBn76RkgrI|aoh~??yv`R+ zaw)#>zQ$c!6#Uz=r)0u-PQrQ5wNnLkiQr#DYO_~3MQX%6f((^E6bRqlo# zY~Ox+L+z-p%j|_NQ-`S;QT~xw)jcl6BDpM&A`;jQofA<%)Mebjj2oD7l&$LZ?R&2nSt(Oqu_w}J zrW(*yvH!+Ateu^8GNldE+Q}6Z_8!6Y@?^ub4?iaytbW=9wBpkDF(hK{l5#l8aY+w#M5G%o*!muujc(1Y8LYF(; zK6TZ@i8quJ_Oz~;Z>(y-cetl@rqgXto4NiWr66&C&QPas(%Y;P0q!FV9$m~enCN1UAUs6v2;tr;^B&wTefVuWi`BIZ%%h- zS3^@)qB!2x7Wu=)xGJfLhxhG44ag=IJ~n zMUpmo2n|kh#|%CbLvg>xo)R{Ovog3 z9h<@?*MC|sRUB*ZHXeq~V0gd`xMoAL^ukc7*(gestH1uvuSa{-#B)m2-V^z{n)7E_ z&eFp4<7-)4H-1-fYz>FEMuJ&+pRK@ojV47SR6k(6OQ8n_EdGl-5?!%Q6hnHRP=N4!BIhMf4;%1YM9R zmU^r7$Y@F%@JYintsbNl8t1twg~T?oPh&g{^kmH(1x8xahmB|z)0ClAjfIAe9D2MI z48+iZr0k?L>^X8|&*y*p+fO&Qwl;tIo$q|-)@@sr{v*40AHgxLIoK9##`rJ6=1Y6M z2XazOsh|z0U8`_N0}E|HA--1zcxt*dhBMTXtV(tgWJUXY2KXA3W$7W(>^tj}>PpXNUDZFY1dK4}P z_Jwtxim4n0W$WPb;ls;=%j2^n@#>C1d1cE$5RR(|PC+?ed2%&t4su zrul|=lb@#fL(0F*)XpSY0!_6|0Z?5fsLqM~`nW4G_(PUKbtH~5gX%N|+tf@osxu=1 zG`chaTAHf)6KlaYLCRL0oCTCGwXdq&Oo-gS1lqZEe^u7j$A5^G0a9peV!WcZ->TBF z_UxK+OxuG`-+J}Jo5~Z31=X@wAftDQfn{jvK1e-`uPjzj83k}wKF+jPA*wfxG-#jy zCmeLR`j!FT^1m%`d$9rEY`xx|ZNL|;aP@}Cb|3J^Ptw zTz^=BaBlSTTgLrocs)kEX}wkKmey$-Ri>a=UEX;W>HrRFpY>Hvwl*9YM zk|_1|YZb_&#+-~pvX>j$#wpx>7BBG(n&1$x<3!9p)N_cV>R2F9y=NIHOmzHOh)RZ;#RHvo4cvH7Zzn9yjW4Cl;T-2NehZcZ9>L;BO{+h-?zYgA~`mF0J*jNg$C?>FxEjwJY92K-73 zyvlN)YYjM_Z_F2;o%6-={PkDGyfpiLo$R%ROZaB%^`gHLzG#Ju`7Pmd27C$`7@XgPk+h0m#~>pXRTi&xUptyG#Pje zIY5oYjbM%G5vKCZcqZSK-LAo=_(Nqq~s76>VDd)1{{=95DIJuGCVN zVX3?*eo(z=cDjrDTa~)xeHgV3k}okS?Mp4~-144#C2z27U1*FNXQLeDrKC|aM`zgz z7qd}bzrhL@vq6sXNn?~rEjl$~Hi8;8nlY)s^1D;U??^SAjq-l)NP=@VO8Au)_<-d; z*BWp<-=J>HM)cSE{Cvzt)$+URjNiSXx-HMY-U3(coQ?ANHxdq65vwhDS}E3Gr)`s> zJcV^)-nJmfU%4v=sww(Nsql zE2+#zhGjWK`Kb&{^H;DZVE%>Kt+(9rusMJdTWXcgsj{ z)9RH+cC~j#x=V*Ecg-}kH@Y%2GV8*<-mWd()rk*H|5P0>_BMunb5q%U)1?hI@Y1&1&nFy;Jbnmw_$FQP<&s znS;G$m6rvaArFB6%nGL*CE=S1hin*HZ&g?!uZxwRTQ#nxYekycYBDcPe38NlMuB^A zK53%}!M(0|Cfy!9Rzl!tMGHW)M!o|BunpHG!>ln1M0wk(O;cwd$RUYwmz+ zTTVR82-`?^(dP2q<5~6`XO25i>Is#HJ92|rwXTwi>QF`AuFLBBR@Bmoo?t_0W8coc zjjjIXzR~oSG<%xI>#B2RSLa7^i}DL{UFr7Gymy}(I5^2Vff8%k26h&$SX2MSfX`}h zt}0)JbnCaTQmzHf!Pg>Ii8s_?%{TRBdZu(OM|!4oyry`hQX+q?Tl-9qVlJ}qC^`I7TDQs*>E;VfX z4_a|XH0_EC5h6FRLePq%=}soUvg*R(3XP*dnHr`Nm1^6a2W$`83ACHhs6=tIK6>Uz zre4i3PZdY1Qo}fcKSQz_rrPuzy7(fRh&_4&Cs^!g*BHh!B%tZy>(}@JJ^@yJBVi(* zM-W}mcmmhFHlez8Ku6biQDBu~GqIxv%NN%pjh8yC+E$4y(tzz1wmbcxdbi)H=GT^X zt@L~929~>emV4ZZM<>Qw$DBjiH5J9aI&k2E%IxNxwygem_tREcp$R9BikD!$t9@$|38yO(?2rv~DbIsRR#gDB8kKN#uO_iq=Y zv#5y^;0E>U&B!}?%Le)OU${o)0Sg=-0k>xZzW7oq+=+4Hl^OOG{K~jqtb!6gi(e7m ziu+3`n)?H1*th(<1un)@!WZAL!g+rQU$I^M&T6nox9ooc{N&@zVP9O|ZuZqL)>wPZ2gg`~3Pg(#84@01tB&(%@UxR|Om<52b^vkOtqZT`%E~zBKrv28U#; zG;tNu;Byjg8(W{U{WT(*d>BH(DQtHLdZh02G*=t#`UUO!dca@fYNNpq%j;2@+NP9q zwb9@+ydEQOUvITN59=+(H^?R|{3>J1x!p~+vD$bkig+!eNQ$^@mNLm!Pyv?)u5>2Fj-TYMo;2)|^I0K6WJPAi;67Mf<{9|H$BR^_mNx z>l0Ns|2HzdJIG@c7oNnqB52Pxmq@6U>dT;wA^|_HAd_CwJmB0XeLo{VlQY9LY1^cE zEe*>Aq=X!rp>T9PUc57Equ3eM$(>QLL7-xvT3busc*jjof9sb2*!Y;5`LEj^_;%uJ z#tixqdW&7-FylFco&{&2%K*FOqCPx2kLwV4q=@?B_EoUp3|=kaiG*SoZ%c^7e%$uI zjO*_M=M?t^eAZTzcuWj{P4NLIpR;j(i{|J@3Ux8B(gNaD1qL|{C=!#EIuN^ux)gAlW@}+WRDe;~9 zrTWAL?OopzGGi&;39{|)w08v#-V!pbgm2d2z`#GMzUg=*D%LPaWi^u$G-^kE$cJAem~=8v2Ina|AShmwqvZkhyguI_pTlTS_TrN z^P5LA$~a|0Yh?&T3@CMJN^w=TEJ5LnUY8|8D{y1EdkxjZ3Vh3TKTF7P%pMJBR;PSG zJ>uJX<2t_vi$n(%J++ZZ$aF-?&7B=`7k0m+%wOt?V^gmTXXU7?ynbM{b5n1B-}VXMDSML&mr3qut}mX76$*4Hv-;;Vtzm9XxfGPG2)Uv8BrO+v@wP%ieTP zPk<@}mc)7(0oE?23d-jas~ZGXsbw)w{VHI!$##s17P|F4>m1?!DF66DOA6V(^Tpg$+m!fzxT^93VozXH6C35p}OS7{R&wT%g4XJDfJ zoD5JbH)NY2Wt*VI?3?1atm73BS&V&;tbPo}4}sz^$U&T$R>EQdV}5!c&Zx*2+xkQ| z%BAjH4&}I9%CS%2MAMziA@T|+b?0)#GvNd3LNf24K?K~J(IILAd>Ix3>eGWb^1iTT z8AVWNb2N>-vvl18mmg0=*GRY+6s?Nhr;Gn_kzbP^ocQJzt@Yov2)Uv1(; zlVd2N07LNH`1lH`4u4+K&AW-OcLhQSo;x_e`5KLeJA7liduYxLHb95dSNUV4HoTvh zub;yKREt0s3|>WfJy-uT;(F}nr~IY34@V#K{#kh+38xiE!Z#C+ zm9Gvq;J*ZQjN9%YCzX}9^_8!Y4%l z3aK;EUe7Qgx5hEsa8GHm58ygkScNKBHzGGs`NXP!XtFX>Xlha_xLae4_)G+GE z2?)DzRdYpN-%8ztd$Kjs>Ip>Vw^zp|Lldjs(bC~yIOGdPmNxC(EG)`3O|U3`*J2|c zPLBnKyr`gD>B-2*nVRzT2FjZKL*?EEcTJ5WBX8s2#Hyp!ur717B$l~`N-Qtx*vDuA z%Nm@c^@hNHC5B)WGFh%|gN}1e5H=;T6j9mAVa*^97FlIbQc_4qkyi`fSkodlE?k8% zM|B(Z2K9AYx9x)zHj{UJ)SQ;)^HbcKns8&T0UnZx$HES*#Y=L~CvboCj#0qOs6fuA zAdLWEGXIfoNoi#3!~#SMEGDmBWt#>r7)+1?g^kToWE&zARcsoj8Kgop7+25IvRb7^ zQE#YT2}Xj0~P_8f-2P_=kEL zy>4%IW~Qq?>KUHRM5@Jw*MO|FBiGgSY;B*ZX{f8t&MF$yq(rqDqg7{nAMN`xnoJ)z zO^-0~rVXTtCR3T#q(n`Q6dEDz`jS_2Af;_78_J5@PnFUmYCQsd40&f2M=))~cv1GI zM~q3O)X-(wDill*k_H6Hj#!x%;e=AaMRP-=ryo`pKfLYW-B%5E&rEJuEP3E%bx+TR z)!R1zbI!i)YgeqQJ%c3!?JM6(EbxtM@MlVWKVBADB$ap!E$DdTLOffTRZ{F_DYCJt zNXQf+suMX{ZCnQ#e;okWJWNiaQI3Fkb0LC#Zf$-TfstL-q}zEXdC%{XWdv1S$!{IMy8KYH;tbz&45 zyd&+#v?Ow48)=AS`Uq>6*WwLbDV(r_M9(!%mX2j0x9LCoSxZNb;R`DW1{a#EoOt_# zA;n4fq7^Q@C=xzrz^9a7i*=iD%6ph&#{Iee%j*}6>)B6g zP4fD~N}X~2@9fl?5Z2;#@2&X+wMKqlw%~1KZw00F$Wa-wf{)pm9 zLs|G}-NNeH0cTTf$y$oRtEbw&RMJpYc==vsW8!Nx%(|2v=EyJ}MPhU*3DeZ1Bz%K` zY5IY$0;j%;)$o%o*IEf=bBd%arq(GUFl{cTTt*ogE)^+q4KmYGZNp-IN24HOHn}>c zBBmpz4%dX&k`b|Oms-*oO!TF``sPZv($tgM+)<)5Ywk9)b5d zfCHW@;gsJbe9;OQvsuFDtZ>VH7BqM+>`9Wl)XMw+lf1u#^ZpX9N3IIExW9z!k*g9; zDW7oS`4Q32i|?;Tu1Yw$g}h#mT$T4Bt;+lB5sM9A4PO)euao`!8`)0@r?w#Bo2_u+ zt q4PFM*`OWsf#PuF=pWDIIh))UsV*`E{(PzM)G~jn8!9Q!j&m`UFi~+w@hp$gD zA5()9KN~FfH~Vvke!b}D=Z)*{OS=A~asQinJ-h*B;F=#0nI2x-Uvu*$z2ZV0CYzaO zqOF8x-dIXx@Bce*fK-L0nCPZF^#Ex1qiBqxVvLmMoRkr{PcyhftmM+1)bDY4Q5Gbo zq`mu>-ju-GR;6h$@mBvkl#cX+DBgczB87j#VesdFt}Z72)*Rm#41B)JivP4@2K*i? z+1XH^L0A)*-58===l%pzq{!9A4r17jOTxlaQY#^rw)VuC=ESgR=k!TOEZH-(>+Z3 zOL1A4iaUKVa4!fI0wvB!FAC^I5%fYWq)_BUIS^TwgVjZ9@i|C-ml;y@fy!)Z+W3nE zO|f=9LJ^Gra**<`Joe8!PAR+8ti+ zUA}3fIMZg?r&}{|2|2wD?;-QRq`it6VL{9r`Z9Pd!Fo$sz8-cClm*}%lACd8ffrWG zgEv)Xm(^QYwnN#muw%!<;HHx8^;a)lDu|87imPvLnpqp$e!X{h@kVv;6?>2Dy<#{P zpWd2w=`gnZKl|gAGdo)6=i1wP4eXFEC3emvVeLKxev1ZYb%-nN#3ndCuzY7ev8gf! zK)n`pR|xUty|Or#EW}AOO_5)h~bJmP>{X-h{kmC*mT&CdR2bZs^moLw9J&2Q%R z?3p`r>#fSrlG4z;eA_bqZ$7YZ_le#6o>_W<6s5hZnxVW`P#AII6qE;F5-BeW2V5uN z^~gpwjJ22bI%~8QPMI;83-c=!6qmR z+4YgeYJlstaS|hi#YuHqe)q8cJHVU8Y9#OX0Snxj#??T=KS(%cC3)g|z~fM!Ruzj0)-x$rN%C z1T7s{j8^ib*8qYYK-G$>=%{~ei*tWLWNf8l{KTeUaMOwL@#Awr<)y^?l*;Ic-#;3y zRFwBbW;@-Zy#fDNn>(`O&ZVV0cSKsY-?>`9vw1_O+ugaLS?BTBiPhbY_s+22)rAAb zh3|izz_Wy(>DJ)z`(LMi1Nb5wRvTrf<$BI2dHpTC9(XLlpQl0bB{r9W0~1B*-a|+R z=+b3yH%${W@u*q!OhC#qs|ELh|B>q<%0}E)EVH`-IF2SF!_MLaERe~MIFZSZvcRbL zn$wbjV=gf~+zrAdZiJLh2O@vcaUJU{G0f?5=op>m8}Qx zZ|jA1<)Jldh;juX?Kc6p8Fqb4Fl5T*f{O+O{PvIIfa@_5$~9taCH$_BNjUTd&Zps{cq;=7_Kf@2yYc!6dvh|xoFBd`3Jcu`J@y`xP+3Q!pVZMqA<^b zI{!oNDfWfq7LC|H`%(#d-mcxdmB^kwmYAV-WZ$`qx?{?ZcBtx(9TyTiF6Nwm)Q`1uY7%~Wc;r_r1%JdYI@*;LI*ma=JpnZ(Z^6Jkdi zQH6Sg(q-UQ$mY@L9oWf*1-=^)GM$dsV=`T-Lz zVsr?Hof%}S-3OHnsJF`N^|%#r9}9fewlDEKVyS+M`~L*^%VT^B#P6iNEmEpEnI?Az zvmj$zFb`G$qU}fp3GtjUJR~P30+I2Fq7yi$Tu%u<5)CLtM&ORqqx>ZCj=g(Ne)`i& zwPLSZu2c3T{ucKkLyn{@(jwmTc8xPj?m*c{!td1Kz$12E*5JhBK^sC{Y?_mq!=NV3Zca+rwV|^>xC4&oSE@@C`VkO|v-5G<|Y1 zb63}t>Rs01Vu_quu?(u@E`pLNW(EbU8pI%@0Mg-{Qt>U76&>c+e!&801!9)~8cX!E zChZsWIm>JgZfDN2sd}6aYNRTN%t0itr`@lJ(CiN7!0xf`sbQ}_wCVVmdHR-1$qf$% z#v+x<6|Te^@-(-3v2|DYmMtrHZf~`mDv`m6a(}(Ma~3Cwlj20s2yjLkQO*Hf7t;vm zgv9Y_fnzBRK5F+vCyg>XE4aNW_!DAJU zL_bTfLN3CYU$6zb+e7fR>XM*_Hiqn6yyva@Xl}N9_K+jls72wi8dS+wWZA5%D{1)g3dDDunXalQ6*bB!aqp3l>Q+%eFIBp6L)7VgaAU)mvsisokUV*(|^9}qIJGe$1V|b z%SAa^JirANu2c{KXI{BDWBKhWamL`TnsZ=~II_waNk-ZlvDJ*3u2SYQwKfFi#EV+! zwr60D@=E1!-Q=nHP-yH4e zu&&HW8aBY*AY1!$m>S8n$(hjz#(hF1ySNS!&ajflXC&>b^)zb0{aTbkCNZ zS6YH$lEP>PqtoG*zE~iVn^Ur9OUuSsb-aDT8#J)Z^-N;*jE-%L4zQ}hIXcwfCn3K4Sx*EG zzl=xGa43t$bi}DSA=mWY$_Vkf0#O zs?wk$w_xPz2f>9FHs*D(>W^8ti0`D==ArJFN4-yrqLF1VV@_rF$wwYx0nA5~c;auA z_||V7IPk5l7^QObFd*hoJ8lgtAw8XhW0V474iU~#s>4ySm19l-j42~Wzq(8kLnJOxq|As6p;hb_KJEn)LpER4;NFq_osY`YBTPkJSjAyT3tGD z;KE~O#Dj%aXD|SiS!dDvLd>=MC_2b!a0a7Lch=uX6SB=WvT{)hjG}@U%W`T(m2g6h z{|N!X*x-@^7Wt}$L?SL(*H83~ooHRheK5+EI1}P0iec$SPp6CpVGXIJlV6E!?5L{h z*ckcB7ebLp=nLvoXKLbet*vwMng{+p9E=3R7%ysJl1I{>1POymi~*+uDyj=s+Dk*_Q~Ud?pn)u|Zrir1 zJf1iZTbvlE@rBEg_0EbojR9x9yDC3DBfVkz#H{kDF_!eyN!-0F38$1gCH%C3(>K(Q z3aN96>bwyVCitu7<@^IUHPb0|t1!7zHl{g`1vKs&*nlkxC1-VozC*tI($4Cp>Vf@J zQ~L+1%{N`!=MAcfKbbu--H`NV9~E-q??4Tpc7v*?_y&;g2q!gS#%q|cUVn#iJ>Qse zqP+gTr0Xds%KP60Q|1Apjk;P2Jo;KuedLPDVw z0FRmNt?*g(Ttbgfy6~om+=(2; z>!`GvsF#7`r6a5tJB4%GQAX3suu15ZSa#UOl(ow$DRJ%M`bz&2|QWs|Elj;t3OY(lbd`Y1!)@d6HLl6>3_*S zhKI4gD5H#R)^w|vw2!e61rsA!G=ruf#wuosE~TS8>7I^~4m*0}DRJ68%BzX?fBQG( zZxfHd_@c7->>*_(O<6f~Ao1*m1fsv;nZ}qr0u3R`N~H~&mabz)EB;|(#;mgPWl$&A ze^%$9u)umMtk})s9u0C_VqzC8xwkO2an#C0xyb=L#8digWGPnF<@CnSjx9HCJUKIS za$}<_&|QD_*X7Oir4=osq0ne+d1-xf`L7RT*WYvb;8m+gu(@imeR;gSDEq*}nZEWx z&yr`Tt+uvp2q%N>zRZU)IIMwk6j=j|^M`-fV`N}?(9tY|&NO$3IeRn_-2`azt!bGv zZI`U3DK~o}A=hXw8dkO}0bK5^T85*vZt8`oWh6ACgR!APvi`}8OjJHiZ^QmLC_|@d zpX$+AG@6~4R<=5aYR5G-oinX-J6vhy6{k)N#1oG+w3Ss=m9;elySf6g+3njJCk_q` z9-M4k*|}?DV*T-~aPQ1aZ+LQQxnS}rnH_;ozDlTq<7{b{?M*zv}GMD*`rE2OBlK>{ip3=|OF+&K zy_Sm%BNoBb0tZehbx6H#?)X~!rq1e8Z%bueZ*ygJ$6U0129d>I3`E+ZogMM9w!YEP z-d5!|SKn3@9cyan307sKrByT!HAgr1)rb9w*VP+4!d2nU;nBpsje)`5>h1w@f-G>U zHxYwdXuFbm7s(m8nvcjN0nH?B0yIO8i88CkA&A7f^w4)C%YTx2WPxpoU9{WEYE>~2 zoZj!er#%vm=63`#BFmTmIWt7+TR!=bkNo<=o8VhABQQ&S86sXY_jJxP<~Z$(W_G0( zz-o@H)ttz*56BSUVzdg$yh>znYI&9Q_%07)2UXPXbm>FWq6&jzhaU{q4nBsWCtAs< z4yAU}>GrAS66XIg3Cq(p-E@y~bTF$t=oy@4u9kX_BW*|H)bwm{@UjV?e|%5Rz@A}G z#yd`~CXRT!Vlj8-j!jcc&*FH*;N^K8vo&P9n-zcL&b&Hj*3$J&^jkG=rsJ23_oD79 zW0h+GktIcHse{ksj-tfbb2xbsO?Y-6Ae3t0Iq@|uWz1Pao`={x1ZD=txQjfOi*ucb zgkHtzt=F&-S6&}Kd*t{?Usq3c=Um&&(fWz9sc1(ep`TyLYVfj9`RZ&N;nQLeXbr6ok}R;%McBQSGpP5(C6B)!O# zieF1hAvv>5_@OnhNXBYFqhXhsp5rVnKt7kBnLGpPpwi*+_A5so@9;;vr+p2XO}m?> zJ2r$%v#X;1dncbzU*7WK_)dR*xT~NfCtOohd2Q`=CHal9;eb2dzajFObnKIXjH@m+ z4P#ir-AhYAT2&w@z5g25F~{)jq&IW@u}(56h>&`jks|XF960h_sf{xmL)(enTJ>W& zJ`!$I(~BmMxGk%GU~07ln^E)Q!Y|dd>lj*|_&8W)8&=T_v2G~F_VRK@mdMI9&e9syj?^nDU^tcP_;O!eUEgwCKURlp3V&Hx)4UCT!qx8P znnLw;-?j&j9eZ$_Pd{GRSrVS=?CtHG3YRF0Jvj6mZcg$H$@}VHZ%b=l-0x`Fe)t>eJ*(cP3(w%sV;}m^e*mXK zi&EQgR%Lq^)sJMmfHq8K2j6ZUnroYxqp#^K+Qbz2=k6lZr0DzjGFB#O#np8xChk)x zyhx`K@8tJ_zgpF*xR85lm*+icl~%nfv9KBm9NhGt>+o(+fRzh)w|v_o*KO1e!rgVs zN#QXm>$Y{7Hd)H0SLJWqjWxL~uOsAWTfY2D;2YkS$3On@b=2HJKbpX+{}fcwN~)l( zDQf9QnAWPWr6P?rNt=`ie&u?vC z?TVI$+68OJE+Ue*w~YjccV(wl91jlWXRfY7o0nBAN>xq*;-DNeV>`y8P{xrY%TSu= zVbWR3q_s{2j)f_l$esBZ5FxgS64Zi(J7FV-?y0*H+Zg{i~~e?aE+c0mHM3 z?$|oDHK+)j^9T!)O1}iX2sCNyl#;@|tQCQT{H9F{!BPsakjgw+Zr;iTNG8xzEw2y^ zx50EiMLN0~Y zZ-jqSUGOWP3kMB>bZ~pGQkZzXe}ckjm0Nit)3tfDch9)bH?gm;fB$&n>e$%)_}JL^ ze0T^cObR%>P5B%2J=ZqLiJ2z?F?EVbNvO24651pr6J!JrG0vP>tbmXoHG2tsIt|@D zA6CBj>El=j2Ub^+>4|4fLhk(yo>^p@U^G#&NGR5ysqtBJ4rv<|m;jBzVaOSs#z)3Y z1GW%fi(+4B>+Wt_JBEL2=QmDFY{XV>uikdU4Yw&yp(S^zUjT)w)Ix5p zQ6cPrYB|lYS)HLZJC%-}z2m-#@sUW-)zn$@)jj(ky!GhO>6M_*7s_p%*gtUaUhhQp z@^GiWtFid+|NYu~?s@R5O4-Exmg(;9X^cVvxP}d*;G*Za2#k#}sQM^qQ5)u~rjUY0 z&E|?}hA_!O0Cah{w5m1?-k>#r4{~bTjFvzZL^e>&uC4a4?_qafZ^x!^xL}k0>Z_i( zdTcPihO54Ka!k#v^FO0F5+6_ec-7;3KJl`0LUDlmk(#>HM`7P`(&C|D5;0a) ze~%QNmtycrspY2)s`+B3kOwlSG-KBClXg3nJz(Cn++Gy}*ug2lj8Ob(6K5DZrSF>4 zYj+*JYwh$k&wlmp+my`24<1&py>QpViZ}7*ZFhf_?-GGT{xm4F#rDVKBgq~k4_8=^ zB`H>W7juv_^Oj(SqNk>eD5naS1YHlV2ibeXDqP<>OzFHHifFy97i}ZjcC@2t*P}V$ zJ<7tu9p>Z3Pr|MANV7Boco3b^G8hNTfNt~B=~rG8Qyvfae1UBv;}ZzD3G`3prR7cb zGyZ0B3N5sgEbTV{S!5Vdq+>)S5G?Lz^~s=3ws$BJ!aiy zpga6bk10I>8R)9^S$UD4)mz19%_@!0(4DmFnbDAe>B!H*VlO_!GOvBl=)er{2N_NF zhWZdFjEU%t+*5^#`+k83>PVEN>J2WRLSQD7(4$x=_cC^c+mTfXH2^oD&W`e!(iL04 z6R9ZSgoVn_0`deJ)F(z;*CPZQA7tcqJ)$=FfkiB6FW2j}>b_dP!8E7``{CD84vwFg z3x(zoB!7ZoQ76W??Ay0x^S*t}>$!Ralzu!YLR`e3e>`x2_~`~=S6{IqZTgB+ zKVu%Cvih6fBoNP__v4>LKM>}cG>)JEEERp|7sefDECF$d$;-LI00f5vok2%Iz zYC~sQ$4vo=!){GBk!}t%*@Uu)^jYk3n#A2vo@r?y?5mbr0URRg zO6&VUzKi;9<1?ZH@IC8k!ESx4>^?{Reih z`hBdc*rI|J|3g}B7wifJyfPYu$8O?Rp-|a6oC6QF0uQxfqyaK1E1g*cL%6?~1GrF+ zVbD%?>yWeZghj=3Z0FI$jIiOPkRLdMPT+*w&E>r?oP`+rJlMkWP{k<^lPwQBmJU0h zPtk~*2gK*uxT7VVFWj<}jwmDj>jhl+4BqS<+IcjVZRHpO~122zsnJLVbTW~C2OZNvLqv% z>kw_YKr(2Pu6Bqw%FZ(nova8CG@fi62v?kZ=*-Ed@4fq#S7H;P66M2*{UxD^*ekEx zeecs^#^4R$*TM_|zrh;{pAd7&RTp}YVc&5c2MyjW;Gkc^eBP;Li7?yBy<(tKGv1+3K z_pSpU=szB#&e&O(x)H8Iz!NVT@J|bP)5UP*F9bXQ1Q^#tIJjg+%=#}N(|kf)-wI{J zxL!Q>+XCLbW`ToV=>NC)?K4IH#r2{;Y8gDf#(hM@BzM=!z*58a7rX9=&#+<%Y5xs1 z7d1C)(ZhozPTj^?XYhHAt(z7tlV8eNE|qg~{5m1UmZ6t*LI5ZOg-cga366xB>+hiA zL{)T$s2(Gurjl1;rKW3Tz*9SVXzG)n3BEr^tHEpM|Su3@1O8(n;ah=pB(C39-qXT2Kq!rY+5ENp@%-eS*?5j zsMnC;^Jr$zYAM7W)KFz^Pe(zCnCLQKUjR z7BT>##0b-#hdK;G6{V=?(Td|CXV%P~ol~5n(}xH9o&GLw;zvrFyT0E2$UW6HHP!d1 znNxlH^3u}>W1g;nYjcge%3b^552`CGDy#1R;R(C#AK~rOZHQ`M(g{sR`g)l1rq*}^ zxsFru!b%N$%bc?L6_lA?rPWr2&+JNd8}6Hr*@|6CiZ%U0M&qR!6qKT8Sd~=Fa;yxD zX`od$-YE@V>;tkMF(r$I8;k;XAZlL|9)Qvw-0;V!v{G8v5N~JI??#xx$hK3;*+$1KX&fj8q@KLu$eN82DWLb^E)z)fhR5N-rW>vEwM za?Isamfd0o3?*y2?_`10TH`7oVAP_Yv_)`Lvp`yWK!F7i4tH9Qp;U6U1x#4GGe~lP z35&8K_GN@aD5Us%x^rqve9mZ3hc{X@y0Eq|S`_tm^hBM$lG>c^o`L#E$rzwxB@tLx zPP@v}(sDvaSL+(y{k`wKyP zJ$hPE4im{Hm!V9GpjnWtZV%;ldGzox4%DH!pbLq?=Ky#SO>^fVTb(m+2#|R~k!e{V zW?nVDEw~{g{jgh~n1g0XFX?Cox^ySJ+zfzL202ADUY|SAFj+ns>FNkIojBMOinOV0c)>J0C zg#BUC(GP2EamuVru`yT;WI`}8%ScFZo!*XIkN`{wju^s(V0VgBOPD*Foi^}={qnDx znYjMAl6~P#91_3A3PDbpss0u#>LF68eJlRzy=9FmH@RmetTEPIWm9^aVwbXRMkRV{ z`Vh#BZ9>EX6PEL!3iCj^yHr8V4vqR*pjp+#C;A!a_7g8?P;|a>F-gy@?N};nuJ7m> zDR7mP*5prg4|rOgOFO{EzW1KS>eVYMOViWRoRz0nYkVL4A?P$`{etp4ylI(j3q9*D zL0~#}NO>{yJs`tf?9O2y&22upPzpAN7$A|>E5aT)-*U!si*p^yb!#8bDXA)OcKRAu z3&SI;eIMc;o|99@=3)g-5y{2TxfXpZq?aKFvxwXC>U~S`lcfxcxrk|<)m}&NB{CLn zMycl@O3S4TM=y!FV$4k~g4YSG1Pj6K?^WM>`e<7H?YGyb9aS@bjmQM$)}a?(7)qSR zD@!WML>Eudaw;{+|9U@@R7oP$>?bv6NI`gQm|Y~bWPw`b_RORfNe_skL2{|nJGXXz z4gaf|-~H}||G&8}fsd=Y?taTiBWvGAqZw&NGaBvN$d={NVr{l1*_JKKi@eE3#tUE@ zA!H2Ln8krWp(QbplwV1cn5HTD0tt2sG$aJb(vq}UfP}Uw1(T(uAuUTuLJ}H}zW@K; z`{vCe*@iCPhk2SicV^x@_ug~QIro3gJ@>a~&d>%*_F8~e{dee7r?#3BH$%X37Og2= zXj7uPFu{$1@h0p`bt&BwqxwTr&*Sm(FjmelAaX&Wi=<@^YIx2UbdyueGh)u)71Z=_ z4W3AiTK(HnGM*aScSumcipBr(1NxU9VEhPmHlA-~txAUor%dU#k9E+Gxl;P1!+sch7~wxqlSu8ZT)DkkmSl+9*{)`hdjT9sF3uW z=YXB8!(qMz)otOXj5DGUXB`*i$C8yIo-0klZuEiCzajJ=6ABB^n7$btIm{D>Yn0m~ z7!&&dg!DCzqDal=O&hn=M2Z|!e6wZarp@Bby0Myug-uQRQv6xiP!kD+ZvOCxZ>|cw z^G=|OrBGm2FC2KgqHZa!$xr*iq{-=2?_DX-Tss zMewIEfsMh+H1=}qFoVnx>j_@OR$;Cs@ehm0H=?`1kX!da@WV$2LkY7LyR7t=@%&hzzKWdiv6-I2Xz?Lzmb76YC6uxl zayN%uLq$F=Yj9;SB+WG;lq{?VyHxG9&7XYmKTg#4w|Fle{r%5tnwn~kL0&!VZtkz! zw0%4nst*NDt2G?X=)mBXzyP_E#-FwON2o4z2VcE0`cwQgDZ<=dh$vyMOnV3>ZlK6k zdd4N&<-!x;A_LP(tAq;{x+TdN3c*Tf6*6r>|J=%k?Hc{%88&Q2zj+GUfNiVaXthm$ z1gwM1u9$_?Lzx(#-PG=KC%wen%vh^Z8ljj<&|HLHOWjx`X)XfIMR7D!kTMua(M%y2 zKM0QKNYqV~X8uz}d@KBI@zm#%84ZS7oU zWrrW=X{xPX)dyn(di+<$uhA2XCjKy;6&9XQ2_Cf1B1(yRP#W9rBsHG4E>to>jHk6& z7kdY}LUh2^aftDhjx{dOdF+)8;_=2V-2C(rz^w?PkW? zqI7}EC2g-Gm7xo?q9X`wkzttK*eux|5(`(oU1{;Q`VOC~ zwtv{u(qDUaYC2NX8tm0q9^)L`*58!oaE=Vr_B6TA{`RZaHdN?E_Xk7XV378aKmr98 zOe9e5I2;ESOsOLxZRL7^{f(I!Yp)tb+bT*BEg)N_ijg8i?h3_|L_-~Ds0%nS*cBcI z2ohnL#9=_2^{1kbeBf=tgtT={gE?!0C=w4frTlmLa(LT7se>cRo{4!h@oad9%*W7zi1rR4m=3KI z^jr468aA75WtrL)%C~Pbd3Y-L5O{6-|k>Wjcr5z%#v=-ydG=I6tg1bJfZ*emuLDpA|@nLuI6KA$s|M)G2*9l zK|l7n7Uzb3kdZ0Z(91R)Yg!xF*g3emciEK(db@ghm#p)wtv@t&%?)dN{E-Gv)r#HO zOC!VeO^e&JS{LtaYs+j7*9HgLIxpG0a!qEMSyJ0s8y@ZO%ULRN=bQ@zvRkb*gI$s0 z9-bcqDgXvVgQNf$kRIB6MV2|FXJ5jX=&!IVEqc{mAJ9Jzm+^=0`hf6fa@HI+PXY(} z1Bzzzjt;yiiVtD-__PP+#er-BQ^de~!GTUfwwxjkL{}w)Fx2!Y!}PGwA&Lu$4smdk z(IFwk&dJ)FXO3R)ZmVwZ+gspsl~xu`_Y4MGOFwv&`KMldu|Bl-w9gBEEe(8qZ?NHK z2mprcnmubwp$E=|57|9n+d#_1w9Pen(vsXr#x}^rPL|Natc&48xoeC4MVb0HW?s!H z4Y|Ba8yoi)cWuArqVJv^GB#hc@9s;Mxjb%5*3IqZN3;fHVS*=JjV3(l=fpnjw#1_i z0_#e}KBU?Jn*smWY~;;Ee}JAOl~5i^7RNmGiXLQ!#Y#9N=qm-+#k}aCuK@4YgT4Z7 zDI?ukil<$nl6H zJT>#{tkOWEe`IgJQ4)Rq1Gne}XaBsc%7rrpaZ2{fW|!s7*N!u*oII%oF><)C53-oA z!keEBZ$4~Xg4o&8SKn$Cc8LOAAN0Y?*F5}|E)SlS##bNeY2`7wD!*a5)jROHe8#z) z>5&54o36)u)2|B+POojKADtcy9YS%mzF{qkMm0AbdRkUAJl?RP#lu~Q)sg1WgMGaR z*EMh7-n{N$Z{NYu=Ivk9@A5YHR^eZ>H&&4O*^<5Kn_)407h@=PpC+RlYK>`+VTz1P zL^s$6j00x;K^8(+1-1d75U~wo@7cF!^yZn%_JidfB+J1&jqHEmp&mTHSi2|{-B1$L ze!gNEyJ9|cL++7hRGq`xC?$cW7<9p76+`-W(!8r$ZaFrydL%qtIy0j?v$C&0{=Mf% zS7v4E8_o_XnTni7?4dc(ixkT;B>*9%4T&oPD8czax87uIxQDjB*(OB9L`XOF|th)l-G_W0AE)xF;p=J?n3C5;OgHvSE= zmPCJb)!x0d$6H%kehvu%{b{yib6Ke79!&_war;P^$+U+W*qxahL3 zMUjzpkAC3`U)eO$xw!B0NrrDQPm*~*s3vj%yfqOyKnxY0ALsJ0hhHtJ?Ft(xP(O*c ziXG7DJfnxmfwezgf(2|L7t%6YH*H^-8k~6ciRX!-7{+zC{7Ke_W@b)gf8yARsWtWW zNP@n8=g#%Gvj6N)k#`oR(tY=}Z2yDp_`hY}!QEHvJ}CaMwZIFEE^tl>qCxlN!cazra$eC%LMPu_dl>C; z=c{lX!GDj(JObn=hx=%f^PHwY>WW(h-Xmivxh!S&dIq21iA4R@p<)sjSGUqnN~B>cw@hY`bmUi)=UcZ4)*1Yqt|rOq2l%bg&ID-erf=6`=&r{KgwB{h}^XpE^F;ATJPVJZ6UPFBQ4eSBi(6Z z3zn{FXlU*2=bWj9E_R40#C@UAWpQV8EC{?jNpv?cZ}DuOogEheg1A{Hk@3zR7BNIE+9`5z;vSIq8ADV6nJLp)6p)vn z#eHAFB^b3Ig)zQ41jfD#1s;Bpn-W6=y(1Q&RP(GsHp1{6TQwtL+a|ox4#mkEA?AT6w zK`-n^xUZ&+e>q!@5y5?=o>Or_ysy$nMuyNwGF}LQfp$I5x>Mg@6IY)vzR$^q)4kr| z2bAN7k!g`~9Os@olH|Br;n*`u83T4jf^~zxwJs>wPK$tGEo|g0+8g_%LIz=%Hq0X$84~+55N|%j%UBzqG=A>~2z$z?QG^ zODEfNjdQq^*c69zO#{@t3774;ri5kG7C~j73PoY^8kxL@8}jGm1Cigz6@piLVoOO= z5TR$er3^hA?(7S+x;9TR9OwIY*OVp4o*9fiGkv1n_1v*n1ZPE~7zw8Au=BboZa+tZ zW4AGIRcjIo-=#+n%w(55`!c2wI|<=>YQU>Dk+r`<+WB{6bBt##}croGzLuPiV4S|72aU=>`@lr>Z@Yqf;-4paqWN3sXjg7(TXFjoq0uR!oM_ zF2;}1_8jKt!{(`k+-~P@af&%^2elGp$Wd4Z_Cir=XQ9R1#bm`_Sj!3}jYNRDlsYfy z8(u{#NHoIU&YXe%l2LzLcgdY4qi=xy!2+_r7!#u$auj)9E@1znJ%+ZHkzp0mT9(mk zK8%=L>S91>7W<@G{0a(h;!@EKK3E$ZCNwdOLZ5gew+O_W#c_Kig0PBf6)@q_z1E5M z)@$c5_R9=o?{8=4aLg{Y?Bgv%gPECwLqBy44h;=bf2;Omr_fVqIx`F$W;`Nz8E9$p zdMZT^+wI4)4YETTrc|A3zdpnk&rN0xRj4qC;P{dTk=V13m^_@nX1VQ=eB4d@=U*o- z4lLc;F}l~iJuA|@w5MljYiGg6#_2s7`5&6@-8Wn_)FrYF>|8n$u5CJ4d@I``V;f;} zU<%Ti0nNbGp{13S zxUqR7MMx{#GMKw-sCeMDfG6~AIma4Ymd|^hs#vBnS2>tc_-SJ_RTld7rQQDE9uZp%IzAo*2 zZA%ICwaqNz-I;1xl`EZJxfFlQy@mps!sJGjFj!5A9^eGv4&!v#q(sQ`)GJTHVhAsU zLVKlXk{h~0ZIg*wYDqj8A)XLQrbyHKXnFY zZ6Jepb6GFOP>l4f8ekxOFswL~h3Eo?OSzCH!HpkrKR98sg2i-Bx)@*rln(|81c^k- zkn)j8S1OQoVo{Iy2LRTrzXJya^7jUe z26&}#|34D%;pfp`VWwl|Q^5B=DC`Oqe`St=&F9BsZ74rs`HG%O!ee>#8y)I;V7~~( zy7yhU(}i~&IVv8d9ZYbElTsQY`E#VijL86>rnAX*YeR-S(#C62;X}KzT2Bt^j*{+04HGN6V4*F za%IbU?$NCl9AV|wmi6Q)S$_xj?xMdKlW~dV#Y(ogQrzM3HUqN^At@1N8SSf5;rw8- z8tL~bfJ1~Q!_bPfc0Wi<%Zc^}vGWf*+#>E=UEg!p$3K48uA?AqSw5?RzWHEHb4o4l-Vap%Y;&IA!h$C+c?igq~7o3(F<86oQ#IV9`v z(7rC(*#O`2kHD>+T9g)xjF+(Vrb^#$^F#wBP($4DEFo5e;ws`DLV~Ys`KuUBnX7;S zXmpe-ota5hdlD6=aH-fLs9}QClc95R1vg}2ejMm7#$Jo3IrS2~*HjkXm zphfxGiq5-G92K#8Tu1!8Z%@@ZgY~Z1eIUcx89sNk-uP6`htWSB=#1ptE{8>b$r!nt zJJDTAGKdrRRP5ZX3H3M&L)9OTt3M=k$Q`2JKSETM2|~BV0wEm1=yE=!V?Lx?^8x1)%K4Cv`G8vJ2AQfN{s^jcmA{@GF3%z3 zIS~~J91g>C4xrRn?37V<$Ht$m&GR&yeycH@;vV^_Qv zr1T^N%_&?;I)tp=2%$GD{FE~~6+fl_8K{g(2O#lNev`OqB7RzMUi_318g1j@LnUq1 zOZs-^`HS7v1$z;n7+67<$T3*I?B zFM?`!P6{(!0D{V<0=TddR0e4(jAIgoX@O)2S?fb@67Y~QQ_^$AXvb7sRRpYvu$wv9 zMu|@ai9nn=!!~K!09$25yicpuZlw39PNcd<)(4DD?fh{UDIu$NR3W1C!22>1 zfs0RRq%44;Lm*WKvlBzE5%QmjscTV+L8`I1oH-Fy74-SJr*|DVuq*o9_pH!QW0vtS zUsMsGGAPE50=+~()G673m?olx^Y!r(lE1>VHD{;y(&p5tpzc}C+fhKwl z!}>%-lKC21R|wJb1htY+q}i7X4*P^fWiHvxQqYK9VTT1NDQSCa$)1|nVhu1?O38e1 zGBOEB;h2x+sbmAlJ1d1m7==DjYnjuuV<{dfa~*24L^^CUnhwItCR(ea%-8`o6uWsw zubX*l*TI9kqR&TvIAeU3@hE>q>4y|v4YN|j5Lp+SFr0c6^~76dP?WmO9a&y z^Ww1_bLNf`(>n{DA~H(Rg33ats9aNF6#B#)t1qmjg=#m9mZ(DDuEg%a?AP?uu3Fyi@hipfZW={ZrjmTWE$~WY1sKNIt&}K&|GOl ztK9c0Q zr+2G%qCG%j-$qloE;wKyHX7Q0Bioj!{grN?t34JHOZp`eD@!sgnf4S_blO}g?A3w* zc9ULr=%zZvc}*M(O%{&ytlzO?ec!sG3ICp<{TT(1o!olcj_}y%;ziw8Tzuf-D+byk z1M9PCA&GcxlNQcBAUWMUjxCh5h-5w23bOv@g!-pr^|vI{a}6QCe+MGxcpnDP$!vx+ zosg?Rz6Ud2&gqrtGfLt2luomk5Yjg;K{XS@58}~%ggvom2Xj&&wew(~_Bf{4E$5NL zb77yBOl6M$ATozCcc`S6=|qkC6`wUfbHh(wy!E8BM^zrELbw~7b z(dfJAX-qPsN7%OavNOz^0iUon9cT@$uu`7#I1l=1w6HmV)@V^;lr=Pli~}D8Xf>FAKIFw;Uu_u zkW71&Qa}cKlq`$QMlxmGJyf++)4(zH`X4at{s&gD_RzhOe-hS8^JG91JBq(1_FDos zz$ldhJ;Leh5O;`~c3~Aw7g{`el(Ni;S6W1mxVw=2nXiKPN@=0ojtT)0^^1api0GoV zc8C=Ln`ET+oPd!?IAuddgeG6#QB<;WWfKr19%z~79d5kox=SV(4izq2e51gNp5jrK zj}kDt@#taEGR&yg#ms^A=}f}bQ{vTaC>>&lsF^LR7_(t*qD%wXz$+Be4rqf-1)3p~ zM6w|5P)f&4tq6Sy)tlIykP2}wkY{ry%%oTwp8Be66nYmth%E2yE$n+{j347~Ei@p- zq)2gU+gLMc(p0txu?W2j75C$U!N_J{5y}*l{V0s#+J#HNeaMX^+QqGXw!{!7g@x+b zZQ4Ae8nkMp75gt>T>Q@7fDwHdoEqB`r9S|246RCEp>Ht0in-&_mUH%aRmP$0*cu^a z!l-7uh?m1{0M$Wkt_UTNWub3CBoYIwpBP=WYLr)Xd3iN1eZ`)Mi9Ngq!i__XVetn& zUZStkDY>$UB`4=IRnW0Ln^gx3T9C9Hv=|Ubbgej`x{83bhn%%Y=F{eCza)L_qRFc# z^i{pT+R|`&^t<9Dq;fp18|^Qn-BAG3)fYFuDcfU|s1Q%{;%QW*1UVpw-E^V5uCgKz1^~RE`f`0SXl;}`EE02yrQpnJFE41NV5SR-y~9njm4eE8)JTtg zJ#pq|5xo8it~YTB#+yZf0f0Ce32j0=hC1iDD3&rN)*wiECi4^7|E=u;JPo$}zF?rj z>#fggbM?3NjWh>Bt?O5GFDvTKtqlY!Yl5{#ZE1m{*jwsqF0X9INup_HdQ;7!?uPOv zx39c3&0W$@9xcKc4S^#vvG9TA>%ctZxfd8>471IrDHf2;r(`5Y++L9!(HAs!2>R1$ zC2vKJLEH`}rG7hxOnmITjTdiviFSVEhi#K`Y5E zq*n?`r~>}QSTH~d8DKCL4A|$ap=i*BxDxmj)61q~Yec&CBU~nAn#F4Ag4MXF_lmDe zL_x|q9n=bxKV=*Q>qXl1#>W<uv=DcmXxPY6EqcEsCLvNxO}pnA8^IRlBPvmDT_6_f z6LmSL<#bb01Ed*{lfK3|3p|KSgM)&u@&MGG=CWSPQV0kQD0WVo_8ZHZ#jF7q-XiXUU; zfJs&+k_O@|uq}Uh>vG+FD{i}J`OV9t--i^aLNBes-1GqSXlImy1{_>&cxUEd+S2yv zNlJ;&^-S)iXMbVHRpuuz`U?q(QRhT04`q_aOR*&PwgmC1C3&q~A7X{Are$JBx?}T~ zIlvzuUfUG%RSyje3|0F=O>2jJ6%{^S6+sB6AO^puCDgpYk=YPvT(YDw(2(g^&>U*% zd8O2q;VKn>KJ6{ZD)EXxWd(YI{DRrajX#QC&<81JVAy@2)Qp%TJOjhxk#vTcCcokz_+e7DBhoI)%v|Tyj_EPw*xEQq zlKZE+rIklrri*0^L-IS;ufyt(5#{}JoXLak>o>Mp4&x$vrErE3|-B_=` zXWajwA*iU z=!NS=aLU|zF;OdG90ov<(2B@olb{vV?jxHHn>)F=TUr`2G`mp9yEF(eaU(<)LoN zj8~`W(tj&(nUdLe&1DInUnxE>F=RASIXG0zI-o zcQ4S5e~P{teRD)FjJ`2~qcsXfqHpMhdg9v=eg!vNsU0_SjV`P--HbAmD^08w(jarV zc>)K}xbt*(1kRjVXNB&p)vJ3)TJ`Pvx1;Ub_12NzmyNF8kvd2Ej&w&I-jU|-!IZom zzKvY&O2e*!gi{++*6nt59G>M2V?hM3QKXhf92&QaTTi5kTkEg&t_kbgqwV^)x9j0G z>RG@>Z^szT`K%Nq$M*iUF6={Y^hhm$PN%z1cRRnwN2N{3Ry;4BB->~`DWRtTPFStt z$v2GL#TeA$cynj4J?M^v^@!V8m_+aKgpSg+N41UEL$@EayBGlyyJIfL73(kE6{>Z* z5x0!L2+7m`{Xnm=Tp#R@e!1IB+hXkBl4f>CpBT^wmm9qY^bYj=ow(zttUGeEjXEzg z2Y1Zo9bwee;(lnj6I#spl(@y0`>k8(3;76fPd);7d{o;38n2Qx#@&%LGDQpXkGmr} zsB{BH2tJA%E+vUiEYo`rz)|wY2YdBp`p`i13EXfix#LdU@l)0v<66xu4La^v-QMPo zfI=`G4s0sW4;;W9qECRvr66g?Ry0~49Eg4y4>%b8ChvG3_zmZwV0Df4ZY}^Sl{Y&L zcZ^@6*lVx&;2EApAkXhpoX*fK#p_r1j)e7Xdiume+w|~A?@Pur7(z4c>}&d0(#-mi zo|pJ^aNvF5z~rZAT2D{G@Z!N7pU>R&0p=r#RSI-}cZBoGquf zuI_mmH%qv8T)*<(8Mrrtysdi|fbGFsoV8}Im`Z1Vp+6zXRQHblZU?$CcJFPVdmHGk zNEkE9H9q~m#M+u}0+T-pY*DsWrf_~gz z(J$@~>VrxW=$i!n(G`YU(Jvoh4TR_$IRA_%EuM?JKf^=f5&gM&j$tEGX6Qm@FkDbc z4gJT0=N_8S)77Zwi1ym#pmk4z=bp59Zmy@N@SJUp%K#nu^fW24z;l>$lv(QO(RVG5 z&lY^5I-3j+y}NSu-!OL18MP*s=KPaAd3&o@uP^t>4l;WQ!wZ=Kc0S3Js)SKvQT$op zm`Yp)xVTMMB^{PrDGr*&?y!(L_1gzIOKnxuLv2+zc4Tbq=vdu^w_{7s(k&fc#jnmj zD9UYBgV*njM0Q?3xNygCW5e){MO9#^HGPy>3AE@oEhTZZFbxY6P-2EFTGYYgAw`r$ zi~fg#M?k~U9?&pR2a-To9XkkuMG>=T$8baA@Q#HRQ5G?vW~DX=e!7~;auRxm@|$ta z$oU9Fl5(2qFF}v6NZOF_OdN*Z9y?wv!9sbvq#;mL6lf@ETGt&4b+2oBTk?)RD_A7k zQ(IP6>&f0y+Pbo}b>+fRn`aW}iR&H8jWVU}rFRk81wfC~`__?XZOEZ%@QAN&XfS05 z&|-SqqQi|%4@H4`x1wiIkW;#lRJ4|EvFK9d=-MPW_&1pAt@ID&CB^5qGH$wcp15fq z6T`g*40I02o)Z>@U!=N}^okg9aUhxMP8wh^HjIC`WI(Se?+W`DZ#%ML=(>r;zp5}8E^mNBH@>{ZVFujo%f}t z)7{omTCMl*gMosvwp#;T9Y|_Tn5{u({}{FyU`2o%;J<{XGiW1>HnqeonOw94XQ>I& zMdD|97^e*8p)m#{M}HhPNke%!SXkA!rESr+Wi`EhOGd}rC$1Y@ab(+Kf4Hl>MjyCD z@6d1F*RNNXwzPHEv~E}$tn3+YTYIo+S>*1EmLDEp7~XJbS!8^9yE|Yywy-6~j7j9x z{|ST&cDx2(Q)E8G@bC)+yLPmE|va8 z9Lb0FGRCI}B?8Mi!U=LcH%gRZ79h8i*jlOFQf@os{~AwKfjg96;LU?8syhCSF`1u{ zm6MyZ7#~{&wB=4?xBeaD6?n+uy*4Q)rDPVF4Zli4WtL9WDAhVAl4krD8|AVa<(juq zVjIKdC4thb%<|wBE&7;ot^SnpcW6wX zjQ-&Uap)Qm(b$S=F**!j6ou2Wkft3+;VxVZipD@;b~A7D$MmY`OUB=AMEkYZjGg+l z@naC)%2l7eD_X`%sxD&BaCDt7feBh+YH(L9h77(@o>akPL3&eFMMV`ZV`soq=n05F zMB)DsqwziC44zQNb40Ts_UJaEdazD0OL&$V4iauXl#By6=yTFF%mcRIsE&JfTf)0< zRhE}m;(9>68oT}OLbpHZ4=DSjF>Iun9`ujc`NY&43j}3pc$&;&pFkO7`b7Uq%Fq?3 z6^X)IT?@rh8*#tXJF%??moe;ntmy$<=;dD;L;B}&MjoprURCMZ@7SB2A}Mmi+ud^KO92}lNmIqc?snsWUd*+i>`BgP`t%##;Ra<3Ayv)^Lo9HJ9uYL0#$? z#b}XF=BkSiOy>iU=}0^<7o4hBwip;jAG_E*E5ktEe2#D!Q{cRC~ zoN{{8(qUKew#v-1?Ci44%5BB2VPoyW#-^(6ro&-wuWD*sh@N}U=+GZFm!MyKT0eV6 zo~Ol=-ROm#o0fHI6pA$mNXP_tcu(ch}cKnaw{jAZhpE4;E8OnbxBMU6b$dNn^h_Ct2Y0yKCa4E*+gyduH zU1r)I2d16_QNIrCS$2R+90&(SE<$qADHH@dJu3>d+GIm;WArE2x!4`b24QT?dHsTV zxMlaUqa7n79lXl?{xV!ce$S)+cfB-_gC2EZ7o;nePwh>VXK|(P9PnT~Wcsx*UYU7q9j-}S)3|QJ zbpqE(T&Hk7jVl|oIajokdX3{?=S4?|82G#`7Kwl~=$$R?Sq%#++(n-9Mo)2Pfg{V6 z<*P3aHa=gT+nQaFo0pBX@!ky%(-92011`k>Xdgt2Hp5EsaK9=KaZ#(gcm&4Ij$r%3 z2H@7ifBCoqxLR@b;~K-Y3)hvnj^kPYWh#v!vQ-I&%MCdJ?{cKJ4Kl*cCHH1Yt*>Bd zTl&y~%6hl6I5*#u*^;&R`2|J(lKQR+SBbaETezSwH!rKHxD+F(=R!k#RA`7==^j*; z!dy~};!$$2zGYR#rN&U^w*f7i+ReDfTet^yvSOBCLt;MK!mLJICV;^(v-s-b{1@A( z5#1(dOIQ@}F|0ru6Ee_;SeO%|MJ3Mt)r{}rnF)zX+)e$Npvb@IEi3clx*`(T)W;Ia z#@_PM{L=DrcR^{nf5CTh9<<+L5Biufsr@CaSf{x<3e=lx_}OXWTJ0mSot);HSlv^` zPHhFO9;dlBRyS$v*8UuJ3U;o?>aH?|wHIL_IL&pjx^Edn+Rf-4r#Tj@JC3?8QMW!; zx4~GYoraEenj7NkHlVH$bsJ-KTaC5a|AF3dnwz38D4K6FI<#9LbDid9`8`da{e;o0 z9WZlXK&aTw@Xa5>}nuYs1!DSk5;Y#44O25An*W4>wigd${^=t-~b_Q9q7? z_zeY$DCCPmg4uBs%?=2bToE6PAX-e>>!GK)6e&nHLud=schz|_x?Dw#fk0!Ct1H7> z*X8fszSUUnt!;G&19X`Ng6`H@?`{4^n_$cTKW!0`yIC!A&)FgqcHJB;`pnjC@B$>a zNN;L-q$1X$6Bwfk?SYgrI>(5pG2^x|HHycrp}Ej80{&yQs2tio2b!TAtDbVKtjn>q zC`ZghIn+A7hC!{s%q$ibcFIu3Pt`d(;GkE*?kdfmul&Sbc*Cou=ZGDr_95zU{cIKFSG`OBY7wvW{Z!u1Gl9Hg&kXrQY}hf z##Z~FRx5eCVNOnn%nK&W(LEKVfEOwWFVq5F5O4Ady!o~Hi&DGmTe*N4a&lVTm3bLi z**Q63tj^8Z?Z$fT0nBqo6IUQv0L@J_JKq+J~*x?cZWc`AWTQceomxQ0jJG(?>! z+$9RqtkS@1*7^xlfiOcYXH?$u;+J3`*$@Kp8kD8~Z zn*ZpYBx1!|Qk>YEOD?Q8J)n)?pzKl5cOK$Cux+AuXA))Rbkn2$BXg2vpdb77QZdGv z+8=OkWC(j)X;MxJW`N9qro|QTdjkQlKj3Pq zDb)-lIO!K$ZK7xXw?~7|BwT{gFylu9`qYX434TO>4)*$i91g)%VKp3rkF?*ICt$G@ zYIk!q@)aM6J-O_ACS}YAAIZJB>+q`h2;U$>O`}E>PFMvV7_Rt;1#VCle3S=1Vumgz zjHTSnz$-0sP!8_3jXi136RaqTGx``FfwnXsX|4{qz3uHs9jZYmlnHprYSk)h&WCsK30dHP09C*h#?^;w9WF5-V<@OzX94<((9bStFm7ZDDAq^NbgY>? z$$%gk5pqrJNykV4A?E@UI|-!L0~#(5%MoH083$uPI8w3L5V;ziRWUf6$}9fO$?F#% zJ-YaM^G>O$Vk!%T`)+IAzrXo5ta6XR|Nb&GzDK*1h@8F3WyI!9Ad*(jNnF7A)3EZ^ zuzZJ1+4;YW^P6da_n~ej&Mym~{!TMP|BiVpsK<`l^H6VMdyKN2*#te-1UV3xi2og~ zin(BYiSe5mak#z$_(6vDG1IR+@S&J;Yy0kbfK3wH$ge3g|x-vhY# zYGzcx5m1rzhm?I1yB*War_rZQ?vk0$6Tq2(VMv2OkZUxt`!B{=lU9F+m0_ z=OlC6q+luBUa*~fj$%7eLlIDc@6gX68s5(z%%EjZdvq_DA8C0sfT!GGyak;9G-Tbf z1e_mMF%!p1$W`R4;Tyr=&&^{;pb<&_gwr-+e}nN`JO{Ffh<%(FLvZPoUJfw77r!_ zN?&=ouQKQk*LiLwi1j~0=Y>L3$#e>6DuqtHk%m2*94)Zvnt}J?VVs#2uuQ|a6q|Z2fuaii_(*XP3tz&XMYcKc5||Lz`wK~7dQa~9 zgg%J3yU}jjB6lXYeC51Y($pM1VSXV7C&9i2ums!un<;%_&Tcb)qkRT5OX5ol(JKO9 zN>QHl{JnozN!mycTei})fR!qMD8XP#_#;rI-)IkmB8e*Xfg*t_g@rwc>HM51{7-;M zNmSmUz@#J+#aqys*mKS2?78;`Atln|a1lb9lNj*!2IFUXsTjvhZ98ZYvN$GnCo$PD zg_DGGKWn9gltP)f3Z*3Va14mJ09eT6fL?Pav9~)x%Vy&>?LkK$M$oJ6kp03Z$0p`SI978cHj`VcY1?3ow&X}a;oreooH217(#g|(HHwS_=_5Da17HbvxT%2Qh1 zQc@B0mX~{j6(udz68ZU=;5?!*{|$Zk9>E+jzOAYg<%nv(aZo}RYE$S%k$KI6ZMbLd}rh5=A0VDXMU!gcC-n6=4R03mr`G%E~+0DLNiI&MXs$B{=$e9 ziNE~+1G_ldBw_&vfrY<>KBiz7m~Y2te`EfG@eSD1W7-#)xn#YzL0Htx?^qqn%uvOE z(W<#8s?j#p<{-l~n3svF~s-@TLK6!LPWJoqyBt7xAX3jfnRp;;#>XX&m^3 zNy*#=vh=uCB|I+tv<_4Z2m-l#a}2-0B1Xijiiq1-u&{MN24XIp1wkiMq|;KssTrn7 zpRU4N83M`Q)P{_<;>xhMvZJ8Sw`FnTNJk*hG19nri?6SsqtY9$ERO#1NKuzuvSzP`&w>I;L7-j>0!g^HkaBpai zmVrJ_L%$OPq;s6R)%YpLs!o(TQ0hgAi$`QlKqfIidN$&c9ci6T;;Km#U7T9a?spV7 zZ&=o{;Q8klv@F}uTvV9u%36K%?#|BLH?Pier5FDGmb$^6i&LtL7VjLaGj{lwtZKZX jan%yPc~I9oci%EHa?9>cT|a2fw(l6KORlUN+Nu3t`-sRg literal 0 HcmV?d00001 diff --git a/Morpho/composeApp/src/commonMain/composeResources/font/IBMPlexSans-MediumItalic.ttf b/Morpho/composeApp/src/commonMain/composeResources/font/IBMPlexSans-MediumItalic.ttf new file mode 100644 index 0000000000000000000000000000000000000000..7787ad27420f67ca33fdfeb936f241712d202076 GIT binary patch literal 185720 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 literal 0 HcmV?d00001 From fcf7b220fddc1befc548e4d888309194e32135a4 Mon Sep 17 00:00:00 2001 From: Orual Date: Wed, 18 Sep 2024 09:40:34 -0400 Subject: [PATCH 20/42] K, things are loading again, at least partially. Also discovered that Bluesky reworked some of the label stuff again, bringing "defaultSetting" and the "Visibility" enum in line with eachother and adding a new "Severity" value, which was causing some errors. --- Butterfly | 2 +- Morpho/composeApp/build.gradle.kts | 5 ++ .../morpho/app/data/ContentLabelService.kt | 19 ++++++- .../com/morpho/app/data/MorphoDataSource.kt | 9 +++- .../com/morpho/app/model/bluesky/BskyLabel.kt | 42 +++++++++------ .../app/model/bluesky/FeedSourceInfo.kt | 24 +++++++-- .../app/model/uidata/ContentLabelService.kt | 30 ++++++++--- .../morpho/app/model/uidata/FeedPresenter.kt | 54 +++++-------------- .../com/morpho/app/model/uidata/UIUpdate.kt | 10 ++-- .../app/screens/main/MainScreenModel.kt | 6 +-- .../app/screens/main/tabbed/TabbedHomeView.kt | 26 +++++++-- .../main/tabbed/TabbedMainScreenModel.kt | 7 ++- .../notifications/NotificationsView.kt | 27 ++++++++-- 13 files changed, 171 insertions(+), 90 deletions(-) diff --git a/Butterfly b/Butterfly index 9e114b9..046d425 160000 --- a/Butterfly +++ b/Butterfly @@ -1 +1 @@ -Subproject commit 9e114b96100c953e9de2fd8d1bd21807f48023be +Subproject commit 046d425000fc7f3ff99642509916e5ecc8874aaa diff --git a/Morpho/composeApp/build.gradle.kts b/Morpho/composeApp/build.gradle.kts index 327bf56..f40ca6d 100644 --- a/Morpho/composeApp/build.gradle.kts +++ b/Morpho/composeApp/build.gradle.kts @@ -225,6 +225,11 @@ kotlin { implementation(compose.desktop.currentOs) } } + getByName("commonMain") { + dependencies { + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.1") + } + } } } diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/data/ContentLabelService.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/data/ContentLabelService.kt index 9b52a8d..84b93ed 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/data/ContentLabelService.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/data/ContentLabelService.kt @@ -10,7 +10,21 @@ import com.morpho.app.model.bluesky.BskyPost import com.morpho.app.model.bluesky.MorphoDataItem import com.morpho.app.model.bluesky.toAtProtoLabel import com.morpho.app.model.bluesky.toListVewBasic -import com.morpho.butterfly.* +import com.morpho.butterfly.AtUri +import com.morpho.butterfly.ContentHandling +import com.morpho.butterfly.Did +import com.morpho.butterfly.InterpretedLabelDefinition +import com.morpho.butterfly.LabelAction +import com.morpho.butterfly.LabelCause +import com.morpho.butterfly.LabelDescription +import com.morpho.butterfly.LabelIcon +import com.morpho.butterfly.LabelSource +import com.morpho.butterfly.LabelTarget +import com.morpho.butterfly.LabelValueDefFlag +import com.morpho.butterfly.LabelValueID +import com.morpho.butterfly.LabelerID +import com.morpho.butterfly.ModerationPreferences +import com.morpho.butterfly.MutedWordTarget import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob @@ -171,8 +185,9 @@ class ContentLabelService: KoinComponent { noOverride = !localLabelDef.configurable, priority = when (localLabelDef.severity) { Severity.INFORM -> 5 - Severity.ALERT -> 1 + Severity.ALERT -> 2 Severity.NONE -> 8 + Severity.WARN -> 1 }, downgraded = false, ) to localLabelDef.toContentHandling( diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/data/MorphoDataSource.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/data/MorphoDataSource.kt index cd9df84..f65b6ad 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/data/MorphoDataSource.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/data/MorphoDataSource.kt @@ -4,7 +4,11 @@ import androidx.compose.ui.util.fastAny import app.cash.paging.PagingConfig import app.cash.paging.PagingSource import app.cash.paging.PagingState -import com.morpho.app.model.bluesky.* +import com.morpho.app.model.bluesky.BskyPostReason +import com.morpho.app.model.bluesky.BskyPostThread +import com.morpho.app.model.bluesky.MorphoDataItem +import com.morpho.app.model.bluesky.ThreadPost +import com.morpho.app.model.bluesky.toPost import com.morpho.app.model.uidata.ContentLabelService import com.morpho.app.model.uidata.Delta import com.morpho.app.model.uidata.Moment @@ -26,6 +30,7 @@ import kotlin.time.Duration abstract class MorphoDataSource: PagingSource(), KoinComponent { val agent: MorphoAgent by inject() val moderator: ContentLabelService by inject() + override val keyReuseSupported: Boolean = true override fun getRefreshKey(state: PagingState): Cursor? { return state.anchorPosition?.let { anchorPosition -> @@ -85,7 +90,7 @@ data class MorphoFeedSource( is PagedResponse.Profile -> pagedList.items } LoadResult.Page( - data = tunedList as List, + data = tunedList.toList(), prevKey = when(params) { is LoadParams.Append -> loadCursor is LoadParams.Prepend -> Cursor.Empty diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/BskyLabel.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/BskyLabel.kt index 94db7f2..ec5d437 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/BskyLabel.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/BskyLabel.kt @@ -5,7 +5,13 @@ package com.morpho.app.model.bluesky import androidx.compose.runtime.Immutable import androidx.compose.ui.text.intl.Locale import app.bsky.actor.Visibility -import com.atproto.label.* +import com.atproto.label.Blurs +import com.atproto.label.DefaultSetting +import com.atproto.label.Label +import com.atproto.label.LabelValueDefinition +import com.atproto.label.LabelValues +import com.atproto.label.SelfLabel +import com.atproto.label.Severity import com.morpho.app.model.uidata.MaybeMomentParceler import com.morpho.app.model.uidata.Moment import com.morpho.app.model.uidata.MomentParceler @@ -78,20 +84,20 @@ data class BskyLabel( return result } - fun getLabelValue(): LabelValue? { + fun getLabelValue(): LabelValues? { return when (value) { - LabelValue.PORN.value -> LabelValue.PORN - LabelValue.GORE.value -> LabelValue.GORE - LabelValue.NSFL.value -> LabelValue.NSFL - LabelValue.SEXUAL.value -> LabelValue.SEXUAL - LabelValue.GRAPHIC_MEDIA.value -> LabelValue.GRAPHIC_MEDIA - LabelValue.NUDITY.value -> LabelValue.NUDITY - LabelValue.DOXXING.value -> LabelValue.DOXXING - LabelValue.DMCA_VIOLATION.value -> LabelValue.DMCA_VIOLATION - LabelValue.NO_PROMOTE.value -> LabelValue.NO_PROMOTE - LabelValue.NO_UNAUTHENTICATED.value -> LabelValue.NO_UNAUTHENTICATED - LabelValue.WARN.value -> LabelValue.WARN - LabelValue.HIDE.value -> LabelValue.HIDE + LabelValues.PORN.value -> LabelValues.PORN + LabelValues.GORE.value -> LabelValues.GORE + LabelValues.NSFL.value -> LabelValues.NSFL + LabelValues.SEXUAL.value -> LabelValues.SEXUAL + LabelValues.GRAPHIC_MEDIA.value -> LabelValues.GRAPHIC_MEDIA + LabelValues.NUDITY.value -> LabelValues.NUDITY + LabelValues.DOXXING.value -> LabelValues.DOXXING + LabelValues.DMCA_VIOLATION.value -> LabelValues.DMCA_VIOLATION + LabelValues.NO_PROMOTE.value -> LabelValues.NO_PROMOTE + LabelValues.NO_UNAUTHENTICATED.value -> LabelValues.NO_UNAUTHENTICATED + LabelValues.WARN.value -> LabelValues.WARN + LabelValues.HIDE.value -> LabelValues.HIDE else -> null } } @@ -106,6 +112,8 @@ enum class LabelSetting { WARN, @SerialName("hide") HIDE, + @SerialName("show") + SHOW, } fun DefaultSetting.toLabelSetting(): LabelSetting { @@ -113,13 +121,14 @@ fun DefaultSetting.toLabelSetting(): LabelSetting { DefaultSetting.IGNORE -> LabelSetting.IGNORE DefaultSetting.WARN -> LabelSetting.WARN DefaultSetting.HIDE -> LabelSetting.HIDE + DefaultSetting.SHOW -> LabelSetting.SHOW } } fun Visibility.toLabelSetting(): LabelSetting { return when (this) { - Visibility.SHOW -> LabelSetting.IGNORE + Visibility.SHOW -> LabelSetting.SHOW Visibility.WARN -> LabelSetting.WARN Visibility.HIDE -> LabelSetting.HIDE Visibility.IGNORE -> LabelSetting.IGNORE @@ -142,9 +151,10 @@ data class BskyLabelDefinition( ): Parcelable { fun getVisibility(): Visibility { return when(defaultSetting) { - LabelSetting.IGNORE -> Visibility.SHOW + LabelSetting.IGNORE -> Visibility.IGNORE LabelSetting.WARN -> Visibility.WARN LabelSetting.HIDE -> Visibility.HIDE + LabelSetting.SHOW -> Visibility.SHOW null -> Visibility.IGNORE } } diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/FeedSourceInfo.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/FeedSourceInfo.kt index a388845..c122753 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/FeedSourceInfo.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/FeedSourceInfo.kt @@ -7,7 +7,12 @@ import app.bsky.feed.GetFeedGeneratorQuery import app.bsky.graph.GetListQuery import app.bsky.graph.ListView import com.morpho.app.model.uidata.ContentCardMapEntry -import com.morpho.butterfly.* +import com.morpho.butterfly.AtUri +import com.morpho.butterfly.ButterflyAgent +import com.morpho.butterfly.Cid +import com.morpho.butterfly.Did +import com.morpho.butterfly.Handle +import com.morpho.butterfly.Nsid import dev.icerock.moko.parcelize.Parcelable import dev.icerock.moko.parcelize.Parcelize import kotlinx.serialization.Serializable @@ -59,6 +64,7 @@ sealed interface FeedSourceInfo: Parcelable { val creatorHandle: Handle val feedDescriptor: FeedDescriptor val type: Nsid + val pinned: Boolean? @Serializable @Immutable @@ -71,6 +77,7 @@ sealed interface FeedSourceInfo: Parcelable { override val creatorDid: Did, override val creatorHandle: Handle, override val feedDescriptor: FeedDescriptor, + override val pinned: Boolean? = null, ): FeedSourceInfo { override val type: Nsid = Nsid("app.bsky.feed.generator") } @@ -88,6 +95,7 @@ sealed interface FeedSourceInfo: Parcelable { override val feedDescriptor: FeedDescriptor, val likeCount: Long? = null, val likeUri: AtUri? = null, + override val pinned: Boolean? = null, ): FeedSourceInfo { override val type: Nsid = Nsid("app.bsky.graph.list") } @@ -104,6 +112,7 @@ sealed interface FeedSourceInfo: Parcelable { override val creatorHandle: Handle = Handle("${displayName.lowercase()}.morpho.app") override val feedDescriptor: FeedDescriptor = FeedDescriptor.Home override val type: Nsid = Nsid("app.morpho.feed.home") + override val pinned: Boolean = true } @Serializable @@ -118,6 +127,7 @@ sealed interface FeedSourceInfo: Parcelable { override val creatorHandle: Handle = Handle("${displayName.lowercase()}.morpho.app") override val feedDescriptor: FeedDescriptor = FeedDescriptor.Home override val type: Nsid = Nsid("app.morpho.feed.following") + override val pinned: Boolean = true } } @@ -162,11 +172,19 @@ suspend fun app.bsky.actor.SavedFeed.toFeedSourceInfo(agent: ButterflyAgent): Re return when(this.type) { FeedType.FEED -> { agent.api.getFeedGenerator(GetFeedGeneratorQuery(AtUri(this.value))) - .map { feed -> feed.view.hydrateFeedGenerator() } + .map { feed -> + feed.view.hydrateFeedGenerator().copy( + pinned = this.pinned + ) + } } FeedType.LIST -> { agent.api.getList(GetListQuery(AtUri(this.value), 1)) - .map { list -> list.list.hydrateList() } + .map { list -> + list.list.hydrateList().copy( + pinned = this.pinned + ) + } } FeedType.TIMELINE -> Result.success(FeedSourceInfo.Following) } diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/ContentLabelService.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/ContentLabelService.kt index b6bbf0c..c60807b 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/ContentLabelService.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/ContentLabelService.kt @@ -11,11 +11,25 @@ import com.morpho.app.model.bluesky.BskyPost import com.morpho.app.model.bluesky.MorphoDataItem import com.morpho.app.model.bluesky.toAtProtoLabel import com.morpho.app.model.bluesky.toListVewBasic -import com.morpho.butterfly.* +import com.morpho.butterfly.AtUri +import com.morpho.butterfly.ContentHandling +import com.morpho.butterfly.Did +import com.morpho.butterfly.InterpretedLabelDefinition +import com.morpho.butterfly.LabelAction +import com.morpho.butterfly.LabelCause +import com.morpho.butterfly.LabelDescription +import com.morpho.butterfly.LabelIcon +import com.morpho.butterfly.LabelSource +import com.morpho.butterfly.LabelTarget +import com.morpho.butterfly.LabelValueDefFlag +import com.morpho.butterfly.LabelValueID +import com.morpho.butterfly.LabelerID +import com.morpho.butterfly.ModerationPreferences +import com.morpho.butterfly.MutedWordTarget import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob -import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking import org.koin.core.component.KoinComponent import org.koin.core.component.inject import org.lighthousegames.logging.logging @@ -50,9 +64,12 @@ class ContentLabelService: KoinComponent { private set init { - serviceScope.launch { - agent.getLabelDefinitions(modPrefs) - agent.getLabelersDetailed(labelers.keys.map { Did(it) }) + runBlocking { + labelDefinitions = agent.getLabelDefinitions(modPrefs) + val details = agent.getLabelersDetailed(labelers.keys.map { Did(it) }).getOrNull()?.associateBy { + it.creator.did.did + } + labelerDetails = details ?: emptyMap() } } @@ -172,8 +189,9 @@ class ContentLabelService: KoinComponent { noOverride = !localLabelDef.configurable, priority = when (localLabelDef.severity) { Severity.INFORM -> 5 - Severity.ALERT -> 1 + Severity.ALERT -> 2 Severity.NONE -> 8 + Severity.WARN -> 1 }, downgraded = false, ) to localLabelDef.toContentHandling( diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/FeedPresenter.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/FeedPresenter.kt index 8014e2f..1390f2d 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/FeedPresenter.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/FeedPresenter.kt @@ -3,13 +3,15 @@ package com.morpho.app.model.uidata import app.bsky.feed.GetAuthorFeedFilter import app.bsky.feed.GetFeedQuery import app.bsky.feed.GetListFeedQuery -import app.bsky.graph.GetListQuery import app.cash.paging.Pager import app.cash.paging.cachedIn import com.morpho.app.data.FeedTuner import com.morpho.app.data.MorphoDataSource import com.morpho.app.data.MorphoFeedSource -import com.morpho.app.model.bluesky.* +import com.morpho.app.model.bluesky.AuthorFilter +import com.morpho.app.model.bluesky.FeedDescriptor +import com.morpho.app.model.bluesky.FeedSourceInfo +import com.morpho.app.model.bluesky.MorphoDataItem import com.morpho.butterfly.ButterflyAgent import com.morpho.butterfly.Cursor import com.morpho.butterfly.FeedRequest @@ -19,7 +21,7 @@ import kotlinx.coroutines.flow.map class FeedPresenter( - descriptor: FeedDescriptor? = null, + var descriptor: FeedDescriptor? = null, ): PagedPresenter() { private var dataSource: MorphoFeedSource = @@ -32,59 +34,27 @@ class FeedPresenter( } } - private fun switchPager(newDataSource: MorphoFeedSource) { - dataSource = newDataSource - pager = Pager(MorphoDataSource.defaultConfig) { - dataSource - } - } + override fun produceUpdates(events: Flow): Flow = events.map { event -> when(event) { - is Event.ComposePost -> UIUpdate.OpenComposer(event.post, event.role) is FeedEvent.Load -> { - switchPager(event.descriptor.getDataSource(agent)) when(event.descriptor) { is FeedDescriptor.Author -> AuthorFeedUpdate.Feed( event.descriptor.did, event.descriptor.filter, pager.flow.cachedIn(presenterScope)) is FeedDescriptor.FeedGen -> { - val info = agent.api - .getList(GetListQuery(event.descriptor.uri, 1)) - .map { it.list.hydrateList() } - if(info.isSuccess) { - switchPager(info.getOrThrow().getDataSource(agent)) - FeedUpdate.Feed(info.getOrThrow(), pager.flow.cachedIn(presenterScope)) - } else { - FeedUpdate.Error(info.exceptionOrNull()?.message ?: - "Failed to load saved feed: ${event.descriptor}, error: $info") - } + FeedUpdate.Feed(event.uri, pager.flow.cachedIn(presenterScope)) } FeedDescriptor.Home -> FeedUpdate.Feed( - FeedSourceInfo.Home, pager.flow.cachedIn(presenterScope)) + FeedSourceInfo.Home.uri, pager.flow.cachedIn(presenterScope)) is FeedDescriptor.Likes -> AuthorFeedUpdate.Likes( event.descriptor.did, pager.flow.cachedIn(presenterScope)) - is FeedDescriptor.List -> FeedUpdate.Error( - "Internal error: LoadLists should not be sent to this presenter") - } - } - is FeedEvent.LoadLists -> FeedUpdate.Error( - "Internal error: LoadLists should not be sent to this presenter") - is FeedEvent.LoadHydrated -> { - switchPager(event.info.getDataSource(agent)) - FeedUpdate.Feed(event.info, pager.flow.cachedIn(presenterScope)) - } - is FeedEvent.LoadSaved -> { - val info = event.info.toFeedSourceInfo(agent) - if(info.isSuccess) { - switchPager(info.getOrThrow().getDataSource(agent)) - FeedUpdate.Feed(info.getOrThrow(), pager.flow.cachedIn(presenterScope)) - } else { - FeedUpdate.Error(info.exceptionOrNull()?.message ?: - "Failed to load saved feed: ${event.info}") + is FeedDescriptor.List -> { + FeedUpdate.Feed(event.uri, pager.flow.cachedIn(presenterScope)) + } } } - is FeedEvent.Peek -> FeedUpdate.Peek(event.info, dataSource.updates()) - else -> FeedUpdate.Error("Unknown event type: $event") + else -> UIUpdate.NoOp } } } diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/UIUpdate.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/UIUpdate.kt index 7fff47d..1a9974f 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/UIUpdate.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/UIUpdate.kt @@ -1,9 +1,13 @@ package com.morpho.app.model.uidata import app.cash.paging.PagingData -import com.morpho.app.model.bluesky.* +import com.morpho.app.model.bluesky.AuthorFilter +import com.morpho.app.model.bluesky.BskyPost +import com.morpho.app.model.bluesky.BskyPostThread +import com.morpho.app.model.bluesky.MorphoDataItem import com.morpho.app.ui.common.ComposerRole import com.morpho.butterfly.AtIdentifier +import com.morpho.butterfly.AtUri import kotlinx.coroutines.flow.Flow sealed interface UIUpdate { @@ -44,12 +48,12 @@ sealed interface FeedUpdate: UIUpdate { data class Error(val error: String): FeedUpdate data class Feed( - val info: FeedSourceInfo, + val uri: AtUri, val feed: Flow>, ): FeedUpdate data class Peek( - val info: FeedSourceInfo, + val uri: AtUri, val post: Flow, ): FeedUpdate } diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/main/MainScreenModel.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/main/MainScreenModel.kt index 1648c4d..238c17d 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/main/MainScreenModel.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/main/MainScreenModel.kt @@ -16,7 +16,7 @@ import com.morpho.app.model.uistate.ContentCardState import com.morpho.app.screens.base.BaseScreenModel import com.morpho.butterfly.AtUri import kotlinx.coroutines.flow.emitAll -import kotlinx.coroutines.flow.filterIsInstance +import kotlinx.coroutines.flow.merge import kotlinx.coroutines.launch import org.lighthousegames.logging.logging @@ -49,7 +49,7 @@ open class MainScreenModel: BaseScreenModel() { source.uri to FeedPresenter(source.feedDescriptor) }) feedStates.putAll(feedSources.map { source -> - source.uri to ContentCardState.Skyline(source.uri) + source.uri to ContentCardState.Skyline(source.uri) }) @@ -57,7 +57,7 @@ open class MainScreenModel: BaseScreenModel() { feedPresenters.forEach { (source, presenter) -> val entry = feedStates[source]?: return@forEach entry.updates.emitAll( - presenter.produceUpdates(entry.events.filterIsInstance()) + presenter.produceUpdates(merge(globalEvents, entry.events)) ) } } diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/main/tabbed/TabbedHomeView.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/main/tabbed/TabbedHomeView.kt index 1413cf9..c5a8c38 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/main/tabbed/TabbedHomeView.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/main/tabbed/TabbedHomeView.kt @@ -10,11 +10,24 @@ import androidx.compose.animation.core.spring import androidx.compose.animation.slideInHorizontally import androidx.compose.animation.slideOutHorizontally import androidx.compose.animation.togetherWith -import androidx.compose.foundation.layout.* -import androidx.compose.material3.* +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.statusBarsPadding +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.SecondaryScrollableTabRow +import androidx.compose.material3.Tab +import androidx.compose.material3.TabRowDefaults import androidx.compose.material3.TabRowDefaults.tabIndicatorOffset -import androidx.compose.runtime.* +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.IntOffset @@ -33,6 +46,7 @@ import cafe.adriel.voyager.transitions.ScreenTransition import cafe.adriel.voyager.transitions.ScreenTransitionContent import coil3.annotation.ExperimentalCoilApi import com.morpho.app.model.uidata.Event +import com.morpho.app.model.uidata.FeedEvent import com.morpho.app.model.uistate.ContentCardState import com.morpho.app.screens.base.tabbed.TabScreen import com.morpho.app.ui.common.LoadingCircle @@ -99,11 +113,14 @@ fun TabScreen.TabbedHomeView( var selectedTabIndex by rememberSaveable { mutableIntStateOf(sm.timelineIndex) } - val tabs = remember( sm.tabs, sm.loaded, sm.tabs.size ) { + List(sm.tabs.size) { index -> + val uri = sm.tabs[index].uri + val desc = sm.feedPresenters[uri]?.descriptor + desc?.let { FeedEvent.Load(it) }?.let { sm.sendGlobalEvent(it) } HomeSkylineTab( index = index.toUShort(), title = sm.tabs[index].title, @@ -135,6 +152,7 @@ fun TabScreen.TabbedHomeView( } else nav.replace(tabs[index]) } else if(index > selectedTabIndex) nav.push(tabs[index]) selectedTabIndex = index + } ) diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/main/tabbed/TabbedMainScreenModel.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/main/tabbed/TabbedMainScreenModel.kt index 474b109..9deaacc 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/main/tabbed/TabbedMainScreenModel.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/main/tabbed/TabbedMainScreenModel.kt @@ -28,7 +28,6 @@ class TabbedMainScreenModel : MainScreenModel() { val timelineIndex = agent.prefs.timelineIndex ?: agent.prefs.saved.indexOfFirst { it.type == FeedType.TIMELINE }.let { if(it == -1) 0 else it } - val lastPinnedIndex = agent.prefs.saved.indexOfLast { it.pinned } var loaded by mutableStateOf(false) @@ -42,10 +41,10 @@ class TabbedMainScreenModel : MainScreenModel() { while(!initialized) { delay(10) } - for(i in 0 .. lastPinnedIndex) { - val source = feedSources[i] - tabs.add(source.toContentCardMapEntry()) + feedSources.filter { it.pinned == true }.forEach { + tabs.add(it.toContentCardMapEntry()) } + loaded = true } diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/notifications/NotificationsView.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/notifications/NotificationsView.kt index 832cf6a..17a79e7 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/notifications/NotificationsView.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/notifications/NotificationsView.kt @@ -1,6 +1,10 @@ package com.morpho.app.screens.notifications -import androidx.compose.foundation.layout.* +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material.icons.Icons @@ -9,9 +13,24 @@ import androidx.compose.material.icons.filled.Settings import androidx.compose.material.pullrefresh.PullRefreshIndicator import androidx.compose.material.pullrefresh.pullRefresh import androidx.compose.material.pullrefresh.rememberPullRefreshState -import androidx.compose.material3.* -import androidx.compose.runtime.* +import androidx.compose.material3.CenterAlignedTopAppBar +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.IconToggleButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.material3.rememberModalBottomSheetState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateListOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.unit.Dp @@ -156,7 +175,7 @@ fun TabScreen.NotificationViewContent( val notifications = pager.collectNotifications(toMarkRead) items( - count = pager.itemCount, + count = notifications.size, //key = { index -> notifications[index].hashCode() }, contentType = { NotificationsListItem From c84dce855001dea915a859daa39914f3b15e1ef3 Mon Sep 17 00:00:00 2001 From: Orual Date: Wed, 18 Sep 2024 12:28:11 -0400 Subject: [PATCH 21/42] More things now loading. Noting that the paging thing is loading very few things at once and making a lot of requests. I think we're filtering out too many things from the feed somehow? --- Butterfly | 2 +- .../androidMain/kotlin/Platform.android.kt | 8 + .../kotlin/com/morpho/app/Platform.android.kt | 9 - .../kotlin/com/morpho/app/Previews.kt | 26 +++ .../com/morpho/app/data/MorphoDataSource.kt | 16 +- .../kotlin/com/morpho/app/di/AppModule.kt | 8 +- .../app/model/bluesky/BskyLabelService.kt | 3 +- .../morpho/app/model/uidata/FeedPresenter.kt | 5 +- .../app/model/uidata/ProfilePresenters.kt | 76 +++++--- .../app/model/uistate/ContentCardState.kt | 20 ++- .../app/screens/base/BaseScreenModel.kt | 19 +- .../app/screens/base/tabbed/NavigationTabs.kt | 25 ++- .../app/screens/main/MainScreenModel.kt | 16 +- .../app/screens/main/tabbed/TabbedHomeView.kt | 10 +- .../main/tabbed/TabbedMainScreenModel.kt | 5 +- .../app/screens/profile/TabbedProfileView.kt | 102 +++++------ .../app/ui/post/NotFoundPostFragment.kt | 162 +++--------------- .../kotlin/com/morpho/app/Previews.kt | 25 +++ Morpho/gradle/libs.versions.toml | 2 +- gradle/libs.versions.toml | 2 +- 20 files changed, 288 insertions(+), 253 deletions(-) delete mode 100644 Morpho/composeApp/src/androidMain/kotlin/com/morpho/app/Platform.android.kt create mode 100644 Morpho/composeApp/src/androidMain/kotlin/com/morpho/app/Previews.kt create mode 100644 Morpho/composeApp/src/desktopMain/kotlin/com/morpho/app/Previews.kt diff --git a/Butterfly b/Butterfly index 046d425..08323c7 160000 --- a/Butterfly +++ b/Butterfly @@ -1 +1 @@ -Subproject commit 046d425000fc7f3ff99642509916e5ecc8874aaa +Subproject commit 08323c780e2a71bc4e66ed472ed528d957e147ae diff --git a/Morpho/composeApp/src/androidMain/kotlin/Platform.android.kt b/Morpho/composeApp/src/androidMain/kotlin/Platform.android.kt index 73f97d9..6b1bab0 100644 --- a/Morpho/composeApp/src/androidMain/kotlin/Platform.android.kt +++ b/Morpho/composeApp/src/androidMain/kotlin/Platform.android.kt @@ -9,6 +9,7 @@ import kotlinx.parcelize.Parceler import kotlinx.parcelize.Parcelize import kotlinx.parcelize.RawValue import kotlinx.parcelize.TypeParceler +import java.util.Locale actual typealias CommonParcelize = Parcelize actual typealias CommonParcelable = Parcelable @@ -34,3 +35,10 @@ class AndroidPlatform : Platform { actual fun getPlatform(): Platform = AndroidPlatform() + + +actual val myLang:String? + get() = Locale.getDefault().language + +actual val myCountry:String? + get() = Locale.getDefault().country \ No newline at end of file diff --git a/Morpho/composeApp/src/androidMain/kotlin/com/morpho/app/Platform.android.kt b/Morpho/composeApp/src/androidMain/kotlin/com/morpho/app/Platform.android.kt deleted file mode 100644 index b1717d1..0000000 --- a/Morpho/composeApp/src/androidMain/kotlin/com/morpho/app/Platform.android.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.morpho.app - -import java.util.Locale - -actual val myLang:String? - get() = Locale.getDefault().language - -actual val myCountry:String? - get() = Locale.getDefault().country \ No newline at end of file diff --git a/Morpho/composeApp/src/androidMain/kotlin/com/morpho/app/Previews.kt b/Morpho/composeApp/src/androidMain/kotlin/com/morpho/app/Previews.kt new file mode 100644 index 0000000..83da450 --- /dev/null +++ b/Morpho/composeApp/src/androidMain/kotlin/com/morpho/app/Previews.kt @@ -0,0 +1,26 @@ +package com.morpho.app.com.morpho.app + +import androidx.compose.foundation.layout.Column +import androidx.compose.runtime.Composable +import androidx.compose.ui.tooling.preview.Preview +import com.morpho.app.ui.post.PlaceholderSkylineItem +import com.morpho.app.ui.post.PostFragmentRole + +@Preview +@Composable +fun PreviewPlaceholderSkylineItem() { + //MorphoTheme { + Column { + Column { + PlaceholderSkylineItem() + PlaceholderSkylineItem(role = PostFragmentRole.PrimaryThreadRoot) + PlaceholderSkylineItem(role = PostFragmentRole.ThreadBranchStart) + PlaceholderSkylineItem(role = PostFragmentRole.ThreadBranchMiddle) + PlaceholderSkylineItem(role = PostFragmentRole.ThreadBranchEnd) + PlaceholderSkylineItem(role = PostFragmentRole.ThreadRootUnfocused) + PlaceholderSkylineItem(role = PostFragmentRole.ThreadEnd) + PlaceholderSkylineItem(elevate = true) + } + } + //} +} \ No newline at end of file diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/data/MorphoDataSource.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/data/MorphoDataSource.kt index f65b6ad..35b6e6f 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/data/MorphoDataSource.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/data/MorphoDataSource.kt @@ -30,21 +30,23 @@ import kotlin.time.Duration abstract class MorphoDataSource: PagingSource(), KoinComponent { val agent: MorphoAgent by inject() val moderator: ContentLabelService by inject() - override val keyReuseSupported: Boolean = true + //override val keyReuseSupported: Boolean = true override fun getRefreshKey(state: PagingState): Cursor? { return state.anchorPosition?.let { anchorPosition -> val anchorPage = state.closestPageToPosition(anchorPosition) - if (anchorPage?.prevKey == null) return Cursor.Empty // First page + if(anchorPage?.prevKey == null && anchorPage?.nextKey == null) null + else if (anchorPage.prevKey == null) anchorPage.nextKey // First page else if (anchorPage.nextKey == null) anchorPage.prevKey // Last page - else anchorPage.prevKey // Initial page + else anchorPage.nextKey // Initial page } } + companion object { val defaultConfig = PagingConfig( - pageSize = 20, - prefetchDistance = 10, - initialLoadSize = 50, + pageSize = 50, + prefetchDistance = 20, + initialLoadSize = 100, enablePlaceholders = true, ) } @@ -93,7 +95,7 @@ data class MorphoFeedSource( data = tunedList.toList(), prevKey = when(params) { is LoadParams.Append -> loadCursor - is LoadParams.Prepend -> Cursor.Empty + is LoadParams.Prepend -> null is LoadParams.Refresh -> Cursor.Empty }, nextKey = pagedList.cursor, diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/di/AppModule.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/di/AppModule.kt index 5c52247..a34f58e 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/di/AppModule.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/di/AppModule.kt @@ -3,7 +3,7 @@ package com.morpho.app.di import com.morpho.app.data.MorphoAgent import com.morpho.app.data.PollBlueService import com.morpho.app.data.PreferencesRepository -import com.morpho.app.model.uidata.* +import com.morpho.app.model.uidata.ContentLabelService import com.morpho.app.screens.base.BaseScreenModel import com.morpho.app.screens.login.LoginScreenModel import com.morpho.app.screens.main.MainScreenModel @@ -44,9 +44,9 @@ val dataModule = module { single { MorphoAgent() } single { ContentLabelService() } single { PollBlueService() } - factory { p -> UserListPresenter(p.get()) } - factory { p -> UserFeedsPresenter(p.get()) } - factory> { p -> FeedPresenter(p.get()) } + //factory { p -> UserListPresenter(p.get()) } + //factory { p -> UserFeedsPresenter(p.get()) } + //factory> { p -> FeedPresenter(p.get()) } } @Suppress("MemberVisibilityCanBePrivate") diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/BskyLabelService.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/BskyLabelService.kt index bab0a28..1c6d29f 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/BskyLabelService.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/BskyLabelService.kt @@ -10,6 +10,7 @@ import com.morpho.butterfly.AtUri import com.morpho.butterfly.Cid import com.morpho.butterfly.Did import com.morpho.butterfly.Handle +import dev.icerock.moko.parcelize.Parcelable import dev.icerock.moko.parcelize.Parcelize import dev.icerock.moko.parcelize.TypeParceler import kotlinx.collections.immutable.persistentListOf @@ -30,7 +31,7 @@ open class BskyLabelService( val indexedAt: Moment, val policies: List, val labels: List, -) { +): Parcelable { val did: Did get() = creator?.did ?: Did("did:blank:did") val handle: Handle diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/FeedPresenter.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/FeedPresenter.kt index 1390f2d..2e29392 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/FeedPresenter.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/FeedPresenter.kt @@ -17,6 +17,7 @@ import com.morpho.butterfly.Cursor import com.morpho.butterfly.FeedRequest import com.morpho.butterfly.PagedResponse import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.map @@ -36,7 +37,9 @@ class FeedPresenter( - override fun produceUpdates(events: Flow): Flow = events.map { event -> + override fun produceUpdates(events: Flow): Flow = events.filter { + it is FeedEvent.Load && it.descriptor == descriptor + }.map { event -> when(event) { is FeedEvent.Load -> { when(event.descriptor) { diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/ProfilePresenters.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/ProfilePresenters.kt index 8c34735..e31ce3b 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/ProfilePresenters.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/ProfilePresenters.kt @@ -11,8 +11,10 @@ import com.morpho.app.model.uistate.ContentCardState import com.morpho.app.model.uistate.ListsOrFeeds import com.morpho.butterfly.Did import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.emitAll import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.merge +import kotlinx.coroutines.launch import org.lighthousegames.logging.logging class MyProfilePresenter( @@ -23,23 +25,45 @@ class MyProfilePresenter( val postsPresenter = FeedPresenter( descriptor = FeedDescriptor.Author(profileState.profile.did, AuthorFilter.PostsNoReplies) ) - val postsUpdates: Flow = postsPresenter.produceUpdates(profileState.events) + val postRepliesPresenter = FeedPresenter( descriptor = FeedDescriptor.Author(profileState.profile.did, AuthorFilter.PostsWithReplies) ) - val postRepliesUpdates: Flow = postRepliesPresenter.produceUpdates(profileState.events) val mediaPresenter = FeedPresenter( descriptor = FeedDescriptor.Author(profileState.profile.did, AuthorFilter.PostsWithMedia) ) - val mediaUpdates: Flow = mediaPresenter.produceUpdates(profileState.events) val likesPresenter = FeedPresenter( descriptor = FeedDescriptor.Likes(profileState.profile.did) ) - val likesUpdates: Flow = likesPresenter.produceUpdates(profileState.events) val listsPresenter = UserListPresenter(profileState.profile.did) - val listsUpdates: Flow = listsPresenter.produceUpdates(profileState.events) val feedsPresenter = UserFeedsPresenter(profileState.profile.did) - val feedsUpdates: Flow = feedsPresenter.produceUpdates(profileState.events) + + init { + presenterScope.launch { + profileState.posts.updates.emitAll( + postsPresenter.produceUpdates(merge(profileState.events, profileState.posts.events))) + } + presenterScope.launch { + profileState.postReplies.updates.emitAll( + postRepliesPresenter.produceUpdates(merge(profileState.events, profileState.postReplies.events))) + } + presenterScope.launch { + profileState.media.updates.emitAll(mediaPresenter + .produceUpdates(merge(profileState.events, profileState.media.events))) + } + presenterScope.launch { + profileState.likes.updates.emitAll(likesPresenter + .produceUpdates(merge(profileState.events, profileState.likes.events))) + } + if(profileState.lists != null) presenterScope.launch { + profileState.lists.updates.emitAll(listsPresenter + .produceUpdates(merge(profileState.events, profileState.lists.events))) + } + if(profileState.feeds != null) presenterScope.launch { + profileState.feeds.updates.emitAll(feedsPresenter + .produceUpdates(merge(profileState.events, profileState.feeds.events))) + } + } companion object { val log = logging("ProfilePresenter") @@ -84,7 +108,7 @@ class MyProfilePresenter( override fun produceUpdates(events: Flow): Flow { val did = profileState.profile.did val combined = merge(events, profileState.events) - val profileUpdates = combined.map { event -> + return combined.map { event -> when (event) { is Event.ComposePost -> UIUpdate.OpenComposer(event.post, event.role) @@ -95,10 +119,6 @@ class MyProfilePresenter( } } } as Flow - return merge( - profileUpdates, postsUpdates, postRepliesUpdates, mediaUpdates, - likesUpdates, listsUpdates, feedsUpdates - ) } } @@ -110,20 +130,38 @@ class ProfilePresenter( val postsPresenter = FeedPresenter( descriptor = FeedDescriptor.Author(profileState.profile.did, AuthorFilter.PostsNoReplies) ) - val postsUpdates: Flow = postsPresenter.produceUpdates(profileState.events) + val postRepliesPresenter = FeedPresenter( descriptor = FeedDescriptor.Author(profileState.profile.did, AuthorFilter.PostsWithReplies) ) - val postRepliesUpdates: Flow = postRepliesPresenter.produceUpdates(profileState.events) val mediaPresenter = FeedPresenter( descriptor = FeedDescriptor.Author(profileState.profile.did, AuthorFilter.PostsWithMedia) ) - val mediaUpdates: Flow = mediaPresenter.produceUpdates(profileState.events) val listsPresenter = UserListPresenter(profileState.profile.did) - val listsUpdates: Flow = listsPresenter.produceUpdates(profileState.events) val feedsPresenter = UserFeedsPresenter(profileState.profile.did) - val feedsUpdates: Flow = feedsPresenter.produceUpdates(profileState.events) + init { + presenterScope.launch { + profileState.posts.updates.emitAll( + postsPresenter.produceUpdates(merge(profileState.events, profileState.posts.events))) + } + presenterScope.launch { + profileState.postReplies.updates.emitAll( + postRepliesPresenter.produceUpdates(merge(profileState.events, profileState.postReplies.events))) + } + presenterScope.launch { + profileState.media.updates.emitAll(mediaPresenter + .produceUpdates(merge(profileState.events, profileState.media.events))) + } + if(profileState.lists != null) presenterScope.launch { + profileState.lists.updates.emitAll(listsPresenter + .produceUpdates(merge(profileState.events, profileState.lists.events))) + } + if(profileState.feeds != null) presenterScope.launch { + profileState.feeds.updates.emitAll(feedsPresenter + .produceUpdates(merge(profileState.events, profileState.feeds.events))) + } + } companion object { val log = logging("ProfilePresenter") suspend fun initialize( @@ -168,7 +206,7 @@ class ProfilePresenter( override fun produceUpdates(events: Flow): Flow { val did = profileState.profile.did val combined = merge(events, profileState.events) - val profileUpdates = combined.map { event -> + return combined.map { event -> when (event) { is ProfileEvent.Block -> if(did == event.subject) { agent.block(event.subject) @@ -224,9 +262,5 @@ class ProfilePresenter( } } } as Flow - return merge( - profileUpdates, postsUpdates, postRepliesUpdates, - mediaUpdates, listsUpdates, feedsUpdates - ) } } \ No newline at end of file diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uistate/ContentCardState.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uistate/ContentCardState.kt index 5268882..eba52da 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uistate/ContentCardState.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uistate/ContentCardState.kt @@ -1,7 +1,19 @@ package com.morpho.app.model.uistate -import com.morpho.app.model.bluesky.* -import com.morpho.app.model.uidata.* +import com.morpho.app.model.bluesky.AuthorFilter +import com.morpho.app.model.bluesky.BskyLabelService +import com.morpho.app.model.bluesky.BskyList +import com.morpho.app.model.bluesky.BskyPost +import com.morpho.app.model.bluesky.DetailedProfile +import com.morpho.app.model.bluesky.Profile +import com.morpho.app.model.uidata.Event +import com.morpho.app.model.uidata.FeedEvent +import com.morpho.app.model.uidata.FeedUpdate +import com.morpho.app.model.uidata.LabelerEvent +import com.morpho.app.model.uidata.ListEvent +import com.morpho.app.model.uidata.ListPageEvent +import com.morpho.app.model.uidata.ThreadEvent +import com.morpho.app.model.uidata.UIUpdate import com.morpho.app.util.MutableSharedFlowSerializer import com.morpho.app.util.MutableStateFlowSerializer import com.morpho.butterfly.AtUri @@ -27,7 +39,9 @@ sealed interface ContentCardState { extraBufferCapacity = 10, onBufferOverflow = BufferOverflow.DROP_OLDEST), override val updates: MutableStateFlow = MutableStateFlow(FeedUpdate.Empty), - ) : ContentCardState + ) : ContentCardState { + + } @Serializable data class PostThread( diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/base/BaseScreenModel.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/base/BaseScreenModel.kt index 0c0493f..62955ab 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/base/BaseScreenModel.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/base/BaseScreenModel.kt @@ -11,11 +11,21 @@ import com.morpho.app.data.MorphoAgent import com.morpho.app.model.bluesky.BskyPost import com.morpho.app.model.bluesky.NotificationsSource import com.morpho.app.model.bluesky.toPost -import com.morpho.app.model.uidata.* +import com.morpho.app.model.uidata.ContentLabelService +import com.morpho.app.model.uidata.Event +import com.morpho.app.model.uidata.MyProfilePresenter +import com.morpho.app.model.uidata.ProfilePresenter +import com.morpho.app.model.uidata.UIUpdate import com.morpho.butterfly.AtUri import com.morpho.butterfly.Did import kotlinx.coroutines.channels.BufferOverflow -import kotlinx.coroutines.flow.* +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.emitAll +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch import org.koin.core.component.KoinComponent import org.koin.core.component.inject @@ -62,10 +72,10 @@ open class BaseScreenModel : ScreenModel, KoinComponent { if(!init) emit(Pair(presenter, MutableStateFlow(UIUpdate.Empty))) else { val stateFlow = MutableStateFlow(UIUpdate.Empty) - emit(Pair(presenter, stateFlow)) screenModelScope.launch { stateFlow.emitAll(presenter.produceUpdates(eventStream)) } + emit(Pair(presenter, stateFlow)) } } @@ -77,10 +87,11 @@ open class BaseScreenModel : ScreenModel, KoinComponent { if(!init) emit(Pair(presenter, MutableStateFlow(UIUpdate.Empty))) else { val stateFlow = MutableStateFlow(UIUpdate.Empty) - emit(Pair(presenter, stateFlow)) screenModelScope.launch { stateFlow.emitAll(presenter.produceUpdates(eventStream)) } + emit(Pair(presenter, stateFlow)) + } } diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/base/tabbed/NavigationTabs.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/base/tabbed/NavigationTabs.kt index 9530c92..2fba160 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/base/tabbed/NavigationTabs.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/base/tabbed/NavigationTabs.kt @@ -1,7 +1,11 @@ package com.morpho.app.screens.base.tabbed import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.* +import androidx.compose.material.icons.filled.AccountCircle +import androidx.compose.material.icons.filled.DynamicFeed +import androidx.compose.material.icons.filled.Home +import androidx.compose.material.icons.filled.NotificationsNone +import androidx.compose.material.icons.filled.Search import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme @@ -9,15 +13,21 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.Immutable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import cafe.adriel.voyager.core.annotation.ExperimentalVoyagerApi +import cafe.adriel.voyager.core.lifecycle.LifecycleEffectOnce import cafe.adriel.voyager.core.model.rememberNavigatorScreenModel +import cafe.adriel.voyager.core.model.screenModelScope import cafe.adriel.voyager.core.screen.Screen import cafe.adriel.voyager.core.screen.ScreenKey import cafe.adriel.voyager.core.screen.uniqueScreenKey import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.Navigator import cafe.adriel.voyager.navigator.currentOrThrow +import com.morpho.app.model.uidata.MyProfilePresenter import com.morpho.app.screens.main.tabbed.TabbedHomeView import com.morpho.app.screens.main.tabbed.TabbedMainScreenModel import com.morpho.app.screens.notifications.NotificationViewContent @@ -30,6 +40,8 @@ import com.morpho.butterfly.AtUri import com.morpho.butterfly.Did import dev.icerock.moko.parcelize.Parcelable import dev.icerock.moko.parcelize.Parcelize +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.launch import kotlinx.serialization.Contextual import kotlinx.serialization.Serializable import kotlinx.serialization.Transient @@ -283,15 +295,20 @@ data object MyProfileTab: TabScreen { override val navBar: @Composable (@Contextual Navigator) -> Unit = { n -> TabbedNavBar(options.index, n) } - @OptIn(ExperimentalMaterial3Api::class) + @OptIn(ExperimentalMaterial3Api::class, ExperimentalVoyagerApi::class) @Composable override fun Content() { val sm = LocalNavigator.currentOrThrow.rememberNavigatorScreenModel { TabbedMainScreenModel() } val eventStream = sm.globalEvents - val myProfilePresenter by sm.getMyProfilePresenter().collectAsState(null) + var myProfilePresenter by remember { mutableStateOf(null) } + LifecycleEffectOnce { + sm.screenModelScope.launch { + sm.getMyProfilePresenter().first().also { it -> myProfilePresenter = it.first } + } + } if(myProfilePresenter != null) { - val myProfileState = myProfilePresenter!!.first.profileState + val myProfileState = myProfilePresenter!!.profileState TabbedProfileContent( ownProfile = true, profileState = null, diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/main/MainScreenModel.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/main/MainScreenModel.kt index 238c17d..a0f25d6 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/main/MainScreenModel.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/main/MainScreenModel.kt @@ -54,16 +54,20 @@ open class MainScreenModel: BaseScreenModel() { screenModelScope.launch { - feedPresenters.forEach { (source, presenter) -> - val entry = feedStates[source]?: return@forEach - entry.updates.emitAll( - presenter.produceUpdates(merge(globalEvents, entry.events)) - ) + for(source in feedSources) { + val entry = feedStates[source.uri]?: return@launch + val presenter = feedPresenters[source.uri] ?: return@launch + screenModelScope.launch { + entry.updates.emitAll( + presenter.produceUpdates(merge(globalEvents, entry.events)) + ) + } } - } + } initialized = true + } } diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/main/tabbed/TabbedHomeView.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/main/tabbed/TabbedHomeView.kt index c5a8c38..ff96945 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/main/tabbed/TabbedHomeView.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/main/tabbed/TabbedHomeView.kt @@ -118,9 +118,7 @@ fun TabScreen.TabbedHomeView( ) { List(sm.tabs.size) { index -> - val uri = sm.tabs[index].uri - val desc = sm.feedPresenters[uri]?.descriptor - desc?.let { FeedEvent.Load(it) }?.let { sm.sendGlobalEvent(it) } + HomeSkylineTab( index = index.toUShort(), title = sm.tabs[index].title, @@ -146,6 +144,7 @@ fun TabScreen.TabbedHomeView( tabIndex = selectedTabIndex, onChanged = { index -> if (index == selectedTabIndex) return@HomeTabRow + if(index < selectedTabIndex) { if (nav.items.contains(tabs[index])) { nav.popUntil {it == tabs[index] } @@ -158,6 +157,7 @@ fun TabScreen.TabbedHomeView( ) }, content = { insets, state -> + SkylineTabTransition(nav, sm, insets, state) }, modifier = Modifier, @@ -273,7 +273,11 @@ data class HomeSkylineTab @OptIn(ExperimentalVoyagerApi::class) constructor( state: ContentCardState?, modifier: Modifier ) { + val presenter = sm.feedPresenters[state?.uri] + val desc = presenter?.descriptor if(state == null) return + desc?.let { FeedEvent.Load(it) }?.let { event -> sm.sendGlobalEvent(event) } + TabbedSkylineFragment( paddingValues = paddingValues, isProfileFeed = false, diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/main/tabbed/TabbedMainScreenModel.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/main/tabbed/TabbedMainScreenModel.kt index 9deaacc..6a0329a 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/main/tabbed/TabbedMainScreenModel.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/main/tabbed/TabbedMainScreenModel.kt @@ -4,7 +4,6 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue -import app.bsky.actor.FeedType import app.bsky.feed.GetPostThreadResponseThreadUnion import cafe.adriel.voyager.core.model.screenModelScope import com.morpho.app.model.bluesky.toContentCardMapEntry @@ -25,9 +24,7 @@ class TabbedMainScreenModel : MainScreenModel() { val tabs = mutableStateListOf() - val timelineIndex = agent.prefs.timelineIndex ?: agent.prefs.saved.indexOfFirst { - it.type == FeedType.TIMELINE - }.let { if(it == -1) 0 else it } + val timelineIndex = 1 var loaded by mutableStateOf(false) diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/profile/TabbedProfileView.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/profile/TabbedProfileView.kt index 5916aa8..b6a888b 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/profile/TabbedProfileView.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/profile/TabbedProfileView.kt @@ -5,9 +5,19 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding -import androidx.compose.material3.* -import androidx.compose.runtime.* +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.SecondaryScrollableTabRow +import androidx.compose.material3.Tab +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.material3.TopAppBarScrollBehavior +import androidx.compose.material3.rememberTopAppBarState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.unit.dp @@ -21,6 +31,7 @@ import cafe.adriel.voyager.navigator.tab.TabDisposable import cafe.adriel.voyager.navigator.tab.TabNavigator import cafe.adriel.voyager.navigator.tab.TabOptions import coil3.annotation.ExperimentalCoilApi +import com.morpho.app.model.bluesky.FeedDescriptor import com.morpho.app.model.uidata.Event import com.morpho.app.model.uidata.FeedEvent import com.morpho.app.model.uistate.ContentCardState @@ -150,12 +161,12 @@ fun TabScreen.TabbedProfileContent( eventCallback: (Event) -> Unit = {}, ) { //ProvideNavigatorLifecycleKMPSupport { - val navigator = LocalNavigator.currentOrThrow - var selectedTabIndex by rememberSaveable { mutableIntStateOf(0) } - val scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior(rememberTopAppBarState()) - val tabs = remember(myProfileState, profileState) { - if(ownProfile) myProfileState.toTabList() else profileState?.toTabList() ?: listOf() - } + val navigator = LocalNavigator.currentOrThrow + var selectedTabIndex by rememberSaveable { mutableIntStateOf(0) } + val scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior(rememberTopAppBarState()) + val tabs = remember(myProfileState, profileState) { + if (ownProfile) myProfileState.toTabList() else profileState?.toTabList() ?: listOf() + } TabNavigator( tab = tabs.first(), disposeNestedNavigators = true, @@ -165,47 +176,33 @@ fun TabScreen.TabbedProfileContent( navBar = { navBar(navigator) }, modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection), topContent = { - if(ownProfile) MyTabbedProfileTopBar( - profile = myProfileState, - scrollBehavior = scrollBehavior, - tabs = tabs, - onBackClicked = { navigator.pop() }, - onTabChanged = { index -> - selectedTabIndex = index - val state = myProfileState.indexToState(index) - val actor = myProfileState.profile.did - when(state) { - is ContentCardState.ProfileTimeline -> state.events - .tryEmit(FeedEvent.LoadFeed(actor, state.filter)) - is ContentCardState.ProfileList -> state.events - .tryEmit(FeedEvent.LoadLists(actor, state.listsOrFeeds)) - is ContentCardState.ProfileLabeler -> {} - else -> {} - } - }, - tabIndex = selectedTabIndex, - ) else if(profileState != null) TabbedProfileTopBar( - profile = profileState, - scrollBehavior = scrollBehavior, - tabs = tabs, - onBackClicked = { navigator.pop() }, - onTabChanged = { index -> - selectedTabIndex = index - val state = profileState.indexToState(index) - val actor = profileState.profile.did - when(state) { - is ContentCardState.ProfileTimeline -> state.events - .tryEmit(FeedEvent.LoadFeed(actor, state.filter)) - is ContentCardState.ProfileList -> state.events - .tryEmit(FeedEvent.LoadLists(actor, state.listsOrFeeds)) - is ContentCardState.ProfileLabeler -> {} - else -> {} - } - }, - tabIndex = selectedTabIndex, - ) else LoadingCircle() + if(ownProfile) { + MyTabbedProfileTopBar( + profile = myProfileState, + scrollBehavior = scrollBehavior, + tabs = tabs, + onBackClicked = { navigator.pop() }, + onTabChanged = { index -> + selectedTabIndex = index + }, + tabIndex = selectedTabIndex, + ) + } else if(profileState != null) { + TabbedProfileTopBar( + profile = profileState, + scrollBehavior = scrollBehavior, + tabs = tabs, + onBackClicked = { navigator.pop() }, + onTabChanged = { index -> + selectedTabIndex = index + }, + tabIndex = selectedTabIndex, + ) + }else LoadingCircle() + }, + content = { insets, state -> + CurrentProfileScreen(eventCallback, insets, state, Modifier) }, - content = { insets, state -> CurrentProfileScreen(eventCallback, insets, state, Modifier) }, state = if(ownProfile) myProfileState.indexToState(selectedTabIndex) else profileState?.indexToState(selectedTabIndex), scrollBehavior = scrollBehavior, @@ -271,6 +268,15 @@ data class ProfileSkylineTab( modifier: Modifier ) { if(state == null) return + when(state) { + is ContentCardState.ProfileTimeline -> if(state.filter != null) { + state.events.tryEmit(FeedEvent.Load(FeedDescriptor.Author(state.profile.did, state.filter))) + } else state.events.tryEmit(FeedEvent.Load(FeedDescriptor.Likes(state.profile.did))) + is ContentCardState.ProfileList -> state.events + .tryEmit(FeedEvent.LoadLists(state.profile.did, state.listsOrFeeds)) + is ContentCardState.ProfileLabeler -> {} + else -> {} + } TabbedSkylineFragment( paddingValues, isProfileFeed = true, diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/post/NotFoundPostFragment.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/post/NotFoundPostFragment.kt index d048f57..fd80b3a 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/post/NotFoundPostFragment.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/post/NotFoundPostFragment.kt @@ -1,28 +1,23 @@ package com.morpho.app.ui.post -import androidx.compose.foundation.layout.* +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding import androidx.compose.foundation.text.selection.SelectionContainer import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.material3.Text -import androidx.compose.material3.surfaceColorAtElevation import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.text.SpanStyle -import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.text.withStyle import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.max -import com.morpho.app.ui.elements.AvatarShape -import com.morpho.app.ui.elements.OutlinedAvatar import com.morpho.app.ui.elements.WrappedColumn import com.morpho.butterfly.AtUri import morpho.app.ui.utils.indentLevel + @Composable fun NotFoundPostFragment( modifier: Modifier = Modifier, @@ -58,144 +53,41 @@ fun NotFoundPostFragment( } } + + @Composable fun PlaceholderSkylineItem( modifier: Modifier = Modifier, - role: PostFragmentRole = PostFragmentRole.Solo, - indentLevel: Int = 0, elevate: Boolean = false, + indentLevel: Int = 0, + role: PostFragmentRole = PostFragmentRole.Solo, ) { - val padding = remember { when(role) { - PostFragmentRole.Solo -> if(indentLevel == 0) Modifier.padding(2.dp) else Modifier - PostFragmentRole.PrimaryThreadRoot -> Modifier.padding(2.dp) - PostFragmentRole.ThreadBranchStart -> Modifier.padding(start = 0.dp, top = 0.dp, end = 2.dp, bottom = 2.dp) - PostFragmentRole.ThreadBranchMiddle -> Modifier.padding(start = 0.dp, top = 0.dp, end = 2.dp, bottom = 2.dp) - PostFragmentRole.ThreadBranchEnd -> Modifier.padding(start = 0.dp, top = 0.dp, end = 2.dp, bottom = 2.dp) - PostFragmentRole.ThreadRootUnfocused -> Modifier.padding(2.dp) - PostFragmentRole.ThreadEnd -> Modifier.padding(start = 2.dp, top = 0.dp, end = 2.dp, bottom = 2.dp) - }} - WrappedColumn(modifier = modifier.then(padding.fillMaxWidth())) { - val indent = remember { when(role) { - PostFragmentRole.Solo -> indentLevel.toFloat() - PostFragmentRole.PrimaryThreadRoot -> indentLevel.toFloat() - PostFragmentRole.ThreadBranchStart -> 0.0f//indentLevel.toFloat() - PostFragmentRole.ThreadBranchMiddle -> 0.0f//indentLevel.toFloat()-1 - PostFragmentRole.ThreadBranchEnd -> 0.0f//indentLevel.toFloat()-1 - PostFragmentRole.ThreadRootUnfocused -> indentLevel.toFloat() - PostFragmentRole.ThreadEnd -> 0.0f - }} - - val bgColor = if (role == PostFragmentRole.PrimaryThreadRoot) { - MaterialTheme.colorScheme.background - } else { - MaterialTheme.colorScheme - .surfaceColorAtElevation( - if (elevate ) 2.dp - else if (indentLevel > 0) (indentLevel*2).dp - else 0.dp - ) - } - Surface ( - shadowElevation = if (elevate || indentLevel > 0) 2.dp else 0.dp, - tonalElevation = if (elevate && role != PostFragmentRole.ThreadEnd) 2.dp - else if (indentLevel > 0) (indentLevel*2).dp else 0.dp, + WrappedColumn( + modifier = Modifier + .fillMaxWidth() + .padding(2.dp) + ) { + Surface( + shadowElevation = max((indentLevel - 1).dp, 0.dp), + tonalElevation = indentLevel.dp, shape = MaterialTheme.shapes.small, - //color = bgColor, - modifier = modifier - .fillMaxWidth(indentLevel(indent)) - .align(Alignment.End) + modifier = Modifier + .fillMaxWidth(indentLevel(indentLevel.toFloat())) ) { - Row( - modifier = Modifier.padding(end = 6.dp) - .fillMaxWidth()//.fillMaxWidth(indentLevel(indent)) - ) { - - if (indent < 2) { - OutlinedAvatar( - url = "", - contentDescription = "Placeholder avatar", - size = 45.dp, - outlineColor = MaterialTheme.colorScheme.background, - avatarShape = AvatarShape.Corner, - modifier = Modifier.padding(end = 2.dp) + Column { + SelectionContainer { + Text( + text = "Post Loading", + style = MaterialTheme.typography.bodyLarge.copy(fontWeight = FontWeight.Light), + color = MaterialTheme.colorScheme.outline, + modifier = Modifier + .padding(50.dp) + .align(Alignment.CenterHorizontally) ) } - - Column( - Modifier - .padding(top = 4.dp, start = 2.dp, end = 6.dp) - .fillMaxWidth()//.fillMaxWidth(indentLevel(indent)) - ) { - - Row( - modifier = Modifier.padding(top = 2.dp, start = 2.dp, end = 4.dp), - horizontalArrangement = Arrangement.End - ) { - if (indent >= 2) { - OutlinedAvatar( - url = "", - contentDescription = "Placeholder avatar", - size = 30.dp, - avatarShape = AvatarShape.Rounded, - outlineColor = MaterialTheme.colorScheme.background, - ) - } - Text( - text = buildAnnotatedString { - withStyle( - style = SpanStyle( - color = MaterialTheme.colorScheme.onSurface, - fontSize = MaterialTheme.typography.labelLarge.fontSize, - fontWeight = FontWeight.Medium - ) - ) { - " " - } - withStyle( - style = SpanStyle( - color = MaterialTheme.colorScheme.onSurfaceVariant, - fontSize = MaterialTheme.typography.labelLarge.fontSize.times( - 0.8f - ) - ) - ) { - append("@ ") - } - - }, - maxLines = 1, - style = MaterialTheme.typography.labelLarge, - overflow = TextOverflow.Ellipsis, - modifier = Modifier - .wrapContentWidth(Alignment.Start) - .weight(10.0F) - .alignByBaseline() - ) - - Spacer(modifier = Modifier.width(1.dp).weight(0.1F)) - Text( - text = " ", - color = MaterialTheme.colorScheme.onSurfaceVariant, - style = MaterialTheme.typography.labelLarge, - fontSize = MaterialTheme.typography.labelLarge.fontSize.div(1.2F), - modifier = Modifier - .wrapContentWidth(Alignment.End) - //.weight(3.0F) - .alignByBaseline(), - maxLines = 1, - overflow = TextOverflow.Visible, - softWrap = false, - ) - } - - Spacer(Modifier.height(100.dp)) - - DummyPostActions() - } } - } } } diff --git a/Morpho/composeApp/src/desktopMain/kotlin/com/morpho/app/Previews.kt b/Morpho/composeApp/src/desktopMain/kotlin/com/morpho/app/Previews.kt new file mode 100644 index 0000000..c19a926 --- /dev/null +++ b/Morpho/composeApp/src/desktopMain/kotlin/com/morpho/app/Previews.kt @@ -0,0 +1,25 @@ +package com.morpho.app + +import androidx.compose.desktop.ui.tooling.preview.Preview +import androidx.compose.foundation.layout.Column +import androidx.compose.runtime.Composable +import com.morpho.app.ui.post.PlaceholderSkylineItem +import com.morpho.app.ui.post.PostFragmentRole +import com.morpho.app.ui.theme.MorphoTheme + +@Preview +@Composable +fun PreviewPlaceholderSkylineItem() { + MorphoTheme { + Column { + PlaceholderSkylineItem() + PlaceholderSkylineItem(role = PostFragmentRole.PrimaryThreadRoot) + PlaceholderSkylineItem(role = PostFragmentRole.ThreadBranchStart) + PlaceholderSkylineItem(role = PostFragmentRole.ThreadBranchMiddle) + PlaceholderSkylineItem(role = PostFragmentRole.ThreadBranchEnd) + PlaceholderSkylineItem(role = PostFragmentRole.ThreadRootUnfocused) + PlaceholderSkylineItem(role = PostFragmentRole.ThreadEnd) + PlaceholderSkylineItem(elevate = true) + } + } +} \ No newline at end of file diff --git a/Morpho/gradle/libs.versions.toml b/Morpho/gradle/libs.versions.toml index 8fd5960..f80039c 100644 --- a/Morpho/gradle/libs.versions.toml +++ b/Morpho/gradle/libs.versions.toml @@ -14,7 +14,7 @@ androidx-test-junit = "1.2.1" appdirs = "1.2.2" compose = "1.6.8" compose-plugin = "1.6.11" -constraintlayoutComposeMultiplatform = "0.3.0-alpha01" +constraintlayoutComposeMultiplatform = "0.4.0" datastorePreferencesCore = "1.1.1" imageLoader = "1.7.8" filekit = "0.8.2" diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 40a247f..ecf0be1 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -14,7 +14,7 @@ androidx-test-junit = "1.2.1" appdirs = "1.2.2" compose = "1.6.8" compose-plugin = "1.6.11" -constraintlayoutComposeMultiplatform = "0.3.0-alpha01" +constraintlayoutComposeMultiplatform = "0.4.0" datastorePreferencesCore = "1.1.1" filekit = "0.8.2" imageLoader = "1.7.8" From d616e1c13a5fa91f9fd1db4fc0b8ceeba2091552 Mon Sep 17 00:00:00 2001 From: Orual Date: Wed, 18 Sep 2024 12:40:32 -0400 Subject: [PATCH 22/42] Made a couple changes that seem to have improved the paging thing a bit, but still being funny. --- .../com/morpho/app/data/MorphoDataSource.kt | 2 +- .../morpho/app/ui/common/SkylineFragment.kt | 48 +++++++++++++++++-- .../composeApp/src/desktopMain/kotlin/main.kt | 10 ++-- 3 files changed, 51 insertions(+), 9 deletions(-) diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/data/MorphoDataSource.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/data/MorphoDataSource.kt index 35b6e6f..8e7cd59 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/data/MorphoDataSource.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/data/MorphoDataSource.kt @@ -47,7 +47,7 @@ abstract class MorphoDataSource: PagingSource(), KoinCom pageSize = 50, prefetchDistance = 20, initialLoadSize = 100, - enablePlaceholders = true, + enablePlaceholders = false, ) } } diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/common/SkylineFragment.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/common/SkylineFragment.kt index 7f03315..9ad8119 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/common/SkylineFragment.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/common/SkylineFragment.kt @@ -4,16 +4,48 @@ import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.border import androidx.compose.foundation.gestures.snapping.rememberSnapFlingBehavior -import androidx.compose.foundation.layout.* +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.WindowInsetsSides +import androidx.compose.foundation.layout.asPaddingValues +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.only +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.safeContent +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.systemBarsPadding import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.* +import androidx.compose.material.icons.filled.Create +import androidx.compose.material.icons.filled.KeyboardArrowUp +import androidx.compose.material.icons.filled.KeyboardDoubleArrowUp +import androidx.compose.material.icons.filled.Refresh +import androidx.compose.material.icons.filled.Settings import androidx.compose.material.pullrefresh.PullRefreshIndicator import androidx.compose.material.pullrefresh.pullRefresh import androidx.compose.material.pullrefresh.rememberPullRefreshState -import androidx.compose.material3.* -import androidx.compose.runtime.* +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.FloatingActionButton +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.IconButtonDefaults +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedIconButton +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.material3.surfaceColorAtElevation +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp @@ -258,7 +290,13 @@ inline fun SkylineFragment ( } } } - else -> { item { LoadingCircle() } } + else -> { item { PlaceholderSkylineItem( + modifier = if(debuggable) Modifier.border(1.dp, Color.Black) else Modifier + .fillMaxWidth() + //.padding(horizontal = 4.dp), + .padding(vertical = 2.dp, horizontal = 4.dp), + elevate = true, + ) } } } if (data?.loadState?.append == LoadStateLoading) item { LoadingCircle() } } diff --git a/Morpho/composeApp/src/desktopMain/kotlin/main.kt b/Morpho/composeApp/src/desktopMain/kotlin/main.kt index e10b862..a53e9ea 100644 --- a/Morpho/composeApp/src/desktopMain/kotlin/main.kt +++ b/Morpho/composeApp/src/desktopMain/kotlin/main.kt @@ -43,6 +43,10 @@ import cafe.adriel.voyager.core.annotation.ExperimentalVoyagerApi import cafe.adriel.voyager.jetpack.ProvideNavigatorLifecycleKMPSupport import ch.qos.logback.classic.LoggerContext import ch.qos.logback.core.util.StatusPrinter2 +import com.github.tkuenneth.nativeparameterstoreaccess.MacOSDefaults.getDefaultsEntry +import com.github.tkuenneth.nativeparameterstoreaccess.NativeParameterStoreAccess.IS_MACOS +import com.github.tkuenneth.nativeparameterstoreaccess.NativeParameterStoreAccess.IS_WINDOWS +import com.github.tkuenneth.nativeparameterstoreaccess.WindowsRegistry.getWindowsRegistryEntry import com.morpho.app.App import com.morpho.app.data.MorphoAgent import com.morpho.app.data.PreferencesRepository @@ -126,7 +130,7 @@ fun main() = application { transparent = undecorated, icon = painterResource(Res.drawable.morpho_icon_transparent) ) { - MorphoTheme(darkTheme = false) { + MorphoTheme(darkTheme = isSystemInDarkTheme()) { if(undecorated) { MorphoWindow( windowState = windowState, @@ -148,7 +152,7 @@ fun main() = application { } } -/* + fun isSystemInDarkTheme(): Boolean { return when { IS_WINDOWS -> { @@ -168,7 +172,7 @@ fun isSystemInDarkTheme(): Boolean { // just default to dark mode for now } } -}*/ +} @OptIn(ExperimentalResourceApi::class) @Composable From c576a107b4bbf11311f969b94393d6ba8c226133 Mon Sep 17 00:00:00 2001 From: Orual Date: Wed, 18 Sep 2024 13:07:44 -0400 Subject: [PATCH 23/42] Fixed busted token refresh --- Butterfly | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Butterfly b/Butterfly index 08323c7..447301b 160000 --- a/Butterfly +++ b/Butterfly @@ -1 +1 @@ -Subproject commit 08323c780e2a71bc4e66ed472ed528d957e147ae +Subproject commit 447301b59d14c7314abca574f310e93e0d204344 From edb096e9b26a17918be7c1cf3d6a3f66d718ed2d Mon Sep 17 00:00:00 2001 From: Orual Date: Wed, 18 Sep 2024 13:38:47 -0400 Subject: [PATCH 24/42] Couple tiny fixes --- Butterfly | 2 +- .../kotlin/com/morpho/app/screens/main/MainScreenModel.kt | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Butterfly b/Butterfly index 447301b..8d8d5da 160000 --- a/Butterfly +++ b/Butterfly @@ -1 +1 @@ -Subproject commit 447301b59d14c7314abca574f310e93e0d204344 +Subproject commit 8d8d5daf0dbd5375f60c85d1a37ec5b45f84e44f diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/main/MainScreenModel.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/main/MainScreenModel.kt index a0f25d6..ae4eca4 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/main/MainScreenModel.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/main/MainScreenModel.kt @@ -55,11 +55,11 @@ open class MainScreenModel: BaseScreenModel() { screenModelScope.launch { for(source in feedSources) { - val entry = feedStates[source.uri]?: return@launch - val presenter = feedPresenters[source.uri] ?: return@launch + val cardState = feedStates[source.uri]?: continue + val presenter = feedPresenters[source.uri] ?: continue screenModelScope.launch { - entry.updates.emitAll( - presenter.produceUpdates(merge(globalEvents, entry.events)) + cardState.updates.emitAll( + presenter.produceUpdates(merge(globalEvents, cardState.events)) ) } } From 24c136942eab7cf5a36be10867da7a3ebf237a4e Mon Sep 17 00:00:00 2001 From: Orual Date: Wed, 18 Sep 2024 20:06:50 -0400 Subject: [PATCH 25/42] Some fixes, new thread bug. Seeing posts that are part of a thread duplicated a bunch for each reply. Definitely need to fix the parenting thing, that's definitely where the problem is. --- Butterfly | 2 +- .../app/model/bluesky/NotificationsSource.kt | 19 +-- .../app/screens/base/BaseScreenModel.kt | 2 +- .../app/screens/main/tabbed/TabbedHomeView.kt | 1 + .../notifications/NotificationsView.kt | 114 +++++++++++------- .../morpho/app/ui/common/SkylineFragment.kt | 14 +-- .../app/ui/common/TabbedSkylineFragment.kt | 12 +- 7 files changed, 89 insertions(+), 75 deletions(-) diff --git a/Butterfly b/Butterfly index 8d8d5da..0dc6479 160000 --- a/Butterfly +++ b/Butterfly @@ -1 +1 @@ -Subproject commit 8d8d5daf0dbd5375f60c85d1a37ec5b45f84e44f +Subproject commit 0dc64799ea533103e24c9e4aaf26fd1333731282 diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/NotificationsSource.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/NotificationsSource.kt index 32be980..6693938 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/NotificationsSource.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/NotificationsSource.kt @@ -2,7 +2,6 @@ package com.morpho.app.model.bluesky import app.bsky.notification.ListNotificationsReason import app.cash.paging.PagingConfig -import app.cash.paging.compose.LazyPagingItems import com.morpho.app.data.MorphoDataSource import com.morpho.app.model.uistate.NotificationsFilterState import com.morpho.butterfly.AtUri @@ -10,7 +9,7 @@ import com.morpho.butterfly.Cursor import kotlinx.serialization.Serializable import org.lighthousegames.logging.logging -class NotificationsSource: MorphoDataSource() { +class NotificationsSource: MorphoDataSource() { companion object { val log = logging() val defaultConfig = PagingConfig( @@ -21,7 +20,7 @@ class NotificationsSource: MorphoDataSource() { ) } - override suspend fun load(params: LoadParams): LoadResult { + override suspend fun load(params: LoadParams): LoadResult { try { val limit = params.loadSize val loadCursor = when(params) { @@ -31,7 +30,7 @@ class NotificationsSource: MorphoDataSource() { } return agent.listNotifications(limit.toLong(), loadCursor.value).map { response -> val newCursor = response.cursor - val items = response.items.map { it.toBskyNotification()} + val items = response.items.map { it.toBskyNotification()}.collectNotifications() LoadResult.Page( data = items, prevKey = when(params) { @@ -50,18 +49,10 @@ class NotificationsSource: MorphoDataSource() { } } -fun LazyPagingItems.collectNotifications( - toMark: List = listOf() -) : List { +fun List.collectNotifications() : List { val seen = mutableListOf() val workList = mutableListOf() - this.itemSnapshotList.map { notif -> - if (notif == null) return@map NotificationsListItem( - notifications = listOf(), - reason = ListNotificationsReason.PLACEHOLDER, - isRead = false, - reasonSubject = null, - ) + this.map { notif -> if(notif.reasonSubject != null && seen.contains(notif.reasonSubject)) { val index = workList.indexOfFirst { it.reasonSubject == notif.reasonSubject diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/base/BaseScreenModel.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/base/BaseScreenModel.kt index 62955ab..19488b4 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/base/BaseScreenModel.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/base/BaseScreenModel.kt @@ -45,7 +45,7 @@ open class BaseScreenModel : ScreenModel, KoinComponent { val isLoggedIn: Boolean get() = agent.isLoggedIn - val notificationsRaw = Pager(NotificationsSource.defaultConfig) { + val notifications = Pager(NotificationsSource.defaultConfig) { NotificationsSource() }.flow.cachedIn(screenModelScope) diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/main/tabbed/TabbedHomeView.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/main/tabbed/TabbedHomeView.kt index ff96945..d7d2b66 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/main/tabbed/TabbedHomeView.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/main/tabbed/TabbedHomeView.kt @@ -283,6 +283,7 @@ data class HomeSkylineTab @OptIn(ExperimentalVoyagerApi::class) constructor( isProfileFeed = false, uiUpdate = state.updates, eventCallback = { sm.sendGlobalEvent(it) }, + getContentHandling = { post -> sm.labelService.getContentHandlingForPost(post).map { it.first } }, ) } diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/notifications/NotificationsView.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/notifications/NotificationsView.kt index 17a79e7..427a463 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/notifications/NotificationsView.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/notifications/NotificationsView.kt @@ -36,8 +36,9 @@ import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.unit.Dp import androidx.constraintlayout.compose.ConstraintLayout import app.cash.paging.LoadStateError -import app.cash.paging.LoadStateNotLoading +import app.cash.paging.LoadStateLoading import app.cash.paging.compose.collectAsLazyPagingItems +import app.cash.paging.compose.itemKey import cafe.adriel.voyager.core.annotation.ExperimentalVoyagerApi import cafe.adriel.voyager.core.model.rememberNavigatorScreenModel import cafe.adriel.voyager.core.model.screenModelScope @@ -47,7 +48,6 @@ import cafe.adriel.voyager.navigator.currentOrThrow import com.morpho.app.model.bluesky.BskyPost import com.morpho.app.model.bluesky.DraftPost import com.morpho.app.model.bluesky.NotificationsListItem -import com.morpho.app.model.bluesky.collectNotifications import com.morpho.app.model.uistate.NotificationsUIState import com.morpho.app.screens.base.tabbed.ProfileTab import com.morpho.app.screens.base.tabbed.TabScreen @@ -61,6 +61,7 @@ import com.morpho.app.ui.elements.WrappedLazyColumn import com.morpho.app.ui.elements.doMenuOperation import com.morpho.app.ui.notifications.NotificationsElement import com.morpho.app.ui.notifications.NotificationsFilterElement +import com.morpho.app.ui.post.PlaceholderSkylineItem import com.morpho.app.util.ClipboardManager import com.morpho.butterfly.AtUri import kotlinx.coroutines.Dispatchers @@ -84,7 +85,7 @@ fun TabScreen.NotificationViewContent( val listState = rememberLazyListState() val scope = rememberCoroutineScope() val uriHandler = LocalUriHandler.current - val pager = sm.notificationsRaw.collectAsLazyPagingItems() + val pager = sm.notifications.collectAsLazyPagingItems() var uiState by rememberSaveable { mutableStateOf(NotificationsUIState()) } val toMarkRead = mutableStateListOf() TabbedScreenScaffold( @@ -128,6 +129,7 @@ fun TabScreen.NotificationViewContent( val clipboardManager = getKoin().get() + ConstraintLayout( Modifier .fillMaxSize() @@ -163,64 +165,84 @@ fun TabScreen.NotificationViewContent( } } } - when(val loadState = pager.loadState.refresh) { - is LoadStateError ->{ - item { Text("Error: ${loadState.error}") } + val refreshLoadState = pager.loadState.refresh + val appendLoadState = pager.loadState.append + + when { + refreshLoadState is LoadStateError || appendLoadState is LoadStateError -> { + item { Text("$refreshLoadState\n$appendLoadState") } item { Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.Center) { TextButton(onClick = { pager.retry() }) { Text("Retry") } } } } - is LoadStateNotLoading -> { + refreshLoadState is LoadStateLoading -> { item { LoadingCircle() } } + else -> { + - val notifications = pager.collectNotifications(toMarkRead) items( - count = notifications.size, - //key = { index -> notifications[index].hashCode() }, + count = pager.itemCount, + key = { pager.itemKey { + it.hashCode() + }}, contentType = { NotificationsListItem } ) { index -> if (state != null) { - NotificationsElement( - item = notifications[index], - showPost = state.showPosts, - getPost = { sm.getPost(it).getOrNull() }, - onUnClicked = { type, rkey -> - sm.agent.deleteRecord(type, rkey) - }, - onAvatarClicked = { - navigator.push(ProfileTab(it)) - }, - onRepostClicked = { - initialContent = it - repostClicked = true - }, - onReplyClicked = { - initialContent = it - composerRole = ComposerRole.Reply - showComposer = true - }, - onMenuClicked = { option, post -> - doMenuOperation(option, post, - clipboardManager = clipboardManager, - uriHandler = uriHandler - ) }, - onLikeClicked = { sm.agent.like(it) }, - onPostClicked = { - navigator.push(ThreadTab(it)) - }, - // If someone hides their read notifications, - // we don't want to just mark them as read unprompted. - // Might cause them to disappear unexpectedly. - readOnLoad = !state.filterState.value.showAlreadyRead, - markRead = { toMarkRead.add(it) }, - resolveHandle = { handle -> sm.agent.resolveHandle(handle).getOrNull() } - ) + when(val item = pager[index]) { + is NotificationsListItem -> { + NotificationsElement( + item = item, + showPost = state.showPosts, + getPost = { sm.getPost(it).getOrNull() }, + onUnClicked = { type, rkey -> + sm.agent.deleteRecord(type, rkey) + }, + onAvatarClicked = { + navigator.push(ProfileTab(it)) + }, + onRepostClicked = { + initialContent = it + repostClicked = true + }, + onReplyClicked = { + initialContent = it + composerRole = ComposerRole.Reply + showComposer = true + }, + onMenuClicked = { option, post -> + doMenuOperation( + option, post, + clipboardManager = clipboardManager, + uriHandler = uriHandler + ) + }, + onLikeClicked = { sm.agent.like(it) }, + onPostClicked = { + navigator.push(ThreadTab(it)) + }, + // If someone hides their read notifications, + // we don't want to just mark them as read unprompted. + // Might cause them to disappear unexpectedly. + readOnLoad = !state.filterState.value.showAlreadyRead, + markRead = { toMarkRead.add(it) }, + resolveHandle = { handle -> + sm.agent.resolveHandle( + handle + ).getOrNull() + } + ) + } + null -> { + PlaceholderSkylineItem() + } + } + } + } } - else -> { item { LoadingCircle() } } } } if(showComposer) { diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/common/SkylineFragment.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/common/SkylineFragment.kt index 9ad8119..c4fd04f 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/common/SkylineFragment.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/common/SkylineFragment.kt @@ -52,7 +52,6 @@ import androidx.compose.ui.unit.dp import androidx.constraintlayout.compose.ConstraintLayout import app.cash.paging.LoadStateError import app.cash.paging.LoadStateLoading -import app.cash.paging.LoadStateNotLoading import app.cash.paging.compose.collectAsLazyPagingItems import app.cash.paging.compose.itemKey import com.atproto.repo.StrongRef @@ -229,8 +228,8 @@ inline fun SkylineFragment ( Text("Retry") } } } } - - is LoadStateNotLoading -> { + is LoadStateLoading -> { item { LoadingCircle() } } + else -> { if(data != null) { items( data.itemCount, key = data.itemKey {when(it) { @@ -288,15 +287,8 @@ inline fun SkylineFragment ( ) } } - } + } } } - else -> { item { PlaceholderSkylineItem( - modifier = if(debuggable) Modifier.border(1.dp, Color.Black) else Modifier - .fillMaxWidth() - //.padding(horizontal = 4.dp), - .padding(vertical = 2.dp, horizontal = 4.dp), - elevate = true, - ) } } } if (data?.loadState?.append == LoadStateLoading) item { LoadingCircle() } } diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/common/TabbedSkylineFragment.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/common/TabbedSkylineFragment.kt index 24d4ac4..98f1df0 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/common/TabbedSkylineFragment.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/common/TabbedSkylineFragment.kt @@ -3,7 +3,13 @@ package com.morpho.app.ui.common import androidx.compose.foundation.layout.PaddingValues import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.rememberModalBottomSheetState -import androidx.compose.runtime.* +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.unit.dp import cafe.adriel.voyager.navigator.LocalNavigator @@ -19,6 +25,7 @@ import com.morpho.app.screens.base.tabbed.ThreadTab import com.morpho.app.ui.elements.doMenuOperation import com.morpho.app.util.ClipboardManager import com.morpho.butterfly.ButterflyAgent +import com.morpho.butterfly.ContentHandling import io.ktor.util.reflect.instanceOf import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.IO @@ -34,6 +41,7 @@ fun TabbedSkylineFragment( isProfileFeed: Boolean = false, uiUpdate: StateFlow, eventCallback: (Event) -> Unit = {}, + getContentHandling: (BskyPost) -> List = { listOf() }, ) { val agent = getKoin().get() val uiState = uiUpdate.collectAsState(initial = UIUpdate.Empty) @@ -91,7 +99,7 @@ fun TabbedSkylineFragment( onReplyClicked = { onReplyClicked(it) }, onLikeClicked = { ref -> agent.like(ref) }, onPostButtonClicked = { onPostButtonClicked() }, - getContentHandling = { post -> listOf() }, + getContentHandling = { post -> getContentHandling(post) }, contentPadding = paddingValues, isProfileFeed = isProfileFeed, feedUpdate = uiUpdate.filterIsInstance(), From b15b9eb961f568803c7a3bdf5cda4d4d37dbd4cc Mon Sep 17 00:00:00 2001 From: Orual Date: Sat, 21 Sep 2024 14:28:54 -0400 Subject: [PATCH 26/42] Navigation drawer added. More work on settings. --- .../ui/common/TabbedScreenScaffold.android.kt | 53 ++-- .../ui/common/TabbedScreenScaffold.apple.kt | 6 + .../kotlin/com/morpho/app/data/MorphoAgent.kt | 12 +- .../morpho/app/data/PreferencesRepository.kt | 7 +- .../app/screens/base/BaseScreenModel.kt | 2 + .../app/screens/base/tabbed/NavigationTabs.kt | 28 +- .../screens/base/tabbed/TabbedBaseScreen.kt | 29 +- .../morpho/app/screens/login/LoginScreen.kt | 2 +- .../app/screens/main/tabbed/TabbedHomeView.kt | 132 ++++++--- .../notifications/NotificationsView.kt | 55 ++-- .../app/screens/profile/TabbedProfileView.kt | 5 + .../screens/settings/TabbedSettingsView.kt | 2 + .../morpho/app/screens/thread/ThreadView.kt | 33 ++- .../com/morpho/app/ui/common/NavDrawer.kt | 262 ++++++++++++++++++ .../morpho/app/ui/common/SkylineFragment.kt | 7 - .../app/ui/common/TabbedScreenScaffold.kt | 8 + .../app/ui/profile/UserStatsFragment.kt | 6 +- .../app/ui/settings/AccessibilitySettings.kt | 8 +- .../kotlin/com/morpho/app/util/json.kt | 2 + .../ui/common/TabbedScreenScaffold.desktop.kt | 64 +++-- .../composeApp/src/desktopMain/kotlin/main.kt | 14 +- 21 files changed, 580 insertions(+), 157 deletions(-) create mode 100644 Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/settings/TabbedSettingsView.kt create mode 100644 Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/common/NavDrawer.kt diff --git a/Morpho/composeApp/src/androidMain/kotlin/com/morpho/app/ui/common/TabbedScreenScaffold.android.kt b/Morpho/composeApp/src/androidMain/kotlin/com/morpho/app/ui/common/TabbedScreenScaffold.android.kt index b405b82..d7a1091 100644 --- a/Morpho/composeApp/src/androidMain/kotlin/com/morpho/app/ui/common/TabbedScreenScaffold.android.kt +++ b/Morpho/composeApp/src/androidMain/kotlin/com/morpho/app/ui/common/TabbedScreenScaffold.android.kt @@ -3,12 +3,14 @@ package com.morpho.app.ui.common import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.navigationBars +import androidx.compose.material3.DrawerState import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Scaffold import androidx.compose.material3.TopAppBarScrollBehavior import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.input.nestedscroll.NestedScrollConnection +import com.morpho.app.model.bluesky.DetailedProfile import com.morpho.app.model.uidata.Event import com.morpho.app.model.uistate.ContentCardState @@ -19,16 +21,23 @@ actual fun TabbedScreenScaffold( topContent: @Composable () -> Unit, state: T?, modifier: Modifier, + drawerState: DrawerState, + profile: DetailedProfile? ) { - Scaffold( - contentWindowInsets = WindowInsets.navigationBars, - modifier = modifier, - topBar = { topContent() }, - bottomBar = { navBar() }, - content = { insets -> - content(insets, state) - } - ) + NavDrawer( + drawerState = drawerState, + profile = profile, + ) { + Scaffold( + contentWindowInsets = WindowInsets.navigationBars, + modifier = modifier, + topBar = { topContent() }, + bottomBar = { navBar() }, + content = { insets -> + content(insets, state) + } + ) + } } @OptIn(ExperimentalMaterial3Api::class) @@ -41,14 +50,22 @@ actual fun TabbedProfileScreenScaffold( modifier: Modifier, scrollBehavior: TopAppBarScrollBehavior, nestedScrollConnection: NestedScrollConnection, + drawerState: DrawerState, + profile: DetailedProfile? ) { - Scaffold( - contentWindowInsets = WindowInsets.navigationBars, - modifier = modifier, - topBar = { topContent(scrollBehavior) }, - bottomBar = { navBar() }, - content = { insets -> - content(insets, state) - } - ) + NavDrawer( + drawerState = drawerState, + profile = profile, + ) { + Scaffold( + contentWindowInsets = WindowInsets.navigationBars, + modifier = modifier, + topBar = { topContent(scrollBehavior) }, + bottomBar = { navBar() }, + content = { insets -> + content(insets, state) + } + ) + } + } \ No newline at end of file diff --git a/Morpho/composeApp/src/appleMain/kotlin/com/morpho/app/ui/common/TabbedScreenScaffold.apple.kt b/Morpho/composeApp/src/appleMain/kotlin/com/morpho/app/ui/common/TabbedScreenScaffold.apple.kt index 2f37bb8..b5c7b5d 100644 --- a/Morpho/composeApp/src/appleMain/kotlin/com/morpho/app/ui/common/TabbedScreenScaffold.apple.kt +++ b/Morpho/composeApp/src/appleMain/kotlin/com/morpho/app/ui/common/TabbedScreenScaffold.apple.kt @@ -3,12 +3,14 @@ package com.morpho.app.ui.common import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.navigationBars +import androidx.compose.material3.DrawerState import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Scaffold import androidx.compose.material3.TopAppBarScrollBehavior import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.input.nestedscroll.NestedScrollConnection +import com.morpho.app.model.bluesky.DetailedProfile import com.morpho.app.model.uidata.Event import com.morpho.app.model.uistate.ContentCardState @@ -19,6 +21,8 @@ actual fun TabbedScreenScaffold( topContent: @Composable () -> Unit, state: T?, modifier: Modifier, + drawerState: DrawerState, + profile: DetailedProfile? ) { Scaffold( contentWindowInsets = WindowInsets.navigationBars, @@ -41,5 +45,7 @@ actual fun TabbedProfileScreenScaffold( modifier: Modifier, scrollBehavior: TopAppBarScrollBehavior, nestedScrollConnection: NestedScrollConnection, + drawerState: DrawerState, + profile: DetailedProfile? ) { } \ No newline at end of file diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/data/MorphoAgent.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/data/MorphoAgent.kt index 1f1db96..930ffd4 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/data/MorphoAgent.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/data/MorphoAgent.kt @@ -3,9 +3,17 @@ package com.morpho.app.data import com.morpho.app.myLang import com.morpho.butterfly.ButterflyAgent import com.morpho.butterfly.Language -import org.koin.core.component.inject class MorphoAgent: ButterflyAgent() { - val morphoPrefsRepo: PreferencesRepository by inject() val myLanguage = Language(myLang ?: "en") // TODO: make this configurable + + val morphoPrefs: MorphoPreferences = MorphoPreferences( + kawaiiMode = true + ) + + + val kawaiiMode: Boolean + get() = morphoPrefs.kawaiiMode + + } \ No newline at end of file diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/data/PreferencesRepository.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/data/PreferencesRepository.kt index 46f27aa..6a87bcd 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/data/PreferencesRepository.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/data/PreferencesRepository.kt @@ -13,7 +13,11 @@ import io.github.xxfast.kstore.KStore import io.github.xxfast.kstore.extensions.updatesOrEmpty import io.github.xxfast.kstore.file.extensions.listStoreOf import kotlinx.coroutines.coroutineScope -import kotlinx.coroutines.flow.* +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.firstOrNull +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.onEach import kotlinx.serialization.Serializable import okio.Path.Companion.toPath import org.koin.core.component.KoinComponent @@ -40,6 +44,7 @@ data class AccessibilityPreferences( data class MorphoPreferences( val tabbed: Boolean = true, val undecorated: Boolean = true, + val kawaiiMode: Boolean = true, val notificationsFilter: NotificationsFilterState = NotificationsFilterState(), val accessibility: AccessibilityPreferences = AccessibilityPreferences(), ) diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/base/BaseScreenModel.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/base/BaseScreenModel.kt index 19488b4..4a04551 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/base/BaseScreenModel.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/base/BaseScreenModel.kt @@ -35,6 +35,8 @@ open class BaseScreenModel : ScreenModel, KoinComponent { val agent: MorphoAgent by inject() val labelService: ContentLabelService by inject() + val kawaiiMode: Boolean + get() = agent.kawaiiMode var userDid: Did? by mutableStateOf(agent.id) protected set diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/base/tabbed/NavigationTabs.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/base/tabbed/NavigationTabs.kt index 2fba160..ca5b998 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/base/tabbed/NavigationTabs.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/base/tabbed/NavigationTabs.kt @@ -6,6 +6,7 @@ import androidx.compose.material.icons.filled.DynamicFeed import androidx.compose.material.icons.filled.Home import androidx.compose.material.icons.filled.NotificationsNone import androidx.compose.material.icons.filled.Search +import androidx.compose.material.icons.filled.Settings import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme @@ -264,10 +265,10 @@ data class ThreadTab( } else { TabbedScreenScaffold( navBar = { navBar(navigator) }, - topContent = { ThreadTopBar(navigator = navigator) }, content = { _, _ -> LoadingCircle() }, + topContent = { ThreadTopBar(navigator = navigator) }, state = threadState, - modifier = Modifier + modifier = Modifier, ) } } @@ -330,4 +331,27 @@ data object MyProfileTab: TabScreen { } +} + +data object SettingsTab : TabScreen { + override val key: ScreenKey = "SettingsTab${uniqueScreenKey}" + + override val navBar: @Composable (@Contextual Navigator) -> Unit = { n -> + TabbedNavBar(MyProfileTab.options.index, n) + } + + @Composable + override fun Content() { + LoadingCircle() + } + + override val options: TabScreenOptions + @Composable get() { + return TabScreenOptions( + index = 5, + icon = { Icon(Icons.Default.Settings, contentDescription = "Settings", + tint = MaterialTheme.colorScheme.onBackground) }, + title = "Settings" + ) + } } \ No newline at end of file diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/base/tabbed/TabbedBaseScreen.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/base/tabbed/TabbedBaseScreen.kt index c9cd21b..f597403 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/base/tabbed/TabbedBaseScreen.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/base/tabbed/TabbedBaseScreen.kt @@ -2,7 +2,14 @@ package com.morpho.app.screens.base.tabbed import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.systemBarsPadding -import androidx.compose.material3.* +import androidx.compose.material3.Badge +import androidx.compose.material3.BadgedBox +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.PrimaryTabRow +import androidx.compose.material3.Tab +import androidx.compose.material3.TabRowDefaults +import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue @@ -38,18 +45,14 @@ data object TabbedBaseScreen: Tab { @OptIn(ExperimentalMaterial3Api::class, ExperimentalVoyagerApi::class) @Composable override fun Content() { - //ProvideNavigatorLifecycleKMPSupport { - Navigator( - HomeTab("startHome"), - disposeBehavior = NavigatorDisposeBehavior( - disposeNestedNavigators = false, - ) - ) { navigator -> - /*LaunchedEffect(Unit) { navigator.replaceAll(HomeTab("startHome2")) }*/ - SlideTabTransition(navigator) - } - //} - + Navigator( + HomeTab("startHome"), + disposeBehavior = NavigatorDisposeBehavior( + disposeNestedNavigators = false, + ) + ) { navigator -> + SlideTabTransition(navigator) + } } override val options: TabOptions diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/login/LoginScreen.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/login/LoginScreen.kt index 6d6cf65..59ded8c 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/login/LoginScreen.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/login/LoginScreen.kt @@ -214,7 +214,7 @@ fun LoginView( ) { var appPWOverride by rememberSaveable { mutableStateOf(false) } - val kawaiiMode = true + val kawaiiMode = remember { screenModel.kawaiiMode } Text( text = "Login to Bluesky", diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/main/tabbed/TabbedHomeView.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/main/tabbed/TabbedHomeView.kt index d7d2b66..af31ddd 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/main/tabbed/TabbedHomeView.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/main/tabbed/TabbedHomeView.kt @@ -10,22 +10,33 @@ import androidx.compose.animation.core.spring import androidx.compose.animation.slideInHorizontally import androidx.compose.animation.slideOutHorizontally import androidx.compose.animation.togetherWith +import androidx.compose.foundation.Image import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.statusBarsPadding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Menu +import androidx.compose.material3.DrawerState +import androidx.compose.material3.DrawerValue import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton import androidx.compose.material3.SecondaryScrollableTabRow import androidx.compose.material3.Tab import androidx.compose.material3.TabRowDefaults import androidx.compose.material3.TabRowDefaults.tabIndicatorOffset import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.material3.rememberDrawerState import androidx.compose.runtime.Composable import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment @@ -56,8 +67,12 @@ import com.morpho.app.ui.elements.AvatarShape import com.morpho.app.ui.elements.OutlinedAvatar import dev.icerock.moko.parcelize.Parcelable import dev.icerock.moko.parcelize.Parcelize +import kotlinx.coroutines.launch import kotlinx.serialization.Serializable +import morpho.composeapp.generated.resources.BlueSkyKawaii +import morpho.composeapp.generated.resources.Res import org.jetbrains.compose.resources.ExperimentalResourceApi +import org.jetbrains.compose.resources.painterResource import kotlin.math.max import kotlin.math.min import cafe.adriel.voyager.navigator.tab.Tab as NavTab @@ -135,8 +150,13 @@ fun TabScreen.TabbedHomeView( ) ) { nav -> val tabUri = sm.uriForTab(selectedTabIndex) + val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed) TabbedScreenScaffold( navBar = { navBar(navigator) }, + content = { insets, state -> + + SkylineTabTransition(nav, sm, insets, state) + }, topContent = { HomeTabRow( tabs = tabs, @@ -152,16 +172,16 @@ fun TabScreen.TabbedHomeView( } else if(index > selectedTabIndex) nav.push(tabs[index]) selectedTabIndex = index - } + }, + drawerState = drawerState, + kawaiiMode = sm.kawaiiMode, ) }, - content = { insets, state -> - - SkylineTabTransition(nav, sm, insets, state) - }, + state = sm.feedStates[tabUri] as ContentCardState.Skyline?, modifier = Modifier, - state = sm.feedStates[tabUri] as ContentCardState.Skyline? + drawerState = drawerState, + profile = sm.userProfile, ) } @@ -211,49 +231,75 @@ fun HomeTabRow( modifier: Modifier = Modifier, tabIndex: Int = 0, onChanged: (Int) -> Unit = {}, + drawerState: DrawerState = rememberDrawerState(initialValue = DrawerValue.Closed), + kawaiiMode: Boolean = false, ) { var selectedTabIndex by rememberSaveable { mutableIntStateOf(tabIndex) } + val scope = rememberCoroutineScope() - SecondaryScrollableTabRow( - selectedTabIndex = selectedTabIndex, - modifier = modifier.fillMaxWidth(),//.zIndex(1f), - edgePadding = 10.dp, - indicator = { tabPositions -> - if(tabPositions.isNotEmpty()) { - TabRowDefaults.SecondaryIndicator( - Modifier.tabIndicatorOffset(tabPositions[max(0, min(selectedTabIndex, tabs.lastIndex))]) - ) - } - } - ) { - tabs.forEachIndexed { index, tab -> - Tab( - selected = selectedTabIndex == index, + TopAppBar( + modifier = Modifier.fillMaxWidth(), + navigationIcon = { + IconButton( onClick = { - selectedTabIndex = max(0, min(index, tabs.lastIndex)) - onChanged(max(0, min(index, tabs.lastIndex))) + if(drawerState.isClosed) scope.launch { drawerState.open() } + else scope.launch { drawerState.close() } }, - //icon = { tab.icon() }, - text = { - Row( - verticalAlignment = Alignment.CenterVertically, - ){ - if(tab.avatar != null) { - OutlinedAvatar( - url = tab.avatar, - size = 20.dp, - avatarShape = AvatarShape.Rounded, - modifier = Modifier.padding(end = 8.dp), - ) - } - Text( - text = tab.title, - //style = MaterialTheme.typography.titleSmall, + modifier = if(kawaiiMode) Modifier.size(90.dp) else Modifier + ) { + if(kawaiiMode) { + Image( + painterResource(Res.drawable.BlueSkyKawaii), + contentDescription = "open navigation drawer (but kawaii)", + ) + } else { + Icon(Icons.Default.Menu, contentDescription = "open navigation drawer") + } + } + }, + title = { + SecondaryScrollableTabRow( + selectedTabIndex = selectedTabIndex, + edgePadding = 10.dp, + indicator = { tabPositions -> + if(tabPositions.isNotEmpty()) { + TabRowDefaults.SecondaryIndicator( + Modifier.tabIndicatorOffset(tabPositions[max(0, min(selectedTabIndex, tabs.lastIndex))]) ) - } } - ) - } - } + } + }, + divider = {}, + //modifier = Modifier.offset(y = 8.dp), + ) { + tabs.forEachIndexed { index, tab -> + Tab( + selected = selectedTabIndex == index, + onClick = { + selectedTabIndex = max(0, min(index, tabs.lastIndex)) + onChanged(max(0, min(index, tabs.lastIndex))) + }, + text = { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.padding(vertical = 12.dp) + ){ + if(tab.avatar != null) { + OutlinedAvatar( + url = tab.avatar, + size = 20.dp, + avatarShape = AvatarShape.Rounded, + modifier = Modifier.padding(end = 8.dp), + ) + } + Text( + text = tab.title, + ) + } } + ) + } + } + }, + ) } diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/notifications/NotificationsView.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/notifications/NotificationsView.kt index 427a463..2545b28 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/notifications/NotificationsView.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/notifications/NotificationsView.kt @@ -14,6 +14,8 @@ import androidx.compose.material.pullrefresh.PullRefreshIndicator import androidx.compose.material.pullrefresh.pullRefresh import androidx.compose.material.pullrefresh.rememberPullRefreshState import androidx.compose.material3.CenterAlignedTopAppBar +import androidx.compose.material3.DrawerState +import androidx.compose.material3.DrawerValue import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon @@ -22,6 +24,7 @@ import androidx.compose.material3.IconToggleButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.material3.TextButton +import androidx.compose.material3.rememberDrawerState import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue @@ -88,26 +91,12 @@ fun TabScreen.NotificationViewContent( val pager = sm.notifications.collectAsLazyPagingItems() var uiState by rememberSaveable { mutableStateOf(NotificationsUIState()) } val toMarkRead = mutableStateListOf() + + val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed) TabbedScreenScaffold( navBar = { navBar(navigator) }, - topContent = { - NotificationsTopBar( - navigator = navigator, - onSettingsClicked = { - showSettings = it - scope.launch { - listState.animateScrollToItem(0) - } - }, - showSettings = showSettings, - hasUnread = hasUnread, - markAsRead = { - sm.updateSeenNotifications() - } - ) - }, - state = uiState, - modifier = Modifier, + drawerState = drawerState, + profile = sm.userProfile, content = { insets, state -> val refreshing by remember { mutableStateOf(false)} @@ -272,7 +261,26 @@ fun TabScreen.NotificationViewContent( centerHorizontallyTo(parent) }, backgroundColor = MaterialTheme.colorScheme.background, contentColor = MaterialTheme.colorScheme.primary) } - } + }, + topContent = { + NotificationsTopBar( + navigator = navigator, + onSettingsClicked = { + showSettings = it + scope.launch { + listState.animateScrollToItem(0) + } + }, + showSettings = showSettings, + hasUnread = hasUnread, + markAsRead = { + sm.updateSeenNotifications() + }, + drawerState = drawerState, + ) + }, + state = uiState, + modifier = Modifier, ) } @@ -283,13 +291,18 @@ fun NotificationsTopBar( onSettingsClicked : (Boolean) -> Unit = {}, showSettings: Boolean = false, hasUnread: Boolean = false, - markAsRead: () -> Unit = {} + markAsRead: () -> Unit = {}, + drawerState: DrawerState = rememberDrawerState(initialValue = DrawerValue.Closed), ) { var show by remember { mutableStateOf(showSettings) } + val scope = rememberCoroutineScope() CenterAlignedTopAppBar( title = { Text("Notifications") }, navigationIcon = { - IconButton(onClick = { navigator.pop() }) { + IconButton(onClick = { + if(drawerState.isClosed) scope.launch { drawerState.open() } + else scope.launch { drawerState.close() } + }) { Icon(Icons.Default.Menu, contentDescription = "Menu") } }, diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/profile/TabbedProfileView.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/profile/TabbedProfileView.kt index b6a888b..666e5aa 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/profile/TabbedProfileView.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/profile/TabbedProfileView.kt @@ -5,12 +5,14 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.material3.DrawerValue import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.SecondaryScrollableTabRow import androidx.compose.material3.Tab import androidx.compose.material3.Text import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.TopAppBarScrollBehavior +import androidx.compose.material3.rememberDrawerState import androidx.compose.material3.rememberTopAppBarState import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue @@ -164,6 +166,7 @@ fun TabScreen.TabbedProfileContent( val navigator = LocalNavigator.currentOrThrow var selectedTabIndex by rememberSaveable { mutableIntStateOf(0) } val scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior(rememberTopAppBarState()) + val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed) val tabs = remember(myProfileState, profileState) { if (ownProfile) myProfileState.toTabList() else profileState?.toTabList() ?: listOf() } @@ -206,6 +209,8 @@ fun TabScreen.TabbedProfileContent( state = if(ownProfile) myProfileState.indexToState(selectedTabIndex) else profileState?.indexToState(selectedTabIndex), scrollBehavior = scrollBehavior, + profile = myProfileState.profile, + drawerState = drawerState, ) } //} diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/settings/TabbedSettingsView.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/settings/TabbedSettingsView.kt new file mode 100644 index 0000000..031816b --- /dev/null +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/settings/TabbedSettingsView.kt @@ -0,0 +1,2 @@ +package com.morpho.app.screens.settings + diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/thread/ThreadView.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/thread/ThreadView.kt index c6e7109..b049d90 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/thread/ThreadView.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/thread/ThreadView.kt @@ -4,8 +4,19 @@ import androidx.compose.animation.ExperimentalAnimationApi import androidx.compose.foundation.layout.PaddingValues import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.ArrowBackIosNew -import androidx.compose.material3.* -import androidx.compose.runtime.* +import androidx.compose.material3.CenterAlignedTopAppBar +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.Text +import androidx.compose.material3.rememberModalBottomSheetState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.unit.dp @@ -26,7 +37,11 @@ import com.morpho.app.screens.base.tabbed.ProfileTab import com.morpho.app.screens.base.tabbed.TabScreen import com.morpho.app.screens.base.tabbed.ThreadTab import com.morpho.app.screens.main.MainScreenModel -import com.morpho.app.ui.common.* +import com.morpho.app.ui.common.BottomSheetPostComposer +import com.morpho.app.ui.common.ComposerRole +import com.morpho.app.ui.common.LoadingCircle +import com.morpho.app.ui.common.RepostQueryDialog +import com.morpho.app.ui.common.TabbedScreenScaffold import com.morpho.app.ui.elements.doMenuOperation import com.morpho.app.ui.thread.ThreadFragment import com.morpho.app.util.ClipboardManager @@ -55,11 +70,6 @@ fun TabScreen.ThreadViewContent( TabbedScreenScaffold( navBar = { navBar(navigator) }, - topContent = { - ThreadTopBar(navigator = navigator) - }, - modifier = Modifier, - state = threadState, content = { insets, state -> when(state) { is ThreadUpdate.Empty -> { @@ -87,7 +97,12 @@ fun TabScreen.ThreadViewContent( } } - } + }, + topContent = { + ThreadTopBar(navigator = navigator) + }, + state = threadState, + modifier = Modifier, ) } diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/common/NavDrawer.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/common/NavDrawer.kt new file mode 100644 index 0000000..eb8a417 --- /dev/null +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/common/NavDrawer.kt @@ -0,0 +1,262 @@ +package com.morpho.app.ui.common + +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.foundation.layout.ExperimentalLayoutApi +import androidx.compose.foundation.layout.FlowRow +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.defaultMinSize +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Feedback +import androidx.compose.material3.Badge +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.DrawerState +import androidx.compose.material3.DrawerValue +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.ModalDrawerSheet +import androidx.compose.material3.ModalNavigationDrawer +import androidx.compose.material3.NavigationDrawerItem +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.material3.rememberDrawerState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalUriHandler +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import cafe.adriel.voyager.core.model.rememberNavigatorScreenModel +import cafe.adriel.voyager.navigator.LocalNavigator +import cafe.adriel.voyager.navigator.Navigator +import cafe.adriel.voyager.navigator.currentOrThrow +import cafe.adriel.voyager.navigator.tab.TabNavigator +import com.morpho.app.model.bluesky.DetailedProfile +import com.morpho.app.screens.base.tabbed.FeedsTab +import com.morpho.app.screens.base.tabbed.HomeTab +import com.morpho.app.screens.base.tabbed.MyProfileTab +import com.morpho.app.screens.base.tabbed.NotificationsTab +import com.morpho.app.screens.base.tabbed.SearchTab +import com.morpho.app.screens.base.tabbed.TabScreen +import com.morpho.app.screens.main.tabbed.TabbedMainScreenModel +import com.morpho.app.ui.elements.AvatarShape +import com.morpho.app.ui.elements.OutlinedAvatar +import com.morpho.app.util.openBrowser +import io.ktor.util.reflect.instanceOf +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch + +@OptIn(ExperimentalLayoutApi::class) +@Composable +fun NavDrawer( + profile: DetailedProfile? = null, + navigator: Navigator = if (LocalNavigator.current?.parent?.instanceOf(TabNavigator::class) == true) { + LocalNavigator.currentOrThrow + } else LocalNavigator.currentOrThrow.parent!!, + drawerState: DrawerState = rememberDrawerState(initialValue = DrawerValue.Closed), + navDrawerContent: @Composable ColumnScope.(drawerState: DrawerState, navigator: Navigator) -> Unit = { + drawer, nav -> + NavDrawerItems(drawerState = drawer, navigator = nav) + }, + content: @Composable () -> Unit, +) { + ModalNavigationDrawer( + gesturesEnabled = true, + drawerState = drawerState, + drawerContent = { + val uriHandler = LocalUriHandler.current + ModalDrawerSheet( + Modifier.width(300.dp) + ) { + val hPad = 16.dp + FlowRow( + verticalArrangement = Arrangement.Bottom, + horizontalArrangement = Arrangement.Start, + ) { + OutlinedAvatar( + url = profile?.avatar.orEmpty(), + contentDescription = + "Avatar for ${profile?.displayName.orEmpty()} ${profile?.handle?.handle.orEmpty()}", + modifier = Modifier.padding(start = hPad, top = hPad, bottom = 4.dp), + size = 80.dp, + avatarShape = AvatarShape.Rounded, + outlineColor = MaterialTheme.colorScheme.background, + onClicked = { navigator.push(MyProfileTab) } + ) + Column( + modifier = Modifier.align(Alignment.Bottom).padding(vertical = 4.dp) + ) { + if(profile?.displayName != null) { + Text( + text = profile.displayName, + style = MaterialTheme.typography.headlineSmall, + color = MaterialTheme.colorScheme.onSurface, + modifier = Modifier.padding(horizontal = hPad, vertical = 0.dp), + ) + } + Text( + text = "@${profile?.handle?.handle?: "Invalid Handle"}", + color = MaterialTheme.colorScheme.primary, + style = MaterialTheme.typography.labelMedium, + modifier = Modifier.padding(horizontal = hPad, vertical = 0.dp), + ) + } + } + Row( + modifier = Modifier.padding(horizontal = hPad) + ) { + TextButton( + onClick = { /*TODO*/ }, + contentPadding = PaddingValues(vertical = 4.dp, horizontal = 4.dp), + modifier = Modifier + .heightIn(min = 20.dp, max = 48.dp) + .defaultMinSize(minWidth = 10.dp) + ) { + Text( + text = "${profile?.followersCount}", + fontWeight = FontWeight.ExtraBold, + style = MaterialTheme.typography.labelMedium, + color = MaterialTheme.colorScheme.onSurface, + ) + Text( + text = " followers", + fontSize = MaterialTheme.typography.labelMedium.fontSize.times(0.9), + fontWeight = FontWeight.Normal, + color = MaterialTheme.colorScheme.onSurface, + style = MaterialTheme.typography.labelMedium, + ) + } + TextButton( + onClick = { /*TODO*/ }, + contentPadding = PaddingValues(vertical = 4.dp, horizontal = 4.dp), + modifier = Modifier + .heightIn(min = 20.dp, max = 48.dp) + .defaultMinSize(minWidth = 10.dp) + ) { + Text( + text = "${profile?.followsCount}", + fontWeight = FontWeight.ExtraBold, + style = MaterialTheme.typography.labelMedium, + color = MaterialTheme.colorScheme.onSurface, + ) + Text( + text = " following", + fontSize = MaterialTheme.typography.labelMedium.fontSize.times(0.9), + fontWeight = FontWeight.Normal, + color = MaterialTheme.colorScheme.onSurface, + style = MaterialTheme.typography.labelMedium, + ) + } + } + HorizontalDivider() + Spacer(Modifier.height(16.dp)) + navDrawerContent(drawerState, navigator) + Spacer(Modifier.weight(1f)) + Row( + modifier = Modifier.padding(horizontal = 16.dp, vertical = 16.dp), + ) { + + TextButton( + onClick = { + openBrowser("https://github.com/morpho-app/Morpho/issues/new", uriHandler) + }, + colors = ButtonDefaults.buttonColors(), + shape = MaterialTheme.shapes.medium, + modifier = Modifier.padding(horizontal = 8.dp), + + ) { + Icon( + imageVector = Icons.Default.Feedback, + contentDescription = "", + //tint = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.align(Alignment.CenterVertically) + .padding(end = 8.dp) + ) + Text( + "Feedback", + //color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.align(Alignment.CenterVertically) + ) + } + TextButton( + onClick = { + openBrowser("https://github.com/morpho-app/Morpho", uriHandler) + }, + border = BorderStroke(1.dp, MaterialTheme.colorScheme.outline), + shape = MaterialTheme.shapes.medium, + modifier = Modifier.padding(horizontal = 8.dp) + ) { + + Text( + "Help", + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + } + } + } + ) { + content() + } +} + +@Composable +fun NavDrawerItem( + tab: TabScreen, + navigator: Navigator = LocalNavigator.currentOrThrow, + drawerState: DrawerState = rememberDrawerState(initialValue = DrawerValue.Closed), + scope: CoroutineScope = rememberCoroutineScope(), + badge: @Composable() (() -> Unit)? = null, +) { + val nav = if (navigator.instanceOf(TabNavigator::class)) { + navigator.parent!! + } else navigator + val selected = nav.lastItem.key == tab.key + NavigationDrawerItem( + modifier = Modifier + .padding(horizontal = 16.dp, vertical = 4.dp), + icon = { tab.options.icon() }, + label = { Text(tab.options.title) }, + selected = selected, + badge = badge, + shape = MaterialTheme.shapes.medium, + onClick = { + if(selected) scope.launch { drawerState.close() } + nav.popUntil { it == tab } + nav.push(tab) + }, + ) +} + +@Composable +fun ColumnScope.NavDrawerItems( + navigator: Navigator = LocalNavigator.currentOrThrow, + drawerState: DrawerState = rememberDrawerState(initialValue = DrawerValue.Closed), +) { + NavDrawerItem(SearchTab, drawerState = drawerState, navigator = navigator) + NavDrawerItem(HomeTab("home"), drawerState = drawerState, navigator = navigator) + NavDrawerItem(NotificationsTab, drawerState = drawerState, navigator = navigator, + badge = { + val sm = LocalNavigator.currentOrThrow.rememberNavigatorScreenModel { TabbedMainScreenModel() } + val unread by sm.unreadNotificationsCount().collectAsState(0) + if(unread > 0) { + Badge( + containerColor = MaterialTheme.colorScheme.secondary + ) + } + }) + NavDrawerItem(FeedsTab, drawerState = drawerState, navigator = navigator) + NavDrawerItem(MyProfileTab, drawerState = drawerState, navigator = navigator) +} \ No newline at end of file diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/common/SkylineFragment.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/common/SkylineFragment.kt index c4fd04f..1722784 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/common/SkylineFragment.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/common/SkylineFragment.kt @@ -53,7 +53,6 @@ import androidx.constraintlayout.compose.ConstraintLayout import app.cash.paging.LoadStateError import app.cash.paging.LoadStateLoading import app.cash.paging.compose.collectAsLazyPagingItems -import app.cash.paging.compose.itemKey import com.atproto.repo.StrongRef import com.morpho.app.model.bluesky.BskyPost import com.morpho.app.model.bluesky.MorphoDataItem @@ -232,12 +231,6 @@ inline fun SkylineFragment ( else -> { if(data != null) { items( data.itemCount, - key = data.itemKey {when(it) { - is MorphoDataItem.FeedItem -> it.key - is MorphoDataItem.Post -> it.key - is MorphoDataItem.Thread -> it.key - else -> it.hashCode() - } } ) { index -> when(val item = data[index]) { is MorphoDataItem.Thread -> { diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/common/TabbedScreenScaffold.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/common/TabbedScreenScaffold.kt index a1a5ee9..9849503 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/common/TabbedScreenScaffold.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/common/TabbedScreenScaffold.kt @@ -8,8 +8,11 @@ import androidx.compose.animation.slideInHorizontally import androidx.compose.animation.slideOutHorizontally import androidx.compose.animation.togetherWith import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.material3.DrawerState +import androidx.compose.material3.DrawerValue import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.TopAppBarScrollBehavior +import androidx.compose.material3.rememberDrawerState import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.input.nestedscroll.NestedScrollConnection @@ -20,6 +23,7 @@ import cafe.adriel.voyager.navigator.CurrentScreen import cafe.adriel.voyager.navigator.Navigator import cafe.adriel.voyager.transitions.ScreenTransition import cafe.adriel.voyager.transitions.ScreenTransitionContent +import com.morpho.app.model.bluesky.DetailedProfile import com.morpho.app.model.uidata.Event import com.morpho.app.model.uistate.ContentCardState @@ -30,6 +34,8 @@ expect fun TabbedScreenScaffold( topContent: @Composable () -> Unit, state: T?, modifier: Modifier, + drawerState: DrawerState = rememberDrawerState(initialValue = DrawerValue.Closed), + profile: DetailedProfile? = null, ) @ExperimentalMaterial3Api @@ -42,6 +48,8 @@ expect fun TabbedProfileScreenScaffold( modifier: Modifier = Modifier, scrollBehavior: TopAppBarScrollBehavior, nestedScrollConnection: NestedScrollConnection = scrollBehavior.nestedScrollConnection, + drawerState: DrawerState = rememberDrawerState(initialValue = DrawerValue.Closed), + profile: DetailedProfile? = null, ) @OptIn(ExperimentalVoyagerApi::class) diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/profile/UserStatsFragment.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/profile/UserStatsFragment.kt index 53508ff..df6df9e 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/profile/UserStatsFragment.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/profile/UserStatsFragment.kt @@ -43,7 +43,7 @@ public fun UserStatsFragment( color = MaterialTheme.colorScheme.onSurface, ) Text( - text = " Followers", + text = " followers", fontSize = MaterialTheme.typography.labelMedium.fontSize.times(0.9), fontWeight = FontWeight.Normal, color = MaterialTheme.colorScheme.onSurface, @@ -64,7 +64,7 @@ public fun UserStatsFragment( color = MaterialTheme.colorScheme.onSurface, ) Text( - text = " Following", + text = " following", fontSize = MaterialTheme.typography.labelMedium.fontSize.times(0.9), fontWeight = FontWeight.Normal, color = MaterialTheme.colorScheme.onSurface, @@ -86,7 +86,7 @@ public fun UserStatsFragment( textAlign = TextAlign.Start ) Text( - text = " Posts", + text = " posts", fontSize = MaterialTheme.typography.labelMedium.fontSize.times(0.9), fontWeight = FontWeight.Normal, color = MaterialTheme.colorScheme.onSurface, diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/settings/AccessibilitySettings.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/settings/AccessibilitySettings.kt index 0521da4..d9f3f42 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/settings/AccessibilitySettings.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/settings/AccessibilitySettings.kt @@ -1,14 +1,20 @@ package com.morpho.app.ui.settings import androidx.compose.material3.Switch -import androidx.compose.runtime.* +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.text.AnnotatedString +import com.morpho.app.data.MorphoAgent import com.morpho.app.ui.elements.SettingsGroup import com.morpho.app.ui.elements.SettingsItem @Composable fun AccessibilitySettings( + agent: MorphoAgent, distinguish: Boolean = true, modifier: Modifier = Modifier, ) { diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/util/json.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/util/json.kt index 49fb31f..f5b200b 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/util/json.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/util/json.kt @@ -1,5 +1,6 @@ package com.morpho.app.util +import com.morpho.butterfly.butterflySerializersModule import kotlinx.serialization.KSerializer import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json @@ -11,6 +12,7 @@ val json = Json { classDiscriminator = "${'$'}type" ignoreUnknownKeys = true prettyPrint = true + serializersModule = butterflySerializersModule } val JsonElement.recordType: String diff --git a/Morpho/composeApp/src/desktopMain/kotlin/com/morpho/app/ui/common/TabbedScreenScaffold.desktop.kt b/Morpho/composeApp/src/desktopMain/kotlin/com/morpho/app/ui/common/TabbedScreenScaffold.desktop.kt index 3218b28..83b59c9 100644 --- a/Morpho/composeApp/src/desktopMain/kotlin/com/morpho/app/ui/common/TabbedScreenScaffold.desktop.kt +++ b/Morpho/composeApp/src/desktopMain/kotlin/com/morpho/app/ui/common/TabbedScreenScaffold.desktop.kt @@ -3,6 +3,7 @@ package com.morpho.app.ui.common import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.navigationBars +import androidx.compose.material3.DrawerState import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Scaffold import androidx.compose.material3.TopAppBarScrollBehavior @@ -10,6 +11,7 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.input.nestedscroll.NestedScrollConnection import androidx.compose.ui.input.nestedscroll.nestedScroll +import com.morpho.app.model.bluesky.DetailedProfile import com.morpho.app.model.uidata.Event import com.morpho.app.model.uistate.ContentCardState import com.morpho.app.ui.elements.WrappedColumn @@ -21,20 +23,27 @@ actual fun TabbedScreenScaffold( topContent: @Composable () -> Unit, state: T?, modifier: Modifier, + drawerState: DrawerState, + profile: DetailedProfile? ) { - Scaffold( - contentWindowInsets = WindowInsets.navigationBars, - modifier = modifier, - bottomBar = { navBar() }, - content = { - WrappedColumn( - modifier = modifier - ) { - topContent() - content(it, state) + NavDrawer( + drawerState = drawerState, + profile = profile, + ) { + Scaffold( + contentWindowInsets = WindowInsets.navigationBars, + modifier = modifier, + bottomBar = { navBar() }, + content = { + WrappedColumn( + modifier = modifier + ) { + topContent() + content(it, state) + } } - } - ) + ) + } } @OptIn(ExperimentalMaterial3Api::class) @@ -47,18 +56,25 @@ actual fun TabbedProfileScreenScaffold( modifier: Modifier, scrollBehavior: TopAppBarScrollBehavior, nestedScrollConnection: NestedScrollConnection, + drawerState: DrawerState, + profile: DetailedProfile? ) { - Scaffold( - contentWindowInsets = WindowInsets.navigationBars, - modifier = modifier, - bottomBar = { navBar() }, - content = { - WrappedColumn( - modifier = modifier.nestedScroll(scrollBehavior.nestedScrollConnection) - ) { - topContent(scrollBehavior) - content(it, state) + NavDrawer( + drawerState = drawerState, + profile = profile, + ) { + Scaffold( + contentWindowInsets = WindowInsets.navigationBars, + modifier = modifier, + bottomBar = { navBar() }, + content = { + WrappedColumn( + modifier = modifier.nestedScroll(scrollBehavior.nestedScrollConnection) + ) { + topContent(scrollBehavior) + content(it, state) + } } - } - ) + ) + } } \ No newline at end of file diff --git a/Morpho/composeApp/src/desktopMain/kotlin/main.kt b/Morpho/composeApp/src/desktopMain/kotlin/main.kt index a53e9ea..22073b4 100644 --- a/Morpho/composeApp/src/desktopMain/kotlin/main.kt +++ b/Morpho/composeApp/src/desktopMain/kotlin/main.kt @@ -49,15 +49,12 @@ import com.github.tkuenneth.nativeparameterstoreaccess.NativeParameterStoreAcces import com.github.tkuenneth.nativeparameterstoreaccess.WindowsRegistry.getWindowsRegistryEntry import com.morpho.app.App import com.morpho.app.data.MorphoAgent -import com.morpho.app.data.PreferencesRepository import com.morpho.app.di.appModule import com.morpho.app.di.dataModule import com.morpho.app.di.storageModule import com.morpho.app.ui.theme.MorphoTheme import com.morpho.butterfly.auth.SessionRepository import com.morpho.butterfly.auth.UserRepository -import kotlinx.coroutines.flow.firstOrNull -import kotlinx.coroutines.runBlocking import morpho.composeapp.generated.resources.Res import morpho.composeapp.generated.resources.morpho_icon_transparent import net.harawata.appdirs.AppDirsFactory @@ -99,17 +96,10 @@ fun main() = application { koin.get { parametersOf(storageDir) } koin.get { parametersOf(storageDir) } val agent = koin.get() - val prefs = koin.get { parametersOf(storageDir) } - - val morphoPrefs = runBlocking { - prefs.prefs.firstOrNull()?.firstOrNull()?.morphoPrefs - } - val (undecorated, tabbed) = if (morphoPrefs != null) { + val morphoPrefs = agent.morphoPrefs + val (undecorated, tabbed) = run { log.d{ "Morpho Preferences: $morphoPrefs" } morphoPrefs.tabbed to morphoPrefs.undecorated - } else { - log.d {"No Morpho Preferences found, using defaults" } - true to true } val windowState = rememberWindowState( placement = WindowPlacement.Floating, From d8a95e4015f74356ae7d04ae4eb8515c195dede5 Mon Sep 17 00:00:00 2001 From: Orual Date: Sat, 21 Sep 2024 14:31:19 -0400 Subject: [PATCH 27/42] Added external extensibility and fallback deserialization to PreferencesUnion as a test. Seems to have worked, now needs to be implemented for the other atproto type unions in here. Ultimately we should be able to generate most of the extra boilerplate with Lexsync alongside the lexicons. --- Butterfly | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Butterfly b/Butterfly index 0dc6479..1ce3474 160000 --- a/Butterfly +++ b/Butterfly @@ -1 +1 @@ -Subproject commit 0dc64799ea533103e24c9e4aaf26fd1333731282 +Subproject commit 1ce3474cb640c8fa2b253d49a685cdda1b59b92f From 7d417ab3ada9097d55791cb958dcf64d4f43cbbb Mon Sep 17 00:00:00 2001 From: Orual Date: Sat, 21 Sep 2024 14:48:56 -0400 Subject: [PATCH 28/42] Bug https://github.com/Kotlin/kotlinx.serialization/issues/2288 is fixed, so the valueClassSerializer workaround is no longer needed and we can delete it and A LOT of boilerplate code. --- Butterfly | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Butterfly b/Butterfly index 1ce3474..b9555f7 160000 --- a/Butterfly +++ b/Butterfly @@ -1 +1 @@ -Subproject commit 1ce3474cb640c8fa2b253d49a685cdda1b59b92f +Subproject commit b9555f72b5b741aacd1362cb3e4391e7ee305b6a From 20536f174be5c0d94f923ec5707ea35fc7333671 Mon Sep 17 00:00:00 2001 From: Orual Date: Sat, 21 Sep 2024 14:49:04 -0400 Subject: [PATCH 29/42] Bug https://github.com/Kotlin/kotlinx.serialization/issues/2288 is fixed, so the valueClassSerializer workaround is no longer needed and we can delete it and A LOT of boilerplate code. --- .../app/model/bluesky/ThreadViewPostUnion.kt | 29 +++---------------- 1 file changed, 4 insertions(+), 25 deletions(-) diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/ThreadViewPostUnion.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/ThreadViewPostUnion.kt index 7a98811..64b6769 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/ThreadViewPostUnion.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/ThreadViewPostUnion.kt @@ -1,48 +1,27 @@ package com.morpho.app.model.bluesky -import kotlinx.serialization.KSerializer import kotlinx.serialization.SerialName -import com.morpho.butterfly.valueClassSerializer import kotlinx.serialization.Serializable import kotlin.jvm.JvmInline -@kotlinx.serialization.Serializable +@Serializable public sealed interface ThreadViewPostUnion { - public class ThreadViewPostSerializer : KSerializer by valueClassSerializer( - serialName = "app.bsky.feed.defs#threadViewPost", - constructor = ThreadViewPostUnion::ThreadViewPost, - valueProvider = ThreadViewPost::value, - valueSerializerProvider = { app.bsky.feed.ThreadViewPost.serializer() }, - ) - @Serializable(with = ThreadViewPostSerializer::class) + @Serializable @JvmInline @SerialName("app.bsky.feed.defs#threadViewPost") public value class ThreadViewPost( public val `value`: app.bsky.feed.ThreadViewPost, ) : ThreadViewPostUnion - public class NotFoundPostSerializer : KSerializer by valueClassSerializer( - serialName = "app.bsky.feed.defs#notFoundPost", - constructor = ThreadViewPostUnion::NotFoundPost, - valueProvider = NotFoundPost::value, - valueSerializerProvider = { app.bsky.feed.NotFoundPost.serializer() }, - ) - - @Serializable(with = NotFoundPostSerializer::class) + @Serializable @JvmInline @SerialName("app.bsky.feed.defs#notFoundPost") public value class NotFoundPost( public val `value`: app.bsky.feed.NotFoundPost, ) : ThreadViewPostUnion - public class BlockedPostSerializer : KSerializer by valueClassSerializer( - serialName = "app.bsky.feed.defs#blockedPost", - constructor = ThreadViewPostUnion::BlockedPost, - valueProvider = BlockedPost::value, - valueSerializerProvider = { app.bsky.feed.BlockedPost.serializer() }, - ) - @Serializable(with = BlockedPostSerializer::class) + @Serializable @JvmInline @SerialName("app.bsky.feed.defs#blockedPost") public value class BlockedPost( From 0e33258a844162bcff78057482e3d072b56cbde3 Mon Sep 17 00:00:00 2001 From: Orual Date: Sat, 21 Sep 2024 23:23:33 -0400 Subject: [PATCH 30/42] Working on the settings screen - Various sections built out - added a library to access build config variables on any platform - Made most of a BskyLabelService display fragment, for the labeler view that doesn't exist quite yet - Added a couple new Morpho-specific settings to the settings class - Various other cleanup and fixes --- Butterfly | 2 +- Morpho/build.gradle.kts | 1 + Morpho/composeApp/build.gradle.kts | 73 +++- .../com/morpho/app/MorphoApplication.kt | 20 +- .../DetailedProfileFragment.android.kt | 236 ++++++++++++- .../profile/DetailedProfileFragment.apple.kt | 16 + .../morpho/app/data/ContentLabelService.kt | 23 +- .../kotlin/com/morpho/app/data/FeedTuner.kt | 28 +- .../kotlin/com/morpho/app/data/MorphoAgent.kt | 139 +++++++- .../morpho/app/data/PreferencesRepository.kt | 320 +++++++++++------- .../kotlin/com/morpho/app/di/AppModule.kt | 6 +- .../app/model/bluesky/BskyLabelService.kt | 152 ++++++++- .../app/model/bluesky/BskyPostFeature.kt | 23 +- .../app/model/bluesky/BskyPreferences.kt | 160 --------- .../app/model/uidata/ContentLabelService.kt | 2 +- .../morpho/app/model/uidata/FeedPresenter.kt | 1 + .../app/model/uidata/ProfilePresenters.kt | 4 +- .../main/tabbed/TabbedMainScreenModel.kt | 3 +- .../app/ui/common/TabbedSkylineFragment.kt | 4 +- .../morpho/app/ui/elements/SettingsItems.kt | 104 +++--- .../app/ui/profile/DetailedProfileFragment.kt | 72 +++- .../app/ui/settings/AccessibilitySettings.kt | 47 +-- .../ui/settings/AdditionalLabelerSettings.kt | 106 ++++++ .../app/ui/settings/AppearanceSettings.kt | 100 ++++++ .../app/ui/settings/BuiltinContentFilters.kt | 164 +++++++++ .../morpho/app/ui/settings/FeedPreferences.kt | 161 +++++++++ .../app/ui/settings/LanguageSettings.kt | 147 ++++++++ .../app/ui/settings/PersonalModSettings.kt | 77 +++++ .../app/ui/settings/SettingsFragment.kt | 114 +++++++ .../kotlin/com/morpho/app/util/json.kt | 41 ++- .../morpho/app/ui/post/PostImage.desktop.kt | 20 +- .../DetailedProfileFragment.desktop.kt | 243 ++++++++++++- .../composeApp/src/desktopMain/kotlin/main.kt | 4 +- Morpho/gradle.properties | 4 +- Morpho/gradle/libs.versions.toml | 19 ++ Morpho/settings.gradle.kts | 1 + gradle.properties | 4 +- gradle/libs.versions.toml | 17 + 38 files changed, 2195 insertions(+), 463 deletions(-) create mode 100644 Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/settings/AdditionalLabelerSettings.kt create mode 100644 Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/settings/AppearanceSettings.kt create mode 100644 Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/settings/BuiltinContentFilters.kt create mode 100644 Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/settings/FeedPreferences.kt create mode 100644 Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/settings/LanguageSettings.kt create mode 100644 Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/settings/PersonalModSettings.kt create mode 100644 Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/settings/SettingsFragment.kt diff --git a/Butterfly b/Butterfly index b9555f7..f0ae7dd 160000 --- a/Butterfly +++ b/Butterfly @@ -1 +1 @@ -Subproject commit b9555f72b5b741aacd1362cb3e4391e7ee305b6a +Subproject commit f0ae7dd5b8b488643b606bfc555ee145500fba07 diff --git a/Morpho/build.gradle.kts b/Morpho/build.gradle.kts index c6d45d4..3922adb 100644 --- a/Morpho/build.gradle.kts +++ b/Morpho/build.gradle.kts @@ -14,6 +14,7 @@ plugins { alias(libs.plugins.kotlinParcelize).apply(false) alias(libs.plugins.androidApplication).apply(false) alias(libs.plugins.androidLibrary).apply(false) + id("com.codingfeline.buildkonfig") version "0.15.2" apply false } diff --git a/Morpho/composeApp/build.gradle.kts b/Morpho/composeApp/build.gradle.kts index f40ca6d..9d6ec2e 100644 --- a/Morpho/composeApp/build.gradle.kts +++ b/Morpho/composeApp/build.gradle.kts @@ -1,3 +1,4 @@ +import com.codingfeline.buildkonfig.compiler.FieldSpec.Type.STRING import org.jetbrains.compose.desktop.application.dsl.TargetFormat import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi @@ -10,12 +11,54 @@ plugins { alias(libs.plugins.compose.compiler) alias(libs.plugins.androidApplication) + id("com.codingfeline.buildkonfig") id("kotlin-parcelize") //id("kotlin-kapt") //id("com.rickclephas.kmp.nativecoroutines") version "1.0.0-ALPHA-27" } +val versionString = "1.0.0-alpha_1" +val packageString = "com.morpho.app" + +buildkonfig { + packageName = packageString + // objectName = "YourAwesomeConfig" + // exposeObjectWithName = "YourAwesomePublicConfig" + + defaultConfigs { + buildConfigField(STRING, "versionString", versionString) + } + defaultConfigs("dev") { + buildConfigField(STRING, "versionString", "${versionString}-dev") + } + + targetConfigs { + create("android") { + buildConfigField(STRING, "versionString", "android-${versionString}") + } + create("desktop") { + buildConfigField(STRING, "versionString", "desktop-${versionString}") + } + create("ios") { + buildConfigField(STRING, "versionString", "ios-${versionString}") + } + + } + targetConfigs("dev") { + create("android") { + buildConfigField(STRING, "versionString", "android-${versionString}-dev") + } + create("desktop") { + buildConfigField(STRING, "versionString", "desktop-${versionString}-dev") + } + create("ios") { + buildConfigField(STRING, "versionString", "ios-${versionString}-dev") + } + + } +} + kotlin { androidTarget { @OptIn(ExperimentalKotlinGradlePluginApi::class) @@ -84,8 +127,8 @@ kotlin { implementation(libs.kotlin.jwt) - implementation("androidx.paging:paging-runtime:3.3.0-alpha02") - implementation("androidx.paging:paging-compose:3.3.0-alpha02") + implementation(libs.androidx.paging.runtime) + implementation(libs.androidx.paging.compose) } commonMain.dependencies { @@ -99,8 +142,8 @@ kotlin { implementation("androidx.datastore:datastore-preferences-core:1.1.1") implementation("androidx.datastore:datastore-core:1.1.1") - implementation("app.cash.paging:paging-common:3.3.0-alpha02-0.5.1") - implementation("app.cash.paging:paging-compose-common:3.3.0-alpha02-0.5.1") + implementation(libs.paging.common) + implementation(libs.paging.compose.common) implementation(compose.runtime) implementation(compose.foundation) @@ -177,8 +220,7 @@ kotlin { implementation(libs.voyager.navigator) // Screen Model implementation(libs.voyager.screenmodel) - implementation("org.jetbrains.androidx.lifecycle:lifecycle-viewmodel-compose:2.8.0") - implementation("cafe.adriel.voyager:voyager-lifecycle-kmp:1.1.0-beta02") + implementation(libs.voyager.lifecycle.kmp) // BottomSheetNavigator implementation(libs.voyager.bottom.sheet.navigator) // TabNavigator @@ -193,12 +235,12 @@ kotlin { implementation(libs.slf4j.api) //implementation(libs.slf4j.simple) - implementation("com.gu.android:toolargetool:0.3.0") - api("dev.icerock.moko:parcelize:0.9.0") + implementation(libs.toolargetool) + api(libs.parcelize) } nativeMain.dependencies { - implementation("app.cash.paging:paging-runtime-uikit:3.3.0-alpha02-0.5.1") + implementation(libs.paging.runtime.uikit) } desktopMain.dependencies { implementation(compose.desktop.currentOs) @@ -215,7 +257,7 @@ kotlin { implementation(libs.kotlin.test) @OptIn(org.jetbrains.compose.ExperimentalComposeLibrary::class) implementation(compose.uiTest) - implementation("app.cash.paging:paging-testing:3.3.0-alpha02-0.5.1") + implementation(libs.paging.testing) } @@ -227,7 +269,7 @@ kotlin { } getByName("commonMain") { dependencies { - implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.1") + implementation(libs.kotlinx.coroutines) } } } @@ -242,11 +284,11 @@ android { sourceSets["main"].resources.srcDirs("src/commonMain/resources") defaultConfig { - applicationId = "com.morpho.app" + applicationId = packageString minSdk = libs.versions.android.minSdk.get().toInt() targetSdk = libs.versions.android.targetSdk.get().toInt() versionCode = 1 - versionName = "1.0" + versionName = versionString } packaging { resources { @@ -302,8 +344,9 @@ compose.desktop { TargetFormat.AppImage, TargetFormat.Pkg ) - packageName = "com.morpho.app" - packageVersion = "1.0.0" + packageName = packageString + + packageVersion = versionString.split("-")[0] } } } diff --git a/Morpho/composeApp/src/androidMain/kotlin/com/morpho/app/MorphoApplication.kt b/Morpho/composeApp/src/androidMain/kotlin/com/morpho/app/MorphoApplication.kt index 4da1904..123ec47 100644 --- a/Morpho/composeApp/src/androidMain/kotlin/com/morpho/app/MorphoApplication.kt +++ b/Morpho/composeApp/src/androidMain/kotlin/com/morpho/app/MorphoApplication.kt @@ -4,12 +4,11 @@ import android.app.Application import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.DefaultLifecycleObserver import com.gu.toolargetool.TooLargeTool +import com.morpho.app.data.MorphoAgent import com.morpho.app.data.PreferencesRepository import com.morpho.app.di.appModule import com.morpho.app.di.dataModule import com.morpho.app.di.storageModule -import com.morpho.butterfly.AtIdentifier -import com.morpho.butterfly.Butterfly import com.morpho.butterfly.auth.SessionRepository import com.morpho.butterfly.auth.UserRepository import org.koin.android.annotation.KoinViewModel @@ -28,7 +27,7 @@ class AndroidMainViewModel(app: Application): AndroidViewModel(app), DefaultLife val sessionRepository = app.getKoin().get() val userRepository = app.getKoin().get() - val api = app.getKoin().get() + val agent = app.getKoin().get() } class MorphoApplication : Application() { @@ -40,17 +39,10 @@ class MorphoApplication : Application() { androidLogger() modules(androidModule, appModule, storageModule, dataModule) }.koin - val sessionRepository = koin.get { parametersOf(cacheDir.path.toString()) } - val userRepository = koin.get { parametersOf(cacheDir.path.toString()) } - val prefs = koin.get { parametersOf(cacheDir.path.toString()) } - val id: AtIdentifier? = if(sessionRepository.auth?.did != null) { - sessionRepository.auth?.did - } else if (sessionRepository.auth?.handle != null) { - sessionRepository.auth?.handle - } else { - userRepository.firstUser()?.id - } - val api = koin.get { parametersOf(id) } + koin.get { parametersOf(cacheDir.path.toString()) } + koin.get { parametersOf(cacheDir.path.toString()) } + koin.get { parametersOf(cacheDir.path.toString()) } + koin.get() super.onCreate() } } diff --git a/Morpho/composeApp/src/androidMain/kotlin/com/morpho/app/ui/profile/DetailedProfileFragment.android.kt b/Morpho/composeApp/src/androidMain/kotlin/com/morpho/app/ui/profile/DetailedProfileFragment.android.kt index 30d216c..f9a3b31 100644 --- a/Morpho/composeApp/src/androidMain/kotlin/com/morpho/app/ui/profile/DetailedProfileFragment.android.kt +++ b/Morpho/composeApp/src/androidMain/kotlin/com/morpho/app/ui/profile/DetailedProfileFragment.android.kt @@ -4,13 +4,38 @@ import androidx.compose.animation.animateContentSize import androidx.compose.animation.core.Spring import androidx.compose.animation.core.spring import androidx.compose.foundation.background -import androidx.compose.foundation.layout.* +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ExperimentalLayoutApi +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.WindowInsetsSides +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.only +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.requiredHeight +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.systemBars +import androidx.compose.foundation.layout.widthIn +import androidx.compose.foundation.layout.windowInsetsPadding +import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.text.selection.SelectionContainer import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.ArrowBackIosNew -import androidx.compose.material3.* +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.IconButtonDefaults +import androidx.compose.material3.LargeTopAppBar +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.material3.TopAppBarScrollBehavior import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -24,7 +49,10 @@ import coil3.compose.AsyncImage import coil3.compose.LocalPlatformContext import coil3.request.ImageRequest import coil3.request.crossfade +import com.morpho.app.model.bluesky.BskyLabelService import com.morpho.app.model.bluesky.DetailedProfile +import com.morpho.app.model.uidata.Event +import com.morpho.app.model.uidata.LabelerEvent import com.morpho.app.ui.elements.AvatarShape import com.morpho.app.ui.elements.OutlinedAvatar import com.morpho.app.ui.elements.RichTextElement @@ -48,6 +76,7 @@ public actual fun DetailedProfileFragment( isTopLevel:Boolean, scrollBehavior: TopAppBarScrollBehavior, onBackClicked: () -> Unit, + eventCallback: (Event) -> Unit, ) { val scrollState = rememberScrollState() val name = profile.displayName ?: profile.handle.handle @@ -255,6 +284,209 @@ public actual fun DetailedProfileFragment( } } + } + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +actual fun LabelerProfileFragment( + labeler: BskyLabelService, + modifier: Modifier, + isSubscribed: Boolean, + isTopLevel: Boolean, + scrollBehavior: TopAppBarScrollBehavior, + onBackClicked: () -> Unit, + eventCallback: (Event) -> Unit, +) { + val scrollState = rememberScrollState() + val name = labeler.displayName ?: labeler.handle.handle + val bannerHeight = if (scrollBehavior.state.collapsedFraction <= .2) { + 155.dp + } else { + (155.dp - (60 * scrollBehavior.state.collapsedFraction).dp) + } + val collapsed = scrollBehavior.state.collapsedFraction > 0.5 + + ConstraintLayout( + modifier = Modifier + .fillMaxWidth() + .nestedScroll(scrollBehavior.nestedScrollConnection) + .background(MaterialTheme.colorScheme.background) + .verticalScroll(scrollState) + ) { + val (appbar, userStats, banner, labels, text, collapsedText) = createRefs() + + AsyncImage( + model = ImageRequest.Builder(LocalPlatformContext.current) + .data(labeler.creator?.banner.orEmpty()) + .crossfade(true) + .build(), + placeholder = painterResource(Res.drawable.test_banner), + contentDescription = "Profile Banner for ${labeler.displayName} ${labeler.handle}", + contentScale = ContentScale.Crop, + alignment = Alignment.TopCenter, + modifier = Modifier + .fillMaxWidth() + .constrainAs(banner) { + top.linkTo(parent.top) + } + .animateContentSize( + spring( + stiffness = Spring.StiffnessMediumLow, + dampingRatio = Spring.DampingRatioNoBouncy + ) + ) + .requiredHeight(bannerHeight) + ) + + LargeTopAppBar( + title = { + ConstraintLayout(//constraintSet = , + modifier = Modifier + .fillMaxWidth() + ) { + val (avatar, buttons, info) = createRefs() + val expanded = scrollBehavior.state.collapsedFraction <= 0.5 + val avatarSize = (80.dp - (30.0 * scrollBehavior.state.collapsedFraction).dp) + val centreGuideFraction = if(expanded) .6f else .5f + val avatarGuide = createGuidelineFromStart(.1f ) + val centreGuide = createGuidelineFromTop(centreGuideFraction) + + if(expanded){ + LabelerButtons( + subscribed = isSubscribed, + modifier = Modifier + .constrainAs(buttons) { + centerAround(centreGuide) + end.linkTo(parent.end, 12.dp) + }, + onSubscribeClicked = { + eventCallback(LabelerEvent.Subscribe(labeler.did)) + }, + onUnsubscribeClicked = { + eventCallback(LabelerEvent.Unsubscribe(labeler.did)) + }, + onMenuClicked = { + // TODO: add labeler menu + }, + ) + OutlinedAvatar( + url = labeler.avatar.orEmpty(), + contentDescription = "Avatar for ${labeler.displayName} ${labeler.handle}", + modifier = Modifier + .constrainAs(avatar) { + centerAround(avatarGuide) + }, + size = avatarSize, + avatarShape = AvatarShape.Rounded + ) + } else { + Surface( + color = MaterialTheme.colorScheme.background, + shape = MaterialTheme.shapes.small, + modifier = Modifier + .height(avatarSize) + .constrainAs(info) { + centerAround(centreGuide) + start.linkTo(avatarGuide, (-20).dp) + }, + ) { + Row { + OutlinedAvatar( + url = labeler.avatar.orEmpty(), + contentDescription = "Avatar for ${labeler.displayName} ${labeler.handle}", + size = avatarSize, + avatarShape = AvatarShape.Rounded + ) + Column( + verticalArrangement = Arrangement.Bottom, + modifier = Modifier.padding(start = 10.dp, end = 8.dp, bottom = 4.dp) + ) { + Text( + text = name, + style = MaterialTheme.typography.headlineSmall, + color = MaterialTheme.colorScheme.onSurface, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + Text( + text = " @${labeler.handle}", + color = MaterialTheme.colorScheme.primary, + style = MaterialTheme.typography.labelMedium, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + } + } + + } + } + + } + }, + navigationIcon = { + if (isTopLevel) { + IconButton( + onClick = { onBackClicked() }, + modifier = Modifier.size(30.dp), + colors = IconButtonDefaults.filledIconButtonColors( + containerColor = MaterialTheme.colorScheme.onSurface.copy(0.6f), + contentColor = MaterialTheme.colorScheme.surface + ) + ) { + Icon( + imageVector = Icons.Default.ArrowBackIosNew, + contentDescription = "Back", + ) + } + } + }, + actions = {}, + scrollBehavior = scrollBehavior, + colors = TopAppBarDefaults.largeTopAppBarColors( + containerColor = Color.Transparent + ), + modifier = Modifier + .constrainAs(appbar) { + top.linkTo(parent.top) + } + .windowInsetsPadding(WindowInsets.systemBars.only(WindowInsetsSides.Top)) + .wrapContentHeight(Alignment.Top) + , + windowInsets = WindowInsets.systemBars.only(WindowInsetsSides.Top) + ) + if(!collapsed){ + Column( + modifier = Modifier + .constrainAs(text) { + top.linkTo(userStats.bottom, (-10).dp) + start.linkTo(parent.start) + } + .padding(start = 20.dp, end = 20.dp, top = 0.dp) + ) { + + SelectionContainer { + Text( + text = name, + style = MaterialTheme.typography.headlineMedium, + color = MaterialTheme.colorScheme.onSurface + ) + } + SelectionContainer { + Text( + text = " @${labeler.handle}", + color = MaterialTheme.colorScheme.primary, + style = MaterialTheme.typography.labelMedium, + ) + } + + Spacer(modifier = Modifier.height(10.dp)) + SelectionContainer { + RichTextElement(labeler.creator?.description.orEmpty()) + } + } + } } } \ No newline at end of file diff --git a/Morpho/composeApp/src/appleMain/kotlin/com/morpho/app/ui/profile/DetailedProfileFragment.apple.kt b/Morpho/composeApp/src/appleMain/kotlin/com/morpho/app/ui/profile/DetailedProfileFragment.apple.kt index f49e03d..4c8a04b 100644 --- a/Morpho/composeApp/src/appleMain/kotlin/com/morpho/app/ui/profile/DetailedProfileFragment.apple.kt +++ b/Morpho/composeApp/src/appleMain/kotlin/com/morpho/app/ui/profile/DetailedProfileFragment.apple.kt @@ -4,7 +4,9 @@ import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.TopAppBarScrollBehavior import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import com.morpho.app.model.bluesky.BskyLabelService import com.morpho.app.model.bluesky.DetailedProfile +import com.morpho.app.model.uidata.Event @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -15,5 +17,19 @@ actual fun DetailedProfileFragment( isTopLevel: Boolean, scrollBehavior: TopAppBarScrollBehavior, onBackClicked: () -> Unit, + eventCallback: (Event) -> Unit, +) { +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +actual fun LabelerProfileFragment( + labeler: BskyLabelService, + modifier: Modifier, + isSubscribed: Boolean, + isTopLevel: Boolean, + scrollBehavior: TopAppBarScrollBehavior, + onBackClicked: () -> Unit, + eventCallback: (Event) -> Unit, ) { } \ No newline at end of file diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/data/ContentLabelService.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/data/ContentLabelService.kt index 84b93ed..d7c7e37 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/data/ContentLabelService.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/data/ContentLabelService.kt @@ -64,7 +64,7 @@ class ContentLabelService: KoinComponent { init { serviceScope.launch { - agent.getLabelDefinitions(modPrefs) + agent.localizeLabelDefinitions(agent.prefs) agent.getLabelersDetailed(labelers.keys.map { Did(it) }) } @@ -166,31 +166,22 @@ class ContentLabelService: KoinComponent { val possibleCauses = filteredPostLabels.mapNotNull { label -> labelDefinitions[label.creator.did]?.get(label.value)?.let { labelDef -> - val localizedDefString = labelDef.allDescriptions.firstOrNull { - it.lang == agent.myLanguage - } ?: labelDef.allDescriptions.firstOrNull { it.lang.tag == "en" } - val localLabelDef = labelDef.copy( - localizedName = localizedDefString?.name ?: labelDef.localizedName, - localizedDescription = localizedDefString?.description - ?: labelDef.localizedDescription, - ) - LabelCause.Label( LabelSource.Labeler(labelerDetails[label.creator.did]!!), label.toAtProtoLabel(), - localLabelDef, - localLabelDef.whatToHide, + labelDef, + labelDef.whatToHide, labels[label.value] ?: labelDef.defaultSetting ?: Visibility.IGNORE, - localLabelDef.behaviours.content, - noOverride = !localLabelDef.configurable, - priority = when (localLabelDef.severity) { + labelDef.behaviours.content, + noOverride = !labelDef.configurable, + priority = when (labelDef.severity) { Severity.INFORM -> 5 Severity.ALERT -> 2 Severity.NONE -> 8 Severity.WARN -> 1 }, downgraded = false, - ) to localLabelDef.toContentHandling( + ) to labelDef.toContentHandling( LabelTarget.Content, avatar = labelerDetails[label.creator.did]?.creator?.avatar ) diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/data/FeedTuner.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/data/FeedTuner.kt index 12720f0..1968955 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/data/FeedTuner.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/data/FeedTuner.kt @@ -1,16 +1,24 @@ package com.morpho.app.data -import com.morpho.app.model.bluesky.* +import com.morpho.app.model.bluesky.AuthorFilter +import com.morpho.app.model.bluesky.FeedDescriptor +import com.morpho.app.model.bluesky.MorphoDataItem +import com.morpho.app.model.bluesky.Profile +import com.morpho.app.model.bluesky.ThreadPost import com.morpho.app.model.uidata.MorphoData import com.morpho.app.model.uidata.areSameAuthor import com.morpho.app.model.uistate.FeedType -import com.morpho.butterfly.* +import com.morpho.butterfly.AtUri import com.morpho.butterfly.BskyPreferences +import com.morpho.butterfly.Did +import com.morpho.butterfly.Language +import com.morpho.butterfly.PagedResponse import kotlinx.collections.immutable.persistentListOf import kotlinx.serialization.Serializable typealias TunerFunction = (List, FeedTuner) -> List +@Suppress("UNCHECKED_CAST") @Serializable data class FeedTuner(val tuners: List> = persistentListOf()) { val seenKeys = mutableSetOf() @@ -95,23 +103,7 @@ data class FeedTuner(val tuners: List> } if(feed.feedType == FeedType.LIST_FOLLOWING || feed.feedType == FeedType.HOME) { - val userDid = Did(prefs.user.userDid) val tuners = mutableListOf(FeedTuner(tuners = persistentListOf(Companion::removeOrphans))) - val feedPrefs = prefs.preferences.feedViewPrefs[feed.uri.atUri] ?: - return tuners.toList() as List> - if(feedPrefs.hideReposts) tuners.add(FeedTuner(tuners = persistentListOf(Companion::removeReposts))) - if(feedPrefs.hideReplies) tuners.add(FeedTuner(tuners = persistentListOf(Companion::removeReplies))) - else { - val followedRepliesOnly: TunerFunction = { f, t -> - followedRepliesOnly(userDid, f, t) - } - tuners.add(FeedTuner(tuners = persistentListOf(followedRepliesOnly))) - } - if(feedPrefs.hideQuotePosts) tuners.add( - FeedTuner(tuners = persistentListOf( - Companion::removeQuotePosts - )) - ) tuners.add(FeedTuner(tuners = persistentListOf(Companion::dedupThreads))) return tuners.toList() as List> } diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/data/MorphoAgent.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/data/MorphoAgent.kt index 930ffd4..862283f 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/data/MorphoAgent.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/data/MorphoAgent.kt @@ -1,19 +1,148 @@ package com.morpho.app.data +import app.bsky.actor.AdultContentPref +import app.bsky.actor.PreferencesUnion +import app.bsky.labeler.LabelerViewDetailed import com.morpho.app.myLang +import com.morpho.app.util.morphoSerializersModule +import com.morpho.butterfly.BlueskyApi +import com.morpho.butterfly.BskyPreferences import com.morpho.butterfly.ButterflyAgent +import com.morpho.butterfly.InterpretedLabelDefinition +import com.morpho.butterfly.LabelValueID +import com.morpho.butterfly.LabelerID import com.morpho.butterfly.Language +import com.morpho.butterfly.localize +import com.morpho.butterfly.xrpc.XrpcBlueskyApi +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.launch +import org.koin.core.component.inject class MorphoAgent: ButterflyAgent() { - val myLanguage = Language(myLang ?: "en") // TODO: make this configurable + // Splices in the app's serializers module to handle any extensions to the type unions + override var api: BlueskyApi = XrpcBlueskyApi(atpClient, morphoSerializersModule) + val localPrefs: PreferencesRepository by inject() - val morphoPrefs: MorphoPreferences = MorphoPreferences( - kawaiiMode = true - ) + private val _morphoPrefs: MutableStateFlow = MutableStateFlow(MorphoPreferences( + kawaiiMode = true, + notificationsFilter = NotificationsFilterPref(), + accessibility = AccessibilityPreferences(), + )) + private val _bskyPrefs: MutableStateFlow = MutableStateFlow(BskyPreferences()) + private var _myLanguage = MutableStateFlow(Language(myLang ?: _morphoPrefs.value.uiLanguage?.tag ?: "en")) + + val morphoPrefs = _morphoPrefs.asStateFlow() + val bskyPrefs = _bskyPrefs.asStateFlow() + val myLanguage = _myLanguage.asStateFlow() + + val labelersDetailed: Flow> = flow { + val labelers = getLabelersDetailed(labelers).getOrNull() ?: listOf() + emit(labelers) + } + + init { + // Belt and suspenders bc of the super/derived class initialization uncertainty + api = XrpcBlueskyApi(atpClient, morphoSerializersModule) + if(id != null) { + serviceScope.launch { + localPrefs.morphoPrefs(id!!).collectLatest { + if (it != null) { + _morphoPrefs.value = it + } + + } + } + serviceScope.launch { + localPrefs.bskyPrefs(id!!).collectLatest { + if (it != null) { + prefs = it + } + } + } + serviceScope.launch { + localPrefs.bskyPrefs(id!!).collectLatest { + if (it != null) { + _bskyPrefs.value = it + } + } + } + serviceScope.launch { + localPrefs.writePreferences( + BskyUserPreferences( + id!!, + getPreferences().getOrNull() ?: BskyPreferences(), + morphoPrefs.value + ) + ) + } + } + } val kawaiiMode: Boolean - get() = morphoPrefs.kawaiiMode + get() = morphoPrefs.value.kawaiiMode == true + + + fun setAccessibilityPrefs(prefs: AccessibilityPreferences) = serviceScope.launch { + updateMorphoPrefs { + val newPrefs = AccessibilityPreferences.update(it.accessibility ?: AccessibilityPreferences(), prefs) + it.copy(accessibility = newPrefs) + } + } + fun setNotificationsFilterPrefs(prefs: NotificationsFilterPref) = serviceScope.launch { + updateMorphoPrefs { + val newPrefs = NotificationsFilterPref.update(it.notificationsFilter ?: NotificationsFilterPref(), prefs) + it.copy(notificationsFilter = newPrefs) + } + } + + fun setDarkMode(setting: DarkModeSetting = DarkModeSetting.SYSTEM) = serviceScope.launch { + updateMorphoPrefs { + it.copy(darkMode = setting) + } + } + + fun setUILanguage(language: Language) = serviceScope.launch { + _myLanguage.value = language + updateMorphoPrefs { + it.copy(uiLanguage = language) + } + } + suspend fun updateMorphoPrefs( + updateFun: (MorphoPreferences) -> MorphoPreferences? + ): Result { + val prefs = updateFun(morphoPrefs.value) + return if(prefs != null) { + localPrefs.setMorphoPrefs(id!!, prefs) + _morphoPrefs.value = prefs + Result.success(prefs) + } else Result.failure(Exception("Update failed")) + } + + suspend fun localizeLabelDefinitions(prefs: BskyPreferences): Map> { + val labelDefs = getLabelDefinitions(prefs) + return labelDefs.map { labeler -> + labeler.key to labeler.value.map { entry -> + val labelDef = entry.value + + entry.key to labelDef.localize(myLanguage.value) + }.associate { it.first to it.second } + }.associate { it.first to it.second } + } + + fun toggleAdultContent(enabled: Boolean) = serviceScope.launch { + updatePreferences { prefs -> + val newPref = if(enabled) AdultContentPref(true) else AdultContentPref(false) + val updatedPrefs = prefs.filter { it !is PreferencesUnion.AdultContentPref }.plus( + PreferencesUnion.AdultContentPref(newPref) + ) + return@updatePreferences updatedPrefs + } + } } \ No newline at end of file diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/data/PreferencesRepository.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/data/PreferencesRepository.kt index 6a87bcd..9c37c12 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/data/PreferencesRepository.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/data/PreferencesRepository.kt @@ -1,62 +1,197 @@ package com.morpho.app.data -import app.bsky.actor.GetProfileQuery -import app.bsky.actor.PutPreferencesRequest -import com.morpho.app.model.bluesky.BskyPreferences -import com.morpho.app.model.bluesky.BskyUser -import com.morpho.app.model.bluesky.toPreferences -import com.morpho.app.model.bluesky.toProfile import com.morpho.app.model.uistate.NotificationsFilterState -import com.morpho.butterfly.AtIdentifier -import com.morpho.butterfly.Butterfly +import com.morpho.butterfly.BskyPreferences +import com.morpho.butterfly.Did +import com.morpho.butterfly.Language import io.github.xxfast.kstore.KStore import io.github.xxfast.kstore.extensions.updatesOrEmpty import io.github.xxfast.kstore.file.extensions.listStoreOf -import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.distinctUntilChanged -import kotlinx.coroutines.flow.firstOrNull -import kotlinx.coroutines.flow.flow -import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.launch import kotlinx.serialization.Serializable import okio.Path.Companion.toPath import org.koin.core.component.KoinComponent -import org.koin.core.component.inject import org.lighthousegames.logging.logging @Serializable data class BskyUserPreferences( - val user: BskyUser, + val did: Did, val preferences: BskyPreferences, val morphoPrefs: MorphoPreferences, ) @Serializable data class AccessibilityPreferences( - val requireAltText: Boolean = false, - val displayLargerAltBadge: Boolean = false, - val reduceMotion: Boolean = false, - val disableAutoplay: Boolean = false, - val disableHaptics: Boolean = false, -) + val requireAltText: Boolean? = false, + val displayLargerAltBadge: Boolean? = false, + val reduceMotion: Boolean? = false, + val disableAutoplay: Boolean? = false, + val disableHaptics: Boolean? = false, + val simpleUI: Boolean? = false, +) { + companion object { + fun update( + existing: AccessibilityPreferences, + new: AccessibilityPreferences, + ) : AccessibilityPreferences { + return AccessibilityPreferences( + requireAltText = new.requireAltText ?: existing.requireAltText, + displayLargerAltBadge = new.displayLargerAltBadge ?: existing.displayLargerAltBadge, + reduceMotion = new.reduceMotion ?: existing.reduceMotion, + disableAutoplay = new.disableAutoplay ?: existing.disableAutoplay, + disableHaptics = new.disableHaptics ?: existing.disableHaptics, + simpleUI = new.simpleUI ?: existing.simpleUI, + ) + } + fun toUpdate( + requireAltText: Boolean? = null, + displayLargerAltBadge: Boolean? = null, + reduceMotion: Boolean? = null, + disableAutoplay: Boolean? = null, + disableHaptics: Boolean? = null, + simpleUI: Boolean? = null, + ): AccessibilityPreferences { + return AccessibilityPreferences( + requireAltText = requireAltText, + displayLargerAltBadge = displayLargerAltBadge, + reduceMotion = reduceMotion, + disableAutoplay = disableAutoplay, + disableHaptics = disableHaptics, + simpleUI = simpleUI, + ) + } + } +} + +enum class DarkModeSetting { + SYSTEM, + LIGHT, + DARK, +} @Serializable data class MorphoPreferences( - val tabbed: Boolean = true, - val undecorated: Boolean = true, - val kawaiiMode: Boolean = true, - val notificationsFilter: NotificationsFilterState = NotificationsFilterState(), - val accessibility: AccessibilityPreferences = AccessibilityPreferences(), -) + val tabbed: Boolean? = true, + val undecorated: Boolean? = true, + val kawaiiMode: Boolean? = true, + val uiLanguage: Language? = Language("en"), + val darkMode: DarkModeSetting? = DarkModeSetting.SYSTEM, + val notificationsFilter: NotificationsFilterPref? = NotificationsFilterPref(), + val accessibility: AccessibilityPreferences? = AccessibilityPreferences(), +) { + companion object { + fun update( + existing: MorphoPreferences, + new: MorphoPreferences, + ) : MorphoPreferences { + return MorphoPreferences( + tabbed = new.tabbed ?: existing.tabbed, + undecorated = new.undecorated ?: existing.undecorated, + kawaiiMode = new.kawaiiMode ?: existing.kawaiiMode, + notificationsFilter = new.notificationsFilter ?: existing.notificationsFilter, + accessibility = new.accessibility ?: existing.accessibility, + darkMode = new.darkMode ?: existing.darkMode, + uiLanguage = new.uiLanguage ?: existing.uiLanguage, + ) + } + + fun toUpdate( + tabbed: Boolean? = null, + undecorated: Boolean? = null, + kawaiiMode: Boolean? = null, + notificationsFilter: NotificationsFilterPref? = null, + accessibility: AccessibilityPreferences? = null, + darkMode: DarkModeSetting? = null, + uiLanguage: Language? = null, + ): MorphoPreferences { + return MorphoPreferences( + tabbed = tabbed, + undecorated = undecorated, + kawaiiMode = kawaiiMode, + notificationsFilter = notificationsFilter, + accessibility = accessibility, + darkMode = darkMode, + uiLanguage = uiLanguage, + ) + } + } +} + +@Serializable +data class NotificationsFilterPref( + val showAlreadyRead: Boolean? = true, + val showLikes: Boolean? = true, + val showReposts: Boolean? = true, + val showFollows: Boolean? = true, + val showMentions: Boolean? = true, + val showQuotes: Boolean? = true, + val showReplies: Boolean? = true, +) { + companion object { + fun update( + existing: NotificationsFilterPref, + new: NotificationsFilterPref, + ) : NotificationsFilterPref { + return NotificationsFilterPref( + showAlreadyRead = new.showAlreadyRead ?: existing.showAlreadyRead, + showLikes = new.showLikes ?: existing.showLikes, + showReposts = new.showReposts ?: existing.showReposts, + showFollows = new.showFollows ?: existing.showFollows, + showMentions = new.showMentions ?: existing.showMentions, + showQuotes = new.showQuotes ?: existing.showQuotes, + showReplies = new.showReplies ?: existing.showReplies, + ) + } + + fun toUpdate( + showAlreadyRead: Boolean? = null, + showLikes: Boolean? = null, + showReposts: Boolean? = null, + showFollows: Boolean? = null, + showMentions: Boolean? = null, + showQuotes: Boolean? = null, + showReplies: Boolean? = null, + ): NotificationsFilterPref { + return NotificationsFilterPref( + showAlreadyRead = showAlreadyRead, + showLikes = showLikes, + showReposts = showReposts, + showFollows = showFollows, + showMentions = showMentions, + showQuotes = showQuotes, + showReplies = showReplies, + ) + } + } + fun toNotificationsFilterState(): NotificationsFilterState { + return NotificationsFilterState( + showAlreadyRead = showAlreadyRead == true, + showLikes = showLikes == true, + showReposts = showReposts == true, + showFollows = showFollows == true, + showMentions = showMentions == true, + showQuotes = showQuotes == true, + showReplies = showReplies == true, + ) + } +} + class PreferencesRepository(storageDir: String): KoinComponent { - private val api: Butterfly by inject() private val _prefsStore: KStore> = listStoreOf( file = "$storageDir/preferences.json".toPath(), enableCache = true ) + val serviceScope = CoroutineScope(SupervisorJob() + Dispatchers.Default) + companion object { val log = logging() } @@ -64,123 +199,58 @@ class PreferencesRepository(storageDir: String): KoinComponent { val prefs: Flow?> get() = _prefsStore.updatesOrEmpty.distinctUntilChanged() - fun userPrefs(id: AtIdentifier): Flow = flow { - prefs.onEach { preferencesList -> - emit(preferencesList?.firstOrNull { p -> - (p.user.userDid == id.toString()) || (p.user.handle == id.toString()) - }) - } - }.distinctUntilChanged() - - - - //@NativeCoroutines - suspend fun getPreferences(id: AtIdentifier, pullRemote: Boolean = false): Result { - val result: Result = getFullPrefsLocal(id) - val newPrefs = if (result.isSuccess && pullRemote) { - val prefs = result.getOrNull() - if (prefs != null) { - pullPreferences(prefs.preferences) - } else { - pullPreferences(null) - } - } else if(pullRemote) { - pullPreferences(null) - } else { - result.map { it.preferences } - }.onSuccess { - val user = getUser(id).getOrNull() - if(result.isFailure) { - val profile = api.api.getProfile(GetProfileQuery(id)) - .getOrNull()?.toProfile() - if(profile != null) { - setPreferences(BskyUser.makeUser(profile), it) - } - } else { - setPreferences(user!!, it, result.getOrNull()!!.morphoPrefs) - } - } - return newPrefs + fun morphoPrefs(did: Did): Flow = userPrefs(did).map { + it?.morphoPrefs } - suspend fun getFullPrefs( - id: AtIdentifier, remote: Boolean = true - ): Result { - return if (remote) getFullPrefsRemote(id) - else getFullPrefsLocal(id) + fun userPrefs(did: Did): Flow = prefs.map { + it?.firstOrNull { prefs -> prefs.did == did } } - suspend fun getFullPrefsLocal(id: AtIdentifier): Result { - val prefs = prefs.firstOrNull()?.firstOrNull { - (it.user.userDid == id.toString()) || (it.user.handle == id.toString()) - } - return if (prefs != null) { - Result.success(prefs) - } else { - Result.failure(Exception("No preferences found for user $id")) - } + fun bskyPrefs(did: Did): Flow = userPrefs(did).map { + it?.preferences } - suspend fun getFullPrefsRemote(id: AtIdentifier): Result { - val prefs = prefs.firstOrNull()?.firstOrNull { - (it.user.userDid == id.toString()) || (it.user.handle == id.toString()) - } - return if (prefs != null) { - pullPreferences(prefs.preferences).map { - BskyUserPreferences(prefs.user, it, prefs.morphoPrefs) + suspend fun setMorphoPrefs(did: Did, prefs: MorphoPreferences) { + _prefsStore.update { + it?.toMutableList()?.apply { + val prefsIndex = it.indexOfFirst { user -> user.did == did } + if (prefsIndex != -1) { + val currentPrefs = this[prefsIndex] + this[prefsIndex] = currentPrefs.copy(morphoPrefs = prefs) + } else { + add(BskyUserPreferences(did, BskyPreferences(), prefs)) + } } - } else { - Result.failure(Exception("No preferences found for user $id")) - } - } - - suspend fun pullPreferences(p: BskyPreferences?): Result { - return if(p != null) api.api.getPreferences().map { it.toPreferences(p) } - else api.api.getPreferences().map { it.toPreferences() } - } - - - //@NativeCoroutines - suspend fun getPrefsLocal(id: AtIdentifier): Result { - val prefs = prefs.firstOrNull()?.firstOrNull { - (it.user.userDid == id.toString()) || (it.user.handle == id.toString()) - } - return if (prefs != null) { - Result.success(prefs.preferences) - } else { - Result.failure(Exception("No preferences found for user $id")) } } - //@NativeCoroutines - suspend fun getUser(id: AtIdentifier): Result { - val user = prefs.firstOrNull()?.firstOrNull { - (it.user.userDid == id.toString()) || (it.user.handle == id.toString()) - }?.user - return if (user != null) { - Result.success(user) - } else { - Result.failure(Exception("No user found for id $id")) + fun writePreferences(prefs: BskyUserPreferences) { + serviceScope.launch { + _prefsStore.update { + it?.toMutableList()?.apply { + val prefsIndex = it.indexOfFirst { user -> user.did == prefs.did } + if (prefsIndex != -1) { + this[prefsIndex] = prefs + } else { + add(prefs) + } + } + } } } - //@NativeCoroutines - suspend fun setPreferences(user: BskyUser, pref: BskyPreferences, morphoPrefs: MorphoPreferences = MorphoPreferences()) = coroutineScope { + suspend fun setBskyPreferences(did: Did, prefs: BskyPreferences) { _prefsStore.update { it?.toMutableList()?.apply { - val prefsIndex = it.indexOfFirst { user.userDid == user.userDid } + val prefsIndex = it.indexOfFirst { user -> user.did == did } if (prefsIndex != -1) { - this[prefsIndex] = BskyUserPreferences(user, pref, morphoPrefs) + val currentPrefs = this[prefsIndex] + this[prefsIndex] = currentPrefs.copy(preferences = prefs) } else { - add(BskyUserPreferences(user, pref, morphoPrefs)) + add(BskyUserPreferences(did, prefs, MorphoPreferences())) } } } } - - //@NativeCoroutines - suspend fun setPreferencesRemote(user: BskyUser, pref: BskyPreferences, morphoPrefs: MorphoPreferences = MorphoPreferences()) = coroutineScope { - setPreferences(user, pref, morphoPrefs) - api.api.putPreferences(PutPreferencesRequest(pref.toRemotePrefs())) - } } \ No newline at end of file diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/di/AppModule.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/di/AppModule.kt index a34f58e..ac65d48 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/di/AppModule.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/di/AppModule.kt @@ -9,8 +9,6 @@ import com.morpho.app.screens.login.LoginScreenModel import com.morpho.app.screens.main.MainScreenModel import com.morpho.app.screens.main.tabbed.TabbedMainScreenModel import com.morpho.app.util.ClipboardManager -import com.morpho.butterfly.AtpAgent -import com.morpho.butterfly.ButterflyAgent import com.morpho.butterfly.auth.SessionRepository import com.morpho.butterfly.auth.UserRepository import com.morpho.butterfly.auth.UserRepositoryImpl @@ -39,8 +37,8 @@ val storageModule = module { } val dataModule = module { - single { AtpAgent() } - single { ButterflyAgent() } +// single { AtpAgent() } +// single { ButterflyAgent() } single { MorphoAgent() } single { ContentLabelService() } single { PollBlueService() } diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/BskyLabelService.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/BskyLabelService.kt index 1c6d29f..776cc09 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/BskyLabelService.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/BskyLabelService.kt @@ -3,6 +3,7 @@ package com.morpho.app.model.bluesky import androidx.compose.runtime.Immutable import app.bsky.labeler.LabelerView import app.bsky.labeler.LabelerViewDetailed +import com.morpho.app.data.MorphoAgent import com.morpho.app.model.uidata.Moment import com.morpho.app.model.uidata.MomentParceler import com.morpho.app.util.mapImmutable @@ -14,7 +15,6 @@ import dev.icerock.moko.parcelize.Parcelable import dev.icerock.moko.parcelize.Parcelize import dev.icerock.moko.parcelize.TypeParceler import kotlinx.collections.immutable.persistentListOf -import kotlinx.datetime.Clock import kotlinx.serialization.Serializable @Parcelize @@ -23,7 +23,7 @@ import kotlinx.serialization.Serializable open class BskyLabelService( val uri: AtUri, val cid: Cid, - val creator: Profile?, + val creator: DetailedProfile?, val likeCount: Long?, val liked: Boolean, val likeUri: AtUri?, @@ -42,23 +42,37 @@ open class BskyLabelService( get() = creator?.avatar } -public data object BlueskyHardcodedLabeler: BskyLabelService( - uri = AtUri("at://morpho/builtin-labeler"), - cid = Cid("builtin-labeler"), - creator = null, - likeCount = 0, - liked = false, - likeUri = null, - indexedAt = Moment(Clock.System.now()), - policies = persistentListOf(), - labels = persistentListOf(), -) - -fun LabelerViewDetailed.toLabelService(): BskyLabelService { +suspend fun LabelerViewDetailed.toLabelService( + agent: MorphoAgent, +): BskyLabelService { + val fullProfile = agent.getProfile(this.creator.did).getOrNull()?.toProfile() ?: DetailedProfile( + did = this.creator.did, + handle = this.creator.handle, + displayName = this.creator.displayName, + description = this.creator.description, + avatar = this.creator.avatar, + banner = null, + followersCount = 0, + followsCount = 0, + postsCount = 0, + labels = this.creator.labels.map { it.toLabel() }, + indexedAt = this.creator.indexedAt?.let { Moment(it) }, + mutedByMe = false, + following = this.creator.viewer?.following?.let { FollowRecord(it) }, + followedBy = this.creator.viewer?.followedBy?.let { FollowRecord(it) }, + numKnownFollowers = 0, + knownFollowers = persistentListOf(), + associated = this.creator.associated?.toBskyProfileAssociated(), + createdAt = this.creator.createdAt?.let { Moment(it) }, + mutedByList = this.creator.viewer?.mutedByList?.toList(), + block = this.creator.viewer?.blocking?.let { BlockRecord(it) }, + blockedBy = this.creator.viewer?.blockedBy == true, + blockingByList = this.creator.viewer?.blockingByList?.toList(), + ) return BskyLabelService( uri = this.uri, cid = this.cid, - creator = this.creator.toProfile(), + creator = fullProfile, likeCount = this.likeCount, liked = (this.viewer?.like != null), likeUri = this.viewer?.like, @@ -68,11 +82,113 @@ fun LabelerViewDetailed.toLabelService(): BskyLabelService { ) } -fun LabelerView.toLabelService(): BskyLabelService { +suspend fun LabelerView.toLabelService( + agent: MorphoAgent? = null, +): BskyLabelService { + val fullProfile = agent?.getProfile(this.creator.did)?.getOrNull()?.toProfile() ?: DetailedProfile( + did = this.creator.did, + handle = this.creator.handle, + displayName = this.creator.displayName, + description = this.creator.description, + avatar = this.creator.avatar, + banner = null, + followersCount = 0, + followsCount = 0, + postsCount = 0, + labels = this.creator.labels.map { it.toLabel() }, + indexedAt = this.creator.indexedAt?.let { Moment(it) }, + mutedByMe = false, + following = this.creator.viewer?.following?.let { FollowRecord(it) }, + followedBy = this.creator.viewer?.followedBy?.let { FollowRecord(it) }, + numKnownFollowers = 0, + knownFollowers = persistentListOf(), + associated = this.creator.associated?.toBskyProfileAssociated(), + createdAt = this.creator.createdAt?.let { Moment(it) }, + mutedByList = this.creator.viewer?.mutedByList?.toList(), + block = this.creator.viewer?.blocking?.let { BlockRecord(it) }, + blockedBy = this.creator.viewer?.blockedBy == true, + blockingByList = this.creator.viewer?.blockingByList?.toList(), + ) + return BskyLabelService( + uri = this.uri, + cid = this.cid, + creator = fullProfile, + likeCount = this.likeCount, + liked = (this.viewer?.like != null), + likeUri = this.viewer?.like, + indexedAt = Moment(this.indexedAt), + policies = persistentListOf(), + labels = this.labels.mapImmutable { it.toLabel() }, + ) +} + +fun LabelerViewDetailed.toLabelServiceLocal(): BskyLabelService { + val fullProfile = DetailedProfile( + did = this.creator.did, + handle = this.creator.handle, + displayName = this.creator.displayName, + description = this.creator.description, + avatar = this.creator.avatar, + banner = null, + followersCount = 0, + followsCount = 0, + postsCount = 0, + labels = this.creator.labels.map { it.toLabel() }, + indexedAt = this.creator.indexedAt?.let { Moment(it) }, + mutedByMe = false, + following = this.creator.viewer?.following?.let { FollowRecord(it) }, + followedBy = this.creator.viewer?.followedBy?.let { FollowRecord(it) }, + numKnownFollowers = 0, + knownFollowers = persistentListOf(), + associated = this.creator.associated?.toBskyProfileAssociated(), + createdAt = this.creator.createdAt?.let { Moment(it) }, + mutedByList = this.creator.viewer?.mutedByList?.toList(), + block = this.creator.viewer?.blocking?.let { BlockRecord(it) }, + blockedBy = this.creator.viewer?.blockedBy == true, + blockingByList = this.creator.viewer?.blockingByList?.toList(), + ) + return BskyLabelService( + uri = this.uri, + cid = this.cid, + creator = fullProfile, + likeCount = this.likeCount, + liked = (this.viewer?.like != null), + likeUri = this.viewer?.like, + indexedAt = Moment(this.indexedAt), + policies = persistentListOf(), + labels = this.labels.mapImmutable { it.toLabel() }, + ) +} + +fun LabelerView.toLabelServiceLocal(): BskyLabelService { + val fullProfile = DetailedProfile( + did = this.creator.did, + handle = this.creator.handle, + displayName = this.creator.displayName, + description = this.creator.description, + avatar = this.creator.avatar, + banner = null, + followersCount = 0, + followsCount = 0, + postsCount = 0, + labels = this.creator.labels.map { it.toLabel() }, + indexedAt = this.creator.indexedAt?.let { Moment(it) }, + mutedByMe = false, + following = this.creator.viewer?.following?.let { FollowRecord(it) }, + followedBy = this.creator.viewer?.followedBy?.let { FollowRecord(it) }, + numKnownFollowers = 0, + knownFollowers = persistentListOf(), + associated = this.creator.associated?.toBskyProfileAssociated(), + createdAt = this.creator.createdAt?.let { Moment(it) }, + mutedByList = this.creator.viewer?.mutedByList?.toList(), + block = this.creator.viewer?.blocking?.let { BlockRecord(it) }, + blockedBy = this.creator.viewer?.blockedBy == true, + blockingByList = this.creator.viewer?.blockingByList?.toList(), + ) return BskyLabelService( uri = this.uri, cid = this.cid, - creator = this.creator.toProfile(), + creator = fullProfile, likeCount = this.likeCount, liked = (this.viewer?.like != null), likeUri = this.viewer?.like, diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/BskyPostFeature.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/BskyPostFeature.kt index ba86165..de81e01 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/BskyPostFeature.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/BskyPostFeature.kt @@ -1,7 +1,14 @@ package com.morpho.app.model.bluesky import androidx.compose.runtime.Immutable -import app.bsky.embed.* +import app.bsky.embed.AspectRatio +import app.bsky.embed.ExternalView +import app.bsky.embed.ImagesView +import app.bsky.embed.RecordViewRecordUnion +import app.bsky.embed.RecordWithMediaViewMediaUnion +import app.bsky.embed.VideoCaption +import app.bsky.embed.VideoView +import app.bsky.embed.VideoViewVideo import app.bsky.feed.Post import app.bsky.feed.PostEmbedUnion import app.bsky.feed.PostViewEmbedUnion @@ -9,9 +16,17 @@ import com.morpho.app.CommonRawValue import com.morpho.app.model.uidata.Moment import com.morpho.app.model.uidata.MomentParceler import com.morpho.app.util.mapImmutable -import com.morpho.butterfly.* +import com.morpho.butterfly.AtUri +import com.morpho.butterfly.Cid +import com.morpho.butterfly.Did +import com.morpho.butterfly.Uri +import com.morpho.butterfly.deserialize import com.morpho.butterfly.model.Blob -import dev.icerock.moko.parcelize.* +import dev.icerock.moko.parcelize.Parcel +import dev.icerock.moko.parcelize.Parcelable +import dev.icerock.moko.parcelize.Parceler +import dev.icerock.moko.parcelize.Parcelize +import dev.icerock.moko.parcelize.TypeParceler import kotlinx.serialization.Serializable import kotlinx.serialization.json.JsonElement @@ -329,7 +344,7 @@ private fun RecordViewRecordUnion.toEmbedRecord(): EmbedRecord { uri = value.uri, cid = value.cid, author = value.creator.toProfile(), - labelService = value.toLabelService(), + labelService = value.toLabelServiceLocal(), ) } diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/BskyPreferences.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/BskyPreferences.kt index f07e3e4..5faf1d8 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/BskyPreferences.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/BskyPreferences.kt @@ -2,163 +2,3 @@ package com.morpho.app.model.bluesky //import com.rickclephas.kmp.nativecoroutines.NativeCoroutines -import androidx.compose.runtime.Immutable -import app.bsky.actor.* -import com.morpho.app.util.mapImmutable -import com.morpho.butterfly.AtUri -import com.morpho.butterfly.Did -import com.morpho.butterfly.Language -import com.morpho.butterfly.model.ReadOnlyList -import kotlinx.collections.immutable.persistentListOf -import kotlinx.collections.immutable.toImmutableList -import kotlinx.collections.immutable.toPersistentList -import kotlinx.serialization.Serializable - -@Serializable -public data class BskyPreferences( - public var personalDetails: PersonalDetailsPref? = null, - public var adultContent: AdultContentPref? = null, - public val feedViewPrefs: MutableMap = mutableMapOf(), - public var skyFeedBuilderFeeds: SkyFeedBuilderFeedsPref? = null, - public var savedFeeds: SavedFeedsPrefV2? = null, - public val contentLabelPrefs: MutableList = mutableListOf(), - public var threadViewPrefs: ThreadViewPref? = null, - // Get system languages and allow customization of this - public var languages: List = listOf(), - public var mergeFeeds: Boolean = false, - public val mutes: List = listOf(), - public val listsMuted: MutableMap = mutableMapOf(), - public var mutedWords: List = listOf(), - public var hiddenPosts: List = listOf(), - public var labelers: List = listOf(), -) { - fun toRemotePrefs(): ReadOnlyList { - val prefs = persistentListOf() - if (this.adultContent != null) prefs.add(PreferencesUnion.AdultContentPref(this.adultContent!!)) - if (this.personalDetails != null) prefs.add(PreferencesUnion.PersonalDetailsPref(this.personalDetails!!)) - if (this.savedFeeds != null) prefs.add(PreferencesUnion.SavedFeedsPrefV2(this.savedFeeds!!)) - if (this.skyFeedBuilderFeeds != null) prefs.add( - PreferencesUnion.SkyFeedBuilderFeedsPref(this.skyFeedBuilderFeeds!!)) - if (this.threadViewPrefs != null) prefs.add(PreferencesUnion.ThreadViewPref(this.threadViewPrefs!!)) - if (this.feedViewPrefs.isNotEmpty()) this.feedViewPrefs.map { PreferencesUnion.FeedViewPref( - FeedViewPref( - it.key, - it.value.hideReplies, - it.value.hideRepliesByUnfollowed, - it.value.hideRepliesByLikeCount, - it.value.hideReposts, - it.value.hideQuotePosts, - if(it.key == "home") this.mergeFeeds else null, - )) } - if (this.contentLabelPrefs.isNotEmpty()) this.contentLabelPrefs.map { - prefs.add(PreferencesUnion.ContentLabelPref(it)) } - if (this.mutedWords.isNotEmpty()) prefs.add( - PreferencesUnion.MutedWordsPref(MutedWordsPref(this.mutedWords.toImmutableList()))) - if (this.labelers.isNotEmpty()) prefs.add( - PreferencesUnion.LabelersPref( - LabelersPref(this.labelers.toImmutableList().mapImmutable { LabelerPrefItem(it) }))) - return prefs.toImmutableList() - } -} - - -@Immutable -@Serializable -data class BskyUser( - val userDid: String, - val handle: String, - val displayName: String?, - val avatar: String?, - val profile: SerializableProfile, -) { - companion object{ - fun makeUser(profile: DetailedProfile): BskyUser { - return BskyUser( - profile.did.did, - profile.handle.handle, - profile.displayName, - profile.avatar, - profile.toSerializableProfile(), - ) - } - } - - fun getProfile(): DetailedProfile { - return profile.toProfile() - } -} - - -@Serializable -public data class BskyFeedPref( - /** - * Hide replies in the feed. - */ - public var hideReplies: Boolean = false, - /** - * Hide replies in the feed if they are not by followed users. - */ - public var hideRepliesByUnfollowed: Boolean = false, - /** - * Hide replies in the feed if they do not have this number of likes. - */ - public var hideRepliesByLikeCount: Long = 2, - /** - * Hide reposts in the feed. - */ - public var hideReposts: Boolean = false, - /** - * Hide quote posts in the feed. - */ - public var hideQuotePosts: Boolean = false, - - // Can be per feed, maybe add "warn" to this as well - public var labelsToHide: List = persistentListOf(), - public var languages: List = persistentListOf(), - public var hidePostsByMuted: Boolean = false -) - -fun GetPreferencesResponse.toPreferences(prefs: BskyPreferences) : BskyPreferences { - preferences.map { pref:PreferencesUnion -> - when(pref) { - is PreferencesUnion.AdultContentPref -> prefs.adultContent = pref.value - is PreferencesUnion.ContentLabelPref -> prefs.contentLabelPrefs.add(pref.value) - is PreferencesUnion.FeedViewPref -> { - if (pref.value.lab_mergeFeedEnabled != null) prefs.mergeFeeds = pref.value.lab_mergeFeedEnabled!! - val labelsToHide = prefs.feedViewPrefs[pref.value.feed]?.labelsToHide - val languages = prefs.feedViewPrefs[pref.value.feed]?.languages - prefs.feedViewPrefs[pref.value.feed] = BskyFeedPref( - hideReplies = pref.value.hideReplies == true, - hideQuotePosts = pref.value.hideQuotePosts == true, - hideReposts = pref.value.hideReposts == true, - hideRepliesByUnfollowed = pref.value.hideRepliesByUnfollowed == true, - hideRepliesByLikeCount = pref.value.hideRepliesByLikeCount?: 0, - ) - if(!labelsToHide.isNullOrEmpty()) prefs.feedViewPrefs[pref.value.feed]?.labelsToHide = labelsToHide - if(!languages.isNullOrEmpty()) prefs.feedViewPrefs[pref.value.feed]?.languages = languages else persistentListOf() - } - is PreferencesUnion.PersonalDetailsPref -> prefs.personalDetails = pref.value - is PreferencesUnion.SavedFeedsPrefV2 -> prefs.savedFeeds = pref.value - is PreferencesUnion.SkyFeedBuilderFeedsPref -> prefs.skyFeedBuilderFeeds = pref.value - is PreferencesUnion.ThreadViewPref -> prefs.threadViewPrefs = pref.value - is PreferencesUnion.HiddenPostsPref -> prefs.hiddenPosts = pref.value.items.toPersistentList() - is PreferencesUnion.LabelersPref -> prefs.labelers = pref.value.labelers.map { it.did }.toPersistentList() - is PreferencesUnion.InterestsPref -> {} - is PreferencesUnion.MutedWordsPref -> prefs.mutedWords = pref.value.items.toPersistentList() - else -> {} - } - } - return prefs -} - -fun GetPreferencesResponse.toPreferences() : BskyPreferences { - val prefs = this.toPreferences(BskyPreferences()) - prefs.feedViewPrefs.map { feed -> - prefs.contentLabelPrefs.map { label -> - if (label.visibility == Visibility.HIDE) feed.value.labelsToHide + label - } - feed.value.languages = prefs.languages - } - return prefs -} - diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/ContentLabelService.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/ContentLabelService.kt index c60807b..fee32b5 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/ContentLabelService.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/ContentLabelService.kt @@ -171,7 +171,7 @@ class ContentLabelService: KoinComponent { val possibleCauses = filteredPostLabels.mapNotNull { label -> labelDefinitions[label.creator.did]?.get(label.value)?.let { labelDef -> val localizedDefString = labelDef.allDescriptions.firstOrNull { - it.lang == agent.myLanguage + it.lang == agent.myLanguage.value } ?: labelDef.allDescriptions.firstOrNull { it.lang.tag == "en" } val localLabelDef = labelDef.copy( localizedName = localizedDefString?.name ?: labelDef.localizedName, diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/FeedPresenter.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/FeedPresenter.kt index 2e29392..23015eb 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/FeedPresenter.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/FeedPresenter.kt @@ -102,6 +102,7 @@ fun getLikesDataSource( return MorphoFeedSource(request, tuners, repliesBumpThreads = true) } +@Suppress("UNCHECKED_CAST") fun getAuthorFeedDataSource( descriptor: FeedDescriptor.Author, agent: ButterflyAgent diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/ProfilePresenters.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/ProfilePresenters.kt index e31ce3b..4e0f022 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/ProfilePresenters.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/ProfilePresenters.kt @@ -78,7 +78,7 @@ class MyProfilePresenter( val hasLists = agent.api .getLists(GetListsQuery(id, 1, null)).getOrNull()?.lists?.isNotEmpty() ?: false val maybeLabeler = agent.getLabelers(listOf(profile.did)) - .getOrNull()?.firstOrNull()?.toLabelService() + .getOrNull()?.firstOrNull()?.toLabelService(agent) return ContentCardState.MyProfile( profile = profile, @@ -175,7 +175,7 @@ class ProfilePresenter( val hasLists = agent.api .getLists(GetListsQuery(actor, 1, null)).getOrNull()?.lists?.isNotEmpty() ?: false val maybeLabeler = agent.getLabelers(listOf(profile.did)) - .getOrNull()?.firstOrNull()?.toLabelService() + .getOrNull()?.firstOrNull()?.toLabelService(agent) return ContentCardState.FullProfile( profile = profile, diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/main/tabbed/TabbedMainScreenModel.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/main/tabbed/TabbedMainScreenModel.kt index 6a0329a..49e553a 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/main/tabbed/TabbedMainScreenModel.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/main/tabbed/TabbedMainScreenModel.kt @@ -24,7 +24,8 @@ class TabbedMainScreenModel : MainScreenModel() { val tabs = mutableStateListOf() - val timelineIndex = 1 + val timelineIndex: Int + get() = agent.prefs.timelineIndex ?: 0 var loaded by mutableStateOf(false) diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/common/TabbedSkylineFragment.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/common/TabbedSkylineFragment.kt index 98f1df0..22d7e1c 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/common/TabbedSkylineFragment.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/common/TabbedSkylineFragment.kt @@ -16,6 +16,7 @@ import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.currentOrThrow import cafe.adriel.voyager.navigator.tab.TabNavigator import com.atproto.repo.StrongRef +import com.morpho.app.data.MorphoAgent import com.morpho.app.model.bluesky.BskyPost import com.morpho.app.model.bluesky.DraftPost import com.morpho.app.model.uidata.Event @@ -24,7 +25,6 @@ import com.morpho.app.screens.base.tabbed.ProfileTab import com.morpho.app.screens.base.tabbed.ThreadTab import com.morpho.app.ui.elements.doMenuOperation import com.morpho.app.util.ClipboardManager -import com.morpho.butterfly.ButterflyAgent import com.morpho.butterfly.ContentHandling import io.ktor.util.reflect.instanceOf import kotlinx.coroutines.Dispatchers @@ -43,7 +43,7 @@ fun TabbedSkylineFragment( eventCallback: (Event) -> Unit = {}, getContentHandling: (BskyPost) -> List = { listOf() }, ) { - val agent = getKoin().get() + val agent = getKoin().get() val uiState = uiUpdate.collectAsState(initial = UIUpdate.Empty) val navigator = if (LocalNavigator.current?.parent?.instanceOf(TabNavigator::class) == true) { LocalNavigator.currentOrThrow diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/elements/SettingsItems.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/elements/SettingsItems.kt index 8305999..507a99f 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/elements/SettingsItems.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/elements/SettingsItems.kt @@ -1,7 +1,16 @@ package com.morpho.app.ui.elements -import androidx.compose.foundation.layout.* -import androidx.compose.material3.* +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.ElevatedCard +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.material3.surfaceColorAtElevation import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -47,59 +56,64 @@ fun SettingsGroup( fun ColumnScope.SettingsItem( text: AnnotatedString? = null, description: AnnotatedString? = null, - modifier: Modifier = Modifier, + modifier: Modifier = Modifier.padding(vertical = 8.dp), content: @Composable (Modifier) -> Unit, ){ - if(text != null && description == null) { - Column( - modifier = modifier, - verticalArrangement = Arrangement.Center, - horizontalAlignment = Alignment.Start - ) { - Text( - text = text, - modifier = Modifier - .padding(12.dp) - .align(Alignment.Start) - ) - content(Modifier.padding(start = 12.dp, end = 12.dp)) - } - } else { - Row( - modifier = modifier, - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.SpaceBetween - ) { - if(description != null && text != null) { - Column( + Surface( + modifier = modifier, + shape = MaterialTheme.shapes.small, + color = MaterialTheme.colorScheme.surfaceColorAtElevation(2.dp), + tonalElevation = 2.dp, + ) { + if(text != null && description == null) { + Column( + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.Start + ) { + Text( + text = text, modifier = Modifier - .padding(horizontal = 12.dp) - ) { - Text( - text = text, - style = MaterialTheme.typography.labelSmall, + .padding(12.dp) + .align(Alignment.Start) + ) + content(Modifier.padding(start = 12.dp, end = 12.dp)) + } + } else { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween + ) { + if(description != null && text != null) { + Column( modifier = Modifier - .padding(12.dp) - .align(Alignment.Start) - ) + .padding(horizontal = 12.dp) + ) { + Text( + text = text, + style = MaterialTheme.typography.labelSmall, + modifier = Modifier + .padding(12.dp) + .align(Alignment.Start) + ) + Text( + text = description, + style = MaterialTheme.typography.bodySmall, + modifier = Modifier + .padding(12.dp) + .align(Alignment.Start) + ) + } + content(Modifier.padding(horizontal = 12.dp)) + } else { + content(Modifier.padding(horizontal = 12.dp)) Text( - text = description, - style = MaterialTheme.typography.bodySmall, + text = description?: AnnotatedString(""), modifier = Modifier .padding(12.dp) - .align(Alignment.Start) ) } - content(Modifier.padding(horizontal = 12.dp)) - } else { - content(Modifier.padding(horizontal = 12.dp)) - Text( - text = description?: AnnotatedString(""), - modifier = Modifier - .padding(12.dp) - ) - } + } } } diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/profile/DetailedProfileFragment.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/profile/DetailedProfileFragment.kt index 5f01233..af8d223 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/profile/DetailedProfileFragment.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/profile/DetailedProfileFragment.kt @@ -2,13 +2,28 @@ package com.morpho.app.ui.profile import androidx.compose.foundation.layout.ExperimentalLayoutApi +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.ExtendedFloatingActionButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.TopAppBarScrollBehavior import androidx.compose.material3.rememberTopAppBarState import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.morpho.app.model.bluesky.BskyLabelService import com.morpho.app.model.bluesky.DetailedProfile +import com.morpho.app.model.uidata.Event import org.jetbrains.compose.resources.ExperimentalResourceApi @@ -26,4 +41,59 @@ expect fun DetailedProfileFragment( scrollBehavior: TopAppBarScrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior( rememberTopAppBarState()), onBackClicked: () -> Unit = {}, -) \ No newline at end of file + eventCallback: (Event) -> Unit = {}, +) + +@OptIn( + ExperimentalMaterial3Api::class, + ExperimentalLayoutApi::class, + ExperimentalResourceApi::class +) +@Composable +expect fun LabelerProfileFragment( + labeler: BskyLabelService, + modifier: Modifier = Modifier, + isSubscribed: Boolean, + isTopLevel:Boolean = false, + scrollBehavior: TopAppBarScrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior( + rememberTopAppBarState()), + onBackClicked: () -> Unit, + eventCallback: (Event) -> Unit, +) + +@Composable +fun LabelerButtons( + modifier: Modifier = Modifier, + subscribed: Boolean = false, + onSubscribeClicked: () -> Unit = {}, + onUnsubscribeClicked: () -> Unit = {}, + onMenuClicked: () -> Unit = {}, +) { + var isSubscribed by remember { mutableStateOf(subscribed) } + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = modifier + .padding(horizontal = 2.dp) + ) { + ExtendedFloatingActionButton( + text = { + Text( + text = if(isSubscribed) "Unsubscribe" else "Subscribe", + style = MaterialTheme.typography.labelLarge, + fontSize = MaterialTheme.typography.labelLarge + .fontSize.times(0.9) + ) + }, + icon = { + }, + onClick = { + if(isSubscribed) onUnsubscribeClicked() else onSubscribeClicked() + isSubscribed = !isSubscribed + }, + shape = ButtonDefaults.filledTonalShape, + modifier = modifier + .heightIn(min = 30.dp, max = 48.dp) + ) + ProfileMenuButton(onClick = onMenuClicked) + } +} \ No newline at end of file diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/settings/AccessibilitySettings.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/settings/AccessibilitySettings.kt index d9f3f42..5138f66 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/settings/AccessibilitySettings.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/settings/AccessibilitySettings.kt @@ -8,16 +8,19 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.text.AnnotatedString +import com.morpho.app.data.AccessibilityPreferences import com.morpho.app.data.MorphoAgent import com.morpho.app.ui.elements.SettingsGroup import com.morpho.app.ui.elements.SettingsItem +import org.koin.compose.getKoin @Composable fun AccessibilitySettings( - agent: MorphoAgent, + agent: MorphoAgent = getKoin().get(), distinguish: Boolean = true, modifier: Modifier = Modifier, ) { + val morphoPrefs = agent.morphoPrefs.value SettingsGroup( title = "Accessibility", modifier = modifier, @@ -29,30 +32,32 @@ fun AccessibilitySettings( ) { SettingsItem( text = AnnotatedString("Require Alt Text")) { var requireAltText by remember { - mutableStateOf(false) - /// TODO: Get preferences + mutableStateOf(morphoPrefs.accessibility?.requireAltText ?: false) } Switch( checked = requireAltText, onCheckedChange = { requireAltText = it - /// TODO: Update preferences + agent.setAccessibilityPrefs( + AccessibilityPreferences.toUpdate(requireAltText = requireAltText) + ) } ) } SettingsItem( text = AnnotatedString("Display larger alt text")) { var showLargerAltText by remember { - mutableStateOf(false) - /// TODO: Get preferences + mutableStateOf(morphoPrefs.accessibility?.displayLargerAltBadge ?: false) } Switch( checked = showLargerAltText, onCheckedChange = { showLargerAltText = it - /// TODO: Update preferences + agent.setAccessibilityPrefs( + AccessibilityPreferences.toUpdate(displayLargerAltBadge = showLargerAltText) + ) } ) } @@ -64,51 +69,53 @@ fun AccessibilitySettings( ) { SettingsItem( text = AnnotatedString("Disable autoplay for media")) { var disableAutoplay by remember { - mutableStateOf(false) - /// TODO: Get preferences + mutableStateOf(morphoPrefs.accessibility?.disableAutoplay ?: false) } Switch( checked = disableAutoplay, onCheckedChange = { disableAutoplay = it - /// TODO: Update preferences + agent.setAccessibilityPrefs( + AccessibilityPreferences.toUpdate(disableAutoplay = disableAutoplay) + ) } ) } SettingsItem( text = AnnotatedString("Reduce/remove animations")) { var reduceMotion by remember { - mutableStateOf(false) - /// TODO: Get preferences + mutableStateOf(morphoPrefs.accessibility?.reduceMotion ?: false) } Switch( checked = reduceMotion, onCheckedChange = { reduceMotion = it - /// TODO: Update preferences + agent.setAccessibilityPrefs( + AccessibilityPreferences.toUpdate(reduceMotion = reduceMotion) + ) } ) } SettingsItem( text = AnnotatedString("Disable haptic feedback")) { var disableHaptics by remember { - mutableStateOf(false) - /// TODO: Get preferences + mutableStateOf(morphoPrefs.accessibility?.disableHaptics ?: false) } Switch( checked = disableHaptics, onCheckedChange = { disableHaptics = it - /// TODO: Update preferences + agent.setAccessibilityPrefs( + AccessibilityPreferences.toUpdate(disableHaptics = disableHaptics) + ) } ) } SettingsItem( text = AnnotatedString("Simplify UI")) { var simpleUI by remember { - mutableStateOf(false) - /// TODO: Get preferences + mutableStateOf(morphoPrefs.accessibility?.simpleUI ?: false) } Switch( @@ -116,7 +123,9 @@ fun AccessibilitySettings( checked = simpleUI, onCheckedChange = { simpleUI = it - /// TODO: Update preferences + agent.setAccessibilityPrefs( + AccessibilityPreferences.toUpdate(simpleUI = simpleUI) + ) } ) } diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/settings/AdditionalLabelerSettings.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/settings/AdditionalLabelerSettings.kt new file mode 100644 index 0000000..f06aee9 --- /dev/null +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/settings/AdditionalLabelerSettings.kt @@ -0,0 +1,106 @@ +package com.morpho.app.ui.settings + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ChevronRight +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import app.bsky.labeler.LabelerViewDetailed +import cafe.adriel.voyager.navigator.LocalNavigator +import cafe.adriel.voyager.navigator.Navigator +import cafe.adriel.voyager.navigator.currentOrThrow +import com.morpho.app.data.MorphoAgent +import com.morpho.app.model.bluesky.toLabelService +import com.morpho.app.ui.elements.AvatarShape +import com.morpho.app.ui.elements.OutlinedAvatar +import com.morpho.app.ui.elements.SettingsGroup +import kotlinx.coroutines.launch +import org.koin.compose.getKoin + +@Composable +fun AdditionalLabelerSettings( + agent: MorphoAgent = getKoin().get(), + modifier: Modifier = Modifier, + distinguish: Boolean = true, + navigator: Navigator = LocalNavigator.currentOrThrow, +) { + val labelers by agent.labelersDetailed.collectAsState(initial = listOf()) + val scope = rememberCoroutineScope() + val onLabelerClicked: (LabelerViewDetailed) -> Unit = { labeler -> + //TODO: open labeler + scope.launch { + val labelerProfile = labeler.toLabelService(agent) + } + } + SettingsGroup( + title = "Advanced labeler settings", + modifier = modifier, + distinguish = distinguish, + ) { + labelers.forEach { labeler -> + LabelerLink( + labeler = labeler, + onClick = { onLabelerClicked(labeler) }, + modifier = modifier + ) + } + + } +} + +@Composable +fun LabelerLink( + labeler: LabelerViewDetailed, + modifier: Modifier = Modifier, + onClick: () -> Unit = {}, +) { + Surface( + modifier = modifier.padding(6.dp), + shape = MaterialTheme.shapes.medium, + tonalElevation = 2.dp, + ) { + Row( + modifier = modifier.clickable(onClick = onClick), + ) { + OutlinedAvatar( + url = labeler.creator.avatar.orEmpty(), + contentDescription = "Avatar for ${labeler.creator.displayName.orEmpty()}", + size = 50.dp, + avatarShape = AvatarShape.Rounded, + modifier = modifier + .padding(6.dp) + ) + Column { + Text( + text = labeler.creator.displayName.orEmpty(), + style = MaterialTheme.typography.headlineSmall, + modifier = modifier + .padding(6.dp) + ) + Text( + text = labeler.creator.handle.handle, + style = MaterialTheme.typography.bodySmall, + modifier = modifier + .padding(6.dp) + ) + } + Icon( + imageVector = Icons.Default.ChevronRight, + contentDescription = "Open Labeler", + modifier = modifier + .padding(6.dp) + ) + } + } +} \ No newline at end of file diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/settings/AppearanceSettings.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/settings/AppearanceSettings.kt new file mode 100644 index 0000000..ab6e585 --- /dev/null +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/settings/AppearanceSettings.kt @@ -0,0 +1,100 @@ +package com.morpho.app.ui.settings + +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.SegmentedButton +import androidx.compose.material3.SingleChoiceSegmentedButtonRow +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.AnnotatedString +import com.morpho.app.data.DarkModeSetting +import com.morpho.app.data.MorphoAgent +import com.morpho.app.ui.elements.SettingsGroup +import com.morpho.app.ui.elements.SettingsItem +import org.koin.compose.getKoin + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun AppearanceSettings( + agent: MorphoAgent = getKoin().get(), + modifier: Modifier = Modifier, + distinguish: Boolean = true, +) { + val morphoPrefs = agent.morphoPrefs.value + SettingsGroup( + title = "Appearance", + modifier = modifier, + distinguish = distinguish, + ) { + SettingsItem( text = AnnotatedString("Mode")) { + var darkMode by remember { + mutableStateOf(morphoPrefs.darkMode ?: DarkModeSetting.SYSTEM) + } + SingleChoiceSegmentedButtonRow { + SegmentedButton( + selected = darkMode == DarkModeSetting.SYSTEM, + onClick = { + darkMode = DarkModeSetting.SYSTEM + agent.setDarkMode(DarkModeSetting.SYSTEM) + }, + shape = MaterialTheme.shapes.small, + label = { Text("System") }, + ) + SegmentedButton( + selected = darkMode == DarkModeSetting.LIGHT, + onClick = { + darkMode = DarkModeSetting.LIGHT + agent.setDarkMode(DarkModeSetting.LIGHT) + }, + shape = MaterialTheme.shapes.small, + label = { Text("Light") }, + ) + SegmentedButton( + selected = darkMode == DarkModeSetting.DARK, + onClick = { + darkMode = DarkModeSetting.DARK + agent.setDarkMode(DarkModeSetting.DARK) + }, + shape = MaterialTheme.shapes.small, + label = { Text("Dark") }, + ) + + } + } + + SettingsItem(text = AnnotatedString("Interface Style")) { + var tabbed by remember { + mutableStateOf(morphoPrefs.tabbed ?: true) + } + SingleChoiceSegmentedButtonRow { + SegmentedButton( + selected = tabbed, + enabled = false, + onClick = { + tabbed = true + // TODO: come back when the non-tabbed view is ready + agent.setDarkMode(DarkModeSetting.DARK) + }, + shape = MaterialTheme.shapes.small, + label = { Text("System") }, + ) + SegmentedButton( + selected = !tabbed, + enabled = false, + onClick = { + tabbed = false + // TODO: come back when the non-tabbed view is ready + agent.setDarkMode(DarkModeSetting.LIGHT) + }, + shape = MaterialTheme.shapes.small, + label = { Text("Light") }, + ) + } + } + } +} \ No newline at end of file diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/settings/BuiltinContentFilters.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/settings/BuiltinContentFilters.kt new file mode 100644 index 0000000..7dfcd88 --- /dev/null +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/settings/BuiltinContentFilters.kt @@ -0,0 +1,164 @@ +package com.morpho.app.ui.settings + +import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.SegmentedButton +import androidx.compose.material3.SingleChoiceSegmentedButtonRow +import androidx.compose.material3.Switch +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.AnnotatedString +import app.bsky.actor.Visibility +import com.morpho.app.data.MorphoAgent +import com.morpho.app.ui.elements.SettingsGroup +import com.morpho.app.ui.elements.SettingsItem +import com.morpho.butterfly.InterpretedLabelDefinition +import com.morpho.butterfly.localize +import org.koin.compose.getKoin + +@Composable +fun BuiltinContentFilters( + agent: MorphoAgent = getKoin().get(), + modifier: Modifier = Modifier, + distinguish: Boolean = true, +) { + var adultContentEnabled by remember { + mutableStateOf(agent.prefs.modPrefs.adultContentEnabled) + } + + var modPrefs by remember { + mutableStateOf(agent.prefs.modPrefs) + } + + SettingsGroup( + title = "Content filters", + modifier = modifier, + distinguish = distinguish, + ) { + + SettingsItem( + text = AnnotatedString("Enable adult content") + ) { + + Switch( + checked = adultContentEnabled, + onCheckedChange = { + adultContentEnabled = it + agent.toggleAdultContent(it) + } + ) + } + + if(adultContentEnabled) { + BuiltinContentFilterSelector( + labelDefinition = com.morpho.butterfly.Porn.localize(agent.myLanguage.value), + initialFilter = modPrefs.labels[com.morpho.butterfly.Porn.identifier] ?: + com.morpho.butterfly.Porn.defaultSetting, + onSelected = { visibility -> + modPrefs = modPrefs.copy( + labels = modPrefs.labels.toMutableMap().apply { + this[com.morpho.butterfly.Porn.identifier] = visibility + } + ) + agent.setContentLabelPref(com.morpho.butterfly.Porn.identifier, visibility) + } + ) + BuiltinContentFilterSelector( + labelDefinition = com.morpho.butterfly.Sexual.localize(agent.myLanguage.value), + initialFilter = modPrefs.labels[com.morpho.butterfly.Sexual.identifier] ?: + com.morpho.butterfly.Sexual.defaultSetting, + onSelected = { visibility -> + modPrefs = modPrefs.copy( + labels = modPrefs.labels.toMutableMap().apply { + this[com.morpho.butterfly.Sexual.identifier] = visibility + } + ) + agent.setContentLabelPref(com.morpho.butterfly.Sexual.identifier, visibility) + } + ) + BuiltinContentFilterSelector( + labelDefinition = com.morpho.butterfly.GraphicMedia.localize(agent.myLanguage.value), + initialFilter = modPrefs.labels[com.morpho.butterfly.GraphicMedia.identifier] ?: + com.morpho.butterfly.GraphicMedia.defaultSetting, + onSelected = { visibility -> + modPrefs = modPrefs.copy( + labels = modPrefs.labels.toMutableMap().apply { + this[com.morpho.butterfly.GraphicMedia.identifier] = visibility + } + ) + agent.setContentLabelPref(com.morpho.butterfly.GraphicMedia.identifier, visibility) + } + ) + } + BuiltinContentFilterSelector( + labelDefinition = com.morpho.butterfly.Nudity.localize(agent.myLanguage.value), + initialFilter = modPrefs.labels[com.morpho.butterfly.Nudity.identifier] ?: + com.morpho.butterfly.Nudity.defaultSetting, + onSelected = { visibility -> + modPrefs = modPrefs.copy( + labels = modPrefs.labels.toMutableMap().apply { + this[com.morpho.butterfly.Nudity.identifier] = visibility + } + ) + agent.setContentLabelPref(com.morpho.butterfly.Nudity.identifier, visibility) + } + ) + + } +} + + + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun ColumnScope.BuiltinContentFilterSelector( + + labelDefinition: InterpretedLabelDefinition, + initialFilter: Visibility, + onSelected: (Visibility) -> Unit, + modifier: Modifier = Modifier, +) { + var setting by remember { mutableStateOf(initialFilter) } + + SettingsItem( + text = AnnotatedString(labelDefinition.localizedName), + description = AnnotatedString(labelDefinition.localizedDescription), + modifier = modifier + ) { + SingleChoiceSegmentedButtonRow { + SegmentedButton( + selected = setting == Visibility.SHOW || setting == Visibility.IGNORE, + onClick = { + setting = Visibility.SHOW + onSelected(Visibility.SHOW) + }, + shape = MaterialTheme.shapes.small, + label = { Text(text = "Show") } + ) + SegmentedButton( + selected = setting == Visibility.WARN, + onClick = { + setting = Visibility.WARN + onSelected(Visibility.WARN) + }, + shape = MaterialTheme.shapes.small, + label = { Text(text = "Warn") } + ) + SegmentedButton( + selected = setting == Visibility.HIDE, + onClick = { + setting = Visibility.HIDE + onSelected(Visibility.HIDE) + }, + shape = MaterialTheme.shapes.small, + label = { Text(text = "Hide") } + ) + } + } +} \ No newline at end of file diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/settings/FeedPreferences.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/settings/FeedPreferences.kt new file mode 100644 index 0000000..f0bcf2b --- /dev/null +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/settings/FeedPreferences.kt @@ -0,0 +1,161 @@ +package com.morpho.app.ui.settings + +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Switch +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.unit.dp +import app.bsky.actor.FeedViewPref +import com.morpho.app.data.MorphoAgent +import com.morpho.app.ui.elements.SettingsGroup +import com.morpho.app.ui.elements.SettingsItem +import org.koin.compose.getKoin + +@Composable +fun FeedPreferences( + agent: MorphoAgent = getKoin().get(), + modifier: Modifier = Modifier, + distinguish: Boolean = true, +) { + val feedPrefs = agent.prefs.feedView ?: FeedViewPref( + feed = "following", + hideReplies = false, + hideRepliesByUnfollowed = true, + hideRepliesByLikeCount = 0, + hideReposts = false, + hideQuotePosts = false, + lab_mergeFeedEnabled = true, + ) + SettingsGroup( + title = "Following Feed Preferences", + modifier = modifier, + distinguish = distinguish, + ) { + SettingsItem( + text = AnnotatedString("Show replies"), + description = AnnotatedString("Show any replies in the following feed at all?"), + ) { + var showReplies by mutableStateOf(feedPrefs.hideReplies != true) + Row { + Switch( + checked = showReplies, + onCheckedChange = { + showReplies = it + agent.setFeedViewPrefs( + feed = "following", + feedViewPref = feedPrefs.copy(hideReplies = !showReplies) + ) + } + ) + Text( + text = if(showReplies) "Show" else "Hide", + style = MaterialTheme.typography.bodyMedium, + modifier = Modifier.padding(start = 8.dp) + ) + } + } + if(feedPrefs.hideReplies != true) { + SettingsItem( + text = AnnotatedString("Show replies by unfollowed"), + description = AnnotatedString("Show replies by people you don't follow, but who are replying to people you do follow?"), + ) { + var showRepliesByUnfollowed by mutableStateOf(feedPrefs.hideRepliesByUnfollowed != true) + Row { + Switch( + checked = showRepliesByUnfollowed, + onCheckedChange = { + showRepliesByUnfollowed = it + agent.setFeedViewPrefs( + feed = "following", + feedViewPref = feedPrefs.copy(hideRepliesByUnfollowed = !showRepliesByUnfollowed) + ) + } + ) + Text( + text = if(showRepliesByUnfollowed) "Show" else "Hide", + style = MaterialTheme.typography.bodyMedium, + modifier = Modifier.padding(start = 8.dp) + ) + } + } + } + SettingsItem( + text = AnnotatedString("Show reposts"), + description = AnnotatedString("Show reposts in the following feed?"), + ) { + var showReposts by mutableStateOf(feedPrefs.hideReposts != true) + Row { + Switch( + checked = showReposts, + onCheckedChange = { + showReposts = it + agent.setFeedViewPrefs( + feed = "following", + feedViewPref = feedPrefs.copy(hideReposts = !showReposts) + ) + } + ) + Text( + text = if(showReposts) "Show" else "Hide", + style = MaterialTheme.typography.bodyMedium, + modifier = Modifier.padding(start = 8.dp) + ) + } + } + SettingsItem( + text = AnnotatedString("Show quote posts"), + description = AnnotatedString( + "Show quote posts in the following feed? (reposts will still be visible, if set to show)" + ), + ) { + var showQuotePosts by mutableStateOf(feedPrefs.hideQuotePosts != true) + Row { + Switch( + checked = showQuotePosts, + onCheckedChange = { + showQuotePosts = it + agent.setFeedViewPrefs( + feed = "following", + feedViewPref = feedPrefs.copy(hideQuotePosts = !showQuotePosts) + ) + } + ) + Text( + text = if(showQuotePosts) "Show" else "Hide", + style = MaterialTheme.typography.bodyMedium, + modifier = Modifier.padding(start = 8.dp) + ) + } + } + SettingsItem( + text = AnnotatedString("Merge feeds into Following"), + description = AnnotatedString("Occasionally show posts from your saved feeds in your following feed?"), + ) { + var mergeFeeds by mutableStateOf(feedPrefs.lab_mergeFeedEnabled != null) + Row { + Switch( + checked = mergeFeeds, + onCheckedChange = { + mergeFeeds = it + agent.setFeedViewPrefs( + feed = "following", + feedViewPref = feedPrefs.copy(lab_mergeFeedEnabled = mergeFeeds) + ) + } + ) + Text( + text = if(mergeFeeds) "Yes" else "No", + style = MaterialTheme.typography.bodyMedium, + modifier = Modifier.padding(start = 8.dp) + ) + } + } + } +} \ No newline at end of file diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/settings/LanguageSettings.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/settings/LanguageSettings.kt new file mode 100644 index 0000000..9c4dcbd --- /dev/null +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/settings/LanguageSettings.kt @@ -0,0 +1,147 @@ +package com.morpho.app.ui.settings + +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.width +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.KeyboardArrowDown +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.DropdownMenu +import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.surfaceColorAtElevation +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.unit.dp +import com.morpho.app.data.MorphoAgent +import com.morpho.app.ui.elements.SettingsGroup +import com.morpho.app.ui.elements.SettingsItem +import com.morpho.butterfly.Language +import org.koin.compose.getKoin + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun LanguageSettings( + agent: MorphoAgent = getKoin().get(), + modifier: Modifier = Modifier, + distinguish: Boolean = true, +) { + val morphoPrefs = agent.morphoPrefs.value + SettingsGroup( + title = "Language Settings", + modifier = modifier, + distinguish = distinguish, + ) { + SettingsItem(text = AnnotatedString("AppLanguage")) { + LanguageDropDownMenu( + onSelected = { lang -> + agent.setUILanguage(lang) + }, + initialLanguage = morphoPrefs.uiLanguage ?: agent.myLanguage.value + ) + } + } +} + +@Composable +fun LanguageDropDownMenu( + onSelected: (Language) -> Unit, + initialLanguage: Language, + expandedInitially: Boolean = false, +) { + Box(Modifier.height(300.dp).fillMaxWidth()) { + val shape = MaterialTheme.shapes.medium + var expanded by remember { mutableStateOf(expandedInitially) } + var language by remember { mutableStateOf(initialLanguage) } + val onItemClicked: (Language) -> Unit = { lang -> + language = lang + onSelected(lang) + expanded = false + } + Button( + onClick = { expanded = !expanded }, + shape = shape, + colors = ButtonDefaults.buttonColors( + containerColor = MaterialTheme.colorScheme.surfaceColorAtElevation(1.dp) + ) + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.padding(horizontal = 14.dp, vertical = 10.dp) + ) { + Text(initialLanguage.toLanguageName()) + Spacer(Modifier.width(4.dp)) + Image(Icons.Rounded.KeyboardArrowDown, null) + } + } + DropdownMenu(modifier = Modifier.align(Alignment.TopCenter).width(240.dp), expanded = expanded, + onDismissRequest = { expanded = false }) { + + DropdownMenuItem(text = { Text(Language("en").toLanguageName()) }, onClick = { + onItemClicked(Language("en")) + }) + DropdownMenuItem(text = { Text(Language("pt").toLanguageName()) }, onClick = { + onItemClicked(Language("pt")) + }) + DropdownMenuItem(text = { Text(Language("fr").toLanguageName()) }, onClick = { + onItemClicked(Language("fr")) + }) + DropdownMenuItem(text = { Text(Language("es").toLanguageName()) }, onClick = { + onItemClicked(Language("es")) + }) + DropdownMenuItem(text = { Text(Language("de").toLanguageName()) }, onClick = { + onItemClicked(Language("de")) + }) + DropdownMenuItem(text = { Text(Language("ar").toLanguageName()) }, onClick = { + onItemClicked(Language("ar")) + }) + DropdownMenuItem(text = { Text(Language("tr").toLanguageName()) }, onClick = { + onItemClicked(Language("tr")) + }) + DropdownMenuItem(text = { Text(Language("ru").toLanguageName()) }, onClick = { + onItemClicked(Language("ru")) + }) + DropdownMenuItem(text = { Text(Language("it").toLanguageName()) }, onClick = { + onItemClicked(Language("it")) + }) + DropdownMenuItem(text = { Text(Language("ja").toLanguageName()) }, onClick = { + onItemClicked(Language("ja")) + }) + DropdownMenuItem(text = { Text(Language("ko").toLanguageName()) }, onClick = { + onItemClicked(Language("ko")) + }) + + } + } +} + +fun Language.toLanguageName(): String { + return when(this) { + Language("en") -> "English" + Language("pt") -> "Português - Portuguese" + Language("fr") -> "Français - French" + Language("es") -> "Español - Spanish" + Language("de") -> "Deutsch - German" + Language("ar") -> "العربية - Arabic" + Language("tr") -> "Türkçe - Turkish" + Language("ru") -> "Русский - Russian" + Language("it") -> "Italiano - Italian" + Language("ja") -> "日本語 - Japanese" + Language("ko") -> "한국어 - Korean" + else -> "Not handled yet" + } +} \ No newline at end of file diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/settings/PersonalModSettings.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/settings/PersonalModSettings.kt new file mode 100644 index 0000000..e54b25e --- /dev/null +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/settings/PersonalModSettings.kt @@ -0,0 +1,77 @@ +package com.morpho.app.ui.settings + +import androidx.compose.foundation.clickable +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Block +import androidx.compose.material.icons.filled.FilterAlt +import androidx.compose.material.icons.filled.ReduceCapacity +import androidx.compose.material.icons.filled.VisibilityOff +import androidx.compose.material3.Icon +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.AnnotatedString +import com.morpho.app.data.MorphoAgent +import com.morpho.app.ui.elements.SettingsGroup +import com.morpho.app.ui.elements.SettingsItem +import org.koin.compose.getKoin + +@Composable +fun PersonalModSettings( + agent: MorphoAgent = getKoin().get(), + modifier: Modifier = Modifier, + distinguish: Boolean = true, +) { + SettingsGroup( + title = "Moderation tools", + modifier = modifier, + distinguish = distinguish, + ) { + SettingsItem( + description = AnnotatedString("Muted words and tags"), + modifier = Modifier.clickable { + + } + ){ + Icon( + Icons.Default.FilterAlt, + contentDescription = "Filter", + ) + } + + SettingsItem( + description = AnnotatedString("Moderation lists"), + modifier = Modifier.clickable { + + } + ){ + Icon( + Icons.Default.ReduceCapacity, + contentDescription = "People", + ) + } + + SettingsItem( + description = AnnotatedString("Muted accounts"), + modifier = Modifier.clickable { + + } + ) { + Icon( + Icons.Default.VisibilityOff, + contentDescription = "Mute/Hide", + ) + } + + SettingsItem( + description = AnnotatedString("Blocked accounts"), + modifier = Modifier.clickable { + + } + ){ + Icon( + Icons.Default.Block, + contentDescription = "Block", + ) + } + } +} \ No newline at end of file diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/settings/SettingsFragment.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/settings/SettingsFragment.kt new file mode 100644 index 0000000..0bf0db5 --- /dev/null +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/settings/SettingsFragment.kt @@ -0,0 +1,114 @@ +package com.morpho.app.ui.settings + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.material.Surface +import androidx.compose.material.Text +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Accessibility +import androidx.compose.material.icons.filled.BackHand +import androidx.compose.material.icons.filled.FilterAlt +import androidx.compose.material.icons.filled.Forum +import androidx.compose.material.icons.filled.ImagesearchRoller +import androidx.compose.material.icons.filled.RssFeed +import androidx.compose.material.icons.filled.Translate +import androidx.compose.material.icons.filled.Tune +import androidx.compose.material3.Icon +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.RectangleShape +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.unit.dp +import com.morpho.app.data.MorphoAgent +import com.morpho.app.ui.elements.SettingsItem +import org.koin.compose.getKoin + +@Composable +fun SettingsFragment( + agent: MorphoAgent = getKoin().get(), + modifier: Modifier = Modifier, +) { + Column { + Text("Basics") + Surface( + elevation = 2.dp, + modifier = Modifier.padding(vertical = 8.dp) + ) { + Column { + SettingsItem( + description = AnnotatedString("Accessibility"), + modifier = Modifier.clickable { } + ) { + Icon(Icons.Default.Accessibility, contentDescription = "Accessibility") + } + SettingsItem( + description = AnnotatedString("Appearance"), + modifier = Modifier.clickable { } + ) { + Icon(Icons.Default.ImagesearchRoller, contentDescription = "Appearance") + } + SettingsItem( + description = AnnotatedString("Languages"), + modifier = Modifier.clickable { } + ) { + Icon(Icons.Default.Translate, contentDescription = "Languages") + } + + SettingsItem( + description = AnnotatedString("Moderation"), + modifier = Modifier.clickable { } + ) { + Icon(Icons.Default.BackHand, contentDescription = "Moderation") + } + SettingsItem( + description = AnnotatedString("Notifications filtering"), + modifier = Modifier.clickable { } + ) { + Icon(Icons.Default.FilterAlt, contentDescription = "Notifications") + } + SettingsItem( + description = AnnotatedString("Following Feed Preferences"), + modifier = Modifier.clickable { } + ) { + Icon(Icons.Default.Tune, contentDescription = "Following Feed Preferences") + } + + SettingsItem( + description = AnnotatedString("Thread Preferences"), + modifier = Modifier.clickable { } + ) { + Icon(Icons.Default.Forum, contentDescription = "Thread Preferences") + } + SettingsItem( + description = AnnotatedString("My Saved Feeds"), + modifier = Modifier.clickable { } + ) { + Icon(Icons.Default.RssFeed, contentDescription = "My Saved Feeds") + } + + } + } + Spacer(modifier = Modifier.height(8.dp)) + Text("Advanced") + Surface( + elevation = 2.dp, + modifier = Modifier.padding(vertical = 8.dp) + ){ + + } + Spacer(modifier = Modifier.height(8.dp)) + TextButton( + onClick = { }, + modifier = Modifier.padding(vertical = 8.dp), + shape = RectangleShape, + ) { + Text("System Log") + } + val version = com.morpho.app.BuildKonfig.versionString + Text("Version $version") + } +} \ No newline at end of file diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/util/json.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/util/json.kt index f5b200b..ab2f59d 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/util/json.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/util/json.kt @@ -1,18 +1,55 @@ package com.morpho.app.util -import com.morpho.butterfly.butterflySerializersModule +import app.bsky.actor.PreferencesUnion +import kotlinx.serialization.InternalSerializationApi import kotlinx.serialization.KSerializer +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json import kotlinx.serialization.json.JsonElement import kotlinx.serialization.json.jsonObject import kotlinx.serialization.json.jsonPrimitive +import kotlinx.serialization.modules.SerializersModule +import kotlinx.serialization.modules.polymorphic +import kotlinx.serialization.modules.subclass +import kotlinx.serialization.serializer +import kotlin.jvm.JvmInline + +@OptIn(InternalSerializationApi::class) +val morphoSerializersModule = SerializersModule { + polymorphic(PreferencesUnion::class) { + subclass(PreferencesUnion.AdultContentPref::class) + subclass(PreferencesUnion.FeedViewPref::class) + subclass(PreferencesUnion.ThreadViewPref::class) + subclass(PreferencesUnion.SkyFeedBuilderFeedsPref::class) + subclass(PreferencesUnion.SavedFeedsPref::class) + subclass(PreferencesUnion.SavedFeedsPrefV2::class) + subclass(PreferencesUnion.PersonalDetailsPref::class) + subclass(PreferencesUnion.ContentLabelPref::class) + subclass(PreferencesUnion.LabelersPref::class) + subclass(PreferencesUnion.HiddenPostsPref::class) + subclass(PreferencesUnion.MutedWordsPref::class) + subclass(PreferencesUnion.InterestsPref::class) + subclass(MorphoPreferences::class) + defaultDeserializer { _ -> + PreferencesUnion.UnknownPreference::class.serializer() + } + } +} + +@Serializable +@JvmInline +@SerialName("app.bsky.actor.defs#morphoPrefs") +value class MorphoPreferences( + val value: com.morpho.app.data.MorphoPreferences +): PreferencesUnion val json = Json { classDiscriminator = "${'$'}type" ignoreUnknownKeys = true prettyPrint = true - serializersModule = butterflySerializersModule + serializersModule = morphoSerializersModule } val JsonElement.recordType: String diff --git a/Morpho/composeApp/src/desktopMain/kotlin/com/morpho/app/ui/post/PostImage.desktop.kt b/Morpho/composeApp/src/desktopMain/kotlin/com/morpho/app/ui/post/PostImage.desktop.kt index bba9528..ca4816b 100644 --- a/Morpho/composeApp/src/desktopMain/kotlin/com/morpho/app/ui/post/PostImage.desktop.kt +++ b/Morpho/composeApp/src/desktopMain/kotlin/com/morpho/app/ui/post/PostImage.desktop.kt @@ -1,9 +1,23 @@ package com.morpho.app.ui.post import MorphoDialog -import androidx.compose.foundation.* -import androidx.compose.foundation.layout.* +import androidx.compose.foundation.HorizontalScrollbar +import androidx.compose.foundation.LocalScrollbarStyle +import androidx.compose.foundation.VerticalScrollbar +import androidx.compose.foundation.background +import androidx.compose.foundation.horizontalScroll +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.requiredWidthIn +import androidx.compose.foundation.layout.widthIn +import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.rememberScrollbarAdapter import androidx.compose.foundation.text.selection.SelectionContainer +import androidx.compose.foundation.verticalScroll import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults import androidx.compose.material3.MaterialTheme @@ -48,7 +62,7 @@ actual fun FullImageView( } val (undecorated, tabbed) = if (morphoPrefs != null) { log.d{ "Morpho Preferences: $morphoPrefs" } - morphoPrefs.tabbed to morphoPrefs.undecorated + (morphoPrefs.tabbed == true) to (morphoPrefs.undecorated == true) } else { log.d {"No Morpho Preferences found, using defaults" } true to true diff --git a/Morpho/composeApp/src/desktopMain/kotlin/com/morpho/app/ui/profile/DetailedProfileFragment.desktop.kt b/Morpho/composeApp/src/desktopMain/kotlin/com/morpho/app/ui/profile/DetailedProfileFragment.desktop.kt index 9c92344..22abc6c 100644 --- a/Morpho/composeApp/src/desktopMain/kotlin/com/morpho/app/ui/profile/DetailedProfileFragment.desktop.kt +++ b/Morpho/composeApp/src/desktopMain/kotlin/com/morpho/app/ui/profile/DetailedProfileFragment.desktop.kt @@ -4,13 +4,38 @@ import androidx.compose.animation.animateContentSize import androidx.compose.animation.core.Spring import androidx.compose.animation.core.spring import androidx.compose.foundation.background -import androidx.compose.foundation.layout.* +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ExperimentalLayoutApi +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.WindowInsetsSides +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.only +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.requiredHeight +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.systemBars +import androidx.compose.foundation.layout.widthIn +import androidx.compose.foundation.layout.windowInsetsPadding +import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.text.selection.SelectionContainer import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.ArrowBackIosNew -import androidx.compose.material3.* +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.LargeTopAppBar +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.material3.TopAppBarScrollBehavior import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.Alignment @@ -26,7 +51,10 @@ import coil3.compose.AsyncImage import coil3.compose.LocalPlatformContext import coil3.request.ImageRequest import coil3.request.crossfade +import com.morpho.app.model.bluesky.BskyLabelService import com.morpho.app.model.bluesky.DetailedProfile +import com.morpho.app.model.uidata.Event +import com.morpho.app.model.uidata.LabelerEvent import com.morpho.app.ui.elements.AvatarShape import com.morpho.app.ui.elements.OutlinedAvatar import com.morpho.app.ui.elements.RichTextElement @@ -49,6 +77,7 @@ actual fun DetailedProfileFragment( isTopLevel: Boolean, scrollBehavior: TopAppBarScrollBehavior, onBackClicked: () -> Unit, + eventCallback: (Event) -> Unit, ) { val scrollState = rememberScrollState(0) val name = profile.displayName ?: profile.handle.handle @@ -268,3 +297,213 @@ actual fun DetailedProfileFragment( } +@OptIn(ExperimentalMaterial3Api::class) +@Composable +actual fun LabelerProfileFragment( + labeler: BskyLabelService, + modifier: Modifier, + isSubscribed: Boolean, + isTopLevel: Boolean, + scrollBehavior: TopAppBarScrollBehavior, + onBackClicked: () -> Unit, + eventCallback: (Event) -> Unit, +) { + val scrollState = rememberScrollState(0) + val name = labeler.displayName ?: labeler.handle.handle + val bannerHeight = if (scrollBehavior.state.collapsedFraction <= .2) { + 135.dp + } else { + (135.dp - (60 * scrollBehavior.state.collapsedFraction).dp) + } + val collapsed = scrollBehavior.state.collapsedFraction > 0.5 + LaunchedEffect(scrollState) { + println("Banner Height: $bannerHeight") + print("Collapsed: $collapsed") + } + + ConstraintLayout( + modifier = Modifier + .fillMaxWidth() + //.requiredHeight(bannerHeight*2) + .nestedScroll(scrollBehavior.nestedScrollConnection) + .background(MaterialTheme.colorScheme.surface) + .verticalScroll(scrollState) + //.border(1.dp, Color.Red) + ) { + val (appbar, userStats, banner, labels, text, collapsedText) = createRefs() + + AsyncImage( + model = ImageRequest.Builder(LocalPlatformContext.current) + .data(labeler.creator?.banner.orEmpty()) + .crossfade(true) + .build(), + placeholder = painterResource(Res.drawable.test_banner), + contentDescription = "Profile Banner for ${labeler.displayName} ${labeler.handle}", + contentScale = ContentScale.Crop, + alignment = Alignment.TopCenter, + modifier = Modifier + .fillMaxWidth() + .constrainAs(banner) { + top.linkTo(parent.top) + } + .animateContentSize( + spring( + stiffness = Spring.StiffnessMediumLow, + dampingRatio = Spring.DampingRatioNoBouncy + ) + ) + .requiredHeight(bannerHeight)//.border(1.dp, Color.Blue) + ) + + LargeTopAppBar( + title = { + ConstraintLayout(//constraintSet = , + modifier = Modifier + .fillMaxWidth() + .fillMaxHeight() + ) { + val (avatar, buttons, info) = createRefs() + val expanded = scrollBehavior.state.collapsedFraction <= 0.5 + val avatarSize = (80.dp - (30.0 * scrollBehavior.state.collapsedFraction).dp) + val centreGuideFraction = if(expanded) .6f else .5f + val avatarGuide = createGuidelineFromStart(.1f ) + val centreGuide = createGuidelineFromTop(centreGuideFraction) + + if(expanded){ + LabelerButtons( + subscribed = isSubscribed, + modifier = Modifier.zIndex(4f) + .constrainAs(buttons) { + centerAround(centreGuide) + end.linkTo(parent.end, 12.dp) + }, + onSubscribeClicked = { + eventCallback(LabelerEvent.Subscribe(labeler.did)) + }, + onUnsubscribeClicked = { + eventCallback(LabelerEvent.Unsubscribe(labeler.did)) + }, + onMenuClicked = { + // TODO: add labeler menu + }, + ) + OutlinedAvatar( + url = labeler.avatar.orEmpty(), + contentDescription = "Avatar for ${labeler.displayName} ${labeler.handle}", + modifier = Modifier.zIndex(4f) + .constrainAs(avatar) { + centerAround(avatarGuide) + }, + size = avatarSize, + outlineSize = 2.dp, + avatarShape = AvatarShape.Rounded + ) + } else { + Surface( + color = MaterialTheme.colorScheme.background, + shape = MaterialTheme.shapes.small, + modifier = Modifier + .height(avatarSize) + .constrainAs(info) { + centerAround(centreGuide) + start.linkTo(avatarGuide, (-20).dp) + }//.border(1.dp, Color.Green), + ) { + Row { + OutlinedAvatar( + url = labeler.avatar.orEmpty(), + contentDescription = "Avatar for ${labeler.displayName} ${labeler.handle}", + size = avatarSize, + outlineSize = 2.dp, + avatarShape = AvatarShape.Rounded + ) + Column( + verticalArrangement = Arrangement.Bottom, + modifier = Modifier.padding(start = 10.dp, end = 8.dp, bottom = 4.dp) + ) { + Text( + text = name, + style = MaterialTheme.typography.headlineSmall, + color = MaterialTheme.colorScheme.onSurface, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + Text( + text = " @${labeler.handle}", + color = MaterialTheme.colorScheme.primary, + style = MaterialTheme.typography.labelMedium, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + } + } + + } + } + + } + }, + navigationIcon = { + if (isTopLevel) { + IconButton( + onClick = { onBackClicked() }, + modifier = Modifier.size(30.dp).zIndex(4f), + + ) { + Icon( + imageVector = Icons.Default.ArrowBackIosNew, + contentDescription = "Back", + ) + } + } + }, + actions = {}, + scrollBehavior = scrollBehavior, + colors = TopAppBarDefaults.largeTopAppBarColors( + containerColor = Color.Transparent + ), + modifier = Modifier + .constrainAs(appbar) { + top.linkTo(parent.top, 15.dp) + }.zIndex(1f) + .windowInsetsPadding(WindowInsets.systemBars.only(WindowInsetsSides.Top)) + .wrapContentHeight(Alignment.Top) + //.border(1.dp, Color.Magenta) + , + windowInsets = WindowInsets.systemBars.only(WindowInsetsSides.Top) + ) + if(!collapsed){ + + Column( + modifier = Modifier + .constrainAs(text) { + top.linkTo(userStats.bottom) + start.linkTo(parent.start) + } + .padding(start = 20.dp, end = 20.dp, top = bannerHeight +40.dp)//.border(1.dp, Color.Yellow) + ) { + + SelectionContainer { + Text( + text = name, + style = MaterialTheme.typography.headlineMedium, + color = MaterialTheme.colorScheme.onSurface + ) + } + SelectionContainer { + Text( + text = " @${labeler.handle}", + color = MaterialTheme.colorScheme.primary, + style = MaterialTheme.typography.labelMedium, + ) + } + + Spacer(modifier = Modifier.height(10.dp)) + SelectionContainer { + RichTextElement(labeler.creator?.description.orEmpty()) + } + } + + } + } +} \ No newline at end of file diff --git a/Morpho/composeApp/src/desktopMain/kotlin/main.kt b/Morpho/composeApp/src/desktopMain/kotlin/main.kt index 22073b4..5f930eb 100644 --- a/Morpho/composeApp/src/desktopMain/kotlin/main.kt +++ b/Morpho/composeApp/src/desktopMain/kotlin/main.kt @@ -49,6 +49,7 @@ import com.github.tkuenneth.nativeparameterstoreaccess.NativeParameterStoreAcces import com.github.tkuenneth.nativeparameterstoreaccess.WindowsRegistry.getWindowsRegistryEntry import com.morpho.app.App import com.morpho.app.data.MorphoAgent +import com.morpho.app.data.PreferencesRepository import com.morpho.app.di.appModule import com.morpho.app.di.dataModule import com.morpho.app.di.storageModule @@ -95,11 +96,12 @@ fun main() = application { cachePath.toNioPath().createDirectories() koin.get { parametersOf(storageDir) } koin.get { parametersOf(storageDir) } + koin.get { parametersOf(storageDir) } val agent = koin.get() val morphoPrefs = agent.morphoPrefs val (undecorated, tabbed) = run { log.d{ "Morpho Preferences: $morphoPrefs" } - morphoPrefs.tabbed to morphoPrefs.undecorated + (morphoPrefs.value.tabbed == true) to (morphoPrefs.value.undecorated == true) } val windowState = rememberWindowState( placement = WindowPlacement.Floating, diff --git a/Morpho/gradle.properties b/Morpho/gradle.properties index 7971b8d..a71343f 100644 --- a/Morpho/gradle.properties +++ b/Morpho/gradle.properties @@ -13,4 +13,6 @@ kotlin.mpp.androidSourceSetLayoutVersion=2 kotlin.mpp.enableCInteropCommonization=true #Development -development=true \ No newline at end of file +development=true + +buildkonfig.flavor=dev \ No newline at end of file diff --git a/Morpho/gradle/libs.versions.toml b/Morpho/gradle/libs.versions.toml index f80039c..cd01c1a 100644 --- a/Morpho/gradle/libs.versions.toml +++ b/Morpho/gradle/libs.versions.toml @@ -35,6 +35,12 @@ kotlinx-datetime = "0.6.0" kotlinx-coroutines = "1.8.1" kotlinx-immutable = "0.3.7" kstore = "0.7.1" +pagingCommon = "3.3.0-alpha02-0.5.1" +pagingComposeCommon = "3.3.0-alpha02-0.5.1" +pagingRuntime = "3.3.0-alpha02" +pagingRuntimeUikit = "3.3.0-alpha02-0.5.1" +pagingTesting = "3.3.0-alpha02-0.5.1" +parcelize = "0.9.0" ktor = "2.3.9" ktorClientAndroid = "[ktor-version]" logbackClassic = "1.5.7" @@ -46,6 +52,8 @@ slf4j-api = "2.0.13" github-kotlin-logging-jvm = "5.1.0" kotlinx-abi-plugin = "0.13.2" window = "1.3.0" +toolargetool = "0.3.0" +voyagerLifecycleKmp = "1.1.0-beta02" material3-android = "1.2.1" accompanist-permissions = "0.32.0" coil = "3.0.0-alpha06" @@ -57,6 +65,8 @@ kmpalette = "3.1.0" androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "androidx-core-ktx" } androidx-datastore-preferences-core = { module = "androidx.datastore:datastore-preferences-core", version.ref = "datastorePreferencesCore" } androidx-test-junit = { group = "androidx.test.ext", name = "junit", version.ref = "androidx-test-junit" } +androidx-paging-compose = { module = "androidx.paging:paging-compose", version.ref = "pagingRuntime" } +androidx-paging-runtime = { module = "androidx.paging:paging-runtime", version.ref = "pagingRuntime" } androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "androidx-espresso-core" } androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "androidx-appcompat" } androidx-material = { group = "com.google.android.material", name = "material", version.ref = "androidx-material" } @@ -130,6 +140,11 @@ slf4j-api = { module = "org.slf4j:slf4j-api", version.ref = "slf4j-api" } oshai-kotlin-logging-jvm = { module = "io.github.oshai:kotlin-logging-jvm", version.ref = "github-kotlin-logging-jvm" } slf4j-simple = { module = "org.slf4j:slf4j-simple", version = "2.0.9" } apache-commons = { module = "org.apache.commons:commons-lang3", version = "3.13.0" } +paging-common = { module = "app.cash.paging:paging-common", version.ref = "pagingCommon" } +paging-compose-common = { module = "app.cash.paging:paging-compose-common", version.ref = "pagingComposeCommon" } +paging-runtime-uikit = { module = "app.cash.paging:paging-runtime-uikit", version.ref = "pagingRuntimeUikit" } +paging-testing = { module = "app.cash.paging:paging-testing", version.ref = "pagingTesting" } +parcelize = { module = "dev.icerock.moko:parcelize", version.ref = "parcelize" } kotlin-gradle-plugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin-gradle-plugin" } kotlin-reflect = { module = "org.jetbrains.kotlin:kotlin-reflect", version.ref = "kotlin-reflect" } @@ -150,8 +165,12 @@ ksp-symbol-processing-api = { module = "com.google.devtools.ksp:symbol-processin kstore = { module = "io.github.xxfast:kstore", version.ref = "kstore" } kstore-file = { module = "io.github.xxfast:kstore-file", version.ref = "kstore" } + +toolargetool = { module = "com.gu.android:toolargetool", version.ref = "toolargetool" } voyager-bottom-sheet-navigator = { module = "cafe.adriel.voyager:voyager-bottom-sheet-navigator", version.ref = "voyager" } voyager-koin = { module = "cafe.adriel.voyager:voyager-koin", version.ref = "voyager" } + +voyager-lifecycle-kmp = { module = "cafe.adriel.voyager:voyager-lifecycle-kmp", version.ref = "voyager" } voyager-navigator = { module = "cafe.adriel.voyager:voyager-navigator", version.ref = "voyager" } voyager-screenmodel = { module = "cafe.adriel.voyager:voyager-screenmodel", version.ref = "voyager" } voyager-tab-navigator = { module = "cafe.adriel.voyager:voyager-tab-navigator", version.ref = "voyager" } diff --git a/Morpho/settings.gradle.kts b/Morpho/settings.gradle.kts index e90a61e..7e05248 100644 --- a/Morpho/settings.gradle.kts +++ b/Morpho/settings.gradle.kts @@ -15,6 +15,7 @@ pluginManagement { } dependencyResolutionManagement { + @Suppress("UnstableApiUsage") repositories { mavenLocal() mavenCentral() diff --git a/gradle.properties b/gradle.properties index 877fcb1..8badbb9 100755 --- a/gradle.properties +++ b/gradle.properties @@ -15,4 +15,6 @@ kotlin.mpp.androidSourceSetLayoutVersion=2 kotlin.mpp.enableCInteropCommonization=true #Development -development=true \ No newline at end of file +development=true + +buildkonfig.flavor=dev \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index ecf0be1..676553d 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -41,9 +41,17 @@ logbackCore = "1.5.7" logging = "1.4.2" nativeparameterstoreaccess = "0.1.0" okio = "3.9.0" +pagingCommon = "3.3.0-alpha02-0.5.1" +pagingComposeCommon = "3.3.0-alpha02-0.5.1" +pagingRuntime = "3.3.0-alpha02" +pagingRuntimeUikit = "3.3.0-alpha02-0.5.1" +pagingTesting = "3.3.0-alpha02-0.5.1" +parcelize = "0.9.0" slf4j-api = "2.0.13" github-kotlin-logging-jvm = "5.1.0" kotlinx-abi-plugin = "0.13.2" +toolargetool = "0.3.0" +voyagerLifecycleKmp = "1.1.0-beta02" window = "1.3.0" material3-android = "1.2.1" accompanist-permissions = "0.32.0" @@ -55,6 +63,8 @@ kmpalette = "3.1.0" [libraries] androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "androidx-core-ktx" } androidx-datastore-preferences-core = { module = "androidx.datastore:datastore-preferences-core", version.ref = "datastorePreferencesCore" } +androidx-paging-compose = { module = "androidx.paging:paging-compose", version.ref = "pagingRuntime" } +androidx-paging-runtime = { module = "androidx.paging:paging-runtime", version.ref = "pagingRuntime" } androidx-test-junit = { group = "androidx.test.ext", name = "junit", version.ref = "androidx-test-junit" } androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "androidx-espresso-core" } androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "androidx-appcompat" } @@ -124,6 +134,11 @@ logback-classic = { module = "ch.qos.logback:logback-classic", version.ref = "lo logback-core = { module = "ch.qos.logback:logback-core", version.ref = "logbackCore" } logging = { module = "org.lighthousegames:logging", version.ref = "logging" } nativeparameterstoreaccess = { module = "com.github.tkuenneth:nativeparameterstoreaccess", version.ref = "nativeparameterstoreaccess" } +paging-common = { module = "app.cash.paging:paging-common", version.ref = "pagingCommon" } +paging-compose-common = { module = "app.cash.paging:paging-compose-common", version.ref = "pagingComposeCommon" } +paging-runtime-uikit = { module = "app.cash.paging:paging-runtime-uikit", version.ref = "pagingRuntimeUikit" } +paging-testing = { module = "app.cash.paging:paging-testing", version.ref = "pagingTesting" } +parcelize = { module = "dev.icerock.moko:parcelize", version.ref = "parcelize" } slf4j-api = { module = "org.slf4j:slf4j-api", version.ref = "slf4j-api" } oshai-kotlin-logging-jvm = { module = "io.github.oshai:kotlin-logging-jvm", version.ref = "github-kotlin-logging-jvm" } slf4j-simple = { module = "org.slf4j:slf4j-simple", version = "2.0.9" } @@ -147,8 +162,10 @@ ksp-symbol-processing-api = { module = "com.google.devtools.ksp:symbol-processin kstore = { module = "io.github.xxfast:kstore", version.ref = "kstore" } kstore-file = { module = "io.github.xxfast:kstore-file", version.ref = "kstore" } +toolargetool = { module = "com.gu.android:toolargetool", version.ref = "toolargetool" } voyager-bottom-sheet-navigator = { module = "cafe.adriel.voyager:voyager-bottom-sheet-navigator", version.ref = "voyager" } voyager-koin = { module = "cafe.adriel.voyager:voyager-koin", version.ref = "voyager" } +voyager-lifecycle-kmp = { module = "cafe.adriel.voyager:voyager-lifecycle-kmp", version.ref = "voyager" } voyager-navigator = { module = "cafe.adriel.voyager:voyager-navigator", version.ref = "voyager" } voyager-screenmodel = { module = "cafe.adriel.voyager:voyager-screenmodel", version.ref = "voyager" } voyager-tab-navigator = { module = "cafe.adriel.voyager:voyager-tab-navigator", version.ref = "voyager" } From 081240dc60f32dc82fb91ce088002d0c6449fc5b Mon Sep 17 00:00:00 2001 From: Orual Date: Sun, 22 Sep 2024 17:27:15 -0400 Subject: [PATCH 31/42] Settings screen accessible and partially functional. - Also found a couple of new keys in the visibility settings/default settings for content labels. --- Butterfly | 2 +- Morpho/composeApp/build.gradle.kts | 9 +- .../kotlin/com/morpho/app/data/MorphoAgent.kt | 22 +- .../morpho/app/data/PreferencesRepository.kt | 3 +- .../com/morpho/app/model/bluesky/BskyLabel.kt | 6 +- .../morpho/app/model/bluesky/BskyPostReply.kt | 14 +- .../com/morpho/app/model/uidata/MorphoData.kt | 32 +- .../app/screens/base/BaseScreenModel.kt | 17 +- .../app/screens/base/tabbed/NavigationTabs.kt | 24 +- .../screens/base/tabbed/TabbedBaseScreen.kt | 7 +- .../morpho/app/screens/login/LoginScreen.kt | 8 +- .../app/screens/main/MainScreenModel.kt | 1 - .../app/screens/main/tabbed/TabbedHomeView.kt | 1 + .../app/screens/profile/TabbedProfileView.kt | 3 +- .../screens/settings/TabbedSettingsView.kt | 406 +++++++++++++++++ .../morpho/app/screens/thread/ThreadView.kt | 2 +- .../com/morpho/app/ui/common/NavDrawer.kt | 2 + .../morpho/app/ui/elements/SettingsItems.kt | 55 ++- .../app/ui/settings/AccessibilitySettings.kt | 37 +- .../ui/settings/AdditionalLabelerSettings.kt | 29 +- .../app/ui/settings/AppearanceSettings.kt | 27 +- .../app/ui/settings/BuiltinContentFilters.kt | 48 +- .../morpho/app/ui/settings/FeedPreferences.kt | 275 +++++++----- .../app/ui/settings/LanguageSettings.kt | 31 +- .../ui/settings/ModerationSettingsFragment.kt | 47 ++ .../app/ui/settings/MutedWordsSettings.kt | 413 ++++++++++++++++++ .../app/ui/settings/PersonalModSettings.kt | 50 ++- .../app/ui/settings/SettingsFragment.kt | 78 +++- .../kotlin/com/morpho/app/ui/theme/Shape.kt | 51 ++- .../kotlin/com/morpho/app/util/json.kt | 12 +- Morpho/gradle/libs.versions.toml | 2 + gradle/libs.versions.toml | 2 + 32 files changed, 1468 insertions(+), 248 deletions(-) create mode 100644 Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/settings/ModerationSettingsFragment.kt create mode 100644 Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/settings/MutedWordsSettings.kt diff --git a/Butterfly b/Butterfly index f0ae7dd..20a5f32 160000 --- a/Butterfly +++ b/Butterfly @@ -1 +1 @@ -Subproject commit f0ae7dd5b8b488643b606bfc555ee145500fba07 +Subproject commit 20a5f32a77326ad58c67798176159d966b111298 diff --git a/Morpho/composeApp/build.gradle.kts b/Morpho/composeApp/build.gradle.kts index 9d6ec2e..1ce360f 100644 --- a/Morpho/composeApp/build.gradle.kts +++ b/Morpho/composeApp/build.gradle.kts @@ -129,6 +129,9 @@ kotlin { implementation(libs.androidx.paging.runtime) implementation(libs.androidx.paging.compose) + + + //implementation(libs.logkmpanion) } commonMain.dependencies { @@ -181,9 +184,6 @@ kotlin { implementation(libs.kotlinx.serialization.cbor) implementation(libs.kotlinx.serialization.json) - - - implementation(kotlin("reflect")) api(libs.logging) @@ -251,6 +251,9 @@ kotlin { implementation(libs.logback.classic) implementation(libs.nativeparameterstoreaccess) implementation(libs.kotlin.jwt) + + + //implementation(libs.logkmpanion) } commonTest.dependencies { diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/data/MorphoAgent.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/data/MorphoAgent.kt index 862283f..04c310f 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/data/MorphoAgent.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/data/MorphoAgent.kt @@ -18,8 +18,10 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.flow import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking import org.koin.core.component.inject class MorphoAgent: ButterflyAgent() { @@ -32,7 +34,7 @@ class MorphoAgent: ButterflyAgent() { notificationsFilter = NotificationsFilterPref(), accessibility = AccessibilityPreferences(), )) - private val _bskyPrefs: MutableStateFlow = MutableStateFlow(BskyPreferences()) + private val _bskyPrefs: MutableStateFlow = MutableStateFlow(prefs) private var _myLanguage = MutableStateFlow(Language(myLang ?: _morphoPrefs.value.uiLanguage?.tag ?: "en")) val morphoPrefs = _morphoPrefs.asStateFlow() @@ -48,24 +50,26 @@ class MorphoAgent: ButterflyAgent() { // Belt and suspenders bc of the super/derived class initialization uncertainty api = XrpcBlueskyApi(atpClient, morphoSerializersModule) if(id != null) { + runBlocking { + getPreferences() + } serviceScope.launch { - localPrefs.morphoPrefs(id!!).collectLatest { - if (it != null) { + localPrefs.morphoPrefs(id!!).distinctUntilChanged().collectLatest { + if (it != null && it != MorphoPreferences()) { _morphoPrefs.value = it } - } } serviceScope.launch { - localPrefs.bskyPrefs(id!!).collectLatest { - if (it != null) { + localPrefs.bskyPrefs(id!!).distinctUntilChanged().collectLatest { + if (it != null && it != BskyPreferences()) { prefs = it } } } serviceScope.launch { - localPrefs.bskyPrefs(id!!).collectLatest { - if (it != null) { + localPrefs.bskyPrefs(id!!).distinctUntilChanged().collectLatest { + if (it != null && it != BskyPreferences()) { _bskyPrefs.value = it } } @@ -74,7 +78,7 @@ class MorphoAgent: ButterflyAgent() { localPrefs.writePreferences( BskyUserPreferences( id!!, - getPreferences().getOrNull() ?: BskyPreferences(), + prefs, morphoPrefs.value ) ) diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/data/PreferencesRepository.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/data/PreferencesRepository.kt index 9c37c12..c10bf68 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/data/PreferencesRepository.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/data/PreferencesRepository.kt @@ -1,5 +1,6 @@ package com.morpho.app.data +import app.bsky.actor.PreferencesUnion import com.morpho.app.model.uistate.NotificationsFilterState import com.morpho.butterfly.BskyPreferences import com.morpho.butterfly.Did @@ -84,7 +85,7 @@ data class MorphoPreferences( val darkMode: DarkModeSetting? = DarkModeSetting.SYSTEM, val notificationsFilter: NotificationsFilterPref? = NotificationsFilterPref(), val accessibility: AccessibilityPreferences? = AccessibilityPreferences(), -) { +): PreferencesUnion.ButterflyPreference() { companion object { fun update( existing: MorphoPreferences, diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/BskyLabel.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/BskyLabel.kt index ec5d437..aec1c8d 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/BskyLabel.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/BskyLabel.kt @@ -114,6 +114,8 @@ enum class LabelSetting { HIDE, @SerialName("show") SHOW, + @SerialName("inform") + INFORM, } fun DefaultSetting.toLabelSetting(): LabelSetting { @@ -122,6 +124,7 @@ fun DefaultSetting.toLabelSetting(): LabelSetting { DefaultSetting.WARN -> LabelSetting.WARN DefaultSetting.HIDE -> LabelSetting.HIDE DefaultSetting.SHOW -> LabelSetting.SHOW + DefaultSetting.INFORM -> LabelSetting.INFORM } } @@ -132,8 +135,8 @@ fun Visibility.toLabelSetting(): LabelSetting { Visibility.WARN -> LabelSetting.WARN Visibility.HIDE -> LabelSetting.HIDE Visibility.IGNORE -> LabelSetting.IGNORE + Visibility.INFORM -> LabelSetting.INFORM } - } @Parcelize @@ -155,6 +158,7 @@ data class BskyLabelDefinition( LabelSetting.WARN -> Visibility.WARN LabelSetting.HIDE -> Visibility.HIDE LabelSetting.SHOW -> Visibility.SHOW + LabelSetting.INFORM -> Visibility.INFORM null -> Visibility.IGNORE } } diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/BskyPostReply.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/BskyPostReply.kt index 861b06a..91aa957 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/BskyPostReply.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/BskyPostReply.kt @@ -2,9 +2,13 @@ package com.morpho.app.model.bluesky import androidx.compose.runtime.Immutable import app.bsky.actor.ProfileViewBasic -import app.bsky.feed.* +import app.bsky.feed.GetPostsQuery +import app.bsky.feed.PostReplyRef +import app.bsky.feed.ReplyRef +import app.bsky.feed.ReplyRefParentUnion +import app.bsky.feed.ReplyRefRootUnion import com.atproto.repo.StrongRef -import com.morpho.butterfly.Butterfly +import com.morpho.app.data.MorphoAgent import dev.icerock.moko.parcelize.Parcelable import dev.icerock.moko.parcelize.Parcelize import kotlinx.collections.immutable.persistentListOf @@ -63,11 +67,11 @@ fun PostReplyRef.toReply(): BskyPostReply { ) } -suspend fun PostReplyRef.hydratedReply(api: Butterfly): BskyPostReply { - val parents = api.api.getPosts(GetPostsQuery(persistentListOf(this.parent.uri, this.root.uri))) +suspend fun PostReplyRef.hydratedReply(agent: MorphoAgent): BskyPostReply { + val parents = agent.api.getPosts(GetPostsQuery(persistentListOf(this.parent.uri, this.root.uri))) .getOrNull()?.posts?.map { it.toPost() } ?: persistentListOf() val grandparent = if (parents.first().reply?.replyRef?.parent?.uri != null) { - api.api.getPosts(GetPostsQuery(persistentListOf(parents.first().reply?.replyRef?.parent?.uri!!))).getOrNull()?.posts?.firstOrNull() + agent.api.getPosts(GetPostsQuery(persistentListOf(parents.first().reply?.replyRef?.parent?.uri!!))).getOrNull()?.posts?.firstOrNull() } else null return BskyPostReply( rootPost = parents.firstOrNull { it.cid == this.root.cid }, diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/MorphoData.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/MorphoData.kt index f3bdbcc..687ed3b 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/MorphoData.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/MorphoData.kt @@ -2,12 +2,30 @@ package com.morpho.app.model.uidata //import com.rickclephas.kmp.nativecoroutines.NativeCoroutines import androidx.compose.runtime.Immutable -import androidx.compose.ui.util.* +import androidx.compose.ui.util.fastAny +import androidx.compose.ui.util.fastDistinctBy +import androidx.compose.ui.util.fastFilterNotNull +import androidx.compose.ui.util.fastForEach +import androidx.compose.ui.util.fastForEachIndexed import app.bsky.feed.FeedViewPost import com.morpho.app.data.FeedTuner -import com.morpho.app.model.bluesky.* +import com.morpho.app.data.MorphoAgent +import com.morpho.app.model.bluesky.AuthorContext +import com.morpho.app.model.bluesky.BskyLabelDefinition +import com.morpho.app.model.bluesky.BskyLabelService +import com.morpho.app.model.bluesky.BskyList +import com.morpho.app.model.bluesky.BskyPostReason +import com.morpho.app.model.bluesky.BskyPostThread +import com.morpho.app.model.bluesky.FeedGenerator +import com.morpho.app.model.bluesky.MorphoDataItem +import com.morpho.app.model.bluesky.Profile +import com.morpho.app.model.bluesky.ThreadPost import com.morpho.app.model.uistate.FeedType -import com.morpho.butterfly.* +import com.morpho.butterfly.AtIdentifier +import com.morpho.butterfly.AtUri +import com.morpho.butterfly.Cid +import com.morpho.butterfly.Did +import com.morpho.butterfly.Handle import dev.icerock.moko.parcelize.Parcelable import dev.icerock.moko.parcelize.Parcelize import kotlinx.collections.immutable.toPersistentList @@ -100,7 +118,7 @@ data class MorphoData( data: MorphoData, uri: AtUri = data.uri, title: String = data.title, - api: Butterfly? = null, + api: MorphoAgent? = null, ): Flow> = flow { val newItems = fromFeed( feed.toList(), AtCursor(responseCursor, 0), @@ -324,7 +342,7 @@ data class MorphoData( depth: Int = 3, height: Int = 80, timeRange: Delta = Delta(Duration.parse("4h")), repliesBumpThreads: Boolean = !isProfileFeed, - api: Butterfly? = null, // allows to just use local data + api: MorphoAgent? = null, // allows to just use local data ): Flow> = flow { val threads = mutableListOf() val replies = mutableListOf() @@ -542,9 +560,9 @@ data class MorphoData( } -fun AtUri.id(api:Butterfly): AtIdentifier { +fun AtUri.id(api:MorphoAgent): AtIdentifier { val idString = atUri.substringAfter("at://").split("/")[0] - return if (idString == "me") api.atpUser!!.id else { + return if (idString == "me") api.id!! else { // TODO: make this resolve a handle to a DID if (idString.contains("did:")) Did(idString) else Handle(idString) } diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/base/BaseScreenModel.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/base/BaseScreenModel.kt index 4a04551..5c284fb 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/base/BaseScreenModel.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/base/BaseScreenModel.kt @@ -8,6 +8,7 @@ import app.cash.paging.cachedIn import cafe.adriel.voyager.core.model.ScreenModel import cafe.adriel.voyager.core.model.screenModelScope import com.morpho.app.data.MorphoAgent +import com.morpho.app.di.UpdateTick import com.morpho.app.model.bluesky.BskyPost import com.morpho.app.model.bluesky.NotificationsSource import com.morpho.app.model.bluesky.toPost @@ -23,8 +24,10 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.emitAll import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch import org.koin.core.component.KoinComponent @@ -57,8 +60,11 @@ open class BaseScreenModel : ScreenModel, KoinComponent { val log = logging() } + private val notificationsTick = UpdateTick(10000) init { - + screenModelScope.launch { + notificationsTick.tick(true) + } } fun sendGlobalEvent(event: Event) { @@ -97,9 +103,12 @@ open class BaseScreenModel : ScreenModel, KoinComponent { } } - fun unreadNotificationsCount() = flow { - emit(agent.unreadNotificationsCount().getOrDefault(0)) - }.stateIn(screenModelScope, SharingStarted.WhileSubscribed(), 0L) + + + fun unreadNotificationsCount() = notificationsTick.t.map { + agent.unreadNotificationsCount().getOrDefault(0) + }.distinctUntilChanged() + .stateIn(screenModelScope, SharingStarted.WhileSubscribed(), 0L) fun updateSeenNotifications() = screenModelScope.launch { agent.updateSeenNotifications() diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/base/tabbed/NavigationTabs.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/base/tabbed/NavigationTabs.kt index ca5b998..6dcbca3 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/base/tabbed/NavigationTabs.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/base/tabbed/NavigationTabs.kt @@ -33,6 +33,8 @@ import com.morpho.app.screens.main.tabbed.TabbedHomeView import com.morpho.app.screens.main.tabbed.TabbedMainScreenModel import com.morpho.app.screens.notifications.NotificationViewContent import com.morpho.app.screens.profile.TabbedProfileContent +import com.morpho.app.screens.settings.SettingsRootPage +import com.morpho.app.screens.settings.SettingsScreenTransition import com.morpho.app.screens.thread.ThreadTopBar import com.morpho.app.screens.thread.ThreadViewContent import com.morpho.app.ui.common.LoadingCircle @@ -333,22 +335,34 @@ data object MyProfileTab: TabScreen { } -data object SettingsTab : TabScreen { - override val key: ScreenKey = "SettingsTab${uniqueScreenKey}" +data object SettingsTab: TabScreen { + override val key: ScreenKey + get() = "SettingsTab${uniqueScreenKey}" override val navBar: @Composable (@Contextual Navigator) -> Unit = { n -> - TabbedNavBar(MyProfileTab.options.index, n) + TabbedNavBar(options.index, n) } @Composable override fun Content() { - LoadingCircle() + val sm = LocalNavigator.currentOrThrow.rememberNavigatorScreenModel { TabbedMainScreenModel() } + val navigator = LocalNavigator.currentOrThrow + Navigator( + SettingsRootPage, + ){ nav -> + SettingsScreenTransition( + navigator = nav, + sm = sm, + parentNav = navigator, + modifier = Modifier + ) + } } override val options: TabScreenOptions @Composable get() { return TabScreenOptions( - index = 5, + index = 6, icon = { Icon(Icons.Default.Settings, contentDescription = "Settings", tint = MaterialTheme.colorScheme.onBackground) }, title = "Settings" diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/base/tabbed/TabbedBaseScreen.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/base/tabbed/TabbedBaseScreen.kt index f597403..b008853 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/base/tabbed/TabbedBaseScreen.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/base/tabbed/TabbedBaseScreen.kt @@ -33,12 +33,15 @@ import cafe.adriel.voyager.navigator.tab.TabOptions import com.morpho.app.screens.main.tabbed.TabbedMainScreenModel import com.morpho.app.ui.common.SlideTabTransition import com.morpho.app.ui.theme.roundedTopR +import dev.icerock.moko.parcelize.Parcelable +import dev.icerock.moko.parcelize.Parcelize import io.ktor.util.reflect.instanceOf import kotlinx.serialization.Serializable import kotlin.math.min +@Parcelize @Serializable -data object TabbedBaseScreen: Tab { +data object TabbedBaseScreen: Tab, Parcelable { override val key: ScreenKey = "TabbedBaseScreen_${hashCode()}" @@ -48,7 +51,7 @@ data object TabbedBaseScreen: Tab { Navigator( HomeTab("startHome"), disposeBehavior = NavigatorDisposeBehavior( - disposeNestedNavigators = false, + disposeNestedNavigators = false ) ) { navigator -> SlideTabTransition(navigator) diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/login/LoginScreen.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/login/LoginScreen.kt index 59ded8c..173db39 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/login/LoginScreen.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/login/LoginScreen.kt @@ -39,20 +39,20 @@ import cafe.adriel.voyager.navigator.currentOrThrow import cafe.adriel.voyager.navigator.tab.LocalTabNavigator import cafe.adriel.voyager.navigator.tab.Tab import cafe.adriel.voyager.navigator.tab.TabOptions -import com.morpho.app.CommonParcelable -import com.morpho.app.CommonParcelize import com.morpho.app.model.uistate.AuthState import com.morpho.app.screens.base.tabbed.TabbedBaseScreen import com.morpho.app.ui.common.LoadingCircle +import dev.icerock.moko.parcelize.Parcelable +import dev.icerock.moko.parcelize.Parcelize import kotlinx.coroutines.launch import kotlinx.serialization.Serializable import morpho.composeapp.generated.resources.BlueSkyKawaii import morpho.composeapp.generated.resources.Res import org.jetbrains.compose.resources.painterResource -@CommonParcelize +@Parcelize @Serializable -data object LoginScreen: Tab, CommonParcelable { +data object LoginScreen: Tab, Parcelable { override val key: ScreenKey = hashCode().toString() + "TabbedLoginScreen" diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/main/MainScreenModel.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/main/MainScreenModel.kt index ae4eca4..39b8dfd 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/main/MainScreenModel.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/main/MainScreenModel.kt @@ -42,7 +42,6 @@ open class MainScreenModel: BaseScreenModel() { init { if(isLoggedIn) screenModelScope.launch { - agent.getPreferences() userProfile = userDid?.let { agent.getProfile(it).getOrNull()?.toProfile() } feedSources.addAll(pinnedFeeds.mapNotNull { feed -> feed.toFeedSourceInfo(agent).getOrNull() }) feedPresenters.putAll(feedSources.map { source -> diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/main/tabbed/TabbedHomeView.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/main/tabbed/TabbedHomeView.kt index af31ddd..337e35e 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/main/tabbed/TabbedHomeView.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/main/tabbed/TabbedHomeView.kt @@ -145,6 +145,7 @@ fun TabScreen.TabbedHomeView( if (tabsCreated) { Navigator( tabs.first(), + key = "homeFeedsNavigator", disposeBehavior = NavigatorDisposeBehavior( //disposeNestedNavigators = false, ) diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/profile/TabbedProfileView.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/profile/TabbedProfileView.kt index 666e5aa..034a511 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/profile/TabbedProfileView.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/profile/TabbedProfileView.kt @@ -173,6 +173,7 @@ fun TabScreen.TabbedProfileContent( TabNavigator( tab = tabs.first(), disposeNestedNavigators = true, + key = "profileTabsNavigator", tabDisposable = { TabDisposable(navigator = it, tabs = tabs) } ) { TabbedProfileScreenScaffold( @@ -201,7 +202,7 @@ fun TabScreen.TabbedProfileContent( }, tabIndex = selectedTabIndex, ) - }else LoadingCircle() + } else LoadingCircle() }, content = { insets, state -> CurrentProfileScreen(eventCallback, insets, state, Modifier) diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/settings/TabbedSettingsView.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/settings/TabbedSettingsView.kt index 031816b..da80900 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/settings/TabbedSettingsView.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/settings/TabbedSettingsView.kt @@ -1,2 +1,408 @@ package com.morpho.app.screens.settings +import androidx.compose.animation.core.FiniteAnimationSpec +import androidx.compose.animation.core.Spring +import androidx.compose.animation.core.VisibilityThreshold +import androidx.compose.animation.core.spring +import androidx.compose.animation.slideInHorizontally +import androidx.compose.animation.slideOutHorizontally +import androidx.compose.animation.togetherWith +import androidx.compose.foundation.layout.padding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ArrowBackIosNew +import androidx.compose.material3.CenterAlignedTopAppBar +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.IntOffset +import cafe.adriel.voyager.core.annotation.ExperimentalVoyagerApi +import cafe.adriel.voyager.core.screen.Screen +import cafe.adriel.voyager.core.screen.ScreenKey +import cafe.adriel.voyager.core.screen.uniqueScreenKey +import cafe.adriel.voyager.core.stack.StackEvent +import cafe.adriel.voyager.navigator.LocalNavigator +import cafe.adriel.voyager.navigator.Navigator +import cafe.adriel.voyager.navigator.currentOrThrow +import cafe.adriel.voyager.transitions.ScreenTransition +import cafe.adriel.voyager.transitions.ScreenTransitionContent +import com.morpho.app.screens.base.tabbed.SettingsTab +import com.morpho.app.screens.base.tabbed.TabbedNavBar +import com.morpho.app.screens.main.tabbed.TabbedMainScreenModel +import com.morpho.app.ui.common.TabbedScreenScaffold +import com.morpho.app.ui.settings.AccessibilitySettings +import com.morpho.app.ui.settings.AppearanceSettings +import com.morpho.app.ui.settings.FeedPreferences +import com.morpho.app.ui.settings.LanguageSettings +import com.morpho.app.ui.settings.ModerationSettingsFragment +import com.morpho.app.ui.settings.SettingsFragment +import dev.icerock.moko.parcelize.Parcelable +import dev.icerock.moko.parcelize.Parcelize +import kotlinx.serialization.Contextual +import kotlinx.serialization.Serializable + +@Composable +public fun CurrentSettingsScreen( + sm: TabbedMainScreenModel, + parentNav: Navigator = LocalNavigator.currentOrThrow, + modifier: Modifier +) { + val navigator = LocalNavigator.currentOrThrow + val currentScreen = navigator.lastItem as SettingsScreen + + navigator.saveableState("currentScreen") { + currentScreen.Content( + sm = sm, + parentNav = parentNav, + modifier = modifier + ) + } +} + + +abstract class SettingsScreen: Screen { + open val title: String = "Settings" + + val navBar: @Composable (@Contextual Navigator) -> Unit = { n -> + TabbedNavBar(SettingsTab.options.index, n) + } + + @Composable + abstract fun Content( + sm: TabbedMainScreenModel, + parentNav: Navigator, + modifier: Modifier + ) + + @OptIn(ExperimentalVoyagerApi::class) + @Composable + final override fun Content() = + Content(TabbedMainScreenModel(), LocalNavigator.currentOrThrow, Modifier) +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun SettingsTopBar( + title: String = "Settings", + navigator: Navigator = LocalNavigator.currentOrThrow +) { + CenterAlignedTopAppBar( + title = { Text(title) }, + navigationIcon = { + IconButton(onClick = { navigator.pop() }) { + Icon(Icons.Default.ArrowBackIosNew, contentDescription = "Back") + } + } + ) +} + +@Parcelize +@Serializable +data object SettingsRootPage: SettingsScreen(), Parcelable { + override val title: String = "Settings" + + @OptIn(ExperimentalMaterial3Api::class, ExperimentalVoyagerApi::class) + @Composable + override fun Content( + sm: TabbedMainScreenModel, + parentNav: Navigator, + modifier: Modifier + ) { + val navigator = LocalNavigator.currentOrThrow + TabbedScreenScaffold( + navBar = { navBar(parentNav) }, + content = { insets, nav -> + SettingsFragment( + agent = sm.agent, + modifier = Modifier.padding(insets), + navigator = nav!! + ) + }, + topContent = { + SettingsTopBar(title = title, navigator = navigator) + }, + state = navigator, + modifier = modifier, + ) + } + + override val key: ScreenKey + get() = "SettingsRootPage_$uniqueScreenKey" +} + +@Parcelize +@Serializable +data object AccessibilitySettingsScreen: SettingsScreen(), Parcelable { + override val title: String = "Accessibility" + + @OptIn(ExperimentalMaterial3Api::class, ExperimentalVoyagerApi::class) + @Composable + override fun Content( + sm: TabbedMainScreenModel, + parentNav: Navigator, + modifier: Modifier + ) { + val navigator = LocalNavigator.currentOrThrow + TabbedScreenScaffold( + navBar = { navBar(parentNav) }, + content = { insets, _ -> + AccessibilitySettings( + agent = sm.agent, + modifier = Modifier.padding(insets), + distinguish = false, + ) + }, + topContent = { + SettingsTopBar(title = title, navigator = navigator) + }, + state = navigator, + modifier = modifier, + ) + } + + override val key: ScreenKey + get() = "AccessibilitySettings_$uniqueScreenKey" + +} + +@Parcelize +@Serializable +data object AppearanceSettingsScreen: SettingsScreen(), Parcelable { + override val title: String = "Appearance" + + @OptIn(ExperimentalMaterial3Api::class, ExperimentalVoyagerApi::class) + @Composable + override fun Content( + sm: TabbedMainScreenModel, + parentNav: Navigator, + modifier: Modifier + ) { + val navigator = LocalNavigator.currentOrThrow + TabbedScreenScaffold( + navBar = { navBar(parentNav) }, + content = { insets, _ -> + AppearanceSettings( + agent = sm.agent, + modifier = Modifier.padding(insets), + distinguish = false, + ) + }, + topContent = { + SettingsTopBar(title = title, navigator = navigator) + }, + state = navigator, + modifier = modifier, + ) + } + + override val key: ScreenKey + get() = "AppearanceSettings_$uniqueScreenKey" +} + +@Parcelize +@Serializable +data object NotificationsSettingsScreen: SettingsScreen(), Parcelable { + override val title: String = "Notifications" + + @OptIn(ExperimentalMaterial3Api::class, ExperimentalVoyagerApi::class) + @Composable + override fun Content( + sm: TabbedMainScreenModel, + parentNav: Navigator, + modifier: Modifier + ) { + val navigator = LocalNavigator.currentOrThrow + TabbedScreenScaffold( + navBar = { navBar(parentNav) }, + content = { insets, nav -> + SettingsFragment( + agent = sm.agent, + modifier = Modifier.padding(insets), + navigator = nav!! + ) + }, + topContent = { + SettingsTopBar(title = title, navigator = navigator) + }, + state = navigator, + modifier = modifier, + ) + } + + override val key: ScreenKey + get() = "NotificationsSettings_$uniqueScreenKey" +} + +@OptIn(ExperimentalVoyagerApi::class) +@Composable +fun SettingsScreenTransition( + navigator: Navigator, + sm: TabbedMainScreenModel, + parentNav: Navigator, + modifier: Modifier, + animationSpec: FiniteAnimationSpec = spring( + stiffness = Spring.StiffnessMediumLow, + visibilityThreshold = IntOffset.VisibilityThreshold + ), + content: ScreenTransitionContent = { + CurrentSettingsScreen(sm, parentNav, modifier) + } +) { + ScreenTransition( + navigator = navigator, + modifier = modifier, + content = content, + disposeScreenAfterTransitionEnd = true, + transition = { + val (initialOffset, targetOffset) = when (navigator.lastEvent) { + StackEvent.Pop -> ({ size: Int -> -size }) to ({ size: Int -> size }) + StackEvent.Replace -> ({ size: Int -> -size }) to ({ size: Int -> size }) + else -> ({ size: Int -> size }) to ({ size: Int -> -size }) + } + + slideInHorizontally(animationSpec, initialOffset) togetherWith + slideOutHorizontally(animationSpec, targetOffset) + + } + ) +} + + +@Parcelize +@Serializable +data object ModerationSettingsScreen: SettingsScreen(), Parcelable { + override val title: String = "Moderation" + + @OptIn(ExperimentalMaterial3Api::class, ExperimentalVoyagerApi::class) + @Composable + override fun Content( + sm: TabbedMainScreenModel, + parentNav: Navigator, + modifier: Modifier + ) { + val navigator = LocalNavigator.currentOrThrow + TabbedScreenScaffold( + navBar = { navBar(parentNav) }, + content = { insets, nav -> + ModerationSettingsFragment( + agent = sm.agent, + modifier = Modifier.padding(insets), + navigator = nav!! + + ) + }, + topContent = { + SettingsTopBar(title = title, navigator = navigator) + }, + state = navigator, + modifier = modifier, + ) + } + + override val key: ScreenKey + get() = "ModerationSettings_$uniqueScreenKey" +} + +@Parcelize +@Serializable +data object LanguageSettingsScreen: SettingsScreen(), Parcelable { + override val title: String = "Language" + + @OptIn(ExperimentalMaterial3Api::class, ExperimentalVoyagerApi::class) + @Composable + override fun Content( + sm: TabbedMainScreenModel, + parentNav: Navigator, + modifier: Modifier + ) { + val navigator = LocalNavigator.currentOrThrow + TabbedScreenScaffold( + navBar = { navBar(parentNav) }, + content = { insets, _ -> + LanguageSettings( + agent = sm.agent, + modifier = Modifier.padding(insets), + distinguish = false, + ) + }, + topContent = { + SettingsTopBar(title = title, navigator = navigator) + }, + state = navigator, + modifier = modifier, + ) + } + + override val key: ScreenKey + get() = "LanguageSettings_$uniqueScreenKey" +} + +@Parcelize +@Serializable +data object ThreadSettingsScreen: SettingsScreen(), Parcelable { + override val title: String = "Thread" + + @OptIn(ExperimentalMaterial3Api::class, ExperimentalVoyagerApi::class) + @Composable + override fun Content( + sm: TabbedMainScreenModel, + parentNav: Navigator, + modifier: Modifier + ) { + val navigator = LocalNavigator.currentOrThrow + TabbedScreenScaffold( + navBar = { navBar(parentNav) }, + content = { insets, nav -> + SettingsFragment( + agent = sm.agent, + modifier = Modifier.padding(insets), + navigator = nav!! + ) + }, + topContent = { + SettingsTopBar(title = title, navigator = navigator) + }, + state = navigator, + modifier = modifier, + ) + } + + override val key: ScreenKey + get() = "ThreadSettings_$uniqueScreenKey" +} + +@Parcelize +@Serializable +data object FeedSettingsScreen: SettingsScreen(), Parcelable { + override val title: String = "Feed" + + @OptIn(ExperimentalMaterial3Api::class, ExperimentalVoyagerApi::class) + @Composable + override fun Content( + sm: TabbedMainScreenModel, + parentNav: Navigator, + modifier: Modifier + ) { + val navigator = LocalNavigator.currentOrThrow + TabbedScreenScaffold( + navBar = { navBar(parentNav) }, + content = { insets, _ -> + FeedPreferences( + agent = sm.agent, + modifier = Modifier.padding(insets), + distinguish = false, + ) + }, + topContent = { + SettingsTopBar(title = title, navigator = navigator) + }, + state = navigator, + modifier = modifier, + ) + } + + override val key: ScreenKey + get() = "FeedSettings_$uniqueScreenKey" +} diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/thread/ThreadView.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/thread/ThreadView.kt index b049d90..18e092a 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/thread/ThreadView.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/thread/ThreadView.kt @@ -62,7 +62,7 @@ import org.koin.compose.getKoin @Composable fun TabScreen.ThreadViewContent( cardState: ContentCardState.PostThread, - navigator:Navigator = LocalNavigator.currentOrThrow, + navigator: Navigator = LocalNavigator.currentOrThrow, ) { val sm = navigator.rememberNavigatorScreenModel { MainScreenModel() } diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/common/NavDrawer.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/common/NavDrawer.kt index eb8a417..4f2de47 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/common/NavDrawer.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/common/NavDrawer.kt @@ -49,6 +49,7 @@ import com.morpho.app.screens.base.tabbed.HomeTab import com.morpho.app.screens.base.tabbed.MyProfileTab import com.morpho.app.screens.base.tabbed.NotificationsTab import com.morpho.app.screens.base.tabbed.SearchTab +import com.morpho.app.screens.base.tabbed.SettingsTab import com.morpho.app.screens.base.tabbed.TabScreen import com.morpho.app.screens.main.tabbed.TabbedMainScreenModel import com.morpho.app.ui.elements.AvatarShape @@ -259,4 +260,5 @@ fun ColumnScope.NavDrawerItems( }) NavDrawerItem(FeedsTab, drawerState = drawerState, navigator = navigator) NavDrawerItem(MyProfileTab, drawerState = drawerState, navigator = navigator) + NavDrawerItem(SettingsTab, drawerState = drawerState, navigator = navigator) } \ No newline at end of file diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/elements/SettingsItems.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/elements/SettingsItems.kt index 507a99f..41291b0 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/elements/SettingsItems.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/elements/SettingsItems.kt @@ -4,18 +4,24 @@ import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.material3.CardDefaults import androidx.compose.material3.ElevatedCard +import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.material3.Text +import androidx.compose.material3.VerticalDivider import androidx.compose.material3.surfaceColorAtElevation import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.RectangleShape import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp @@ -37,15 +43,18 @@ fun SettingsGroup( elevation = if (distinguish) CardDefaults.elevatedCardElevation(4.dp) else CardDefaults.elevatedCardElevation(0.dp) , modifier = modifier, - shape = MaterialTheme.shapes.small, + shape = if(distinguish) MaterialTheme.shapes.small else RectangleShape, ) { - Text( - text = title, - style = MaterialTheme.typography.titleMedium, - modifier = Modifier - .padding(12.dp) - .align(Alignment.Start) - ) + if(title.isNotBlank()) { + Text( + text = title, + style = MaterialTheme.typography.titleMedium, + modifier = Modifier + .padding(12.dp) + .align(Alignment.Start) + ) + HorizontalDivider(Modifier.padding(bottom = 4.dp)) + } content() } } @@ -56,22 +65,26 @@ fun SettingsGroup( fun ColumnScope.SettingsItem( text: AnnotatedString? = null, description: AnnotatedString? = null, - modifier: Modifier = Modifier.padding(vertical = 8.dp), + modifier: Modifier = Modifier, + spacing: Dp = 0.dp, content: @Composable (Modifier) -> Unit, ){ Surface( - modifier = modifier, - shape = MaterialTheme.shapes.small, + modifier = modifier.fillMaxWidth().padding(vertical = spacing), + shape = if(spacing > 0.dp) MaterialTheme.shapes.small else RectangleShape, color = MaterialTheme.colorScheme.surfaceColorAtElevation(2.dp), tonalElevation = 2.dp, ) { if(text != null && description == null) { Column( verticalArrangement = Arrangement.Center, - horizontalAlignment = Alignment.Start + horizontalAlignment = Alignment.Start, + modifier = Modifier.padding(horizontal = 12.dp) + .padding(end = 12.dp) ) { Text( text = text, + color = MaterialTheme.colorScheme.onSurface, modifier = Modifier .padding(12.dp) .align(Alignment.Start) @@ -81,33 +94,39 @@ fun ColumnScope.SettingsItem( } else { Row( verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.SpaceBetween + horizontalArrangement = Arrangement.Start, + modifier = Modifier.padding(horizontal = 12.dp).fillMaxWidth() ) { if(description != null && text != null) { + content(Modifier.padding(horizontal = 12.dp)) + VerticalDivider(Modifier.height(40.dp)) Column( modifier = Modifier - .padding(horizontal = 12.dp) + .padding(end = 12.dp) ) { Text( text = text, - style = MaterialTheme.typography.labelSmall, + style = MaterialTheme.typography.titleSmall, + color = MaterialTheme.colorScheme.onSurface, modifier = Modifier - .padding(12.dp) + .padding(top = 12.dp, start = 12.dp, end = 12.dp) .align(Alignment.Start) ) Text( text = description, - style = MaterialTheme.typography.bodySmall, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, modifier = Modifier .padding(12.dp) .align(Alignment.Start) ) } - content(Modifier.padding(horizontal = 12.dp)) + } else { content(Modifier.padding(horizontal = 12.dp)) Text( text = description?: AnnotatedString(""), + color = MaterialTheme.colorScheme.onSurface, modifier = Modifier .padding(12.dp) ) diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/settings/AccessibilitySettings.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/settings/AccessibilitySettings.kt index 5138f66..8338cd7 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/settings/AccessibilitySettings.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/settings/AccessibilitySettings.kt @@ -1,5 +1,6 @@ package com.morpho.app.ui.settings +import androidx.compose.foundation.layout.padding import androidx.compose.material3.Switch import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue @@ -8,6 +9,7 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.unit.dp import com.morpho.app.data.AccessibilityPreferences import com.morpho.app.data.MorphoAgent import com.morpho.app.ui.elements.SettingsGroup @@ -19,18 +21,20 @@ fun AccessibilitySettings( agent: MorphoAgent = getKoin().get(), distinguish: Boolean = true, modifier: Modifier = Modifier, + topLevel: Boolean = true, ) { val morphoPrefs = agent.morphoPrefs.value SettingsGroup( - title = "Accessibility", + title = if(!topLevel) "Accessibility" else "", modifier = modifier, distinguish = distinguish, ) { SettingsGroup( title = "Alt Text", distinguish = true, + modifier = Modifier.padding(8.dp), ) { - SettingsItem( text = AnnotatedString("Require Alt Text")) { + SettingsItem( description = AnnotatedString("Require Alt Text")) { mod -> var requireAltText by remember { mutableStateOf(morphoPrefs.accessibility?.requireAltText ?: false) } @@ -42,11 +46,12 @@ fun AccessibilitySettings( agent.setAccessibilityPrefs( AccessibilityPreferences.toUpdate(requireAltText = requireAltText) ) - } + }, + modifier = mod ) } - SettingsItem( text = AnnotatedString("Display larger alt text")) { + SettingsItem( description = AnnotatedString("Display larger alt text")) { mod -> var showLargerAltText by remember { mutableStateOf(morphoPrefs.accessibility?.displayLargerAltBadge ?: false) } @@ -58,7 +63,8 @@ fun AccessibilitySettings( agent.setAccessibilityPrefs( AccessibilityPreferences.toUpdate(displayLargerAltBadge = showLargerAltText) ) - } + }, + modifier = mod ) } } @@ -66,8 +72,9 @@ fun AccessibilitySettings( SettingsGroup( title = "Sensory", distinguish = true, + modifier = Modifier.padding(8.dp), ) { - SettingsItem( text = AnnotatedString("Disable autoplay for media")) { + SettingsItem( description = AnnotatedString("Disable autoplay for media")) { mod -> var disableAutoplay by remember { mutableStateOf(morphoPrefs.accessibility?.disableAutoplay ?: false) } @@ -79,11 +86,12 @@ fun AccessibilitySettings( agent.setAccessibilityPrefs( AccessibilityPreferences.toUpdate(disableAutoplay = disableAutoplay) ) - } + }, + modifier = mod ) } - SettingsItem( text = AnnotatedString("Reduce/remove animations")) { + SettingsItem( description = AnnotatedString("Reduce/remove animations")) { mod -> var reduceMotion by remember { mutableStateOf(morphoPrefs.accessibility?.reduceMotion ?: false) } @@ -95,10 +103,11 @@ fun AccessibilitySettings( agent.setAccessibilityPrefs( AccessibilityPreferences.toUpdate(reduceMotion = reduceMotion) ) - } + }, + modifier = mod ) } - SettingsItem( text = AnnotatedString("Disable haptic feedback")) { + SettingsItem( description = AnnotatedString("Disable haptic feedback")) { mod -> var disableHaptics by remember { mutableStateOf(morphoPrefs.accessibility?.disableHaptics ?: false) } @@ -110,10 +119,11 @@ fun AccessibilitySettings( agent.setAccessibilityPrefs( AccessibilityPreferences.toUpdate(disableHaptics = disableHaptics) ) - } + }, + modifier = mod ) } - SettingsItem( text = AnnotatedString("Simplify UI")) { + SettingsItem( description = AnnotatedString("Simplify UI")) { mod -> var simpleUI by remember { mutableStateOf(morphoPrefs.accessibility?.simpleUI ?: false) } @@ -126,7 +136,8 @@ fun AccessibilitySettings( agent.setAccessibilityPrefs( AccessibilityPreferences.toUpdate(simpleUI = simpleUI) ) - } + }, + modifier = mod ) } } diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/settings/AdditionalLabelerSettings.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/settings/AdditionalLabelerSettings.kt index f06aee9..3e5d84e 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/settings/AdditionalLabelerSettings.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/settings/AdditionalLabelerSettings.kt @@ -3,7 +3,10 @@ package com.morpho.app.ui.settings import androidx.compose.foundation.clickable 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.padding +import androidx.compose.foundation.layout.width import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.ChevronRight import androidx.compose.material3.Icon @@ -14,6 +17,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import app.bsky.labeler.LabelerViewDetailed @@ -66,40 +70,37 @@ fun LabelerLink( onClick: () -> Unit = {}, ) { Surface( - modifier = modifier.padding(6.dp), + modifier = modifier, shape = MaterialTheme.shapes.medium, tonalElevation = 2.dp, ) { Row( - modifier = modifier.clickable(onClick = onClick), + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.fillMaxWidth() + .clickable(onClick = onClick).padding(12.dp), ) { OutlinedAvatar( url = labeler.creator.avatar.orEmpty(), contentDescription = "Avatar for ${labeler.creator.displayName.orEmpty()}", size = 50.dp, avatarShape = AvatarShape.Rounded, - modifier = modifier - .padding(6.dp) ) - Column { + Column( + Modifier.padding(horizontal = 12.dp) + ) { Text( text = labeler.creator.displayName.orEmpty(), - style = MaterialTheme.typography.headlineSmall, - modifier = modifier - .padding(6.dp) + style = MaterialTheme.typography.titleSmall, ) Text( - text = labeler.creator.handle.handle, - style = MaterialTheme.typography.bodySmall, - modifier = modifier - .padding(6.dp) + text = "@${labeler.creator.handle.handle}", + style = MaterialTheme.typography.bodyMedium, ) } + Spacer(modifier = Modifier.width(6.dp).weight(1F)) Icon( imageVector = Icons.Default.ChevronRight, contentDescription = "Open Labeler", - modifier = modifier - .padding(6.dp) ) } } diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/settings/AppearanceSettings.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/settings/AppearanceSettings.kt index ab6e585..4aff38d 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/settings/AppearanceSettings.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/settings/AppearanceSettings.kt @@ -1,7 +1,6 @@ package com.morpho.app.ui.settings import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.MaterialTheme import androidx.compose.material3.SegmentedButton import androidx.compose.material3.SingleChoiceSegmentedButtonRow import androidx.compose.material3.Text @@ -16,6 +15,9 @@ import com.morpho.app.data.DarkModeSetting import com.morpho.app.data.MorphoAgent import com.morpho.app.ui.elements.SettingsGroup import com.morpho.app.ui.elements.SettingsItem +import com.morpho.app.ui.theme.segmentedButtonEnd +import com.morpho.app.ui.theme.segmentedButtonMiddle +import com.morpho.app.ui.theme.segmentedButtonStart import org.koin.compose.getKoin @OptIn(ExperimentalMaterial3Api::class) @@ -23,11 +25,12 @@ import org.koin.compose.getKoin fun AppearanceSettings( agent: MorphoAgent = getKoin().get(), modifier: Modifier = Modifier, - distinguish: Boolean = true, + distinguish: Boolean = false, + topLevel: Boolean = true, ) { val morphoPrefs = agent.morphoPrefs.value SettingsGroup( - title = "Appearance", + title = if(!topLevel) "Appearance" else "", modifier = modifier, distinguish = distinguish, ) { @@ -35,14 +38,16 @@ fun AppearanceSettings( var darkMode by remember { mutableStateOf(morphoPrefs.darkMode ?: DarkModeSetting.SYSTEM) } - SingleChoiceSegmentedButtonRow { + SingleChoiceSegmentedButtonRow( + modifier = it + ) { SegmentedButton( selected = darkMode == DarkModeSetting.SYSTEM, onClick = { darkMode = DarkModeSetting.SYSTEM agent.setDarkMode(DarkModeSetting.SYSTEM) }, - shape = MaterialTheme.shapes.small, + shape = segmentedButtonStart.small, label = { Text("System") }, ) SegmentedButton( @@ -51,7 +56,7 @@ fun AppearanceSettings( darkMode = DarkModeSetting.LIGHT agent.setDarkMode(DarkModeSetting.LIGHT) }, - shape = MaterialTheme.shapes.small, + shape = segmentedButtonMiddle, label = { Text("Light") }, ) SegmentedButton( @@ -60,7 +65,7 @@ fun AppearanceSettings( darkMode = DarkModeSetting.DARK agent.setDarkMode(DarkModeSetting.DARK) }, - shape = MaterialTheme.shapes.small, + shape = segmentedButtonEnd.small, label = { Text("Dark") }, ) @@ -71,7 +76,9 @@ fun AppearanceSettings( var tabbed by remember { mutableStateOf(morphoPrefs.tabbed ?: true) } - SingleChoiceSegmentedButtonRow { + SingleChoiceSegmentedButtonRow( + modifier = it + ) { SegmentedButton( selected = tabbed, enabled = false, @@ -80,7 +87,7 @@ fun AppearanceSettings( // TODO: come back when the non-tabbed view is ready agent.setDarkMode(DarkModeSetting.DARK) }, - shape = MaterialTheme.shapes.small, + shape = segmentedButtonStart.small, label = { Text("System") }, ) SegmentedButton( @@ -91,7 +98,7 @@ fun AppearanceSettings( // TODO: come back when the non-tabbed view is ready agent.setDarkMode(DarkModeSetting.LIGHT) }, - shape = MaterialTheme.shapes.small, + shape = segmentedButtonEnd.small, label = { Text("Light") }, ) } diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/settings/BuiltinContentFilters.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/settings/BuiltinContentFilters.kt index 7dfcd88..5bfde3e 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/settings/BuiltinContentFilters.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/settings/BuiltinContentFilters.kt @@ -1,6 +1,8 @@ package com.morpho.app.ui.settings import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.MaterialTheme import androidx.compose.material3.SegmentedButton @@ -14,10 +16,15 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.unit.dp import app.bsky.actor.Visibility import com.morpho.app.data.MorphoAgent import com.morpho.app.ui.elements.SettingsGroup import com.morpho.app.ui.elements.SettingsItem +import com.morpho.app.ui.theme.segmentedButtonEnd +import com.morpho.app.ui.theme.segmentedButtonMiddle +import com.morpho.app.ui.theme.segmentedButtonStart import com.morpho.butterfly.InterpretedLabelDefinition import com.morpho.butterfly.localize import org.koin.compose.getKoin @@ -43,7 +50,7 @@ fun BuiltinContentFilters( ) { SettingsItem( - text = AnnotatedString("Enable adult content") + description = AnnotatedString("Enable adult content") ) { Switch( @@ -70,16 +77,16 @@ fun BuiltinContentFilters( } ) BuiltinContentFilterSelector( - labelDefinition = com.morpho.butterfly.Sexual.localize(agent.myLanguage.value), - initialFilter = modPrefs.labels[com.morpho.butterfly.Sexual.identifier] ?: - com.morpho.butterfly.Sexual.defaultSetting, + labelDefinition = com.morpho.butterfly.NSFW.localize(agent.myLanguage.value), + initialFilter = modPrefs.labels[com.morpho.butterfly.NSFW.identifier] ?: + com.morpho.butterfly.NSFW.defaultSetting, onSelected = { visibility -> modPrefs = modPrefs.copy( labels = modPrefs.labels.toMutableMap().apply { - this[com.morpho.butterfly.Sexual.identifier] = visibility + this[com.morpho.butterfly.NSFW.identifier] = visibility } ) - agent.setContentLabelPref(com.morpho.butterfly.Sexual.identifier, visibility) + agent.setContentLabelPref(com.morpho.butterfly.NSFW.identifier, visibility) } ) BuiltinContentFilterSelector( @@ -109,7 +116,7 @@ fun BuiltinContentFilters( agent.setContentLabelPref(com.morpho.butterfly.Nudity.identifier, visibility) } ) - + Spacer(modifier = Modifier.height(6.dp)) } } @@ -125,20 +132,35 @@ fun ColumnScope.BuiltinContentFilterSelector( modifier: Modifier = Modifier, ) { var setting by remember { mutableStateOf(initialFilter) } + val text = buildAnnotatedString { + pushStyle(MaterialTheme.typography.titleSmall.toSpanStyle().copy( + color = MaterialTheme.colorScheme.onSurface + )) + append("${labelDefinition.localizedName}\n") + pop() + pushStyle(MaterialTheme.typography.bodyMedium.toSpanStyle().copy( + color = MaterialTheme.colorScheme.onSurfaceVariant + )) + append(labelDefinition.localizedDescription) + pop() + + toAnnotatedString() + } SettingsItem( - text = AnnotatedString(labelDefinition.localizedName), - description = AnnotatedString(labelDefinition.localizedDescription), + text = text, modifier = modifier ) { - SingleChoiceSegmentedButtonRow { + SingleChoiceSegmentedButtonRow( + modifier = it + ) { SegmentedButton( selected = setting == Visibility.SHOW || setting == Visibility.IGNORE, onClick = { setting = Visibility.SHOW onSelected(Visibility.SHOW) }, - shape = MaterialTheme.shapes.small, + shape = segmentedButtonStart.small, label = { Text(text = "Show") } ) SegmentedButton( @@ -147,7 +169,7 @@ fun ColumnScope.BuiltinContentFilterSelector( setting = Visibility.WARN onSelected(Visibility.WARN) }, - shape = MaterialTheme.shapes.small, + shape = segmentedButtonMiddle, label = { Text(text = "Warn") } ) SegmentedButton( @@ -156,7 +178,7 @@ fun ColumnScope.BuiltinContentFilterSelector( setting = Visibility.HIDE onSelected(Visibility.HIDE) }, - shape = MaterialTheme.shapes.small, + shape = segmentedButtonEnd.small, label = { Text(text = "Hide") } ) } diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/settings/FeedPreferences.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/settings/FeedPreferences.kt index f0bcf2b..2318062 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/settings/FeedPreferences.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/settings/FeedPreferences.kt @@ -1,7 +1,12 @@ package com.morpho.app.ui.settings +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.CheckCircle +import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Switch import androidx.compose.material3.Text @@ -9,6 +14,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.unit.dp @@ -16,6 +22,7 @@ import app.bsky.actor.FeedViewPref import com.morpho.app.data.MorphoAgent import com.morpho.app.ui.elements.SettingsGroup import com.morpho.app.ui.elements.SettingsItem +import com.morpho.app.ui.elements.WrappedColumn import org.koin.compose.getKoin @Composable @@ -23,6 +30,7 @@ fun FeedPreferences( agent: MorphoAgent = getKoin().get(), modifier: Modifier = Modifier, distinguish: Boolean = true, + topLevel: Boolean = true, ) { val feedPrefs = agent.prefs.feedView ?: FeedViewPref( feed = "following", @@ -34,127 +42,196 @@ fun FeedPreferences( lab_mergeFeedEnabled = true, ) SettingsGroup( - title = "Following Feed Preferences", + title = if(!topLevel) "Following Feed Preferences" else "", modifier = modifier, distinguish = distinguish, ) { - SettingsItem( - text = AnnotatedString("Show replies"), - description = AnnotatedString("Show any replies in the following feed at all?"), - ) { - var showReplies by mutableStateOf(feedPrefs.hideReplies != true) - Row { - Switch( - checked = showReplies, - onCheckedChange = { - showReplies = it - agent.setFeedViewPrefs( - feed = "following", - feedViewPref = feedPrefs.copy(hideReplies = !showReplies) - ) - } - ) - Text( - text = if(showReplies) "Show" else "Hide", - style = MaterialTheme.typography.bodyMedium, - modifier = Modifier.padding(start = 8.dp) - ) - } - } - if(feedPrefs.hideReplies != true) { + WrappedColumn { SettingsItem( - text = AnnotatedString("Show replies by unfollowed"), - description = AnnotatedString("Show replies by people you don't follow, but who are replying to people you do follow?"), + text = AnnotatedString("Show replies"), + description = AnnotatedString("Show any replies in the following feed at all?"), ) { - var showRepliesByUnfollowed by mutableStateOf(feedPrefs.hideRepliesByUnfollowed != true) - Row { + var showReplies by mutableStateOf(feedPrefs.hideReplies != true) + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Start, + modifier = it + ) { Switch( - checked = showRepliesByUnfollowed, + checked = showReplies, + thumbContent = { + if (showReplies) { + Icon( + Icons.Default.CheckCircle, + contentDescription = null, + modifier = Modifier.size(20.dp) + ) + } + }, onCheckedChange = { - showRepliesByUnfollowed = it + showReplies = it agent.setFeedViewPrefs( feed = "following", - feedViewPref = feedPrefs.copy(hideRepliesByUnfollowed = !showRepliesByUnfollowed) + feedViewPref = feedPrefs.copy(hideReplies = !showReplies) ) } ) Text( - text = if(showRepliesByUnfollowed) "Show" else "Hide", + text = if (showReplies) "Show" else "Hide", style = MaterialTheme.typography.bodyMedium, modifier = Modifier.padding(start = 8.dp) ) } } - } - SettingsItem( - text = AnnotatedString("Show reposts"), - description = AnnotatedString("Show reposts in the following feed?"), - ) { - var showReposts by mutableStateOf(feedPrefs.hideReposts != true) - Row { - Switch( - checked = showReposts, - onCheckedChange = { - showReposts = it - agent.setFeedViewPrefs( - feed = "following", - feedViewPref = feedPrefs.copy(hideReposts = !showReposts) + if (feedPrefs.hideReplies != true) { + SettingsItem( + text = AnnotatedString("Show replies by unfollowed"), + description = AnnotatedString("Show replies by people you don't follow, but who are replying to people you do follow?"), + ) { + var showRepliesByUnfollowed by mutableStateOf(feedPrefs.hideRepliesByUnfollowed != true) + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Start, + modifier = it + ) { + Switch( + checked = showRepliesByUnfollowed, + thumbContent = { + if (showRepliesByUnfollowed) { + Icon( + Icons.Default.CheckCircle, + contentDescription = null, + modifier = Modifier.size(20.dp) + ) + } + }, + onCheckedChange = { + showRepliesByUnfollowed = it + agent.setFeedViewPrefs( + feed = "following", + feedViewPref = feedPrefs.copy(hideRepliesByUnfollowed = !showRepliesByUnfollowed) + ) + } ) - } - ) - Text( - text = if(showReposts) "Show" else "Hide", - style = MaterialTheme.typography.bodyMedium, - modifier = Modifier.padding(start = 8.dp) - ) - } - } - SettingsItem( - text = AnnotatedString("Show quote posts"), - description = AnnotatedString( - "Show quote posts in the following feed? (reposts will still be visible, if set to show)" - ), - ) { - var showQuotePosts by mutableStateOf(feedPrefs.hideQuotePosts != true) - Row { - Switch( - checked = showQuotePosts, - onCheckedChange = { - showQuotePosts = it - agent.setFeedViewPrefs( - feed = "following", - feedViewPref = feedPrefs.copy(hideQuotePosts = !showQuotePosts) + Text( + text = if (showRepliesByUnfollowed) "Show" else "Hide", + style = MaterialTheme.typography.bodyMedium, + modifier = Modifier.padding(start = 8.dp) ) } - ) - Text( - text = if(showQuotePosts) "Show" else "Hide", - style = MaterialTheme.typography.bodyMedium, - modifier = Modifier.padding(start = 8.dp) - ) + } } - } - SettingsItem( - text = AnnotatedString("Merge feeds into Following"), - description = AnnotatedString("Occasionally show posts from your saved feeds in your following feed?"), - ) { - var mergeFeeds by mutableStateOf(feedPrefs.lab_mergeFeedEnabled != null) - Row { - Switch( - checked = mergeFeeds, - onCheckedChange = { - mergeFeeds = it - agent.setFeedViewPrefs( - feed = "following", - feedViewPref = feedPrefs.copy(lab_mergeFeedEnabled = mergeFeeds) - ) - } - ) - Text( - text = if(mergeFeeds) "Yes" else "No", - style = MaterialTheme.typography.bodyMedium, - modifier = Modifier.padding(start = 8.dp) - ) + SettingsItem( + text = AnnotatedString("Show reposts"), + description = AnnotatedString("Show reposts in the following feed?"), + ) { + var showReposts by mutableStateOf(feedPrefs.hideReposts != true) + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Start, + modifier = it + ) { + Switch( + checked = showReposts, + thumbContent = { + if (showReposts) { + Icon( + Icons.Default.CheckCircle, + contentDescription = null, + modifier = Modifier.size(20.dp) + ) + } + }, + onCheckedChange = { + showReposts = it + agent.setFeedViewPrefs( + feed = "following", + feedViewPref = feedPrefs.copy(hideReposts = !showReposts) + ) + } + ) + Text( + text = if (showReposts) "Show" else "Hide", + style = MaterialTheme.typography.bodyMedium, + modifier = Modifier.padding(start = 8.dp) + ) + } + } + SettingsItem( + text = AnnotatedString("Show quote posts"), + description = AnnotatedString( + "Show quote posts in the following feed? (reposts will still be visible, if set to show)" + ), + ) { + var showQuotePosts by mutableStateOf(feedPrefs.hideQuotePosts != true) + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Start, + modifier = it + ) { + Switch( + checked = showQuotePosts, + thumbContent = { + if (showQuotePosts) { + Icon( + Icons.Default.CheckCircle, + contentDescription = null, + modifier = Modifier.size(20.dp) + ) + } + }, + onCheckedChange = { + showQuotePosts = it + agent.setFeedViewPrefs( + feed = "following", + feedViewPref = feedPrefs.copy(hideQuotePosts = !showQuotePosts) + ) + } + ) + Text( + text = if (showQuotePosts) "Show" else "Hide", + style = MaterialTheme.typography.bodyMedium, + modifier = Modifier.padding(start = 8.dp) + ) + } + } + SettingsItem( + text = AnnotatedString("Merge feeds into Following"), + description = AnnotatedString("Occasionally show posts from your saved feeds in your following feed?"), + ) { + var mergeFeeds by mutableStateOf(feedPrefs.lab_mergeFeedEnabled != null) + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Start, + modifier = it + ) { + + Switch( + checked = mergeFeeds, + thumbContent = { + if (mergeFeeds) { + Icon( + Icons.Default.CheckCircle, + contentDescription = null, + modifier = Modifier.size(20.dp) + ) + } + }, + onCheckedChange = { + mergeFeeds = it + agent.setFeedViewPrefs( + feed = "following", + feedViewPref = feedPrefs.copy(lab_mergeFeedEnabled = mergeFeeds) + ) + } + ) + Text( + text = if (mergeFeeds) "Yes" else "No", + style = MaterialTheme.typography.bodyMedium, + modifier = Modifier.padding(horizontal = 8.dp) + ) + + } } } } diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/settings/LanguageSettings.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/settings/LanguageSettings.kt index 9c4dcbd..42a0251 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/settings/LanguageSettings.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/settings/LanguageSettings.kt @@ -1,6 +1,6 @@ package com.morpho.app.ui.settings -import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer @@ -15,6 +15,7 @@ import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.DropdownMenu import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.material3.surfaceColorAtElevation @@ -39,14 +40,15 @@ fun LanguageSettings( agent: MorphoAgent = getKoin().get(), modifier: Modifier = Modifier, distinguish: Boolean = true, + topLevel: Boolean = true, ) { val morphoPrefs = agent.morphoPrefs.value SettingsGroup( - title = "Language Settings", + title = if(!topLevel) "Language Settings" else "", modifier = modifier, distinguish = distinguish, ) { - SettingsItem(text = AnnotatedString("AppLanguage")) { + SettingsItem(text = AnnotatedString("App Language")) { LanguageDropDownMenu( onSelected = { lang -> agent.setUILanguage(lang) @@ -63,7 +65,7 @@ fun LanguageDropDownMenu( initialLanguage: Language, expandedInitially: Boolean = false, ) { - Box(Modifier.height(300.dp).fillMaxWidth()) { + Box(Modifier.height(100.dp).fillMaxWidth()) { val shape = MaterialTheme.shapes.medium var expanded by remember { mutableStateOf(expandedInitially) } var language by remember { mutableStateOf(initialLanguage) } @@ -75,17 +77,24 @@ fun LanguageDropDownMenu( Button( onClick = { expanded = !expanded }, shape = shape, - colors = ButtonDefaults.buttonColors( - containerColor = MaterialTheme.colorScheme.surfaceColorAtElevation(1.dp) - ) + colors = ButtonDefaults.filledTonalButtonColors( + containerColor = MaterialTheme.colorScheme.surfaceColorAtElevation(12.dp) + ), + modifier = Modifier.width(240.dp) ) { Row( verticalAlignment = Alignment.CenterVertically, - modifier = Modifier.padding(horizontal = 14.dp, vertical = 10.dp) + horizontalArrangement = Arrangement.SpaceBetween, + modifier = Modifier.padding(start = 12.dp, top = 10.dp, bottom = 10.dp) ) { - Text(initialLanguage.toLanguageName()) - Spacer(Modifier.width(4.dp)) - Image(Icons.Rounded.KeyboardArrowDown, null) + Text( + initialLanguage.toLanguageName() + ) + Spacer(Modifier.width(4.dp).weight(1f)) + Icon( + Icons.Rounded.KeyboardArrowDown, + null, + ) } } DropdownMenu(modifier = Modifier.align(Alignment.TopCenter).width(240.dp), expanded = expanded, diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/settings/ModerationSettingsFragment.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/settings/ModerationSettingsFragment.kt new file mode 100644 index 0000000..a6715e6 --- /dev/null +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/settings/ModerationSettingsFragment.kt @@ -0,0 +1,47 @@ +package com.morpho.app.ui.settings + +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import cafe.adriel.voyager.navigator.LocalNavigator +import cafe.adriel.voyager.navigator.Navigator +import cafe.adriel.voyager.navigator.currentOrThrow +import com.morpho.app.data.MorphoAgent +import com.morpho.app.ui.elements.WrappedLazyColumn +import org.koin.compose.getKoin + +@Composable +fun ModerationSettingsFragment( + agent: MorphoAgent = getKoin().get(), + modifier: Modifier = Modifier, + navigator: Navigator = LocalNavigator.currentOrThrow, +) { + WrappedLazyColumn( + modifier = modifier + ) { + item { + PersonalModSettings( + agent = agent, + distinguish = true, + navigator = navigator, + modifier = Modifier.padding(horizontal = 16.dp, vertical = 4.dp) + ) + } + item { + BuiltinContentFilters( + agent = agent, + distinguish = true, + modifier = Modifier.padding(horizontal = 16.dp, vertical = 4.dp) + ) + } + item { + AdditionalLabelerSettings( + agent = agent, + distinguish = true, + modifier = Modifier.padding(horizontal = 16.dp, vertical = 4.dp) + ) + } + + } +} \ No newline at end of file diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/settings/MutedWordsSettings.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/settings/MutedWordsSettings.kt new file mode 100644 index 0000000..35ff48d --- /dev/null +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/settings/MutedWordsSettings.kt @@ -0,0 +1,413 @@ +package com.morpho.app.ui.settings + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ExperimentalLayoutApi +import androidx.compose.foundation.layout.FlowRow +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Add +import androidx.compose.material.icons.filled.Close +import androidx.compose.material3.FilledTonalButton +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedIconButton +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.RadioButton +import androidx.compose.material3.Surface +import androidx.compose.material3.Switch +import androidx.compose.material3.Text +import androidx.compose.material3.surfaceColorAtElevation +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.runtime.toMutableStateList +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalFocusManager +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.TextFieldValue +import androidx.compose.ui.unit.dp +import app.bsky.actor.MuteTargetGroup +import app.bsky.actor.MutedWord +import com.morpho.app.data.MorphoAgent +import com.morpho.app.model.uidata.Moment +import com.morpho.app.ui.elements.WrappedColumn +import com.morpho.app.ui.elements.WrappedLazyColumn +import com.morpho.app.util.getFormattedDateTimeSince +import com.morpho.butterfly.model.Timestamp +import com.morpho.butterfly.mutedWordContent +import com.morpho.butterfly.mutedWordTag +import kotlinx.collections.immutable.persistentListOf +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch +import kotlinx.datetime.Clock +import kotlinx.datetime.Instant +import org.koin.compose.getKoin +import kotlin.time.Duration + +@OptIn(ExperimentalLayoutApi::class) +@Composable +fun MutedWordsSettings( + agent: MorphoAgent = getKoin().get(), + scope: CoroutineScope = rememberCoroutineScope(), + modifier: Modifier = Modifier, +) { + var word: TextFieldValue by remember { mutableStateOf(TextFieldValue("")) } + val focusManager = LocalFocusManager.current + var duration by remember { mutableStateOf(MuteDuration.FOREVER) } + var target by remember { mutableStateOf(MuteTargetGroup.ALL) } + var targetType by remember { mutableStateOf(mutedWordContent) } + val mutedWords = agent.prefs.modPrefs.mutedWords.toMutableStateList() + WrappedLazyColumn ( + modifier = modifier.fillMaxWidth() + ) { + val verticalPadding = 8.dp + item { + WrappedColumn( + horizontalAlignment = Alignment.Start, + modifier = Modifier.fillMaxWidth() + ) { + Text( + "Edit muted words and tags", + style = MaterialTheme.typography.titleMedium, + modifier = Modifier.padding(vertical = verticalPadding) + ) + Text( + "Posts can be muted based on their text, tags, or both. " + + "Avoid muting very common words, phrases, or tags, " + + "as this can prevent you from seeing essentially any posts.", + style = MaterialTheme.typography.bodyMedium, + modifier = Modifier.padding(vertical = verticalPadding, horizontal = 8.dp) + ) + OutlinedTextField( + value = word, + placeholder = { Text(text = "Enter a word or tag to mute") }, + onValueChange = { text: TextFieldValue -> + word = text + }, + keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done), + keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }), + modifier = Modifier.padding(vertical = verticalPadding, horizontal = 8.dp).fillMaxWidth() + ) + Text( + "Duration", + style = MaterialTheme.typography.titleSmall, + modifier = Modifier.padding(vertical = verticalPadding) + ) + FlowRow( + modifier = Modifier.fillMaxWidth().padding(vertical = verticalPadding) + ) { + MutedWordDurationSelector( + initialDuration = duration, + text = "Forever", + value = MuteDuration.FOREVER, + onSelected = { duration = it } + ) + MutedWordDurationSelector( + initialDuration = duration, + text = "1 day", + value = MuteDuration.ONE_DAY, + onSelected = { duration = it } + ) + MutedWordDurationSelector( + initialDuration = duration, + text = "1 week", + value = MuteDuration.ONE_WEEK, + onSelected = { duration = it } + ) + MutedWordDurationSelector( + initialDuration = duration, + text = "1 month", + value = MuteDuration.ONE_MONTH, + onSelected = { duration = it } + ) + + } + Text( + "Mute in:", + style = MaterialTheme.typography.titleSmall, + modifier = Modifier.padding(vertical = verticalPadding) + ) + Row( + modifier = Modifier.fillMaxWidth().padding(vertical = verticalPadding) + ) { + Surface( + shape = MaterialTheme.shapes.small, + tonalElevation = 8.dp, + //color = MaterialTheme.colorScheme.surfaceColorAtElevation(8.dp), + modifier = Modifier.padding(horizontal = 4.dp) + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.padding(horizontal = 4.dp) + ) { + RadioButton( + selected = targetType == mutedWordContent, + onClick = { + targetType = mutedWordContent + } + ) + Text( + text = "Text and Tags", + style = MaterialTheme.typography.bodyMedium, + modifier = Modifier.padding(end = 8.dp) + ) + } + } + Surface( + shape = MaterialTheme.shapes.small, + tonalElevation = 8.dp, + //color = MaterialTheme.colorScheme.surfaceColorAtElevation(8.dp), + modifier = Modifier.padding(horizontal = 4.dp) + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.padding(horizontal = 4.dp) + ) { + RadioButton( + selected = targetType == mutedWordTag, + onClick = { + targetType = mutedWordTag + } + ) + Text( + text = "Tags Only", + style = MaterialTheme.typography.bodyMedium, + modifier = Modifier.padding(end = 8.dp) + ) + } + } + } + Text( + "Additional options:", + style = MaterialTheme.typography.titleSmall, + modifier = Modifier.padding(vertical = verticalPadding) + ) + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.fillMaxWidth().padding(vertical = verticalPadding) + ){ + Switch( + checked = target == MuteTargetGroup.EXCLUDE_FOLLOWING, + onCheckedChange = { + target = if(it) MuteTargetGroup.EXCLUDE_FOLLOWING else MuteTargetGroup.ALL + } + ) + Text( + text = "Exclude users that you follow", + style = MaterialTheme.typography.bodyMedium, + modifier = Modifier.padding(horizontal = 12.dp) + ) + } + FilledTonalButton( + enabled = word.text.isNotEmpty(), + onClick = { + val now = Clock.System.now() + val expiresAt: Timestamp? = if(duration == MuteDuration.FOREVER) null + else now.plus(duration.duration) + val newWord = MutedWord( + value = word.text, + targets = if(targetType == mutedWordContent) persistentListOf( + mutedWordContent, + mutedWordTag + ) else persistentListOf(mutedWordTag), + actorTarget = target, + expiresAt = expiresAt?.toString(), + ) + mutedWords.add(newWord) + scope.launch { + agent.updateMutedWord(newWord) + } + }, + shape = MaterialTheme.shapes.medium, + modifier = Modifier.padding(verticalPadding).fillMaxWidth() + ) { + Text( + text = "Add", + style = MaterialTheme.typography.labelMedium, + ) + Icon( + imageVector = Icons.Default.Add, + contentDescription = "Add", + ) + } + HorizontalDivider(Modifier.fillMaxWidth().padding(vertical = verticalPadding)) + Text( + "Words you have muted", + style = MaterialTheme.typography.titleSmall, + modifier = Modifier.padding(vertical = verticalPadding) + ) + } + } + if(mutedWords.isEmpty()) { + item { + Surface( + shape = MaterialTheme.shapes.small, + tonalElevation = 8.dp, + //color = MaterialTheme.colorScheme.surfaceColorAtElevation(8.dp), + modifier = Modifier.fillMaxWidth().padding( verticalPadding), + ) { + Text( + text = "No muted words", + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.padding(12.dp) + ) + } + } + } else { + items(mutedWords) { + MutedWordListItem( + word = it, + onRemoveClicked = { + mutedWords.remove(it) + scope.launch { + agent.removeMutedWord(it) + + } + } + ) + } + + } + + + } +} + +@Composable +fun MutedWordListItem( + word: MutedWord, + onRemoveClicked: () -> Unit, + modifier: Modifier = Modifier, +) { + Surface( + shape = MaterialTheme.shapes.small, + tonalElevation = 8.dp, + //color = MaterialTheme.colorScheme.surfaceColorAtElevation(8.dp), + modifier = modifier.fillMaxWidth().padding(4.dp), + ) { + Row( + verticalAlignment = Alignment.Top, + modifier = Modifier.padding(4.dp) + ) { + Column { + val valueAndTargets = buildAnnotatedString { + pushStyle( + SpanStyle(fontWeight = FontWeight.Bold) + ) + append(word.value) + pop() + append(" in ") + pushStyle( + SpanStyle(fontWeight = FontWeight.SemiBold) + ) + val targetsString = if(word.targets.contains(mutedWordContent) && word.targets.contains(mutedWordTag)) { + "Text and Tags" + } else if(word.targets.contains(mutedWordContent)) { + "Text" + } else if(word.targets.contains(mutedWordTag)) { + "Tags" + } else word.targets.joinToString(", ") { it.mutedWordTarget } + append(targetsString) + pop() + + toAnnotatedString() + } + Text( + text = valueAndTargets, + style = MaterialTheme.typography.bodyMedium, + ) + val expiry = word.expiresAt?.let {Moment( Instant.parse(it)) } + val timeToExpire = if(expiry != null) { + getFormattedDateTimeSince(expiry) + } else null + val expiryAndExludes = buildAnnotatedString { + if(timeToExpire != null) { + append("Expires in ") + append(timeToExpire) + pushStyle( + SpanStyle(fontWeight = FontWeight.SemiBold) + ) + append(" - ") + pop() + } + if(word.actorTarget != MuteTargetGroup.ALL) { + append("Excludes users that you follow") + } + toAnnotatedString() + } + Text( + text = expiryAndExludes, + color = MaterialTheme.colorScheme.onSurfaceVariant, + style = MaterialTheme.typography.bodyMedium, + ) + + } + OutlinedIconButton( + onClick = onRemoveClicked, + modifier = Modifier.padding(4.dp), + ) { + Icon( + imageVector = Icons.Default.Close, + contentDescription = "Remove", + ) + } + } + } +} + +@Composable +fun MutedWordDurationSelector( + initialDuration: MuteDuration, + text: String, + value: MuteDuration, + onSelected: (MuteDuration) -> Unit, + modifier: Modifier = Modifier, +) { + var duration by remember { mutableStateOf(initialDuration) } + Surface( + shape = MaterialTheme.shapes.small, + tonalElevation = 8.dp, + color = MaterialTheme.colorScheme.surfaceColorAtElevation(8.dp), + modifier = modifier.padding(4.dp), + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.padding(horizontal = 4.dp) + ) { + RadioButton( + selected = duration == value, + onClick = { + duration = value + onSelected(value) + } + ) + Text( + text = text, + style = MaterialTheme.typography.bodyMedium, + modifier = Modifier.padding(end = 12.dp) + ) + } + } +} + +enum class MuteDuration(val duration: Duration, val text: String) { + FOREVER(Duration.INFINITE, "Forever"), + ONE_DAY(Duration.parse("24h"), "24 hours"), + ONE_WEEK(Duration.parse("7d"), "7 days"), + ONE_MONTH(Duration.parse("30d"), "30 days"), +} + diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/settings/PersonalModSettings.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/settings/PersonalModSettings.kt index e54b25e..c639832 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/settings/PersonalModSettings.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/settings/PersonalModSettings.kt @@ -1,26 +1,47 @@ package com.morpho.app.ui.settings import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.padding +import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Block import androidx.compose.material.icons.filled.FilterAlt import androidx.compose.material.icons.filled.ReduceCapacity import androidx.compose.material.icons.filled.VisibilityOff +import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon +import androidx.compose.material3.ModalBottomSheet +import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable +import androidx.compose.runtime.Immutable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.unit.dp +import cafe.adriel.voyager.navigator.LocalNavigator +import cafe.adriel.voyager.navigator.Navigator +import cafe.adriel.voyager.navigator.currentOrThrow import com.morpho.app.data.MorphoAgent import com.morpho.app.ui.elements.SettingsGroup import com.morpho.app.ui.elements.SettingsItem +import kotlinx.serialization.Serializable import org.koin.compose.getKoin +@OptIn(ExperimentalMaterialApi::class, ExperimentalMaterial3Api::class) @Composable fun PersonalModSettings( agent: MorphoAgent = getKoin().get(), modifier: Modifier = Modifier, distinguish: Boolean = true, + navigator: Navigator = LocalNavigator.currentOrThrow, ) { + val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) + val scope = rememberCoroutineScope() + var sheetOption by remember { mutableStateOf(SheetOption.Hide) } SettingsGroup( title = "Moderation tools", modifier = modifier, @@ -29,7 +50,7 @@ fun PersonalModSettings( SettingsItem( description = AnnotatedString("Muted words and tags"), modifier = Modifier.clickable { - + sheetOption = SheetOption.MuteWords } ){ Icon( @@ -74,4 +95,31 @@ fun PersonalModSettings( ) } } + if(sheetOption != SheetOption.Hide) { + ModalBottomSheet( + onDismissRequest = { + sheetOption = SheetOption.Hide + }, + sheetState = sheetState + ) { + when(sheetOption) { + SheetOption.MuteWords -> { + MutedWordsSettings( + agent = agent, + scope = scope, + modifier = Modifier.padding(16.dp) + ) + } + SheetOption.Hide -> {} + } + } + } + +} + +@Serializable +@Immutable +enum class SheetOption { + MuteWords, + Hide } \ No newline at end of file diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/settings/SettingsFragment.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/settings/SettingsFragment.kt index 0bf0db5..3a8cdfe 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/settings/SettingsFragment.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/settings/SettingsFragment.kt @@ -3,6 +3,7 @@ package com.morpho.app.ui.settings import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Column 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.material.Surface @@ -16,14 +17,26 @@ import androidx.compose.material.icons.filled.ImagesearchRoller import androidx.compose.material.icons.filled.RssFeed import androidx.compose.material.icons.filled.Translate import androidx.compose.material.icons.filled.Tune +import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.RectangleShape import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.unit.dp +import cafe.adriel.voyager.navigator.LocalNavigator +import cafe.adriel.voyager.navigator.Navigator +import cafe.adriel.voyager.navigator.currentOrThrow import com.morpho.app.data.MorphoAgent +import com.morpho.app.screens.settings.AccessibilitySettingsScreen +import com.morpho.app.screens.settings.AppearanceSettingsScreen +import com.morpho.app.screens.settings.FeedSettingsScreen +import com.morpho.app.screens.settings.LanguageSettingsScreen +import com.morpho.app.screens.settings.ModerationSettingsScreen +import com.morpho.app.screens.settings.NotificationsSettingsScreen +import com.morpho.app.screens.settings.ThreadSettingsScreen import com.morpho.app.ui.elements.SettingsItem import org.koin.compose.getKoin @@ -31,9 +44,20 @@ import org.koin.compose.getKoin fun SettingsFragment( agent: MorphoAgent = getKoin().get(), modifier: Modifier = Modifier, + navigator: Navigator = LocalNavigator.currentOrThrow, ) { - Column { - Text("Basics") + Column( + modifier = Modifier + .fillMaxWidth().then(modifier) + + ) { + Spacer(modifier = Modifier.height(8.dp)) + Text( + "Basics", + style = MaterialTheme.typography.titleSmall, + color = MaterialTheme.colorScheme.onBackground, + modifier = Modifier.padding(horizontal = 12.dp) + ) Surface( elevation = 2.dp, modifier = Modifier.padding(vertical = 8.dp) @@ -41,45 +65,59 @@ fun SettingsFragment( Column { SettingsItem( description = AnnotatedString("Accessibility"), - modifier = Modifier.clickable { } + modifier = Modifier.clickable { + navigator.push(AccessibilitySettingsScreen) + }.fillMaxWidth() ) { Icon(Icons.Default.Accessibility, contentDescription = "Accessibility") } SettingsItem( description = AnnotatedString("Appearance"), - modifier = Modifier.clickable { } + modifier = Modifier.clickable { + navigator.push(AppearanceSettingsScreen) + } ) { Icon(Icons.Default.ImagesearchRoller, contentDescription = "Appearance") } SettingsItem( description = AnnotatedString("Languages"), - modifier = Modifier.clickable { } + modifier = Modifier.clickable { + navigator.push(LanguageSettingsScreen) + } ) { Icon(Icons.Default.Translate, contentDescription = "Languages") } SettingsItem( description = AnnotatedString("Moderation"), - modifier = Modifier.clickable { } + modifier = Modifier.clickable { + navigator.push(ModerationSettingsScreen) + } ) { Icon(Icons.Default.BackHand, contentDescription = "Moderation") } SettingsItem( description = AnnotatedString("Notifications filtering"), - modifier = Modifier.clickable { } + modifier = Modifier.clickable { + navigator.push(NotificationsSettingsScreen) + } ) { Icon(Icons.Default.FilterAlt, contentDescription = "Notifications") } SettingsItem( description = AnnotatedString("Following Feed Preferences"), - modifier = Modifier.clickable { } + modifier = Modifier.clickable { + navigator.push(FeedSettingsScreen) + } ) { Icon(Icons.Default.Tune, contentDescription = "Following Feed Preferences") } SettingsItem( description = AnnotatedString("Thread Preferences"), - modifier = Modifier.clickable { } + modifier = Modifier.clickable { + navigator.push(ThreadSettingsScreen) + } ) { Icon(Icons.Default.Forum, contentDescription = "Thread Preferences") } @@ -93,7 +131,12 @@ fun SettingsFragment( } } Spacer(modifier = Modifier.height(8.dp)) - Text("Advanced") + Text( + "Advanced", + style = MaterialTheme.typography.titleSmall, + color = MaterialTheme.colorScheme.onBackground, + modifier = Modifier.padding(horizontal = 12.dp) + ) Surface( elevation = 2.dp, modifier = Modifier.padding(vertical = 8.dp) @@ -103,12 +146,21 @@ fun SettingsFragment( Spacer(modifier = Modifier.height(8.dp)) TextButton( onClick = { }, - modifier = Modifier.padding(vertical = 8.dp), + colors = ButtonDefaults.elevatedButtonColors(), + modifier = Modifier.padding(vertical = 8.dp).padding(horizontal = 12.dp), shape = RectangleShape, ) { - Text("System Log") + Text("System Log", + style = MaterialTheme.typography.titleMedium, + color = MaterialTheme.colorScheme.onBackground, + modifier = Modifier + ) } val version = com.morpho.app.BuildKonfig.versionString - Text("Version $version") + Text( + "Version $version", + modifier = Modifier.padding(horizontal = 12.dp), + color = MaterialTheme.colorScheme.onBackground + ) } } \ No newline at end of file diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/theme/Shape.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/theme/Shape.kt index f03cc54..319a3d7 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/theme/Shape.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/theme/Shape.kt @@ -3,6 +3,7 @@ package com.morpho.app.ui.theme import androidx.compose.foundation.shape.CornerSize import androidx.compose.material3.ShapeDefaults import androidx.compose.material3.Shapes +import androidx.compose.ui.graphics.RectangleShape import androidx.compose.ui.unit.dp val roundedTopLBotR = Shapes( @@ -67,4 +68,52 @@ val roundedBotR = Shapes( topStart = CornerSize(0.dp), bottomStart = CornerSize(0.dp), ) -) \ No newline at end of file +) + +val segmentedButtonMiddle = RectangleShape + +val segmentedButtonStart = Shapes( + extraSmall = ShapeDefaults.ExtraSmall.copy( + bottomEnd = CornerSize(0.dp), + topEnd = CornerSize(0.dp), + ), + small = ShapeDefaults.Small.copy( + bottomEnd = CornerSize(0.dp), + topEnd = CornerSize(0.dp), + ), + medium = ShapeDefaults.Medium.copy( + bottomEnd = CornerSize(0.dp), + topEnd = CornerSize(0.dp), + ), + large = ShapeDefaults.Large.copy( + bottomEnd = CornerSize(0.dp), + topEnd = CornerSize(0.dp), + ), + extraLarge = ShapeDefaults.ExtraLarge.copy( + bottomEnd = CornerSize(0.dp), + topEnd = CornerSize(0.dp), + ) +) + +val segmentedButtonEnd = Shapes( + extraSmall = ShapeDefaults.ExtraSmall.copy( + bottomStart = CornerSize(0.dp), + topStart = CornerSize(0.dp), + ), + small = ShapeDefaults.Small.copy( + bottomStart = CornerSize(0.dp), + topStart = CornerSize(0.dp), + ), + medium = ShapeDefaults.Medium.copy( + bottomStart = CornerSize(0.dp), + topStart = CornerSize(0.dp), + ), + large = ShapeDefaults.Large.copy( + bottomStart = CornerSize(0.dp), + topStart = CornerSize(0.dp), + ), + extraLarge = ShapeDefaults.ExtraLarge.copy( + bottomStart = CornerSize(0.dp), + topStart = CornerSize(0.dp), + ) +) diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/util/json.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/util/json.kt index ab2f59d..cc87d97 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/util/json.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/util/json.kt @@ -3,8 +3,6 @@ package com.morpho.app.util import app.bsky.actor.PreferencesUnion import kotlinx.serialization.InternalSerializationApi import kotlinx.serialization.KSerializer -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json import kotlinx.serialization.json.JsonElement @@ -14,7 +12,6 @@ import kotlinx.serialization.modules.SerializersModule import kotlinx.serialization.modules.polymorphic import kotlinx.serialization.modules.subclass import kotlinx.serialization.serializer -import kotlin.jvm.JvmInline @OptIn(InternalSerializationApi::class) val morphoSerializersModule = SerializersModule { @@ -31,19 +28,14 @@ val morphoSerializersModule = SerializersModule { subclass(PreferencesUnion.HiddenPostsPref::class) subclass(PreferencesUnion.MutedWordsPref::class) subclass(PreferencesUnion.InterestsPref::class) - subclass(MorphoPreferences::class) + subclass(PreferencesUnion.ButterflyPreference::class) + subclass(PreferencesUnion.UnknownPreference::class) defaultDeserializer { _ -> PreferencesUnion.UnknownPreference::class.serializer() } } } -@Serializable -@JvmInline -@SerialName("app.bsky.actor.defs#morphoPrefs") -value class MorphoPreferences( - val value: com.morpho.app.data.MorphoPreferences -): PreferencesUnion val json = Json { classDiscriminator = "${'$'}type" diff --git a/Morpho/gradle/libs.versions.toml b/Morpho/gradle/libs.versions.toml index cd01c1a..3ee3141 100644 --- a/Morpho/gradle/libs.versions.toml +++ b/Morpho/gradle/libs.versions.toml @@ -46,6 +46,7 @@ ktorClientAndroid = "[ktor-version]" logbackClassic = "1.5.7" logbackCore = "1.5.7" logging = "1.4.2" +logkmpanion = "1.8.0" nativeparameterstoreaccess = "0.1.0" okio = "3.9.0" slf4j-api = "2.0.13" @@ -135,6 +136,7 @@ filekit-core = { module = "io.github.vinceglb:filekit-core", version.ref = "file logback-classic = { module = "ch.qos.logback:logback-classic", version.ref = "logbackClassic" } logback-core = { module = "ch.qos.logback:logback-core", version.ref = "logbackCore" } logging = { module = "org.lighthousegames:logging", version.ref = "logging" } +logkmpanion = { module = "io.github.idfinance-oss:logkmpanion", version.ref = "logkmpanion" } nativeparameterstoreaccess = { module = "com.github.tkuenneth:nativeparameterstoreaccess", version.ref = "nativeparameterstoreaccess" } slf4j-api = { module = "org.slf4j:slf4j-api", version.ref = "slf4j-api" } oshai-kotlin-logging-jvm = { module = "io.github.oshai:kotlin-logging-jvm", version.ref = "github-kotlin-logging-jvm" } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 676553d..5362090 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -39,6 +39,7 @@ ktor = "2.3.9" logbackClassic = "1.5.7" logbackCore = "1.5.7" logging = "1.4.2" +logkmpanion = "1.8.0" nativeparameterstoreaccess = "0.1.0" okio = "3.9.0" pagingCommon = "3.3.0-alpha02-0.5.1" @@ -133,6 +134,7 @@ koin-compose = { module = "io.insert-koin:koin-compose", version.ref = "koin-com logback-classic = { module = "ch.qos.logback:logback-classic", version.ref = "logbackClassic" } logback-core = { module = "ch.qos.logback:logback-core", version.ref = "logbackCore" } logging = { module = "org.lighthousegames:logging", version.ref = "logging" } +logkmpanion = { module = "io.github.idfinance-oss:logkmpanion", version.ref = "logkmpanion" } nativeparameterstoreaccess = { module = "com.github.tkuenneth:nativeparameterstoreaccess", version.ref = "nativeparameterstoreaccess" } paging-common = { module = "app.cash.paging:paging-common", version.ref = "pagingCommon" } paging-compose-common = { module = "app.cash.paging:paging-compose-common", version.ref = "pagingComposeCommon" } From 82478891818e56db373812d14aa80a77c3d8884a Mon Sep 17 00:00:00 2001 From: Orual Date: Sun, 22 Sep 2024 20:43:09 -0400 Subject: [PATCH 32/42] Few additional fixes --- .gitignore | 1 + Butterfly | 2 +- Morpho/.gitignore | 1 + Morpho/composeApp/build.gradle.kts | 1 + .../kotlin/com/morpho/app/data/MorphoAgent.kt | 22 ++++------ .../com/morpho/app/ui/post/PostImage.kt | 22 +++++++--- .../app/ui/settings/AppearanceSettings.kt | 3 +- .../morpho/app/ui/post/PostImage.desktop.kt | 10 ++--- .../composeApp/src/desktopMain/kotlin/main.kt | 29 +++++++++---- .../src/desktopMain/resources/logback.xml | 42 +++++++++++++++++++ 10 files changed, 97 insertions(+), 36 deletions(-) create mode 100644 Morpho/composeApp/src/desktopMain/resources/logback.xml diff --git a/.gitignore b/.gitignore index f4311b2..c8a02ec 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,4 @@ Morpho/.kotlin .kotlin /.kotlin/ Butterfly +*.log \ No newline at end of file diff --git a/Butterfly b/Butterfly index 20a5f32..1bcd5c2 160000 --- a/Butterfly +++ b/Butterfly @@ -1 +1 @@ -Subproject commit 20a5f32a77326ad58c67798176159d966b111298 +Subproject commit 1bcd5c2fa312d1654801bcbf59079c6e21e69197 diff --git a/Morpho/.gitignore b/Morpho/.gitignore index 6af20c0..0e36f72 100644 --- a/Morpho/.gitignore +++ b/Morpho/.gitignore @@ -19,3 +19,4 @@ captures !*.xcodeproj/project.xcworkspace/ !*.xcworkspace/contents.xcworkspacedata **/xcshareddata/WorkspaceSettings.xcsettings +*.log diff --git a/Morpho/composeApp/build.gradle.kts b/Morpho/composeApp/build.gradle.kts index 1ce360f..7324576 100644 --- a/Morpho/composeApp/build.gradle.kts +++ b/Morpho/composeApp/build.gradle.kts @@ -198,6 +198,7 @@ kotlin { implementation(libs.koin.core.coroutines) implementation(libs.koin.annotations) implementation(libs.koin.compose) + implementation("io.insert-koin:koin-logger-slf4j:3.5.3") // Enables FileKit without Compose dependencies diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/data/MorphoAgent.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/data/MorphoAgent.kt index 04c310f..986a4d2 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/data/MorphoAgent.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/data/MorphoAgent.kt @@ -5,7 +5,6 @@ import app.bsky.actor.PreferencesUnion import app.bsky.labeler.LabelerViewDetailed import com.morpho.app.myLang import com.morpho.app.util.morphoSerializersModule -import com.morpho.butterfly.BlueskyApi import com.morpho.butterfly.BskyPreferences import com.morpho.butterfly.ButterflyAgent import com.morpho.butterfly.InterpretedLabelDefinition @@ -16,7 +15,6 @@ import com.morpho.butterfly.localize import com.morpho.butterfly.xrpc.XrpcBlueskyApi import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.flow @@ -25,21 +23,15 @@ import kotlinx.coroutines.runBlocking import org.koin.core.component.inject class MorphoAgent: ButterflyAgent() { - // Splices in the app's serializers module to handle any extensions to the type unions - override var api: BlueskyApi = XrpcBlueskyApi(atpClient, morphoSerializersModule) val localPrefs: PreferencesRepository by inject() - private val _morphoPrefs: MutableStateFlow = MutableStateFlow(MorphoPreferences( + val morphoPrefs: MutableStateFlow = MutableStateFlow(MorphoPreferences( kawaiiMode = true, notificationsFilter = NotificationsFilterPref(), accessibility = AccessibilityPreferences(), )) - private val _bskyPrefs: MutableStateFlow = MutableStateFlow(prefs) - private var _myLanguage = MutableStateFlow(Language(myLang ?: _morphoPrefs.value.uiLanguage?.tag ?: "en")) - - val morphoPrefs = _morphoPrefs.asStateFlow() - val bskyPrefs = _bskyPrefs.asStateFlow() - val myLanguage = _myLanguage.asStateFlow() + val bskyPrefs: MutableStateFlow = MutableStateFlow(prefs) + val myLanguage = MutableStateFlow(Language(myLang ?: morphoPrefs.value.uiLanguage?.tag ?: "en")) val labelersDetailed: Flow> = flow { val labelers = getLabelersDetailed(labelers).getOrNull() ?: listOf() @@ -56,7 +48,7 @@ class MorphoAgent: ButterflyAgent() { serviceScope.launch { localPrefs.morphoPrefs(id!!).distinctUntilChanged().collectLatest { if (it != null && it != MorphoPreferences()) { - _morphoPrefs.value = it + morphoPrefs.value = it } } } @@ -70,7 +62,7 @@ class MorphoAgent: ButterflyAgent() { serviceScope.launch { localPrefs.bskyPrefs(id!!).distinctUntilChanged().collectLatest { if (it != null && it != BskyPreferences()) { - _bskyPrefs.value = it + bskyPrefs.value = it } } } @@ -111,7 +103,7 @@ class MorphoAgent: ButterflyAgent() { } fun setUILanguage(language: Language) = serviceScope.launch { - _myLanguage.value = language + myLanguage.value = language updateMorphoPrefs { it.copy(uiLanguage = language) } @@ -122,7 +114,7 @@ class MorphoAgent: ButterflyAgent() { val prefs = updateFun(morphoPrefs.value) return if(prefs != null) { localPrefs.setMorphoPrefs(id!!, prefs) - _morphoPrefs.value = prefs + morphoPrefs.value = prefs Result.success(prefs) } else Result.failure(Exception("Update failed")) } diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/post/PostImage.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/post/PostImage.kt index 82f3ba6..7d827de 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/post/PostImage.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/post/PostImage.kt @@ -2,14 +2,26 @@ package com.morpho.app.ui.post import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.* +import androidx.compose.foundation.layout.BoxWithConstraints +import androidx.compose.foundation.layout.ExperimentalLayoutApi +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.requiredHeightIn +import androidx.compose.foundation.layout.requiredWidthIn import androidx.compose.foundation.lazy.staggeredgrid.LazyVerticalStaggeredGrid import androidx.compose.foundation.lazy.staggeredgrid.StaggeredGridCells import androidx.compose.foundation.lazy.staggeredgrid.items import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.text.selection.SelectionContainer import androidx.compose.foundation.verticalScroll -import androidx.compose.material3.* +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -40,7 +52,7 @@ fun PostImages( val numImages = rememberSaveable { imagesFeature.images.size} if(numImages > 1) { LazyVerticalStaggeredGrid( - columns = StaggeredGridCells.Adaptive(120.dp), + columns = StaggeredGridCells.Adaptive(150.dp), contentPadding = PaddingValues(2.dp), modifier = modifier .padding(top = 6.dp) @@ -49,7 +61,7 @@ fun PostImages( items(imagesFeature.images) {image -> PostImageThumb( image = image, - modifier = Modifier.fillMaxWidth() + modifier = Modifier.fillMaxWidth().padding(2.dp) ) } } @@ -100,11 +112,9 @@ fun PostImageThumb( if (ratio > 1) { height /= ratio height = height.roundToInt().toFloat() - width = width.roundToInt().toFloat() } else { width /= ratio width = width.roundToInt().toFloat() - height = height.roundToInt().toFloat() } AsyncImage( model = ImageRequest.Builder(LocalPlatformContext.current) diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/settings/AppearanceSettings.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/settings/AppearanceSettings.kt index 4aff38d..7e906b2 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/settings/AppearanceSettings.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/settings/AppearanceSettings.kt @@ -5,6 +5,7 @@ import androidx.compose.material3.SegmentedButton import androidx.compose.material3.SingleChoiceSegmentedButtonRow import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -28,7 +29,7 @@ fun AppearanceSettings( distinguish: Boolean = false, topLevel: Boolean = true, ) { - val morphoPrefs = agent.morphoPrefs.value + val morphoPrefs by agent.morphoPrefs.collectAsState(initial = agent.morphoPrefs.value) SettingsGroup( title = if(!topLevel) "Appearance" else "", modifier = modifier, diff --git a/Morpho/composeApp/src/desktopMain/kotlin/com/morpho/app/ui/post/PostImage.desktop.kt b/Morpho/composeApp/src/desktopMain/kotlin/com/morpho/app/ui/post/PostImage.desktop.kt index ca4816b..1a7f9ba 100644 --- a/Morpho/composeApp/src/desktopMain/kotlin/com/morpho/app/ui/post/PostImage.desktop.kt +++ b/Morpho/composeApp/src/desktopMain/kotlin/com/morpho/app/ui/post/PostImage.desktop.kt @@ -102,10 +102,10 @@ actual fun FullImageView( image = image, onDismissRequest = onDismissRequest ) - println("Image width: ${image.aspectRatio?.width} Image height: ${image.aspectRatio?.height} Ratio: $ratio") - println("Width: ${state.size.width.value.toInt()} Height: ${state.size.height.value.toInt()} Ratio: ${ - state.size.width.value.toInt().toFloat() / state.size.height.value.toInt().toFloat() - }") +// println("Image width: ${image.aspectRatio?.width} Image height: ${image.aspectRatio?.height} Ratio: $ratio") +// println("Width: ${state.size.width.value.toInt()} Height: ${state.size.height.value.toInt()} Ratio: ${ +// state.size.width.value.toInt().toFloat() / state.size.height.value.toInt().toFloat() +// }") } } else { DesktopImageViewContent( @@ -171,7 +171,7 @@ fun DesktopImageViewContent( text = image.alt, style = MaterialTheme.typography.bodyLarge, color = Color.White, - modifier = Modifier.padding(12.dp), + modifier = Modifier.padding(top= 4.dp, bottom = 12.dp, start = 12.dp, end = 12.dp), textAlign = TextAlign.Start ) } diff --git a/Morpho/composeApp/src/desktopMain/kotlin/main.kt b/Morpho/composeApp/src/desktopMain/kotlin/main.kt index 5f930eb..cbbac92 100644 --- a/Morpho/composeApp/src/desktopMain/kotlin/main.kt +++ b/Morpho/composeApp/src/desktopMain/kotlin/main.kt @@ -25,6 +25,9 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.derivedStateOf +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.shadow @@ -40,7 +43,6 @@ import androidx.compose.ui.window.application import androidx.compose.ui.window.rememberDialogState import androidx.compose.ui.window.rememberWindowState import cafe.adriel.voyager.core.annotation.ExperimentalVoyagerApi -import cafe.adriel.voyager.jetpack.ProvideNavigatorLifecycleKMPSupport import ch.qos.logback.classic.LoggerContext import ch.qos.logback.core.util.StatusPrinter2 import com.github.tkuenneth.nativeparameterstoreaccess.MacOSDefaults.getDefaultsEntry @@ -48,6 +50,7 @@ import com.github.tkuenneth.nativeparameterstoreaccess.NativeParameterStoreAcces import com.github.tkuenneth.nativeparameterstoreaccess.NativeParameterStoreAccess.IS_WINDOWS import com.github.tkuenneth.nativeparameterstoreaccess.WindowsRegistry.getWindowsRegistryEntry import com.morpho.app.App +import com.morpho.app.data.DarkModeSetting import com.morpho.app.data.MorphoAgent import com.morpho.app.data.PreferencesRepository import com.morpho.app.di.appModule @@ -65,25 +68,29 @@ import org.jetbrains.compose.resources.imageResource import org.jetbrains.compose.resources.painterResource import org.koin.core.annotation.KoinExperimentalAPI import org.koin.core.context.startKoin -import org.koin.core.logger.Level import org.koin.core.parameter.parametersOf +import org.koin.logger.slf4jLogger import org.lighthousegames.logging.KmLogging import org.lighthousegames.logging.LogLevel import org.lighthousegames.logging.PlatformLogger import org.lighthousegames.logging.VariableLogLevel import org.lighthousegames.logging.logging +import org.slf4j.Logger import org.slf4j.LoggerFactory import kotlin.io.path.createDirectories val log = logging("main") +fun getLogger(): Logger = LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME) + +fun getLogger(name: String): Logger = LoggerFactory.getLogger(name) + @OptIn(KoinExperimentalAPI::class, ExperimentalResourceApi::class, ExperimentalVoyagerApi::class) fun main() = application { - ProvideNavigatorLifecycleKMPSupport { StatusPrinter2().print(LoggerFactory.getILoggerFactory() as LoggerContext) KmLogging.setLoggers(PlatformLogger(VariableLogLevel(LogLevel.Verbose))) val koin = startKoin { - printLogger(Level.DEBUG) + slf4jLogger() modules(appModule, storageModule, dataModule) }.koin val storageDir = AppDirsFactory.getInstance() @@ -98,8 +105,8 @@ fun main() = application { koin.get { parametersOf(storageDir) } koin.get { parametersOf(storageDir) } val agent = koin.get() - val morphoPrefs = agent.morphoPrefs - val (undecorated, tabbed) = run { + val morphoPrefs by derivedStateOf { agent.morphoPrefs } + val (undecorated, tabbed) = remember { log.d{ "Morpho Preferences: $morphoPrefs" } (morphoPrefs.value.tabbed == true) to (morphoPrefs.value.undecorated == true) } @@ -122,7 +129,13 @@ fun main() = application { transparent = undecorated, icon = painterResource(Res.drawable.morpho_icon_transparent) ) { - MorphoTheme(darkTheme = isSystemInDarkTheme()) { + val darkTheme by derivedStateOf { when(morphoPrefs.value.darkMode){ + DarkModeSetting.SYSTEM -> isSystemInDarkTheme() + DarkModeSetting.LIGHT -> false + DarkModeSetting.DARK -> true + null -> isSystemInDarkTheme() + } } + MorphoTheme(darkTheme = darkTheme) { if(undecorated) { MorphoWindow( windowState = windowState, @@ -141,7 +154,7 @@ fun main() = application { } } - } + } diff --git a/Morpho/composeApp/src/desktopMain/resources/logback.xml b/Morpho/composeApp/src/desktopMain/resources/logback.xml new file mode 100644 index 0000000..062ef69 --- /dev/null +++ b/Morpho/composeApp/src/desktopMain/resources/logback.xml @@ -0,0 +1,42 @@ + + + + + + + + morpho-app-run_${bySecond}.log + + %-4relative [%thread] %-5level %logger{35} -%kvp -%msg%n + + + + + + true + + morpho-app-ongoing.%d{yyyy-MM-dd}.log + 30 + 3GB + + + + %-4relative [%thread] %-5level %logger{35} -%kvp -%msg%n + + + + + + %-4relative [%thread] %-5level %logger{35} -%kvp- %msg %n + + + + + + + + + \ No newline at end of file From 86f8ae690f81a09115f93e3bd4eb0924235bc7a8 Mon Sep 17 00:00:00 2001 From: Orual Date: Sun, 22 Sep 2024 20:45:28 -0400 Subject: [PATCH 33/42] making sure the log files aren't committed --- .gitignore | 2 +- Morpho/.gitignore | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index c8a02ec..b31ba96 100644 --- a/.gitignore +++ b/.gitignore @@ -15,4 +15,4 @@ Morpho/.kotlin .kotlin /.kotlin/ Butterfly -*.log \ No newline at end of file +**.log \ No newline at end of file diff --git a/Morpho/.gitignore b/Morpho/.gitignore index 0e36f72..10f8b01 100644 --- a/Morpho/.gitignore +++ b/Morpho/.gitignore @@ -19,4 +19,4 @@ captures !*.xcodeproj/project.xcworkspace/ !*.xcworkspace/contents.xcworkspacedata **/xcshareddata/WorkspaceSettings.xcsettings -*.log +**.log From a62dd2dedff33c83fee88835b0f2859fc52b1eca Mon Sep 17 00:00:00 2001 From: Orual Date: Wed, 25 Sep 2024 11:16:52 -0400 Subject: [PATCH 34/42] Have a better template implementation for stringy atproto unions/enums for code generation. see lines 89 and subsequent in Types.kt in this commit. --- Butterfly | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Butterfly b/Butterfly index 1bcd5c2..873d1fe 160000 --- a/Butterfly +++ b/Butterfly @@ -1 +1 @@ -Subproject commit 1bcd5c2fa312d1654801bcbf59079c6e21e69197 +Subproject commit 873d1fee22b96ea4eadcf7a4f59707e05da0fd2e From 45c12b85093c358cbc8445720be792e623f23ed0 Mon Sep 17 00:00:00 2001 From: Orual Date: Wed, 25 Sep 2024 11:16:52 -0400 Subject: [PATCH 35/42] Have a better template implementation for stringy atproto unions/enums for code generation. see lines 89 and subsequent in Types.kt in this commit. --- Butterfly | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Butterfly b/Butterfly index 1bcd5c2..acd3992 160000 --- a/Butterfly +++ b/Butterfly @@ -1 +1 @@ -Subproject commit 1bcd5c2fa312d1654801bcbf59079c6e21e69197 +Subproject commit acd39924d6fe4f6ab0f674597f3706501fa0b09a From 56a85fee72668f223dbfc4c59dd4383a1a3e44cf Mon Sep 17 00:00:00 2001 From: Orual Date: Wed, 25 Sep 2024 11:29:25 -0400 Subject: [PATCH 36/42] Have a better template implementation for stringy atproto unions/enums for code generation. see lines 89 and subsequent in Types.kt in this commit. --- Butterfly | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Butterfly b/Butterfly index acd3992..8197f8a 160000 --- a/Butterfly +++ b/Butterfly @@ -1 +1 @@ -Subproject commit acd39924d6fe4f6ab0f674597f3706501fa0b09a +Subproject commit 8197f8ab4434034bc027446feb7bd33e2d736e9c From bcdbed4610c1cccf1b45ff0dfbe3fd286714cd97 Mon Sep 17 00:00:00 2001 From: Orual Date: Sun, 29 Sep 2024 17:46:11 -0400 Subject: [PATCH 37/42] Bunch of code for switching accounts and the like. Also, added first bit of support for longer posts and profile descriptions bridged from Mastodon. --- Butterfly | 2 +- Morpho/composeApp/build.gradle.kts | 7 + .../com/morpho/app/MorphoApplication.kt | 13 +- .../kotlin/com/morpho/app/Platform.android.kt | 6 + .../DetailedProfileFragment.android.kt | 6 +- .../kotlin/com/morpho/app/Platform.apple.kt | 6 +- .../commonMain/kotlin/com/morpho/app/App.kt | 34 +- .../kotlin/com/morpho/app/Platform.kt | 2 + .../morpho/app/data/ContentLabelService.kt | 90 ++-- .../kotlin/com/morpho/app/data/MorphoAgent.kt | 86 ++-- .../com/morpho/app/data/MorphoDataSource.kt | 1 - .../kotlin/com/morpho/app/di/AppModule.kt | 51 ++- .../app/model/bluesky/MorphoDataItem.kt | 16 +- .../app/model/uidata/ContentLabelService.kt | 422 +++++++++--------- .../com/morpho/app/model/uidata/MorphoData.kt | 43 +- .../app/model/uidata/ProfilePresenters.kt | 13 +- .../app/model/uistate/ContentCardState.kt | 20 + .../app/screens/base/BaseScreenModel.kt | 73 ++- .../app/screens/base/tabbed/NavigationTabs.kt | 89 +++- .../screens/base/tabbed/TabbedBaseScreen.kt | 3 +- .../morpho/app/screens/login/LoginScreen.kt | 8 +- .../app/screens/login/LoginScreenModel.kt | 7 +- .../app/screens/main/MainScreenModel.kt | 52 ++- .../app/screens/main/tabbed/TabbedHomeView.kt | 61 ++- .../main/tabbed/TabbedMainScreenModel.kt | 55 ++- .../notifications/NotificationsView.kt | 8 +- .../app/screens/profile/TabbedProfileView.kt | 30 +- .../screens/settings/TabbedSettingsView.kt | 14 +- .../morpho/app/screens/thread/ThreadView.kt | 58 +-- .../com/morpho/app/ui/common/NavDrawer.kt | 4 +- .../morpho/app/ui/common/SkylineFragment.kt | 141 ++++-- .../app/ui/common/SkylineThreadFragment.kt | 14 +- .../app/ui/common/TabbedSkylineFragment.kt | 33 +- .../com/morpho/app/ui/elements/RichText.kt | 16 +- .../morpho/app/ui/elements/SettingsItems.kt | 2 +- .../notifications/NotificationAvatarList.kt | 16 +- .../ui/notifications/NotificationsElement.kt | 33 +- .../morpho/app/ui/post/FullPostFragment.kt | 68 ++- .../com/morpho/app/ui/post/PostFragment.kt | 91 ++-- .../com/morpho/app/ui/post/PostLinkEmbed.kt | 6 +- .../app/ui/profile/CompactProfileFragment.kt | 142 ++++++ .../com/morpho/app/ui/profile/ProfileLabel.kt | 5 +- .../morpho/app/ui/profile/ProfileLabels.kt | 8 +- .../app/ui/settings/BuiltinContentFilters.kt | 81 ++++ .../app/ui/settings/SettingsFragment.kt | 11 +- .../morpho/app/ui/settings/UserManagement.kt | 209 +++++++++ .../kotlin/com/morpho/app/ui/theme/Shape.kt | 2 + .../morpho/app/ui/thread/ThreadFragment.kt | 14 +- .../com/morpho/app/ui/thread/ThreadItem.kt | 14 +- .../com/morpho/app/ui/thread/ThreadReply.kt | 12 +- .../com/morpho/app/ui/thread/ThreadTree.kt | 16 +- .../com/morpho/app/ui/utils/Interaction.kt | 117 +++++ .../kotlin/com/morpho/app/Platform.desktop.kt | 13 +- .../composeApp/src/desktopMain/kotlin/main.kt | 27 +- Morpho/gradle/libs.versions.toml | 3 +- gradle/libs.versions.toml | 4 +- 56 files changed, 1713 insertions(+), 665 deletions(-) create mode 100644 Morpho/composeApp/src/androidMain/kotlin/com/morpho/app/Platform.android.kt create mode 100644 Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/profile/CompactProfileFragment.kt create mode 100644 Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/settings/UserManagement.kt create mode 100644 Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/utils/Interaction.kt diff --git a/Butterfly b/Butterfly index 8197f8a..eba8fb0 160000 --- a/Butterfly +++ b/Butterfly @@ -1 +1 @@ -Subproject commit 8197f8ab4434034bc027446feb7bd33e2d736e9c +Subproject commit eba8fb0c68e2ddd515773ab283db8e77dee3ffa6 diff --git a/Morpho/composeApp/build.gradle.kts b/Morpho/composeApp/build.gradle.kts index 7324576..1c3966c 100644 --- a/Morpho/composeApp/build.gradle.kts +++ b/Morpho/composeApp/build.gradle.kts @@ -28,9 +28,15 @@ buildkonfig { defaultConfigs { buildConfigField(STRING, "versionString", versionString) + buildConfigField(STRING, "packageName", packageString) + buildConfigField(STRING, "appName", "Morpho") + buildConfigField(STRING, "versionNumber", "0.1.0") } defaultConfigs("dev") { buildConfigField(STRING, "versionString", "${versionString}-dev") + buildConfigField(STRING, "packageName", packageString) + buildConfigField(STRING, "appName", "Morpho") + buildConfigField(STRING, "versionNumber", "0.1.0") } targetConfigs { @@ -213,6 +219,7 @@ kotlin { implementation(libs.ktor.contentnegotiation) implementation(libs.ktor.serialization.json) implementation(libs.ktor.websockets) + implementation(libs.ktor.client.encoding) implementation(libs.ktor.client.resources) implementation(libs.ktor.client.auth) diff --git a/Morpho/composeApp/src/androidMain/kotlin/com/morpho/app/MorphoApplication.kt b/Morpho/composeApp/src/androidMain/kotlin/com/morpho/app/MorphoApplication.kt index 123ec47..74b5ae7 100644 --- a/Morpho/composeApp/src/androidMain/kotlin/com/morpho/app/MorphoApplication.kt +++ b/Morpho/composeApp/src/androidMain/kotlin/com/morpho/app/MorphoApplication.kt @@ -7,8 +7,7 @@ import com.gu.toolargetool.TooLargeTool import com.morpho.app.data.MorphoAgent import com.morpho.app.data.PreferencesRepository import com.morpho.app.di.appModule -import com.morpho.app.di.dataModule -import com.morpho.app.di.storageModule + import com.morpho.butterfly.auth.SessionRepository import com.morpho.butterfly.auth.UserRepository import org.koin.android.annotation.KoinViewModel @@ -37,11 +36,13 @@ class MorphoApplication : Application() { val koin = startKoin { androidContext(this@MorphoApplication) androidLogger() - modules(androidModule, appModule, storageModule, dataModule) + modules(androidModule, appModule)//, storageModule, dataModule) }.koin - koin.get { parametersOf(cacheDir.path.toString()) } - koin.get { parametersOf(cacheDir.path.toString()) } - koin.get { parametersOf(cacheDir.path.toString()) } + + val storageDir = getPlatformStorageDir(filesDir.path.toString()) + koin.get { parametersOf(storageDir) } + koin.get { parametersOf(storageDir) } + koin.get { parametersOf(storageDir) } koin.get() super.onCreate() } diff --git a/Morpho/composeApp/src/androidMain/kotlin/com/morpho/app/Platform.android.kt b/Morpho/composeApp/src/androidMain/kotlin/com/morpho/app/Platform.android.kt new file mode 100644 index 0000000..d3bf814 --- /dev/null +++ b/Morpho/composeApp/src/androidMain/kotlin/com/morpho/app/Platform.android.kt @@ -0,0 +1,6 @@ +package com.morpho.app + + +actual fun getPlatformStorageDir(baseDir: String): String { + return baseDir +} \ No newline at end of file diff --git a/Morpho/composeApp/src/androidMain/kotlin/com/morpho/app/ui/profile/DetailedProfileFragment.android.kt b/Morpho/composeApp/src/androidMain/kotlin/com/morpho/app/ui/profile/DetailedProfileFragment.android.kt index f9a3b31..67fbf50 100644 --- a/Morpho/composeApp/src/androidMain/kotlin/com/morpho/app/ui/profile/DetailedProfileFragment.android.kt +++ b/Morpho/composeApp/src/androidMain/kotlin/com/morpho/app/ui/profile/DetailedProfileFragment.android.kt @@ -280,7 +280,11 @@ public actual fun DetailedProfileFragment( Spacer(modifier = Modifier.height(10.dp)) SelectionContainer { - RichTextElement(profile.description.orEmpty()) + RichTextElement( + profile.description.orEmpty() + ) { facetTypes -> + + } } } diff --git a/Morpho/composeApp/src/appleMain/kotlin/com/morpho/app/Platform.apple.kt b/Morpho/composeApp/src/appleMain/kotlin/com/morpho/app/Platform.apple.kt index 6309ccc..8355b95 100644 --- a/Morpho/composeApp/src/appleMain/kotlin/com/morpho/app/Platform.apple.kt +++ b/Morpho/composeApp/src/appleMain/kotlin/com/morpho/app/Platform.apple.kt @@ -9,4 +9,8 @@ actual val myLang:String? get() = NSLocale.currentLocale.languageCode actual val myCountry:String? - get() = NSLocale.currentLocale.countryCode \ No newline at end of file + get() = NSLocale.currentLocale.countryCode + +actual fun getPlatformStorageDir(baseDir: String): String { + TODO("Not yet implemented") +} \ No newline at end of file diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/App.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/App.kt index 2ca9d7f..ac2c539 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/App.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/App.kt @@ -2,20 +2,23 @@ package com.morpho.app import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable -import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import cafe.adriel.voyager.core.annotation.ExperimentalVoyagerApi import cafe.adriel.voyager.jetpack.ProvideNavigatorLifecycleKMPSupport import cafe.adriel.voyager.navigator.tab.CurrentTab -import cafe.adriel.voyager.navigator.tab.TabDisposable import cafe.adriel.voyager.navigator.tab.TabNavigator -import com.morpho.app.screens.base.BaseScreenModel +import com.morpho.app.data.ContentLabelService +import com.morpho.app.data.MorphoAgent import com.morpho.app.screens.base.tabbed.TabbedBaseScreen import com.morpho.app.screens.login.LoginScreen +import com.morpho.app.screens.main.tabbed.TabbedMainScreenModel import org.jetbrains.compose.resources.ExperimentalResourceApi import org.jetbrains.compose.ui.tooling.preview.Preview import org.koin.compose.KoinContext -import org.koin.compose.getKoin +import org.koin.compose.koinInject +import org.koin.core.parameter.parametersOf @OptIn(ExperimentalResourceApi::class, ExperimentalVoyagerApi::class) @Composable @@ -24,9 +27,14 @@ fun App() { KoinContext { MaterialTheme { ProvideNavigatorLifecycleKMPSupport { + val agent = koinInject() + val labelService = koinInject() + val screenModel = koinInject( + parameters = { parametersOf(agent, labelService) } + ) + val loggedIn by screenModel.isLoggedIn + .collectAsState(initial = screenModel.isLoggedIn.value) - val screenModel = getKoin().get() - val loggedIn by derivedStateOf { screenModel.isLoggedIn } TabNavigator( tab = if (loggedIn) { @@ -34,13 +42,15 @@ fun App() { } else { LoginScreen }, - tabDisposable = { - TabDisposable( - navigator = it, - tabs = listOf(TabbedBaseScreen, LoginScreen) - ) - } + disposeNestedNavigators = true, ) { + LaunchedEffect(loggedIn) { + if(loggedIn) { + it.current = TabbedBaseScreen + } else { + it.current = LoginScreen + } + } CurrentTab() } } diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/Platform.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/Platform.kt index 8f6eb6b..e3fe75b 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/Platform.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/Platform.kt @@ -12,6 +12,8 @@ expect fun getPlatform(): Platform expect val myLang:String? expect val myCountry:String? +expect fun getPlatformStorageDir(baseDir: String = ""): String + // For Android @Parcelize @OptIn(ExperimentalMultiplatform::class) @OptionalExpectation diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/data/ContentLabelService.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/data/ContentLabelService.kt index d7c7e37..4f2dba8 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/data/ContentLabelService.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/data/ContentLabelService.kt @@ -28,7 +28,7 @@ import com.morpho.butterfly.MutedWordTarget import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob -import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking import org.koin.core.component.KoinComponent import org.koin.core.component.inject import org.lighthousegames.logging.logging @@ -63,9 +63,12 @@ class ContentLabelService: KoinComponent { private set init { - serviceScope.launch { - agent.localizeLabelDefinitions(agent.prefs) - agent.getLabelersDetailed(labelers.keys.map { Did(it) }) + runBlocking { + labelDefinitions = agent.getLabelDefinitions(modPrefs) + val details = agent.getLabelersDetailed(labelers.keys.map { Did(it) }).getOrNull()?.associateBy { + it.creator.did.did + } + labelerDetails = details ?: emptyMap() } } @@ -74,39 +77,39 @@ class ContentLabelService: KoinComponent { return when (item) { is MorphoDataItem.Post -> { item.post.author.mutedByMe - || item.post.author.blocking - || item.post.author.blockedBy - || hiddenPosts.any { uri -> item.containsUri(uri) } - || mutedWords.any { - item.post.text.contains(it.value, ignoreCase = true) - } || if(!modPrefs.adultContentEnabled) { - val adultLabels = item.post.labels.filter { label -> - labelDefinitions[label.creator.did]?.get(label.value)?.flags - ?.contains(LabelValueDefFlag.Adult) == true - } - adultLabels.isNotEmpty() - } else { - item.post.labels.any { label -> - labels[label.value] == Visibility.HIDE - } - } + || item.post.author.blocking + || item.post.author.blockedBy + || hiddenPosts.any { uri -> item.containsUri(uri) } + || mutedWords.any { + item.post.text.contains(it.value, ignoreCase = true) + } || if(!modPrefs.adultContentEnabled) { + val adultLabels = item.post.labels.filter { label -> + labelDefinitions[label.creator.did]?.get(label.value)?.flags + ?.contains(LabelValueDefFlag.Adult) == true + } + adultLabels.isNotEmpty() + } else { + item.post.labels.any { label -> + labels[label.value] == Visibility.HIDE + } + } } is MorphoDataItem.Thread -> { item.thread.anyMutedOrBlocked() - || hiddenPosts.any { uri -> item.containsUri(uri) } - || mutedWords.any { - item.thread.containsWord(it.value) - } || if(!modPrefs.adultContentEnabled) { - val adultLabels = item.thread.getLabels().filter { label -> - labelDefinitions[label.creator.did]?.get(label.value)?.flags - ?.contains(LabelValueDefFlag.Adult) == true - } - adultLabels.isNotEmpty() - } else { - item.thread.getLabels().any { label -> - labels[label.value] == Visibility.HIDE - } + || hiddenPosts.any { uri -> item.containsUri(uri) } + || mutedWords.any { + item.thread.containsWord(it.value) + } || if(!modPrefs.adultContentEnabled) { + val adultLabels = item.thread.getLabels().filter { label -> + labelDefinitions[label.creator.did]?.get(label.value)?.flags + ?.contains(LabelValueDefFlag.Adult) == true } + adultLabels.isNotEmpty() + } else { + item.thread.getLabels().any { label -> + labels[label.value] == Visibility.HIDE + } + } } } } @@ -166,22 +169,31 @@ class ContentLabelService: KoinComponent { val possibleCauses = filteredPostLabels.mapNotNull { label -> labelDefinitions[label.creator.did]?.get(label.value)?.let { labelDef -> + val localizedDefString = labelDef.allDescriptions.firstOrNull { + it.lang == agent.myLanguage.value + } ?: labelDef.allDescriptions.firstOrNull { it.lang.tag == "en" } + val localLabelDef = labelDef.copy( + localizedName = localizedDefString?.name ?: labelDef.localizedName, + localizedDescription = localizedDefString?.description + ?: labelDef.localizedDescription, + ) + LabelCause.Label( LabelSource.Labeler(labelerDetails[label.creator.did]!!), label.toAtProtoLabel(), - labelDef, - labelDef.whatToHide, + localLabelDef, + localLabelDef.whatToHide, labels[label.value] ?: labelDef.defaultSetting ?: Visibility.IGNORE, - labelDef.behaviours.content, - noOverride = !labelDef.configurable, - priority = when (labelDef.severity) { + localLabelDef.behaviours.content, + noOverride = !localLabelDef.configurable, + priority = when (localLabelDef.severity) { Severity.INFORM -> 5 Severity.ALERT -> 2 Severity.NONE -> 8 Severity.WARN -> 1 }, downgraded = false, - ) to labelDef.toContentHandling( + ) to localLabelDef.toContentHandling( LabelTarget.Content, avatar = labelerDetails[label.creator.did]?.creator?.avatar ) diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/data/MorphoAgent.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/data/MorphoAgent.kt index 986a4d2..2c9670b 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/data/MorphoAgent.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/data/MorphoAgent.kt @@ -3,27 +3,34 @@ package com.morpho.app.data import app.bsky.actor.AdultContentPref import app.bsky.actor.PreferencesUnion import app.bsky.labeler.LabelerViewDetailed +import com.morpho.app.model.bluesky.DetailedProfile +import com.morpho.app.model.bluesky.toProfile import com.morpho.app.myLang -import com.morpho.app.util.morphoSerializersModule import com.morpho.butterfly.BskyPreferences import com.morpho.butterfly.ButterflyAgent +import com.morpho.butterfly.Did import com.morpho.butterfly.InterpretedLabelDefinition import com.morpho.butterfly.LabelValueID import com.morpho.butterfly.LabelerID import com.morpho.butterfly.Language +import com.morpho.butterfly.auth.SessionRepository +import com.morpho.butterfly.auth.UserRepository import com.morpho.butterfly.localize -import com.morpho.butterfly.xrpc.XrpcBlueskyApi +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch -import kotlinx.coroutines.runBlocking -import org.koin.core.component.inject -class MorphoAgent: ButterflyAgent() { - val localPrefs: PreferencesRepository by inject() +class MorphoAgent( + val localPrefs: PreferencesRepository, + userData: UserRepository, + session: SessionRepository, +): ButterflyAgent(userData, session) { + //val localPrefs: PreferencesRepository by inject() val morphoPrefs: MutableStateFlow = MutableStateFlow(MorphoPreferences( kawaiiMode = true, @@ -39,43 +46,42 @@ class MorphoAgent: ButterflyAgent() { } init { - // Belt and suspenders bc of the super/derived class initialization uncertainty - api = XrpcBlueskyApi(atpClient, morphoSerializersModule) - if(id != null) { - runBlocking { - getPreferences() - } - serviceScope.launch { - localPrefs.morphoPrefs(id!!).distinctUntilChanged().collectLatest { - if (it != null && it != MorphoPreferences()) { - morphoPrefs.value = it + serviceScope.launch { + while(!isLoggedIn) delay(10) + if(isLoggedIn) { + serviceScope.launch { + localPrefs.morphoPrefs(id!!).distinctUntilChanged().collectLatest { + if (it != null && it != MorphoPreferences()) { + morphoPrefs.value = it + } } } - } - serviceScope.launch { - localPrefs.bskyPrefs(id!!).distinctUntilChanged().collectLatest { - if (it != null && it != BskyPreferences()) { - prefs = it + serviceScope.launch { + localPrefs.bskyPrefs(id!!).distinctUntilChanged().collectLatest { + if (it != null && it != BskyPreferences()) { + prefs = it + } } } - } - serviceScope.launch { - localPrefs.bskyPrefs(id!!).distinctUntilChanged().collectLatest { - if (it != null && it != BskyPreferences()) { - bskyPrefs.value = it + serviceScope.launch { + localPrefs.bskyPrefs(id!!).distinctUntilChanged().collectLatest { + if (it != null && it != BskyPreferences()) { + bskyPrefs.value = it + } } } - } - serviceScope.launch { - localPrefs.writePreferences( - BskyUserPreferences( - id!!, - prefs, - morphoPrefs.value + serviceScope.launch { + localPrefs.writePreferences( + BskyUserPreferences( + id!!, + prefs, + morphoPrefs.value + ) ) - ) + } } } + } @@ -140,5 +146,17 @@ class MorphoAgent: ButterflyAgent() { } } + fun getAccounts(): Flow> { + return userData.users().map { users -> + users.mapNotNull { + getProfile(it.id).getOrNull()?.toProfile() + } + }.distinctUntilChanged() + } + + fun removeAccount(did: Did) = serviceScope.launch { + userData.removeUser(did) + } + } \ No newline at end of file diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/data/MorphoDataSource.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/data/MorphoDataSource.kt index 8e7cd59..0d12141 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/data/MorphoDataSource.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/data/MorphoDataSource.kt @@ -9,7 +9,6 @@ import com.morpho.app.model.bluesky.BskyPostThread import com.morpho.app.model.bluesky.MorphoDataItem import com.morpho.app.model.bluesky.ThreadPost import com.morpho.app.model.bluesky.toPost -import com.morpho.app.model.uidata.ContentLabelService import com.morpho.app.model.uidata.Delta import com.morpho.app.model.uidata.Moment import com.morpho.butterfly.ButterflyAgent diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/di/AppModule.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/di/AppModule.kt index ac65d48..935bfe9 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/di/AppModule.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/di/AppModule.kt @@ -1,9 +1,9 @@ package com.morpho.app.di +import com.morpho.app.data.ContentLabelService import com.morpho.app.data.MorphoAgent import com.morpho.app.data.PollBlueService import com.morpho.app.data.PreferencesRepository -import com.morpho.app.model.uidata.ContentLabelService import com.morpho.app.screens.base.BaseScreenModel import com.morpho.app.screens.login.LoginScreenModel import com.morpho.app.screens.main.MainScreenModel @@ -16,36 +16,45 @@ import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.asSharedFlow +import org.koin.core.module.dsl.bind +import org.koin.core.module.dsl.factoryOf import org.koin.core.module.dsl.singleOf -import org.koin.dsl.bind import org.koin.dsl.module val appModule = module { - single { BaseScreenModel() } - single { MainScreenModel() } - single { TabbedMainScreenModel() } - single { LoginScreenModel() } + singleOf(::BaseScreenModel) + singleOf(::MainScreenModel) + singleOf(::TabbedMainScreenModel) + factoryOf(::LoginScreenModel) factory { p-> UpdateTick(p.get()) } + singleOf(::MorphoAgent) + singleOf(::ContentLabelService) + singleOf(::PollBlueService) + singleOf(::SessionRepository) + singleOf(::PreferencesRepository) + singleOf(::UserRepositoryImpl) { + bind() + } single { ClipboardManager } } -val storageModule = module { - single { p-> SessionRepository(p.get()) } - single { p-> PreferencesRepository(p.get())} - singleOf(::UserRepositoryImpl) bind UserRepository::class -} +//val storageModule = module { +// single { p-> SessionRepository(p.get()) } +// single { p-> PreferencesRepository(p.get())} +// singleOf(::UserRepositoryImpl) bind UserRepository::class +//} -val dataModule = module { -// single { AtpAgent() } -// single { ButterflyAgent() } - single { MorphoAgent() } - single { ContentLabelService() } - single { PollBlueService() } - //factory { p -> UserListPresenter(p.get()) } - //factory { p -> UserFeedsPresenter(p.get()) } - //factory> { p -> FeedPresenter(p.get()) } -} +//val dataModule = module { +//// single { AtpAgent() } +//// single { ButterflyAgent() } +// single { MorphoAgent() } +// single { ContentLabelService() } +// single { PollBlueService() } +// //factory { p -> UserListPresenter(p.get()) } +// //factory { p -> UserFeedsPresenter(p.get()) } +// //factory> { p -> FeedPresenter(p.get()) } +//} @Suppress("MemberVisibilityCanBePrivate") public class UpdateTick(val millis: Long) { diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/MorphoDataItem.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/MorphoDataItem.kt index 8c15fb8..96f7de1 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/MorphoDataItem.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/MorphoDataItem.kt @@ -1,10 +1,12 @@ package com.morpho.app.model.bluesky import androidx.compose.runtime.Immutable +import app.bsky.actor.Visibility import app.bsky.feed.* import com.morpho.app.CommonParcelize import com.morpho.app.util.deserialize import com.morpho.butterfly.AtUri +import com.morpho.butterfly.InterpretedLabelDefinition import dev.icerock.moko.parcelize.Parcelable import dev.icerock.moko.parcelize.Parcelize import kotlinx.serialization.Serializable @@ -288,7 +290,7 @@ sealed interface MorphoDataItem: Parcelable { @Serializable @CommonParcelize data class ProfileItem( - val profile:Profile, + val profile: DetailedProfile, ): MorphoDataItem @Immutable @@ -303,15 +305,10 @@ sealed interface MorphoDataItem: Parcelable { @Serializable @CommonParcelize data class ModLabel( - val label: BskyLabelDefinition, + val label: InterpretedLabelDefinition, + val setting: Visibility, ): MorphoDataItem - @Immutable - @Serializable - @CommonParcelize - data class LabelService( - val service: BskyLabelService, - ): MorphoDataItem fun containsUri(uri: AtUri): Boolean { return when(this) { @@ -335,7 +332,6 @@ sealed interface MorphoDataItem: Parcelable { is ListInfo -> list.uri == uri is ModLabel -> label.identifier == uri.atUri is ProfileItem -> false - is LabelService -> service.uri == uri } } @@ -361,7 +357,6 @@ sealed interface MorphoDataItem: Parcelable { is ListInfo -> listOf(list.uri) is ModLabel -> listOf() is ProfileItem -> listOf() - is LabelService -> listOf(service.uri) } } @@ -373,7 +368,6 @@ sealed interface MorphoDataItem: Parcelable { is ListInfo -> list.uri is ModLabel -> null is ProfileItem -> null - is LabelService -> service.uri } } diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/ContentLabelService.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/ContentLabelService.kt index fee32b5..7734e33 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/ContentLabelService.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/ContentLabelService.kt @@ -1,212 +1,212 @@ package com.morpho.app.model.uidata - -import app.bsky.actor.MuteTargetGroup -import app.bsky.actor.MutedWord -import app.bsky.actor.Visibility -import app.bsky.labeler.LabelerViewDetailed -import com.atproto.label.Blurs -import com.atproto.label.Severity -import com.morpho.app.data.MorphoAgent -import com.morpho.app.model.bluesky.BskyPost -import com.morpho.app.model.bluesky.MorphoDataItem -import com.morpho.app.model.bluesky.toAtProtoLabel -import com.morpho.app.model.bluesky.toListVewBasic -import com.morpho.butterfly.AtUri -import com.morpho.butterfly.ContentHandling -import com.morpho.butterfly.Did -import com.morpho.butterfly.InterpretedLabelDefinition -import com.morpho.butterfly.LabelAction -import com.morpho.butterfly.LabelCause -import com.morpho.butterfly.LabelDescription -import com.morpho.butterfly.LabelIcon -import com.morpho.butterfly.LabelSource -import com.morpho.butterfly.LabelTarget -import com.morpho.butterfly.LabelValueDefFlag -import com.morpho.butterfly.LabelValueID -import com.morpho.butterfly.LabelerID -import com.morpho.butterfly.ModerationPreferences -import com.morpho.butterfly.MutedWordTarget -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.SupervisorJob -import kotlinx.coroutines.runBlocking -import org.koin.core.component.KoinComponent -import org.koin.core.component.inject -import org.lighthousegames.logging.logging - -class ContentLabelService: KoinComponent { - val agent: MorphoAgent by inject() - val serviceScope = CoroutineScope(SupervisorJob() + Dispatchers.Default) - companion object { - val log = logging("ContentLabelService") - } - - val modPrefs: ModerationPreferences - get() = agent.prefs.modPrefs - - val hiddenPosts: List - get() = modPrefs.hiddenPosts - - val mutedWords: List - get() = modPrefs.mutedWords - - - val labelers: Map> - get() = modPrefs.labelers - - val labels: Map - get() = modPrefs.labels - - var labelDefinitions: Map> = emptyMap() - private set - - var labelerDetails: Map = emptyMap() - private set - - init { - runBlocking { - labelDefinitions = agent.getLabelDefinitions(modPrefs) - val details = agent.getLabelersDetailed(labelers.keys.map { Did(it) }).getOrNull()?.associateBy { - it.creator.did.did - } - labelerDetails = details ?: emptyMap() - } - - } - - fun shouldHideItem(item: MorphoDataItem.FeedItem): Boolean { - return when (item) { - is MorphoDataItem.Post -> { - item.post.author.mutedByMe - || item.post.author.blocking - || item.post.author.blockedBy - || hiddenPosts.any { uri -> item.containsUri(uri) } - || mutedWords.any { - item.post.text.contains(it.value, ignoreCase = true) - } || if(!modPrefs.adultContentEnabled) { - val adultLabels = item.post.labels.filter { label -> - labelDefinitions[label.creator.did]?.get(label.value)?.flags - ?.contains(LabelValueDefFlag.Adult) == true - } - adultLabels.isNotEmpty() - } else { - item.post.labels.any { label -> - labels[label.value] == Visibility.HIDE - } - } - } - is MorphoDataItem.Thread -> { - item.thread.anyMutedOrBlocked() - || hiddenPosts.any { uri -> item.containsUri(uri) } - || mutedWords.any { - item.thread.containsWord(it.value) - } || if(!modPrefs.adultContentEnabled) { - val adultLabels = item.thread.getLabels().filter { label -> - labelDefinitions[label.creator.did]?.get(label.value)?.flags - ?.contains(LabelValueDefFlag.Adult) == true - } - adultLabels.isNotEmpty() - } else { - item.thread.getLabels().any { label -> - labels[label.value] == Visibility.HIDE - } - } - } - } - } - - fun getContentHandlingForPost(post: BskyPost): List> { - val result = mutableListOf>() - val postLabels = post.labels - - if(post.author.mutedByMe) { - result.add(ContentHandling( - scope = Blurs.CONTENT, - action = LabelAction.Blur, - source = LabelDescription.YouMuted, - id = "muted", - icon = LabelIcon.EyeSlash(labelerAvatar = null), - ) to LabelCause.Muted(LabelSource.User, false)) - } - if(post.author.mutedByList != null) { - val list = post.author.mutedByList!! - result.add(ContentHandling( - scope = Blurs.CONTENT, - action = LabelAction.Blur, - source = LabelDescription.MuteList( - list.name, - list.uri, - ), - id = "muted-word", - icon = LabelIcon.EyeSlash( labelerAvatar = list.avatar), - ) to LabelCause.Muted(LabelSource.List(list.toListVewBasic()), false)) - } - val anyMutedWords = mutedWords.filter { post.text.contains(it.value, ignoreCase = true) } - if(anyMutedWords.isNotEmpty()) anyMutedWords.forEach { word -> - if(!word.targets.contains(MutedWordTarget("content"))) return@forEach - if(word.actorTarget == MuteTargetGroup.EXCLUDE_FOLLOWING && post.author.followedByMe) return@forEach - result.add(ContentHandling( - scope = Blurs.CONTENT, - action = LabelAction.Blur, - source = LabelDescription.MutedWord(word.value), - id = "muted-word", - icon = LabelIcon.EyeSlash(), - ) to LabelCause.MutedWord(LabelSource.User, false)) - } - - - if (postLabels.isNotEmpty()) { - log.verbose { "Post ${post.uri} has labels: ${postLabels.joinToString { it.value }}" } - // Adult content hiding if someone doesn't have it enabled is handled earlier, - // before rendering starts, as is Visibility.HIDE - // so we don't need to worry about it here - val relevantLabels = labels.filter { prefLabel -> - (prefLabel.value == Visibility.WARN || prefLabel.value == Visibility.HIDE) - && postLabels.any { it.value == it.value } }.toList() - .sortedBy { it.second.ordering } - val filteredPostLabels = postLabels.filter { label -> - relevantLabels.any { label.value == it.first } - } - - val possibleCauses = filteredPostLabels.mapNotNull { label -> - labelDefinitions[label.creator.did]?.get(label.value)?.let { labelDef -> - val localizedDefString = labelDef.allDescriptions.firstOrNull { - it.lang == agent.myLanguage.value - } ?: labelDef.allDescriptions.firstOrNull { it.lang.tag == "en" } - val localLabelDef = labelDef.copy( - localizedName = localizedDefString?.name ?: labelDef.localizedName, - localizedDescription = localizedDefString?.description - ?: labelDef.localizedDescription, - ) - - LabelCause.Label( - LabelSource.Labeler(labelerDetails[label.creator.did]!!), - label.toAtProtoLabel(), - localLabelDef, - localLabelDef.whatToHide, - labels[label.value] ?: labelDef.defaultSetting ?: Visibility.IGNORE, - localLabelDef.behaviours.content, - noOverride = !localLabelDef.configurable, - priority = when (localLabelDef.severity) { - Severity.INFORM -> 5 - Severity.ALERT -> 2 - Severity.NONE -> 8 - Severity.WARN -> 1 - }, - downgraded = false, - ) to localLabelDef.toContentHandling( - LabelTarget.Content, - avatar = labelerDetails[label.creator.did]?.creator?.avatar - ) - } - }.sortedBy{ it.first.priority } - possibleCauses.forEach { (cause, handling) -> - result.add(handling to cause) - } - } - - log.verbose { "Post ${post.uri} has handling: \n$result" } - return result.toList() - } - -} \ No newline at end of file +// +//import app.bsky.actor.MuteTargetGroup +//import app.bsky.actor.MutedWord +//import app.bsky.actor.Visibility +//import app.bsky.labeler.LabelerViewDetailed +//import com.atproto.label.Blurs +//import com.atproto.label.Severity +//import com.morpho.app.data.MorphoAgent +//import com.morpho.app.model.bluesky.BskyPost +//import com.morpho.app.model.bluesky.MorphoDataItem +//import com.morpho.app.model.bluesky.toAtProtoLabel +//import com.morpho.app.model.bluesky.toListVewBasic +//import com.morpho.butterfly.AtUri +//import com.morpho.butterfly.ContentHandling +//import com.morpho.butterfly.Did +//import com.morpho.butterfly.InterpretedLabelDefinition +//import com.morpho.butterfly.LabelAction +//import com.morpho.butterfly.LabelCause +//import com.morpho.butterfly.LabelDescription +//import com.morpho.butterfly.LabelIcon +//import com.morpho.butterfly.LabelSource +//import com.morpho.butterfly.LabelTarget +//import com.morpho.butterfly.LabelValueDefFlag +//import com.morpho.butterfly.LabelValueID +//import com.morpho.butterfly.LabelerID +//import com.morpho.butterfly.ModerationPreferences +//import com.morpho.butterfly.MutedWordTarget +//import kotlinx.coroutines.CoroutineScope +//import kotlinx.coroutines.Dispatchers +//import kotlinx.coroutines.SupervisorJob +//import kotlinx.coroutines.runBlocking +//import org.koin.core.component.KoinComponent +//import org.koin.core.component.inject +//import org.lighthousegames.logging.logging +// +//class ContentLabelService: KoinComponent { +// val agent: MorphoAgent by inject() +// val serviceScope = CoroutineScope(SupervisorJob() + Dispatchers.Default) +// companion object { +// val log = logging("ContentLabelService") +// } +// +// val modPrefs: ModerationPreferences +// get() = agent.prefs.modPrefs +// +// val hiddenPosts: List +// get() = modPrefs.hiddenPosts +// +// val mutedWords: List +// get() = modPrefs.mutedWords +// +// +// val labelers: Map> +// get() = modPrefs.labelers +// +// val labels: Map +// get() = modPrefs.labels +// +// var labelDefinitions: Map> = emptyMap() +// private set +// +// var labelerDetails: Map = emptyMap() +// private set +// +// init { +// runBlocking { +// labelDefinitions = agent.getLabelDefinitions(modPrefs) +// val details = agent.getLabelersDetailed(labelers.keys.map { Did(it) }).getOrNull()?.associateBy { +// it.creator.did.did +// } +// labelerDetails = details ?: emptyMap() +// } +// +// } +// +// fun shouldHideItem(item: MorphoDataItem.FeedItem): Boolean { +// return when (item) { +// is MorphoDataItem.Post -> { +// item.post.author.mutedByMe +// || item.post.author.blocking +// || item.post.author.blockedBy +// || hiddenPosts.any { uri -> item.containsUri(uri) } +// || mutedWords.any { +// item.post.text.contains(it.value, ignoreCase = true) +// } || if(!modPrefs.adultContentEnabled) { +// val adultLabels = item.post.labels.filter { label -> +// labelDefinitions[label.creator.did]?.get(label.value)?.flags +// ?.contains(LabelValueDefFlag.Adult) == true +// } +// adultLabels.isNotEmpty() +// } else { +// item.post.labels.any { label -> +// labels[label.value] == Visibility.HIDE +// } +// } +// } +// is MorphoDataItem.Thread -> { +// item.thread.anyMutedOrBlocked() +// || hiddenPosts.any { uri -> item.containsUri(uri) } +// || mutedWords.any { +// item.thread.containsWord(it.value) +// } || if(!modPrefs.adultContentEnabled) { +// val adultLabels = item.thread.getLabels().filter { label -> +// labelDefinitions[label.creator.did]?.get(label.value)?.flags +// ?.contains(LabelValueDefFlag.Adult) == true +// } +// adultLabels.isNotEmpty() +// } else { +// item.thread.getLabels().any { label -> +// labels[label.value] == Visibility.HIDE +// } +// } +// } +// } +// } +// +// fun getContentHandlingForPost(post: BskyPost): List> { +// val result = mutableListOf>() +// val postLabels = post.labels +// +// if(post.author.mutedByMe) { +// result.add(ContentHandling( +// scope = Blurs.CONTENT, +// action = LabelAction.Blur, +// source = LabelDescription.YouMuted, +// id = "muted", +// icon = LabelIcon.EyeSlash(labelerAvatar = null), +// ) to LabelCause.Muted(LabelSource.User, false)) +// } +// if(post.author.mutedByList != null) { +// val list = post.author.mutedByList!! +// result.add(ContentHandling( +// scope = Blurs.CONTENT, +// action = LabelAction.Blur, +// source = LabelDescription.MuteList( +// list.name, +// list.uri, +// ), +// id = "muted-word", +// icon = LabelIcon.EyeSlash( labelerAvatar = list.avatar), +// ) to LabelCause.Muted(LabelSource.List(list.toListVewBasic()), false)) +// } +// val anyMutedWords = mutedWords.filter { post.text.contains(it.value, ignoreCase = true) } +// if(anyMutedWords.isNotEmpty()) anyMutedWords.forEach { word -> +// if(!word.targets.contains(MutedWordTarget("content"))) return@forEach +// if(word.actorTarget == MuteTargetGroup.EXCLUDE_FOLLOWING && post.author.followedByMe) return@forEach +// result.add(ContentHandling( +// scope = Blurs.CONTENT, +// action = LabelAction.Blur, +// source = LabelDescription.MutedWord(word.value), +// id = "muted-word", +// icon = LabelIcon.EyeSlash(), +// ) to LabelCause.MutedWord(LabelSource.User, false)) +// } +// +// +// if (postLabels.isNotEmpty()) { +// log.verbose { "Post ${post.uri} has labels: ${postLabels.joinToString { it.value }}" } +// // Adult content hiding if someone doesn't have it enabled is handled earlier, +// // before rendering starts, as is Visibility.HIDE +// // so we don't need to worry about it here +// val relevantLabels = labels.filter { prefLabel -> +// (prefLabel.value == Visibility.WARN || prefLabel.value == Visibility.HIDE) +// && postLabels.any { it.value == it.value } }.toList() +// .sortedBy { it.second.ordering } +// val filteredPostLabels = postLabels.filter { label -> +// relevantLabels.any { label.value == it.first } +// } +// +// val possibleCauses = filteredPostLabels.mapNotNull { label -> +// labelDefinitions[label.creator.did]?.get(label.value)?.let { labelDef -> +// val localizedDefString = labelDef.allDescriptions.firstOrNull { +// it.lang == agent.myLanguage.value +// } ?: labelDef.allDescriptions.firstOrNull { it.lang.tag == "en" } +// val localLabelDef = labelDef.copy( +// localizedName = localizedDefString?.name ?: labelDef.localizedName, +// localizedDescription = localizedDefString?.description +// ?: labelDef.localizedDescription, +// ) +// +// LabelCause.Label( +// LabelSource.Labeler(labelerDetails[label.creator.did]!!), +// label.toAtProtoLabel(), +// localLabelDef, +// localLabelDef.whatToHide, +// labels[label.value] ?: labelDef.defaultSetting ?: Visibility.IGNORE, +// localLabelDef.behaviours.content, +// noOverride = !localLabelDef.configurable, +// priority = when (localLabelDef.severity) { +// Severity.INFORM -> 5 +// Severity.ALERT -> 2 +// Severity.NONE -> 8 +// Severity.WARN -> 1 +// }, +// downgraded = false, +// ) to localLabelDef.toContentHandling( +// LabelTarget.Content, +// avatar = labelerDetails[label.creator.did]?.creator?.avatar +// ) +// } +// }.sortedBy{ it.first.priority } +// possibleCauses.forEach { (cause, handling) -> +// result.add(handling to cause) +// } +// } +// +// log.verbose { "Post ${post.uri} has handling: \n$result" } +// return result.toList() +// } +// +//} \ No newline at end of file diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/MorphoData.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/MorphoData.kt index 687ed3b..d1a060c 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/MorphoData.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/MorphoData.kt @@ -11,14 +11,12 @@ import app.bsky.feed.FeedViewPost import com.morpho.app.data.FeedTuner import com.morpho.app.data.MorphoAgent import com.morpho.app.model.bluesky.AuthorContext -import com.morpho.app.model.bluesky.BskyLabelDefinition -import com.morpho.app.model.bluesky.BskyLabelService import com.morpho.app.model.bluesky.BskyList import com.morpho.app.model.bluesky.BskyPostReason import com.morpho.app.model.bluesky.BskyPostThread +import com.morpho.app.model.bluesky.DetailedProfile import com.morpho.app.model.bluesky.FeedGenerator import com.morpho.app.model.bluesky.MorphoDataItem -import com.morpho.app.model.bluesky.Profile import com.morpho.app.model.bluesky.ThreadPost import com.morpho.app.model.uistate.FeedType import com.morpho.butterfly.AtIdentifier @@ -238,7 +236,7 @@ data class MorphoData( fun fromProfileList( title: String, uri: AtUri, - list: List, + list: List, cursor: AtCursor = AtCursor.EMPTY, ): MorphoData { return MorphoData( @@ -263,33 +261,6 @@ data class MorphoData( ) } - fun fromModLabelDefs( - title: String, - uri: AtUri, - labels: List, - cursor: AtCursor = AtCursor.EMPTY, - ): MorphoData { - return MorphoData( - title = title, - uri = uri, - cursor = cursor, - items = labels.map { MorphoDataItem.ModLabel(it) }.toMutableList(), - ) - } - - fun fromModServiceDefs( - title: String, - uri: AtUri, - services: List, - cursor: AtCursor = AtCursor.EMPTY, - ): MorphoData { - return MorphoData( - title = title, - uri = uri, - cursor = cursor, - items = services.map { MorphoDataItem.LabelService(it) }.toMutableList(), - ) - } } @@ -332,7 +303,6 @@ data class MorphoData( is MorphoDataItem.ListInfo -> it.list.cid == cid is MorphoDataItem.ModLabel -> false is MorphoDataItem.ProfileItem -> false - is MorphoDataItem.LabelService -> it.service.cid == cid else -> {false} } } @@ -550,7 +520,6 @@ data class MorphoData( is MorphoDataItem.ListInfo -> it.list.uri is MorphoDataItem.ModLabel -> it.label.identifier is MorphoDataItem.ProfileItem -> it.profile.did - is MorphoDataItem.LabelService -> it.service.uri else -> {it.hashCode()} } } return this.copy(items = newList) @@ -560,11 +529,11 @@ data class MorphoData( } -fun AtUri.id(api:MorphoAgent): AtIdentifier { +suspend fun AtUri.id(agent: MorphoAgent): AtIdentifier { val idString = atUri.substringAfter("at://").split("/")[0] - return if (idString == "me") api.id!! else { - // TODO: make this resolve a handle to a DID - if (idString.contains("did:")) Did(idString) else Handle(idString) + return if (idString == "me") agent.id!! else { + if (idString.contains("did:")) Did(idString) + else agent.resolveHandle(Handle(idString)).getOrNull() ?: Handle(idString) } } diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/ProfilePresenters.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/ProfilePresenters.kt index 4e0f022..d7cdd60 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/ProfilePresenters.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/ProfilePresenters.kt @@ -4,6 +4,7 @@ import app.bsky.feed.GetActorFeedsQuery import app.bsky.graph.GetListsQuery import com.morpho.app.data.MorphoAgent import com.morpho.app.model.bluesky.AuthorFilter +import com.morpho.app.model.bluesky.DetailedProfile import com.morpho.app.model.bluesky.FeedDescriptor import com.morpho.app.model.bluesky.toLabelService import com.morpho.app.model.bluesky.toProfile @@ -69,9 +70,10 @@ class MyProfilePresenter( val log = logging("ProfilePresenter") suspend fun initialize( agent: MorphoAgent, + myProfile: DetailedProfile? = null, ): ContentCardState.MyProfile? { val id = agent.id ?: return null - val profile = agent.getProfile(id).getOrNull()?.toProfile() ?: return null + val profile = myProfile ?: agent.getProfile(id).getOrNull()?.toProfile() ?: return null val hasFeeds = agent.api .getActorFeeds(GetActorFeedsQuery(id, 1, null)).getOrNull()?.feeds?.isNotEmpty() ?: false @@ -98,8 +100,9 @@ class MyProfilePresenter( } suspend fun create( agent: MorphoAgent, + profile: DetailedProfile? = null, ): MyProfilePresenter? { - val state = initialize(agent) ?: return null + val state = initialize(agent, profile) ?: return null return MyProfilePresenter(state) } } @@ -167,8 +170,9 @@ class ProfilePresenter( suspend fun initialize( agent: MorphoAgent, actor: Did, + actorProfile: DetailedProfile? = null, ): ContentCardState.FullProfile? { - val profile = agent.getProfile(actor).getOrNull()?.toProfile() ?: return null + val profile = actorProfile ?: agent.getProfile(actor).getOrNull()?.toProfile() ?: return null val hasFeeds = agent.api .getActorFeeds(GetActorFeedsQuery(actor, 1, null)).getOrNull()?.feeds?.isNotEmpty() ?: false @@ -196,8 +200,9 @@ class ProfilePresenter( suspend fun create( agent: MorphoAgent, actor: Did, + actorProfile: DetailedProfile? = null, ): ProfilePresenter? { - val state = initialize(agent, actor) ?: return null + val state = initialize(agent, actor, actorProfile) ?: return null return ProfilePresenter(state) } } diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uistate/ContentCardState.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uistate/ContentCardState.kt index eba52da..209260b 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uistate/ContentCardState.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uistate/ContentCardState.kt @@ -1,5 +1,6 @@ package com.morpho.app.model.uistate +import androidx.compose.runtime.Immutable import com.morpho.app.model.bluesky.AuthorFilter import com.morpho.app.model.bluesky.BskyLabelService import com.morpho.app.model.bluesky.BskyList @@ -17,11 +18,20 @@ import com.morpho.app.model.uidata.UIUpdate import com.morpho.app.util.MutableSharedFlowSerializer import com.morpho.app.util.MutableStateFlowSerializer import com.morpho.butterfly.AtUri +import dev.icerock.moko.parcelize.Parcelable +import dev.icerock.moko.parcelize.Parcelize import kotlinx.coroutines.channels.BufferOverflow import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.serialization.Serializable +@Parcelize +@Serializable +@Immutable +data class ScrollPosition( + val index: Int = 0, + val scrollOffset: Int = 0, +): Parcelable @Suppress("unused") @Serializable @@ -31,6 +41,7 @@ sealed interface ContentCardState { val events: MutableSharedFlow @Serializable(with = MutableStateFlowSerializer::class) val updates: MutableStateFlow + val scrollPosition: MutableStateFlow @Serializable data class Skyline( @@ -39,6 +50,7 @@ sealed interface ContentCardState { extraBufferCapacity = 10, onBufferOverflow = BufferOverflow.DROP_OLDEST), override val updates: MutableStateFlow = MutableStateFlow(FeedUpdate.Empty), + override val scrollPosition: MutableStateFlow = MutableStateFlow(ScrollPosition()), ) : ContentCardState { } @@ -50,6 +62,7 @@ sealed interface ContentCardState { extraBufferCapacity = 10, onBufferOverflow = BufferOverflow.DROP_OLDEST), override val updates: MutableStateFlow = MutableStateFlow(UIUpdate.Empty), + override val scrollPosition: MutableStateFlow = MutableStateFlow(ScrollPosition()), ): ContentCardState { override val uri: AtUri = post.uri init { @@ -67,6 +80,7 @@ sealed interface ContentCardState { extraBufferCapacity = 10, onBufferOverflow = BufferOverflow.DROP_OLDEST), override val updates: MutableStateFlow = MutableStateFlow(FeedUpdate.Empty), + override val scrollPosition: MutableStateFlow = MutableStateFlow(ScrollPosition()), ) : ContentCardState { override val uri: AtUri = when(filter) { AuthorFilter.PostsWithReplies -> AtUri.profileRepliesUri(profile.did) @@ -84,6 +98,7 @@ sealed interface ContentCardState { extraBufferCapacity = 10, onBufferOverflow = BufferOverflow.DROP_OLDEST), override val updates: MutableStateFlow = MutableStateFlow(UIUpdate.Empty), + override val scrollPosition: MutableStateFlow = MutableStateFlow(ScrollPosition()), ): ContentCardState { override val uri: AtUri = when(listsOrFeeds) { ListsOrFeeds.Lists -> AtUri.profileUserListsUri(profile.did) @@ -98,6 +113,7 @@ sealed interface ContentCardState { extraBufferCapacity = 10, onBufferOverflow = BufferOverflow.DROP_OLDEST), override val updates: MutableStateFlow = MutableStateFlow(UIUpdate.Empty), + override val scrollPosition: MutableStateFlow = MutableStateFlow(ScrollPosition()), ): ContentCardState data class FullProfile( @@ -112,6 +128,7 @@ sealed interface ContentCardState { extraBufferCapacity = 10, onBufferOverflow = BufferOverflow.DROP_OLDEST), override val updates: MutableStateFlow = MutableStateFlow(UIUpdate.Empty), + override val scrollPosition: MutableStateFlow = MutableStateFlow(ScrollPosition()), ) : ContentCardState { override val uri: AtUri = AtUri.profileUri(profile.did) } @@ -129,6 +146,7 @@ sealed interface ContentCardState { extraBufferCapacity = 10, onBufferOverflow = BufferOverflow.DROP_OLDEST), override val updates: MutableStateFlow = MutableStateFlow(UIUpdate.Empty), + override val scrollPosition: MutableStateFlow = MutableStateFlow(ScrollPosition()), ) : ContentCardState { override val uri: AtUri = AtUri.profileUri(profile.did) } @@ -140,6 +158,7 @@ sealed interface ContentCardState { extraBufferCapacity = 10, onBufferOverflow = BufferOverflow.DROP_OLDEST), override val updates: MutableStateFlow = MutableStateFlow(UIUpdate.Empty), + override val scrollPosition: MutableStateFlow = MutableStateFlow(ScrollPosition()), ) : ContentCardState { override val uri: AtUri = list.uri } @@ -151,6 +170,7 @@ sealed interface ContentCardState { extraBufferCapacity = 10, onBufferOverflow = BufferOverflow.DROP_OLDEST), override val updates: MutableStateFlow = MutableStateFlow(UIUpdate.Empty), + override val scrollPosition: MutableStateFlow = MutableStateFlow(ScrollPosition()), ) : ContentCardState { override val uri: AtUri = list.uri } diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/base/BaseScreenModel.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/base/BaseScreenModel.kt index 5c284fb..c3114f5 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/base/BaseScreenModel.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/base/BaseScreenModel.kt @@ -7,19 +7,23 @@ import app.cash.paging.Pager import app.cash.paging.cachedIn import cafe.adriel.voyager.core.model.ScreenModel import cafe.adriel.voyager.core.model.screenModelScope +import com.morpho.app.data.ContentLabelService import com.morpho.app.data.MorphoAgent import com.morpho.app.di.UpdateTick import com.morpho.app.model.bluesky.BskyPost +import com.morpho.app.model.bluesky.DetailedProfile import com.morpho.app.model.bluesky.NotificationsSource import com.morpho.app.model.bluesky.toPost -import com.morpho.app.model.uidata.ContentLabelService +import com.morpho.app.model.bluesky.toProfile import com.morpho.app.model.uidata.Event import com.morpho.app.model.uidata.MyProfilePresenter import com.morpho.app.model.uidata.ProfilePresenter import com.morpho.app.model.uidata.UIUpdate import com.morpho.butterfly.AtUri import com.morpho.butterfly.Did +import kotlinx.coroutines.Job import kotlinx.coroutines.channels.BufferOverflow +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow @@ -30,13 +34,17 @@ import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch -import org.koin.core.component.KoinComponent -import org.koin.core.component.inject import org.lighthousegames.logging.logging -open class BaseScreenModel : ScreenModel, KoinComponent { - val agent: MorphoAgent by inject() - val labelService: ContentLabelService by inject() +open class BaseScreenModel( + val agent: MorphoAgent, + val labelService: ContentLabelService +) : ScreenModel { + //val agent: MorphoAgent by inject() + //val labelService: ContentLabelService by inject() + + var userProfile: DetailedProfile? by mutableStateOf(null) + protected set val kawaiiMode: Boolean get() = agent.kawaiiMode @@ -47,14 +55,13 @@ open class BaseScreenModel : ScreenModel, KoinComponent { val globalEvents = MutableSharedFlow( extraBufferCapacity = 100, onBufferOverflow = BufferOverflow.DROP_OLDEST) - val isLoggedIn: Boolean - get() = agent.isLoggedIn + val isLoggedIn = MutableStateFlow(agent.isLoggedIn) val notifications = Pager(NotificationsSource.defaultConfig) { NotificationsSource() }.flow.cachedIn(screenModelScope) - + var notifJob: Job? = null companion object { val log = logging() @@ -63,6 +70,18 @@ open class BaseScreenModel : ScreenModel, KoinComponent { private val notificationsTick = UpdateTick(10000) init { screenModelScope.launch { + if(!agent.isLoggedIn) { + while(!agent.isLoggedIn) { + delay(100) + isLoggedIn.value = agent.isLoggedIn + } + } + } + screenModelScope.launch { + while(!isLoggedIn.value) delay(10) + userProfile = userDid?.let { agent.getProfile(it).getOrNull()?.toProfile() } + } + notifJob = screenModelScope.launch { notificationsTick.tick(true) } } @@ -71,12 +90,36 @@ open class BaseScreenModel : ScreenModel, KoinComponent { globalEvents.tryEmit(event) } + open fun logout() { + agent.logout().invokeOnCompletion { + deinit() + isLoggedIn.value = false + userDid = null + userProfile = null + + } + } + + open fun switchUser(did: Did) { + screenModelScope.launch { + deinit() + agent.switchUser(did) + userDid = did + userProfile = agent.getProfile(did).getOrNull()?.toProfile() + notifJob = screenModelScope.launch { + notificationsTick.tick(true) + } + } + + } + fun getProfilePresenter( id: Did, init: Boolean = false, eventStream: Flow = globalEvents ): Flow>> = flow { - val presenter = ProfilePresenter.create(agent, id)?: return@flow + val profile = agent.getProfile(id).getOrNull()?.toProfile() + val presenter = ProfilePresenter.create(agent, id, profile)?: return@flow if(!init) emit(Pair(presenter, MutableStateFlow(UIUpdate.Empty))) else { val stateFlow = MutableStateFlow(UIUpdate.Empty) @@ -85,13 +128,13 @@ open class BaseScreenModel : ScreenModel, KoinComponent { } emit(Pair(presenter, stateFlow)) } - } + }.distinctUntilChanged() as Flow>> fun getMyProfilePresenter( init: Boolean = false, eventStream: Flow = globalEvents ): Flow>> = flow { - val presenter = MyProfilePresenter.create(agent)?: return@flow + val presenter = MyProfilePresenter.create(agent, userProfile)?: return@flow if(!init) emit(Pair(presenter, MutableStateFlow(UIUpdate.Empty))) else { val stateFlow = MutableStateFlow(UIUpdate.Empty) @@ -101,7 +144,7 @@ open class BaseScreenModel : ScreenModel, KoinComponent { emit(Pair(presenter, stateFlow)) } - } + }.distinctUntilChanged() as Flow>> @@ -122,5 +165,9 @@ open class BaseScreenModel : ScreenModel, KoinComponent { }.getOrDefault(Result.failure(Exception("Post not found"))) } + open fun deinit() { + notifJob?.cancel() + } + } \ No newline at end of file diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/base/tabbed/NavigationTabs.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/base/tabbed/NavigationTabs.kt index 6dcbca3..762e9a2 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/base/tabbed/NavigationTabs.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/base/tabbed/NavigationTabs.kt @@ -20,15 +20,18 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import cafe.adriel.voyager.core.annotation.ExperimentalVoyagerApi import cafe.adriel.voyager.core.lifecycle.LifecycleEffectOnce -import cafe.adriel.voyager.core.model.rememberNavigatorScreenModel import cafe.adriel.voyager.core.model.screenModelScope import cafe.adriel.voyager.core.screen.Screen import cafe.adriel.voyager.core.screen.ScreenKey import cafe.adriel.voyager.core.screen.uniqueScreenKey +import cafe.adriel.voyager.koin.koinNavigatorScreenModel import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.Navigator import cafe.adriel.voyager.navigator.currentOrThrow +import com.morpho.app.model.bluesky.FeedGenerator +import com.morpho.app.model.bluesky.UserList import com.morpho.app.model.uidata.MyProfilePresenter +import com.morpho.app.model.uidata.ProfilePresenter import com.morpho.app.screens.main.tabbed.TabbedHomeView import com.morpho.app.screens.main.tabbed.TabbedMainScreenModel import com.morpho.app.screens.notifications.NotificationViewContent @@ -88,8 +91,8 @@ data class HomeTab( @OptIn(ExperimentalVoyagerApi::class) @Composable override fun Content() { - val sm = LocalNavigator.currentOrThrow.rememberNavigatorScreenModel { TabbedMainScreenModel() } - TabbedHomeView(sm) + val sm = LocalNavigator.currentOrThrow.koinNavigatorScreenModel() + TabbedHomeView(sm = sm) } override val options: TabScreenOptions @@ -209,20 +212,24 @@ data class ProfileTab( @kotlin.jvm.Transient @Transient override val navBar: @Composable (@Contextual Navigator) -> Unit = { n -> TabbedNavBar(options.index, n) } - @OptIn(ExperimentalMaterial3Api::class) + @OptIn(ExperimentalMaterial3Api::class, ExperimentalVoyagerApi::class) @Composable override fun Content() { - val sm = LocalNavigator.currentOrThrow.rememberNavigatorScreenModel { TabbedMainScreenModel() } + val sm = LocalNavigator.currentOrThrow.koinNavigatorScreenModel() val eventStream = sm.globalEvents - val profilePresenter by sm.getProfilePresenter(id).collectAsState(null) - val myProfilePresenter by sm.getMyProfilePresenter().collectAsState(null) + var myProfilePresenter by remember { mutableStateOf(null) } + var profilePresenter by remember { mutableStateOf(null) } + LifecycleEffectOnce { + sm.screenModelScope.launch { + sm.getMyProfilePresenter().first().also { it -> myProfilePresenter = it.first } + sm.getProfilePresenter(id, true).first().also { it -> profilePresenter = it.first } + } + } if(profilePresenter != null && myProfilePresenter != null) { - val presenter = profilePresenter!!.first - val updates = profilePresenter!!.second - - val myProfileState = myProfilePresenter!!.first.profileState + val profileState = profilePresenter!!.profileState + val myProfileState = myProfilePresenter!!.profileState TabbedProfileContent( - profileState = presenter.profileState, + profileState = profileState, myProfileState = myProfileState, eventCallback = { eventStream.tryEmit(it) } ) @@ -260,7 +267,7 @@ data class ThreadTab( @Composable override fun Content() { val navigator = LocalNavigator.currentOrThrow - val sm = navigator.rememberNavigatorScreenModel { TabbedMainScreenModel() } + val sm = navigator.koinNavigatorScreenModel() val threadState by sm.getThread(uri).collectAsState(null) if(threadState != null) { ThreadViewContent(threadState!!, navigator) @@ -301,7 +308,7 @@ data object MyProfileTab: TabScreen { @OptIn(ExperimentalMaterial3Api::class, ExperimentalVoyagerApi::class) @Composable override fun Content() { - val sm = LocalNavigator.currentOrThrow.rememberNavigatorScreenModel { TabbedMainScreenModel() } + val sm = LocalNavigator.currentOrThrow.koinNavigatorScreenModel() val eventStream = sm.globalEvents var myProfilePresenter by remember { mutableStateOf(null) } LifecycleEffectOnce { @@ -345,7 +352,7 @@ data object SettingsTab: TabScreen { @Composable override fun Content() { - val sm = LocalNavigator.currentOrThrow.rememberNavigatorScreenModel { TabbedMainScreenModel() } + val sm = LocalNavigator.currentOrThrow.koinNavigatorScreenModel() val navigator = LocalNavigator.currentOrThrow Navigator( SettingsRootPage, @@ -368,4 +375,56 @@ data object SettingsTab: TabScreen { title = "Settings" ) } +} + +data class FeedPageTab( + val feed: FeedGenerator, +): TabScreen { + override val key: ScreenKey + get() = "FeedPageTab_${uniqueScreenKey}" + + override val navBar: @Composable (@Contextual Navigator) -> Unit = { n -> + TabbedNavBar(options.index, n) + } + + @Composable + override fun Content() { + TODO("Not yet implemented") + } + + override val options: TabScreenOptions + @Composable get() { + return TabScreenOptions( + index = 6, + icon = { Icon(Icons.Default.Settings, contentDescription = "Settings", + tint = MaterialTheme.colorScheme.onBackground) }, + title = feed.displayName + ) + } +} + +data class UserListPageTab( + val list: UserList, +): TabScreen { + override val key: ScreenKey + get() = "UserListPageTab_${uniqueScreenKey}" + + override val navBar: @Composable (@Contextual Navigator) -> Unit = { n -> + TabbedNavBar(options.index, n) + } + + @Composable + override fun Content() { + TODO("Not yet implemented") + } + + override val options: TabScreenOptions + @Composable get() { + return TabScreenOptions( + index = 6, + icon = { Icon(Icons.Default.Settings, contentDescription = "Settings", + tint = MaterialTheme.colorScheme.onBackground) }, + title = list.name + ) + } } \ No newline at end of file diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/base/tabbed/TabbedBaseScreen.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/base/tabbed/TabbedBaseScreen.kt index b008853..b099ce8 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/base/tabbed/TabbedBaseScreen.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/base/tabbed/TabbedBaseScreen.kt @@ -23,6 +23,7 @@ import androidx.compose.ui.unit.dp import cafe.adriel.voyager.core.annotation.ExperimentalVoyagerApi import cafe.adriel.voyager.core.model.rememberNavigatorScreenModel import cafe.adriel.voyager.core.screen.ScreenKey +import cafe.adriel.voyager.koin.koinNavigatorScreenModel import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.Navigator import cafe.adriel.voyager.navigator.NavigatorDisposeBehavior @@ -93,7 +94,7 @@ fun TabNavigationItem( icon = { when (tab) { is NotificationsTab -> { - val sm = LocalNavigator.currentOrThrow.rememberNavigatorScreenModel { TabbedMainScreenModel() } + val sm = navigator.koinNavigatorScreenModel() val unread by sm.unreadNotificationsCount().collectAsState(0) BadgedBox( badge = { diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/login/LoginScreen.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/login/LoginScreen.kt index 173db39..b2e1d9a 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/login/LoginScreen.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/login/LoginScreen.kt @@ -31,11 +31,8 @@ import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.PasswordVisualTransformation import androidx.compose.ui.unit.dp -import cafe.adriel.voyager.core.model.rememberNavigatorScreenModel import cafe.adriel.voyager.core.model.screenModelScope import cafe.adriel.voyager.core.screen.ScreenKey -import cafe.adriel.voyager.navigator.LocalNavigator -import cafe.adriel.voyager.navigator.currentOrThrow import cafe.adriel.voyager.navigator.tab.LocalTabNavigator import cafe.adriel.voyager.navigator.tab.Tab import cafe.adriel.voyager.navigator.tab.TabOptions @@ -49,6 +46,7 @@ import kotlinx.serialization.Serializable import morpho.composeapp.generated.resources.BlueSkyKawaii import morpho.composeapp.generated.resources.Res import org.jetbrains.compose.resources.painterResource +import org.koin.compose.koinInject @Parcelize @Serializable @@ -63,9 +61,9 @@ data object LoginScreen: Tab, Parcelable { val focusManager = LocalFocusManager.current val snackbarHostState = remember { SnackbarHostState() } val tabNavigator = LocalTabNavigator.current - val screenModel = LocalNavigator.currentOrThrow.rememberNavigatorScreenModel { LoginScreenModel() } + val screenModel = koinInject() - if(screenModel.isLoggedIn) { + if(screenModel.isLoggedIn.value) { tabNavigator.current = TabbedBaseScreen } diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/login/LoginScreenModel.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/login/LoginScreenModel.kt index bd5e738..4199ec5 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/login/LoginScreenModel.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/login/LoginScreenModel.kt @@ -4,6 +4,8 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import cafe.adriel.voyager.core.model.screenModelScope +import com.morpho.app.data.ContentLabelService +import com.morpho.app.data.MorphoAgent import com.morpho.app.model.uistate.AuthState import com.morpho.app.model.uistate.LoginState import com.morpho.app.model.uistate.UiLoadingState @@ -15,7 +17,10 @@ import com.morpho.butterfly.auth.Server import kotlinx.coroutines.launch import org.lighthousegames.logging.logging -class LoginScreenModel: BaseScreenModel() { +class LoginScreenModel( + agent: MorphoAgent, + labelService: ContentLabelService, +): BaseScreenModel(agent, labelService) { var loginState: LoginState by mutableStateOf(LoginState()) var email by mutableStateOf("") diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/main/MainScreenModel.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/main/MainScreenModel.kt index 39b8dfd..eeb3351 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/main/MainScreenModel.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/main/MainScreenModel.kt @@ -1,30 +1,35 @@ package com.morpho.app.screens.main -import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateListOf -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.setValue import app.bsky.actor.SavedFeed import cafe.adriel.voyager.core.model.screenModelScope -import com.morpho.app.model.bluesky.DetailedProfile +import com.morpho.app.data.ContentLabelService +import com.morpho.app.data.MorphoAgent import com.morpho.app.model.bluesky.FeedSourceInfo import com.morpho.app.model.bluesky.toFeedSourceInfo -import com.morpho.app.model.bluesky.toProfile import com.morpho.app.model.uidata.FeedEvent import com.morpho.app.model.uidata.FeedPresenter import com.morpho.app.model.uistate.ContentCardState import com.morpho.app.screens.base.BaseScreenModel import com.morpho.butterfly.AtUri +import com.morpho.butterfly.Did +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.emitAll import kotlinx.coroutines.flow.merge import kotlinx.coroutines.launch import org.lighthousegames.logging.logging -open class MainScreenModel: BaseScreenModel() { +open class MainScreenModel( + agent: MorphoAgent, + labelService: ContentLabelService, +): BaseScreenModel( + agent = agent, + labelService = labelService, +) { + - var userProfile: DetailedProfile? by mutableStateOf(null) - protected set val feedSources = mutableStateListOf() val feedPresenters = mutableMapOf>() @@ -40,9 +45,16 @@ open class MainScreenModel: BaseScreenModel() { val log = logging("MainScreenModel") } + private var presenterJob: Job? = null + init { - if(isLoggedIn) screenModelScope.launch { - userProfile = userDid?.let { agent.getProfile(it).getOrNull()?.toProfile() } + initialize() + } + + protected fun initialize() { + screenModelScope.launch { + while(!isLoggedIn.value) delay(10) + feedSources.addAll(pinnedFeeds.mapNotNull { feed -> feed.toFeedSourceInfo(agent).getOrNull() }) feedPresenters.putAll(feedSources.map { source -> source.uri to FeedPresenter(source.feedDescriptor) @@ -52,7 +64,7 @@ open class MainScreenModel: BaseScreenModel() { }) - screenModelScope.launch { + presenterJob = screenModelScope.launch { for(source in feedSources) { val cardState = feedStates[source.uri]?: continue val presenter = feedPresenters[source.uri] ?: continue @@ -71,6 +83,24 @@ open class MainScreenModel: BaseScreenModel() { } + override fun deinit() { + super.deinit() + feedSources.clear() + feedStates.clear() + presenterJob?.cancel() + initialized = false + } + + override fun logout() { + super.logout() + deinit() + initialize() + } + override fun switchUser(did: Did) { + super.switchUser(did) + deinit() + initialize() + } } \ No newline at end of file diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/main/tabbed/TabbedHomeView.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/main/tabbed/TabbedHomeView.kt index 337e35e..76481a9 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/main/tabbed/TabbedHomeView.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/main/tabbed/TabbedHomeView.kt @@ -17,6 +17,7 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.statusBarsPadding +import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Menu import androidx.compose.material3.DrawerState @@ -32,6 +33,7 @@ import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar import androidx.compose.material3.rememberDrawerState import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf @@ -39,10 +41,12 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue +import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.dp +import app.cash.paging.compose.collectAsLazyPagingItems import cafe.adriel.voyager.core.annotation.ExperimentalVoyagerApi import cafe.adriel.voyager.core.screen.ScreenKey import cafe.adriel.voyager.core.screen.uniqueScreenKey @@ -56,9 +60,11 @@ import cafe.adriel.voyager.navigator.tab.TabOptions import cafe.adriel.voyager.transitions.ScreenTransition import cafe.adriel.voyager.transitions.ScreenTransitionContent import coil3.annotation.ExperimentalCoilApi +import com.morpho.app.data.ContentLabelService +import com.morpho.app.data.MorphoAgent import com.morpho.app.model.uidata.Event -import com.morpho.app.model.uidata.FeedEvent import com.morpho.app.model.uistate.ContentCardState +import com.morpho.app.model.uistate.ScrollPosition import com.morpho.app.screens.base.tabbed.TabScreen import com.morpho.app.ui.common.LoadingCircle import com.morpho.app.ui.common.TabbedScreenScaffold @@ -67,12 +73,14 @@ import com.morpho.app.ui.elements.AvatarShape import com.morpho.app.ui.elements.OutlinedAvatar import dev.icerock.moko.parcelize.Parcelable import dev.icerock.moko.parcelize.Parcelize +import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.launch import kotlinx.serialization.Serializable import morpho.composeapp.generated.resources.BlueSkyKawaii import morpho.composeapp.generated.resources.Res import org.jetbrains.compose.resources.ExperimentalResourceApi import org.jetbrains.compose.resources.painterResource +import org.koin.compose.koinInject import kotlin.math.max import kotlin.math.min import cafe.adriel.voyager.navigator.tab.Tab as NavTab @@ -110,8 +118,9 @@ abstract class SkylineTab: NavTab { @OptIn(ExperimentalVoyagerApi::class) @Composable - final override fun Content() = - Content(TabbedMainScreenModel(),PaddingValues(0.dp), null, Modifier) + final override fun Content() = Content( + TabbedMainScreenModel(koinInject(), koinInject()), + PaddingValues(0.dp), null, Modifier) } @@ -120,11 +129,10 @@ abstract class SkylineTab: NavTab { ) @Composable fun TabScreen.TabbedHomeView( - sm: TabbedMainScreenModel = LocalNavigator.currentOrThrow.koinNavigatorScreenModel() + navigator: Navigator = LocalNavigator.currentOrThrow, + sm: TabbedMainScreenModel = navigator.koinNavigatorScreenModel() ) { //ProvideNavigatorLifecycleKMPSupport { - val navigator = LocalNavigator.currentOrThrow - var selectedTabIndex by rememberSaveable { mutableIntStateOf(sm.timelineIndex) } @@ -320,19 +328,40 @@ data class HomeSkylineTab @OptIn(ExperimentalVoyagerApi::class) constructor( state: ContentCardState?, modifier: Modifier ) { - val presenter = sm.feedPresenters[state?.uri] - val desc = presenter?.descriptor if(state == null) return - desc?.let { FeedEvent.Load(it) }?.let { event -> sm.sendGlobalEvent(event) } - - TabbedSkylineFragment( - paddingValues = paddingValues, - isProfileFeed = false, - uiUpdate = state.updates, - eventCallback = { sm.sendGlobalEvent(it) }, - getContentHandling = { post -> sm.labelService.getContentHandlingForPost(post).map { it.first } }, + val data = sm.tabPagers[state.uri]?.collectAsLazyPagingItems() + val listState = rememberLazyListState( + state.scrollPosition.value.index, + state.scrollPosition.value.scrollOffset ) + LaunchedEffect(listState) { + snapshotFlow { listState.firstVisibleItemIndex } + .distinctUntilChanged() + .collect { + state.scrollPosition.value = ScrollPosition( + index = listState.firstVisibleItemIndex, + scrollOffset = listState.firstVisibleItemScrollOffset + ) + } + } + + + if(data != null) { + TabbedSkylineFragment( + paddingValues = paddingValues, + isProfileFeed = false, + uiUpdate = state.updates, + eventCallback = { sm.sendGlobalEvent(it) }, + getContentHandling = { post -> sm.labelService.getContentHandlingForPost(post).map { it.first } }, + pager = data, + listState = listState, + agent = sm.agent, + ) + } else { + LoadingCircle() + } + } override val key: ScreenKey diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/main/tabbed/TabbedMainScreenModel.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/main/tabbed/TabbedMainScreenModel.kt index 49e553a..3d90dc6 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/main/tabbed/TabbedMainScreenModel.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/main/tabbed/TabbedMainScreenModel.kt @@ -2,27 +2,40 @@ package com.morpho.app.screens.main.tabbed import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateListOf +import androidx.compose.runtime.mutableStateMapOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import app.bsky.feed.GetPostThreadResponseThreadUnion +import app.cash.paging.PagingData +import app.cash.paging.cachedIn +import app.cash.paging.compose.LazyPagingItems import cafe.adriel.voyager.core.model.screenModelScope +import com.morpho.app.data.ContentLabelService +import com.morpho.app.data.MorphoAgent +import com.morpho.app.model.bluesky.MorphoDataItem import com.morpho.app.model.bluesky.toContentCardMapEntry import com.morpho.app.model.bluesky.toThread import com.morpho.app.model.uidata.ContentCardMapEntry +import com.morpho.app.model.uidata.FeedPresenter import com.morpho.app.model.uidata.ThreadUpdate import com.morpho.app.model.uistate.ContentCardState import com.morpho.app.screens.main.MainScreenModel import com.morpho.butterfly.AtUri +import com.morpho.butterfly.Did import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flow import kotlinx.coroutines.launch +import org.koin.core.KoinApplication.Companion.init import org.lighthousegames.logging.logging -class TabbedMainScreenModel : MainScreenModel() { +class TabbedMainScreenModel( + agent: MorphoAgent, labelService: ContentLabelService, +) : MainScreenModel(agent, labelService) { val tabs = mutableStateListOf() + val tabPagers = mutableStateMapOf>>() val timelineIndex: Int get() = agent.prefs.timelineIndex ?: 0 @@ -35,17 +48,23 @@ class TabbedMainScreenModel : MainScreenModel() { } init { + initializeTabs() + } + + private fun initializeTabs() { screenModelScope.launch { - while(!initialized) { - delay(10) - } - feedSources.filter { it.pinned == true }.forEach { - tabs.add(it.toContentCardMapEntry()) + while(!initialized) delay(10) + feedSources.filter { it.pinned == true }.forEach { info -> + tabs.add(info.toContentCardMapEntry()) + (feedPresenters[info.uri]?.pager?.flow?.cachedIn(screenModelScope) + as Flow>).let { + tabPagers[info.uri] = it + } + } loaded = true } - } fun uriForTab(index: Int): AtUri { @@ -66,5 +85,27 @@ class TabbedMainScreenModel : MainScreenModel() { } + override fun deinit() { + super.deinit() + tabPagers.clear() + tabs.clear() + loaded = false + } + + + override fun logout() { + super.logout() + deinit() + initialize() + initializeTabs() + } + + override fun switchUser(did: Did) { + super.switchUser(did) + deinit() + initialize() + initializeTabs() + } + } \ No newline at end of file diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/notifications/NotificationsView.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/notifications/NotificationsView.kt index 2545b28..10a5f37 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/notifications/NotificationsView.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/notifications/NotificationsView.kt @@ -43,8 +43,8 @@ import app.cash.paging.LoadStateLoading import app.cash.paging.compose.collectAsLazyPagingItems import app.cash.paging.compose.itemKey import cafe.adriel.voyager.core.annotation.ExperimentalVoyagerApi -import cafe.adriel.voyager.core.model.rememberNavigatorScreenModel import cafe.adriel.voyager.core.model.screenModelScope +import cafe.adriel.voyager.koin.koinNavigatorScreenModel import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.Navigator import cafe.adriel.voyager.navigator.currentOrThrow @@ -54,7 +54,6 @@ import com.morpho.app.model.bluesky.NotificationsListItem import com.morpho.app.model.uistate.NotificationsUIState import com.morpho.app.screens.base.tabbed.ProfileTab import com.morpho.app.screens.base.tabbed.TabScreen -import com.morpho.app.screens.base.tabbed.ThreadTab import com.morpho.app.screens.main.tabbed.TabbedMainScreenModel import com.morpho.app.ui.common.BottomSheetPostComposer import com.morpho.app.ui.common.ComposerRole @@ -81,7 +80,7 @@ fun TabScreen.NotificationViewContent( navigator: Navigator = LocalNavigator.currentOrThrow, ) { - val sm = navigator.rememberNavigatorScreenModel { TabbedMainScreenModel() } + val sm = navigator.koinNavigatorScreenModel() val numberUnread = sm.unreadNotificationsCount().value var showSettings by remember { mutableStateOf(false) } val hasUnread = remember(numberUnread) { numberUnread > 0 } @@ -208,9 +207,6 @@ fun TabScreen.NotificationViewContent( ) }, onLikeClicked = { sm.agent.like(it) }, - onPostClicked = { - navigator.push(ThreadTab(it)) - }, // If someone hides their read notifications, // we don't want to just mark them as read unprompted. // Might cause them to disappear unexpectedly. diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/profile/TabbedProfileView.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/profile/TabbedProfileView.kt index 034a511..3f2976b 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/profile/TabbedProfileView.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/profile/TabbedProfileView.kt @@ -15,6 +15,7 @@ import androidx.compose.material3.TopAppBarScrollBehavior import androidx.compose.material3.rememberDrawerState import androidx.compose.material3.rememberTopAppBarState import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.remember @@ -23,6 +24,7 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.unit.dp +import app.cash.paging.compose.collectAsLazyPagingItems import cafe.adriel.voyager.core.annotation.ExperimentalVoyagerApi import cafe.adriel.voyager.core.screen.ScreenKey import cafe.adriel.voyager.core.screen.uniqueScreenKey @@ -34,8 +36,11 @@ import cafe.adriel.voyager.navigator.tab.TabNavigator import cafe.adriel.voyager.navigator.tab.TabOptions import coil3.annotation.ExperimentalCoilApi import com.morpho.app.model.bluesky.FeedDescriptor +import com.morpho.app.model.uidata.AuthorFeedUpdate import com.morpho.app.model.uidata.Event import com.morpho.app.model.uidata.FeedEvent +import com.morpho.app.model.uidata.FeedUpdate +import com.morpho.app.model.uidata.UIUpdate import com.morpho.app.model.uistate.ContentCardState import com.morpho.app.screens.base.tabbed.TabScreen import com.morpho.app.ui.common.LoadingCircle @@ -283,12 +288,25 @@ data class ProfileSkylineTab( is ContentCardState.ProfileLabeler -> {} else -> {} } - TabbedSkylineFragment( - paddingValues, - isProfileFeed = true, - uiUpdate = state.updates, - eventCallback = eventCallback, - ) + val data = when(val feedState = state.updates.collectAsState(initial = UIUpdate.Empty).value) { + is AuthorFeedUpdate.Feed -> feedState.feed.collectAsLazyPagingItems() + is AuthorFeedUpdate.Feeds -> feedState.feed.collectAsLazyPagingItems() + is AuthorFeedUpdate.Likes -> feedState.feed.collectAsLazyPagingItems() + is AuthorFeedUpdate.Lists -> feedState.feed.collectAsLazyPagingItems() + is FeedUpdate.Feed -> feedState.feed.collectAsLazyPagingItems() + else -> null + } + if(data != null) { + TabbedSkylineFragment( + paddingValues, + isProfileFeed = true, + uiUpdate = state.updates, + eventCallback = eventCallback, + pager = data, + ) + } else { + LoadingCircle() + } } override val key: ScreenKey diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/settings/TabbedSettingsView.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/settings/TabbedSettingsView.kt index da80900..1c64ed6 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/settings/TabbedSettingsView.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/settings/TabbedSettingsView.kt @@ -28,6 +28,8 @@ import cafe.adriel.voyager.navigator.Navigator import cafe.adriel.voyager.navigator.currentOrThrow import cafe.adriel.voyager.transitions.ScreenTransition import cafe.adriel.voyager.transitions.ScreenTransitionContent +import com.morpho.app.data.ContentLabelService +import com.morpho.app.data.MorphoAgent import com.morpho.app.screens.base.tabbed.SettingsTab import com.morpho.app.screens.base.tabbed.TabbedNavBar import com.morpho.app.screens.main.tabbed.TabbedMainScreenModel @@ -42,6 +44,7 @@ import dev.icerock.moko.parcelize.Parcelable import dev.icerock.moko.parcelize.Parcelize import kotlinx.serialization.Contextual import kotlinx.serialization.Serializable +import org.koin.compose.koinInject @Composable public fun CurrentSettingsScreen( @@ -79,7 +82,8 @@ abstract class SettingsScreen: Screen { @OptIn(ExperimentalVoyagerApi::class) @Composable final override fun Content() = - Content(TabbedMainScreenModel(), LocalNavigator.currentOrThrow, Modifier) + Content(TabbedMainScreenModel(koinInject(), koinInject()), + LocalNavigator.currentOrThrow, Modifier) } @OptIn(ExperimentalMaterial3Api::class) @@ -115,9 +119,9 @@ data object SettingsRootPage: SettingsScreen(), Parcelable { navBar = { navBar(parentNav) }, content = { insets, nav -> SettingsFragment( - agent = sm.agent, modifier = Modifier.padding(insets), - navigator = nav!! + navigator = nav!!, + sm = sm, ) }, topContent = { @@ -218,7 +222,7 @@ data object NotificationsSettingsScreen: SettingsScreen(), Parcelable { navBar = { navBar(parentNav) }, content = { insets, nav -> SettingsFragment( - agent = sm.agent, + sm = sm, modifier = Modifier.padding(insets), navigator = nav!! ) @@ -356,7 +360,7 @@ data object ThreadSettingsScreen: SettingsScreen(), Parcelable { navBar = { navBar(parentNav) }, content = { insets, nav -> SettingsFragment( - agent = sm.agent, + sm = sm, modifier = Modifier.padding(insets), navigator = nav!! ) diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/thread/ThreadView.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/thread/ThreadView.kt index 18e092a..ef6b6ff 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/thread/ThreadView.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/thread/ThreadView.kt @@ -23,6 +23,7 @@ import androidx.compose.ui.unit.dp import cafe.adriel.voyager.core.annotation.ExperimentalVoyagerApi import cafe.adriel.voyager.core.model.rememberNavigatorScreenModel import cafe.adriel.voyager.core.model.screenModelScope +import cafe.adriel.voyager.koin.koinNavigatorScreenModel import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.Navigator import cafe.adriel.voyager.navigator.currentOrThrow @@ -44,6 +45,7 @@ import com.morpho.app.ui.common.RepostQueryDialog import com.morpho.app.ui.common.TabbedScreenScaffold import com.morpho.app.ui.elements.doMenuOperation import com.morpho.app.ui.thread.ThreadFragment +import com.morpho.app.ui.utils.ItemClicked import com.morpho.app.util.ClipboardManager import com.morpho.butterfly.AtIdentifier import com.morpho.butterfly.AtUri @@ -65,7 +67,7 @@ fun TabScreen.ThreadViewContent( navigator: Navigator = LocalNavigator.currentOrThrow, ) { - val sm = navigator.rememberNavigatorScreenModel { MainScreenModel() } + val sm = navigator.koinNavigatorScreenModel() val threadState by cardState.updates.filterIsInstance().collectAsState(ThreadUpdate.Empty) TabbedScreenScaffold( @@ -140,31 +142,35 @@ fun ThreadView( val clipboard = getKoin().get() val scope = rememberCoroutineScope() val uriHandler = LocalUriHandler.current - ThreadFragment(thread = thread, - contentPadding = insets, - onItemClicked = { navigator.push(ThreadTab(it)) }, - onProfileClicked = { - scope.launch { - val did = resolveHandle(it) - if(did != null) navigator.push(ProfileTab(did)) - } - }, - onUnClicked = {type, uri -> deleteRecord(type, uri)}, - onRepostClicked = { - initialContent = it - repostClicked = true - }, - onReplyClicked = { - initialContent = it - composerRole = ComposerRole.Reply - showComposer = true - }, - onMenuClicked = { option, post -> - doMenuOperation(option, post, - clipboardManager = clipboard, - uriHandler = uriHandler - ) }, - onLikeClicked = { createRecord(RecordUnion.Like(it)) }, + ThreadFragment( + thread = thread, + contentPadding = insets, + onItemClicked = ItemClicked( + uriHandler = uriHandler, + navigator = navigator, + ), + onProfileClicked = { + scope.launch { + val did = resolveHandle(it) + if(did != null) navigator.push(ProfileTab(did)) + } + }, + onUnClicked = { type, uri -> deleteRecord(type, uri) }, + onRepostClicked = { + initialContent = it + repostClicked = true + }, + onReplyClicked = { + initialContent = it + composerRole = ComposerRole.Reply + showComposer = true + }, + onMenuClicked = { option, post -> + doMenuOperation(option, post, + clipboardManager = clipboard, + uriHandler = uriHandler + ) }, + onLikeClicked = { createRecord(RecordUnion.Like(it)) }, ) if(repostClicked) { RepostQueryDialog( diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/common/NavDrawer.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/common/NavDrawer.kt index 4f2de47..e6befaa 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/common/NavDrawer.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/common/NavDrawer.kt @@ -38,7 +38,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp -import cafe.adriel.voyager.core.model.rememberNavigatorScreenModel +import cafe.adriel.voyager.koin.koinNavigatorScreenModel import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.Navigator import cafe.adriel.voyager.navigator.currentOrThrow @@ -250,7 +250,7 @@ fun ColumnScope.NavDrawerItems( NavDrawerItem(HomeTab("home"), drawerState = drawerState, navigator = navigator) NavDrawerItem(NotificationsTab, drawerState = drawerState, navigator = navigator, badge = { - val sm = LocalNavigator.currentOrThrow.rememberNavigatorScreenModel { TabbedMainScreenModel() } + val sm = LocalNavigator.currentOrThrow.koinNavigatorScreenModel() val unread by sm.unreadNotificationsCount().collectAsState(0) if(unread > 0) { Badge( diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/common/SkylineFragment.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/common/SkylineFragment.kt index 1722784..82ac16c 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/common/SkylineFragment.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/common/SkylineFragment.kt @@ -18,6 +18,7 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.safeContent import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.systemBarsPadding +import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material.icons.Icons @@ -48,25 +49,39 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.unit.dp import androidx.constraintlayout.compose.ConstraintLayout +import app.bsky.actor.Visibility import app.cash.paging.LoadStateError import app.cash.paging.LoadStateLoading +import app.cash.paging.compose.LazyPagingItems import app.cash.paging.compose.collectAsLazyPagingItems +import app.cash.paging.compose.itemKey +import cafe.adriel.voyager.navigator.LocalNavigator +import cafe.adriel.voyager.navigator.currentOrThrow import com.atproto.repo.StrongRef import com.morpho.app.model.bluesky.BskyPost import com.morpho.app.model.bluesky.MorphoDataItem +import com.morpho.app.model.bluesky.NotificationsListItem import com.morpho.app.model.uidata.AuthorFeedUpdate import com.morpho.app.model.uidata.FeedUpdate import com.morpho.app.model.uidata.UIUpdate import com.morpho.app.ui.elements.MenuOptions import com.morpho.app.ui.elements.WrappedLazyColumn +import com.morpho.app.ui.lists.FeedListEntryFragment +import com.morpho.app.ui.lists.UserListEntryFragment import com.morpho.app.ui.post.PlaceholderSkylineItem import com.morpho.app.ui.post.PostFragment +import com.morpho.app.ui.profile.CompactProfileFragment +import com.morpho.app.ui.settings.ContentLabelSelector +import com.morpho.app.ui.utils.ItemClicked +import com.morpho.app.ui.utils.OnItemClicked import com.morpho.butterfly.AtIdentifier import com.morpho.butterfly.AtUri import com.morpho.butterfly.ContentHandling import com.morpho.butterfly.model.RecordType +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.filterIsInstance import kotlinx.coroutines.launch @@ -76,42 +91,27 @@ typealias OnPostClicked = (AtUri) -> Unit @OptIn(ExperimentalMaterialApi::class, ExperimentalFoundationApi::class) @Composable -inline fun SkylineFragment ( +fun SkylineFragment ( modifier: Modifier = Modifier, - noinline onItemClicked: OnPostClicked, - noinline onProfileClicked: (AtIdentifier) -> Unit = {}, - crossinline onPostButtonClicked: () -> Unit = {}, - noinline onReplyClicked: (BskyPost) -> Unit = { }, - noinline onRepostClicked: (BskyPost) -> Unit = { }, - noinline onLikeClicked: (StrongRef) -> Unit = { }, - noinline onMenuClicked: (MenuOptions, BskyPost) -> Unit = { _, _ -> }, - noinline onUnClicked: (type: RecordType, uri: AtUri) -> Unit = { _, _ -> }, - noinline getContentHandling: (BskyPost) -> List = { listOf() }, + onItemClicked: OnItemClicked = ItemClicked( + uriHandler = LocalUriHandler.current, + navigator = LocalNavigator.currentOrThrow, + ), + onPostButtonClicked: () -> Unit = {}, + onReplyClicked: (BskyPost) -> Unit = { }, + onRepostClicked: (BskyPost) -> Unit = { }, + onLikeClicked: (StrongRef) -> Unit = { }, + onMenuClicked: (MenuOptions, BskyPost) -> Unit = { _, _ -> }, + onUnClicked: (type: RecordType, uri: AtUri) -> Unit = { _, _ -> }, + onLabelChoiceSelected: (Visibility) -> Unit = { }, + getContentHandling: (BskyPost) -> List = { listOf() }, contentPadding: PaddingValues = PaddingValues(0.dp), isProfileFeed: Boolean = false, debuggable: Boolean = false, - feedUpdate: Flow, + pager: LazyPagingItems, + listState: LazyListState = rememberLazyListState(), + scope: CoroutineScope = rememberCoroutineScope(), ) { - val scope = rememberCoroutineScope() - - val listState = rememberLazyListState() - val state = feedUpdate.filterIsInstance().collectAsState( - when(feedUpdate) { - is AuthorFeedUpdate -> AuthorFeedUpdate.Empty - is FeedUpdate<*> -> FeedUpdate.Empty - else -> UIUpdate.Empty - } - ) - val data = when(state.value) { - is AuthorFeedUpdate.Feed -> (state.value as AuthorFeedUpdate.Feed).feed.collectAsLazyPagingItems() - is AuthorFeedUpdate.Feeds -> (state.value as AuthorFeedUpdate.Feeds).feed.collectAsLazyPagingItems() - is AuthorFeedUpdate.Likes -> (state.value as AuthorFeedUpdate.Likes).feed.collectAsLazyPagingItems() - is AuthorFeedUpdate.Lists -> (state.value as AuthorFeedUpdate.Lists).feed.collectAsLazyPagingItems() - is FeedUpdate.Feed -> (state.value as FeedUpdate.Feed).feed.collectAsLazyPagingItems() - else -> null - } - - val scrolledDownSome by remember { derivedStateOf { @@ -127,7 +127,7 @@ inline fun SkylineFragment ( val refreshing by remember { mutableStateOf(false) } - val refreshState = rememberPullRefreshState(refreshing, {data?.refresh()}) + val refreshState = rememberPullRefreshState(refreshing, {pager.refresh()}) ConstraintLayout( @@ -185,7 +185,7 @@ inline fun SkylineFragment ( .weight(0.4f) ) IconButton( - onClick = {data?.refresh()}, + onClick = {pager.refresh()}, colors = IconButtonDefaults.iconButtonColors( containerColor = MaterialTheme.colorScheme.background, contentColor = MaterialTheme.colorScheme.onSurfaceVariant @@ -219,29 +219,39 @@ inline fun SkylineFragment ( } } } - when(val loadState = data?.loadState?.refresh) { - is LoadStateError -> { - item { Text("Error: ${loadState.error}") } + val refreshLoadState = pager.loadState.refresh + val appendLoadState = pager.loadState.append + + when { + refreshLoadState is LoadStateError || appendLoadState is LoadStateError -> { + item { Text("$refreshLoadState\n$appendLoadState") } item { Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.Center) { - TextButton(onClick = { data.retry() }) { + TextButton(onClick = { pager.retry() }) { Text("Retry") } } } } - is LoadStateLoading -> { item { LoadingCircle() } } - else -> { if(data != null) { + refreshLoadState is LoadStateLoading -> { item { LoadingCircle() } } + else -> { items( - data.itemCount, + count = pager.itemCount, + key = { pager.itemKey { + it.hashCode() + }}, + contentType = { + MorphoDataItem + } ) { index -> - when(val item = data[index]) { + when(val item = pager[index]) { is MorphoDataItem.Thread -> { SkylineThreadFragment( thread = item.thread, - modifier = if(debuggable) Modifier.border(1.dp, Color.White) else Modifier + modifier = if(debuggable) Modifier.border(1.dp, Color.White) + else Modifier .fillMaxWidth() //.padding(horizontal = 4.dp), .padding(vertical = 2.dp, horizontal = 4.dp), onItemClicked = onItemClicked, - onProfileClicked = onProfileClicked, + onProfileClicked = { onItemClicked.onProfileClicked(it) }, onUnClicked = onUnClicked, onRepostClicked = onRepostClicked, onReplyClicked = onReplyClicked, @@ -253,13 +263,14 @@ inline fun SkylineFragment ( } is MorphoDataItem.Post -> { PostFragment( - modifier = if(debuggable) Modifier.border(1.dp, Color.Blue) else Modifier + modifier = if(debuggable) Modifier.border(1.dp, Color.Blue) + else Modifier .fillMaxWidth() //.padding(horizontal = 4.dp), .padding(vertical = 2.dp, horizontal = 4.dp), post = item.post, onItemClicked = onItemClicked, - onProfileClicked = onProfileClicked, + onProfileClicked = { onItemClicked.onProfileClicked(it) }, elevate = true, onUnClicked = onUnClicked, onRepostClicked = onRepostClicked, @@ -269,10 +280,45 @@ inline fun SkylineFragment ( getContentHandling = getContentHandling, ) } + is MorphoDataItem.FeedInfo -> { + FeedListEntryFragment( + feed = item.feed, + onFeedClicked = { + + }, + likeClicked = { _ , _ -> }, + saveFeedClicked = { _, _ -> }, + hasFeedSaved = false, + ) + + } + is MorphoDataItem.ListInfo -> { + UserListEntryFragment( + list = item.list, + onListClicked = { }, + hasListPinned = false, + muteListClicked = { _, _ -> }, + blockListClicked = { _, _ -> }, + ) + } + is MorphoDataItem.ModLabel -> { + ContentLabelSelector( + labelItem = item, + onSelected = onLabelChoiceSelected + ) + } + is MorphoDataItem.ProfileItem -> { + CompactProfileFragment( + profile = item.profile, + onProfileClicked = { onItemClicked.onProfileClicked(it) }, + onItemClicked = onItemClicked, + ) + } else -> { PlaceholderSkylineItem( - modifier = if(debuggable) Modifier.border(1.dp, Color.Black) else Modifier + modifier = if(debuggable) Modifier.border(1.dp, Color.Black) + else Modifier .fillMaxWidth() //.padding(horizontal = 4.dp), .padding(vertical = 2.dp, horizontal = 4.dp), @@ -280,10 +326,9 @@ inline fun SkylineFragment ( ) } } - } } + } } } - if (data?.loadState?.append == LoadStateLoading) item { LoadingCircle() } } if (scrolledDownSome) { diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/common/SkylineThreadFragment.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/common/SkylineThreadFragment.kt index 8bf8e5d..9a4947b 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/common/SkylineThreadFragment.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/common/SkylineThreadFragment.kt @@ -10,9 +10,12 @@ import androidx.compose.runtime.* import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.unit.dp import androidx.compose.ui.util.fastForEach import androidx.compose.ui.util.fastForEachIndexed +import cafe.adriel.voyager.navigator.LocalNavigator +import cafe.adriel.voyager.navigator.currentOrThrow import com.atproto.repo.StrongRef import com.morpho.app.model.bluesky.BskyPost import com.morpho.app.model.bluesky.BskyPostThread @@ -22,6 +25,8 @@ import com.morpho.app.ui.post.PostFragment import com.morpho.app.ui.post.PostFragmentRole import com.morpho.app.ui.thread.ThreadItem import com.morpho.app.ui.thread.ThreadTree +import com.morpho.app.ui.utils.ItemClicked +import com.morpho.app.ui.utils.OnItemClicked import com.morpho.butterfly.AtIdentifier import com.morpho.butterfly.AtUri import com.morpho.butterfly.ContentHandling @@ -31,7 +36,10 @@ import com.morpho.butterfly.model.RecordType inline fun SkylineThreadFragment( thread: BskyPostThread, modifier: Modifier = Modifier, - crossinline onItemClicked: OnPostClicked = {}, + onItemClicked: OnItemClicked = ItemClicked( + uriHandler = LocalUriHandler.current, + navigator = LocalNavigator.currentOrThrow, + ), crossinline onProfileClicked: (AtIdentifier) -> Unit = {}, crossinline onReplyClicked: (BskyPost) -> Unit = { }, crossinline onRepostClicked: (BskyPost) -> Unit = { }, @@ -68,7 +76,7 @@ inline fun SkylineThreadFragment( role = PostFragmentRole.Solo, elevate = true, modifier = if(debuggable) Modifier.border(1.dp, Color.Cyan) else Modifier, - onItemClicked = {onItemClicked(it) }, + onItemClicked = onItemClicked, onProfileClicked = { onProfileClicked(it) }, onUnClicked = { type,uri-> onUnClicked(type,uri) }, onRepostClicked = { onRepostClicked(it) }, @@ -340,7 +348,7 @@ inline fun SkylineThreadFragment( ThreadTree( reply = post, indentLevel = 1, modifier = Modifier.padding(4.dp), - onItemClicked = {onItemClicked(it) }, + onItemClicked = onItemClicked, onProfileClicked = { onProfileClicked(it) }, onUnClicked = { type,uri-> onUnClicked(type,uri) }, onRepostClicked = { onRepostClicked(it) }, diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/common/TabbedSkylineFragment.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/common/TabbedSkylineFragment.kt index 22d7e1c..485abc6 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/common/TabbedSkylineFragment.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/common/TabbedSkylineFragment.kt @@ -1,6 +1,8 @@ package com.morpho.app.ui.common import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable @@ -12,6 +14,7 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.unit.dp +import app.cash.paging.compose.LazyPagingItems import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.currentOrThrow import cafe.adriel.voyager.navigator.tab.TabNavigator @@ -19,14 +22,17 @@ import com.atproto.repo.StrongRef import com.morpho.app.data.MorphoAgent import com.morpho.app.model.bluesky.BskyPost import com.morpho.app.model.bluesky.DraftPost +import com.morpho.app.model.bluesky.MorphoDataItem import com.morpho.app.model.uidata.Event import com.morpho.app.model.uidata.UIUpdate import com.morpho.app.screens.base.tabbed.ProfileTab import com.morpho.app.screens.base.tabbed.ThreadTab import com.morpho.app.ui.elements.doMenuOperation +import com.morpho.app.ui.utils.ItemClicked import com.morpho.app.util.ClipboardManager import com.morpho.butterfly.ContentHandling import io.ktor.util.reflect.instanceOf +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.IO import kotlinx.coroutines.flow.StateFlow @@ -42,13 +48,15 @@ fun TabbedSkylineFragment( uiUpdate: StateFlow, eventCallback: (Event) -> Unit = {}, getContentHandling: (BskyPost) -> List = { listOf() }, + pager: LazyPagingItems, + listState: LazyListState = rememberLazyListState(), + scope: CoroutineScope = rememberCoroutineScope(), + agent: MorphoAgent = getKoin().get(), ) { - val agent = getKoin().get() val uiState = uiUpdate.collectAsState(initial = UIUpdate.Empty) val navigator = if (LocalNavigator.current?.parent?.instanceOf(TabNavigator::class) == true) { LocalNavigator.currentOrThrow } else LocalNavigator.currentOrThrow.parent!! - val scope = rememberCoroutineScope() var repostClicked by remember { mutableStateOf(false) } var initialContent: BskyPost? by remember { mutableStateOf(null) } var showComposer by remember { mutableStateOf(false) } @@ -82,13 +90,16 @@ fun TabbedSkylineFragment( val clipboard = getKoin().get() if(uiState.value !is UIUpdate.Empty) { SkylineFragment( - onProfileClicked = { actor -> - scope.launch { - val did = agent.resolveHandle(actor).getOrNull() - if(did != null) navigator.push(ProfileTab(did)) - } - }, - onItemClicked = { uri -> navigator.push(ThreadTab(uri)) }, + onItemClicked = ItemClicked( + uriHandler = LocalUriHandler.current, + navigator = navigator, + profileCallback = { actor -> + scope.launch { + val did = agent.resolveHandle(actor).getOrNull() + if(did != null) navigator.push(ProfileTab(did)) + } + }, + ), onUnClicked = { type, rkey -> agent.deleteRecord(type, rkey) }, onRepostClicked = { onRepostClicked(it) }, onMenuClicked = { option, post -> @@ -102,7 +113,9 @@ fun TabbedSkylineFragment( getContentHandling = { post -> getContentHandling(post) }, contentPadding = paddingValues, isProfileFeed = isProfileFeed, - feedUpdate = uiUpdate.filterIsInstance(), + pager = pager, + listState = listState, + scope = scope, ) if(repostClicked) { RepostQueryDialog( diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/elements/RichText.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/elements/RichText.kt index f818e47..2a9e33b 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/elements/RichText.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/elements/RichText.kt @@ -29,6 +29,8 @@ import coil3.request.crossfade import com.morpho.app.model.bluesky.BskyFacet import com.morpho.app.model.bluesky.FacetType import com.morpho.app.model.bluesky.RichTextFormat.* +import com.morpho.app.util.BlueskyText +import com.morpho.app.util.makeBlueskyText import com.morpho.app.util.utf16FacetIndex import kotlinx.collections.immutable.persistentListOf import okio.ByteString.Companion.encodeUtf8 @@ -39,12 +41,15 @@ fun RichTextElement( text: String, modifier: Modifier = Modifier, facets: List = persistentListOf(), - onClick: (List) -> Unit = {}, maxLines: Int = 20, + onClick: (List) -> Unit = {}, ) { val layoutResult = remember { mutableStateOf(null) } - val utf8Text = text.encodeUtf8() - val splitText = text.split("◌").listIterator() // special BlueMoji character + val bskyText = if(facets.isEmpty()) { + makeBlueskyText(text) + } else BlueskyText(text, facets) + val utf8Text = bskyText.text.encodeUtf8() + val splitText = bskyText.text.split("◌").listIterator() // special BlueMoji character val formattedText = buildAnnotatedString { pushStyle(SpanStyle(MaterialTheme.colorScheme.onSurface)) pushStyle(SpanStyle( @@ -53,7 +58,8 @@ fun RichTextElement( fontSize = MaterialTheme.typography.bodyMedium.fontSize, )) append(splitText.next()) - facets.fastForEach { facet -> + + bskyText.facets.fastForEach { facet -> val bounds = text.utf16FacetIndex(utf8Text, facet.start, facet.end) val start = bounds.first val end = bounds.second @@ -173,7 +179,7 @@ fun RichTextElement( .pointerInput(onClick) { detectTapGestures( - onLongPress ={ + onLongPress = { } ) { pos -> diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/elements/SettingsItems.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/elements/SettingsItems.kt index 41291b0..9934e86 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/elements/SettingsItems.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/elements/SettingsItems.kt @@ -62,7 +62,7 @@ fun SettingsGroup( @Composable -fun ColumnScope.SettingsItem( +fun SettingsItem( text: AnnotatedString? = null, description: AnnotatedString? = null, modifier: Modifier = Modifier, diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/notifications/NotificationAvatarList.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/notifications/NotificationAvatarList.kt index fa4de6e..41aa9f6 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/notifications/NotificationAvatarList.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/notifications/NotificationAvatarList.kt @@ -1,7 +1,12 @@ package com.morpho.app.ui.notifications import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.* +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.ExpandLess import androidx.compose.material.icons.filled.ExpandMore @@ -9,7 +14,11 @@ import androidx.compose.material3.Icon import androidx.compose.material3.IconToggleButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text -import androidx.compose.runtime.* +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.text.SpanStyle @@ -19,6 +28,7 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.withStyle import androidx.compose.ui.unit.dp import com.morpho.app.model.bluesky.NotificationsListItem +import com.morpho.app.ui.elements.AvatarShape import com.morpho.app.ui.elements.OutlinedAvatar import com.morpho.butterfly.Did import kotlin.math.min @@ -43,6 +53,7 @@ fun NotificationAvatarList( OutlinedAvatar( url = it.author.avatar.orEmpty(), onClicked = { onClicked(it.author.did) }, + avatarShape = AvatarShape.Rounded, modifier = Modifier.padding(4.dp) ) } @@ -50,6 +61,7 @@ fun NotificationAvatarList( OutlinedAvatar( url = item.notifications.first().author.avatar.orEmpty(), onClicked = { onClicked(item.notifications.first().author.did) }, + avatarShape = AvatarShape.Rounded, modifier = Modifier.padding(4.dp) ) } diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/notifications/NotificationsElement.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/notifications/NotificationsElement.kt index 231128e..b0f70ef 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/notifications/NotificationsElement.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/notifications/NotificationsElement.kt @@ -1,24 +1,39 @@ package com.morpho.app.ui.notifications -import androidx.compose.foundation.layout.* +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.wrapContentSize import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.ExpandLess import androidx.compose.material.icons.filled.ExpandMore import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.IconToggleButton -import androidx.compose.runtime.* +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp +import cafe.adriel.voyager.navigator.LocalNavigator +import cafe.adriel.voyager.navigator.currentOrThrow import com.atproto.repo.StrongRef import com.morpho.app.model.bluesky.BskyNotification import com.morpho.app.model.bluesky.BskyPost import com.morpho.app.model.bluesky.NotificationsListItem -import com.morpho.app.ui.common.OnPostClicked import com.morpho.app.ui.elements.MenuOptions import com.morpho.app.ui.post.PostFragment +import com.morpho.app.ui.utils.ItemClicked import com.morpho.app.util.getFormattedDateTimeSince import com.morpho.butterfly.AtIdentifier import com.morpho.butterfly.AtUri @@ -31,7 +46,10 @@ fun NotificationsElement( item: NotificationsListItem, showPost: Boolean = true, getPost: suspend (AtUri) -> BskyPost?, - onPostClicked: OnPostClicked, + onItemClicked: ItemClicked = ItemClicked( + uriHandler = LocalUriHandler.current, + navigator = LocalNavigator.currentOrThrow, + ), onAvatarClicked: (Did) -> Unit = {}, onReplyClicked: (BskyPost) -> Unit = { }, onRepostClicked: (BskyPost) -> Unit = { }, @@ -137,10 +155,9 @@ fun NotificationsElement( PostFragment( post = post!!, elevate = true, - onItemClicked = { - if(!readOnLoad) markRead(item.notifications.first().uri) - onPostClicked(it) - }, + onItemClicked = onItemClicked.copy( + callbackAlways = { if(!readOnLoad) markRead(item.notifications.first().uri) } + ), onProfileClicked = { if(!readOnLoad) markRead(item.notifications.first().uri) scope.launch { diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/post/FullPostFragment.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/post/FullPostFragment.kt index 1a66c3c..959f5b5 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/post/FullPostFragment.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/post/FullPostFragment.kt @@ -3,7 +3,15 @@ package com.morpho.app.ui.post import androidx.compose.foundation.background import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.* +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.ExperimentalLayoutApi +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.wrapContentWidth import androidx.compose.foundation.text.selection.DisableSelection import androidx.compose.foundation.text.selection.SelectionContainer import androidx.compose.material.icons.Icons @@ -12,7 +20,11 @@ import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text -import androidx.compose.runtime.* +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalUriHandler @@ -24,13 +36,28 @@ import androidx.compose.ui.text.withStyle import androidx.compose.ui.unit.dp import androidx.compose.ui.util.fastAny import androidx.compose.ui.util.fastForEach +import cafe.adriel.voyager.navigator.LocalNavigator +import cafe.adriel.voyager.navigator.currentOrThrow import com.atproto.label.Blurs import com.atproto.repo.StrongRef import com.morpho.app.model.bluesky.BskyPost import com.morpho.app.model.bluesky.FacetType -import com.morpho.app.ui.elements.* -import com.morpho.app.util.openBrowser -import com.morpho.butterfly.* +import com.morpho.app.ui.elements.ContentHider +import com.morpho.app.ui.elements.MenuOptions +import com.morpho.app.ui.elements.OutlinedAvatar +import com.morpho.app.ui.elements.PostMenu +import com.morpho.app.ui.elements.RichTextElement +import com.morpho.app.ui.elements.WrappedColumn +import com.morpho.app.ui.utils.ItemClicked +import com.morpho.app.ui.utils.OnFacetClicked +import com.morpho.app.ui.utils.OnItemClicked +import com.morpho.app.ui.utils.OnPostClicked +import com.morpho.butterfly.AtIdentifier +import com.morpho.butterfly.AtUri +import com.morpho.butterfly.ContentHandling +import com.morpho.butterfly.LabelAction +import com.morpho.butterfly.LabelDescription +import com.morpho.butterfly.LabelIcon import com.morpho.butterfly.model.RecordType import kotlinx.collections.immutable.toImmutableList import kotlinx.datetime.TimeZone @@ -42,7 +69,10 @@ import org.jetbrains.compose.resources.ExperimentalResourceApi fun FullPostFragment( post: BskyPost, modifier: Modifier = Modifier, - onItemClicked: (AtUri) -> Unit = {}, + onItemClicked: OnItemClicked = ItemClicked( + uriHandler = LocalUriHandler.current, + navigator = LocalNavigator.currentOrThrow, + ), onProfileClicked: (AtIdentifier) -> Unit = {}, onReplyClicked: (BskyPost) -> Unit = { }, onRepostClicked: (BskyPost) -> Unit = { }, @@ -67,8 +97,13 @@ fun FullPostFragment( getContentHandling(post) }.toImmutableList() } - val uriHandler = LocalUriHandler.current + val onPostClicked: OnPostClicked = remember { { uri -> + onItemClicked.onRichTextFacetClicked(uri = uri) + } } + val onFacetClicked: OnFacetClicked = remember { { facet -> + onItemClicked.onRichTextFacetClicked(facet = facet) + } } WrappedColumn( modifier @@ -159,7 +194,7 @@ fun FullPostFragment( text = post.text, facets = post.facets, //modifier = Modifier.padding(bottom = 2.dp).padding(start = 0.dp, end = 6.dp), - onItemClicked = { onItemClicked(post.uri) }, + onItemClicked = { onPostClicked(post.uri) }, onProfileClicked = onProfileClicked, getContentHandling = getContentHandling ) @@ -169,24 +204,11 @@ fun FullPostFragment( facets = post.facets, onClick = { facetTypes -> if (facetTypes.isEmpty()) { - onItemClicked(post.uri) + onPostClicked(post.uri) return@RichTextElement } facetTypes.fastForEach { - when(it) { - is FacetType.ExternalLink -> { - openBrowser(it.uri.uri, uriHandler) - } - is FacetType.Tag -> {onItemClicked(post.uri)} - is FacetType.UserDidMention -> { - onProfileClicked(it.did) - } - is FacetType.UserHandleMention -> { - onProfileClicked(it.handle) - } - - else -> {} - } + onFacetClicked(it) } }, ) diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/post/PostFragment.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/post/PostFragment.kt index 2b04281..192acf5 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/post/PostFragment.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/post/PostFragment.kt @@ -4,11 +4,26 @@ package com.morpho.app.ui.post import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource -import androidx.compose.foundation.layout.* +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.foundation.layout.ExperimentalLayoutApi +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.offset +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.wrapContentWidth import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.Reply import androidx.compose.material.icons.filled.Repeat -import androidx.compose.material3.* +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.material3.surfaceColorAtElevation import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Alignment @@ -23,16 +38,37 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.withStyle import androidx.compose.ui.unit.dp import androidx.compose.ui.util.fastAny +import cafe.adriel.voyager.navigator.LocalNavigator +import cafe.adriel.voyager.navigator.currentOrThrow import com.atproto.label.Blurs import com.atproto.repo.StrongRef -import com.morpho.app.model.bluesky.* +import com.morpho.app.model.bluesky.BskyPost +import com.morpho.app.model.bluesky.BskyPostFeature +import com.morpho.app.model.bluesky.BskyPostReason +import com.morpho.app.model.bluesky.EmbedRecord +import com.morpho.app.model.bluesky.FacetType +import com.morpho.app.model.bluesky.TimelinePostMedia import com.morpho.app.ui.common.OnPostClicked -import com.morpho.app.ui.elements.* +import com.morpho.app.ui.elements.AvatarShape +import com.morpho.app.ui.elements.ContentHider +import com.morpho.app.ui.elements.MenuOptions +import com.morpho.app.ui.elements.MorphoHighlightIndication +import com.morpho.app.ui.elements.OutlinedAvatar +import com.morpho.app.ui.elements.RichTextElement +import com.morpho.app.ui.elements.WrappedColumn import com.morpho.app.ui.lists.FeedListEntryFragment import com.morpho.app.ui.lists.UserListEntryFragment +import com.morpho.app.ui.utils.ItemClicked +import com.morpho.app.ui.utils.OnFacetClicked +import com.morpho.app.ui.utils.OnItemClicked import com.morpho.app.util.getFormattedDateTimeSince import com.morpho.app.util.openBrowser -import com.morpho.butterfly.* +import com.morpho.butterfly.AtIdentifier +import com.morpho.butterfly.AtUri +import com.morpho.butterfly.ContentHandling +import com.morpho.butterfly.LabelAction +import com.morpho.butterfly.LabelDescription +import com.morpho.butterfly.LabelIcon import com.morpho.butterfly.model.RecordType import kotlinx.collections.immutable.toImmutableList import morpho.app.ui.utils.indentLevel @@ -49,7 +85,10 @@ fun PostFragment( role: PostFragmentRole = PostFragmentRole.Solo, indentLevel: Int = 0, elevate: Boolean = false, - onItemClicked: OnPostClicked = {}, + onItemClicked: OnItemClicked = ItemClicked( + uriHandler = LocalUriHandler.current, + navigator = LocalNavigator.currentOrThrow, + ), onProfileClicked: (AtIdentifier) -> Unit = {}, onReplyClicked: (BskyPost) -> Unit = { }, onRepostClicked: (BskyPost) -> Unit = { }, @@ -67,7 +106,6 @@ fun PostFragment( PostFragmentRole.ThreadRootUnfocused -> Modifier.padding(2.dp) PostFragmentRole.ThreadEnd -> Modifier.padding(start = 2.dp, top = 0.dp, end = 2.dp, bottom = 2.dp) }} - val uriHandler = LocalUriHandler.current WrappedColumn(modifier = modifier.then(padding.fillMaxWidth())) { val delta = remember { getFormattedDateTimeSince(post.createdAt) } val indent = remember { when(role) { @@ -102,6 +140,12 @@ fun PostFragment( getContentHandling(post) }.toImmutableList() } + val onPostClicked: OnPostClicked = remember { { uri -> + onItemClicked.onRichTextFacetClicked(uri = uri) + } } + val onFacetClicked: OnFacetClicked = remember { { facet -> + onItemClicked.onRichTextFacetClicked(facet = facet) + } } Surface ( shadowElevation = if (elevate || indentLevel > 0) 2.dp else 0.dp, @@ -116,7 +160,7 @@ fun PostFragment( interactionSource = interactionSource, indication = indication, enabled = true, - onClick = { onItemClicked(post.uri) } + onClick = { onPostClicked(post.uri) } ) ) { @@ -246,7 +290,7 @@ fun PostFragment( text = post.text, facets = post.facets, //modifier = Modifier.padding(bottom = 2.dp).padding(start = 0.dp, end = 6.dp), - onItemClicked = { onItemClicked(post.uri) }, + onItemClicked = { onPostClicked(post.uri) }, onProfileClicked = onProfileClicked, getContentHandling = getContentHandling ) @@ -257,24 +301,11 @@ fun PostFragment( modifier = Modifier.padding(end = 2.dp), onClick = { facetTypes -> if (facetTypes.isEmpty()) { - onItemClicked(post.uri) + onPostClicked(post.uri) return@RichTextElement } facetTypes.forEach { - when(it) { - is FacetType.ExternalLink -> { - openBrowser(it.uri.uri, uriHandler) - } - is FacetType.Tag -> {onItemClicked(post.uri)} - is FacetType.UserDidMention -> { - onProfileClicked(it.did) - } - is FacetType.UserHandleMention -> { - onProfileClicked(it.handle) - } - - else -> {} - } + onFacetClicked(it) } }, ) @@ -328,7 +359,10 @@ internal inline fun ReplyIndicator( @Composable inline fun ColumnScope.PostFeatureElement( feature: BskyPostFeature? = null, - crossinline onItemClicked: OnPostClicked = {}, + onItemClicked: OnItemClicked = ItemClicked( + uriHandler = LocalUriHandler.current, + navigator = LocalNavigator.currentOrThrow, + ), crossinline onLikeClicked: (StrongRef) -> Unit = { }, crossinline onUnClicked: (type: RecordType, uri: AtUri) -> Unit = { _, _ -> }, contentHandling: List = listOf() @@ -411,7 +445,10 @@ inline fun ColumnScope.PostFeatureElement( inline fun ColumnScope.RecordFeature( record: EmbedRecord? = null, media: TimelinePostMedia? = null, - crossinline onItemClicked: OnPostClicked = {}, + onItemClicked: OnItemClicked = ItemClicked( + uriHandler = LocalUriHandler.current, + navigator = LocalNavigator.currentOrThrow, + ), crossinline onLikeClicked: (StrongRef) -> Unit = { }, crossinline onUnClicked: (type: RecordType, uri: AtUri) -> Unit = { _, _ -> }, contentHandling: List = listOf(), @@ -468,7 +505,7 @@ inline fun ColumnScope.RecordFeature( is EmbedRecord.InvisibleEmbedPost -> EmbedNotFoundPostFragment(uri = record.uri) is EmbedRecord.VisibleEmbedPost -> EmbedPostFragment( post = record, - onItemClicked = { onItemClicked(record.uri) }, + onItemClicked = { onItemClicked.onRichTextFacetClicked(uri = record.uri) }, modifier = Modifier.align(Alignment.CenterHorizontally) ) diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/post/PostLinkEmbed.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/post/PostLinkEmbed.kt index df1a174..8b2b41a 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/post/PostLinkEmbed.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/post/PostLinkEmbed.kt @@ -9,7 +9,6 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip @@ -23,7 +22,6 @@ import coil3.size.Size import com.morpho.app.model.bluesky.BskyPostFeature import com.morpho.app.ui.elements.RichTextElement import com.morpho.app.ui.elements.WrappedColumn -import com.morpho.app.util.makeBlueskyText import org.jetbrains.compose.resources.ExperimentalResourceApi @OptIn(ExperimentalLayoutApi::class, ExperimentalResourceApi::class) @@ -69,10 +67,8 @@ fun PostLinkEmbed( modifier = Modifier.padding(8.dp) ) if(linkData.description.isNotEmpty()) { - val bskyTxt = remember { makeBlueskyText(linkData.description) } RichTextElement( - text = bskyTxt.text, - facets = bskyTxt.facets, + text = linkData.description, modifier = Modifier.padding(start = 8.dp, end = 8.dp, top = 4.dp, bottom = 8.dp) ) } diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/profile/CompactProfileFragment.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/profile/CompactProfileFragment.kt new file mode 100644 index 0000000..29cd6ee --- /dev/null +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/profile/CompactProfileFragment.kt @@ -0,0 +1,142 @@ +package com.morpho.app.ui.profile + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.wrapContentWidth +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.input.pointer.PointerIcon +import androidx.compose.ui.input.pointer.pointerHoverIcon +import androidx.compose.ui.platform.LocalUriHandler +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.text.withStyle +import androidx.compose.ui.unit.dp +import androidx.compose.ui.util.fastForEach +import cafe.adriel.voyager.navigator.LocalNavigator +import cafe.adriel.voyager.navigator.currentOrThrow +import com.morpho.app.model.bluesky.DetailedProfile +import com.morpho.app.ui.elements.AvatarShape +import com.morpho.app.ui.elements.MorphoHighlightIndication +import com.morpho.app.ui.elements.OutlinedAvatar +import com.morpho.app.ui.elements.RichTextElement +import com.morpho.app.ui.elements.WrappedColumn +import com.morpho.app.ui.utils.ItemClicked +import com.morpho.app.ui.utils.OnItemClicked +import com.morpho.butterfly.Did + +@Composable +fun CompactProfileFragment( + profile: DetailedProfile, + elevate: Boolean = false, + modifier: Modifier = Modifier, + onProfileClicked: (Did) -> Unit = { }, + onItemClicked: OnItemClicked = ItemClicked( + uriHandler = LocalUriHandler.current, + navigator = LocalNavigator.currentOrThrow, + ), +) { + val interactionSource = remember { MutableInteractionSource() } + val indication = remember { MorphoHighlightIndication() } + WrappedColumn(modifier = modifier.fillMaxWidth()) { + Surface ( + shadowElevation = if (elevate ) 2.dp else 0.dp, + tonalElevation = if (elevate) 2.dp else 0.dp, + shape = MaterialTheme.shapes.small, + //color = bgColor, + modifier = modifier + .fillMaxWidth() + .align(Alignment.End) + .clickable( + interactionSource = interactionSource, + indication = indication, + enabled = true, + onClick = { onProfileClicked(profile.did) } + ) + + ) { + Row( + modifier = Modifier.padding(end = 6.dp) + .fillMaxWidth()//.fillMaxWidth(indentLevel(indent)) + ) { + OutlinedAvatar( + url = profile.avatar.orEmpty(), + contentDescription = "Avatar for ${profile.displayName} ${profile.handle}", + size = 45.dp, + outlineColor = MaterialTheme.colorScheme.background, + modifier = Modifier.padding(end = 2.dp), + avatarShape = AvatarShape.Corner + ) + Column( + Modifier + .padding(top = 4.dp, start = 2.dp, end = 6.dp) + .fillMaxWidth()//.fillMaxWidth(indentLevel(indent)) + ) { + Text( + text = buildAnnotatedString { + withStyle( + style = SpanStyle( + color = MaterialTheme.colorScheme.onSurface, + fontSize = MaterialTheme.typography.labelLarge.fontSize, + fontWeight = FontWeight.Medium + ) + ) { + if (profile.displayName != null) append("${profile.displayName} ") + } + withStyle( + style = SpanStyle( + color = MaterialTheme.colorScheme.onSurfaceVariant, + fontSize = MaterialTheme.typography.labelLarge.fontSize.times( + 0.8f + ) + ) + ) { + append("@${profile.handle}") + } + + }, + maxLines = 1, + style = MaterialTheme.typography.labelLarge, + overflow = TextOverflow.Ellipsis, + modifier = Modifier + .wrapContentWidth(Alignment.Start) + .pointerHoverIcon(PointerIcon.Hand) + .clickable( + interactionSource = interactionSource, + indication = indication, + enabled = true, + onClick = { onProfileClicked(profile.did) } + ) + ) + ProfileLabels( + labels = profile.labels, + modifier = Modifier.padding(vertical = 4.dp) + ) { label -> + + } + RichTextElement( + profile.description.orEmpty(), + maxLines = 4, + modifier = Modifier.padding(vertical = 4.dp) + ) { facetTypes -> + facetTypes.fastForEach { + onItemClicked.onRichTextFacetClicked(facet = it) + } + } + + } + } + } + } +} \ No newline at end of file diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/profile/ProfileLabel.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/profile/ProfileLabel.kt index ba8a1af..0f93706 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/profile/ProfileLabel.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/profile/ProfileLabel.kt @@ -15,11 +15,12 @@ import com.morpho.app.model.bluesky.BskyLabel @Composable fun ProfileLabel( modifier: Modifier = Modifier, - label: BskyLabel + label: BskyLabel, + onClick: (BskyLabel) -> Unit = {}, ) { InputChip( selected = true, - onClick = { /*TODO*/ }, + onClick = { onClick(label) }, label = { Text( text = label.value, diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/profile/ProfileLabels.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/profile/ProfileLabels.kt index ac6e9f3..d3c8eb1 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/profile/ProfileLabels.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/profile/ProfileLabels.kt @@ -10,7 +10,8 @@ import com.morpho.app.model.bluesky.BskyLabel @Composable fun ProfileLabels( modifier: Modifier = Modifier, - labels: List + labels: List, + onLabelClicked: (BskyLabel) -> Unit = {}, ) { FlowRow( modifier = modifier @@ -19,8 +20,9 @@ fun ProfileLabels( ProfileLabel( label = it, modifier = modifier - - ) + ) { label -> + onLabelClicked(label) + } } } } \ No newline at end of file diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/settings/BuiltinContentFilters.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/settings/BuiltinContentFilters.kt index 5bfde3e..50383e9 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/settings/BuiltinContentFilters.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/settings/BuiltinContentFilters.kt @@ -3,6 +3,7 @@ package com.morpho.app.ui.settings import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.MaterialTheme import androidx.compose.material3.SegmentedButton @@ -19,7 +20,9 @@ import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.unit.dp import app.bsky.actor.Visibility +import com.atproto.label.Severity import com.morpho.app.data.MorphoAgent +import com.morpho.app.model.bluesky.MorphoDataItem import com.morpho.app.ui.elements.SettingsGroup import com.morpho.app.ui.elements.SettingsItem import com.morpho.app.ui.theme.segmentedButtonEnd @@ -183,4 +186,82 @@ fun ColumnScope.BuiltinContentFilterSelector( ) } } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun ContentLabelSelector( + labelItem: MorphoDataItem.ModLabel, + onSelected: (Visibility) -> Unit, + modifier: Modifier = Modifier, +) { + + var setting by remember { mutableStateOf(labelItem.setting) } + val label = labelItem.label + val onItemClicked: (Visibility) -> Unit = remember { onSelected } + val text = buildAnnotatedString { + pushStyle(MaterialTheme.typography.titleSmall.toSpanStyle().copy( + color = MaterialTheme.colorScheme.onSurface + )) + append("${label.localizedName}\n") + pop() + pushStyle(MaterialTheme.typography.bodyMedium.toSpanStyle().copy( + color = MaterialTheme.colorScheme.onSurfaceVariant + )) + append(label.localizedDescription) + pop() + + toAnnotatedString() + } + val middleButtonText = remember { + if(label.severity == Severity.INFORM) { + "Show badge" + } else { + "Warn" + } + } + + SettingsItem( + text = text, + modifier = modifier.padding(horizontal = 8.dp), + spacing = 8.dp, + ) { + SingleChoiceSegmentedButtonRow( + modifier = it + ) { + SegmentedButton( + selected = setting == Visibility.SHOW || setting == Visibility.IGNORE, + onClick = { + setting = Visibility.SHOW + onItemClicked(Visibility.SHOW) + }, + shape = segmentedButtonStart.small, + label = { Text(text = "Show") } + ) + SegmentedButton( + selected = setting == Visibility.WARN || setting == Visibility.INFORM, + onClick = { + if(label.severity == Severity.INFORM) { + setting = Visibility.INFORM + onItemClicked(Visibility.INFORM) + } else { + setting = Visibility.WARN + onItemClicked(Visibility.WARN) + } + }, + shape = segmentedButtonMiddle, + label = { Text(text = middleButtonText) } + ) + SegmentedButton( + selected = setting == Visibility.HIDE, + onClick = { + setting = Visibility.HIDE + onItemClicked(Visibility.HIDE) + }, + shape = segmentedButtonEnd.small, + label = { Text(text = "Hide") } + ) + } + } + } \ No newline at end of file diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/settings/SettingsFragment.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/settings/SettingsFragment.kt index 3a8cdfe..1be0bd4 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/settings/SettingsFragment.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/settings/SettingsFragment.kt @@ -26,10 +26,12 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.RectangleShape import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.unit.dp +import cafe.adriel.voyager.koin.koinNavigatorScreenModel import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.Navigator import cafe.adriel.voyager.navigator.currentOrThrow import com.morpho.app.data.MorphoAgent +import com.morpho.app.screens.main.tabbed.TabbedMainScreenModel import com.morpho.app.screens.settings.AccessibilitySettingsScreen import com.morpho.app.screens.settings.AppearanceSettingsScreen import com.morpho.app.screens.settings.FeedSettingsScreen @@ -42,15 +44,20 @@ import org.koin.compose.getKoin @Composable fun SettingsFragment( - agent: MorphoAgent = getKoin().get(), - modifier: Modifier = Modifier, navigator: Navigator = LocalNavigator.currentOrThrow, + sm: TabbedMainScreenModel = navigator.koinNavigatorScreenModel(), + modifier: Modifier = Modifier, + ) { Column( modifier = Modifier .fillMaxWidth().then(modifier) ) { + UserManagement(sm = sm, navigator = navigator, + profiles = sm.agent.getAccounts(), + myProfile = sm.userProfile, + modifier = Modifier.padding(12.dp)) Spacer(modifier = Modifier.height(8.dp)) Text( "Basics", diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/settings/UserManagement.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/settings/UserManagement.kt new file mode 100644 index 0000000..71ed023 --- /dev/null +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/settings/UserManagement.kt @@ -0,0 +1,209 @@ +package com.morpho.app.ui.settings + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.selection.DisableSelection +import androidx.compose.foundation.text.selection.SelectionContainer +import androidx.compose.material.DropdownMenu +import androidx.compose.material.Surface +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ExpandMore +import androidx.compose.material.icons.filled.MoreVert +import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.unit.dp +import com.morpho.app.data.MorphoAgent +import com.morpho.app.model.bluesky.DetailedProfile +import com.morpho.app.ui.elements.AvatarShape +import com.morpho.app.ui.elements.OutlinedAvatar +import com.morpho.app.ui.elements.SettingsGroup +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.MenuDefaults +import androidx.compose.material3.MenuItemColors +import androidx.compose.material3.Text +import androidx.compose.material3.surfaceColorAtElevation +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.RectangleShape +import cafe.adriel.voyager.koin.koinNavigatorScreenModel +import cafe.adriel.voyager.navigator.LocalNavigator +import cafe.adriel.voyager.navigator.Navigator +import cafe.adriel.voyager.navigator.currentOrThrow +import cafe.adriel.voyager.navigator.tab.LocalTabNavigator +import com.morpho.app.screens.base.tabbed.MyProfileTab +import com.morpho.app.screens.login.LoginScreen +import com.morpho.app.screens.main.tabbed.TabbedMainScreenModel +import com.morpho.butterfly.Did +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.launch +import org.koin.compose.getKoin + +@Composable +fun UserManagement( + navigator: Navigator = LocalNavigator.currentOrThrow, + sm: TabbedMainScreenModel = navigator.koinNavigatorScreenModel(), + profiles: Flow> = sm.agent.getAccounts(), + myProfile: DetailedProfile? = null, + modifier: Modifier = Modifier, + distinguish: Boolean = false, + topLevel: Boolean = false, +) { + val users = profiles.collectAsState(initial = if(myProfile != null) listOf(myProfile) else listOf()) + val loggedInUser = remember { users.value.firstOrNull { it.did == sm.agent.id } } + val otherUsers = remember { users.value.filter { it.did != sm.agent.id } } + val mainNav = remember { + when (navigator.level) { + 0 -> navigator + else -> navigator.parent!! + } + } + val rootNav = LocalTabNavigator.current + val menuOptionClicked: (AccountMenuOption, Did) -> Unit = remember { + { option, did -> + when(option) { + AccountMenuOption.RemoveAccount -> { + sm.agent.removeAccount(did) + if(sm.agent.id == did) { + sm.logout() + mainNav.popUntilRoot() + rootNav.current = LoginScreen + } + } + AccountMenuOption.LogOut -> { + sm.logout() + mainNav.popUntilRoot() + rootNav.current = LoginScreen + } + } + } + } + SettingsGroup( + title = if(!topLevel) "Account" else "", + modifier = modifier.fillMaxWidth(), + distinguish = distinguish, + ) { + if(loggedInUser != null) { + Text( + text = "Logged in as ${loggedInUser.displayName ?: loggedInUser.handle.handle}", + style = MaterialTheme.typography.titleMedium, + modifier = Modifier.padding(12.dp) + ) + AccountItem( + profile = loggedInUser, + onClick = { + mainNav.push(MyProfileTab) + }, + onMenuClicked = menuOptionClicked + ) + } + Text( + text = "Other accounts", + style = MaterialTheme.typography.titleMedium, + modifier = Modifier.padding(12.dp) + ) + otherUsers.forEach { profile -> + AccountItem( + profile = profile, + onClick = { sm.switchUser(profile.did) }, + onMenuClicked = menuOptionClicked + ) + } + } + +} + +@Composable +fun AccountItem( + profile: DetailedProfile, + modifier: Modifier = Modifier, + onClick: () -> Unit = {}, + onMenuClicked: (AccountMenuOption, Did) -> Unit = { _, _ -> }, +) { + Row( + modifier = modifier.padding(12.dp).fillMaxWidth().clickable { onClick() } + ) { + OutlinedAvatar( + url = profile.avatar.orEmpty(), + contentDescription = "Avatar for ${profile.displayName}", + size = 50.dp, + avatarShape = AvatarShape.Rounded, + outlineColor = MaterialTheme.colorScheme.background, + ) + Column( + Modifier.padding(horizontal = 12.dp).weight(1f) + ) { + val name = profile.displayName ?: profile.handle.handle + Text( + text = name, + style = MaterialTheme.typography.labelLarge, + color = MaterialTheme.colorScheme.onSurface + ) + Text( + text = "${profile.handle}", + color = MaterialTheme.colorScheme.onSurfaceVariant, + style = MaterialTheme.typography.labelMedium, + ) + } + var showMenu by remember { mutableStateOf(false) } + IconButton( + onClick = { + showMenu = !showMenu + } + ) { + Icon( + imageVector = Icons.Default.MoreVert, + contentDescription = "Menu", + ) + DisableSelection { + AccountMenu(expanded = showMenu, onItemClicked = { + showMenu = false + onMenuClicked(it, profile.did) + }, onDismissRequest = { + showMenu = false + }) + } + } + + + } +} + +enum class AccountMenuOption(val value: String) { + RemoveAccount("Remove Account"), + LogOut("Log Out"), +} + +@Composable +fun AccountMenu( + expanded : Boolean = false, + onItemClicked: (AccountMenuOption) -> Unit = {}, + onDismissRequest: () -> Unit = {}, +) { + DropdownMenu( + expanded = expanded, onDismissRequest = {onDismissRequest()}, + modifier = Modifier.background( + MaterialTheme.colorScheme.surfaceColorAtElevation(2.dp), + RoundedCornerShape(2.dp) + ) + ) { + AccountMenuOption.entries.forEach { + DropdownMenuItem(text = { Text(it.value) }, colors = MenuDefaults.itemColors().copy( + textColor = MaterialTheme.colorScheme.onSurface, + ), onClick = { onItemClicked(it) }) + } + } +} \ No newline at end of file diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/theme/Shape.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/theme/Shape.kt index 319a3d7..7e7e561 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/theme/Shape.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/theme/Shape.kt @@ -6,6 +6,8 @@ import androidx.compose.material3.Shapes import androidx.compose.ui.graphics.RectangleShape import androidx.compose.ui.unit.dp + + val roundedTopLBotR = Shapes( extraSmall = ShapeDefaults.ExtraSmall.copy(topEnd = CornerSize(0.dp), bottomStart = CornerSize(0.dp)), small = ShapeDefaults.Small.copy(topEnd = CornerSize(0.dp), bottomStart = CornerSize(0.dp)), diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/thread/ThreadFragment.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/thread/ThreadFragment.kt index f6c2c89..d0655f1 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/thread/ThreadFragment.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/thread/ThreadFragment.kt @@ -11,8 +11,11 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.unit.dp import androidx.compose.ui.util.fastForEach +import cafe.adriel.voyager.navigator.LocalNavigator +import cafe.adriel.voyager.navigator.currentOrThrow import com.atproto.repo.StrongRef import com.morpho.app.model.bluesky.BskyPost import com.morpho.app.model.bluesky.BskyPostThread @@ -23,6 +26,8 @@ import com.morpho.app.ui.post.BlockedPostFragment import com.morpho.app.ui.post.FullPostFragment import com.morpho.app.ui.post.NotFoundPostFragment import com.morpho.app.ui.post.PostFragmentRole +import com.morpho.app.ui.utils.ItemClicked +import com.morpho.app.ui.utils.OnItemClicked import com.morpho.butterfly.AtIdentifier import com.morpho.butterfly.AtUri import com.morpho.butterfly.ContentHandling @@ -40,7 +45,10 @@ fun ThreadFragment( it.hashCode().toLong() } }, - onItemClicked: OnPostClicked = {}, + onItemClicked: OnItemClicked = ItemClicked( + uriHandler = LocalUriHandler.current, + navigator = LocalNavigator.currentOrThrow, + ), onProfileClicked: (AtIdentifier) -> Unit = {}, onReplyClicked: (BskyPost) -> Unit = { }, onRepostClicked: (BskyPost) -> Unit = { }, @@ -74,7 +82,7 @@ fun ThreadFragment( item(key = threadPost.post.cid) { FullPostFragment( post = root.post, - onItemClicked = {onItemClicked(it) }, + onItemClicked = onItemClicked, onProfileClicked = { onProfileClicked(it) }, onUnClicked = { type,uri-> onUnClicked(type,uri) }, onRepostClicked = { onRepostClicked(it) }, @@ -182,7 +190,7 @@ fun ThreadFragment( modifier = Modifier.padding(vertical = 1.dp, horizontal = 3.dp), indentLevel = 1, comparator = comparator, - onItemClicked = {onItemClicked(it) }, + onItemClicked = onItemClicked, onProfileClicked = { onProfileClicked(it) }, onUnClicked = { type,uri-> onUnClicked(type,uri) }, onRepostClicked = { onRepostClicked(it) }, diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/thread/ThreadItem.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/thread/ThreadItem.kt index ed126c2..85912bd 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/thread/ThreadItem.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/thread/ThreadItem.kt @@ -2,6 +2,9 @@ package com.morpho.app.ui.thread import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalUriHandler +import cafe.adriel.voyager.navigator.LocalNavigator +import cafe.adriel.voyager.navigator.currentOrThrow import com.atproto.repo.StrongRef import com.morpho.app.model.bluesky.BskyPost import com.morpho.app.model.bluesky.BskyPostReason @@ -9,6 +12,8 @@ import com.morpho.app.model.bluesky.ThreadPost import com.morpho.app.ui.common.OnPostClicked import com.morpho.app.ui.elements.MenuOptions import com.morpho.app.ui.post.* +import com.morpho.app.ui.utils.ItemClicked +import com.morpho.app.ui.utils.OnItemClicked import com.morpho.butterfly.AtIdentifier import com.morpho.butterfly.AtUri import com.morpho.butterfly.ContentHandling @@ -22,7 +27,10 @@ inline fun ThreadItem( role: PostFragmentRole = PostFragmentRole.ThreadBranchStart, elevate: Boolean = false, reason: BskyPostReason? = null, - crossinline onItemClicked: OnPostClicked = {}, + onItemClicked: OnItemClicked = ItemClicked( + uriHandler = LocalUriHandler.current, + navigator = LocalNavigator.currentOrThrow, + ), crossinline onProfileClicked: (AtIdentifier) -> Unit = {}, crossinline onReplyClicked: (BskyPost) -> Unit = { }, crossinline onRepostClicked: (BskyPost) -> Unit = { }, @@ -37,7 +45,7 @@ inline fun ThreadItem( FullPostFragment( post = item.post.copy(reason = reason, reply = item.post.reply?.copy(parentPost = null)), modifier = modifier, - onItemClicked = {onItemClicked(it) }, + onItemClicked = onItemClicked, onProfileClicked = { onProfileClicked(it) }, onUnClicked = { type,uri-> onUnClicked(type,uri) }, onRepostClicked = { onRepostClicked(it) }, @@ -53,7 +61,7 @@ inline fun ThreadItem( modifier = modifier, indentLevel = indentLevel, elevate = elevate, - onItemClicked = {onItemClicked(it) }, + onItemClicked = onItemClicked, onProfileClicked = { onProfileClicked(it) }, onUnClicked = { type,uri-> onUnClicked(type,uri) }, onRepostClicked = { onRepostClicked(it) }, diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/thread/ThreadReply.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/thread/ThreadReply.kt index e7f9f1b..bce561f 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/thread/ThreadReply.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/thread/ThreadReply.kt @@ -2,6 +2,9 @@ package com.morpho.app.ui.thread import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalUriHandler +import cafe.adriel.voyager.navigator.LocalNavigator +import cafe.adriel.voyager.navigator.currentOrThrow import com.atproto.repo.StrongRef import com.morpho.app.model.bluesky.BskyPost import com.morpho.app.model.bluesky.ThreadPost @@ -11,6 +14,8 @@ import com.morpho.app.ui.post.BlockedPostFragment import com.morpho.app.ui.post.NotFoundPostFragment import com.morpho.app.ui.post.PostFragment import com.morpho.app.ui.post.PostFragmentRole +import com.morpho.app.ui.utils.ItemClicked +import com.morpho.app.ui.utils.OnItemClicked import com.morpho.butterfly.AtIdentifier import com.morpho.butterfly.AtUri import com.morpho.butterfly.ContentHandling @@ -22,7 +27,10 @@ inline fun ThreadReply( modifier: Modifier = Modifier, indentLevel: Int = 1, role: PostFragmentRole = PostFragmentRole.ThreadBranchMiddle, - crossinline onItemClicked: OnPostClicked = {}, + onItemClicked: OnItemClicked = ItemClicked( + uriHandler = LocalUriHandler.current, + navigator = LocalNavigator.currentOrThrow, + ), crossinline onProfileClicked: (AtIdentifier) -> Unit = {}, crossinline onReplyClicked: (BskyPost) -> Unit = { }, crossinline onRepostClicked: (BskyPost) -> Unit = { }, @@ -46,7 +54,7 @@ inline fun ThreadReply( indentLevel = indentLevel, modifier = modifier, elevate = r != PostFragmentRole.ThreadBranchStart, - onItemClicked = {onItemClicked(it) }, + onItemClicked = onItemClicked, onProfileClicked = { onProfileClicked(it) }, onUnClicked = { type,uri-> onUnClicked(type,uri) }, onRepostClicked = { onRepostClicked(it) }, diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/thread/ThreadTree.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/thread/ThreadTree.kt index ed6d3ec..1937019 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/thread/ThreadTree.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/thread/ThreadTree.kt @@ -15,8 +15,11 @@ import androidx.compose.ui.geometry.Size import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.StrokeCap import androidx.compose.ui.graphics.drawscope.Stroke +import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.unit.dp import androidx.compose.ui.util.fastForEachIndexed +import cafe.adriel.voyager.navigator.LocalNavigator +import cafe.adriel.voyager.navigator.currentOrThrow import com.atproto.repo.StrongRef import com.morpho.app.model.bluesky.BskyPost import com.morpho.app.model.bluesky.ThreadPost @@ -24,6 +27,8 @@ import com.morpho.app.ui.common.OnPostClicked import com.morpho.app.ui.elements.MenuOptions import com.morpho.app.ui.elements.WrappedColumn import com.morpho.app.ui.post.PostFragmentRole +import com.morpho.app.ui.utils.ItemClicked +import com.morpho.app.ui.utils.OnItemClicked import com.morpho.butterfly.AtIdentifier import com.morpho.butterfly.AtUri import com.morpho.butterfly.ContentHandling @@ -43,7 +48,10 @@ fun ThreadTree( it.hashCode().toLong() } }, - onItemClicked: OnPostClicked = {}, + onItemClicked: OnItemClicked = ItemClicked( + uriHandler = LocalUriHandler.current, + navigator = LocalNavigator.currentOrThrow, + ), onProfileClicked: (AtIdentifier) -> Unit = {}, onReplyClicked: (BskyPost) -> Unit = { }, onRepostClicked: (BskyPost) -> Unit = { }, @@ -141,7 +149,7 @@ fun ThreadTree( modifier = if(replies.size > 1) Modifier.padding(start = 2.dp, top = 2.dp) else if(replies.size == 1) Modifier.padding(start = 1.dp, top = 1.dp) else Modifier, - onItemClicked = {onItemClicked(it) }, + onItemClicked = onItemClicked, onProfileClicked = { onProfileClicked(it) }, onUnClicked = { type,uri-> onUnClicked(type,uri) }, onRepostClicked = { onRepostClicked(it) }, @@ -185,7 +193,7 @@ fun ThreadTree( } }.padding(start = 3.dp), indentLevel = indentLevel + 1, - onItemClicked = { onItemClicked(it) }, + onItemClicked = onItemClicked, onProfileClicked = { onProfileClicked(it) }, onUnClicked = { type, uri -> onUnClicked(type, uri) }, onRepostClicked = { onRepostClicked(it) }, @@ -204,7 +212,7 @@ fun ThreadTree( role = PostFragmentRole.ThreadBranchEnd, indentLevel = indentLevel, modifier = Modifier.padding(start = 4.dp, top = 2.dp), - onItemClicked = { onItemClicked(it) }, + onItemClicked = onItemClicked, onProfileClicked = { onProfileClicked(it) }, onUnClicked = { type, uri -> onUnClicked(type, uri) }, onRepostClicked = { onRepostClicked(it) }, diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/utils/Interaction.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/utils/Interaction.kt new file mode 100644 index 0000000..221f0db --- /dev/null +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/utils/Interaction.kt @@ -0,0 +1,117 @@ +package com.morpho.app.ui.utils + +import androidx.compose.ui.platform.UriHandler +import cafe.adriel.voyager.navigator.Navigator +import com.morpho.app.model.bluesky.FacetType +import com.morpho.app.screens.base.tabbed.ProfileTab +import com.morpho.app.screens.base.tabbed.ThreadTab +import com.morpho.app.util.openBrowser +import com.morpho.butterfly.AtIdentifier +import com.morpho.butterfly.AtUri +import com.morpho.butterfly.Did +import com.morpho.butterfly.Uri + +fun onProfileClickedImmediate( + actor: AtIdentifier, + navigator: Navigator, +) { + if(actor is Did) navigator.push(ProfileTab(actor)) +} + +fun onPostClickedImmediate( + uri: AtUri, + navigator: Navigator, +) { + navigator.push(ThreadTab(uri)) +} +typealias OnPostClicked = (AtUri) -> Unit +typealias OnFacetClicked = (FacetType) -> Unit +typealias OnLinkClicked = (Uri) -> Unit + +interface OnItemClicked { + val uriHandler: UriHandler + val navigator: Navigator + + fun onRichTextFacetClicked( + facet: FacetType? = null, + uri: AtUri? = null, + linkCallback: ((Uri) -> Unit)? = null, + profileCallback: ((AtIdentifier) -> Unit)? = null, + facetCallback: ((FacetType) -> Unit)? = null, + postCallback: ((AtUri?) -> Unit)? = null, + ) + + fun onPostClicked(uri: AtUri) + + fun onProfileClicked(id: AtIdentifier) +} + +data class ItemClicked( + override val uriHandler: UriHandler, + override val navigator: Navigator, + val linkCallback: (Uri) -> Unit = { link -> openBrowser(link.uri, uriHandler) }, + val profileCallback: (AtIdentifier) -> Unit = { onProfileClickedImmediate(it, navigator) }, + val facetCallback: (FacetType) -> Unit = { + when(it) { + is FacetType.UserDidMention -> { + profileCallback(it.did) + } + is FacetType.UserHandleMention -> { + profileCallback(it.handle) + } + is FacetType.ExternalLink -> { + linkCallback(it.uri) + } + else -> {} + } + }, + val postCallback: (AtUri?) -> Unit = { uri -> + if(uri != null) onPostClickedImmediate(uri, navigator) + }, + val callbackAlways: () -> Unit = { }, +): OnItemClicked { + + + override fun onRichTextFacetClicked( + facet: FacetType?, + uri: AtUri?, + linkCallback: ((Uri) -> Unit)?, + profileCallback: ((AtIdentifier) -> Unit)?, + facetCallback: ((FacetType) -> Unit)?, + postCallback: ((AtUri?) -> Unit)? + ) { + val facetFun = facetCallback ?: this.facetCallback + when(facet) { + is FacetType.UserDidMention -> { + if(profileCallback != null) profileCallback(facet.did) + else this.profileCallback(facet.did) + } + is FacetType.UserHandleMention -> { + if(profileCallback != null) profileCallback(facet.handle) + else this.profileCallback(facet.handle) + } + is FacetType.ExternalLink -> { + linkCallback(facet.uri) + } + is FacetType.Tag -> facetFun(facet) + is FacetType.PollBlueOption -> facetFun(facet) + is FacetType.BlueMoji -> facetFun(facet) + is FacetType.Format -> facetFun(facet) + FacetType.PollBlueQuestion -> facetFun(facet) + is FacetType.UnknownFacet -> facetFun(facet) + null -> if(postCallback != null) postCallback(uri) else this.postCallback(uri) + } + callbackAlways() + } + + override fun onPostClicked(uri: AtUri) { + postCallback(uri) + callbackAlways() + } + + override fun onProfileClicked(id: AtIdentifier) { + profileCallback(id) + callbackAlways() + } +} + diff --git a/Morpho/composeApp/src/desktopMain/kotlin/com/morpho/app/Platform.desktop.kt b/Morpho/composeApp/src/desktopMain/kotlin/com/morpho/app/Platform.desktop.kt index 77fb745..946a350 100644 --- a/Morpho/composeApp/src/desktopMain/kotlin/com/morpho/app/Platform.desktop.kt +++ b/Morpho/composeApp/src/desktopMain/kotlin/com/morpho/app/Platform.desktop.kt @@ -1,7 +1,10 @@ @file:Suppress("EXPECT_ACTUAL_CLASSIFIERS_ARE_IN_BETA_WARNING") package com.morpho.app import kotlinx.datetime.LocalDateTime +import net.harawata.appdirs.AppDirsFactory +import okio.Path.Companion.toPath import java.util.Locale +import kotlin.io.path.createDirectories // Note: no need to define CommonParcelize here (bc its @OptionalExpectation) actual interface CommonParcelable // not used on iOS @@ -25,4 +28,12 @@ actual fun getPlatform(): Platform = JVMPlatform() actual val myCountry: String? get() = Locale.getDefault().country actual val myLang: String? - get() = Locale.getDefault().language \ No newline at end of file + get() = Locale.getDefault().language + +actual fun getPlatformStorageDir(baseDir: String): String { + val storageDir = AppDirsFactory.getInstance() + .getUserDataDir(BuildKonfig.packageName, BuildKonfig.versionNumber, BuildKonfig.appName) + val path = storageDir.toPath() + path.toNioPath().createDirectories() + return storageDir.toString() +} \ No newline at end of file diff --git a/Morpho/composeApp/src/desktopMain/kotlin/main.kt b/Morpho/composeApp/src/desktopMain/kotlin/main.kt index cbbac92..3bc7f51 100644 --- a/Morpho/composeApp/src/desktopMain/kotlin/main.kt +++ b/Morpho/composeApp/src/desktopMain/kotlin/main.kt @@ -54,15 +54,12 @@ import com.morpho.app.data.DarkModeSetting import com.morpho.app.data.MorphoAgent import com.morpho.app.data.PreferencesRepository import com.morpho.app.di.appModule -import com.morpho.app.di.dataModule -import com.morpho.app.di.storageModule +import com.morpho.app.getPlatformStorageDir import com.morpho.app.ui.theme.MorphoTheme import com.morpho.butterfly.auth.SessionRepository import com.morpho.butterfly.auth.UserRepository import morpho.composeapp.generated.resources.Res import morpho.composeapp.generated.resources.morpho_icon_transparent -import net.harawata.appdirs.AppDirsFactory -import okio.Path.Companion.toPath import org.jetbrains.compose.resources.ExperimentalResourceApi import org.jetbrains.compose.resources.imageResource import org.jetbrains.compose.resources.painterResource @@ -77,7 +74,6 @@ import org.lighthousegames.logging.VariableLogLevel import org.lighthousegames.logging.logging import org.slf4j.Logger import org.slf4j.LoggerFactory -import kotlin.io.path.createDirectories val log = logging("main") @@ -91,20 +87,15 @@ fun main() = application { KmLogging.setLoggers(PlatformLogger(VariableLogLevel(LogLevel.Verbose))) val koin = startKoin { slf4jLogger() - modules(appModule, storageModule, dataModule) + modules(appModule)//, storageModule, dataModule) }.koin - val storageDir = AppDirsFactory.getInstance() - .getUserDataDir("com.morpho.app", "0.1.0", "Morpho") - val path = storageDir.toPath() - path.toNioPath().createDirectories() - val cacheDir = AppDirsFactory.getInstance() - .getUserCacheDir("com.morpho.app", "0.1.0", "Morpho") - val cachePath = cacheDir.toPath() - cachePath.toNioPath().createDirectories() - koin.get { parametersOf(storageDir) } - koin.get { parametersOf(storageDir) } - koin.get { parametersOf(storageDir) } - val agent = koin.get() + val storageDir = getPlatformStorageDir() + val sessionRepo = koin.get { parametersOf(storageDir) } + val userRepo = koin.get { parametersOf(storageDir) } + val prefsRepo = koin.get { parametersOf(storageDir) } + val agent = koin.get { + parametersOf(sessionRepo, userRepo, prefsRepo) + } val morphoPrefs by derivedStateOf { agent.morphoPrefs } val (undecorated, tabbed) = remember { log.d{ "Morpho Preferences: $morphoPrefs" } diff --git a/Morpho/gradle/libs.versions.toml b/Morpho/gradle/libs.versions.toml index 3ee3141..f59a621 100644 --- a/Morpho/gradle/libs.versions.toml +++ b/Morpho/gradle/libs.versions.toml @@ -34,7 +34,7 @@ kotlinx-serialization = "1.6.3" kotlinx-datetime = "0.6.0" kotlinx-coroutines = "1.8.1" kotlinx-immutable = "0.3.7" -kstore = "0.7.1" +kstore = "0.8.0" pagingCommon = "3.3.0-alpha02-0.5.1" pagingComposeCommon = "3.3.0-alpha02-0.5.1" pagingRuntime = "3.3.0-alpha02" @@ -152,6 +152,7 @@ kotlin-gradle-plugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", v kotlin-reflect = { module = "org.jetbrains.kotlin:kotlin-reflect", version.ref = "kotlin-reflect" } ktor-client-auth = { module = "io.ktor:ktor-client-auth", version.ref = "ktor" } ktor-client-okhttp = { module = "io.ktor:ktor-client-okhttp", version.ref = "ktor" } +ktor-client-encoding = { module = "io.ktor:ktor-client-encoding", version.ref = "ktor" } ktor-client-resources = { module = "io.ktor:ktor-client-resources", version.ref = "ktor" } ktor-cio = { module = "io.ktor:ktor-client-cio", version.ref = "ktor" } ktor-contentnegotiation = { module = "io.ktor:ktor-client-content-negotiation", version.ref = "ktor" } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 5362090..cef4ed9 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -34,8 +34,9 @@ kotlinx-serialization = "1.6.3" kotlinx-datetime = "0.6.0" kotlinx-coroutines = "1.8.1" kotlinx-immutable = "0.3.7" -kstore = "0.7.1" +kstore = "0.8.0" ktor = "2.3.9" +ktorClientEncoding = "2.3.9" logbackClassic = "1.5.7" logbackCore = "1.5.7" logging = "1.4.2" @@ -131,6 +132,7 @@ koin-androidx-compose = { module = "io.insert-koin:koin-androidx-compose", versi koin-compose = { module = "io.insert-koin:koin-compose", version.ref = "koin-compose" } +ktor-client-encoding = { module = "io.ktor:ktor-client-encoding", version.ref = "ktor" } logback-classic = { module = "ch.qos.logback:logback-classic", version.ref = "logbackClassic" } logback-core = { module = "ch.qos.logback:logback-core", version.ref = "logbackCore" } logging = { module = "org.lighthousegames:logging", version.ref = "logging" } From 20fe819ec1f389583265801a31bfff5342a102ee Mon Sep 17 00:00:00 2001 From: Orual Date: Sun, 20 Oct 2024 16:18:08 -0400 Subject: [PATCH 38/42] Fixed a bunch of the issues with the thread collection and timeline deduplication. --- Butterfly | 2 +- .../morpho/app/data/ContentLabelService.kt | 6 +- .../kotlin/com/morpho/app/data/FeedTuner.kt | 53 +--- .../com/morpho/app/data/MorphoDataSource.kt | 112 +++++--- .../morpho/app/model/bluesky/BskyPostReply.kt | 5 +- .../app/model/bluesky/BskyPostThread.kt | 184 +++++------- .../app/model/bluesky/MorphoDataItem.kt | 270 ++++++++++-------- .../app/model/bluesky/NotificationsSource.kt | 10 +- .../com/morpho/app/model/uidata/MorphoData.kt | 4 +- .../app/screens/base/BaseScreenModel.kt | 16 +- .../screens/base/tabbed/TabbedBaseScreen.kt | 3 +- .../app/screens/main/tabbed/TabbedHomeView.kt | 2 +- .../notifications/NotificationsView.kt | 4 +- .../morpho/app/ui/common/SkylineFragment.kt | 11 +- .../ui/notifications/NotificationsElement.kt | 36 ++- 15 files changed, 351 insertions(+), 367 deletions(-) diff --git a/Butterfly b/Butterfly index eba8fb0..c4f9d8c 160000 --- a/Butterfly +++ b/Butterfly @@ -1 +1 @@ -Subproject commit eba8fb0c68e2ddd515773ab283db8e77dee3ffa6 +Subproject commit c4f9d8c7a938b30fceb032353e8d8b3d19394f9c diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/data/ContentLabelService.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/data/ContentLabelService.kt index 4f2dba8..e6bb36d 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/data/ContentLabelService.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/data/ContentLabelService.kt @@ -30,11 +30,11 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.runBlocking import org.koin.core.component.KoinComponent -import org.koin.core.component.inject +import org.koin.core.component.get import org.lighthousegames.logging.logging class ContentLabelService: KoinComponent { - val agent: MorphoAgent by inject() + val agent: MorphoAgent = get() val serviceScope = CoroutineScope(SupervisorJob() + Dispatchers.Default) companion object { val log = logging("ContentLabelService") @@ -106,7 +106,7 @@ class ContentLabelService: KoinComponent { } adultLabels.isNotEmpty() } else { - item.thread.getLabels().any { label -> + item.thread.getLabels().all { label -> labels[label.value] == Visibility.HIDE } } diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/data/FeedTuner.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/data/FeedTuner.kt index 1968955..e07fc3b 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/data/FeedTuner.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/data/FeedTuner.kt @@ -221,55 +221,6 @@ data class FeedTuner(val tuners: List } } - fun tune( - feed: MorphoData - ): MorphoData { - var workingFeed = feed.items - tuners.forEach { tuner -> - workingFeed = tuner(workingFeed, this) - } - workingFeed = workingFeed.map { item -> - if(seenKeys.contains(item.key)) return@map null - else if(item is MorphoDataItem.Thread) { - val itemUris = item.getUris() - val seenInThisThread = itemUris.filter { seenUris.contains(it) } - if(seenInThisThread.isNotEmpty()) { - if(seenInThisThread.size == itemUris.size) { - return@map null - } else { - val newParents = item.thread.parents.filter { parent -> - when(parent) { - is ThreadPost.ViewablePost -> parent.post.uri in seenInThisThread - is ThreadPost.BlockedPost -> false - is ThreadPost.NotFoundPost -> false - } - } - val newThread = item.copy(thread = item.thread.filterReplies { reply -> - when(reply) { - is ThreadPost.ViewablePost -> reply.post.uri in seenInThisThread - is ThreadPost.BlockedPost -> false - is ThreadPost.NotFoundPost -> false - } - }.copy(parents = newParents)) - seenUris.addAll(itemUris) - if(newThread.thread.replies.isEmpty() && newThread.thread.parents.isEmpty()) { - return@map null - } else { - return@map newThread - } - } - } else { - seenUris.addAll(itemUris) - item - } - } else { - val disableDedub = item.isReply && item.isRepost - if(!disableDedub) seenKeys.add(item.key) - item - } - }.filterNotNull() as List - return feed.copy(items = workingFeed) - } fun tune( feed: PagedResponse.Feed ): PagedResponse.Feed { @@ -286,7 +237,7 @@ data class FeedTuner(val tuners: List + item.thread.parents.filter { parent -> when(parent) { is ThreadPost.ViewablePost -> parent.post.uri in seenInThisThread is ThreadPost.BlockedPost -> false @@ -299,7 +250,7 @@ data class FeedTuner(val tuners: List false is ThreadPost.NotFoundPost -> false } - }.copy(parents = newParents)) + }.copy(parent = item.thread.parent)) seenUris.addAll(itemUris) if(newThread.thread.replies.isEmpty() && newThread.thread.parents.isEmpty()) { return@map null diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/data/MorphoDataSource.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/data/MorphoDataSource.kt index 0d12141..189e88f 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/data/MorphoDataSource.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/data/MorphoDataSource.kt @@ -1,7 +1,5 @@ package com.morpho.app.data -import androidx.compose.ui.util.fastAny -import app.cash.paging.PagingConfig import app.cash.paging.PagingSource import app.cash.paging.PagingState import com.morpho.app.model.bluesky.BskyPostReason @@ -9,6 +7,7 @@ import com.morpho.app.model.bluesky.BskyPostThread import com.morpho.app.model.bluesky.MorphoDataItem import com.morpho.app.model.bluesky.ThreadPost import com.morpho.app.model.bluesky.toPost +import com.morpho.app.model.bluesky.toThreadPost import com.morpho.app.model.uidata.Delta import com.morpho.app.model.uidata.Moment import com.morpho.butterfly.ButterflyAgent @@ -22,13 +21,14 @@ import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flowOn import kotlinx.datetime.Instant import org.koin.core.component.KoinComponent -import org.koin.core.component.inject +import org.koin.core.component.get +import org.lighthousegames.logging.logging import kotlin.time.Duration abstract class MorphoDataSource: PagingSource(), KoinComponent { - val agent: MorphoAgent by inject() - val moderator: ContentLabelService by inject() + val agent: MorphoAgent = get() + val moderator: ContentLabelService = get() //override val keyReuseSupported: Boolean = true override fun getRefreshKey(state: PagingState): Cursor? { @@ -58,6 +58,11 @@ data class MorphoFeedSource( val repliesBumpThreads: Boolean = false, val collectThreads: Boolean = true, ): MorphoDataSource() { + + companion object { + val log = logging("MorphoFeedSource") + } + override suspend fun load( params: LoadParams ): LoadResult { @@ -71,20 +76,29 @@ data class MorphoFeedSource( return request(loadCursor, limit.toLong()).map { pagedList -> + val tunedList = when(pagedList) { is PagedResponse.Feed -> { +// val jsonToLog = json.encodeToString(pagedList.copy() as PagedResponse.Feed) +// log.d { +// "Feed reponse:\n$jsonToLog" +// } var tunedFeed = pagedList.copy( items = if(collectThreads) { - pagedList.items.filter { !moderator.shouldHideItem(it) } + pagedList.items.filterNot { moderator.shouldHideItem(it) } .collectThreads( repliesBumpThreads = repliesBumpThreads, agent = agent ).getOrNull() ?: pagedList.items - } else pagedList.items + } else pagedList.items.filterNot { moderator.shouldHideItem(it) } ) tuners.forEach { tuner -> tunedFeed = tuner.tune(tunedFeed) } +// val tunedJson = json.encodeToString(tunedFeed.copy() as PagedResponse.Feed) +// log.d { +// "Feed reponse after tuning:\n$tunedJson" +// } tunedFeed } is PagedResponse.FromRecord -> pagedList.items @@ -175,29 +189,41 @@ suspend fun List.collectThreads( isOrphan = root != null && parent != null, ) } - val newReply = replies[index] ?: return@forEachIndexed // Update in case we changed it above - val replyRef = newReply.post.reply?.replyRef ?: return@forEachIndexed + val newReply = replies[index] // Update in case we changed it above + val replyRef = newReply?.post?.reply?.replyRef ?: return@forEachIndexed val parent = replyRef.parent.uri val root = replyRef.root.uri - val inThread = threads.indexOfFirst { it?.containsUri(parent) ?: false || it?.containsUri(root) ?: false } + val inThread = threads.indexOfFirst { it?.containsUri(parent) == true || it?.containsUri(root) == true } if (inThread != -1) { val thread = threads.getOrNull(inThread) ?: return@forEachIndexed - threads[inThread] = thread.addReply(newReply.post) + threads[inThread] = thread.addReply(newReply.post).copy(isIncompleteThread = false) replies[index] = null + return@forEachIndexed } - val inCandidates = threadCandidates.indexOfFirst { it?.containsUri(parent) ?: false || it?.containsUri(root) ?: false } + val inCandidates = threadCandidates.indexOfFirst { it?.containsUri(parent) == false || it?.containsUri(root) == false } if (inCandidates != -1) { val thread = threadCandidates.getOrNull(inCandidates) ?: return@forEachIndexed - threadCandidates[inCandidates] = thread.addReply(newReply.post) + threadCandidates[inCandidates] = thread.addReply(newReply.post).copy(isIncompleteThread = false) replies[index] = null + return@forEachIndexed } + threadCandidates.add(MorphoDataItem.Thread(BskyPostThread( + post = newReply.post, + parent = newReply.post.reply.parentPost?.toThreadPost(), + replies = listOf(), + ), isIncompleteThread = true)) } threadCandidates.forEachIndexed { index, thread -> if (thread == null) return@forEachIndexed - val rootInThreads = threads.indexOfFirst { t -> t?.containsUri(thread.rootUri) ?: false } + val rootInThreads = threads.indexOfFirst { t -> t?.containsUri(thread.rootUri) == true } if (rootInThreads == - 1) { - val threadToSplice = threads.getOrNull(rootInThreads) ?: return@forEachIndexed + val threadToSplice = threads.getOrNull(rootInThreads) + if(threadToSplice == null) { + threads.add(thread.copy(isIncompleteThread = false)) + threadCandidates[index] = null + return@forEachIndexed + } if( thread.thread.parents.firstOrNull() is ThreadPost.ViewablePost && threadToSplice.thread.parents.firstOrNull() is ThreadPost.ViewablePost @@ -214,7 +240,7 @@ suspend fun List.collectThreads( newReplies.add(ThreadPost.ViewablePost(threadToSplice.thread.post, threadToSplice.thread.parents.last(),threadToSplice.thread.replies)) val newThread = BskyPostThread( post = newEntry.post, - parents = listOf(), + parent = newEntry.parent, replies = newReplies.distinctBy { it.uri }, ) threads[rootInThreads] = threadToSplice.copy(thread = newThread, isIncompleteThread = false) @@ -236,7 +262,7 @@ suspend fun List.collectThreads( newReplies.add(oldReply) val newThread = BskyPostThread( post = newEntry.post, - parents = listOf(newParent), + parent = newParent, replies = newReplies.distinctBy { it.uri }, ) threads[rootInThreads] = threadToSplice.copy(thread = newThread, isIncompleteThread = false) @@ -245,7 +271,7 @@ suspend fun List.collectThreads( } } else { - val inThreads = threads.indexOfFirst { t -> t?.containsUri(thread.thread.post.uri) ?: false } + val inThreads = threads.indexOfFirst { t -> t?.containsUri(thread.thread.post.uri) == true } if (inThreads == - 1) { val threadToSplice = threads.getOrNull(index) ?: return@forEachIndexed threads[index] = threadToSplice.addReply(ThreadPost.ViewablePost(thread.thread.post, thread.thread.parents.last(), thread.thread.replies)) @@ -253,29 +279,30 @@ suspend fun List.collectThreads( } } } - threadCandidates.filterNotNull() + threadCandidates.filterNotNull().filterNot { it.isIncompleteThread } if (threadCandidates.isNotEmpty()) threads.addAll(threadCandidates) val newReplies = replies.filterNotNull() .distinctBy { it.getUri() } .filterNot { reply -> - if(reply.isRepost) return@filterNot false - if(reply.isQuotePost) return@filterNot false - reply.getUris().any { uri -> threads.any { it?.containsUri(uri) ?: false } } - }.sortedByDescending { when(it.reason) { - is BskyPostReason.BskyPostRepost -> it.reason.indexedAt - else -> it.post.createdAt - } }.iterator() + if(reply.isRepost) false + else if(reply.isQuotePost) false + else reply.getUris().any { uri -> threads.any { it?.containsUri(uri) == true } } + } +// .sortedByDescending { when(it.reason) { +// is BskyPostReason.BskyPostRepost -> it.reason.indexedAt +// else -> it.post.createdAt +// } } var newPosts = posts.toList().filterNotNull() newPosts = newPosts.distinctBy { it.getUri() } newPosts = newPosts.filterNot { post -> - if(post.isRepost) return@filterNot false - if(post.isQuotePost) return@filterNot false - post.getUris().any { uri -> threads.any { it?.containsUri(uri) ?: false } } - }.sortedByDescending { when(it.reason) { - is BskyPostReason.BskyPostRepost -> it.reason.indexedAt - else -> it.post.createdAt - } } - val newPostsIter = newPosts.iterator() + if(post.isRepost) false + else if(post.isQuotePost) false + else post.getUris().any { uri -> threads.any { it?.containsUri(uri) == true } } + } +// .sortedByDescending { when(it.reason) { +// is BskyPostReason.BskyPostRepost -> it.reason.indexedAt +// else -> it.post.createdAt +// } } var newThreads = threads.toList().filterNotNull() newThreads = newThreads.sortedByDescending { if(!repliesBumpThreads) { it.rootAccessiblePost.createdAt @@ -291,17 +318,14 @@ suspend fun List.collectThreads( }) } } newThreads = newThreads.distinctBy { it.getUri() } - .filterNot { thread -> - thread.getUris().filterNot { uri -> - newThreads.fastAny { it.getUri() == uri } }.size > 1 - } - val newThreadsIter = newThreads.iterator() +// .filterNot { thread -> +// thread.getUris().filterNot { uri -> +// newThreads.fastAny { it.getUri() == uri } }.size > 1 +// } val newFeed = mutableListOf() - while(newPostsIter.hasNext() || newThreadsIter.hasNext() || newReplies.hasNext() ) { - if(newPostsIter.hasNext()) newFeed.add(newPostsIter.next()) - if(newThreadsIter.hasNext()) newFeed.add(newThreadsIter.next()) - if(newReplies.hasNext()) newFeed.add(newReplies.next()) - } + newFeed.addAll(newPosts) + newFeed.addAll(newThreads) + newFeed.addAll(newReplies) val dedupedFeed = newFeed.distinctBy { it.getUri() } val sortedFeed = dedupedFeed.sortedByDescending { when(it) { diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/BskyPostReply.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/BskyPostReply.kt index 91aa957..77b0705 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/BskyPostReply.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/BskyPostReply.kt @@ -61,9 +61,10 @@ fun PostReplyRef.toReplyRef(): BskyPostReplyRef { } fun PostReplyRef.toReply(): BskyPostReply { + val replyRef = this.toReplyRef() return BskyPostReply( - replyRef = this.toReplyRef(), - grandParentAuthor = this.grandParentAuthor?.toProfile() + replyRef = replyRef, + grandParentAuthor = replyRef.grandParentAuthor ) } diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/BskyPostThread.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/BskyPostThread.kt index 6e5a9e4..8e7ead9 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/BskyPostThread.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/BskyPostThread.kt @@ -13,7 +13,6 @@ import com.morpho.butterfly.Cid import dev.icerock.moko.parcelize.Parcelable import dev.icerock.moko.parcelize.Parcelize import kotlinx.collections.immutable.persistentListOf -import kotlinx.collections.immutable.toImmutableList import kotlinx.serialization.Serializable @@ -22,9 +21,11 @@ import kotlinx.serialization.Serializable @Serializable data class BskyPostThread( val post: BskyPost, - val parents: List, + val parent: ThreadPost? = null, val replies: List, ): Parcelable { + val parents: List = if(parent != null) listOf(parent) + parent.parents() else listOf() + operator fun contains(other: Any?) : Boolean { when(other) { null -> return false @@ -32,13 +33,7 @@ data class BskyPostThread( is AtUri -> return other == post.uri is BskyPost -> return other.cid == post.cid else -> { - parents.map { - return when(it) { - is BlockedPost -> false - is NotFoundPost -> false - is ViewablePost -> it.contains(other) - } - } + replies.map { return when(it) { is BlockedPost -> false @@ -74,10 +69,10 @@ data class BskyPostThread( } fun filterReplies(filter: (ThreadPost) -> Boolean): BskyPostThread { - val threadReplies = this.replies.toMutableList() + val threadReplies: MutableList = this.replies.toMutableList() threadReplies.fastForEachIndexed { index, reply -> - if (filter(reply)) { - threadReplies.removeAt(index) + if (reply != null && filter(reply)) { + threadReplies[index] = null } else { if (reply is ViewablePost) { threadReplies[index] = reply.copy( @@ -88,39 +83,28 @@ data class BskyPostThread( } return BskyPostThread( post = post, - parents = parents, - replies = threadReplies + parent = parent, + replies = threadReplies.filterNotNull() ) } - fun addReply(reply: ThreadPost.ViewablePost): BskyPostThread { + fun addReply(reply: ViewablePost): BskyPostThread { if(reply.uri == post.uri) return BskyPostThread( post = post, - parents = parents.distinctBy { it.uri }.filterNot { it.uri == reply.uri || it.uri == post.uri }, + parent = if(reply.parent is ViewablePost && parent !is ViewablePost) + reply.parent else parent, replies = replies.distinctBy { it.uri }.filterNot { it.uri == reply.uri || it.uri == post.uri }, ) - val parent = reply.post.reply?.parentPost?.uri ?: return this - val root = reply.post.reply.rootPost?.uri ?: return this - val newParents = this.parents.toMutableList() + reply.post.reply?.parentPost?.uri ?: return this + reply.post.reply.rootPost?.uri ?: return this val threadReplies = this.replies.toMutableList() - val inParents = this.parents.indexOfFirst { - it.uri == parent || it.uri == root - } - val inReplies = this.replies.indexOfFirst { - it.uri == parent || it.uri == root - } - if (inParents != -1) { - val replyParent = parents[inParents] - replyParent.addReply(reply) - newParents[inParents] = replyParent - } else if (inReplies != -1) { - val replyParent = threadReplies[inReplies] - replyParent.addReply(reply) - threadReplies[inReplies] = replyParent - } + val inParents = this.parents.any { it.uri == reply.uri } + val inReplies = this.replies.firstOrNull { it.uri == reply.uri } return BskyPostThread( post = post, - parents = newParents.distinctBy { it.uri }.filterNot { it.uri == reply.uri || it.uri == post.uri }, + parent = if (!inParents) parent else if (inReplies != null) { + if (inReplies is ViewablePost) parent?.addReply(inReplies) else parent + } else parent, replies = threadReplies.distinctBy { it.uri }.filterNot { it.uri == reply.uri || it.uri == post.uri }, ) } @@ -128,58 +112,31 @@ data class BskyPostThread( fun addReply(reply: BskyPost): BskyPostThread { if(reply.uri == post.uri) return BskyPostThread( post = post, - parents = parents.distinctBy { it.uri }.filterNot { it.uri == reply.uri || it.uri == post.uri }, + parent = parent, replies = replies.distinctBy { it.uri }.filterNot { it.uri == reply.uri || it.uri == post.uri }, ) - val parent = reply.reply?.parentPost?.uri ?: return this - val root = reply.reply.rootPost?.uri ?: return this - val newParents = this.parents.toMutableList() + reply.reply?.parentPost?.uri ?: return this + reply.reply.rootPost?.uri ?: return this val threadReplies = this.replies.toMutableList() - val inParents = this.parents.indexOfFirst { - it.uri == parent || it.uri == root - } - val inReplies = this.replies.indexOfFirst { - it.uri == parent + val inParents = this.parents.any { it.uri == reply.uri } + val inReplies = this.replies.any { it.uri == reply.uri } + val inAnyParentReplies = this.parents.any { + if(it is ViewablePost) it.replies.any { it.uri == reply.uri } else false } - if (inParents != -1) { - val replyParent = parents[inParents] - replyParent.addReply(reply) - newParents[inParents] = replyParent - } else if (inReplies != -1) { - val replyParent = threadReplies[inReplies] - replyParent.addReply(reply) - threadReplies[inReplies] = replyParent + if(!inReplies && !inParents && !inAnyParentReplies) { + threadReplies.add(reply.toThreadPost()) + } else if(!inParents && inAnyParentReplies) { + parent?.addReply(reply) } - return BskyPostThread( + val newThread = BskyPostThread( post = post, - parents = newParents.distinctBy { it.uri }.filterNot { it.uri == reply.uri || it.uri == post.uri }, - replies = threadReplies.distinctBy { it.uri }.filterNot { it.uri == reply.uri || it.uri == post.uri }, + parent = parent, + replies = threadReplies.distinctBy { it.uri }, ) + return newThread } } -fun List.inParentOrder(): List { - val newList = this.toMutableList() - this.forEachIndexed { index, threadPost -> - when(threadPost) { - is ViewablePost -> { - val parentUri = threadPost.post.reply?.replyRef?.parent?.uri - if (threadPost.post.reply == null) { - newList.add(0, threadPost) - return@forEachIndexed - } - val parentIndex = newList.indexOfFirst { it.uri == parentUri } - if (parentIndex != -1 && parentIndex != index - 1) { - newList.add(parentIndex+1, threadPost) - return@forEachIndexed - } - } - else -> return@forEachIndexed - } - } - return newList.distinctBy { it.uri } -} - @Parcelize @Immutable @Serializable @@ -205,7 +162,6 @@ sealed interface ThreadPost:Parcelable { } - operator fun contains(other: Any?) : Boolean { when(other) { is Cid -> { @@ -267,6 +223,15 @@ sealed interface ThreadPost:Parcelable { } } + fun parents(): List { + val parent = when(this) { + is ViewablePost -> this.parent + is BlockedPost -> null + is NotFoundPost -> null + } + return if(parent != null) listOf(parent) + parent.parents() else listOf() + } + @Immutable @Serializable data class NotFoundPost( @@ -333,6 +298,14 @@ sealed interface ThreadPost:Parcelable { } } + fun addParentReply(reply: BskyPost): ThreadPost { + return if(this !is ViewablePost) this + else if(this.parent == null) this + else if(reply.reply?.parentPost?.uri == this.parent.uri) { + this.parent.addReply(reply) + } else this.parent.addParentReply(reply) + } + fun addReply(reply: BskyPost): ThreadPost { return addReply(ViewablePost(reply)) @@ -356,38 +329,25 @@ sealed interface ThreadPost:Parcelable { } fun ThreadViewPost.toThread(): BskyPostThread { - val parents = when(parent) { - is ThreadViewPostParentUnion.ThreadViewPost -> { - (parent as ThreadViewPostParentUnion.ThreadViewPost).value.findParentChain() - } - else -> persistentListOf() - } - if (parents.isEmpty()) { - return BskyPostThread( - post = post.toPost(), - parents = persistentListOf(), - replies = replies.mapImmutable { it.toThreadPost() } - ) - } else { - val rootPost = parents.last().toPost() - val entryPost = this.post.toPost(BskyPostReply(parents.first().toPost(), rootPost, parents.first().toPost().reply?.parentPost?.author), null) - return BskyPostThread( - post = entryPost, - parents = parents.mapIndexed { index, post -> - post.toThreadPost( - if(index == parents.lastIndex) { - post.toPost() - } else { - parents[index + 1].toPost() - }, - ) - }.reversed().toImmutableList(), - replies = replies.mapImmutable { reply -> reply.toThreadPost() }, - ) - } + val entryPost = this.post.toPost() + val newParent = parent?.toThreadPost() + val rootPost = parent?.getRoot() + val parentPost = if(newParent is ThreadPost.ViewablePost) newParent else null + val grandParent = if(parentPost?.parent is ThreadPost.ViewablePost) parentPost.parent else null + val postReply = BskyPostReply( + rootPost = rootPost?.toPost(), + parentPost = parentPost?.post, + grandParentAuthor = grandParent?.post?.author, + replyRef = entryPost.reply?.replyRef + ) + return BskyPostThread( + post = entryPost.copy(reply = postReply), + parent = newParent, + replies = replies.map { it.toThreadPost() } + ) } -fun ThreadViewPost.toThreadPost( root: BskyPost): ThreadPost { +fun ThreadViewPost.toThreadPost(): ThreadPost { val post = post.toPost(null, null) return ViewablePost( post = post, @@ -422,6 +382,12 @@ fun ThreadViewPostParentUnion.toThreadPost(): ThreadPost = when (this) { is ThreadViewPostParentUnion.BlockedPost -> BlockedPost(value.uri) } - +fun ThreadViewPostParentUnion.getRoot(): ThreadViewPost? { + return when(this) { + is ThreadViewPostParentUnion.ThreadViewPost -> this.value.parent?.getRoot() + is ThreadViewPostParentUnion.NotFoundPost -> null + is ThreadViewPostParentUnion.BlockedPost -> null + } +} diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/MorphoDataItem.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/MorphoDataItem.kt index 96f7de1..7942b2e 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/MorphoDataItem.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/MorphoDataItem.kt @@ -29,123 +29,7 @@ sealed interface MorphoDataItem: Parcelable { @Serializable @CommonParcelize sealed interface FeedItem: MorphoDataItem { - companion object { - fun fromFeedViewPost(feedPost: FeedViewPost): FeedItem { - val items = mutableListOf() - val reason = feedPost.reason - val post = feedPost.post - val reply = feedPost.reply - var isIncompleteThread = false - var isOrphan = false - if (reply == null) { - val newPost = post.toPost() - return Post(newPost, newPost.reason, isOrphan = true) - } - if (reason != null) { - return Post(post.toPost(reply.toReply(), reason.toReason()), reason.toReason()) - } - var isRootBlocked = false - var isRootNotFound = false - val root = reply.root - val rootUri = when(root) { - is ReplyRefRootUnion.BlockedPost -> { - isRootBlocked = true - (reply.root as ReplyRefRootUnion.BlockedPost).value.uri - } - is ReplyRefRootUnion.NotFoundPost -> { - isRootNotFound = true - (reply.root as ReplyRefRootUnion.NotFoundPost).value.uri - } - is ReplyRefRootUnion.PostView -> { - (reply.root as ReplyRefRootUnion.PostView).value.uri - } - } - val parent = when(val parent = reply.parent) { - is ReplyRefParentUnion.BlockedPost -> { - null - } - is ReplyRefParentUnion.NotFoundPost -> { - null - } - is ReplyRefParentUnion.PostView -> { - (parent as ReplyRefParentUnion.PostView).value - } - } - items.add(feedPost.post) - val grandparent = if (!isRootBlocked && !isRootNotFound - && when(reply.parent) { - is ReplyRefParentUnion.BlockedPost -> { - false - } - is ReplyRefParentUnion.NotFoundPost -> { - false - } - is ReplyRefParentUnion.PostView -> { - val parentRef = reply.parent as ReplyRefParentUnion.PostView - val parentPost = try { - app.bsky.feed.Post.serializer().deserialize(parentRef.value.record) - } catch (e: Exception) { - null - } - parentPost?.reply?.parent?.uri == rootUri - } - }) { - root - } else null - var isGrandParentBlocked = false - var isGrandParentNotFound = false - when(grandparent) { - is ReplyRefRootUnion.BlockedPost -> isGrandParentBlocked = true - is ReplyRefRootUnion.NotFoundPost -> isGrandParentNotFound = true - is ReplyRefRootUnion.PostView -> {} - null -> isGrandParentNotFound = true - } - if(parent != null) items.add(0, parent) - if (isGrandParentBlocked && isGrandParentNotFound) isOrphan = true - if (isRootBlocked || isRootNotFound) { - return Post(post.toPost(reply.toReply(),null), null, isOrphan = true) - } - if (rootUri == parent?.uri) { - return if (items.size == 1) { - Post(post.toPost(reply.toReply(), null), null, isOrphan = isOrphan) - } else { - val parents = items.map { - ThreadPost.ViewablePost(it.toPost(), null, listOf()) - } - Thread( - BskyPostThread( - post = post.toPost(reply.toReply(), null), - parents = parents, - replies = listOf() - ), - null, - isIncompleteThread = isIncompleteThread, - ) - } - } - if(root is ReplyRefRootUnion.PostView) items.add(0, root.value) - if (grandparent != null && grandparent is ReplyRefRootUnion.PostView) { - items.add(0, grandparent.value) - isIncompleteThread = true - } - return if (items.size == 1) { - Post(post.toPost(reply.toReply(), null), null, isOrphan = isOrphan) - } else { - val parents = items.map { - ThreadPost.ViewablePost(it.toPost(), null,listOf()) - } - Thread( - BskyPostThread( - post = post.toPost(reply.toReply(), null), - parents = parents, - replies = listOf() - ), - null, - isIncompleteThread = true, - ) - } - } - } + fun getAuthors(): AuthorContext? { return when(this) { is Post -> { @@ -250,6 +134,85 @@ sealed interface MorphoDataItem: Parcelable { is Post -> post.likeCount is Thread -> thread.post.likeCount } + + companion object { + fun fromFeedViewPost(feedPost: FeedViewPost): FeedItem { + val items = mutableListOf() + val reason = feedPost.reason + val post = feedPost.post + val reply = feedPost.reply + var isIncompleteThread = false + var isOrphan = false + if (reply == null) { + val newPost = post.toPost() + return Post(newPost, newPost.reason, isOrphan = isOrphan) + } + if (reason != null) { + return Post(post.toPost(reply.toReply(), reason.toReason()), isOrphan = isOrphan) + } + + val rootUri = reply.root.getRootStatus()?.second ?: post.uri + val rootStatus = reply.root.getRootStatus()?.first ?: PostStatus.NotFound + val root = reply.root.postView() + val parent = reply.parent.postView() + items.add(feedPost.post) + val grandparent = if(rootStatus == PostStatus.Viewable && when(reply.parent) { + is ReplyRefParentUnion.BlockedPost -> false + is ReplyRefParentUnion.NotFoundPost -> false + is ReplyRefParentUnion.PostView -> { + val parentRef = reply.parent as ReplyRefParentUnion.PostView + val parentPost = try { + app.bsky.feed.Post.serializer().deserialize(parentRef.value.record) + } catch (e: Exception) { + null + } + parentPost?.reply?.parent?.uri == rootUri + } + }) { root } else null + + if(parent != null) items.add(0, parent) + if (grandparent == null) isOrphan = true + if (rootStatus != PostStatus.Viewable) { + return Post(post.toPost(reply.toReply(), feedPost.reason?.toReason()), null, isOrphan = true) + } + if (rootUri == parent?.uri) { + return if (items.size == 1) { + Post(post.toPost(reply.toReply(), feedPost.reason?.toReason()), null, isOrphan = isOrphan) + } else { + Thread( + BskyPostThread( + post = post.toPost(reply.toReply(), null), + + replies = listOf() + ), + null, + isIncompleteThread = isIncompleteThread, + ) + } + } + if(root != null) items.add(0, root) + if (grandparent != null) { + items.add(0, grandparent) + isIncompleteThread = true + } + return if (items.size == 1) { + Post(post.toPost(reply.toReply(), feedPost.reason?.toReason()), feedPost.reason?.toReason(), isOrphan = isOrphan) + } else { + + val thread = BskyPostThread( + post = post.toPost(reply.toReply(), null), + parent = parent?.toThreadPost(items), + replies = listOf() + ) + + Thread( + thread, + null, + isIncompleteThread = isIncompleteThread, + ) + } + } + } } @Immutable @@ -381,4 +344,75 @@ data class AuthorContext( val parentAuthor: Profile? = null, val grandParentAuthor: Profile? = null, val rootAuthor: Profile? = null, -) \ No newline at end of file +) + +fun PostView.parentOrNull(posts: List): ThreadPost? { + val post = this.toPost() + val parentUri = post.reply?.replyRef?.parent?.uri + return if(parentUri != null) (posts.firstOrNull { it.uri == parentUri })?.toPost()?.let { bskyPost -> + val recParent = parentOrNull(posts.filterNot { it.uri == parentUri }) + ThreadPost.ViewablePost(bskyPost, recParent, listOf( + ThreadPost.ViewablePost(post, ThreadPost.ViewablePost(bskyPost, recParent, listOf()), listOf()) + )) + } else null +} + +fun PostView.toThreadPost(posts: List): ThreadPost { + val parent = this.parentOrNull(posts.filterNot { it.uri == this.uri }) + return ThreadPost.ViewablePost(this.toPost(), parent, listOf()) +} + +fun BskyPost.toThreadPost(reply: BskyPost? = null): ThreadPost { + val parent = this.reply?.parentPost + return ThreadPost.ViewablePost(this, parent?.toThreadPost(), + reply?.let { listOf(it.toThreadPost())} ?: listOf()) +} + +fun FeedViewPost.toThreadPost(reply: BskyPost? = null): ThreadPost { + val post = this.toPost() + val parent = post.reply?.parentPost + return ThreadPost.ViewablePost(post, parent?.toThreadPost(post), + reply?.let { listOf(it.toThreadPost())} ?: listOf()) +} + +enum class PostStatus { + Viewable, + NotFound, + Blocked, +} + +inline fun ReplyRefParentUnion?.getParentStatus(): Pair? { + return when(this) { + is ReplyRefParentUnion.BlockedPost -> PostStatus.Blocked to this.value.uri + is ReplyRefParentUnion.NotFoundPost -> PostStatus.NotFound to this.value.uri + is ReplyRefParentUnion.PostView -> PostStatus.Viewable to this.value.uri + null -> null + } +} + +inline fun ReplyRefParentUnion?.postView(): PostView? { + return when(this) { + is ReplyRefParentUnion.BlockedPost -> null + is ReplyRefParentUnion.NotFoundPost -> null + is ReplyRefParentUnion.PostView -> this.value + null -> null + } +} + +inline fun ReplyRefRootUnion?.getRootStatus(): Pair? { + return when(this) { + is ReplyRefRootUnion.BlockedPost -> PostStatus.Blocked to this.value.uri + is ReplyRefRootUnion.NotFoundPost -> PostStatus.NotFound to this.value.uri + is ReplyRefRootUnion.PostView -> PostStatus.Viewable to this.value.uri + null -> null + } +} + +inline fun ReplyRefRootUnion?.postView(): PostView? { + return when(this) { + is ReplyRefRootUnion.BlockedPost -> null + is ReplyRefRootUnion.NotFoundPost -> null + is ReplyRefRootUnion.PostView -> this.value + null -> null + } +} \ No newline at end of file diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/NotificationsSource.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/NotificationsSource.kt index 6693938..f4bbc28 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/NotificationsSource.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/NotificationsSource.kt @@ -1,7 +1,6 @@ package com.morpho.app.model.bluesky import app.bsky.notification.ListNotificationsReason -import app.cash.paging.PagingConfig import com.morpho.app.data.MorphoDataSource import com.morpho.app.model.uistate.NotificationsFilterState import com.morpho.butterfly.AtUri @@ -13,9 +12,9 @@ class NotificationsSource: MorphoDataSource() { companion object { val log = logging() val defaultConfig = PagingConfig( - pageSize = 20, + pageSize = 40, prefetchDistance = 20, - initialLoadSize = 50, + initialLoadSize = 80, enablePlaceholders = false, ) } @@ -29,16 +28,15 @@ class NotificationsSource: MorphoDataSource() { is LoadParams.Refresh -> Cursor.Empty } return agent.listNotifications(limit.toLong(), loadCursor.value).map { response -> - val newCursor = response.cursor val items = response.items.map { it.toBskyNotification()}.collectNotifications() LoadResult.Page( data = items, prevKey = when(params) { is LoadParams.Append -> loadCursor - is LoadParams.Prepend -> Cursor.Empty + is LoadParams.Prepend -> null is LoadParams.Refresh -> Cursor.Empty }, - nextKey = newCursor, + nextKey = response.cursor, ) }.onFailure { return LoadResult.Error(it) diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/MorphoData.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/MorphoData.kt index d1a060c..ca5894c 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/MorphoData.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/MorphoData.kt @@ -394,7 +394,7 @@ data class MorphoData( newReplies.add(ThreadPost.ViewablePost(threadToSplice.thread.post, null, threadToSplice.thread.replies)) val newThread = BskyPostThread( post = newEntry.post, - parents = listOf(), + parent = null, replies = newReplies.distinctBy { it.uri }, ) threads[rootInThreads] = threadToSplice.copy(thread = newThread, isIncompleteThread = false) @@ -416,7 +416,7 @@ data class MorphoData( newReplies.add(oldReply) val newThread = BskyPostThread( post = newEntry.post, - parents = listOf(newParent), + parent = newParent, replies = newReplies.distinctBy { it.uri }, ) threads[rootInThreads] = threadToSplice.copy(thread = newThread, isIncompleteThread = false) diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/base/BaseScreenModel.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/base/BaseScreenModel.kt index c3114f5..012a492 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/base/BaseScreenModel.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/base/BaseScreenModel.kt @@ -3,7 +3,6 @@ package com.morpho.app.screens.base import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue -import app.cash.paging.Pager import app.cash.paging.cachedIn import cafe.adriel.voyager.core.model.ScreenModel import cafe.adriel.voyager.core.model.screenModelScope @@ -27,12 +26,12 @@ import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.emitAll import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch import org.lighthousegames.logging.logging @@ -146,15 +145,20 @@ open class BaseScreenModel( } }.distinctUntilChanged() as Flow>> + private val _unreadNotificationsCount = MutableStateFlow(0L) + val unreadNotificationsCount: StateFlow = _unreadNotificationsCount.asStateFlow() + fun hasUnreadNotifications(): Flow = unreadNotificationsCount.map { it > 0 } fun unreadNotificationsCount() = notificationsTick.t.map { - agent.unreadNotificationsCount().getOrDefault(0) - }.distinctUntilChanged() - .stateIn(screenModelScope, SharingStarted.WhileSubscribed(), 0L) + val count = agent.unreadNotificationsCount().getOrDefault(0) + _unreadNotificationsCount.value = count + count + } fun updateSeenNotifications() = screenModelScope.launch { agent.updateSeenNotifications() + _unreadNotificationsCount.value = 0 globalEvents.emit(Event.UpdateSeenNotifications()) } diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/base/tabbed/TabbedBaseScreen.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/base/tabbed/TabbedBaseScreen.kt index b099ce8..a23582d 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/base/tabbed/TabbedBaseScreen.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/base/tabbed/TabbedBaseScreen.kt @@ -21,7 +21,6 @@ import androidx.compose.ui.semantics.semantics import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import cafe.adriel.voyager.core.annotation.ExperimentalVoyagerApi -import cafe.adriel.voyager.core.model.rememberNavigatorScreenModel import cafe.adriel.voyager.core.screen.ScreenKey import cafe.adriel.voyager.koin.koinNavigatorScreenModel import cafe.adriel.voyager.navigator.LocalNavigator @@ -95,7 +94,7 @@ fun TabNavigationItem( when (tab) { is NotificationsTab -> { val sm = navigator.koinNavigatorScreenModel() - val unread by sm.unreadNotificationsCount().collectAsState(0) + val unread by sm.unreadNotificationsCount.collectAsState(0) BadgedBox( badge = { if (unread > 0) { diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/main/tabbed/TabbedHomeView.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/main/tabbed/TabbedHomeView.kt index 76481a9..5f7a9ad 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/main/tabbed/TabbedHomeView.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/main/tabbed/TabbedHomeView.kt @@ -149,7 +149,7 @@ fun TabScreen.TabbedHomeView( ) } } - val tabsCreated by derivedStateOf { sm.loaded } + val tabsCreated by derivedStateOf { sm.loaded && sm.tabs.isNotEmpty() } if (tabsCreated) { Navigator( tabs.first(), diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/notifications/NotificationsView.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/notifications/NotificationsView.kt index 10a5f37..b20cad6 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/notifications/NotificationsView.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/notifications/NotificationsView.kt @@ -27,6 +27,7 @@ import androidx.compose.material3.TextButton import androidx.compose.material3.rememberDrawerState import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateOf @@ -81,9 +82,8 @@ fun TabScreen.NotificationViewContent( ) { val sm = navigator.koinNavigatorScreenModel() - val numberUnread = sm.unreadNotificationsCount().value var showSettings by remember { mutableStateOf(false) } - val hasUnread = remember(numberUnread) { numberUnread > 0 } + val hasUnread by sm.hasUnreadNotifications().collectAsState(initial = false) val listState = rememberLazyListState() val scope = rememberCoroutineScope() val uriHandler = LocalUriHandler.current diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/common/SkylineFragment.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/common/SkylineFragment.kt index 82ac16c..82ce2bc 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/common/SkylineFragment.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/common/SkylineFragment.kt @@ -41,7 +41,6 @@ import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.material3.surfaceColorAtElevation import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -56,17 +55,12 @@ import app.bsky.actor.Visibility import app.cash.paging.LoadStateError import app.cash.paging.LoadStateLoading import app.cash.paging.compose.LazyPagingItems -import app.cash.paging.compose.collectAsLazyPagingItems import app.cash.paging.compose.itemKey import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.currentOrThrow import com.atproto.repo.StrongRef import com.morpho.app.model.bluesky.BskyPost import com.morpho.app.model.bluesky.MorphoDataItem -import com.morpho.app.model.bluesky.NotificationsListItem -import com.morpho.app.model.uidata.AuthorFeedUpdate -import com.morpho.app.model.uidata.FeedUpdate -import com.morpho.app.model.uidata.UIUpdate import com.morpho.app.ui.elements.MenuOptions import com.morpho.app.ui.elements.WrappedLazyColumn import com.morpho.app.ui.lists.FeedListEntryFragment @@ -77,13 +71,10 @@ import com.morpho.app.ui.profile.CompactProfileFragment import com.morpho.app.ui.settings.ContentLabelSelector import com.morpho.app.ui.utils.ItemClicked import com.morpho.app.ui.utils.OnItemClicked -import com.morpho.butterfly.AtIdentifier import com.morpho.butterfly.AtUri import com.morpho.butterfly.ContentHandling import com.morpho.butterfly.model.RecordType import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.filterIsInstance import kotlinx.coroutines.launch typealias OnPostClicked = (AtUri) -> Unit @@ -335,7 +326,7 @@ fun SkylineFragment ( OutlinedIconButton( onClick = { scope.launch { - //refreshPull() + pager.refresh() if (scrolledDownLots) { listState.scrollToItem(0) } else { diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/notifications/NotificationsElement.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/notifications/NotificationsElement.kt index b0f70ef..d1beb20 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/notifications/NotificationsElement.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/notifications/NotificationsElement.kt @@ -1,5 +1,7 @@ package com.morpho.app.ui.notifications +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer @@ -13,6 +15,7 @@ import androidx.compose.material.icons.filled.ExpandMore import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.IconToggleButton +import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue @@ -88,6 +91,12 @@ fun NotificationsElement( else -> {} } } + var unread by remember { mutableStateOf(item.notifications.any { !it.isRead }) } + val markAsRead: (AtUri) -> Unit = remember { { uri -> + markRead(uri) + unread = false + } } + remember { if (!readOnLoad) return@remember // We just mark the first notification as read, @@ -102,7 +111,12 @@ fun NotificationsElement( } } val number = remember { item.notifications.size } - Column { + Column( + modifier = if(unread) Modifier + .background(MaterialTheme.colorScheme.primary.copy(alpha = 0.5f)) + .clickable { markAsRead(item.notifications.first().uri) } + else Modifier.clickable { markAsRead(item.notifications.first().uri) } + ) { Row( ) { Column( @@ -122,7 +136,7 @@ fun NotificationsElement( checked = expand, onCheckedChange = { expand = it - markRead(item.notifications.first().uri) + markAsRead(item.notifications.first().uri) }, ) { if (expand) { @@ -156,34 +170,36 @@ fun NotificationsElement( PostFragment( post = post!!, elevate = true, onItemClicked = onItemClicked.copy( - callbackAlways = { if(!readOnLoad) markRead(item.notifications.first().uri) } + callbackAlways = { + if(!readOnLoad) markAsRead(item.notifications.first().uri) + } ), onProfileClicked = { - if(!readOnLoad) markRead(item.notifications.first().uri) + if(!readOnLoad) markAsRead(item.notifications.first().uri) scope.launch { resolveHandle(it)?.let { did -> onAvatarClicked(did) } } }, onUnClicked = { type, uri -> - if(!readOnLoad) markRead(item.notifications.first().uri) + if(!readOnLoad) markAsRead(item.notifications.first().uri) onUnClicked(type, uri) }, onRepostClicked = { onRepostClicked(it) - if(!readOnLoad) markRead(item.notifications.first().uri) + if(!readOnLoad) markAsRead(item.notifications.first().uri) }, onReplyClicked = { onReplyClicked(it) - if(!readOnLoad) markRead(item.notifications.first().uri) + if(!readOnLoad) markAsRead(item.notifications.first().uri) }, onMenuClicked = { option, p -> onMenuClicked(option, p) - if(!readOnLoad) markRead(item.notifications.first().uri) + if(!readOnLoad) markAsRead(item.notifications.first().uri) }, onLikeClicked = { onLikeClicked(it) - if(!readOnLoad) markRead(item.notifications.first().uri) - }, + if(!readOnLoad) markAsRead(item.notifications.first().uri) + }, ) } } From e042fab8438d5b646cc451dcd0ed21aaf6ef00c1 Mon Sep 17 00:00:00 2001 From: Orual Date: Sun, 20 Oct 2024 16:20:08 -0400 Subject: [PATCH 39/42] Fixed a bunch of the issues with the thread collection and timeline deduplication. --- .../kotlin/com/morpho/app/model/bluesky/NotificationsSource.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/NotificationsSource.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/NotificationsSource.kt index f4bbc28..abfe48c 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/NotificationsSource.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/bluesky/NotificationsSource.kt @@ -11,7 +11,7 @@ import org.lighthousegames.logging.logging class NotificationsSource: MorphoDataSource() { companion object { val log = logging() - val defaultConfig = PagingConfig( + val defaultConfig = app.cash.paging.PagingConfig( pageSize = 40, prefetchDistance = 20, initialLoadSize = 80, From c2a8338b18024fd16c23791e5464a67bcc6d960a Mon Sep 17 00:00:00 2001 From: Orual Date: Mon, 21 Oct 2024 11:23:42 -0400 Subject: [PATCH 40/42] Further timeline filtering, presentation, etc. fixes. Much happier with where I am on this now, doesn't feel *buggy* like it did for a good while after the refactor. - Seem to have eliminated most or all of the duplicates without just filtering out almost everything randomly sometimes, especially duplicates across pages of data. - Still some ways to be more efficient, make fewer copies, go over the list fewer times (idk how well the Kotlin compiler optimizes this stuff) - Threads are fairly reliably collected together, but still some times where you get chunks of the same thread that aren't spliced correctly. - Got rid of an error on pager invalidation/refresh, so refresh seems to work better. --- .../kotlin/com/morpho/app/data/FeedTuner.kt | 19 +++++++-------- .../com/morpho/app/data/MorphoDataSource.kt | 11 ++++----- .../morpho/app/model/uidata/FeedPresenter.kt | 6 +++-- .../app/screens/base/BaseScreenModel.kt | 1 + .../app/ui/common/SkylineThreadFragment.kt | 23 +++++++++++-------- 5 files changed, 33 insertions(+), 27 deletions(-) diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/data/FeedTuner.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/data/FeedTuner.kt index e07fc3b..c3e26b1 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/data/FeedTuner.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/data/FeedTuner.kt @@ -228,14 +228,14 @@ data class FeedTuner(val tuners: List workingFeed = tuner(workingFeed, this) } - workingFeed = workingFeed.map { item -> - if(seenKeys.contains(item.key)) return@map null + workingFeed = workingFeed.mapNotNull { item -> + if(seenKeys.contains(item.key)) null else if(item is MorphoDataItem.Thread) { val itemUris = item.getUris() val seenInThisThread = itemUris.filter { seenUris.contains(it) } if(seenInThisThread.isNotEmpty()) { if(seenInThisThread.size == itemUris.size) { - return@map null + null } else { item.thread.parents.filter { parent -> when(parent) { @@ -253,9 +253,9 @@ data class FeedTuner(val tuners: List(val tuners: List - return feed.copy(items = workingFeed) + //item + } as List + return feed.copy(items = workingFeed.ifEmpty { feed.items.distinctBy { it.getUri() } }) } diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/data/MorphoDataSource.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/data/MorphoDataSource.kt index 189e88f..6f95ffc 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/data/MorphoDataSource.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/data/MorphoDataSource.kt @@ -42,7 +42,7 @@ abstract class MorphoDataSource: PagingSource(), KoinCom } companion object { - val defaultConfig = PagingConfig( + val defaultConfig = app.cash.paging.PagingConfig( pageSize = 50, prefetchDistance = 20, initialLoadSize = 100, @@ -282,7 +282,7 @@ suspend fun List.collectThreads( threadCandidates.filterNotNull().filterNot { it.isIncompleteThread } if (threadCandidates.isNotEmpty()) threads.addAll(threadCandidates) val newReplies = replies.filterNotNull() - .distinctBy { it.getUri() } + //.distinctBy { it.getUri() } .filterNot { reply -> if(reply.isRepost) false else if(reply.isQuotePost) false @@ -293,7 +293,7 @@ suspend fun List.collectThreads( // else -> it.post.createdAt // } } var newPosts = posts.toList().filterNotNull() - newPosts = newPosts.distinctBy { it.getUri() } + //newPosts = newPosts.distinctBy { it.getUri() } newPosts = newPosts.filterNot { post -> if(post.isRepost) false else if(post.isQuotePost) false @@ -317,7 +317,7 @@ suspend fun List.collectThreads( maxOf(acc, postTime) }) } } - newThreads = newThreads.distinctBy { it.getUri() } + // newThreads = newThreads.distinctBy { it.getUri() } // .filterNot { thread -> // thread.getUris().filterNot { uri -> // newThreads.fastAny { it.getUri() == uri } }.size > 1 @@ -326,8 +326,7 @@ suspend fun List.collectThreads( newFeed.addAll(newPosts) newFeed.addAll(newThreads) newFeed.addAll(newReplies) - val dedupedFeed = newFeed.distinctBy { it.getUri() } - val sortedFeed = dedupedFeed.sortedByDescending { + val sortedFeed = newFeed.sortedByDescending { when(it) { is MorphoDataItem.Post -> when(it.reason) { is BskyPostReason.BskyPostFeedPost -> it.post.createdAt diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/FeedPresenter.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/FeedPresenter.kt index 23015eb..4acb62c 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/FeedPresenter.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/model/uidata/FeedPresenter.kt @@ -3,6 +3,7 @@ package com.morpho.app.model.uidata import app.bsky.feed.GetAuthorFeedFilter import app.bsky.feed.GetFeedQuery import app.bsky.feed.GetListFeedQuery +import app.cash.paging.InvalidatingPagingSourceFactory import app.cash.paging.Pager import app.cash.paging.cachedIn import com.morpho.app.data.FeedTuner @@ -25,13 +26,14 @@ class FeedPresenter( var descriptor: FeedDescriptor? = null, ): PagedPresenter() { - private var dataSource: MorphoFeedSource = + private var pagerFactory = InvalidatingPagingSourceFactory { descriptor?.getDataSource(agent) ?: getTimelineDataSource(agent) + } override var pager: Pager = run { val pagingConfig = MorphoDataSource.defaultConfig Pager(pagingConfig) { - dataSource + pagerFactory.invoke() } } diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/base/BaseScreenModel.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/base/BaseScreenModel.kt index 012a492..27fb71b 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/base/BaseScreenModel.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/base/BaseScreenModel.kt @@ -3,6 +3,7 @@ package com.morpho.app.screens.base import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue +import app.cash.paging.Pager import app.cash.paging.cachedIn import cafe.adriel.voyager.core.model.ScreenModel import cafe.adriel.voyager.core.model.screenModelScope diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/common/SkylineThreadFragment.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/common/SkylineThreadFragment.kt index 9a4947b..a3a89b0 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/common/SkylineThreadFragment.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/common/SkylineThreadFragment.kt @@ -100,7 +100,8 @@ inline fun SkylineThreadFragment( if(parents.size > 3) { ThreadItem( item = thread.parents[0], - modifier = if(debuggable) Modifier.border(1.dp, Color.Green) else Modifier, + modifier = if(debuggable) Modifier.border(1.dp, Color.Green) + else Modifier.padding(vertical = 2.dp), role = PostFragmentRole.ThreadBranchStart, indentLevel = 1, elevate = true, @@ -171,7 +172,8 @@ inline fun SkylineThreadFragment( ThreadItem( item = post, role = role, - modifier = if(debuggable) Modifier.border(1.dp, Color.White) else Modifier, + modifier = if(debuggable) Modifier.border(1.dp, Color.White) + else Modifier.padding(vertical = 2.dp), indentLevel = 1, reason = reason, elevate = true, @@ -195,7 +197,7 @@ inline fun SkylineThreadFragment( modifier = if (debuggable) Modifier.border( 1.dp, Color.Yellow - ) else Modifier, + ) else Modifier.padding(vertical = 2.dp), elevate = true, onItemClicked = onItemClicked, onProfileClicked = onProfileClicked, @@ -231,7 +233,8 @@ inline fun SkylineThreadFragment( ThreadItem( item = post, role = role, - modifier = if(debuggable) Modifier.border(1.dp, Color.Red) else Modifier, + modifier = if(debuggable) Modifier.border(1.dp, Color.Red) + else Modifier.padding(vertical = 2.dp), indentLevel = 1, reason = reason, elevate = true, @@ -260,8 +263,8 @@ inline fun SkylineThreadFragment( role = role, reason = null, elevate = true, - modifier = if(debuggable) Modifier.border(1.dp, Color.Magenta) else Modifier, - //.padding(4.dp), + modifier = if(debuggable) Modifier.border(1.dp, Color.Magenta) + else Modifier.padding(vertical = 2.dp), onItemClicked = onItemClicked, onProfileClicked = onProfileClicked, onUnClicked = onUnClicked, @@ -292,7 +295,7 @@ inline fun SkylineThreadFragment( reason = null, elevate = true, modifier = if(debuggable) Modifier.border(1.dp, Color.Blue) else Modifier - .padding(4.dp), + .padding(top = 4.dp).padding(horizontal = 4.dp), onItemClicked = onItemClicked, onProfileClicked = onProfileClicked, onUnClicked = onUnClicked, @@ -314,7 +317,7 @@ inline fun SkylineThreadFragment( Column( modifier = modifier - .padding(4.dp), + //.padding(4.dp), ) { if(threadPost.replies.size > 2) { TextButton( @@ -347,7 +350,7 @@ inline fun SkylineThreadFragment( if (post.replies.isNotEmpty() && replies.size > 1) { ThreadTree( reply = post, indentLevel = 1, - modifier = Modifier.padding(4.dp), + modifier = Modifier.padding(start = 4.dp, end = 1.dp, bottom = 2.dp), onItemClicked = onItemClicked, onProfileClicked = { onProfileClicked(it) }, onUnClicked = { type,uri-> onUnClicked(type,uri) }, @@ -362,7 +365,7 @@ inline fun SkylineThreadFragment( item = post, role = PostFragmentRole.ThreadEnd, indentLevel = 1, - modifier = Modifier.padding(4.dp), + modifier = Modifier.padding(start = 4.dp, end = 1.dp, bottom = 2.dp), onItemClicked = onItemClicked, onProfileClicked = onProfileClicked, onUnClicked = onUnClicked, From adb8e45bbd69da8f90dee2275de6a8ed92fe6f03 Mon Sep 17 00:00:00 2001 From: Orual Date: Mon, 21 Oct 2024 16:03:35 -0400 Subject: [PATCH 41/42] Switching users works now! --- Butterfly | 2 +- .../app/screens/base/BaseScreenModel.kt | 16 +++++++------- .../app/screens/main/MainScreenModel.kt | 8 ++++--- .../app/screens/main/tabbed/TabbedHomeView.kt | 15 +++++-------- .../main/tabbed/TabbedMainScreenModel.kt | 12 ++++++----- .../morpho/app/ui/settings/UserManagement.kt | 21 ++++++++++++------- 6 files changed, 40 insertions(+), 34 deletions(-) diff --git a/Butterfly b/Butterfly index c4f9d8c..142c3c1 160000 --- a/Butterfly +++ b/Butterfly @@ -1 +1 @@ -Subproject commit c4f9d8c7a938b30fceb032353e8d8b3d19394f9c +Subproject commit 142c3c1711f85fa5fd9fa281c92af11eb6add358 diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/base/BaseScreenModel.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/base/BaseScreenModel.kt index 27fb71b..bd77f57 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/base/BaseScreenModel.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/base/BaseScreenModel.kt @@ -91,24 +91,24 @@ open class BaseScreenModel( } open fun logout() { - agent.logout().invokeOnCompletion { - deinit() - isLoggedIn.value = false - userDid = null - userProfile = null - - } + deinit() + isLoggedIn.value = false + userDid = null + userProfile = null + agent.logout() } open fun switchUser(did: Did) { screenModelScope.launch { deinit() agent.switchUser(did) - userDid = did userProfile = agent.getProfile(did).getOrNull()?.toProfile() + }.invokeOnCompletion { + userDid = did notifJob = screenModelScope.launch { notificationsTick.tick(true) } + isLoggedIn.value = agent.isLoggedIn } } diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/main/MainScreenModel.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/main/MainScreenModel.kt index eeb3351..5331789 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/main/MainScreenModel.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/main/MainScreenModel.kt @@ -85,21 +85,23 @@ open class MainScreenModel( override fun deinit() { super.deinit() + presenterJob?.cancel() feedSources.clear() feedStates.clear() - presenterJob?.cancel() initialized = false } override fun logout() { - super.logout() deinit() + super.logout() + initialize() } override fun switchUser(did: Did) { - super.switchUser(did) deinit() + super.switchUser(did) + initialize() } diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/main/tabbed/TabbedHomeView.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/main/tabbed/TabbedHomeView.kt index 5f7a9ad..e85dad7 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/main/tabbed/TabbedHomeView.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/main/tabbed/TabbedHomeView.kt @@ -85,6 +85,7 @@ import kotlin.math.max import kotlin.math.min import cafe.adriel.voyager.navigator.tab.Tab as NavTab + @Composable public fun CurrentSkylineScreen( sm: TabbedMainScreenModel, @@ -136,20 +137,14 @@ fun TabScreen.TabbedHomeView( var selectedTabIndex by rememberSaveable { mutableIntStateOf(sm.timelineIndex) } - val tabs = remember( - sm.tabs, sm.loaded, sm.tabs.size - ) { - - List(sm.tabs.size) { index -> - + val tabs by derivedStateOf { + sm.tabs.mapIndexed { index, tab -> HomeSkylineTab( - index = index.toUShort(), - title = sm.tabs[index].title, - avatar = sm.tabs[index].avatar, + index = index.toUShort(), title = tab.title, avatar = tab.avatar ) } } - val tabsCreated by derivedStateOf { sm.loaded && sm.tabs.isNotEmpty() } + val tabsCreated by derivedStateOf { sm.loaded && sm.tabs.isNotEmpty() && sm.isLoggedIn.value } if (tabsCreated) { Navigator( tabs.first(), diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/main/tabbed/TabbedMainScreenModel.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/main/tabbed/TabbedMainScreenModel.kt index 3d90dc6..860ed0f 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/main/tabbed/TabbedMainScreenModel.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/main/tabbed/TabbedMainScreenModel.kt @@ -34,7 +34,9 @@ class TabbedMainScreenModel( agent: MorphoAgent, labelService: ContentLabelService, ) : MainScreenModel(agent, labelService) { - val tabs = mutableStateListOf() + private val _tabs = mutableListOf() + val tabs: List + get() = _tabs.toList() val tabPagers = mutableStateMapOf>>() val timelineIndex: Int @@ -55,7 +57,7 @@ class TabbedMainScreenModel( screenModelScope.launch { while(!initialized) delay(10) feedSources.filter { it.pinned == true }.forEach { info -> - tabs.add(info.toContentCardMapEntry()) + _tabs.add(info.toContentCardMapEntry()) (feedPresenters[info.uri]?.pager?.flow?.cachedIn(screenModelScope) as Flow>).let { tabPagers[info.uri] = it @@ -88,21 +90,21 @@ class TabbedMainScreenModel( override fun deinit() { super.deinit() tabPagers.clear() - tabs.clear() + _tabs.clear() loaded = false } override fun logout() { - super.logout() deinit() + super.logout() initialize() initializeTabs() } override fun switchUser(did: Did) { - super.switchUser(did) deinit() + super.switchUser(did) initialize() initializeTabs() } diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/settings/UserManagement.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/settings/UserManagement.kt index 71ed023..ed72355 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/settings/UserManagement.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/settings/UserManagement.kt @@ -62,15 +62,18 @@ fun UserManagement( distinguish: Boolean = false, topLevel: Boolean = false, ) { - val users = profiles.collectAsState(initial = if(myProfile != null) listOf(myProfile) else listOf()) - val loggedInUser = remember { users.value.firstOrNull { it.did == sm.agent.id } } - val otherUsers = remember { users.value.filter { it.did != sm.agent.id } } + val users = profiles.collectAsState(initial = listOf()) + val loggedInUser = users.value.firstOrNull { it.did == sm.agent.id } + val otherUsers = users.value.filterNot { it.did == loggedInUser?.did } val mainNav = remember { when (navigator.level) { 0 -> navigator else -> navigator.parent!! } } + println("Users: $users") + println("Logged in user: $loggedInUser") + println("Other users: $otherUsers") val rootNav = LocalTabNavigator.current val menuOptionClicked: (AccountMenuOption, Did) -> Unit = remember { { option, did -> @@ -96,14 +99,15 @@ fun UserManagement( modifier = modifier.fillMaxWidth(), distinguish = distinguish, ) { - if(loggedInUser != null) { + if(loggedInUser != null || myProfile != null) { + val profile = loggedInUser ?: myProfile!! Text( - text = "Logged in as ${loggedInUser.displayName ?: loggedInUser.handle.handle}", + text = "Logged in as ${profile.displayName ?: profile.handle.handle}", style = MaterialTheme.typography.titleMedium, modifier = Modifier.padding(12.dp) ) AccountItem( - profile = loggedInUser, + profile = profile, onClick = { mainNav.push(MyProfileTab) }, @@ -118,7 +122,10 @@ fun UserManagement( otherUsers.forEach { profile -> AccountItem( profile = profile, - onClick = { sm.switchUser(profile.did) }, + onClick = { + sm.switchUser(profile.did) + mainNav.popUntilRoot() + }, onMenuClicked = menuOptionClicked ) } From 4ba597d2b0dabc0214bd61bcfb30a3b315f81f9a Mon Sep 17 00:00:00 2001 From: Orual Date: Tue, 22 Oct 2024 10:10:23 -0400 Subject: [PATCH 42/42] Some library updates - current showstopper android bug, crash due to nav drawer or modal bottom sheet when switching to notification screen --- Butterfly | 2 +- .../androidMain/kotlin/Platform.android.kt | 44 ------------------ .../kotlin/com/morpho/app/Platform.android.kt | 42 +++++++++++++++++ .../commonMain/kotlin/com/morpho/app/App.kt | 46 ++++++++++++++----- .../app/screens/base/tabbed/NavigationTabs.kt | 9 ++++ .../app/screens/main/tabbed/TabbedHomeView.kt | 14 +++--- .../notifications/NotificationsView.kt | 8 ++-- .../com/morpho/app/ui/common/PostComposer.kt | 2 +- .../morpho/app/ui/common/SkylineFragment.kt | 4 +- .../morpho/app/ui/common/SkylineTopAppBar.kt | 14 +++--- .../app/ui/elements/HighlightIndication.kt | 40 ---------------- .../morpho/app/ui/elements/OutlinedAvatar.kt | 3 +- .../morpho/app/ui/post/EmbedPostFragment.kt | 4 +- .../com/morpho/app/ui/post/PostFragment.kt | 4 +- .../app/ui/profile/CompactProfileFragment.kt | 4 +- .../composeApp/src/desktopMain/kotlin/main.kt | 31 +++++++------ Morpho/gradle/libs.versions.toml | 6 +-- gradle/libs.versions.toml | 6 +-- 18 files changed, 139 insertions(+), 144 deletions(-) diff --git a/Butterfly b/Butterfly index 142c3c1..3d39e7c 160000 --- a/Butterfly +++ b/Butterfly @@ -1 +1 @@ -Subproject commit 142c3c1711f85fa5fd9fa281c92af11eb6add358 +Subproject commit 3d39e7ca8973b8eb26a0af787a4e67214adacdce diff --git a/Morpho/composeApp/src/androidMain/kotlin/Platform.android.kt b/Morpho/composeApp/src/androidMain/kotlin/Platform.android.kt index 6b1bab0..e69de29 100644 --- a/Morpho/composeApp/src/androidMain/kotlin/Platform.android.kt +++ b/Morpho/composeApp/src/androidMain/kotlin/Platform.android.kt @@ -1,44 +0,0 @@ -@file:Suppress("EXPECT_ACTUAL_CLASSIFIERS_ARE_IN_BETA_WARNING") -package com.morpho.app - -import android.os.Build -import android.os.Parcel -import android.os.Parcelable -import kotlinx.datetime.LocalDateTime -import kotlinx.parcelize.Parceler -import kotlinx.parcelize.Parcelize -import kotlinx.parcelize.RawValue -import kotlinx.parcelize.TypeParceler -import java.util.Locale - -actual typealias CommonParcelize = Parcelize -actual typealias CommonParcelable = Parcelable - -actual typealias CommonRawValue = RawValue -actual typealias CommonParceler = Parceler -actual typealias CommonTypeParceler = TypeParceler -actual object LocalDateTimeParceler : Parceler { - override fun create(parcel: Parcel): LocalDateTime { - val date = parcel.readString() - return date?.let { LocalDateTime.parse(it) } - ?: LocalDateTime(0, 0, 0, 0, 0) - } - - override fun LocalDateTime.write(parcel: Parcel, flags: Int) { - parcel.writeString(this.toString()) - } -} - -class AndroidPlatform : Platform { - override val name: String = "Android ${Build.VERSION.SDK_INT}" -} - -actual fun getPlatform(): Platform = AndroidPlatform() - - - -actual val myLang:String? - get() = Locale.getDefault().language - -actual val myCountry:String? - get() = Locale.getDefault().country \ No newline at end of file diff --git a/Morpho/composeApp/src/androidMain/kotlin/com/morpho/app/Platform.android.kt b/Morpho/composeApp/src/androidMain/kotlin/com/morpho/app/Platform.android.kt index d3bf814..46d0f5f 100644 --- a/Morpho/composeApp/src/androidMain/kotlin/com/morpho/app/Platform.android.kt +++ b/Morpho/composeApp/src/androidMain/kotlin/com/morpho/app/Platform.android.kt @@ -1,5 +1,47 @@ +@file:Suppress("EXPECT_ACTUAL_CLASSIFIERS_ARE_IN_BETA_WARNING") package com.morpho.app +import android.os.Build +import android.os.Parcel +import android.os.Parcelable +import kotlinx.datetime.LocalDateTime +import kotlinx.parcelize.Parceler +import kotlinx.parcelize.Parcelize +import kotlinx.parcelize.RawValue +import kotlinx.parcelize.TypeParceler +import java.util.Locale + +actual typealias CommonParcelize = Parcelize +actual typealias CommonParcelable = Parcelable + +actual typealias CommonRawValue = RawValue +actual typealias CommonParceler = Parceler +actual typealias CommonTypeParceler = TypeParceler +actual object LocalDateTimeParceler : Parceler { + override fun create(parcel: Parcel): LocalDateTime { + val date = parcel.readString() + return date?.let { LocalDateTime.parse(it) } + ?: LocalDateTime(0, 0, 0, 0, 0) + } + + override fun LocalDateTime.write(parcel: Parcel, flags: Int) { + parcel.writeString(this.toString()) + } +} + +class AndroidPlatform : Platform { + override val name: String = "Android ${Build.VERSION.SDK_INT}" +} + +actual fun getPlatform(): Platform = AndroidPlatform() + + + +actual val myLang:String? + get() = Locale.getDefault().language + +actual val myCountry:String? + get() = Locale.getDefault().country actual fun getPlatformStorageDir(baseDir: String): String { return baseDir diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/App.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/App.kt index ac2c539..e072c1b 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/App.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/App.kt @@ -1,41 +1,63 @@ package com.morpho.app +import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import cafe.adriel.voyager.core.annotation.ExperimentalVoyagerApi import cafe.adriel.voyager.jetpack.ProvideNavigatorLifecycleKMPSupport import cafe.adriel.voyager.navigator.tab.CurrentTab import cafe.adriel.voyager.navigator.tab.TabNavigator import com.morpho.app.data.ContentLabelService +import com.morpho.app.data.DarkModeSetting import com.morpho.app.data.MorphoAgent import com.morpho.app.screens.base.tabbed.TabbedBaseScreen import com.morpho.app.screens.login.LoginScreen import com.morpho.app.screens.main.tabbed.TabbedMainScreenModel +import com.morpho.app.ui.theme.MorphoTheme +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.mapLatest import org.jetbrains.compose.resources.ExperimentalResourceApi import org.jetbrains.compose.ui.tooling.preview.Preview import org.koin.compose.KoinContext import org.koin.compose.koinInject import org.koin.core.parameter.parametersOf -@OptIn(ExperimentalResourceApi::class, ExperimentalVoyagerApi::class) +@OptIn(ExperimentalResourceApi::class, ExperimentalVoyagerApi::class, + ExperimentalCoroutinesApi::class +) @Composable @Preview fun App() { KoinContext { - MaterialTheme { - ProvideNavigatorLifecycleKMPSupport { - val agent = koinInject() - val labelService = koinInject() - val screenModel = koinInject( - parameters = { parametersOf(agent, labelService) } - ) - val loggedIn by screenModel.isLoggedIn - .collectAsState(initial = screenModel.isLoggedIn.value) - + //ProvideNavigatorLifecycleKMPSupport { + val agent = koinInject() + val labelService = koinInject() + val screenModel = koinInject( + parameters = { parametersOf(agent, labelService) } + ) + val loggedIn by screenModel.isLoggedIn + .collectAsState(initial = screenModel.isLoggedIn.value) + val morphoPrefs by derivedStateOf { agent.morphoPrefs } + val isSystemInDarkTheme = isSystemInDarkTheme() + val darkTheme by morphoPrefs.mapLatest { + when(it.darkMode){ + DarkModeSetting.SYSTEM -> isSystemInDarkTheme + DarkModeSetting.LIGHT -> false + DarkModeSetting.DARK -> true + null -> isSystemInDarkTheme + } + }.collectAsState(when(morphoPrefs.value.darkMode){ + DarkModeSetting.SYSTEM -> isSystemInDarkTheme + DarkModeSetting.LIGHT -> false + DarkModeSetting.DARK -> true + null -> isSystemInDarkTheme + }) + MorphoTheme(darkTheme = darkTheme) { TabNavigator( tab = if (loggedIn) { TabbedBaseScreen @@ -54,6 +76,6 @@ fun App() { CurrentTab() } } - } + // } } } \ No newline at end of file diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/base/tabbed/NavigationTabs.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/base/tabbed/NavigationTabs.kt index 762e9a2..83c3e55 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/base/tabbed/NavigationTabs.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/base/tabbed/NavigationTabs.kt @@ -342,6 +342,9 @@ data object MyProfileTab: TabScreen { } +@Parcelize +@Immutable +@Serializable data object SettingsTab: TabScreen { override val key: ScreenKey get() = "SettingsTab${uniqueScreenKey}" @@ -377,6 +380,9 @@ data object SettingsTab: TabScreen { } } +@Parcelize +@Immutable +@Serializable data class FeedPageTab( val feed: FeedGenerator, ): TabScreen { @@ -403,6 +409,9 @@ data class FeedPageTab( } } +@Parcelize +@Immutable +@Serializable data class UserListPageTab( val list: UserList, ): TabScreen { diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/main/tabbed/TabbedHomeView.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/main/tabbed/TabbedHomeView.kt index e85dad7..5c49146 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/main/tabbed/TabbedHomeView.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/main/tabbed/TabbedHomeView.kt @@ -265,13 +265,13 @@ fun HomeTabRow( SecondaryScrollableTabRow( selectedTabIndex = selectedTabIndex, edgePadding = 10.dp, - indicator = { tabPositions -> - if(tabPositions.isNotEmpty()) { - TabRowDefaults.SecondaryIndicator( - Modifier.tabIndicatorOffset(tabPositions[max(0, min(selectedTabIndex, tabs.lastIndex))]) - ) - } - }, +// indicator = { tabPositions -> +// if(tabPositions.isNotEmpty()) { +// TabRowDefaults.SecondaryIndicator( +// Modifier.tabIndicatorOffset(tabPositions[max(0, min(selectedTabIndex, tabs.lastIndex))]) +// ) +// } +// }, divider = {}, //modifier = Modifier.offset(y = 8.dp), ) { diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/notifications/NotificationsView.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/notifications/NotificationsView.kt index b20cad6..636b542 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/notifications/NotificationsView.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/screens/notifications/NotificationsView.kt @@ -90,7 +90,7 @@ fun TabScreen.NotificationViewContent( val pager = sm.notifications.collectAsLazyPagingItems() var uiState by rememberSaveable { mutableStateOf(NotificationsUIState()) } val toMarkRead = mutableStateListOf() - + val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed) TabbedScreenScaffold( navBar = { navBar(navigator) }, @@ -109,7 +109,7 @@ fun TabScreen.NotificationViewContent( var initialContent: BskyPost? by remember { mutableStateOf(null) } var showComposer by remember { mutableStateOf(false)} var composerRole by remember { mutableStateOf(ComposerRole.StandalonePost)} - val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) + // Probably pull this farther up, // but this means if you don't explicitly cancel you don't lose the post var draft by remember{ mutableStateOf(DraftPost()) } @@ -170,9 +170,9 @@ fun TabScreen.NotificationViewContent( items( count = pager.itemCount, - key = { pager.itemKey { + key = pager.itemKey { it.hashCode() - }}, + }, contentType = { NotificationsListItem } diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/common/PostComposer.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/common/PostComposer.kt index 504795a..1c90c7a 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/common/PostComposer.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/common/PostComposer.kt @@ -62,7 +62,7 @@ inline fun BottomSheetPostComposer( if (!sheetState.isVisible) { onDismissRequest() } } }, containerColor = MaterialTheme.colorScheme.background, sheetState = sheetState, - windowInsets = WindowInsets.navigationBars.union(WindowInsets.ime), + contentWindowInsets = { WindowInsets.navigationBars.union(WindowInsets.ime) }, ){ PostComposer( diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/common/SkylineFragment.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/common/SkylineFragment.kt index 82ce2bc..b6e487e 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/common/SkylineFragment.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/common/SkylineFragment.kt @@ -225,9 +225,9 @@ fun SkylineFragment ( else -> { items( count = pager.itemCount, - key = { pager.itemKey { + key = pager.itemKey { it.hashCode() - }}, + }, contentType = { MorphoDataItem } diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/common/SkylineTopAppBar.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/common/SkylineTopAppBar.kt index ebfe567..51f77c5 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/common/SkylineTopAppBar.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/common/SkylineTopAppBar.kt @@ -71,13 +71,13 @@ fun SkylineTopBar( selectedTabIndex = selectedTab, modifier = modifier.offset(y = (-8).dp, x = 4.dp ), edgePadding = 10.dp, - indicator = { tabPositions -> - if(tabPositions.isNotEmpty()) { - TabRowDefaults.SecondaryIndicator( - Modifier.tabIndicatorOffset(tabPositions[max(0, min(selectedTab, tabList.lastIndex))]) - ) - } - }, +// indicator = { tabPositions -> +// if(tabPositions.isNotEmpty()) { +// TabRowDefaults.SecondaryIndicator( +// Modifier.tabIndicatorOffset(tabPositions[max(0, min(selectedTab, tabList.lastIndex))]) +// ) +// } +// }, ) { tabList.forEachIndexed { index, tab -> Tab(selected = selectedTab == index, diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/elements/HighlightIndication.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/elements/HighlightIndication.kt index a76c6a0..01d5e9a 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/elements/HighlightIndication.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/elements/HighlightIndication.kt @@ -1,41 +1 @@ package com.morpho.app.ui.elements - -import androidx.compose.foundation.Indication -import androidx.compose.foundation.IndicationInstance -import androidx.compose.foundation.interaction.InteractionSource -import androidx.compose.foundation.interaction.collectIsFocusedAsState -import androidx.compose.runtime.Composable -import androidx.compose.runtime.State -import androidx.compose.runtime.getValue -import androidx.compose.runtime.remember -import androidx.compose.ui.geometry.CornerRadius -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.drawscope.ContentDrawScope -import androidx.compose.ui.graphics.drawscope.Stroke -import androidx.compose.ui.unit.dp - -class MorphoHighlightIndicationInstance(isEnabledState: State) : - IndicationInstance { - private val isEnabled by isEnabledState - override fun ContentDrawScope.drawIndication() { - drawContent() - if (isEnabled) { - drawRoundRect(cornerRadius = CornerRadius(4.dp.toPx()), size = size, color = Color.Gray, alpha = 0.2f) - drawRoundRect(cornerRadius = CornerRadius(4.dp.toPx()), - style = Stroke(width = Stroke.HairlineWidth), - size = size, color = Color.White, alpha = 0.9f) - } - } - -} - -class MorphoHighlightIndication : Indication { - @Composable - override fun rememberUpdatedInstance(interactionSource: InteractionSource): - IndicationInstance { - val isFocusedState = interactionSource.collectIsFocusedAsState() - return remember(interactionSource) { - MorphoHighlightIndicationInstance(isEnabledState = isFocusedState) - } - } -} \ No newline at end of file diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/elements/OutlinedAvatar.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/elements/OutlinedAvatar.kt index 8c13037..ae4f156 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/elements/OutlinedAvatar.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/elements/OutlinedAvatar.kt @@ -16,6 +16,7 @@ package com.morpho.app.ui.elements * limitations under the License. */ +import androidx.compose.foundation.LocalIndication import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.* @@ -75,7 +76,7 @@ fun OutlinedAvatar( AvatarShape.Corner -> roundedTopLBotR.small } val interactionSource = remember { MutableInteractionSource() } - val indication = remember { MorphoHighlightIndication() } + val indication = LocalIndication.current val pxSize = LocalDensity.current.run { (size-outlineSize).toPx()*2 }.toInt() val sB = when(avatarShape) { AvatarShape.Circle -> CircleShape.createOutline( diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/post/EmbedPostFragment.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/post/EmbedPostFragment.kt index 7197dd5..7453c97 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/post/EmbedPostFragment.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/post/EmbedPostFragment.kt @@ -2,6 +2,7 @@ package com.morpho.app.ui.post import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.LocalIndication import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.* @@ -22,7 +23,6 @@ import androidx.compose.ui.text.withStyle import androidx.compose.ui.unit.dp import androidx.compose.ui.util.fastForEach import com.morpho.app.model.bluesky.* -import com.morpho.app.ui.elements.MorphoHighlightIndication import com.morpho.app.ui.elements.OutlinedAvatar import com.morpho.app.ui.elements.RichTextElement import com.morpho.app.ui.elements.WrappedColumn @@ -47,7 +47,7 @@ fun EmbedPostFragment( var hidePost by rememberSaveable { mutableStateOf(post.author.mutedByMe) } val muted = rememberSaveable { post.author.mutedByMe } val interactionSource = remember { MutableInteractionSource() } - val indication = remember { MorphoHighlightIndication() } + val indication = LocalIndication.current val uriHandler = LocalUriHandler.current WrappedColumn( modifier diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/post/PostFragment.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/post/PostFragment.kt index 192acf5..2e649a3 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/post/PostFragment.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/post/PostFragment.kt @@ -2,6 +2,7 @@ package com.morpho.app.ui.post import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.LocalIndication import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Arrangement @@ -52,7 +53,6 @@ import com.morpho.app.ui.common.OnPostClicked import com.morpho.app.ui.elements.AvatarShape import com.morpho.app.ui.elements.ContentHider import com.morpho.app.ui.elements.MenuOptions -import com.morpho.app.ui.elements.MorphoHighlightIndication import com.morpho.app.ui.elements.OutlinedAvatar import com.morpho.app.ui.elements.RichTextElement import com.morpho.app.ui.elements.WrappedColumn @@ -119,7 +119,7 @@ fun PostFragment( }} val interactionSource = remember { MutableInteractionSource() } - val indication = remember { MorphoHighlightIndication() } + val indication = LocalIndication.current val bgColor = if (role == PostFragmentRole.PrimaryThreadRoot) { MaterialTheme.colorScheme.background } else { diff --git a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/profile/CompactProfileFragment.kt b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/profile/CompactProfileFragment.kt index 29cd6ee..e929401 100644 --- a/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/profile/CompactProfileFragment.kt +++ b/Morpho/composeApp/src/commonMain/kotlin/com/morpho/app/ui/profile/CompactProfileFragment.kt @@ -1,5 +1,6 @@ package com.morpho.app.ui.profile +import androidx.compose.foundation.LocalIndication import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Column @@ -28,7 +29,6 @@ import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.currentOrThrow import com.morpho.app.model.bluesky.DetailedProfile import com.morpho.app.ui.elements.AvatarShape -import com.morpho.app.ui.elements.MorphoHighlightIndication import com.morpho.app.ui.elements.OutlinedAvatar import com.morpho.app.ui.elements.RichTextElement import com.morpho.app.ui.elements.WrappedColumn @@ -48,7 +48,7 @@ fun CompactProfileFragment( ), ) { val interactionSource = remember { MutableInteractionSource() } - val indication = remember { MorphoHighlightIndication() } + val indication = LocalIndication.current WrappedColumn(modifier = modifier.fillMaxWidth()) { Surface ( shadowElevation = if (elevate ) 2.dp else 0.dp, diff --git a/Morpho/composeApp/src/desktopMain/kotlin/main.kt b/Morpho/composeApp/src/desktopMain/kotlin/main.kt index 3bc7f51..cfbac5d 100644 --- a/Morpho/composeApp/src/desktopMain/kotlin/main.kt +++ b/Morpho/composeApp/src/desktopMain/kotlin/main.kt @@ -25,6 +25,7 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.remember @@ -58,6 +59,8 @@ import com.morpho.app.getPlatformStorageDir import com.morpho.app.ui.theme.MorphoTheme import com.morpho.butterfly.auth.SessionRepository import com.morpho.butterfly.auth.UserRepository +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.mapLatest import morpho.composeapp.generated.resources.Res import morpho.composeapp.generated.resources.morpho_icon_transparent import org.jetbrains.compose.resources.ExperimentalResourceApi @@ -81,7 +84,9 @@ fun getLogger(): Logger = LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME) fun getLogger(name: String): Logger = LoggerFactory.getLogger(name) -@OptIn(KoinExperimentalAPI::class, ExperimentalResourceApi::class, ExperimentalVoyagerApi::class) +@OptIn(KoinExperimentalAPI::class, ExperimentalResourceApi::class, ExperimentalVoyagerApi::class, + ExperimentalCoroutinesApi::class +) fun main() = application { StatusPrinter2().print(LoggerFactory.getILoggerFactory() as LoggerContext) KmLogging.setLoggers(PlatformLogger(VariableLogLevel(LogLevel.Verbose))) @@ -108,11 +113,7 @@ fun main() = application { Window( onCloseRequest = { - // possible hack to catch the exception on exit - try { (::exitApplication)() } catch (e: Exception) { - e.printStackTrace() - - } + (::exitApplication)() }, state = windowState, title = "Morpho", @@ -120,21 +121,25 @@ fun main() = application { transparent = undecorated, icon = painterResource(Res.drawable.morpho_icon_transparent) ) { - val darkTheme by derivedStateOf { when(morphoPrefs.value.darkMode){ + val darkTheme by morphoPrefs.mapLatest { + when(it.darkMode){ + DarkModeSetting.SYSTEM -> isSystemInDarkTheme() + DarkModeSetting.LIGHT -> false + DarkModeSetting.DARK -> true + null -> isSystemInDarkTheme() + } + }.collectAsState(when(morphoPrefs.value.darkMode){ DarkModeSetting.SYSTEM -> isSystemInDarkTheme() DarkModeSetting.LIGHT -> false DarkModeSetting.DARK -> true null -> isSystemInDarkTheme() - } } - MorphoTheme(darkTheme = darkTheme) { + }) + MorphoTheme(darkTheme = darkTheme, dynamicColor = false) { if(undecorated) { MorphoWindow( windowState = windowState, onCloseRequest = { - try { (::exitApplication)() } catch (e: Exception) { - e.printStackTrace() - - } + (::exitApplication)() } ) { App() diff --git a/Morpho/gradle/libs.versions.toml b/Morpho/gradle/libs.versions.toml index f59a621..4fa05c9 100644 --- a/Morpho/gradle/libs.versions.toml +++ b/Morpho/gradle/libs.versions.toml @@ -12,8 +12,8 @@ androidx-espresso-core = "3.6.1" androidx-material = "1.12.0" androidx-test-junit = "1.2.1" appdirs = "1.2.2" -compose = "1.6.8" -compose-plugin = "1.6.11" +compose = "1.6.7" +compose-plugin = "1.7.0" constraintlayoutComposeMultiplatform = "0.4.0" datastorePreferencesCore = "1.1.1" imageLoader = "1.7.8" @@ -71,7 +71,7 @@ androidx-paging-runtime = { module = "androidx.paging:paging-runtime", version.r androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "androidx-espresso-core" } androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "androidx-appcompat" } androidx-material = { group = "com.google.android.material", name = "material", version.ref = "androidx-material" } -androidx-material3 = { group = "com.google.android.material3", name = "material" } +androidx-material3 = { group = "com.google.android.material3", name = "material", version.ref = "androidx-material" } androidx-constraintlayout = { group = "androidx.constraintlayout", name = "constraintlayout", version.ref = "androidx-constraintlayout" } androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "androidx-activityCompose" } appdirs = { module = "net.harawata:appdirs", version.ref = "appdirs" } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index cef4ed9..1ffb2ff 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -12,8 +12,8 @@ androidx-espresso-core = "3.6.1" androidx-material = "1.12.0" androidx-test-junit = "1.2.1" appdirs = "1.2.2" -compose = "1.6.8" -compose-plugin = "1.6.11" +compose = "1.6.7" +compose-plugin = "1.7.0" constraintlayoutComposeMultiplatform = "0.4.0" datastorePreferencesCore = "1.1.1" filekit = "0.8.2" @@ -71,7 +71,7 @@ androidx-test-junit = { group = "androidx.test.ext", name = "junit", version.ref androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "androidx-espresso-core" } androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "androidx-appcompat" } androidx-material = { group = "com.google.android.material", name = "material", version.ref = "androidx-material" } -androidx-material3 = { group = "com.google.android.material3", name = "material" } +androidx-material3 = { group = "com.google.android.material3", name = "material", version.ref = "androidx-material" } androidx-constraintlayout = { group = "androidx.constraintlayout", name = "constraintlayout", version.ref = "androidx-constraintlayout" } androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "androidx-activityCompose" } accompanist-permissions = { module = "com.google.accompanist:accompanist-permissions", version.ref = "accompanist-permissions" }

%&@v=gYvc4vNT7#e@jbP-k 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 M=B16z)B9}y7kF7-Pyhe` literal 0 HcmV?d00001 diff --git a/Morpho/composeApp/src/commonMain/composeResources/font/IBMPlexSans-Regular.ttf b/Morpho/composeApp/src/commonMain/composeResources/font/IBMPlexSans-Regular.ttf new file mode 100644 index 0000000000000000000000000000000000000000..b5819647e1dfa7dae61b29e582f3464a90c537e1 GIT binary patch literal 175748 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 Omrr(PR(4MAwEho+ftIfT literal 0 HcmV?d00001 diff --git a/Morpho/composeApp/src/commonMain/composeResources/font/IBMPlexSans-SemiBold.ttf b/Morpho/composeApp/src/commonMain/composeResources/font/IBMPlexSans-SemiBold.ttf new file mode 100644 index 0000000000000000000000000000000000000000..a5bd9ee62898e170f975ac7a7bb2e255bfbd7ab4 GIT binary patch literal 177272 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 XcWv*}^{dUiJP*buOx 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 EA7Mlp{Qv*} literal 0 HcmV?d00001 diff --git a/Morpho/composeApp/src/commonMain/composeResources/font/IBMPlexSans-Thin.ttf b/Morpho/composeApp/src/commonMain/composeResources/font/IBMPlexSans-Thin.ttf new file mode 100644 index 0000000000000000000000000000000000000000..910458a9e8c595366546f20d9dcb513bc2fb0428 GIT binary patch literal 178896 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 Q{KyUKIx;&u*6lL>AKz%1D*ylh literal 0 HcmV?d00001 diff --git a/Morpho/composeApp/src/commonMain/composeResources/font/IBMPlexSans-ThinItalic.ttf b/Morpho/composeApp/src/commonMain/composeResources/font/IBMPlexSans-ThinItalic.ttf new file mode 100644 index 0000000000000000000000000000000000000000..d5b4be6556c156b5554c0fbcaab27732c9ac1775 GIT binary patch literal 188304 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 Unit ) { + MaterialTheme( + colorScheme = if (darkTheme) { + DarkColorScheme + } else { + LightColorScheme + }, + typography = MorphoTypography(), + content = content + ) } \ No newline at end of file diff --git a/Morpho/composeApp/src/main/res/font/ibm_plex_sans.ttf b/Morpho/composeApp/src/main/res/font/ibm_plex_sans.ttf new file mode 100644 index 0000000000000000000000000000000000000000..f18ce2160735d7277cd5343e2947cbb7d7b7b4cd GIT binary patch literal 120340 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 NH%Ib(li`-q&OIL+#orW50ccmQ7@ACYUAHT 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