diff --git a/shared/src/commonMain/composeResources/values-de/strings.xml b/shared/src/commonMain/composeResources/values-de/strings.xml index 1d5fd5fe8..c9fc4079d 100644 --- a/shared/src/commonMain/composeResources/values-de/strings.xml +++ b/shared/src/commonMain/composeResources/values-de/strings.xml @@ -26,7 +26,6 @@ Für den Quellcode und weitere technische Details besuchen Sie das GitHub-Reposi Über die App Unsere Partner Verhaltenskodex - Newsfeed Einstellungen Twitter Slack @@ -79,10 +78,6 @@ Wir verarbeiten Ihre Daten gemäß dem Datenschutzhinweis der App. Sie können I Erhalten Sie Erinnerungen für Sitzungen, die Sie im Zeitplan mit einem Lesezeichen versehen haben, damit Sie sie nicht verpassen. Zeitplan-Updates Bleiben Sie über wichtige Änderungen informiert! Erhalten Sie eine Benachrichtigung, wenn es eine bedeutende Aktualisierung des Veranstaltungszeitplans gibt. - KotlinConf-Neuigkeiten - Erhalten Sie Neuigkeiten zwischen KotlinConf-Veranstaltungen, einschließlich Benachrichtigungen über Ticketverkäufe, neue Videos, Wettbewerbe, Zeitplanankündigungen und mehr. - JetBrains-Neuigkeiten - Erhalten Sie Neuigkeiten über JetBrains-Produkte. Lass uns anfangen! Verhaltenskodex @@ -135,9 +130,6 @@ Wir verarbeiten Ihre Daten gemäß dem Datenschutzhinweis der App. Sie können I Erdgeschoss Erster Stock - Neuigkeiten - Keine Neuigkeiten... ist das eine gute Nachricht? - Sprecher Keine Ergebnisse Unsere Sprecher spielen Verstecken. Geben Sie uns einen Moment, um sie zu finden! diff --git a/shared/src/commonMain/composeResources/values/strings.xml b/shared/src/commonMain/composeResources/values/strings.xml index a14b9893f..6ebf7958b 100644 --- a/shared/src/commonMain/composeResources/values/strings.xml +++ b/shared/src/commonMain/composeResources/values/strings.xml @@ -26,7 +26,6 @@ For the source code and more technical details, visit the GitHub repository.About the app Our partners Code of conduct - News feed Settings Twitter Slack @@ -79,10 +78,6 @@ We will process your data in accordance with the App Privacy Notice. You can adj Get reminders for sessions that you’ve bookmarked in the schedule to make sure you don’t miss them. Schedule updates Stay informed about crucial changes! Get an alert if there's a significant update to the event schedule. - KotlinConf News - Receive news in between KotlinConf events, including notifications about ticket sales, new videos, contests, schedule announcements, and more. - JetBrains News - Get news about JetBrains products. Let’s get started! Code of Conduct @@ -135,9 +130,6 @@ We will process your data in accordance with the App Privacy Notice. You can adj Ground floor First floor - News - No news… is good news? - Speakers No results Our speakers are playing hide and seek. Give us a moment to find them! diff --git a/shared/src/commonMain/kotlin/org/jetbrains/kotlinconf/APIClient.kt b/shared/src/commonMain/kotlin/org/jetbrains/kotlinconf/APIClient.kt index a8e23889e..a5e6f7b9c 100644 --- a/shared/src/commonMain/kotlin/org/jetbrains/kotlinconf/APIClient.kt +++ b/shared/src/commonMain/kotlin/org/jetbrains/kotlinconf/APIClient.kt @@ -142,9 +142,6 @@ class APIClient( } } - suspend fun getNews(): List? { - return safeApiCall { client.get("news").body().items } - } /** * Runs the [call], returning its result or `null` if exceptions occurred. diff --git a/shared/src/commonMain/kotlin/org/jetbrains/kotlinconf/AppInit.kt b/shared/src/commonMain/kotlin/org/jetbrains/kotlinconf/AppInit.kt index 12ec898b6..8a8ed62af 100644 --- a/shared/src/commonMain/kotlin/org/jetbrains/kotlinconf/AppInit.kt +++ b/shared/src/commonMain/kotlin/org/jetbrains/kotlinconf/AppInit.kt @@ -7,12 +7,9 @@ import com.mmk.kmpnotifier.notification.configuration.NotificationPlatformConfig import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob -import org.jetbrains.kotlinconf.navigation.navigateToNews import org.jetbrains.kotlinconf.navigation.navigateToSession import org.jetbrains.kotlinconf.screens.AboutConferenceViewModel import org.jetbrains.kotlinconf.screens.LicensesViewModel -import org.jetbrains.kotlinconf.screens.NewsDetailViewModel -import org.jetbrains.kotlinconf.screens.NewsListViewModel import org.jetbrains.kotlinconf.screens.PrivacyNoticeViewModel import org.jetbrains.kotlinconf.screens.ScheduleViewModel import org.jetbrains.kotlinconf.screens.SessionViewModel @@ -81,8 +78,6 @@ private fun initKoin( val viewModelModule = module { viewModelOf(::AboutConferenceViewModel) viewModelOf(::LicensesViewModel) - viewModelOf(::NewsDetailViewModel) - viewModelOf(::NewsListViewModel) viewModelOf(::PrivacyNoticeViewModel) viewModelOf(::ScheduleViewModel) viewModelOf(::SessionViewModel) @@ -104,31 +99,24 @@ private fun initNotifier( ) { NotifierManager.initialize(configuration) NotifierManager.addListener(object : Listener { - var taggedLogger: TaggedLogger? = logger.tagged("KMPNotifier") + var taggedLogger: TaggedLogger = logger.tagged("KMPNotifier") override fun onNotificationClicked(data: PayloadData) { super.onNotificationClicked(data) - taggedLogger?.log { "Notification clicked with $data" } - - val newsId = data[PushNotificationConstants.KEY_NEWS_ID] as? String - if (newsId != null) { - taggedLogger?.log { "Navigating to news: $newsId" } - navigateToNews(newsId) - return - } + taggedLogger.log { "Notification clicked with $data" } val sessionId = data[PushNotificationConstants.KEY_SESSION_ID] as? String if (sessionId != null) { - taggedLogger?.log { "Navigating to session: $sessionId" } + taggedLogger.log { "Navigating to session: $sessionId" } navigateToSession(SessionId(sessionId)) return } - taggedLogger?.log { "No data to navigate with, ignoring notification" } + taggedLogger.log { "No data to navigate with, ignoring notification" } } override fun onNewToken(token: String) { - taggedLogger?.log { "New token received: $token" } + taggedLogger.log { "New token received: $token" } } }) } diff --git a/shared/src/commonMain/kotlin/org/jetbrains/kotlinconf/ConferenceService.kt b/shared/src/commonMain/kotlin/org/jetbrains/kotlinconf/ConferenceService.kt index 8823a368f..4e325dbed 100644 --- a/shared/src/commonMain/kotlin/org/jetbrains/kotlinconf/ConferenceService.kt +++ b/shared/src/commonMain/kotlin/org/jetbrains/kotlinconf/ConferenceService.kt @@ -54,9 +54,6 @@ class ConferenceService( // Download fresh conference data loadConferenceData() - // Load fresh news items - loadNews() - // Wait for user ID to be loaded userIdLoaded.await() @@ -76,8 +73,6 @@ class ConferenceService( val notifier = NotifierManager.getPushNotifier() listOf( settings.scheduleUpdates to PushNotificationConstants.TOPIC_SCHEDULE_UPDATES, - settings.kotlinConfNews to PushNotificationConstants.TOPIC_KOTLINCONF_NEWS, - settings.jetBrainsNews to PushNotificationConstants.TOPIC_JETBRAINS_NEWS, ).forEach { (enabled, topic) -> if (enabled) notifier.subscribeToTopic(topic) else notifier.unSubscribeFromTopic(topic) @@ -86,12 +81,6 @@ class ConferenceService( } } - val news: Flow> = storage.getNews() - .map { newsItems -> - val now = timeProvider.now() - newsItems.map { newsItem -> mapNewsItemToDisplayItem(newsItem, now) } - } - .flowOn(Dispatchers.Default) val agenda: StateFlow> = combine( @@ -235,8 +224,6 @@ class ConferenceService( it ?: NotificationSettings( sessionReminders = true, scheduleUpdates = true, - kotlinConfNews = true, - jetBrainsNews = true, ) } @@ -284,10 +271,6 @@ class ConferenceService( fun sessionsForSpeakerFlow(id: SpeakerId): Flow> = sessionCards.map { sessions -> sessions.filter { id in it.speakerIds } } - fun newsById(newsId: String): Flow = - news.map { allNews -> - allNews.find { it.id == newsId } - } fun setFavorite(sessionId: SessionId, favorite: Boolean) { scope.launch { @@ -362,32 +345,6 @@ class ConferenceService( localNotificationService.cancel(LocalNotificationId(Type.SessionEnd, sessionId.id)) } - private fun mapNewsItemToDisplayItem( - item: NewsItem, - now: LocalDateTime, - ): NewsDisplayItem { - return NewsDisplayItem( - id = item.id, - photoUrl = item.photoUrl, - date = item.publicationDate.toNewsDisplayTime(now), - title = item.title, - content = item.content, - ) - } - - private fun LocalDateTime.toNewsDisplayTime(now: LocalDateTime): String { - val isToday = year == now.year && dayOfYear == now.dayOfYear - return when { - isToday -> DateTimeFormatting.time(this) - year == now.year -> DateTimeFormatting.date(this) - else -> DateTimeFormatting.dateWithYear(this) - } - } - - suspend fun loadNews() { - val news = client.getNews() ?: return - storage.setNews(news) - } private suspend fun syncVotes() { if (!checkUserId()) return diff --git a/shared/src/commonMain/kotlin/org/jetbrains/kotlinconf/Model.kt b/shared/src/commonMain/kotlin/org/jetbrains/kotlinconf/Model.kt index 4f7976d8c..f79dd7c2d 100644 --- a/shared/src/commonMain/kotlin/org/jetbrains/kotlinconf/Model.kt +++ b/shared/src/commonMain/kotlin/org/jetbrains/kotlinconf/Model.kt @@ -119,8 +119,6 @@ data class NewsListResponse( data class NotificationSettings( val sessionReminders: Boolean, val scheduleUpdates: Boolean, - val kotlinConfNews: Boolean, - val jetBrainsNews: Boolean, ) { - fun hasAnyEnabled() = sessionReminders || scheduleUpdates || kotlinConfNews || jetBrainsNews + fun hasAnyEnabled() = sessionReminders || scheduleUpdates } diff --git a/shared/src/commonMain/kotlin/org/jetbrains/kotlinconf/PushNotificationConstants.kt b/shared/src/commonMain/kotlin/org/jetbrains/kotlinconf/PushNotificationConstants.kt index 065b6862d..b1171d401 100644 --- a/shared/src/commonMain/kotlin/org/jetbrains/kotlinconf/PushNotificationConstants.kt +++ b/shared/src/commonMain/kotlin/org/jetbrains/kotlinconf/PushNotificationConstants.kt @@ -1,10 +1,7 @@ package org.jetbrains.kotlinconf object PushNotificationConstants { - const val TOPIC_JETBRAINS_NEWS = "JetBrainsNews" - const val TOPIC_KOTLINCONF_NEWS = "KotlinConfNews" const val TOPIC_SCHEDULE_UPDATES = "ScheduleUpdates" - const val KEY_NEWS_ID = "newsId" const val KEY_SESSION_ID = "sessionId" } diff --git a/shared/src/commonMain/kotlin/org/jetbrains/kotlinconf/navigation/KotlinConfNavHost.kt b/shared/src/commonMain/kotlin/org/jetbrains/kotlinconf/navigation/KotlinConfNavHost.kt index c43df2c31..b04093095 100644 --- a/shared/src/commonMain/kotlin/org/jetbrains/kotlinconf/navigation/KotlinConfNavHost.kt +++ b/shared/src/commonMain/kotlin/org/jetbrains/kotlinconf/navigation/KotlinConfNavHost.kt @@ -32,8 +32,6 @@ import org.jetbrains.kotlinconf.screens.DeveloperMenuScreen import org.jetbrains.kotlinconf.screens.LicensesScreen import org.jetbrains.kotlinconf.screens.MainScreen import org.jetbrains.kotlinconf.screens.NestedMapScreen -import org.jetbrains.kotlinconf.screens.NewsDetailScreen -import org.jetbrains.kotlinconf.screens.NewsListScreen import org.jetbrains.kotlinconf.screens.PartnerDetailScreen import org.jetbrains.kotlinconf.screens.PartnersScreen import org.jetbrains.kotlinconf.screens.VisitorPrivacyNotice @@ -61,10 +59,6 @@ fun navigateToSession(sessionId: SessionId) { notificationNavRequests.trySend(SessionScreen(sessionId)) } -fun navigateToNews(newsId: String) { - notificationNavRequests.trySend(NewsDetailScreen(newsId = newsId)) -} - private val notificationNavRequests = Channel(capacity = 1) @Composable @@ -236,17 +230,6 @@ fun NavGraphBuilder.screens(navController: NavHostController) { ) } - composable { - NewsListScreen( - onNewsClick = { newsId -> navController.navigate(NewsDetailScreen(newsId)) }, - onBack = navController::navigateUp, - ) - } - composable { - val newsId = it.toRoute().newsId - NewsDetailScreen(newsId, onBack = navController::navigateUp) - } - composable { DeveloperMenuScreen(onBack = navController::navigateUp) } diff --git a/shared/src/commonMain/kotlin/org/jetbrains/kotlinconf/navigation/Routes.kt b/shared/src/commonMain/kotlin/org/jetbrains/kotlinconf/navigation/Routes.kt index 8658ab672..8ff003811 100644 --- a/shared/src/commonMain/kotlin/org/jetbrains/kotlinconf/navigation/Routes.kt +++ b/shared/src/commonMain/kotlin/org/jetbrains/kotlinconf/navigation/Routes.kt @@ -108,13 +108,6 @@ data object MapScreen @SerialName("MapDetail") data class NestedMapScreen(val roomName: String) -@Serializable -@SerialName("News") -data object NewsListScreen - -@Serializable -@SerialName("News") -data class NewsDetailScreen(val newsId: String) @Serializable @SerialName("DeveloperMenu") diff --git a/shared/src/commonMain/kotlin/org/jetbrains/kotlinconf/screens/InfoScreen.kt b/shared/src/commonMain/kotlin/org/jetbrains/kotlinconf/screens/InfoScreen.kt index e333a6110..c42733118 100644 --- a/shared/src/commonMain/kotlin/org/jetbrains/kotlinconf/screens/InfoScreen.kt +++ b/shared/src/commonMain/kotlin/org/jetbrains/kotlinconf/screens/InfoScreen.kt @@ -28,7 +28,6 @@ import kotlinconfapp.shared.generated.resources.info_link_code_of_conduct import kotlinconfapp.shared.generated.resources.info_link_description_bluesky import kotlinconfapp.shared.generated.resources.info_link_description_slack import kotlinconfapp.shared.generated.resources.info_link_description_twitter -import kotlinconfapp.shared.generated.resources.info_link_news_feed import kotlinconfapp.shared.generated.resources.info_link_partners import kotlinconfapp.shared.generated.resources.info_title import kotlinconfapp.shared.generated.resources.kotlinconf_by_jetbrains @@ -47,7 +46,6 @@ import org.jetbrains.kotlinconf.ui.theme.KotlinConfTheme fun InfoScreen( onAboutConf: () -> Unit, onAboutApp: () -> Unit, - onNewsFeed: () -> Unit, onOurPartners: () -> Unit, onCodeOfConduct: () -> Unit, onTwitter: () -> Unit, @@ -77,7 +75,6 @@ fun InfoScreen( PageMenuItem(stringResource(Res.string.info_link_about_conf), onClick = onAboutConf) PageMenuItem(stringResource(Res.string.info_link_about_app), onClick = onAboutApp) PageMenuItem(stringResource(Res.string.info_link_settings), onClick = onSettings) - PageMenuItem(stringResource(Res.string.info_link_news_feed), onClick = onNewsFeed) PageMenuItem(stringResource(Res.string.info_link_partners), onClick = onOurPartners) PageMenuItem(stringResource(Res.string.info_link_code_of_conduct), onClick = onCodeOfConduct) Row( diff --git a/shared/src/commonMain/kotlin/org/jetbrains/kotlinconf/screens/MainScreen.kt b/shared/src/commonMain/kotlin/org/jetbrains/kotlinconf/screens/MainScreen.kt index 845d5d865..1d2a9e96d 100644 --- a/shared/src/commonMain/kotlin/org/jetbrains/kotlinconf/screens/MainScreen.kt +++ b/shared/src/commonMain/kotlin/org/jetbrains/kotlinconf/screens/MainScreen.kt @@ -53,7 +53,6 @@ import org.jetbrains.kotlinconf.navigation.AboutConferenceScreen import org.jetbrains.kotlinconf.navigation.CodeOfConductScreen import org.jetbrains.kotlinconf.navigation.InfoScreen import org.jetbrains.kotlinconf.navigation.MapScreen -import org.jetbrains.kotlinconf.navigation.NewsListScreen import org.jetbrains.kotlinconf.navigation.PartnersScreen import org.jetbrains.kotlinconf.navigation.AppPrivacyNoticePrompt import org.jetbrains.kotlinconf.navigation.ScheduleScreen @@ -100,7 +99,6 @@ fun MainScreen( InfoScreen( onAboutConf = { rootNavController.navigate(AboutConferenceScreen) }, onAboutApp = { rootNavController.navigate(AboutAppScreen) }, - onNewsFeed = { rootNavController.navigate(NewsListScreen) }, onOurPartners = { rootNavController.navigate(PartnersScreen) }, onCodeOfConduct = { rootNavController.navigate(CodeOfConductScreen) }, onTwitter = { uriHandler.openUri(URLs.TWITTER) }, diff --git a/shared/src/commonMain/kotlin/org/jetbrains/kotlinconf/screens/NewsDetailScreen.kt b/shared/src/commonMain/kotlin/org/jetbrains/kotlinconf/screens/NewsDetailScreen.kt deleted file mode 100644 index 9a675dfac..000000000 --- a/shared/src/commonMain/kotlin/org/jetbrains/kotlinconf/screens/NewsDetailScreen.kt +++ /dev/null @@ -1,86 +0,0 @@ -package org.jetbrains.kotlinconf.screens - -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.verticalScroll -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp -import androidx.lifecycle.compose.collectAsStateWithLifecycle -import kotlinconfapp.shared.generated.resources.Res -import kotlinconfapp.shared.generated.resources.arrow_left_24 -import kotlinconfapp.shared.generated.resources.navigate_back -import kotlinconfapp.shared.generated.resources.news_feed_title -import org.jetbrains.compose.resources.stringResource -import org.jetbrains.kotlinconf.ui.components.Divider -import org.jetbrains.kotlinconf.ui.components.MainHeaderTitleBar -import org.jetbrains.kotlinconf.ui.components.MarkdownView -import org.jetbrains.kotlinconf.ui.components.NetworkImage -import org.jetbrains.kotlinconf.ui.components.Text -import org.jetbrains.kotlinconf.ui.components.TopMenuButton -import org.jetbrains.kotlinconf.ui.theme.KotlinConfTheme -import org.jetbrains.kotlinconf.utils.topInsetPadding -import org.koin.compose.viewmodel.koinViewModel -import org.koin.core.parameter.parametersOf - -@Composable -fun NewsDetailScreen( - newsId: String, - onBack: () -> Unit, - viewModel: NewsDetailViewModel = koinViewModel { parametersOf(newsId) } -) { - val state = viewModel.newsItem.collectAsStateWithLifecycle().value - - Column( - Modifier - .fillMaxSize() - .background(color = KotlinConfTheme.colors.mainBackground) - .padding(topInsetPadding()) - ) { - MainHeaderTitleBar( - title = state?.date ?: stringResource(Res.string.news_feed_title), - startContent = { - TopMenuButton( - icon = Res.drawable.arrow_left_24, - contentDescription = stringResource(Res.string.navigate_back), - onClick = onBack, - ) - } - ) - - Divider(thickness = 1.dp, color = KotlinConfTheme.colors.strokePale) - - Column( - Modifier - .fillMaxSize() - .background(color = KotlinConfTheme.colors.mainBackground) - .verticalScroll(rememberScrollState()) - ) { - if (state != null) { - state.photoUrl?.let { url -> - NetworkImage( - photoUrl = url, - contentDescription = null, - modifier = Modifier.fillMaxWidth(), - ) - } - - Column( - modifier = Modifier - .fillMaxWidth() - .padding(vertical = 16.dp, horizontal = 12.dp) - ) { - Text(text = state.title, style = KotlinConfTheme.typography.h2) - Spacer(Modifier.height(12.dp)) - MarkdownView(text = state.content) - } - } - } - } -} diff --git a/shared/src/commonMain/kotlin/org/jetbrains/kotlinconf/screens/NewsDetailViewModel.kt b/shared/src/commonMain/kotlin/org/jetbrains/kotlinconf/screens/NewsDetailViewModel.kt deleted file mode 100644 index 6d4f39cd0..000000000 --- a/shared/src/commonMain/kotlin/org/jetbrains/kotlinconf/screens/NewsDetailViewModel.kt +++ /dev/null @@ -1,17 +0,0 @@ -package org.jetbrains.kotlinconf.screens - -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import kotlinx.coroutines.flow.SharingStarted -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.stateIn -import org.jetbrains.kotlinconf.ConferenceService -import org.jetbrains.kotlinconf.NewsDisplayItem - -class NewsDetailViewModel( - service: ConferenceService, - newsId: String, -) : ViewModel() { - val newsItem: StateFlow = service.newsById(newsId) - .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), null) -} diff --git a/shared/src/commonMain/kotlin/org/jetbrains/kotlinconf/screens/NewsListScreen.kt b/shared/src/commonMain/kotlin/org/jetbrains/kotlinconf/screens/NewsListScreen.kt deleted file mode 100644 index 8c06f7442..000000000 --- a/shared/src/commonMain/kotlin/org/jetbrains/kotlinconf/screens/NewsListScreen.kt +++ /dev/null @@ -1,64 +0,0 @@ -package org.jetbrains.kotlinconf.screens - -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items -import androidx.compose.foundation.lazy.rememberLazyListState -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp -import androidx.lifecycle.compose.collectAsStateWithLifecycle -import kotlinconfapp.shared.generated.resources.Res -import kotlinconfapp.shared.generated.resources.news_feed_empty -import kotlinconfapp.shared.generated.resources.news_feed_title -import org.jetbrains.compose.resources.stringResource -import org.jetbrains.kotlinconf.ScreenWithTitle -import org.jetbrains.kotlinconf.ScrollToTopHandler -import org.jetbrains.kotlinconf.ui.components.MajorError -import org.jetbrains.kotlinconf.ui.components.NewsCard -import org.jetbrains.kotlinconf.utils.bottomInsetPadding -import org.jetbrains.kotlinconf.utils.plus -import org.koin.compose.viewmodel.koinViewModel - -@Composable -fun NewsListScreen( - onNewsClick: (String) -> Unit, - onBack: () -> Unit, - viewModel: NewsListViewModel = koinViewModel(), -) { - ScreenWithTitle( - title = stringResource(Res.string.news_feed_title), - onBack = onBack, - ) { - val news by viewModel.news.collectAsStateWithLifecycle() - - if (news.isNotEmpty()) { - val listState = rememberLazyListState() - ScrollToTopHandler(listState) - LazyColumn( - modifier = Modifier.weight(1f), - contentPadding = PaddingValues(vertical = 16.dp) + bottomInsetPadding(), - verticalArrangement = Arrangement.spacedBy(16.dp), - state = listState, - ) { - items(news) { newsItem -> - NewsCard( - title = newsItem.title, - date = newsItem.date, - photoUrl = newsItem.photoUrl, - onClick = { onNewsClick(newsItem.id) } - ) - } - } - } else { - Box(Modifier.fillMaxSize().weight(1f), contentAlignment = Alignment.Center) { - MajorError(message = stringResource(Res.string.news_feed_empty)) - } - } - } -} diff --git a/shared/src/commonMain/kotlin/org/jetbrains/kotlinconf/screens/NewsListViewModel.kt b/shared/src/commonMain/kotlin/org/jetbrains/kotlinconf/screens/NewsListViewModel.kt deleted file mode 100644 index a5872fd0e..000000000 --- a/shared/src/commonMain/kotlin/org/jetbrains/kotlinconf/screens/NewsListViewModel.kt +++ /dev/null @@ -1,16 +0,0 @@ -package org.jetbrains.kotlinconf.screens - -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import kotlinx.coroutines.flow.SharingStarted -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.stateIn -import org.jetbrains.kotlinconf.ConferenceService -import org.jetbrains.kotlinconf.NewsDisplayItem - -class NewsListViewModel( - service: ConferenceService -) : ViewModel() { - val news: StateFlow> = service.news - .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), emptyList()) -} diff --git a/shared/src/commonMain/kotlin/org/jetbrains/kotlinconf/screens/NotificationSettings.kt b/shared/src/commonMain/kotlin/org/jetbrains/kotlinconf/screens/NotificationSettings.kt index 1b4d08b9a..44464d033 100644 --- a/shared/src/commonMain/kotlin/org/jetbrains/kotlinconf/screens/NotificationSettings.kt +++ b/shared/src/commonMain/kotlin/org/jetbrains/kotlinconf/screens/NotificationSettings.kt @@ -5,10 +5,6 @@ import androidx.compose.foundation.layout.Column import androidx.compose.runtime.Composable import androidx.compose.ui.unit.dp import kotlinconfapp.shared.generated.resources.Res -import kotlinconfapp.shared.generated.resources.notifications_jetbrains_news_description -import kotlinconfapp.shared.generated.resources.notifications_jetbrains_news_title -import kotlinconfapp.shared.generated.resources.notifications_kotlinconf_news_description -import kotlinconfapp.shared.generated.resources.notifications_kotlinconf_news_title import kotlinconfapp.shared.generated.resources.notifications_schedule_update_description import kotlinconfapp.shared.generated.resources.notifications_schedule_update_title import kotlinconfapp.shared.generated.resources.notifications_session_reminders_description @@ -38,17 +34,5 @@ fun NotificationSettings( onToggle = { enabled -> onChangeSettings(notificationSettings.copy(scheduleUpdates = enabled)) }, note = stringResource(Res.string.notifications_schedule_update_description), ) - SettingsItem( - title = stringResource(Res.string.notifications_kotlinconf_news_title), - enabled = notificationSettings.kotlinConfNews, - onToggle = { enabled -> onChangeSettings(notificationSettings.copy(kotlinConfNews = enabled)) }, - note = stringResource(Res.string.notifications_kotlinconf_news_description), - ) - SettingsItem( - title = stringResource(Res.string.notifications_jetbrains_news_title), - enabled = notificationSettings.jetBrainsNews, - onToggle = { enabled -> onChangeSettings(notificationSettings.copy(jetBrainsNews = enabled)) }, - note = stringResource(Res.string.notifications_jetbrains_news_description), - ) } } diff --git a/shared/src/commonMain/kotlin/org/jetbrains/kotlinconf/storage/ApplicationStorage.kt b/shared/src/commonMain/kotlin/org/jetbrains/kotlinconf/storage/ApplicationStorage.kt index 576e676b0..fab9dd778 100644 --- a/shared/src/commonMain/kotlin/org/jetbrains/kotlinconf/storage/ApplicationStorage.kt +++ b/shared/src/commonMain/kotlin/org/jetbrains/kotlinconf/storage/ApplicationStorage.kt @@ -3,7 +3,6 @@ package org.jetbrains.kotlinconf.storage import kotlinx.coroutines.flow.Flow import org.jetbrains.kotlinconf.Conference import org.jetbrains.kotlinconf.Flags -import org.jetbrains.kotlinconf.NewsItem import org.jetbrains.kotlinconf.NotificationSettings import org.jetbrains.kotlinconf.SessionId import org.jetbrains.kotlinconf.Theme @@ -28,9 +27,6 @@ interface ApplicationStorage { fun getFavorites(): Flow> suspend fun setFavorites(value: Set) - fun getNews(): Flow> - suspend fun setNews(value: List) - fun getNotificationSettings(): Flow suspend fun setNotificationSettings(value: NotificationSettings) diff --git a/shared/src/commonMain/kotlin/org/jetbrains/kotlinconf/storage/MultiplatformSettingsStorage.kt b/shared/src/commonMain/kotlin/org/jetbrains/kotlinconf/storage/MultiplatformSettingsStorage.kt index fbffdd009..567352dad 100644 --- a/shared/src/commonMain/kotlin/org/jetbrains/kotlinconf/storage/MultiplatformSettingsStorage.kt +++ b/shared/src/commonMain/kotlin/org/jetbrains/kotlinconf/storage/MultiplatformSettingsStorage.kt @@ -11,20 +11,25 @@ import kotlinx.serialization.SerializationException import kotlinx.serialization.json.Json import org.jetbrains.kotlinconf.Conference import org.jetbrains.kotlinconf.Flags -import org.jetbrains.kotlinconf.NewsItem import org.jetbrains.kotlinconf.NotificationSettings import org.jetbrains.kotlinconf.SessionId import org.jetbrains.kotlinconf.Theme import org.jetbrains.kotlinconf.VoteInfo +import org.jetbrains.kotlinconf.utils.Logger +import org.jetbrains.kotlinconf.utils.TaggedLogger +import org.jetbrains.kotlinconf.utils.tagged @OptIn(ExperimentalSettingsApi::class) class MultiplatformSettingsStorage( private val settings: ObservableSettings, + logger: Logger? = null, // TODO inject a logger here https://github.com/JetBrains/kotlinconf-app/issues/544 ) : ApplicationStorage { private val json = Json { ignoreUnknownKeys = true } + private var taggedLogger: TaggedLogger? = logger?.tagged("MultiplatformSettingsStorage") + private inline fun String?.decodeOrNull(): T? { if (this == null) return null @@ -62,12 +67,6 @@ class MultiplatformSettingsStorage( override suspend fun setFavorites(value: Set) = settings .set(Keys.FAVORITES, json.encodeToString(value)) - override fun getNews(): Flow> = settings.getStringOrNullFlow(Keys.NEWS_CACHE) - .map { it.decodeOrNull>() ?: emptyList() } - - override suspend fun setNews(value: List) = settings - .set(Keys.NEWS_CACHE, json.encodeToString(value)) - override fun getNotificationSettings(): Flow = settings.getStringOrNullFlow(Keys.NOTIFICATION_SETTINGS) .map { it.decodeOrNull() } @@ -91,16 +90,62 @@ class MultiplatformSettingsStorage( .set(Keys.FLAGS, json.encodeToString(value)) override fun ensureCurrentVersion() { - val version = settings.getInt(Keys.STORAGE_VERSION, 0) - if (version < CURRENT_STORAGE_VERSION) { - // Fully destructive migration on version mismatch - settings.clear() - settings.set(Keys.STORAGE_VERSION, CURRENT_STORAGE_VERSION) + var version = settings.getInt(Keys.STORAGE_VERSION, 0) + + taggedLogger?.log { "Storage version is $version" } + + if (version == 0) { + // Fully destructive migration on unknown previous version + destructiveUpgrade() + return + } + + while (version < CURRENT_STORAGE_VERSION) { + taggedLogger?.log { "Finding migrations from $version to $CURRENT_STORAGE_VERSION..." } + + // Find a migration from the current version that takes us as far forward as possible + val nextMigration = migrations.filter { it.from == version }.maxByOrNull { it.to } + if (nextMigration == null) { + taggedLogger?.log { "No matching migrations found" } + + // Failed to find a migration path to latest, fall back to destructive + destructiveUpgrade() + return + } + + taggedLogger?.log { "Running migration from ${nextMigration.from} to ${nextMigration.to}" } + + nextMigration.migrate() + version = nextMigration.to + settings.set(Keys.STORAGE_VERSION, version) + + taggedLogger?.log { "Successfully migrated to $version" } } } + private fun destructiveUpgrade() { + taggedLogger?.log { "Performing destructive upgrade to $CURRENT_STORAGE_VERSION" } + settings.clear() + settings.set(Keys.STORAGE_VERSION, CURRENT_STORAGE_VERSION) + } + + private data class Migration(val from: Int, val to: Int, val migrate: () -> Unit) + + private val migrations = listOf( + Migration(V2025, V2026) { + // News were removed + settings.remove(Keys.NEWS_CACHE) + // We removed news fields from NotificationSettings, read/write it once to update + settings.getStringOrNull(Keys.NOTIFICATION_SETTINGS) + ?.decodeOrNull() + ?.let { settings.set(Keys.NOTIFICATION_SETTINGS, json.encodeToString(it)) } + }, + ) + private companion object { - const val CURRENT_STORAGE_VERSION: Int = 2025_000 + const val V2025 = 2025_000 + const val V2026 = 2026_000 + const val CURRENT_STORAGE_VERSION: Int = V2026 } private object Keys { diff --git a/ui-components/src/commonMain/kotlin/org/jetbrains/kotlinconf/ui/components/NewsCard.kt b/ui-components/src/commonMain/kotlin/org/jetbrains/kotlinconf/ui/components/NewsCard.kt deleted file mode 100644 index 6156dd837..000000000 --- a/ui-components/src/commonMain/kotlin/org/jetbrains/kotlinconf/ui/components/NewsCard.kt +++ /dev/null @@ -1,60 +0,0 @@ -package org.jetbrains.kotlinconf.ui.components - -import androidx.compose.foundation.background -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.heightIn -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.unit.dp -import coil3.compose.AsyncImage -import org.jetbrains.kotlinconf.ui.theme.KotlinConfTheme - -@Composable -fun NewsCard( - title: String, - date: String, - photoUrl: String?, - onClick: () -> Unit, - modifier: Modifier = Modifier -) { - Column( - modifier = modifier - .fillMaxWidth() - .clip(RoundedCornerShape(8.dp)) - .background(color = KotlinConfTheme.colors.tileBackground) - .clickable(onClick = onClick) - ) { - if (photoUrl != null) { - AsyncImage( - model = photoUrl, - contentDescription = null, - contentScale = ContentScale.FillWidth, - modifier = Modifier - .fillMaxWidth() - .heightIn(max = 140.dp) - .background(KotlinConfTheme.colors.purpleText), - ) - } - Column(Modifier.fillMaxWidth().padding(16.dp)) { - Text( - text = date, - style = KotlinConfTheme.typography.text2, - color = KotlinConfTheme.colors.secondaryText, - ) - Spacer(modifier = Modifier.size(4.dp)) - Text( - text = title, - style = KotlinConfTheme.typography.h3, - color = KotlinConfTheme.colors.primaryText, - ) - } - } -}