Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
08fa4d4
- adds PersonalizationScreen.kt
Williamrai Mar 27, 2026
327446e
- adds raw state for PersonalizationScreen to derive interest ui states
Williamrai Mar 27, 2026
4d3ceba
- adds click event on category and adds comments
Williamrai Mar 27, 2026
7415c8a
Merge branch 'explore-feed-upgrade-design' into ef-interest
Williamrai Mar 27, 2026
6c875b1
Merge remote-tracking branch 'origin/explore-feed-upgrade-design' int…
Williamrai Mar 27, 2026
cc4df5a
- adds personalization screen as activity
Williamrai Mar 27, 2026
2f8cd85
- adds navigation
Williamrai Mar 27, 2026
a26073a
- adds more UI for interest selection and business logic
Williamrai Mar 30, 2026
96884e0
- adds shimmer effect
Williamrai Mar 31, 2026
77b8446
Merge remote-tracking branch 'origin/explore-feed-upgrade-design' int…
Williamrai Mar 31, 2026
f997427
Merge remote-tracking branch 'origin/explore-feed-upgrade-design' int…
Williamrai Apr 2, 2026
8a385a2
Merge remote-tracking branch 'origin/explore-feed-upgrade-design' int…
Williamrai Apr 2, 2026
a5d0f5a
- adds onboarding topics data and message api call
Williamrai Apr 3, 2026
2ad35ce
- adds bottom selection bar
Williamrai Apr 3, 2026
6727166
- dose not replace selected articles
Williamrai Apr 3, 2026
c18c051
- adds proper api call to getArticlesByTopic
Williamrai Apr 6, 2026
bde9b70
Merge remote-tracking branch 'origin/explore-feed-upgrade-design' int…
Williamrai Apr 6, 2026
06fc061
- updates interest screen ui to be fully scrollable
Williamrai Apr 6, 2026
4927654
- adds interest entity, dao, and viewmodel logic
Williamrai Apr 7, 2026
910a4e1
- converts selectedTopics to list of onboarding for readability and a…
Williamrai Apr 7, 2026
24acbc6
- adds preview
Williamrai Apr 8, 2026
8aa6030
- adds namespace to Interest entity
Williamrai Apr 8, 2026
7d3cfea
- rename and moves re-usable interest selection components to compose…
Williamrai Apr 8, 2026
8ebcdff
Merge branch 'explore-feed-upgrade-design' into ef-interest
cooltey Apr 8, 2026
eed54e7
fix lint
cooltey Apr 8, 2026
4295b99
- code fixes
Williamrai Apr 9, 2026
ede9223
- renames articleTopics to queryTopicId
Williamrai Apr 9, 2026
6683a9b
- splits Interest.kt into ArticleInterest.kt and TopicInterest.kt
Williamrai Apr 9, 2026
3d0a190
- maps one to many relationship between article and topic interest
Williamrai Apr 10, 2026
31086ba
- adds code to restore persisted interest for returning user
Williamrai Apr 10, 2026
3db7658
- adds showError
Williamrai Apr 10, 2026
2c13fbe
- code fixes
Williamrai Apr 13, 2026
b067d2c
- adds new db structure json file
Williamrai Apr 13, 2026
29f4fe1
- fix migration
Williamrai Apr 13, 2026
0f67fe6
- fix db unit test
Williamrai Apr 13, 2026
3295164
Merge branch 'explore-feed-upgrade-design' into ef-interest
cooltey Apr 14, 2026
e82a175
- code/ui fixes
Williamrai Apr 14, 2026
2f008fd
- adds feed preference state, structure and base UI
Williamrai Apr 14, 2026
8291ecc
- feed screen architecture refinement
Williamrai Apr 15, 2026
60d8d1c
- moves personalization to feed package, separate interest into its o…
Williamrai Apr 15, 2026
ea72b27
Merge branch 'ef-interest' into ef-onboarding-preference
Williamrai Apr 15, 2026
eb96ce7
- moves feedPreference to its own package
Williamrai Apr 15, 2026
f52b605
- rename repository to interestSelectionRepository
Williamrai Apr 15, 2026
1776161
Merge branch 'ef-interest' into ef-onboarding-preference
Williamrai Apr 15, 2026
fa0138f
Merge remote-tracking branch 'origin/explore-feed-upgrade-design' int…
Williamrai Apr 16, 2026
e266cac
- adds DTO class ArticleWithTopic to query results from both entities…
Williamrai Apr 16, 2026
9e563e3
- lint fixes
Williamrai Apr 16, 2026
c2f611b
- adds empty state for personalized content
Williamrai Apr 17, 2026
854c78a
- adds API call for getting preview for getInterests
Williamrai Apr 17, 2026
4660ede
- api call fixes
Williamrai Apr 17, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package org.wikipedia.compose.components

import androidx.compose.foundation.text.TextAutoSize
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
Expand Down Expand Up @@ -44,7 +45,8 @@ fun HtmlText(
overflow: TextOverflow = TextOverflow.Ellipsis,
lineHeight: TextUnit = 1.6.em,
linkInteractionListener: LinkInteractionListener = defaultLinkInteractionListener(),
textAlign: TextAlign = TextAlign.Start
textAlign: TextAlign = TextAlign.Start,
autoSize: TextAutoSize? = null
) {
Text(
modifier = modifier,
Expand All @@ -58,7 +60,8 @@ fun HtmlText(
color = color,
maxLines = maxLines,
overflow = overflow,
textAlign = textAlign
textAlign = textAlign,
autoSize = autoSize
)
}

Expand Down
12 changes: 10 additions & 2 deletions app/src/main/java/org/wikipedia/feed/HomeViewModel.kt
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@ import org.wikipedia.dataclient.page.PageSummary
import org.wikipedia.feed.image.FeaturedImage
import org.wikipedia.feed.news.NewsItem
import org.wikipedia.feed.onthisday.OnThisDay
import org.wikipedia.feed.personalization.feedpreference.FeedPreferenceType
import org.wikipedia.feed.topread.TopRead
import org.wikipedia.settings.Prefs
import java.time.LocalDate

enum class HomeTab { COMMUNITY, FOR_YOU }
Expand Down Expand Up @@ -56,7 +58,9 @@ class HomeViewModel : ViewModel() {

val wikiSite get() = WikipediaApp.instance.wikiSite

private val _selectedTab = MutableStateFlow(HomeTab.COMMUNITY)
private val _selectedTab = MutableStateFlow(
if (Prefs.exploreFeedPreferenceSelection == FeedPreferenceType.PERSONALIZED) HomeTab.FOR_YOU else HomeTab.COMMUNITY
)
val selectedTab = _selectedTab.asStateFlow()

private val _communityState = MutableStateFlow(CommunityContentState())
Expand Down Expand Up @@ -88,7 +92,11 @@ class HomeViewModel : ViewModel() {
}

init {
loadCommunityContent()
if (_selectedTab.value == HomeTab.COMMUNITY) {
loadCommunityContent()
} else {
loadForYouContent()
}
}

fun refreshCommunityContent() {
Expand Down
2 changes: 1 addition & 1 deletion app/src/main/java/org/wikipedia/feed/news/NewsItem.kt
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import org.wikipedia.util.ImageUrlUtil
@Serializable
class NewsItem(
val story: String = "",
val links: List<PageSummary> = emptyList(),
val links: List<PageSummary> = emptyList()
) : Parcelable {

fun linkCards(wiki: WikiSite): List<NewsLinkCard> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import kotlinx.coroutines.launch
import org.wikipedia.R
import org.wikipedia.compose.components.PageIndicator
import org.wikipedia.compose.theme.WikipediaTheme
import org.wikipedia.feed.personalization.feedpreference.FeedPreferenceScreen
import org.wikipedia.feed.personalization.interest.InterestOnboardingScreen

// TODO: probably renaming the screen name
Expand All @@ -46,6 +47,7 @@ fun PersonalizationScreen(
) {
val coroutineScope = rememberCoroutineScope()
val interestUiState = viewModel.interestUiState.collectAsState()
val feedPreferenceUiState = viewModel.feedPreferenceUiState.collectAsState()
val pagerState = rememberPagerState(pageCount = { screens.size })

LaunchedEffect(pagerState.currentPage) {
Expand Down Expand Up @@ -101,13 +103,23 @@ fun PersonalizationScreen(
viewModel.deselectAllArticles()
},
retryLoading = {
viewModel.retryLoading()
viewModel.retryInterestsLoading()
},
showError = showError
)
}
PersonalizationPage.FEED_PREFERENCE -> {
// TODO: implement feed preference screen
FeedPreferenceScreen(
modifier = Modifier
.fillMaxSize()
.background(WikipediaTheme.colors.paperColor)
.padding(top = 40.dp),
selectedType = feedPreferenceUiState.value.selectedType,
communityContentState = feedPreferenceUiState.value.communityState,
personalizedContentState = feedPreferenceUiState.value.personalizedState,
onTypeSelected = { viewModel.onFeedPreferenceTypeSelected(it) },
onRetryClick = { viewModel.retryFeedPreferenceLoading(it) }
)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@ import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import org.wikipedia.WikipediaApp
import org.wikipedia.database.AppDatabase
import org.wikipedia.feed.personalization.feedpreference.FeedContentState
import org.wikipedia.feed.personalization.feedpreference.FeedPreferenceContent
import org.wikipedia.feed.personalization.feedpreference.FeedPreferenceRepository
import org.wikipedia.feed.personalization.feedpreference.FeedPreferenceType
import org.wikipedia.feed.personalization.feedpreference.FeedPreferenceUiState
import org.wikipedia.feed.personalization.interest.ArticlesState
import org.wikipedia.feed.personalization.interest.InterestSelectionRepository
import org.wikipedia.feed.personalization.interest.InterestUiState
Expand All @@ -36,8 +41,15 @@ private data class PersonalizedViewModelState(
val articlesLoading: Boolean = false,
val articlesError: Throwable? = null,
val selectedArticles: Set<PageTitle> = emptySet(),
val selectedTopics: List<OnboardingTopic> = emptyList()
val selectedTopics: List<OnboardingTopic> = emptyList(),
// Feed preference screen properties
val feedPreferenceType: FeedPreferenceType = FeedPreferenceType.COMMUNITY,
val communityContent: List<FeedPreferenceContent> = emptyList(),
val communityLoading: Boolean = false,
val communityError: Throwable? = null,
val personalizedContent: List<FeedPreferenceContent> = emptyList(),
val personalizedLoading: Boolean = false,
val personalizedError: Throwable? = null
) {
fun toInterestUiState(): InterestUiState {
return InterestUiState(
Expand All @@ -64,12 +76,27 @@ private data class PersonalizedViewModelState(
)
}

// Each screen in the personalization flow would have its own function
// fun toFeedPreferenceUiState(): FeedPreferenceUiState { ... }
fun toFeedPreferenceUiState(): FeedPreferenceUiState {
return FeedPreferenceUiState(
selectedType = feedPreferenceType,
communityState = when {
communityLoading -> FeedContentState.Loading
communityError != null -> FeedContentState.Error(communityError)
else -> FeedContentState.Success(communityContent)
},
personalizedState = when {
personalizedLoading -> FeedContentState.Loading
personalizedError != null -> FeedContentState.Error(personalizedError)
personalizedContent.isEmpty() -> FeedContentState.Empty
else -> FeedContentState.Success(personalizedContent)
}
)
}
}

class PersonalizationViewModel(
private val interestSelectionRepository: InterestSelectionRepository
private val interestSelectionRepository: InterestSelectionRepository,
private val feedPreferenceRepository: FeedPreferenceRepository
) : ViewModel() {
// Single source of truth for all personalization state, can be easily extended to include feed preference and language selection states as well
private val state = MutableStateFlow(PersonalizedViewModelState())
Expand All @@ -85,9 +112,18 @@ class PersonalizationViewModel(
initialValue = state.value.toInterestUiState()
)

val feedPreferenceUiState = state
.map { it.toFeedPreferenceUiState() }
.stateIn(
scope = viewModelScope,
started = SharingStarted.Eagerly,
initialValue = state.value.toFeedPreferenceUiState()
)

fun onPageChanged(screen: PersonalizationPage) {
when (screen) {
PersonalizationPage.INTERESTS -> loadInterestSelectionScreen()
PersonalizationPage.FEED_PREFERENCE -> loadFeedPreferenceScreen()
else -> {}
}
}
Expand All @@ -101,6 +137,38 @@ class PersonalizationViewModel(
}
}

private fun loadFeedPreferenceScreen() {
if (state.value.communityContent.isEmpty()) {
loadCommunityPreviewContent()
}
loadPersonalizedPreviewContent()
}

private fun loadCommunityPreviewContent() {
viewModelScope.launch(CoroutineExceptionHandler { _, throwable ->
state.update { it.copy(communityLoading = false, communityError = throwable) }
L.e(throwable)
}) {
state.update { it.copy(communityLoading = true, communityError = null) }
val communityContent = feedPreferenceRepository.getCommunityContent()
state.update { it.copy(communityContent = communityContent, communityLoading = false) }
}
}

private fun loadPersonalizedPreviewContent() {
viewModelScope.launch(CoroutineExceptionHandler { _, throwable ->
state.update { it.copy(personalizedLoading = false, personalizedError = throwable) }
L.e(throwable)
}) {
state.update { it.copy(personalizedLoading = true, personalizedError = null) }
val personalizedContent = feedPreferenceRepository.getInterests(
selectedTopics = state.value.selectedTopics,
selectedArticles = state.value.selectedArticles
)
state.update { it.copy(personalizedContent = personalizedContent, personalizedLoading = false) }
}
}

private suspend fun loadTopics() {
if (state.value.topics.isNotEmpty()) return

Expand Down Expand Up @@ -281,7 +349,7 @@ class PersonalizationViewModel(
}
}

fun retryLoading() {
fun retryInterestsLoading() {
val last = state.value.selectedTopics.lastOrNull()
if (last != null) {
loadArticlesByTopic(topic = last)
Expand All @@ -290,6 +358,18 @@ class PersonalizationViewModel(
}
}

fun onFeedPreferenceTypeSelected(type: FeedPreferenceType) {
feedPreferenceRepository.saveFeedPreferenceSelection(type)
state.update { it.copy(feedPreferenceType = type) }
}

fun retryFeedPreferenceLoading(type: FeedPreferenceType) {
when (type) {
FeedPreferenceType.COMMUNITY -> loadCommunityPreviewContent()
FeedPreferenceType.PERSONALIZED -> loadPersonalizedPreviewContent()
}
}

companion object {
val Factory = viewModelFactory {
initializer {
Expand All @@ -300,6 +380,10 @@ class PersonalizationViewModel(
historyEntryWithImageDao = AppDatabase.instance.historyEntryWithImageDao(),
readingListPageDao = AppDatabase.instance.readingListPageDao(),
wikiSite = WikipediaApp.instance.wikiSite
),
feedPreferenceRepository = FeedPreferenceRepository(
historyEntryWithImageDao = AppDatabase.instance.historyEntryWithImageDao(),
wikiSite = WikipediaApp.instance.wikiSite
)
)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
package org.wikipedia.feed.personalization.feedpreference

import org.wikipedia.dataclient.ServiceFactory
import org.wikipedia.dataclient.WikiSite
import org.wikipedia.feed.personalization.interest.OnboardingTopic
import org.wikipedia.history.db.HistoryEntryWithImageDao
import org.wikipedia.page.PageTitle
import org.wikipedia.settings.Prefs
import org.wikipedia.util.StringUtil
import java.time.LocalDate

class FeedPreferenceRepository(
private val historyEntryWithImageDao: HistoryEntryWithImageDao,
private val wikiSite: WikiSite
) {
suspend fun getCommunityContent(): List<FeedPreferenceContent> {
val currentDate = LocalDate.now()

val response = ServiceFactory.getRest(wikiSite).getFeedFeatured(
year = currentDate.year.toString(),
month = "%02d".format(currentDate.monthValue),
day = "%02d".format(currentDate.dayOfMonth),
lang = wikiSite.languageCode
)

val featuredArticle = response.tfa?.let { article ->
FeedPreferenceContent(
title = article.displayTitle,
description = article.description,
imageUrl = article.thumbnailUrl,
tag = "Featured Article" // TODO: use localized string resource
)
}

val pictureOfTheDay = response.potd?.let { potd ->
FeedPreferenceContent(
title = null,
description = potd.description.text,
imageUrl = potd.thumbnailUrl,
tag = "Picture of the Day" // TODO: use localized string resource
)
}

val inTheNewsArticle = response.news?.first().let { newsItem ->
FeedPreferenceContent(
title = null,
description = StringUtil.removeHTMLTags(newsItem?.story),
imageUrl = newsItem?.thumbUrl(),
tag = "In the News" // TODO: use localized string resource
)
}

return listOfNotNull(
featuredArticle,
pictureOfTheDay,
inTheNewsArticle
)
}

suspend fun getInterests(
selectedTopics: List<OnboardingTopic>,
selectedArticles: Set<PageTitle>
): List<FeedPreferenceContent> {
if (selectedTopics.isNotEmpty()) {
val contentSize = 3
val topicsToUse = selectedTopics.takeLast(contentSize)
val baseLimit = contentSize / topicsToUse.size
val remainder = contentSize % topicsToUse.size

val content = topicsToUse.flatMapIndexed { index, topic ->
val limit = if (index < remainder) baseLimit + 1 else baseLimit
val response = ServiceFactory.get(wikiSite).getArticlesByTopic(articleTopics = topic.queryTopicId, limit = limit)
response.query?.pages?.map { page ->
FeedPreferenceContent(
title = page.title,
description = page.description,
imageUrl = page.thumbUrl(),
tag = topic.displayTitle
)
} ?: emptyList()
}
return content
}

if (selectedArticles.isNotEmpty()) {
val moreLikeSearchTerm = "morelike:${selectedArticles.take(3).joinToString("|") { it.prefixedText }}"
val response = ServiceFactory.get(wikiSite).searchMoreLike(searchTerm = moreLikeSearchTerm, gsrLimit = 3, piLimit = 3)
val content = response.query?.pages?.map { page ->
FeedPreferenceContent(
title = page.title,
description = page.description,
imageUrl = page.thumbUrl(),
tag = null
)
} ?: emptyList()
return content
}

val readingHistory = historyEntryWithImageDao.getMostRecentEntriesWithImage(3)
if (readingHistory.size >= 3) {
val moreLikeSearchTerm = "morelike:${readingHistory.take(3).joinToString("|") { it.apiTitle }}"
val response = ServiceFactory.get(wikiSite).searchMoreLike(searchTerm = moreLikeSearchTerm, gsrLimit = 3, piLimit = 3)
val content = response.query?.pages?.map { page ->
FeedPreferenceContent(
title = page.title,
description = page.description,
imageUrl = page.thumbUrl(),
tag = null
)
} ?: emptyList()
return content
}

return listOf()
}

fun saveFeedPreferenceSelection(preferenceType: FeedPreferenceType) {
Prefs.exploreFeedPreferenceSelection = preferenceType
}
}
Loading
Loading