From 456948587500831a8a43a5677c1059bc9462a9a1 Mon Sep 17 00:00:00 2001 From: Dmitry Brant Date: Wed, 23 Jul 2025 14:50:57 -0400 Subject: [PATCH 01/70] Add preferences. --- .../java/org/wikipedia/activitytab/ActivityTabABTest.kt | 9 +++++++++ app/src/main/java/org/wikipedia/main/MainFragment.kt | 3 +++ app/src/main/java/org/wikipedia/settings/Prefs.kt | 4 ++++ app/src/main/res/values/preference_keys.xml | 1 + 4 files changed, 17 insertions(+) create mode 100644 app/src/main/java/org/wikipedia/activitytab/ActivityTabABTest.kt diff --git a/app/src/main/java/org/wikipedia/activitytab/ActivityTabABTest.kt b/app/src/main/java/org/wikipedia/activitytab/ActivityTabABTest.kt new file mode 100644 index 00000000000..2649e1d74ce --- /dev/null +++ b/app/src/main/java/org/wikipedia/activitytab/ActivityTabABTest.kt @@ -0,0 +1,9 @@ +package org.wikipedia.activitytab + +import org.wikipedia.analytics.ABTest + +class ActivityTabABTest : ABTest("activityTab", GROUP_SIZE_2) { + fun isInTestGroup(): Boolean { + return group == GROUP_2 + } +} diff --git a/app/src/main/java/org/wikipedia/main/MainFragment.kt b/app/src/main/java/org/wikipedia/main/MainFragment.kt index 8aaa9910009..d1da26690a2 100644 --- a/app/src/main/java/org/wikipedia/main/MainFragment.kt +++ b/app/src/main/java/org/wikipedia/main/MainFragment.kt @@ -37,6 +37,7 @@ import org.wikipedia.R import org.wikipedia.WikipediaApp import org.wikipedia.activity.BaseActivity import org.wikipedia.activity.FragmentUtil.getCallback +import org.wikipedia.activitytab.ActivityTabABTest import org.wikipedia.analytics.eventplatform.ReadingListsAnalyticsHelper import org.wikipedia.auth.AccountUtil import org.wikipedia.commons.FilePageActivity @@ -173,6 +174,8 @@ class MainFragment : Fragment(), BackPressedHandler, MenuProvider, FeedFragment. true } + binding.mainNavTabLayout.setOverlayDot(NavTab.EDITS, ActivityTabABTest().isInTestGroup() && !Prefs.activityTabRedDotShown) + notificationButtonView = NotificationButtonView(requireActivity()) maybeShowEditsTooltip() diff --git a/app/src/main/java/org/wikipedia/settings/Prefs.kt b/app/src/main/java/org/wikipedia/settings/Prefs.kt index d8d17369bf1..1cd4ebcc45f 100644 --- a/app/src/main/java/org/wikipedia/settings/Prefs.kt +++ b/app/src/main/java/org/wikipedia/settings/Prefs.kt @@ -828,4 +828,8 @@ object Prefs { var resetRecommendedReadingList get() = PrefsIoUtil.getBoolean(R.string.preference_key_recommended_reading_list_reset, false) set(value) = PrefsIoUtil.setBoolean(R.string.preference_key_recommended_reading_list_reset, value) + + var activityTabRedDotShown + get() = PrefsIoUtil.getBoolean(R.string.preference_key_activity_tab_red_dot_shown, false) + set(value) = PrefsIoUtil.setBoolean(R.string.preference_key_activity_tab_red_dot_shown, value) } diff --git a/app/src/main/res/values/preference_keys.xml b/app/src/main/res/values/preference_keys.xml index 908bbfc7731..3fc39a6d746 100644 --- a/app/src/main/res/values/preference_keys.xml +++ b/app/src/main/res/values/preference_keys.xml @@ -175,6 +175,7 @@ otdEntryDialogShown otdNotificationState otdGameFirstPlayedShown + activityTabRedDotShown placesDefaultLocationLatLng deleteLocalDonationHistory categoryPlayground From e14c60cd21943030273e2fecbf1aaf243378c6d4 Mon Sep 17 00:00:00 2001 From: Dmitry Brant Date: Fri, 25 Jul 2025 08:55:32 -0400 Subject: [PATCH 02/70] Basic scaffolding for activity fragment. --- .../activitytab/ActivityTabFragment.kt | 118 ++++++++++++++++++ .../activitytab/ActivityTabViewModel.kt | 35 ++++++ .../main/java/org/wikipedia/navtab/NavTab.kt | 8 +- .../res/drawable/selector_nav_activity.xml | 6 + app/src/main/res/values-qq/strings.xml | 1 + app/src/main/res/values/strings.xml | 1 + .../main/res/xml/developer_preferences.xml | 13 ++ 7 files changed, 180 insertions(+), 2 deletions(-) create mode 100644 app/src/main/java/org/wikipedia/activitytab/ActivityTabFragment.kt create mode 100644 app/src/main/java/org/wikipedia/activitytab/ActivityTabViewModel.kt create mode 100644 app/src/main/res/drawable/selector_nav_activity.xml diff --git a/app/src/main/java/org/wikipedia/activitytab/ActivityTabFragment.kt b/app/src/main/java/org/wikipedia/activitytab/ActivityTabFragment.kt new file mode 100644 index 00000000000..1ef43851725 --- /dev/null +++ b/app/src/main/java/org/wikipedia/activitytab/ActivityTabFragment.kt @@ -0,0 +1,118 @@ +package org.wikipedia.activitytab + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.LinearProgressIndicator +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.ComposeView +import androidx.fragment.app.Fragment +import androidx.fragment.app.viewModels +import org.wikipedia.compose.components.error.WikiErrorClickEvents +import org.wikipedia.compose.components.error.WikiErrorView +import org.wikipedia.compose.theme.BaseTheme +import org.wikipedia.compose.theme.WikipediaTheme +import org.wikipedia.settings.Prefs +import org.wikipedia.util.UiState + +class ActivityTabFragment : Fragment() { + + private val viewModel: ActivityTabViewModel by viewModels() + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { + super.onCreateView(inflater, container, savedInstanceState) + Prefs.activityTabRedDotShown = true + + return ComposeView(requireContext()).apply { + setContent { + BaseTheme { + ActivityTabScreen( + uiState = viewModel.uiState.collectAsState().value, + wikiErrorClickEvents = WikiErrorClickEvents( + retryClickListener = { + viewModel.load() + } + ) + ) + } + } + } + } + + @Composable + fun ActivityTabScreen( + uiState: UiState, + wikiErrorClickEvents: WikiErrorClickEvents? = null + ) { + Scaffold( + modifier = Modifier + .fillMaxSize() + .background(WikipediaTheme.colors.paperColor), + containerColor = WikipediaTheme.colors.paperColor + ) { paddingValues -> + when (uiState) { + is UiState.Loading -> { + Box( + modifier = Modifier + .fillMaxWidth() + .padding(paddingValues), + ) { + LinearProgressIndicator( + modifier = Modifier.fillMaxWidth(), + color = WikipediaTheme.colors.progressiveColor, + trackColor = WikipediaTheme.colors.borderColor + ) + } + } + is UiState.Error -> { + Box( + modifier = Modifier + .fillMaxSize() + .padding(paddingValues), + contentAlignment = Alignment.Center + ) { + WikiErrorView( + modifier = Modifier + .fillMaxWidth(), + caught = uiState.error, + errorClickEvents = wikiErrorClickEvents + ) + } + } + is UiState.Success -> { + Box( + modifier = Modifier + .fillMaxSize() + .padding(paddingValues) + ) { + Text( + text = "TODO!", + modifier = Modifier.align(Alignment.Center) + ) + } + } + } + } + } + + companion object { + fun newInstance(): ActivityTabFragment { + return ActivityTabFragment().apply { + arguments = Bundle().apply { + // TODO + } + } + } + } +} diff --git a/app/src/main/java/org/wikipedia/activitytab/ActivityTabViewModel.kt b/app/src/main/java/org/wikipedia/activitytab/ActivityTabViewModel.kt new file mode 100644 index 00000000000..e9e9c4de068 --- /dev/null +++ b/app/src/main/java/org/wikipedia/activitytab/ActivityTabViewModel.kt @@ -0,0 +1,35 @@ +package org.wikipedia.activitytab + +import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.CoroutineExceptionHandler +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch +import org.wikipedia.util.UiState + +class ActivityTabViewModel(savedStateHandle: SavedStateHandle) : ViewModel() { + + private val _uiState = MutableStateFlow>(UiState.Loading) + val uiState: StateFlow> = _uiState.asStateFlow() + + init { + load() + } + + fun load() { + viewModelScope.launch(CoroutineExceptionHandler { _, throwable -> + _uiState.value = UiState.Error(throwable) + }) { + _uiState.value = UiState.Loading + + // TODO + delay(2000) + + _uiState.value = UiState.Success(Unit) + } + } +} diff --git a/app/src/main/java/org/wikipedia/navtab/NavTab.kt b/app/src/main/java/org/wikipedia/navtab/NavTab.kt index 81a4875a89d..6f008bf4bd3 100644 --- a/app/src/main/java/org/wikipedia/navtab/NavTab.kt +++ b/app/src/main/java/org/wikipedia/navtab/NavTab.kt @@ -4,6 +4,8 @@ import androidx.annotation.DrawableRes import androidx.annotation.StringRes import androidx.fragment.app.Fragment import org.wikipedia.R +import org.wikipedia.activitytab.ActivityTabABTest +import org.wikipedia.activitytab.ActivityTabFragment import org.wikipedia.feed.FeedFragment import org.wikipedia.history.HistoryFragment import org.wikipedia.model.EnumCode @@ -32,10 +34,12 @@ enum class NavTab constructor( } }, EDITS( - R.string.nav_item_suggested_edits, R.id.nav_tab_edits, R.drawable.selector_nav_edits + if (ActivityTabABTest().isInTestGroup()) R.string.nav_item_activity else R.string.nav_item_suggested_edits, + R.id.nav_tab_edits, + if (ActivityTabABTest().isInTestGroup()) R.drawable.selector_nav_activity else R.drawable.selector_nav_edits ) { override fun newInstance(): Fragment { - return SuggestedEditsTasksFragment.newInstance() + return if (ActivityTabABTest().isInTestGroup()) ActivityTabFragment.newInstance() else SuggestedEditsTasksFragment.newInstance() } }, MORE(R.string.nav_item_more, R.id.nav_tab_more, R.drawable.ic_menu_white_24dp) { diff --git a/app/src/main/res/drawable/selector_nav_activity.xml b/app/src/main/res/drawable/selector_nav_activity.xml new file mode 100644 index 00000000000..7967d30f8f0 --- /dev/null +++ b/app/src/main/res/drawable/selector_nav_activity.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values-qq/strings.xml b/app/src/main/res/values-qq/strings.xml index b0b84eef4b7..c8fb4bc2144 100644 --- a/app/src/main/res/values-qq/strings.xml +++ b/app/src/main/res/values-qq/strings.xml @@ -60,6 +60,7 @@ Navigation menu item label for accessing menu items.\n{{identical|More}} Navigation menu item label for search.\n\n{{Identical|Search}} Navigation menu item label for contribute.\n\n{{Identical|Contribute}} + Navigation menu item label for the user\'s activity. Error message shown when network cannot be reached when loading a page Text for button that retries page loading when tapped.\n{{Identical|Retry}} Text for button that retries loading the offline card in the event of an error while loading.\n{{Identical|Retry}} diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 5c8d42d0b9d..e81f97fb9e1 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -17,6 +17,7 @@ Search Filter my lists Edits + Activity More Search Contribute diff --git a/app/src/main/res/xml/developer_preferences.xml b/app/src/main/res/xml/developer_preferences.xml index 40ce6a697b6..2a416d39f5e 100644 --- a/app/src/main/res/xml/developer_preferences.xml +++ b/app/src/main/res/xml/developer_preferences.xml @@ -530,4 +530,17 @@ android:title="@string/preference_key_yir_survey_shown" /> + + + + + + + + From 9c9720edc98b36df1cbe6fe3bf4496558b9a782d Mon Sep 17 00:00:00 2001 From: Dmitry Brant Date: Fri, 25 Jul 2025 08:58:03 -0400 Subject: [PATCH 03/70] Feature-flag for pre-beta only. --- .../main/java/org/wikipedia/activitytab/ActivityTabABTest.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/src/main/java/org/wikipedia/activitytab/ActivityTabABTest.kt b/app/src/main/java/org/wikipedia/activitytab/ActivityTabABTest.kt index 2649e1d74ce..c320483c2ad 100644 --- a/app/src/main/java/org/wikipedia/activitytab/ActivityTabABTest.kt +++ b/app/src/main/java/org/wikipedia/activitytab/ActivityTabABTest.kt @@ -1,9 +1,11 @@ package org.wikipedia.activitytab import org.wikipedia.analytics.ABTest +import org.wikipedia.util.ReleaseUtil class ActivityTabABTest : ABTest("activityTab", GROUP_SIZE_2) { fun isInTestGroup(): Boolean { return group == GROUP_2 + && ReleaseUtil.isPreBetaRelease // TODO: remove before releasing } } From 69330fc49f7e4864a4e4f342dacda7ab5de6d95b Mon Sep 17 00:00:00 2001 From: Dmitry Brant Date: Fri, 25 Jul 2025 09:07:14 -0400 Subject: [PATCH 04/70] Lint. --- .../main/java/org/wikipedia/activitytab/ActivityTabABTest.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/org/wikipedia/activitytab/ActivityTabABTest.kt b/app/src/main/java/org/wikipedia/activitytab/ActivityTabABTest.kt index c320483c2ad..73daa3b91e3 100644 --- a/app/src/main/java/org/wikipedia/activitytab/ActivityTabABTest.kt +++ b/app/src/main/java/org/wikipedia/activitytab/ActivityTabABTest.kt @@ -5,7 +5,7 @@ import org.wikipedia.util.ReleaseUtil class ActivityTabABTest : ABTest("activityTab", GROUP_SIZE_2) { fun isInTestGroup(): Boolean { - return group == GROUP_2 - && ReleaseUtil.isPreBetaRelease // TODO: remove before releasing + return group == GROUP_2 && + ReleaseUtil.isPreBetaRelease // TODO: remove before releasing } } From b63b7e638fc99dd8c478ce38877f6a35940e7e79 Mon Sep 17 00:00:00 2001 From: Dmitry Brant Date: Fri, 1 Aug 2025 14:39:53 -0400 Subject: [PATCH 05/70] Make it ready for building. --- .../activitytab/ActivityTabFragment.kt | 3 +- .../activitytab/ActivityTabViewModel.kt | 26 +++++++- .../wikipedia/categories/db/CategoryDao.kt | 3 + .../games/onthisday/OnThisDayGameViewModel.kt | 64 +++++++++---------- .../main/res/drawable/outline_activity_24.xml | 5 ++ .../res/drawable/selector_nav_activity.xml | 4 +- 6 files changed, 67 insertions(+), 38 deletions(-) create mode 100644 app/src/main/res/drawable/outline_activity_24.xml diff --git a/app/src/main/java/org/wikipedia/activitytab/ActivityTabFragment.kt b/app/src/main/java/org/wikipedia/activitytab/ActivityTabFragment.kt index 1ef43851725..0af2ef46c24 100644 --- a/app/src/main/java/org/wikipedia/activitytab/ActivityTabFragment.kt +++ b/app/src/main/java/org/wikipedia/activitytab/ActivityTabFragment.kt @@ -98,7 +98,8 @@ class ActivityTabFragment : Fragment() { ) { Text( text = "TODO!", - modifier = Modifier.align(Alignment.Center) + modifier = Modifier.align(Alignment.Center), + color = WikipediaTheme.colors.primaryColor ) } } diff --git a/app/src/main/java/org/wikipedia/activitytab/ActivityTabViewModel.kt b/app/src/main/java/org/wikipedia/activitytab/ActivityTabViewModel.kt index e9e9c4de068..9db4ab9e0b0 100644 --- a/app/src/main/java/org/wikipedia/activitytab/ActivityTabViewModel.kt +++ b/app/src/main/java/org/wikipedia/activitytab/ActivityTabViewModel.kt @@ -4,18 +4,29 @@ import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import kotlinx.coroutines.CoroutineExceptionHandler -import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch +import org.wikipedia.WikipediaApp +import org.wikipedia.categories.db.Category +import org.wikipedia.database.AppDatabase +import org.wikipedia.donate.DonationResult +import org.wikipedia.games.onthisday.OnThisDayGameViewModel +import org.wikipedia.settings.Prefs import org.wikipedia.util.UiState +import java.time.LocalDate class ActivityTabViewModel(savedStateHandle: SavedStateHandle) : ViewModel() { private val _uiState = MutableStateFlow>(UiState.Loading) val uiState: StateFlow> = _uiState.asStateFlow() + var gameStatistics: OnThisDayGameViewModel.GameStatistics? = null + var donationResults: List = emptyList() + + var topCategories: List = emptyList() + init { load() } @@ -26,8 +37,17 @@ class ActivityTabViewModel(savedStateHandle: SavedStateHandle) : ViewModel() { }) { _uiState.value = UiState.Loading - // TODO - delay(2000) + val currentDate = LocalDate.now() + val languageCode = WikipediaApp.instance.wikiSite.languageCode + + // TODO: do something with game statistics + gameStatistics = OnThisDayGameViewModel.getGameStatistics(languageCode) + + // TODO: do something with donation results + donationResults = Prefs.donationResults + + // TODO: do something with top categories + topCategories = AppDatabase.instance.categoryDao().getTopCategoriesByMonth(currentDate.year, currentDate.monthValue) _uiState.value = UiState.Success(Unit) } diff --git a/app/src/main/java/org/wikipedia/categories/db/CategoryDao.kt b/app/src/main/java/org/wikipedia/categories/db/CategoryDao.kt index 48920ec9a79..3e8c8a75b62 100644 --- a/app/src/main/java/org/wikipedia/categories/db/CategoryDao.kt +++ b/app/src/main/java/org/wikipedia/categories/db/CategoryDao.kt @@ -25,6 +25,9 @@ interface CategoryDao { @Query("SELECT year, month, title, lang, SUM (count) AS count FROM Category WHERE year BETWEEN :startYear AND :endYear GROUP BY title, lang ORDER BY count DESC") suspend fun getCategoriesByTimeRange(startYear: Int, endYear: Int): List + @Query("SELECT year, month, title, lang, SUM (count) AS count FROM Category WHERE year = :year AND month = :month GROUP BY title, lang ORDER BY count DESC") + suspend fun getTopCategoriesByMonth(year: Int, month: Int): List + @Query("SELECT * FROM Category") suspend fun getAllCategories(): List diff --git a/app/src/main/java/org/wikipedia/games/onthisday/OnThisDayGameViewModel.kt b/app/src/main/java/org/wikipedia/games/onthisday/OnThisDayGameViewModel.kt index c33c9c9dc1b..e4248330d61 100644 --- a/app/src/main/java/org/wikipedia/games/onthisday/OnThisDayGameViewModel.kt +++ b/app/src/main/java/org/wikipedia/games/onthisday/OnThisDayGameViewModel.kt @@ -165,14 +165,14 @@ class OnThisDayGameViewModel(savedStateHandle: SavedStateHandle) : ViewModel() { currentQuestionState = composeQuestionState(0), currentQuestionIndex = currentState.totalQuestions ) - _gameState.postValue(GameEnded(currentState, getGameStatistics())) + _gameState.postValue(GameEnded(currentState, getGameStatistics(wikiSite.languageCode))) } else if (currentState.currentQuestionState.month == currentMonth && currentState.currentQuestionState.day == currentDay && currentState.currentQuestionIndex == 0 && !currentState.currentQuestionState.goToNext) { // we're just starting the current game. _gameState.postValue(GameStarted(currentState)) } else if (currentState.currentQuestionState.month == currentMonth && currentState.currentQuestionState.day == currentDay && currentState.currentQuestionIndex >= currentState.totalQuestions) { // we're already done for today. - _gameState.postValue(GameEnded(currentState, getGameStatistics())) + _gameState.postValue(GameEnded(currentState, getGameStatistics(wikiSite.languageCode))) } else if (currentState.currentQuestionState.month != currentMonth || currentState.currentQuestionState.day != currentDay) { // the date in our current state doesn't match the requested date, so start a new game. currentState = currentState.copy(currentQuestionState = composeQuestionState(0), currentQuestionIndex = 0, answerState = List(MAX_QUESTIONS) { false }) @@ -243,7 +243,7 @@ class OnThisDayGameViewModel(savedStateHandle: SavedStateHandle) : ViewModel() { AppDatabase.instance.dailyGameHistoryDao().update(gameHistory) } } - _gameState.postValue(GameEnded(currentState, getGameStatistics())) + _gameState.postValue(GameEnded(currentState, getGameStatistics(wikiSite.languageCode))) } else { currentState = currentState.copy( currentQuestionState = composeQuestionState(nextQuestionIndex), @@ -347,35 +347,6 @@ class OnThisDayGameViewModel(savedStateHandle: SavedStateHandle) : ViewModel() { loadGameState(useDateFromState = false) } - private suspend fun getGameStatistics(): GameStatistics { - return withContext(Dispatchers.IO) { - val totalGamesPlayed = async { - AppDatabase.instance.dailyGameHistoryDao().getTotalGamesPlayed( - gameName = WikiGames.WHICH_CAME_FIRST.ordinal, - language = wikiSite.languageCode - ) - } - val averageScore = async { - AppDatabase.instance.dailyGameHistoryDao().getAverageScore( - gameName = WikiGames.WHICH_CAME_FIRST.ordinal, - language = wikiSite.languageCode - ) - } - val currentStreak = async { - AppDatabase.instance.dailyGameHistoryDao().getCurrentStreak( - gameName = WikiGames.WHICH_CAME_FIRST.ordinal, - language = wikiSite.languageCode - ) - } - - GameStatistics( - totalGamesPlayed.await(), - averageScore.await(), - currentStreak.await() - ) - } - } - // TODO: remove this in May, 2026 private suspend fun migrateGameHistoryFromPrefsToDatabase() { if (Prefs.otdGameHistory.isEmpty()) { @@ -474,5 +445,34 @@ class OnThisDayGameViewModel(savedStateHandle: SavedStateHandle) : ViewModel() { fun dateReleasedForLang(lang: String): LocalDate { return if (lang == "de" || ReleaseUtil.isPreBetaRelease) LocalDate.of(2025, 2, 20) else LocalDate.of(2025, 5, 21) } + + suspend fun getGameStatistics(languageCode: String): GameStatistics { + return withContext(Dispatchers.IO) { + val totalGamesPlayed = async { + AppDatabase.instance.dailyGameHistoryDao().getTotalGamesPlayed( + gameName = WikiGames.WHICH_CAME_FIRST.ordinal, + language = languageCode + ) + } + val averageScore = async { + AppDatabase.instance.dailyGameHistoryDao().getAverageScore( + gameName = WikiGames.WHICH_CAME_FIRST.ordinal, + language = languageCode + ) + } + val currentStreak = async { + AppDatabase.instance.dailyGameHistoryDao().getCurrentStreak( + gameName = WikiGames.WHICH_CAME_FIRST.ordinal, + language = languageCode + ) + } + + GameStatistics( + totalGamesPlayed.await(), + averageScore.await(), + currentStreak.await() + ) + } + } } } diff --git a/app/src/main/res/drawable/outline_activity_24.xml b/app/src/main/res/drawable/outline_activity_24.xml new file mode 100644 index 00000000000..9194f47b859 --- /dev/null +++ b/app/src/main/res/drawable/outline_activity_24.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/selector_nav_activity.xml b/app/src/main/res/drawable/selector_nav_activity.xml index 7967d30f8f0..4dd42599146 100644 --- a/app/src/main/res/drawable/selector_nav_activity.xml +++ b/app/src/main/res/drawable/selector_nav_activity.xml @@ -1,6 +1,6 @@ - - + + \ No newline at end of file From 156d1e13bfce8237923e70d9dbb3466e89329cab Mon Sep 17 00:00:00 2001 From: Dmitry Brant Date: Wed, 13 Aug 2025 17:32:58 -0400 Subject: [PATCH 06/70] Knock out time spent reading. --- .../activitytab/ActivityTabFragment.kt | 80 ++++++++++++++++++- .../activitytab/ActivityTabViewModel.kt | 11 +++ .../history/db/HistoryEntryWithImageDao.kt | 7 ++ app/src/main/res/values-qq/strings.xml | 4 + app/src/main/res/values/strings.xml | 4 + 5 files changed, 102 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/org/wikipedia/activitytab/ActivityTabFragment.kt b/app/src/main/java/org/wikipedia/activitytab/ActivityTabFragment.kt index 0af2ef46c24..258155eeb54 100644 --- a/app/src/main/java/org/wikipedia/activitytab/ActivityTabFragment.kt +++ b/app/src/main/java/org/wikipedia/activitytab/ActivityTabFragment.kt @@ -6,9 +6,11 @@ import android.view.View import android.view.ViewGroup import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.LinearProgressIndicator import androidx.compose.material3.Scaffold import androidx.compose.material3.Text @@ -16,9 +18,21 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Brush import androidx.compose.ui.platform.ComposeView +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.TextUnit +import androidx.compose.ui.unit.TextUnitType +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels +import org.wikipedia.R +import org.wikipedia.compose.ComposeColors import org.wikipedia.compose.components.error.WikiErrorClickEvents import org.wikipedia.compose.components.error.WikiErrorView import org.wikipedia.compose.theme.BaseTheme @@ -39,6 +53,7 @@ class ActivityTabFragment : Fragment() { BaseTheme { ActivityTabScreen( uiState = viewModel.uiState.collectAsState().value, + timeSpentState = viewModel.timeSpentState.collectAsState().value, wikiErrorClickEvents = WikiErrorClickEvents( retryClickListener = { viewModel.load() @@ -53,6 +68,7 @@ class ActivityTabFragment : Fragment() { @Composable fun ActivityTabScreen( uiState: UiState, + timeSpentState: UiState, wikiErrorClickEvents: WikiErrorClickEvents? = null ) { Scaffold( @@ -91,16 +107,72 @@ class ActivityTabFragment : Fragment() { } } is UiState.Success -> { - Box( + Column( modifier = Modifier - .fillMaxSize() + .fillMaxWidth() .padding(paddingValues) + .background( + brush = Brush.verticalGradient( + colors = listOf( + WikipediaTheme.colors.paperColor, + WikipediaTheme.colors.additionColor + ) + ) + ) ) { Text( - text = "TODO!", - modifier = Modifier.align(Alignment.Center), + text = stringResource(R.string.activity_tab_user_reading, "Dmitry Brant"), + modifier = Modifier.padding(top = 16.dp).align(Alignment.CenterHorizontally), + fontSize = 22.sp, + fontWeight = FontWeight.Medium, + textAlign = TextAlign.Center, color = WikipediaTheme.colors.primaryColor ) + Box( + modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp) + .background( + color = WikipediaTheme.colors.additionColor, + shape = RoundedCornerShape(8.dp) + ) + .align(Alignment.CenterHorizontally), + ) { + Text( + text = stringResource(R.string.activity_tab_on_wikipedia_android).uppercase(), + modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp), + fontSize = 11.sp, + fontFamily = FontFamily.Monospace, + letterSpacing = TextUnit(0.8f, TextUnitType.Sp), + textAlign = TextAlign.Center, + color = WikipediaTheme.colors.primaryColor + ) + } + if (timeSpentState is UiState.Success) { + Text( + text = stringResource(R.string.activity_tab_weekly_time_spent_hm, (timeSpentState.data / 3600), (timeSpentState.data % 60)), + modifier = Modifier.padding(top = 12.dp).align(Alignment.CenterHorizontally), + fontSize = 32.sp, + fontWeight = FontWeight.W500, + textAlign = TextAlign.Center, + style = TextStyle( + brush = Brush.linearGradient( + colors = listOf( + ComposeColors.Red700, + ComposeColors.Orange500, + ComposeColors.Yellow500, + ComposeColors.Blue300 + ) + ) + ), + color = WikipediaTheme.colors.primaryColor + ) + Text( + text = "Time spent reading this week", + modifier = Modifier.padding(top = 8.dp, bottom = 16.dp).align(Alignment.CenterHorizontally), + fontWeight = FontWeight.W500, + textAlign = TextAlign.Center, + color = WikipediaTheme.colors.primaryColor + ) + } } } } diff --git a/app/src/main/java/org/wikipedia/activitytab/ActivityTabViewModel.kt b/app/src/main/java/org/wikipedia/activitytab/ActivityTabViewModel.kt index 9db4ab9e0b0..cf180e50de1 100644 --- a/app/src/main/java/org/wikipedia/activitytab/ActivityTabViewModel.kt +++ b/app/src/main/java/org/wikipedia/activitytab/ActivityTabViewModel.kt @@ -16,12 +16,18 @@ import org.wikipedia.games.onthisday.OnThisDayGameViewModel import org.wikipedia.settings.Prefs import org.wikipedia.util.UiState import java.time.LocalDate +import java.time.LocalDateTime +import java.time.ZoneId +import java.util.concurrent.TimeUnit class ActivityTabViewModel(savedStateHandle: SavedStateHandle) : ViewModel() { private val _uiState = MutableStateFlow>(UiState.Loading) val uiState: StateFlow> = _uiState.asStateFlow() + private val _timeSpentState = MutableStateFlow>(UiState.Loading) + val timeSpentState: StateFlow> = _timeSpentState.asStateFlow() + var gameStatistics: OnThisDayGameViewModel.GameStatistics? = null var donationResults: List = emptyList() @@ -40,6 +46,11 @@ class ActivityTabViewModel(savedStateHandle: SavedStateHandle) : ViewModel() { val currentDate = LocalDate.now() val languageCode = WikipediaApp.instance.wikiSite.languageCode + val now = LocalDateTime.now().atZone(ZoneId.systemDefault()).toInstant().toEpochMilli() + val sevenDaysAgo = now - TimeUnit.DAYS.toMillis(7) + val totalTimeSpent = AppDatabase.instance.historyEntryWithImageDao().getTimeSpentSinceTimeStamp(sevenDaysAgo) + _timeSpentState.value = UiState.Success(totalTimeSpent) + // TODO: do something with game statistics gameStatistics = OnThisDayGameViewModel.getGameStatistics(languageCode) diff --git a/app/src/main/java/org/wikipedia/history/db/HistoryEntryWithImageDao.kt b/app/src/main/java/org/wikipedia/history/db/HistoryEntryWithImageDao.kt index 7ca5fba6284..ebce65badba 100644 --- a/app/src/main/java/org/wikipedia/history/db/HistoryEntryWithImageDao.kt +++ b/app/src/main/java/org/wikipedia/history/db/HistoryEntryWithImageDao.kt @@ -29,6 +29,13 @@ interface HistoryEntryWithImageDao { @RewriteQueriesToDropUnusedColumns suspend fun findEntriesBy(excludeSource1: Int, excludeSource2: Int, excludeSource3: Int, minTimeSpent: Int, limit: Int): List + @Query("SELECT SUM(timeSpentSec) FROM (" + + " SELECT DISTINCT HistoryEntry.lang, HistoryEntry.apiTitle, PageImage.timeSpentSec FROM HistoryEntry" + + " LEFT OUTER JOIN PageImage ON (HistoryEntry.namespace = PageImage.namespace AND HistoryEntry.apiTitle = PageImage.apiTitle AND HistoryEntry.lang = PageImage.lang)" + + " WHERE timestamp > :timeStamp" + + ")") + suspend fun getTimeSpentSinceTimeStamp(timeStamp: Long): Long + suspend fun findHistoryItem(wikiSite: WikiSite, searchQuery: String): SearchResults { var normalizedQuery = StringUtils.stripAccents(searchQuery) if (normalizedQuery.isEmpty()) { diff --git a/app/src/main/res/values-qq/strings.xml b/app/src/main/res/values-qq/strings.xml index e422d51ac9b..78afa4bfdcd 100644 --- a/app/src/main/res/values-qq/strings.xml +++ b/app/src/main/res/values-qq/strings.xml @@ -1171,6 +1171,10 @@ Edit summary when an image caption was added. Edit summary when an image and its caption were added. Button label for viewing an edit after it is published. + Title of the current user\'s reading activity. The %s is replaced with the user name. + Subtitle of the reading activity statistics, specific to the Wikipedia Android app. + Time spent reading this week, expressed in hours and minutes. The %1$d symbol is replaced with hours, and %2$d is replaced with minutes. + Label underneath the weekly time spent in the app. Title shown at the top of the activity for the file page. Button label to add image caption for the file. Button label to add image tags for the file. diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 618977f8da6..aa1dde0a9e7 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1190,6 +1190,10 @@ Added caption Added image and caption View + %s\'s reading + On Wikipedia Android + %1$dh %2$dm + Time spent reading this week From 344db85bf896f7fe77bcd2743be68f1298ee4e08 Mon Sep 17 00:00:00 2001 From: Dmitry Brant Date: Wed, 13 Aug 2025 17:52:06 -0400 Subject: [PATCH 07/70] Correct username. --- .../main/java/org/wikipedia/activitytab/ActivityTabFragment.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/org/wikipedia/activitytab/ActivityTabFragment.kt b/app/src/main/java/org/wikipedia/activitytab/ActivityTabFragment.kt index 258155eeb54..fc7e93bcbf8 100644 --- a/app/src/main/java/org/wikipedia/activitytab/ActivityTabFragment.kt +++ b/app/src/main/java/org/wikipedia/activitytab/ActivityTabFragment.kt @@ -32,6 +32,7 @@ import androidx.compose.ui.unit.sp import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels import org.wikipedia.R +import org.wikipedia.auth.AccountUtil import org.wikipedia.compose.ComposeColors import org.wikipedia.compose.components.error.WikiErrorClickEvents import org.wikipedia.compose.components.error.WikiErrorView @@ -121,7 +122,7 @@ class ActivityTabFragment : Fragment() { ) ) { Text( - text = stringResource(R.string.activity_tab_user_reading, "Dmitry Brant"), + text = stringResource(R.string.activity_tab_user_reading, AccountUtil.userName), modifier = Modifier.padding(top = 16.dp).align(Alignment.CenterHorizontally), fontSize = 22.sp, fontWeight = FontWeight.Medium, From 847810f4f3bd5bf6a81d503880e52229dd61ceaa Mon Sep 17 00:00:00 2001 From: Dmitry Brant Date: Thu, 14 Aug 2025 11:47:34 -0400 Subject: [PATCH 08/70] Add preview. --- .../activitytab/ActivityTabFragment.kt | 21 +++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/org/wikipedia/activitytab/ActivityTabFragment.kt b/app/src/main/java/org/wikipedia/activitytab/ActivityTabFragment.kt index fc7e93bcbf8..c8106c74b2a 100644 --- a/app/src/main/java/org/wikipedia/activitytab/ActivityTabFragment.kt +++ b/app/src/main/java/org/wikipedia/activitytab/ActivityTabFragment.kt @@ -25,6 +25,7 @@ import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.TextUnit import androidx.compose.ui.unit.TextUnitType import androidx.compose.ui.unit.dp @@ -39,6 +40,8 @@ import org.wikipedia.compose.components.error.WikiErrorView import org.wikipedia.compose.theme.BaseTheme import org.wikipedia.compose.theme.WikipediaTheme import org.wikipedia.settings.Prefs +import org.wikipedia.theme.Theme +import org.wikipedia.util.Resource import org.wikipedia.util.UiState class ActivityTabFragment : Fragment() { @@ -53,6 +56,7 @@ class ActivityTabFragment : Fragment() { setContent { BaseTheme { ActivityTabScreen( + userName = AccountUtil.userName, uiState = viewModel.uiState.collectAsState().value, timeSpentState = viewModel.timeSpentState.collectAsState().value, wikiErrorClickEvents = WikiErrorClickEvents( @@ -68,6 +72,7 @@ class ActivityTabFragment : Fragment() { @Composable fun ActivityTabScreen( + userName: String, uiState: UiState, timeSpentState: UiState, wikiErrorClickEvents: WikiErrorClickEvents? = null @@ -122,7 +127,7 @@ class ActivityTabFragment : Fragment() { ) ) { Text( - text = stringResource(R.string.activity_tab_user_reading, AccountUtil.userName), + text = stringResource(R.string.activity_tab_user_reading, userName), modifier = Modifier.padding(top = 16.dp).align(Alignment.CenterHorizontally), fontSize = 22.sp, fontWeight = FontWeight.Medium, @@ -167,7 +172,7 @@ class ActivityTabFragment : Fragment() { color = WikipediaTheme.colors.primaryColor ) Text( - text = "Time spent reading this week", + text = stringResource(R.string.activity_tab_weekly_time_spent), modifier = Modifier.padding(top = 8.dp, bottom = 16.dp).align(Alignment.CenterHorizontally), fontWeight = FontWeight.W500, textAlign = TextAlign.Center, @@ -180,6 +185,18 @@ class ActivityTabFragment : Fragment() { } } + @Preview(showBackground = true) + @Composable + fun DefaultPreviewSourceSelectionScreen() { + BaseTheme(currentTheme = Theme.LIGHT) { + ActivityTabScreen( + userName = "User", + uiState = UiState.Success(Unit), + timeSpentState = UiState.Success(123456L) + ) + } + } + companion object { fun newInstance(): ActivityTabFragment { return ActivityTabFragment().apply { From bb6d797670a88db783fd6659e2e3fe8ecd3ba4a2 Mon Sep 17 00:00:00 2001 From: William Rai <48931640+Williamrai@users.noreply.github.com> Date: Thu, 14 Aug 2025 11:50:58 -0400 Subject: [PATCH 09/70] - code changes (#5838) --- .../activitytab/ActivityTabFragment.kt | 206 +++++++++--------- .../activitytab/ActivityTabViewModel.kt | 30 +-- 2 files changed, 104 insertions(+), 132 deletions(-) diff --git a/app/src/main/java/org/wikipedia/activitytab/ActivityTabFragment.kt b/app/src/main/java/org/wikipedia/activitytab/ActivityTabFragment.kt index fc7e93bcbf8..d5d7db08c43 100644 --- a/app/src/main/java/org/wikipedia/activitytab/ActivityTabFragment.kt +++ b/app/src/main/java/org/wikipedia/activitytab/ActivityTabFragment.kt @@ -7,11 +7,11 @@ import android.view.ViewGroup import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.LinearProgressIndicator import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -35,7 +35,6 @@ import org.wikipedia.R import org.wikipedia.auth.AccountUtil import org.wikipedia.compose.ComposeColors import org.wikipedia.compose.components.error.WikiErrorClickEvents -import org.wikipedia.compose.components.error.WikiErrorView import org.wikipedia.compose.theme.BaseTheme import org.wikipedia.compose.theme.WikipediaTheme import org.wikipedia.settings.Prefs @@ -53,13 +52,7 @@ class ActivityTabFragment : Fragment() { setContent { BaseTheme { ActivityTabScreen( - uiState = viewModel.uiState.collectAsState().value, timeSpentState = viewModel.timeSpentState.collectAsState().value, - wikiErrorClickEvents = WikiErrorClickEvents( - retryClickListener = { - viewModel.load() - } - ) ) } } @@ -68,9 +61,7 @@ class ActivityTabFragment : Fragment() { @Composable fun ActivityTabScreen( - uiState: UiState, - timeSpentState: UiState, - wikiErrorClickEvents: WikiErrorClickEvents? = null + timeSpentState: UiState ) { Scaffold( modifier = Modifier @@ -78,108 +69,111 @@ class ActivityTabFragment : Fragment() { .background(WikipediaTheme.colors.paperColor), containerColor = WikipediaTheme.colors.paperColor ) { paddingValues -> - when (uiState) { - is UiState.Loading -> { - Box( - modifier = Modifier - .fillMaxWidth() - .padding(paddingValues), - ) { - LinearProgressIndicator( - modifier = Modifier.fillMaxWidth(), - color = WikipediaTheme.colors.progressiveColor, - trackColor = WikipediaTheme.colors.borderColor - ) - } - } - is UiState.Error -> { - Box( - modifier = Modifier - .fillMaxSize() - .padding(paddingValues), - contentAlignment = Alignment.Center - ) { - WikiErrorView( - modifier = Modifier - .fillMaxWidth(), - caught = uiState.error, - errorClickEvents = wikiErrorClickEvents - ) - } - } - is UiState.Success -> { - Column( - modifier = Modifier - .fillMaxWidth() - .padding(paddingValues) - .background( - brush = Brush.verticalGradient( - colors = listOf( - WikipediaTheme.colors.paperColor, - WikipediaTheme.colors.additionColor - ) - ) + Column( + modifier = Modifier + .fillMaxWidth() + .padding(paddingValues) + .background( + brush = Brush.verticalGradient( + colors = listOf( + WikipediaTheme.colors.paperColor, + WikipediaTheme.colors.additionColor ) - ) { - Text( - text = stringResource(R.string.activity_tab_user_reading, AccountUtil.userName), - modifier = Modifier.padding(top = 16.dp).align(Alignment.CenterHorizontally), - fontSize = 22.sp, - fontWeight = FontWeight.Medium, - textAlign = TextAlign.Center, - color = WikipediaTheme.colors.primaryColor ) - Box( - modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp) - .background( - color = WikipediaTheme.colors.additionColor, - shape = RoundedCornerShape(8.dp) - ) - .align(Alignment.CenterHorizontally), - ) { - Text( - text = stringResource(R.string.activity_tab_on_wikipedia_android).uppercase(), - modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp), - fontSize = 11.sp, - fontFamily = FontFamily.Monospace, - letterSpacing = TextUnit(0.8f, TextUnitType.Sp), - textAlign = TextAlign.Center, - color = WikipediaTheme.colors.primaryColor - ) - } - if (timeSpentState is UiState.Success) { - Text( - text = stringResource(R.string.activity_tab_weekly_time_spent_hm, (timeSpentState.data / 3600), (timeSpentState.data % 60)), - modifier = Modifier.padding(top = 12.dp).align(Alignment.CenterHorizontally), - fontSize = 32.sp, - fontWeight = FontWeight.W500, - textAlign = TextAlign.Center, - style = TextStyle( - brush = Brush.linearGradient( - colors = listOf( - ComposeColors.Red700, - ComposeColors.Orange500, - ComposeColors.Yellow500, - ComposeColors.Blue300 - ) - ) - ), - color = WikipediaTheme.colors.primaryColor - ) - Text( - text = "Time spent reading this week", - modifier = Modifier.padding(top = 8.dp, bottom = 16.dp).align(Alignment.CenterHorizontally), - fontWeight = FontWeight.W500, - textAlign = TextAlign.Center, - color = WikipediaTheme.colors.primaryColor - ) + ) + ) { + // All module will have their own state management + // TimeSpentModule + TimeSpentModule( + timeSpentState = timeSpentState, + wikiErrorClickEvents = WikiErrorClickEvents( + retryClickListener = { + viewModel.loadTimeSpent() } - } - } + ) + ) + // Monthly insights + + // Categories module + + // impact module + + // Game module + + // other module } } } + // @TODO: error view and handling + @Composable + fun ColumnScope.TimeSpentModule( + modifier: Modifier = Modifier, + timeSpentState: UiState, + wikiErrorClickEvents: WikiErrorClickEvents? = null + ) { + Text( + text = stringResource(R.string.activity_tab_user_reading, AccountUtil.userName), + modifier = Modifier + .padding(top = 16.dp) + .align(Alignment.CenterHorizontally), + fontSize = 22.sp, + fontWeight = FontWeight.Medium, + textAlign = TextAlign.Center, + color = WikipediaTheme.colors.primaryColor + ) + Box( + modifier = Modifier + .padding(horizontal = 8.dp, vertical = 4.dp) + .background( + color = WikipediaTheme.colors.additionColor, + shape = RoundedCornerShape(8.dp) + ) + .align(Alignment.CenterHorizontally), + ) { + Text( + text = stringResource(R.string.activity_tab_on_wikipedia_android).uppercase(), + modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp), + fontSize = 11.sp, + fontFamily = FontFamily.Monospace, + letterSpacing = TextUnit(0.8f, TextUnitType.Sp), + textAlign = TextAlign.Center, + color = WikipediaTheme.colors.primaryColor + ) + } + if (timeSpentState is UiState.Success) { + Text( + text = stringResource(R.string.activity_tab_weekly_time_spent_hm, (timeSpentState.data / 3600), (timeSpentState.data % 60)), + modifier = Modifier + .padding(top = 12.dp) + .align(Alignment.CenterHorizontally), + fontSize = 32.sp, + fontWeight = FontWeight.W500, + textAlign = TextAlign.Center, + style = TextStyle( + brush = Brush.linearGradient( + colors = listOf( + ComposeColors.Red700, + ComposeColors.Orange500, + ComposeColors.Yellow500, + ComposeColors.Blue300 + ) + ) + ), + color = WikipediaTheme.colors.primaryColor + ) + Text( + text = "Time spent reading this week", + modifier = Modifier + .padding(top = 8.dp, bottom = 16.dp) + .align(Alignment.CenterHorizontally), + fontWeight = FontWeight.W500, + textAlign = TextAlign.Center, + color = WikipediaTheme.colors.primaryColor + ) + } + } + companion object { fun newInstance(): ActivityTabFragment { return ActivityTabFragment().apply { diff --git a/app/src/main/java/org/wikipedia/activitytab/ActivityTabViewModel.kt b/app/src/main/java/org/wikipedia/activitytab/ActivityTabViewModel.kt index cf180e50de1..f5cb60d9723 100644 --- a/app/src/main/java/org/wikipedia/activitytab/ActivityTabViewModel.kt +++ b/app/src/main/java/org/wikipedia/activitytab/ActivityTabViewModel.kt @@ -8,23 +8,16 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch -import org.wikipedia.WikipediaApp import org.wikipedia.categories.db.Category import org.wikipedia.database.AppDatabase import org.wikipedia.donate.DonationResult import org.wikipedia.games.onthisday.OnThisDayGameViewModel -import org.wikipedia.settings.Prefs import org.wikipedia.util.UiState -import java.time.LocalDate import java.time.LocalDateTime import java.time.ZoneId import java.util.concurrent.TimeUnit class ActivityTabViewModel(savedStateHandle: SavedStateHandle) : ViewModel() { - - private val _uiState = MutableStateFlow>(UiState.Loading) - val uiState: StateFlow> = _uiState.asStateFlow() - private val _timeSpentState = MutableStateFlow>(UiState.Loading) val timeSpentState: StateFlow> = _timeSpentState.asStateFlow() @@ -34,33 +27,18 @@ class ActivityTabViewModel(savedStateHandle: SavedStateHandle) : ViewModel() { var topCategories: List = emptyList() init { - load() + loadTimeSpent() } - fun load() { + fun loadTimeSpent() { viewModelScope.launch(CoroutineExceptionHandler { _, throwable -> - _uiState.value = UiState.Error(throwable) + _timeSpentState.value = UiState.Error(throwable) }) { - _uiState.value = UiState.Loading - - val currentDate = LocalDate.now() - val languageCode = WikipediaApp.instance.wikiSite.languageCode - + _timeSpentState.value = UiState.Loading val now = LocalDateTime.now().atZone(ZoneId.systemDefault()).toInstant().toEpochMilli() val sevenDaysAgo = now - TimeUnit.DAYS.toMillis(7) val totalTimeSpent = AppDatabase.instance.historyEntryWithImageDao().getTimeSpentSinceTimeStamp(sevenDaysAgo) _timeSpentState.value = UiState.Success(totalTimeSpent) - - // TODO: do something with game statistics - gameStatistics = OnThisDayGameViewModel.getGameStatistics(languageCode) - - // TODO: do something with donation results - donationResults = Prefs.donationResults - - // TODO: do something with top categories - topCategories = AppDatabase.instance.categoryDao().getTopCategoriesByMonth(currentDate.year, currentDate.monthValue) - - _uiState.value = UiState.Success(Unit) } } } From 0e58868310142e84d59ca0dd82a7be07297b905e Mon Sep 17 00:00:00 2001 From: Dmitry Brant Date: Thu, 14 Aug 2025 12:03:24 -0400 Subject: [PATCH 10/70] Clean up a bit. --- .../activitytab/ActivityTabFragment.kt | 41 +++++++------------ 1 file changed, 15 insertions(+), 26 deletions(-) diff --git a/app/src/main/java/org/wikipedia/activitytab/ActivityTabFragment.kt b/app/src/main/java/org/wikipedia/activitytab/ActivityTabFragment.kt index a93ea21cb4b..83a94eb37b8 100644 --- a/app/src/main/java/org/wikipedia/activitytab/ActivityTabFragment.kt +++ b/app/src/main/java/org/wikipedia/activitytab/ActivityTabFragment.kt @@ -7,7 +7,6 @@ import android.view.ViewGroup import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding @@ -86,18 +85,9 @@ class ActivityTabFragment : Fragment() { ) ) ) { - Text( - text = stringResource(R.string.activity_tab_user_reading, userName), - modifier = Modifier.padding(top = 16.dp) - .align(Alignment.CenterHorizontally), - fontSize = 22.sp, - fontWeight = FontWeight.Medium, - textAlign = TextAlign.Center, - color = WikipediaTheme.colors.primaryColor - ) - // All module will have their own state management - // TimeSpentModule TimeSpentModule( + modifier = Modifier.align(Alignment.CenterHorizontally), + userName = userName, timeSpentState = timeSpentState, wikiErrorClickEvents = WikiErrorClickEvents( retryClickListener = { @@ -105,6 +95,7 @@ class ActivityTabFragment : Fragment() { } ) ) + // Monthly insights // Categories module @@ -118,6 +109,7 @@ class ActivityTabFragment : Fragment() { } } + @Preview @Composable fun ActivityTabScreenPreview() { BaseTheme(currentTheme = Theme.LIGHT) { @@ -130,29 +122,28 @@ class ActivityTabFragment : Fragment() { // @TODO: error view and handling @Composable - fun ColumnScope.TimeSpentModule( - modifier: Modifier = Modifier, + fun TimeSpentModule( + modifier: Modifier, + userName: String, timeSpentState: UiState, wikiErrorClickEvents: WikiErrorClickEvents? = null ) { Text( - text = stringResource(R.string.activity_tab_user_reading, AccountUtil.userName), - modifier = Modifier - .padding(top = 16.dp) - .align(Alignment.CenterHorizontally), + text = stringResource(R.string.activity_tab_user_reading, userName), + modifier = modifier + .padding(top = 16.dp), fontSize = 22.sp, fontWeight = FontWeight.Medium, textAlign = TextAlign.Center, color = WikipediaTheme.colors.primaryColor ) Box( - modifier = Modifier + modifier = modifier .padding(horizontal = 8.dp, vertical = 4.dp) .background( color = WikipediaTheme.colors.additionColor, shape = RoundedCornerShape(8.dp) ) - .align(Alignment.CenterHorizontally), ) { Text( text = stringResource(R.string.activity_tab_on_wikipedia_android).uppercase(), @@ -167,9 +158,8 @@ class ActivityTabFragment : Fragment() { if (timeSpentState is UiState.Success) { Text( text = stringResource(R.string.activity_tab_weekly_time_spent_hm, (timeSpentState.data / 3600), (timeSpentState.data % 60)), - modifier = Modifier - .padding(top = 12.dp) - .align(Alignment.CenterHorizontally), + modifier = modifier + .padding(top = 12.dp), fontSize = 32.sp, fontWeight = FontWeight.W500, textAlign = TextAlign.Center, @@ -187,9 +177,8 @@ class ActivityTabFragment : Fragment() { ) Text( text = "Time spent reading this week", - modifier = Modifier - .padding(top = 8.dp, bottom = 16.dp) - .align(Alignment.CenterHorizontally), + modifier = modifier + .padding(top = 8.dp, bottom = 16.dp), fontWeight = FontWeight.W500, textAlign = TextAlign.Center, color = WikipediaTheme.colors.primaryColor From 1f4917f6f5dae517333a8d6a848221be7c1c2708 Mon Sep 17 00:00:00 2001 From: Dmitry Brant Date: Thu, 14 Aug 2025 15:15:02 -0400 Subject: [PATCH 11/70] Fill things out a bit... --- .../activitytab/ActivityTabFragment.kt | 305 ++++++++++++++++-- .../activitytab/ActivityTabViewModel.kt | 47 ++- .../wikipedia/history/db/HistoryEntryDao.kt | 6 + .../readinglist/db/ReadingListPageDao.kt | 6 + app/src/main/res/drawable/ic_newsstand_24.xml | 5 + app/src/main/res/values-qq/strings.xml | 2 + app/src/main/res/values/strings.xml | 2 + 7 files changed, 341 insertions(+), 32 deletions(-) create mode 100644 app/src/main/res/drawable/ic_newsstand_24.xml diff --git a/app/src/main/java/org/wikipedia/activitytab/ActivityTabFragment.kt b/app/src/main/java/org/wikipedia/activitytab/ActivityTabFragment.kt index 83a94eb37b8..b530dceb527 100644 --- a/app/src/main/java/org/wikipedia/activitytab/ActivityTabFragment.kt +++ b/app/src/main/java/org/wikipedia/activitytab/ActivityTabFragment.kt @@ -4,21 +4,36 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.background +import androidx.compose.foundation.border import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.Icon import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.graphics.painter.BrushPainter +import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.ComposeView +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontFamily @@ -29,17 +44,23 @@ import androidx.compose.ui.unit.TextUnit import androidx.compose.ui.unit.TextUnitType import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import androidx.core.net.toUri import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels +import coil3.compose.AsyncImage import org.wikipedia.R import org.wikipedia.auth.AccountUtil import org.wikipedia.compose.ComposeColors import org.wikipedia.compose.components.error.WikiErrorClickEvents import org.wikipedia.compose.theme.BaseTheme import org.wikipedia.compose.theme.WikipediaTheme +import org.wikipedia.dataclient.WikiSite +import org.wikipedia.page.PageTitle import org.wikipedia.settings.Prefs import org.wikipedia.theme.Theme import org.wikipedia.util.UiState +import org.wikipedia.views.imageservice.ImageService +import java.util.Locale class ActivityTabFragment : Fragment() { @@ -54,7 +75,7 @@ class ActivityTabFragment : Fragment() { BaseTheme { ActivityTabScreen( userName = AccountUtil.userName, - timeSpentState = viewModel.timeSpentState.collectAsState().value, + readingHistoryState = viewModel.readingHistoryState.collectAsState().value, ) } } @@ -64,7 +85,7 @@ class ActivityTabFragment : Fragment() { @Composable fun ActivityTabScreen( userName: String, - timeSpentState: UiState + readingHistoryState: UiState ) { Scaffold( modifier = Modifier @@ -85,13 +106,13 @@ class ActivityTabFragment : Fragment() { ) ) ) { - TimeSpentModule( + ReadingHistoryModule( modifier = Modifier.align(Alignment.CenterHorizontally), userName = userName, - timeSpentState = timeSpentState, + readingHistoryState = readingHistoryState, wikiErrorClickEvents = WikiErrorClickEvents( retryClickListener = { - viewModel.loadTimeSpent() + viewModel.loadReadingHistory() } ) ) @@ -99,33 +120,28 @@ class ActivityTabFragment : Fragment() { // Monthly insights // Categories module + } - // impact module + // --- new column --- - // Game module + // impact module - // other module - } - } - } + // game module - @Preview - @Composable - fun ActivityTabScreenPreview() { - BaseTheme(currentTheme = Theme.LIGHT) { - ActivityTabScreen( - userName = "User", - timeSpentState = UiState.Success(123456L) - ) + // donation module + + // --- new column --- + + // history module } } // @TODO: error view and handling @Composable - fun TimeSpentModule( + fun ReadingHistoryModule( modifier: Modifier, userName: String, - timeSpentState: UiState, + readingHistoryState: UiState, wikiErrorClickEvents: WikiErrorClickEvents? = null ) { Text( @@ -155,9 +171,14 @@ class ActivityTabFragment : Fragment() { color = WikipediaTheme.colors.primaryColor ) } - if (timeSpentState is UiState.Success) { + if (readingHistoryState is UiState.Success) { + val readingHistory = readingHistoryState.data Text( - text = stringResource(R.string.activity_tab_weekly_time_spent_hm, (timeSpentState.data / 3600), (timeSpentState.data % 60)), + text = stringResource( + R.string.activity_tab_weekly_time_spent_hm, + (readingHistory.timeSpentThisWeek / 3600), + (readingHistory.timeSpentThisWeek % 60) + ), modifier = modifier .padding(top = 12.dp), fontSize = 32.sp, @@ -176,13 +197,249 @@ class ActivityTabFragment : Fragment() { color = WikipediaTheme.colors.primaryColor ) Text( - text = "Time spent reading this week", + text = stringResource(R.string.activity_tab_weekly_time_spent), modifier = modifier .padding(top = 8.dp, bottom = 16.dp), fontWeight = FontWeight.W500, textAlign = TextAlign.Center, color = WikipediaTheme.colors.primaryColor ) + + Card( + modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp), + elevation = CardDefaults.cardElevation(defaultElevation = 0.dp), + colors = CardDefaults.cardColors( + containerColor = WikipediaTheme.colors.paperColor + ), + border = BorderStroke( + width = 1.dp, + color = WikipediaTheme.colors.borderColor + ), + shape = RoundedCornerShape(12.dp) + ) { + Row( + modifier = modifier.fillMaxWidth() + .padding(top = 16.dp, start = 16.dp, end = 16.dp) + ) { + Column( + modifier = modifier.weight(1f) + ) { + Row( + modifier = modifier.fillMaxWidth() + ) { + Icon( + modifier = Modifier.size(16.dp), + painter = painterResource(R.drawable.ic_newsstand_24), + tint = WikipediaTheme.colors.primaryColor, + contentDescription = null + ) + Text( + text = stringResource(R.string.activity_tab_monthly_articles_read), + modifier = Modifier.padding(start = 8.dp), + fontWeight = FontWeight.Medium, + fontSize = 12.sp, + color = WikipediaTheme.colors.primaryColor + ) + } + } + Icon( + modifier = Modifier.size(24.dp), + painter = painterResource(R.drawable.ic_chevron_forward_white_24dp), + tint = WikipediaTheme.colors.primaryColor, + contentDescription = null + ) + } + Text( + text = "9:34 AM", + modifier = Modifier.padding(start = 16.dp, top = 2.dp), + fontSize = 12.sp, + color = WikipediaTheme.colors.secondaryColor + ) + Row( + modifier = modifier.fillMaxWidth().padding(top = 6.dp, bottom = 16.dp) + ) { + Text( + text = readingHistory.articlesReadThisMonth.toString(), + modifier = Modifier.padding(start = 16.dp).align(Alignment.Bottom), + fontWeight = FontWeight.Medium, + fontSize = 22.sp, + color = WikipediaTheme.colors.primaryColor + ) + Spacer(modifier = Modifier.weight(1f)) + Box( + modifier = Modifier.padding(end = 16.dp) + .size(width = 80.dp, height = 48.dp) + .background( + color = WikipediaTheme.colors.additionColor, + shape = RoundedCornerShape(2.dp) + ) + ) + } + } + + Card( + modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp, vertical = 16.dp), + elevation = CardDefaults.cardElevation(defaultElevation = 0.dp), + colors = CardDefaults.cardColors( + containerColor = WikipediaTheme.colors.paperColor + ), + border = BorderStroke( + width = 1.dp, + color = WikipediaTheme.colors.borderColor + ), + shape = RoundedCornerShape(12.dp) + ) { + Row( + modifier = modifier.fillMaxWidth() + .padding(top = 16.dp, start = 16.dp, end = 16.dp) + ) { + Column( + modifier = modifier.weight(1f) + ) { + Row( + modifier = modifier.fillMaxWidth() + ) { + Icon( + modifier = Modifier.size(16.dp), + painter = painterResource(R.drawable.ic_bookmark_border_white_24dp), + tint = WikipediaTheme.colors.primaryColor, + contentDescription = null + ) + Text( + text = stringResource(R.string.activity_tab_monthly_articles_saved), + modifier = Modifier.padding(start = 8.dp), + fontWeight = FontWeight.Medium, + fontSize = 12.sp, + color = WikipediaTheme.colors.primaryColor + ) + } + } + Icon( + modifier = Modifier.size(24.dp), + painter = painterResource(R.drawable.ic_chevron_forward_white_24dp), + tint = WikipediaTheme.colors.primaryColor, + contentDescription = null + ) + } + Text( + text = "9:34 AM", + modifier = Modifier.padding(start = 16.dp, top = 2.dp), + fontSize = 12.sp, + color = WikipediaTheme.colors.secondaryColor + ) + Row( + modifier = modifier.fillMaxWidth().padding(top = 6.dp, bottom = 16.dp) + ) { + Text( + text = readingHistory.articlesSavedThisMonth.toString(), + modifier = Modifier.padding(start = 16.dp).align(Alignment.Bottom), + fontWeight = FontWeight.Medium, + fontSize = 22.sp, + color = WikipediaTheme.colors.primaryColor + ) + Spacer(modifier = Modifier.weight(1f)) + Row( + modifier = Modifier.padding(end = 16.dp) + ) { + val itemsToShow = if (readingHistory.articlesSaved.size <= 4) readingHistory.articlesSaved.size else 3 + val showOverflowItem = readingHistory.articlesSaved.size > 4 + + for (i in 0 until itemsToShow) { + val url = readingHistory.articlesSaved[i].thumbUrl + if (url == null) { + Box( + modifier = Modifier.padding(start = 4.dp).size(38.dp) + .background( + color = Color.White, + shape = RoundedCornerShape(19.dp) + ).border( + 0.5.dp, + WikipediaTheme.colors.borderColor, + RoundedCornerShape(19.dp)) + ) { + Icon( + modifier = Modifier.size(24.dp).align(Alignment.Center), + painter = painterResource(R.drawable.ic_wikipedia_b), + tint = WikipediaTheme.colors.primaryColor, + contentDescription = null + ) + } + } else { + val request = ImageService.getRequest(LocalContext.current, url = url) + AsyncImage( + model = request, + placeholder = BrushPainter(SolidColor(WikipediaTheme.colors.borderColor)), + error = BrushPainter(SolidColor(WikipediaTheme.colors.borderColor)), + contentScale = ContentScale.Crop, + contentDescription = null, + modifier = Modifier.padding(start = 4.dp).size(38.dp) + .clip(RoundedCornerShape(19.dp)) + ) + } + } + + if (showOverflowItem) { + Box( + modifier = Modifier.padding(start = 4.dp).size(38.dp) + .background( + color = WikipediaTheme.colors.placeholderColor, + shape = RoundedCornerShape(19.dp) + ) + ) { + Text( + text = String.format(Locale.getDefault(), "+%d", readingHistory.articlesSavedThisMonth - 3), + modifier = Modifier.align(Alignment.Center), + fontWeight = FontWeight.Medium, + fontSize = 11.sp, + color = Color.White + ) + } + } + } + } + } + // empty state + } + } + + @Preview + @Composable + fun ActivityTabScreenPreview() { + val site = WikiSite("https://en.wikipedia.org/".toUri(), "en") + BaseTheme(currentTheme = Theme.LIGHT) { + ActivityTabScreen( + userName = "User", + readingHistoryState = UiState.Success(ActivityTabViewModel.ReadingHistory( + timeSpentThisWeek = 12345, + articlesReadThisMonth = 123, + articlesReadByWeek = listOf(0, 12, 34, 56), + articlesSavedThisMonth = 23, + articlesSaved = listOf( + PageTitle(text = "Psychology of art", wiki = site, thumbUrl = "foo.jpg", description = "Study of mental functions and behaviors", displayText = null), + PageTitle(text = "Industrial design", wiki = site, thumbUrl = null, description = "Process of design applied to physical products", displayText = null), + PageTitle(text = "Dufourspitze", wiki = site, thumbUrl = "foo.jpg", description = "Highest mountain in Switzerland", displayText = null), + PageTitle(text = "Barack Obama", wiki = site, thumbUrl = "foo.jpg", description = "President of the United States from 2009 to 2017", displayText = null), + PageTitle(text = "Octagon house", wiki = site, thumbUrl = "foo.jpg", description = "North American house style briefly popular in the 1850s", displayText = null) + ) + )) + ) + } + } + + @Preview + @Composable + fun ActivityTabScreenEmptyPreview() { + BaseTheme(currentTheme = Theme.LIGHT) { + ActivityTabScreen( + userName = "User", + readingHistoryState = UiState.Success(ActivityTabViewModel.ReadingHistory( + timeSpentThisWeek = 0, + articlesReadThisMonth = 0, + articlesReadByWeek = listOf(0, 0, 0, 0), + articlesSavedThisMonth = 0, + articlesSaved = emptyList() + )) + ) } } diff --git a/app/src/main/java/org/wikipedia/activitytab/ActivityTabViewModel.kt b/app/src/main/java/org/wikipedia/activitytab/ActivityTabViewModel.kt index f5cb60d9723..9fab175bef4 100644 --- a/app/src/main/java/org/wikipedia/activitytab/ActivityTabViewModel.kt +++ b/app/src/main/java/org/wikipedia/activitytab/ActivityTabViewModel.kt @@ -12,14 +12,16 @@ import org.wikipedia.categories.db.Category import org.wikipedia.database.AppDatabase import org.wikipedia.donate.DonationResult import org.wikipedia.games.onthisday.OnThisDayGameViewModel +import org.wikipedia.page.PageTitle +import org.wikipedia.readinglist.database.ReadingListPage import org.wikipedia.util.UiState import java.time.LocalDateTime import java.time.ZoneId import java.util.concurrent.TimeUnit class ActivityTabViewModel(savedStateHandle: SavedStateHandle) : ViewModel() { - private val _timeSpentState = MutableStateFlow>(UiState.Loading) - val timeSpentState: StateFlow> = _timeSpentState.asStateFlow() + private val _readingHistoryState = MutableStateFlow>(UiState.Loading) + val readingHistoryState: StateFlow> = _readingHistoryState.asStateFlow() var gameStatistics: OnThisDayGameViewModel.GameStatistics? = null var donationResults: List = emptyList() @@ -27,18 +29,47 @@ class ActivityTabViewModel(savedStateHandle: SavedStateHandle) : ViewModel() { var topCategories: List = emptyList() init { - loadTimeSpent() + loadReadingHistory() } - fun loadTimeSpent() { + fun loadReadingHistory() { viewModelScope.launch(CoroutineExceptionHandler { _, throwable -> - _timeSpentState.value = UiState.Error(throwable) + _readingHistoryState.value = UiState.Error(throwable) }) { - _timeSpentState.value = UiState.Loading + _readingHistoryState.value = UiState.Loading val now = LocalDateTime.now().atZone(ZoneId.systemDefault()).toInstant().toEpochMilli() - val sevenDaysAgo = now - TimeUnit.DAYS.toMillis(7) + val weekInMillis = TimeUnit.DAYS.toMillis(7) + val sevenDaysAgo = now - weekInMillis val totalTimeSpent = AppDatabase.instance.historyEntryWithImageDao().getTimeSpentSinceTimeStamp(sevenDaysAgo) - _timeSpentState.value = UiState.Success(totalTimeSpent) + + val thirtyDaysAgo = now - TimeUnit.DAYS.toMillis(30) + val articlesReadThisMonth = AppDatabase.instance.historyEntryDao().getTotalEntriesSince(thirtyDaysAgo) ?: 0 + val articlesReadByWeek = mutableListOf() + for (i in 1..4) { + val weekAgo = now - weekInMillis + val articlesRead = AppDatabase.instance.historyEntryDao().getTotalEntriesBetween(weekAgo, weekAgo + weekInMillis) + articlesReadByWeek.add(articlesRead?.toInt() ?: 0) + } + + val articlesSavedThisMonth = AppDatabase.instance.readingListPageDao().getTotalPagesSince(thirtyDaysAgo) ?: 0 + val articlesSaved = AppDatabase.instance.readingListPageDao().getPagesSince(thirtyDaysAgo, 4) + .map { ReadingListPage.toPageTitle(it) } + + _readingHistoryState.value = UiState.Success(ReadingHistory( + totalTimeSpent, + articlesReadThisMonth, + articlesReadByWeek, + articlesSavedThisMonth, + articlesSaved) + ) } } + + class ReadingHistory( + val timeSpentThisWeek: Long, + val articlesReadThisMonth: Long, + val articlesReadByWeek: List, + val articlesSavedThisMonth: Long, + val articlesSaved: List + ) } diff --git a/app/src/main/java/org/wikipedia/history/db/HistoryEntryDao.kt b/app/src/main/java/org/wikipedia/history/db/HistoryEntryDao.kt index 8fe66f1d01b..ac4713cbbdb 100644 --- a/app/src/main/java/org/wikipedia/history/db/HistoryEntryDao.kt +++ b/app/src/main/java/org/wikipedia/history/db/HistoryEntryDao.kt @@ -39,6 +39,12 @@ interface HistoryEntryDao { @Query("DELETE FROM HistoryEntry WHERE authority = :authority AND lang = :lang AND namespace = :namespace AND apiTitle = :apiTitle") suspend fun deleteBy(authority: String, lang: String, namespace: String?, apiTitle: String) + @Query("SELECT COUNT(*) FROM HistoryEntry WHERE timestamp > :timestamp") + suspend fun getTotalEntriesSince(timestamp: Long): Long? + + @Query("SELECT COUNT(*) FROM HistoryEntry WHERE timestamp BETWEEN :timestampStart AND :timestampEnd") + suspend fun getTotalEntriesBetween(timestampStart: Long, timestampEnd: Long): Long? + @Transaction suspend fun insert(entries: List) { entries.forEach { diff --git a/app/src/main/java/org/wikipedia/readinglist/db/ReadingListPageDao.kt b/app/src/main/java/org/wikipedia/readinglist/db/ReadingListPageDao.kt index a2e65a94428..4367cecedf8 100644 --- a/app/src/main/java/org/wikipedia/readinglist/db/ReadingListPageDao.kt +++ b/app/src/main/java/org/wikipedia/readinglist/db/ReadingListPageDao.kt @@ -86,6 +86,12 @@ interface ReadingListPageDao { @Query("SELECT * FROM ReadingListPage WHERE remoteId < 1") fun getAllPagesToBeSynced(): List + @Query("SELECT COUNT(*) FROM ReadingListPage WHERE mtime > :timestamp") + suspend fun getTotalPagesSince(timestamp: Long): Long? + + @Query("SELECT * FROM ReadingListPage WHERE mtime > :timestamp ORDER BY mtime DESC LIMIT :limit") + suspend fun getPagesSince(timestamp: Long, limit: Int): List + fun getAllPagesToBeSaved() = getPagesByStatus(ReadingListPage.STATUS_QUEUE_FOR_SAVE, true) fun getAllPagesToBeForcedSave() = getPagesByStatus(ReadingListPage.STATUS_QUEUE_FOR_FORCED_SAVE, true) diff --git a/app/src/main/res/drawable/ic_newsstand_24.xml b/app/src/main/res/drawable/ic_newsstand_24.xml new file mode 100644 index 00000000000..fa10498e306 --- /dev/null +++ b/app/src/main/res/drawable/ic_newsstand_24.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/values-qq/strings.xml b/app/src/main/res/values-qq/strings.xml index 78afa4bfdcd..707b0a7d689 100644 --- a/app/src/main/res/values-qq/strings.xml +++ b/app/src/main/res/values-qq/strings.xml @@ -1175,6 +1175,8 @@ Subtitle of the reading activity statistics, specific to the Wikipedia Android app. Time spent reading this week, expressed in hours and minutes. The %1$d symbol is replaced with hours, and %2$d is replaced with minutes. Label underneath the weekly time spent in the app. + Label on a card that lists the number of articles read this month. + Label on a card that lists the number of articles saved this month to a reading list. Title shown at the top of the activity for the file page. Button label to add image caption for the file. Button label to add image tags for the file. diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index aa1dde0a9e7..7885f6d6cbc 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1194,6 +1194,8 @@ On Wikipedia Android %1$dh %2$dm Time spent reading this week + Articles read this month + Articles saved this month From c0d49247318b78b51a2288b9ee5ca8c0eb62d8a8 Mon Sep 17 00:00:00 2001 From: Dmitry Brant Date: Thu, 14 Aug 2025 17:01:23 -0400 Subject: [PATCH 12/70] Stay on target... --- .../activitytab/ActivityTabFragment.kt | 108 ++++++++++++++---- .../activitytab/ActivityTabViewModel.kt | 21 ++-- .../wikipedia/history/db/HistoryEntryDao.kt | 4 +- .../readinglist/db/ReadingListPageDao.kt | 3 + app/src/main/res/values-qq/strings.xml | 2 + app/src/main/res/values/strings.xml | 2 + 6 files changed, 111 insertions(+), 29 deletions(-) diff --git a/app/src/main/java/org/wikipedia/activitytab/ActivityTabFragment.kt b/app/src/main/java/org/wikipedia/activitytab/ActivityTabFragment.kt index b530dceb527..c2312a2b3bb 100644 --- a/app/src/main/java/org/wikipedia/activitytab/ActivityTabFragment.kt +++ b/app/src/main/java/org/wikipedia/activitytab/ActivityTabFragment.kt @@ -1,14 +1,17 @@ package org.wikipedia.activitytab import android.os.Bundle +import android.text.format.DateFormat import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.background import androidx.compose.foundation.border +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize @@ -16,6 +19,8 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults import androidx.compose.material3.Icon @@ -60,6 +65,9 @@ import org.wikipedia.settings.Prefs import org.wikipedia.theme.Theme import org.wikipedia.util.UiState import org.wikipedia.views.imageservice.ImageService +import java.time.LocalDate +import java.time.LocalDateTime +import java.time.format.DateTimeFormatter import java.util.Locale class ActivityTabFragment : Fragment() { @@ -82,6 +90,11 @@ class ActivityTabFragment : Fragment() { } } + override fun onResume() { + super.onResume() + viewModel.loadReadingHistory() + } + @Composable fun ActivityTabScreen( userName: String, @@ -117,8 +130,6 @@ class ActivityTabFragment : Fragment() { ) ) - // Monthly insights - // Categories module } @@ -132,7 +143,7 @@ class ActivityTabFragment : Fragment() { // --- new column --- - // history module + // timeline module } } @@ -173,6 +184,8 @@ class ActivityTabFragment : Fragment() { } if (readingHistoryState is UiState.Success) { val readingHistory = readingHistoryState.data + val todayDate = LocalDate.now() + Text( text = stringResource( R.string.activity_tab_weekly_time_spent_hm, @@ -206,7 +219,10 @@ class ActivityTabFragment : Fragment() { ) Card( - modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp), + modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp) + .clickable { + // TODO + }, elevation = CardDefaults.cardElevation(defaultElevation = 0.dp), colors = CardDefaults.cardColors( containerColor = WikipediaTheme.colors.paperColor @@ -245,16 +261,23 @@ class ActivityTabFragment : Fragment() { Icon( modifier = Modifier.size(24.dp), painter = painterResource(R.drawable.ic_chevron_forward_white_24dp), - tint = WikipediaTheme.colors.primaryColor, + tint = WikipediaTheme.colors.secondaryColor, contentDescription = null ) } - Text( - text = "9:34 AM", - modifier = Modifier.padding(start = 16.dp, top = 2.dp), - fontSize = 12.sp, - color = WikipediaTheme.colors.secondaryColor - ) + if (readingHistory.lastArticleReadTime != null) { + Text( + text = if (todayDate == readingHistory.lastArticleReadTime.toLocalDate()) + readingHistory.lastArticleReadTime + .format(DateTimeFormatter.ofPattern(DateFormat.getBestDateTimePattern(Locale.getDefault(), "hhmm a"))) + else + readingHistory.lastArticleReadTime + .format(DateTimeFormatter.ofPattern(DateFormat.getBestDateTimePattern(Locale.getDefault(), "MMMM d"))), + modifier = Modifier.padding(start = 16.dp, top = 2.dp), + fontSize = 12.sp, + color = WikipediaTheme.colors.secondaryColor + ) + } Row( modifier = modifier.fillMaxWidth().padding(top = 6.dp, bottom = 16.dp) ) { @@ -278,7 +301,10 @@ class ActivityTabFragment : Fragment() { } Card( - modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp, vertical = 16.dp), + modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp, vertical = 16.dp) + .clickable { + // TODO + }, elevation = CardDefaults.cardElevation(defaultElevation = 0.dp), colors = CardDefaults.cardColors( containerColor = WikipediaTheme.colors.paperColor @@ -317,16 +343,23 @@ class ActivityTabFragment : Fragment() { Icon( modifier = Modifier.size(24.dp), painter = painterResource(R.drawable.ic_chevron_forward_white_24dp), - tint = WikipediaTheme.colors.primaryColor, + tint = WikipediaTheme.colors.secondaryColor, contentDescription = null ) } - Text( - text = "9:34 AM", - modifier = Modifier.padding(start = 16.dp, top = 2.dp), - fontSize = 12.sp, - color = WikipediaTheme.colors.secondaryColor - ) + if (readingHistory.lastArticleSavedTime != null) { + Text( + text = if (todayDate == readingHistory.lastArticleSavedTime.toLocalDate()) + readingHistory.lastArticleSavedTime + .format(DateTimeFormatter.ofPattern(DateFormat.getBestDateTimePattern(Locale.getDefault(), "hhmm a"))) + else + readingHistory.lastArticleSavedTime + .format(DateTimeFormatter.ofPattern(DateFormat.getBestDateTimePattern(Locale.getDefault(), "MMMM d"))), + modifier = Modifier.padding(start = 16.dp, top = 2.dp), + fontSize = 12.sp, + color = WikipediaTheme.colors.secondaryColor + ) + } Row( modifier = modifier.fillMaxWidth().padding(top = 6.dp, bottom = 16.dp) ) { @@ -398,7 +431,38 @@ class ActivityTabFragment : Fragment() { } } } - // empty state + + if (readingHistory.articlesReadThisMonth == 0L && readingHistory.articlesSavedThisMonth == 0L) { + Text( + text = stringResource(R.string.activity_tab_discover_encourage), + modifier = modifier.padding(horizontal = 16.dp, vertical = 8.dp), + fontWeight = FontWeight.Medium, + fontSize = 14.sp, + color = WikipediaTheme.colors.primaryColor + ) + Button( + modifier = modifier.padding(top = 8.dp, bottom = 16.dp), + contentPadding = PaddingValues(horizontal = 18.dp), + colors = ButtonDefaults.buttonColors( + containerColor = WikipediaTheme.colors.progressiveColor, + contentColor = WikipediaTheme.colors.paperColor, + ), + onClick = { + // TODO + }, + ) { + Icon( + modifier = Modifier.size(20.dp), + painter = painterResource(R.drawable.ic_globe), + tint = WikipediaTheme.colors.paperColor, + contentDescription = null + ) + Text( + modifier = Modifier.padding(start = 6.dp), + text = stringResource(R.string.activity_tab_explore_wikipedia) + ) + } + } } } @@ -412,8 +476,10 @@ class ActivityTabFragment : Fragment() { readingHistoryState = UiState.Success(ActivityTabViewModel.ReadingHistory( timeSpentThisWeek = 12345, articlesReadThisMonth = 123, + lastArticleReadTime = LocalDateTime.now(), articlesReadByWeek = listOf(0, 12, 34, 56), articlesSavedThisMonth = 23, + lastArticleSavedTime = LocalDateTime.of(2025, 6, 1, 12, 30), articlesSaved = listOf( PageTitle(text = "Psychology of art", wiki = site, thumbUrl = "foo.jpg", description = "Study of mental functions and behaviors", displayText = null), PageTitle(text = "Industrial design", wiki = site, thumbUrl = null, description = "Process of design applied to physical products", displayText = null), @@ -435,8 +501,10 @@ class ActivityTabFragment : Fragment() { readingHistoryState = UiState.Success(ActivityTabViewModel.ReadingHistory( timeSpentThisWeek = 0, articlesReadThisMonth = 0, + lastArticleReadTime = null, articlesReadByWeek = listOf(0, 0, 0, 0), articlesSavedThisMonth = 0, + lastArticleSavedTime = null, articlesSaved = emptyList() )) ) diff --git a/app/src/main/java/org/wikipedia/activitytab/ActivityTabViewModel.kt b/app/src/main/java/org/wikipedia/activitytab/ActivityTabViewModel.kt index 9fab175bef4..d6f25c732cb 100644 --- a/app/src/main/java/org/wikipedia/activitytab/ActivityTabViewModel.kt +++ b/app/src/main/java/org/wikipedia/activitytab/ActivityTabViewModel.kt @@ -15,6 +15,7 @@ import org.wikipedia.games.onthisday.OnThisDayGameViewModel import org.wikipedia.page.PageTitle import org.wikipedia.readinglist.database.ReadingListPage import org.wikipedia.util.UiState +import java.time.Instant import java.time.LocalDateTime import java.time.ZoneId import java.util.concurrent.TimeUnit @@ -47,20 +48,24 @@ class ActivityTabViewModel(savedStateHandle: SavedStateHandle) : ViewModel() { val articlesReadByWeek = mutableListOf() for (i in 1..4) { val weekAgo = now - weekInMillis - val articlesRead = AppDatabase.instance.historyEntryDao().getTotalEntriesBetween(weekAgo, weekAgo + weekInMillis) - articlesReadByWeek.add(articlesRead?.toInt() ?: 0) + val articlesRead = AppDatabase.instance.historyEntryDao().getHistoryCount(weekAgo, weekAgo + weekInMillis) + articlesReadByWeek.add(articlesRead) } + val mostRecentReadTime = AppDatabase.instance.historyEntryDao().getMostRecentEntry()?.timestamp?.toInstant()?.atZone(ZoneId.systemDefault())?.toLocalDateTime() val articlesSavedThisMonth = AppDatabase.instance.readingListPageDao().getTotalPagesSince(thirtyDaysAgo) ?: 0 val articlesSaved = AppDatabase.instance.readingListPageDao().getPagesSince(thirtyDaysAgo, 4) .map { ReadingListPage.toPageTitle(it) } + val mostRecentSaveTime = AppDatabase.instance.readingListPageDao().getMostRecentSavedPage()?.mtime?.let { Instant.ofEpochMilli(it) }?.atZone(ZoneId.systemDefault())?.toLocalDateTime() _readingHistoryState.value = UiState.Success(ReadingHistory( - totalTimeSpent, - articlesReadThisMonth, - articlesReadByWeek, - articlesSavedThisMonth, - articlesSaved) + timeSpentThisWeek = totalTimeSpent, + articlesReadThisMonth = articlesReadThisMonth, + lastArticleReadTime = mostRecentReadTime, + articlesReadByWeek = articlesReadByWeek, + articlesSavedThisMonth = articlesSavedThisMonth, + lastArticleSavedTime = mostRecentSaveTime, + articlesSaved = articlesSaved) ) } } @@ -68,8 +73,10 @@ class ActivityTabViewModel(savedStateHandle: SavedStateHandle) : ViewModel() { class ReadingHistory( val timeSpentThisWeek: Long, val articlesReadThisMonth: Long, + val lastArticleReadTime: LocalDateTime?, val articlesReadByWeek: List, val articlesSavedThisMonth: Long, + val lastArticleSavedTime: LocalDateTime?, val articlesSaved: List ) } diff --git a/app/src/main/java/org/wikipedia/history/db/HistoryEntryDao.kt b/app/src/main/java/org/wikipedia/history/db/HistoryEntryDao.kt index ac4713cbbdb..66bcde5f38b 100644 --- a/app/src/main/java/org/wikipedia/history/db/HistoryEntryDao.kt +++ b/app/src/main/java/org/wikipedia/history/db/HistoryEntryDao.kt @@ -42,8 +42,8 @@ interface HistoryEntryDao { @Query("SELECT COUNT(*) FROM HistoryEntry WHERE timestamp > :timestamp") suspend fun getTotalEntriesSince(timestamp: Long): Long? - @Query("SELECT COUNT(*) FROM HistoryEntry WHERE timestamp BETWEEN :timestampStart AND :timestampEnd") - suspend fun getTotalEntriesBetween(timestampStart: Long, timestampEnd: Long): Long? + @Query("SELECT * FROM HistoryEntry ORDER BY timestamp DESC LIMIT 1") + suspend fun getMostRecentEntry(): HistoryEntry? @Transaction suspend fun insert(entries: List) { diff --git a/app/src/main/java/org/wikipedia/readinglist/db/ReadingListPageDao.kt b/app/src/main/java/org/wikipedia/readinglist/db/ReadingListPageDao.kt index 4367cecedf8..a34b5a19854 100644 --- a/app/src/main/java/org/wikipedia/readinglist/db/ReadingListPageDao.kt +++ b/app/src/main/java/org/wikipedia/readinglist/db/ReadingListPageDao.kt @@ -92,6 +92,9 @@ interface ReadingListPageDao { @Query("SELECT * FROM ReadingListPage WHERE mtime > :timestamp ORDER BY mtime DESC LIMIT :limit") suspend fun getPagesSince(timestamp: Long, limit: Int): List + @Query("SELECT * FROM ReadingListPage ORDER BY mtime DESC LIMIT 1") + suspend fun getMostRecentSavedPage(): ReadingListPage? + fun getAllPagesToBeSaved() = getPagesByStatus(ReadingListPage.STATUS_QUEUE_FOR_SAVE, true) fun getAllPagesToBeForcedSave() = getPagesByStatus(ReadingListPage.STATUS_QUEUE_FOR_FORCED_SAVE, true) diff --git a/app/src/main/res/values-qq/strings.xml b/app/src/main/res/values-qq/strings.xml index 707b0a7d689..3215b1d729c 100644 --- a/app/src/main/res/values-qq/strings.xml +++ b/app/src/main/res/values-qq/strings.xml @@ -1177,6 +1177,8 @@ Label underneath the weekly time spent in the app. Label on a card that lists the number of articles read this month. Label on a card that lists the number of articles saved this month to a reading list. + Button label to go to the Explore screen. + Label encouraging the user to discover new articles. Title shown at the top of the activity for the file page. Button label to add image caption for the file. Button label to add image tags for the file. diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 7885f6d6cbc..2901961fc2a 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1196,6 +1196,8 @@ Time spent reading this week Articles read this month Articles saved this month + Explore Wikipedia + Discover something new to read From 075a5ea8e8143e2f76c6b8979b9a6ce1ad58eb1e Mon Sep 17 00:00:00 2001 From: Dmitry Brant Date: Thu, 14 Aug 2025 17:41:26 -0400 Subject: [PATCH 13/70] Tiny bar chart. --- .../activitytab/ActivityTabFragment.kt | 16 ++-- .../activitytab/ActivityTabViewModel.kt | 13 +-- .../compose/components/TinyBarChart.kt | 86 +++++++++++++++++++ .../wikipedia/history/db/HistoryEntryDao.kt | 2 +- .../readinglist/db/ReadingListPageDao.kt | 2 +- 5 files changed, 103 insertions(+), 16 deletions(-) create mode 100644 app/src/main/java/org/wikipedia/compose/components/TinyBarChart.kt diff --git a/app/src/main/java/org/wikipedia/activitytab/ActivityTabFragment.kt b/app/src/main/java/org/wikipedia/activitytab/ActivityTabFragment.kt index c2312a2b3bb..2127c5287f0 100644 --- a/app/src/main/java/org/wikipedia/activitytab/ActivityTabFragment.kt +++ b/app/src/main/java/org/wikipedia/activitytab/ActivityTabFragment.kt @@ -56,6 +56,7 @@ import coil3.compose.AsyncImage import org.wikipedia.R import org.wikipedia.auth.AccountUtil import org.wikipedia.compose.ComposeColors +import org.wikipedia.compose.components.TinyBarChart import org.wikipedia.compose.components.error.WikiErrorClickEvents import org.wikipedia.compose.theme.BaseTheme import org.wikipedia.compose.theme.WikipediaTheme @@ -289,13 +290,12 @@ class ActivityTabFragment : Fragment() { color = WikipediaTheme.colors.primaryColor ) Spacer(modifier = Modifier.weight(1f)) - Box( - modifier = Modifier.padding(end = 16.dp) - .size(width = 80.dp, height = 48.dp) - .background( - color = WikipediaTheme.colors.additionColor, - shape = RoundedCornerShape(2.dp) - ) + + TinyBarChart( + values = readingHistory.articlesReadByWeek, + modifier = Modifier.padding(end = 16.dp).size(72.dp, if (readingHistory.articlesReadThisMonth == 0) 32.dp else 48.dp), + minColor = ComposeColors.Gray300, + maxColor = ComposeColors.Green600 ) } } @@ -432,7 +432,7 @@ class ActivityTabFragment : Fragment() { } } - if (readingHistory.articlesReadThisMonth == 0L && readingHistory.articlesSavedThisMonth == 0L) { + if (readingHistory.articlesReadThisMonth == 0 && readingHistory.articlesSavedThisMonth == 0) { Text( text = stringResource(R.string.activity_tab_discover_encourage), modifier = modifier.padding(horizontal = 16.dp, vertical = 8.dp), diff --git a/app/src/main/java/org/wikipedia/activitytab/ActivityTabViewModel.kt b/app/src/main/java/org/wikipedia/activitytab/ActivityTabViewModel.kt index d6f25c732cb..bf9405d0257 100644 --- a/app/src/main/java/org/wikipedia/activitytab/ActivityTabViewModel.kt +++ b/app/src/main/java/org/wikipedia/activitytab/ActivityTabViewModel.kt @@ -40,14 +40,15 @@ class ActivityTabViewModel(savedStateHandle: SavedStateHandle) : ViewModel() { _readingHistoryState.value = UiState.Loading val now = LocalDateTime.now().atZone(ZoneId.systemDefault()).toInstant().toEpochMilli() val weekInMillis = TimeUnit.DAYS.toMillis(7) - val sevenDaysAgo = now - weekInMillis - val totalTimeSpent = AppDatabase.instance.historyEntryWithImageDao().getTimeSpentSinceTimeStamp(sevenDaysAgo) + var weekAgo = now - weekInMillis + val totalTimeSpent = AppDatabase.instance.historyEntryWithImageDao().getTimeSpentSinceTimeStamp(weekAgo) val thirtyDaysAgo = now - TimeUnit.DAYS.toMillis(30) val articlesReadThisMonth = AppDatabase.instance.historyEntryDao().getTotalEntriesSince(thirtyDaysAgo) ?: 0 val articlesReadByWeek = mutableListOf() - for (i in 1..4) { - val weekAgo = now - weekInMillis + articlesReadByWeek.add(AppDatabase.instance.historyEntryDao().getTotalEntriesSince(weekAgo) ?: 0) + for (i in 1..3) { + weekAgo -= weekInMillis val articlesRead = AppDatabase.instance.historyEntryDao().getHistoryCount(weekAgo, weekAgo + weekInMillis) articlesReadByWeek.add(articlesRead) } @@ -72,10 +73,10 @@ class ActivityTabViewModel(savedStateHandle: SavedStateHandle) : ViewModel() { class ReadingHistory( val timeSpentThisWeek: Long, - val articlesReadThisMonth: Long, + val articlesReadThisMonth: Int, val lastArticleReadTime: LocalDateTime?, val articlesReadByWeek: List, - val articlesSavedThisMonth: Long, + val articlesSavedThisMonth: Int, val lastArticleSavedTime: LocalDateTime?, val articlesSaved: List ) diff --git a/app/src/main/java/org/wikipedia/compose/components/TinyBarChart.kt b/app/src/main/java/org/wikipedia/compose/components/TinyBarChart.kt new file mode 100644 index 00000000000..be93bf42fba --- /dev/null +++ b/app/src/main/java/org/wikipedia/compose/components/TinyBarChart.kt @@ -0,0 +1,86 @@ +package org.wikipedia.compose.components + +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.CornerRadius +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import org.wikipedia.compose.theme.BaseTheme +import org.wikipedia.theme.Theme +import kotlin.math.max + +@Composable +fun TinyBarChart( + values: List, + modifier: Modifier = Modifier, + minColor: Color = Color.Gray, + maxColor: Color = Color.Green +) { + val maxValue = values.maxOrNull() ?: 1 + val minValue = 0 + val valueRange = max(maxValue - minValue, 1) + + Canvas( + modifier = modifier + ) { + val canvasWidth = size.width + val canvasHeight = size.height + val spacing = 6.dp.toPx() + val totalSpacing = spacing * (values.size - 1) + val barWidth = (canvasWidth - totalSpacing) / values.size + + values.forEachIndexed { index, value -> + val barHeight = max(if (maxValue > 0) { + (value.toFloat() / maxValue.toFloat()) * canvasHeight + } else { + 0f + }, 4.dp.toPx()) + + val colorProgress = if (valueRange > 0) { + (value - minValue).toFloat() / valueRange.toFloat() + } else { + 1f + } + + val barColor = Color( + red = minColor.red + (maxColor.red - minColor.red) * colorProgress, + green = minColor.green + (maxColor.green - minColor.green) * colorProgress, + blue = minColor.blue + (maxColor.blue - minColor.blue) * colorProgress, + alpha = minColor.alpha + (maxColor.alpha - minColor.alpha) * colorProgress + ) + + // Draw the bar from bottom up + drawRoundRect( + color = barColor, + topLeft = Offset(x = index * (barWidth + spacing), y = canvasHeight - barHeight), + size = Size(width = barWidth, height = barHeight), + cornerRadius = CornerRadius(2.dp.toPx(), 2.dp.toPx()) + ) + } + } +} + +@Preview(showBackground = true) +@Composable +private fun TinyBarChartPreview() { + BaseTheme( + currentTheme = Theme.LIGHT + ) { + Box( + modifier = Modifier + .fillMaxSize(), + contentAlignment = Alignment.Center + ) { + SearchEmptyView( + emptyTexTitle = "No languages found" + ) + } + } +} diff --git a/app/src/main/java/org/wikipedia/history/db/HistoryEntryDao.kt b/app/src/main/java/org/wikipedia/history/db/HistoryEntryDao.kt index 66bcde5f38b..2da4f3f57cf 100644 --- a/app/src/main/java/org/wikipedia/history/db/HistoryEntryDao.kt +++ b/app/src/main/java/org/wikipedia/history/db/HistoryEntryDao.kt @@ -40,7 +40,7 @@ interface HistoryEntryDao { suspend fun deleteBy(authority: String, lang: String, namespace: String?, apiTitle: String) @Query("SELECT COUNT(*) FROM HistoryEntry WHERE timestamp > :timestamp") - suspend fun getTotalEntriesSince(timestamp: Long): Long? + suspend fun getTotalEntriesSince(timestamp: Long): Int? @Query("SELECT * FROM HistoryEntry ORDER BY timestamp DESC LIMIT 1") suspend fun getMostRecentEntry(): HistoryEntry? diff --git a/app/src/main/java/org/wikipedia/readinglist/db/ReadingListPageDao.kt b/app/src/main/java/org/wikipedia/readinglist/db/ReadingListPageDao.kt index a34b5a19854..411657df402 100644 --- a/app/src/main/java/org/wikipedia/readinglist/db/ReadingListPageDao.kt +++ b/app/src/main/java/org/wikipedia/readinglist/db/ReadingListPageDao.kt @@ -87,7 +87,7 @@ interface ReadingListPageDao { fun getAllPagesToBeSynced(): List @Query("SELECT COUNT(*) FROM ReadingListPage WHERE mtime > :timestamp") - suspend fun getTotalPagesSince(timestamp: Long): Long? + suspend fun getTotalPagesSince(timestamp: Long): Int? @Query("SELECT * FROM ReadingListPage WHERE mtime > :timestamp ORDER BY mtime DESC LIMIT :limit") suspend fun getPagesSince(timestamp: Long, limit: Int): List From ad268a6bc249a33c9943676b1e2d16a2d51ae17c Mon Sep 17 00:00:00 2001 From: William Rai <48931640+Williamrai@users.noreply.github.com> Date: Thu, 14 Aug 2025 19:36:17 -0400 Subject: [PATCH 14/70] Activity tab categories module design (#5833) * - adds TopCategoriesView, separate loadCategories function and load function in onResume * - adds full views for top categories view - adds interaction, code fixes, ui updates and resource files * - adds click action, navigation logics - ui/code fixes * - ui fix * - code/design fixes * Tiny bar chart. * Yes. --------- Co-authored-by: Dmitry Brant --- .../activitytab/ActivityTabFragment.kt | 112 ++++++++++++------ .../activitytab/ActivityTabViewModel.kt | 30 +++-- .../activitytab/TopCategoriesView.kt | 110 +++++++++++++++++ .../compose/components/TinyBarChart.kt | 86 ++++++++++++++ .../wikipedia/history/db/HistoryEntryDao.kt | 2 +- .../java/org/wikipedia/main/MainFragment.kt | 7 +- .../readinglist/db/ReadingListPageDao.kt | 2 +- 7 files changed, 297 insertions(+), 52 deletions(-) create mode 100644 app/src/main/java/org/wikipedia/activitytab/TopCategoriesView.kt create mode 100644 app/src/main/java/org/wikipedia/compose/components/TinyBarChart.kt diff --git a/app/src/main/java/org/wikipedia/activitytab/ActivityTabFragment.kt b/app/src/main/java/org/wikipedia/activitytab/ActivityTabFragment.kt index c2312a2b3bb..972a249eb4d 100644 --- a/app/src/main/java/org/wikipedia/activitytab/ActivityTabFragment.kt +++ b/app/src/main/java/org/wikipedia/activitytab/ActivityTabFragment.kt @@ -18,6 +18,7 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults @@ -54,8 +55,12 @@ import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels import coil3.compose.AsyncImage import org.wikipedia.R +import org.wikipedia.activity.FragmentUtil.getCallback import org.wikipedia.auth.AccountUtil +import org.wikipedia.categories.CategoryActivity +import org.wikipedia.categories.db.Category import org.wikipedia.compose.ComposeColors +import org.wikipedia.compose.components.TinyBarChart import org.wikipedia.compose.components.error.WikiErrorClickEvents import org.wikipedia.compose.theme.BaseTheme import org.wikipedia.compose.theme.WikipediaTheme @@ -72,6 +77,10 @@ import java.util.Locale class ActivityTabFragment : Fragment() { + interface Callback { + fun onNavigateToReadingLists() + } + private val viewModel: ActivityTabViewModel by viewModels() override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { @@ -83,7 +92,7 @@ class ActivityTabFragment : Fragment() { BaseTheme { ActivityTabScreen( userName = AccountUtil.userName, - readingHistoryState = viewModel.readingHistoryState.collectAsState().value, + readingHistoryState = viewModel.readingHistoryState.collectAsState().value ) } } @@ -106,44 +115,48 @@ class ActivityTabFragment : Fragment() { .background(WikipediaTheme.colors.paperColor), containerColor = WikipediaTheme.colors.paperColor ) { paddingValues -> - Column( - modifier = Modifier - .fillMaxWidth() - .padding(paddingValues) - .background( - brush = Brush.verticalGradient( - colors = listOf( - WikipediaTheme.colors.paperColor, - WikipediaTheme.colors.additionColor + LazyColumn { + item { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(paddingValues) + .background( + brush = Brush.verticalGradient( + colors = listOf( + WikipediaTheme.colors.paperColor, + WikipediaTheme.colors.additionColor + ) + ) + ) + ) { + ReadingHistoryModule( + modifier = Modifier.align(Alignment.CenterHorizontally), + userName = userName, + readingHistoryState = readingHistoryState, + wikiErrorClickEvents = WikiErrorClickEvents( + retryClickListener = { + viewModel.loadReadingHistory() + } ) ) - ) - ) { - ReadingHistoryModule( - modifier = Modifier.align(Alignment.CenterHorizontally), - userName = userName, - readingHistoryState = readingHistoryState, - wikiErrorClickEvents = WikiErrorClickEvents( - retryClickListener = { - viewModel.loadReadingHistory() - } - ) - ) - // Categories module - } + // Categories module + } + } - // --- new column --- + // --- new column --- - // impact module + // impact module - // game module + // game module - // donation module + // donation module - // --- new column --- + // --- new column --- - // timeline module + // timeline module + } } } @@ -289,13 +302,12 @@ class ActivityTabFragment : Fragment() { color = WikipediaTheme.colors.primaryColor ) Spacer(modifier = Modifier.weight(1f)) - Box( - modifier = Modifier.padding(end = 16.dp) - .size(width = 80.dp, height = 48.dp) - .background( - color = WikipediaTheme.colors.additionColor, - shape = RoundedCornerShape(2.dp) - ) + + TinyBarChart( + values = readingHistory.articlesReadByWeek, + modifier = Modifier.padding(end = 16.dp).size(72.dp, if (readingHistory.articlesReadThisMonth == 0) 32.dp else 48.dp), + minColor = ComposeColors.Gray300, + maxColor = ComposeColors.Green600 ) } } @@ -432,7 +444,20 @@ class ActivityTabFragment : Fragment() { } } - if (readingHistory.articlesReadThisMonth == 0L && readingHistory.articlesSavedThisMonth == 0L) { + if (readingHistory.topCategories.isNotEmpty()) { + TopCategoriesView( + modifier = Modifier + .fillMaxWidth() + .padding(start = 16.dp, end = 16.dp, bottom = 16.dp), + categories = readingHistory.topCategories, + onClick = { + val pageTitle = viewModel.createPageTitleForCategory(it) + startActivity(CategoryActivity.newIntent(requireActivity(), pageTitle)) + } + ) + } + + if (readingHistory.articlesReadThisMonth == 0 && readingHistory.articlesSavedThisMonth == 0) { Text( text = stringResource(R.string.activity_tab_discover_encourage), modifier = modifier.padding(horizontal = 16.dp, vertical = 8.dp), @@ -486,6 +511,10 @@ class ActivityTabFragment : Fragment() { PageTitle(text = "Dufourspitze", wiki = site, thumbUrl = "foo.jpg", description = "Highest mountain in Switzerland", displayText = null), PageTitle(text = "Barack Obama", wiki = site, thumbUrl = "foo.jpg", description = "President of the United States from 2009 to 2017", displayText = null), PageTitle(text = "Octagon house", wiki = site, thumbUrl = "foo.jpg", description = "North American house style briefly popular in the 1850s", displayText = null) + ), + topCategories = listOf( + Category(2025, 1, "Category:Ancient history", "en", 1), + Category(2025, 1, "Category:World literature", "en", 1), ) )) ) @@ -505,7 +534,8 @@ class ActivityTabFragment : Fragment() { articlesReadByWeek = listOf(0, 0, 0, 0), articlesSavedThisMonth = 0, lastArticleSavedTime = null, - articlesSaved = emptyList() + articlesSaved = emptyList(), + topCategories = emptyList() )) ) } @@ -520,4 +550,8 @@ class ActivityTabFragment : Fragment() { } } } + + private fun callback(): Callback? { + return getCallback(this, Callback::class.java) + } } diff --git a/app/src/main/java/org/wikipedia/activitytab/ActivityTabViewModel.kt b/app/src/main/java/org/wikipedia/activitytab/ActivityTabViewModel.kt index d6f25c732cb..c075f75abbc 100644 --- a/app/src/main/java/org/wikipedia/activitytab/ActivityTabViewModel.kt +++ b/app/src/main/java/org/wikipedia/activitytab/ActivityTabViewModel.kt @@ -10,12 +10,14 @@ import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch import org.wikipedia.categories.db.Category import org.wikipedia.database.AppDatabase +import org.wikipedia.dataclient.WikiSite import org.wikipedia.donate.DonationResult import org.wikipedia.games.onthisday.OnThisDayGameViewModel import org.wikipedia.page.PageTitle import org.wikipedia.readinglist.database.ReadingListPage import org.wikipedia.util.UiState import java.time.Instant +import java.time.LocalDate import java.time.LocalDateTime import java.time.ZoneId import java.util.concurrent.TimeUnit @@ -27,8 +29,6 @@ class ActivityTabViewModel(savedStateHandle: SavedStateHandle) : ViewModel() { var gameStatistics: OnThisDayGameViewModel.GameStatistics? = null var donationResults: List = emptyList() - var topCategories: List = emptyList() - init { loadReadingHistory() } @@ -40,14 +40,15 @@ class ActivityTabViewModel(savedStateHandle: SavedStateHandle) : ViewModel() { _readingHistoryState.value = UiState.Loading val now = LocalDateTime.now().atZone(ZoneId.systemDefault()).toInstant().toEpochMilli() val weekInMillis = TimeUnit.DAYS.toMillis(7) - val sevenDaysAgo = now - weekInMillis - val totalTimeSpent = AppDatabase.instance.historyEntryWithImageDao().getTimeSpentSinceTimeStamp(sevenDaysAgo) + var weekAgo = now - weekInMillis + val totalTimeSpent = AppDatabase.instance.historyEntryWithImageDao().getTimeSpentSinceTimeStamp(weekAgo) val thirtyDaysAgo = now - TimeUnit.DAYS.toMillis(30) val articlesReadThisMonth = AppDatabase.instance.historyEntryDao().getTotalEntriesSince(thirtyDaysAgo) ?: 0 val articlesReadByWeek = mutableListOf() - for (i in 1..4) { - val weekAgo = now - weekInMillis + articlesReadByWeek.add(AppDatabase.instance.historyEntryDao().getTotalEntriesSince(weekAgo) ?: 0) + for (i in 1..3) { + weekAgo -= weekInMillis val articlesRead = AppDatabase.instance.historyEntryDao().getHistoryCount(weekAgo, weekAgo + weekInMillis) articlesReadByWeek.add(articlesRead) } @@ -58,6 +59,9 @@ class ActivityTabViewModel(savedStateHandle: SavedStateHandle) : ViewModel() { .map { ReadingListPage.toPageTitle(it) } val mostRecentSaveTime = AppDatabase.instance.readingListPageDao().getMostRecentSavedPage()?.mtime?.let { Instant.ofEpochMilli(it) }?.atZone(ZoneId.systemDefault())?.toLocalDateTime() + val currentDate = LocalDate.now() + val topCategories = AppDatabase.instance.categoryDao().getTopCategoriesByMonth(currentDate.year, currentDate.monthValue) + _readingHistoryState.value = UiState.Success(ReadingHistory( timeSpentThisWeek = totalTimeSpent, articlesReadThisMonth = articlesReadThisMonth, @@ -65,18 +69,24 @@ class ActivityTabViewModel(savedStateHandle: SavedStateHandle) : ViewModel() { articlesReadByWeek = articlesReadByWeek, articlesSavedThisMonth = articlesSavedThisMonth, lastArticleSavedTime = mostRecentSaveTime, - articlesSaved = articlesSaved) + articlesSaved = articlesSaved, + topCategories.take(3)) ) } } + fun createPageTitleForCategory(category: Category): PageTitle { + return PageTitle(title = category.title, wiki = WikiSite.forLanguageCode(category.lang)) + } + class ReadingHistory( val timeSpentThisWeek: Long, - val articlesReadThisMonth: Long, + val articlesReadThisMonth: Int, val lastArticleReadTime: LocalDateTime?, val articlesReadByWeek: List, - val articlesSavedThisMonth: Long, + val articlesSavedThisMonth: Int, val lastArticleSavedTime: LocalDateTime?, - val articlesSaved: List + val articlesSaved: List, + val topCategories: List ) } diff --git a/app/src/main/java/org/wikipedia/activitytab/TopCategoriesView.kt b/app/src/main/java/org/wikipedia/activitytab/TopCategoriesView.kt new file mode 100644 index 00000000000..5c0dcf07167 --- /dev/null +++ b/app/src/main/java/org/wikipedia/activitytab/TopCategoriesView.kt @@ -0,0 +1,110 @@ +package org.wikipedia.activitytab + +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import org.wikipedia.R +import org.wikipedia.categories.db.Category +import org.wikipedia.compose.components.WikiCard +import org.wikipedia.compose.theme.BaseTheme +import org.wikipedia.compose.theme.WikipediaTheme +import org.wikipedia.theme.Theme +import org.wikipedia.util.StringUtil + +@Composable +fun TopCategoriesView( + modifier: Modifier = Modifier, + categories: List, + onClick: (Category) -> Unit +) { + WikiCard( + modifier = modifier, + elevation = 0.dp, + border = BorderStroke( + width = 1.dp, + color = WikipediaTheme.colors.borderColor + ) + ) { + Column( + modifier = Modifier.padding(top = 16.dp) + ) { + Row( + modifier = Modifier.padding(horizontal = 16.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + Icon( + modifier = Modifier + .size(16.dp), + painter = painterResource(R.drawable.ic_category_black_24dp), + tint = WikipediaTheme.colors.primaryColor, + contentDescription = null + ) + Text( + text = "Top categories read this month", + style = MaterialTheme.typography.labelMedium, + color = WikipediaTheme.colors.primaryColor, + fontWeight = FontWeight.SemiBold + ) + } + + categories.forEachIndexed { index, value -> + Box( + modifier = Modifier + .fillMaxWidth() + .clickable(onClick = { onClick(value) }) + ) { + Text( + modifier = Modifier + .padding(horizontal = 32.dp, vertical = 16.dp), + text = StringUtil.removeNamespace(value.title), + style = MaterialTheme.typography.bodyLarge, + color = WikipediaTheme.colors.primaryColor + ) + } + + if (index < categories.size - 1) { + HorizontalDivider( + modifier = Modifier.padding(horizontal = 16.dp), + color = WikipediaTheme.colors.borderColor + ) + } + } + } + } +} + +@Preview +@Composable +private fun TopCategoriesViewPreview() { + BaseTheme( + currentTheme = Theme.LIGHT + ) { + TopCategoriesView( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + categories = listOf( + Category(2025, 1, "Category:Ancient History", "en", 1), + Category(2025, 1, "Category:World knowledge literature", "en", 1), + Category(2025, 1, "Category:Random stories of the Ancient Civilization", "en", 1), + ), + onClick = {} + ) + } +} diff --git a/app/src/main/java/org/wikipedia/compose/components/TinyBarChart.kt b/app/src/main/java/org/wikipedia/compose/components/TinyBarChart.kt new file mode 100644 index 00000000000..be93bf42fba --- /dev/null +++ b/app/src/main/java/org/wikipedia/compose/components/TinyBarChart.kt @@ -0,0 +1,86 @@ +package org.wikipedia.compose.components + +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.CornerRadius +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import org.wikipedia.compose.theme.BaseTheme +import org.wikipedia.theme.Theme +import kotlin.math.max + +@Composable +fun TinyBarChart( + values: List, + modifier: Modifier = Modifier, + minColor: Color = Color.Gray, + maxColor: Color = Color.Green +) { + val maxValue = values.maxOrNull() ?: 1 + val minValue = 0 + val valueRange = max(maxValue - minValue, 1) + + Canvas( + modifier = modifier + ) { + val canvasWidth = size.width + val canvasHeight = size.height + val spacing = 6.dp.toPx() + val totalSpacing = spacing * (values.size - 1) + val barWidth = (canvasWidth - totalSpacing) / values.size + + values.forEachIndexed { index, value -> + val barHeight = max(if (maxValue > 0) { + (value.toFloat() / maxValue.toFloat()) * canvasHeight + } else { + 0f + }, 4.dp.toPx()) + + val colorProgress = if (valueRange > 0) { + (value - minValue).toFloat() / valueRange.toFloat() + } else { + 1f + } + + val barColor = Color( + red = minColor.red + (maxColor.red - minColor.red) * colorProgress, + green = minColor.green + (maxColor.green - minColor.green) * colorProgress, + blue = minColor.blue + (maxColor.blue - minColor.blue) * colorProgress, + alpha = minColor.alpha + (maxColor.alpha - minColor.alpha) * colorProgress + ) + + // Draw the bar from bottom up + drawRoundRect( + color = barColor, + topLeft = Offset(x = index * (barWidth + spacing), y = canvasHeight - barHeight), + size = Size(width = barWidth, height = barHeight), + cornerRadius = CornerRadius(2.dp.toPx(), 2.dp.toPx()) + ) + } + } +} + +@Preview(showBackground = true) +@Composable +private fun TinyBarChartPreview() { + BaseTheme( + currentTheme = Theme.LIGHT + ) { + Box( + modifier = Modifier + .fillMaxSize(), + contentAlignment = Alignment.Center + ) { + SearchEmptyView( + emptyTexTitle = "No languages found" + ) + } + } +} diff --git a/app/src/main/java/org/wikipedia/history/db/HistoryEntryDao.kt b/app/src/main/java/org/wikipedia/history/db/HistoryEntryDao.kt index 66bcde5f38b..2da4f3f57cf 100644 --- a/app/src/main/java/org/wikipedia/history/db/HistoryEntryDao.kt +++ b/app/src/main/java/org/wikipedia/history/db/HistoryEntryDao.kt @@ -40,7 +40,7 @@ interface HistoryEntryDao { suspend fun deleteBy(authority: String, lang: String, namespace: String?, apiTitle: String) @Query("SELECT COUNT(*) FROM HistoryEntry WHERE timestamp > :timestamp") - suspend fun getTotalEntriesSince(timestamp: Long): Long? + suspend fun getTotalEntriesSince(timestamp: Long): Int? @Query("SELECT * FROM HistoryEntry ORDER BY timestamp DESC LIMIT 1") suspend fun getMostRecentEntry(): HistoryEntry? diff --git a/app/src/main/java/org/wikipedia/main/MainFragment.kt b/app/src/main/java/org/wikipedia/main/MainFragment.kt index 1a4d91cc96b..cc93e14de29 100644 --- a/app/src/main/java/org/wikipedia/main/MainFragment.kt +++ b/app/src/main/java/org/wikipedia/main/MainFragment.kt @@ -38,6 +38,7 @@ import org.wikipedia.WikipediaApp import org.wikipedia.activity.BaseActivity import org.wikipedia.activity.FragmentUtil.getCallback import org.wikipedia.activitytab.ActivityTabABTest +import org.wikipedia.activitytab.ActivityTabFragment import org.wikipedia.analytics.eventplatform.ReadingListsAnalyticsHelper import org.wikipedia.auth.AccountUtil import org.wikipedia.commons.FilePageActivity @@ -92,7 +93,7 @@ import org.wikipedia.yearinreview.YearInReviewEntryDialog import java.io.File import java.util.concurrent.TimeUnit -class MainFragment : Fragment(), BackPressedHandler, MenuProvider, FeedFragment.Callback, HistoryFragment.Callback, MenuNavTabDialog.Callback { +class MainFragment : Fragment(), BackPressedHandler, MenuProvider, FeedFragment.Callback, HistoryFragment.Callback, MenuNavTabDialog.Callback, ActivityTabFragment.Callback { interface Callback { fun onTabChanged(tab: NavTab) fun updateToolbarElevation(elevate: Boolean) @@ -612,6 +613,10 @@ class MainFragment : Fragment(), BackPressedHandler, MenuProvider, FeedFragment. return getCallback(this, Callback::class.java) } + override fun onNavigateToReadingLists() { + goToTab(NavTab.READING_LISTS) + } + companion object { // Actually shows on the 4th time of using the app. The Pref.incrementExploreFeedVisitCount() gets call after MainFragment.onResume() private const val SHOW_EDITS_SNACKBAR_COUNT = 3 diff --git a/app/src/main/java/org/wikipedia/readinglist/db/ReadingListPageDao.kt b/app/src/main/java/org/wikipedia/readinglist/db/ReadingListPageDao.kt index a34b5a19854..411657df402 100644 --- a/app/src/main/java/org/wikipedia/readinglist/db/ReadingListPageDao.kt +++ b/app/src/main/java/org/wikipedia/readinglist/db/ReadingListPageDao.kt @@ -87,7 +87,7 @@ interface ReadingListPageDao { fun getAllPagesToBeSynced(): List @Query("SELECT COUNT(*) FROM ReadingListPage WHERE mtime > :timestamp") - suspend fun getTotalPagesSince(timestamp: Long): Long? + suspend fun getTotalPagesSince(timestamp: Long): Int? @Query("SELECT * FROM ReadingListPage WHERE mtime > :timestamp ORDER BY mtime DESC LIMIT :limit") suspend fun getPagesSince(timestamp: Long, limit: Int): List From 036a1062afe86bb7397d58147aeec5868e8a1f36 Mon Sep 17 00:00:00 2001 From: Dmitry Brant Date: Thu, 14 Aug 2025 19:47:40 -0400 Subject: [PATCH 15/70] Wire it up. --- .../activitytab/ActivityTabFragment.kt | 25 ++++++++++++++----- .../java/org/wikipedia/main/MainFragment.kt | 4 +-- 2 files changed, 21 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/org/wikipedia/activitytab/ActivityTabFragment.kt b/app/src/main/java/org/wikipedia/activitytab/ActivityTabFragment.kt index 972a249eb4d..c671dc637b2 100644 --- a/app/src/main/java/org/wikipedia/activitytab/ActivityTabFragment.kt +++ b/app/src/main/java/org/wikipedia/activitytab/ActivityTabFragment.kt @@ -65,6 +65,7 @@ import org.wikipedia.compose.components.error.WikiErrorClickEvents import org.wikipedia.compose.theme.BaseTheme import org.wikipedia.compose.theme.WikipediaTheme import org.wikipedia.dataclient.WikiSite +import org.wikipedia.navtab.NavTab import org.wikipedia.page.PageTitle import org.wikipedia.settings.Prefs import org.wikipedia.theme.Theme @@ -78,7 +79,7 @@ import java.util.Locale class ActivityTabFragment : Fragment() { interface Callback { - fun onNavigateToReadingLists() + fun onNavigateTo(navTab: NavTab) } private val viewModel: ActivityTabViewModel by viewModels() @@ -92,7 +93,10 @@ class ActivityTabFragment : Fragment() { BaseTheme { ActivityTabScreen( userName = AccountUtil.userName, - readingHistoryState = viewModel.readingHistoryState.collectAsState().value + readingHistoryState = viewModel.readingHistoryState.collectAsState().value, + onArticlesReadClick = { callback()?.onNavigateTo(NavTab.SEARCH) }, + onArticlesSavedClick = { callback()?.onNavigateTo(NavTab.READING_LISTS) }, + onExploreClick = { callback()?.onNavigateTo(NavTab.EXPLORE) } ) } } @@ -107,7 +111,10 @@ class ActivityTabFragment : Fragment() { @Composable fun ActivityTabScreen( userName: String, - readingHistoryState: UiState + readingHistoryState: UiState, + onArticlesReadClick: () -> Unit = {}, + onArticlesSavedClick: () -> Unit = {}, + onExploreClick: () -> Unit = {}, ) { Scaffold( modifier = Modifier @@ -134,6 +141,9 @@ class ActivityTabFragment : Fragment() { modifier = Modifier.align(Alignment.CenterHorizontally), userName = userName, readingHistoryState = readingHistoryState, + onArticlesReadClick = onArticlesReadClick, + onArticlesSavedClick = onArticlesSavedClick, + onExploreClick = onExploreClick, wikiErrorClickEvents = WikiErrorClickEvents( retryClickListener = { viewModel.loadReadingHistory() @@ -166,6 +176,9 @@ class ActivityTabFragment : Fragment() { modifier: Modifier, userName: String, readingHistoryState: UiState, + onArticlesReadClick: () -> Unit = {}, + onArticlesSavedClick: () -> Unit = {}, + onExploreClick: () -> Unit = {}, wikiErrorClickEvents: WikiErrorClickEvents? = null ) { Text( @@ -234,7 +247,7 @@ class ActivityTabFragment : Fragment() { Card( modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp) .clickable { - // TODO + onArticlesReadClick() }, elevation = CardDefaults.cardElevation(defaultElevation = 0.dp), colors = CardDefaults.cardColors( @@ -315,7 +328,7 @@ class ActivityTabFragment : Fragment() { Card( modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp, vertical = 16.dp) .clickable { - // TODO + onArticlesSavedClick() }, elevation = CardDefaults.cardElevation(defaultElevation = 0.dp), colors = CardDefaults.cardColors( @@ -473,7 +486,7 @@ class ActivityTabFragment : Fragment() { contentColor = WikipediaTheme.colors.paperColor, ), onClick = { - // TODO + onExploreClick() }, ) { Icon( diff --git a/app/src/main/java/org/wikipedia/main/MainFragment.kt b/app/src/main/java/org/wikipedia/main/MainFragment.kt index cc93e14de29..a0227726c19 100644 --- a/app/src/main/java/org/wikipedia/main/MainFragment.kt +++ b/app/src/main/java/org/wikipedia/main/MainFragment.kt @@ -613,8 +613,8 @@ class MainFragment : Fragment(), BackPressedHandler, MenuProvider, FeedFragment. return getCallback(this, Callback::class.java) } - override fun onNavigateToReadingLists() { - goToTab(NavTab.READING_LISTS) + override fun onNavigateTo(navTab: NavTab) { + goToTab(navTab) } companion object { From b89e7c48b9f5120205b481bd8827bb29eb47c041 Mon Sep 17 00:00:00 2001 From: Cooltey Feng Date: Fri, 15 Aug 2025 04:53:01 -0700 Subject: [PATCH 16/70] Activity Tab: Donation Module (#5834) * Activity Tab: Donation Module * Fix lint and update branch * Set up strings and rename classes * Update preview * Update preview again --- .../activitytab/ActivityTabFragment.kt | 41 ++++++ .../activitytab/ActivityTabViewModel.kt | 32 +++- .../wikipedia/activitytab/DonationModule.kt | 138 ++++++++++++++++++ .../drawable/outline_credit_card_heart_24.xml | 5 + app/src/main/res/values-qq/strings.xml | 3 + app/src/main/res/values/strings.xml | 3 + 6 files changed, 218 insertions(+), 4 deletions(-) create mode 100644 app/src/main/java/org/wikipedia/activitytab/DonationModule.kt create mode 100644 app/src/main/res/drawable/outline_credit_card_heart_24.xml diff --git a/app/src/main/java/org/wikipedia/activitytab/ActivityTabFragment.kt b/app/src/main/java/org/wikipedia/activitytab/ActivityTabFragment.kt index c671dc637b2..c5a7fe7d935 100644 --- a/app/src/main/java/org/wikipedia/activitytab/ActivityTabFragment.kt +++ b/app/src/main/java/org/wikipedia/activitytab/ActivityTabFragment.kt @@ -55,6 +55,7 @@ import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels import coil3.compose.AsyncImage import org.wikipedia.R +import org.wikipedia.activity.BaseActivity import org.wikipedia.activity.FragmentUtil.getCallback import org.wikipedia.auth.AccountUtil import org.wikipedia.categories.CategoryActivity @@ -93,6 +94,7 @@ class ActivityTabFragment : Fragment() { BaseTheme { ActivityTabScreen( userName = AccountUtil.userName, + donationUiState = viewModel.donationUiState.collectAsState().value, readingHistoryState = viewModel.readingHistoryState.collectAsState().value, onArticlesReadClick = { callback()?.onNavigateTo(NavTab.SEARCH) }, onArticlesSavedClick = { callback()?.onNavigateTo(NavTab.READING_LISTS) }, @@ -111,6 +113,7 @@ class ActivityTabFragment : Fragment() { @Composable fun ActivityTabScreen( userName: String, + donationUiState: UiState, readingHistoryState: UiState, onArticlesReadClick: () -> Unit = {}, onArticlesSavedClick: () -> Unit = {}, @@ -155,6 +158,42 @@ class ActivityTabFragment : Fragment() { } } + item { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(paddingValues) + .background( + brush = Brush.verticalGradient( + colors = listOf( + WikipediaTheme.colors.paperColor, + WikipediaTheme.colors.additionColor + ) + ) + ) + ) { + if (donationUiState is UiState.Success) { + // TODO: default is off. Handle this when building the configuration screen. + DonationModule( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 16.dp, horizontal = 16.dp), + uiState = donationUiState, + wikiErrorClickEvents = WikiErrorClickEvents( + retryClickListener = { + viewModel.loadDonationResults() + } + ), + onClick = { + (requireActivity() as? BaseActivity)?.launchDonateDialog( + campaignId = ActivityTabViewModel.CAMPAIGN_ID + ) + } + ) + } + } + } + // --- new column --- // impact module @@ -511,6 +550,7 @@ class ActivityTabFragment : Fragment() { BaseTheme(currentTheme = Theme.LIGHT) { ActivityTabScreen( userName = "User", + donationUiState = UiState.Success("5 days ago"), readingHistoryState = UiState.Success(ActivityTabViewModel.ReadingHistory( timeSpentThisWeek = 12345, articlesReadThisMonth = 123, @@ -540,6 +580,7 @@ class ActivityTabFragment : Fragment() { BaseTheme(currentTheme = Theme.LIGHT) { ActivityTabScreen( userName = "User", + donationUiState = UiState.Success("5 days ago"), readingHistoryState = UiState.Success(ActivityTabViewModel.ReadingHistory( timeSpentThisWeek = 0, articlesReadThisMonth = 0, diff --git a/app/src/main/java/org/wikipedia/activitytab/ActivityTabViewModel.kt b/app/src/main/java/org/wikipedia/activitytab/ActivityTabViewModel.kt index c075f75abbc..056cb548ece 100644 --- a/app/src/main/java/org/wikipedia/activitytab/ActivityTabViewModel.kt +++ b/app/src/main/java/org/wikipedia/activitytab/ActivityTabViewModel.kt @@ -1,5 +1,6 @@ package org.wikipedia.activitytab +import android.text.format.DateUtils import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope @@ -11,26 +12,27 @@ import kotlinx.coroutines.launch import org.wikipedia.categories.db.Category import org.wikipedia.database.AppDatabase import org.wikipedia.dataclient.WikiSite -import org.wikipedia.donate.DonationResult -import org.wikipedia.games.onthisday.OnThisDayGameViewModel import org.wikipedia.page.PageTitle import org.wikipedia.readinglist.database.ReadingListPage +import org.wikipedia.settings.Prefs import org.wikipedia.util.UiState import java.time.Instant import java.time.LocalDate import java.time.LocalDateTime import java.time.ZoneId +import java.time.ZoneOffset import java.util.concurrent.TimeUnit class ActivityTabViewModel(savedStateHandle: SavedStateHandle) : ViewModel() { private val _readingHistoryState = MutableStateFlow>(UiState.Loading) val readingHistoryState: StateFlow> = _readingHistoryState.asStateFlow() - var gameStatistics: OnThisDayGameViewModel.GameStatistics? = null - var donationResults: List = emptyList() + private val _donationUiState = MutableStateFlow>(UiState.Loading) + val donationUiState: StateFlow> = _donationUiState.asStateFlow() init { loadReadingHistory() + loadDonationResults() } fun loadReadingHistory() { @@ -75,6 +77,24 @@ class ActivityTabViewModel(savedStateHandle: SavedStateHandle) : ViewModel() { } } + fun loadDonationResults() { + viewModelScope.launch(CoroutineExceptionHandler { _, throwable -> + _donationUiState.value = UiState.Error(throwable) + }) { + _donationUiState.value = UiState.Loading + val lastDonationTime = Prefs.donationResults.lastOrNull()?.dateTime?.let { + val timestampInLong = LocalDateTime.parse(it).toInstant(ZoneOffset.UTC).epochSecond + val relativeTime = DateUtils.getRelativeTimeSpanString( + timestampInLong * 1000, // Convert seconds to milliseconds + System.currentTimeMillis(), + 0L + ) + return@let relativeTime.toString() + } + _donationUiState.value = UiState.Success(lastDonationTime) + } + } + fun createPageTitleForCategory(category: Category): PageTitle { return PageTitle(title = category.title, wiki = WikiSite.forLanguageCode(category.lang)) } @@ -89,4 +109,8 @@ class ActivityTabViewModel(savedStateHandle: SavedStateHandle) : ViewModel() { val articlesSaved: List, val topCategories: List ) + + companion object { + const val CAMPAIGN_ID = "appmenu_activity" + } } diff --git a/app/src/main/java/org/wikipedia/activitytab/DonationModule.kt b/app/src/main/java/org/wikipedia/activitytab/DonationModule.kt new file mode 100644 index 00000000000..fe8a31183f8 --- /dev/null +++ b/app/src/main/java/org/wikipedia/activitytab/DonationModule.kt @@ -0,0 +1,138 @@ +package org.wikipedia.activitytab + +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +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.foundation.layout.size +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import org.wikipedia.R +import org.wikipedia.compose.components.WikiCard +import org.wikipedia.compose.components.error.WikiErrorClickEvents +import org.wikipedia.compose.components.error.WikiErrorView +import org.wikipedia.compose.theme.BaseTheme +import org.wikipedia.compose.theme.WikipediaTheme +import org.wikipedia.theme.Theme +import org.wikipedia.util.UiState + +@Composable +fun DonationModule( + modifier: Modifier = Modifier, + uiState: UiState, + wikiErrorClickEvents: WikiErrorClickEvents? = null, + onClick: (() -> Unit)? = null +) { + WikiCard( + modifier = modifier + .clickable(onClick = { onClick?.invoke() }), + elevation = 0.dp, + border = BorderStroke( + width = 1.dp, + color = WikipediaTheme.colors.borderColor + ) + ) { + when (uiState) { + is UiState.Error -> { + Box( + modifier = modifier + .fillMaxWidth() + .height(200.dp), + contentAlignment = Alignment.Center + ) { + WikiErrorView( + modifier = Modifier + .fillMaxWidth(), + caught = uiState.error, + errorClickEvents = wikiErrorClickEvents + ) + } + } + + UiState.Loading -> { + Box( + modifier = modifier + .fillMaxWidth() + .height(200.dp) + ) { + CircularProgressIndicator( + modifier = Modifier + .align(Alignment.Center) + .padding(24.dp), + color = WikipediaTheme.colors.progressiveColor + ) + } + } + + is UiState.Success -> { + val lastDonationTime = uiState.data ?: stringResource(R.string.activity_tab_donation_unknown) + Column( + modifier = Modifier + .padding(horizontal = 16.dp) + .padding(top = 16.dp, bottom = 8.dp) + ) { + Row { + Icon( + modifier = Modifier + .size(16.dp), + painter = painterResource(R.drawable.outline_credit_card_heart_24), + tint = WikipediaTheme.colors.primaryColor, + contentDescription = null + ) + Text( + modifier = Modifier.padding(start = 16.dp), + text = stringResource(R.string.activity_tab_donation_last_donation), + style = MaterialTheme.typography.labelMedium, + color = WikipediaTheme.colors.primaryColor, + fontWeight = FontWeight.SemiBold + ) + Text( + modifier = Modifier.padding(start = 4.dp, end = 16.dp), + text = stringResource(R.string.activity_tab_donation_in_app), + style = MaterialTheme.typography.bodySmall, + color = WikipediaTheme.colors.primaryColor + ) + } + + Text( + modifier = Modifier.padding(top = 16.dp, bottom = 8.dp), + text = lastDonationTime, + style = MaterialTheme.typography.titleLarge, + color = WikipediaTheme.colors.progressiveColor, + fontWeight = FontWeight.Medium + ) + } + } + } + } +} + +@Preview +@Composable +private fun DonationViewPreview() { + BaseTheme( + currentTheme = Theme.LIGHT + ) { + DonationModule( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + uiState = UiState.Success("5 days ago"), + onClick = {} + ) + } +} diff --git a/app/src/main/res/drawable/outline_credit_card_heart_24.xml b/app/src/main/res/drawable/outline_credit_card_heart_24.xml new file mode 100644 index 00000000000..ce58e574488 --- /dev/null +++ b/app/src/main/res/drawable/outline_credit_card_heart_24.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/values-qq/strings.xml b/app/src/main/res/values-qq/strings.xml index 3215b1d729c..25c98fe1a6d 100644 --- a/app/src/main/res/values-qq/strings.xml +++ b/app/src/main/res/values-qq/strings.xml @@ -1179,6 +1179,9 @@ Label on a card that lists the number of articles saved this month to a reading list. Button label to go to the Explore screen. Label encouraging the user to discover new articles. + Label on card that shows the last donation time. + Subtitle of the donation card that indicates last donation. + Subtitle of the donation card that indicates in-app donation. Title shown at the top of the activity for the file page. Button label to add image caption for the file. Button label to add image tags for the file. diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 2901961fc2a..8626892bf07 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1198,6 +1198,9 @@ Articles saved this month Explore Wikipedia Discover something new to read + Unknown + Last donation + in app From 96d53fbc74f350eba623ddc006cffdaed3c3c2f2 Mon Sep 17 00:00:00 2001 From: Dmitry Brant Date: Fri, 15 Aug 2025 08:35:40 -0400 Subject: [PATCH 17/70] Keep tightening. --- .../activitytab/ActivityTabFragment.kt | 115 +++++++++--------- .../activitytab/ActivityTabViewModel.kt | 2 + .../wikipedia/activitytab/DonationModule.kt | 29 ++--- .../activitytab/TopCategoriesView.kt | 13 +- app/src/main/res/values-qq/strings.xml | 1 - app/src/main/res/values/strings.xml | 3 +- 6 files changed, 75 insertions(+), 88 deletions(-) diff --git a/app/src/main/java/org/wikipedia/activitytab/ActivityTabFragment.kt b/app/src/main/java/org/wikipedia/activitytab/ActivityTabFragment.kt index c5a7fe7d935..1b460d10f23 100644 --- a/app/src/main/java/org/wikipedia/activitytab/ActivityTabFragment.kt +++ b/app/src/main/java/org/wikipedia/activitytab/ActivityTabFragment.kt @@ -9,6 +9,7 @@ import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.background import androidx.compose.foundation.border import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues @@ -24,7 +25,9 @@ import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults +import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -41,7 +44,6 @@ import androidx.compose.ui.platform.ComposeView import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign @@ -78,7 +80,6 @@ import java.time.format.DateTimeFormatter import java.util.Locale class ActivityTabFragment : Fragment() { - interface Callback { fun onNavigateTo(navTab: NavTab) } @@ -94,8 +95,8 @@ class ActivityTabFragment : Fragment() { BaseTheme { ActivityTabScreen( userName = AccountUtil.userName, - donationUiState = viewModel.donationUiState.collectAsState().value, readingHistoryState = viewModel.readingHistoryState.collectAsState().value, + donationUiState = viewModel.donationUiState.collectAsState().value, onArticlesReadClick = { callback()?.onNavigateTo(NavTab.SEARCH) }, onArticlesSavedClick = { callback()?.onNavigateTo(NavTab.READING_LISTS) }, onExploreClick = { callback()?.onNavigateTo(NavTab.EXPLORE) } @@ -113,8 +114,8 @@ class ActivityTabFragment : Fragment() { @Composable fun ActivityTabScreen( userName: String, - donationUiState: UiState, readingHistoryState: UiState, + donationUiState: UiState, onArticlesReadClick: () -> Unit = {}, onArticlesSavedClick: () -> Unit = {}, onExploreClick: () -> Unit = {}, @@ -153,8 +154,6 @@ class ActivityTabFragment : Fragment() { } ) ) - - // Categories module } } @@ -172,6 +171,10 @@ class ActivityTabFragment : Fragment() { ) ) ) { + // impact module + + // game module + if (donationUiState is UiState.Success) { // TODO: default is off. Handle this when building the configuration screen. DonationModule( @@ -196,14 +199,6 @@ class ActivityTabFragment : Fragment() { // --- new column --- - // impact module - - // game module - - // donation module - - // --- new column --- - // timeline module } } @@ -224,7 +219,7 @@ class ActivityTabFragment : Fragment() { text = stringResource(R.string.activity_tab_user_reading, userName), modifier = modifier .padding(top = 16.dp), - fontSize = 22.sp, + style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.Medium, textAlign = TextAlign.Center, color = WikipediaTheme.colors.primaryColor @@ -247,7 +242,12 @@ class ActivityTabFragment : Fragment() { color = WikipediaTheme.colors.primaryColor ) } - if (readingHistoryState is UiState.Success) { + if (readingHistoryState is UiState.Loading) { + CircularProgressIndicator( + modifier = modifier.padding(vertical = 16.dp).size(48.dp), + color = WikipediaTheme.colors.progressiveColor + ) + } else if (readingHistoryState is UiState.Success) { val readingHistory = readingHistoryState.data val todayDate = LocalDate.now() @@ -257,12 +257,10 @@ class ActivityTabFragment : Fragment() { (readingHistory.timeSpentThisWeek / 3600), (readingHistory.timeSpentThisWeek % 60) ), - modifier = modifier - .padding(top = 12.dp), - fontSize = 32.sp, - fontWeight = FontWeight.W500, + modifier = modifier.padding(top = 12.dp), + fontWeight = FontWeight.Medium, textAlign = TextAlign.Center, - style = TextStyle( + style = MaterialTheme.typography.headlineLarge.copy( brush = Brush.linearGradient( colors = listOf( ComposeColors.Red700, @@ -278,7 +276,7 @@ class ActivityTabFragment : Fragment() { text = stringResource(R.string.activity_tab_weekly_time_spent), modifier = modifier .padding(top = 8.dp, bottom = 16.dp), - fontWeight = FontWeight.W500, + style = MaterialTheme.typography.labelLarge, textAlign = TextAlign.Center, color = WikipediaTheme.colors.primaryColor ) @@ -306,7 +304,8 @@ class ActivityTabFragment : Fragment() { modifier = modifier.weight(1f) ) { Row( - modifier = modifier.fillMaxWidth() + modifier = modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(8.dp) ) { Icon( modifier = Modifier.size(16.dp), @@ -316,12 +315,23 @@ class ActivityTabFragment : Fragment() { ) Text( text = stringResource(R.string.activity_tab_monthly_articles_read), - modifier = Modifier.padding(start = 8.dp), - fontWeight = FontWeight.Medium, - fontSize = 12.sp, + style = MaterialTheme.typography.labelMedium, color = WikipediaTheme.colors.primaryColor ) } + if (readingHistory.lastArticleReadTime != null) { + Text( + text = if (todayDate == readingHistory.lastArticleReadTime.toLocalDate()) + readingHistory.lastArticleReadTime + .format(DateTimeFormatter.ofPattern(DateFormat.getBestDateTimePattern(Locale.getDefault(), "hhmm a"))) + else + readingHistory.lastArticleReadTime + .format(DateTimeFormatter.ofPattern(DateFormat.getBestDateTimePattern(Locale.getDefault(), "MMMM d"))), + modifier = Modifier.padding(top = 4.dp), + style = MaterialTheme.typography.bodySmall, + color = WikipediaTheme.colors.secondaryColor + ) + } } Icon( modifier = Modifier.size(24.dp), @@ -330,27 +340,15 @@ class ActivityTabFragment : Fragment() { contentDescription = null ) } - if (readingHistory.lastArticleReadTime != null) { - Text( - text = if (todayDate == readingHistory.lastArticleReadTime.toLocalDate()) - readingHistory.lastArticleReadTime - .format(DateTimeFormatter.ofPattern(DateFormat.getBestDateTimePattern(Locale.getDefault(), "hhmm a"))) - else - readingHistory.lastArticleReadTime - .format(DateTimeFormatter.ofPattern(DateFormat.getBestDateTimePattern(Locale.getDefault(), "MMMM d"))), - modifier = Modifier.padding(start = 16.dp, top = 2.dp), - fontSize = 12.sp, - color = WikipediaTheme.colors.secondaryColor - ) - } + Row( modifier = modifier.fillMaxWidth().padding(top = 6.dp, bottom = 16.dp) ) { Text( text = readingHistory.articlesReadThisMonth.toString(), modifier = Modifier.padding(start = 16.dp).align(Alignment.Bottom), + style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.Medium, - fontSize = 22.sp, color = WikipediaTheme.colors.primaryColor ) Spacer(modifier = Modifier.weight(1f)) @@ -387,7 +385,8 @@ class ActivityTabFragment : Fragment() { modifier = modifier.weight(1f) ) { Row( - modifier = modifier.fillMaxWidth() + modifier = modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(8.dp) ) { Icon( modifier = Modifier.size(16.dp), @@ -397,12 +396,23 @@ class ActivityTabFragment : Fragment() { ) Text( text = stringResource(R.string.activity_tab_monthly_articles_saved), - modifier = Modifier.padding(start = 8.dp), - fontWeight = FontWeight.Medium, - fontSize = 12.sp, + style = MaterialTheme.typography.labelMedium, color = WikipediaTheme.colors.primaryColor ) } + if (readingHistory.lastArticleSavedTime != null) { + Text( + text = if (todayDate == readingHistory.lastArticleSavedTime.toLocalDate()) + readingHistory.lastArticleSavedTime + .format(DateTimeFormatter.ofPattern(DateFormat.getBestDateTimePattern(Locale.getDefault(), "hhmm a"))) + else + readingHistory.lastArticleSavedTime + .format(DateTimeFormatter.ofPattern(DateFormat.getBestDateTimePattern(Locale.getDefault(), "MMMM d"))), + modifier = Modifier.padding(top = 4.dp), + style = MaterialTheme.typography.bodySmall, + color = WikipediaTheme.colors.secondaryColor + ) + } } Icon( modifier = Modifier.size(24.dp), @@ -411,27 +421,14 @@ class ActivityTabFragment : Fragment() { contentDescription = null ) } - if (readingHistory.lastArticleSavedTime != null) { - Text( - text = if (todayDate == readingHistory.lastArticleSavedTime.toLocalDate()) - readingHistory.lastArticleSavedTime - .format(DateTimeFormatter.ofPattern(DateFormat.getBestDateTimePattern(Locale.getDefault(), "hhmm a"))) - else - readingHistory.lastArticleSavedTime - .format(DateTimeFormatter.ofPattern(DateFormat.getBestDateTimePattern(Locale.getDefault(), "MMMM d"))), - modifier = Modifier.padding(start = 16.dp, top = 2.dp), - fontSize = 12.sp, - color = WikipediaTheme.colors.secondaryColor - ) - } Row( modifier = modifier.fillMaxWidth().padding(top = 6.dp, bottom = 16.dp) ) { Text( text = readingHistory.articlesSavedThisMonth.toString(), modifier = Modifier.padding(start = 16.dp).align(Alignment.Bottom), + style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.Medium, - fontSize = 22.sp, color = WikipediaTheme.colors.primaryColor ) Spacer(modifier = Modifier.weight(1f)) @@ -580,7 +577,7 @@ class ActivityTabFragment : Fragment() { BaseTheme(currentTheme = Theme.LIGHT) { ActivityTabScreen( userName = "User", - donationUiState = UiState.Success("5 days ago"), + donationUiState = UiState.Success("Unknown"), readingHistoryState = UiState.Success(ActivityTabViewModel.ReadingHistory( timeSpentThisWeek = 0, articlesReadThisMonth = 0, diff --git a/app/src/main/java/org/wikipedia/activitytab/ActivityTabViewModel.kt b/app/src/main/java/org/wikipedia/activitytab/ActivityTabViewModel.kt index 056cb548ece..ecd71ffab44 100644 --- a/app/src/main/java/org/wikipedia/activitytab/ActivityTabViewModel.kt +++ b/app/src/main/java/org/wikipedia/activitytab/ActivityTabViewModel.kt @@ -5,6 +5,7 @@ import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import kotlinx.coroutines.CoroutineExceptionHandler +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow @@ -40,6 +41,7 @@ class ActivityTabViewModel(savedStateHandle: SavedStateHandle) : ViewModel() { _readingHistoryState.value = UiState.Error(throwable) }) { _readingHistoryState.value = UiState.Loading + delay(3000) val now = LocalDateTime.now().atZone(ZoneId.systemDefault()).toInstant().toEpochMilli() val weekInMillis = TimeUnit.DAYS.toMillis(7) var weekAgo = now - weekInMillis diff --git a/app/src/main/java/org/wikipedia/activitytab/DonationModule.kt b/app/src/main/java/org/wikipedia/activitytab/DonationModule.kt index fe8a31183f8..fd799ca6357 100644 --- a/app/src/main/java/org/wikipedia/activitytab/DonationModule.kt +++ b/app/src/main/java/org/wikipedia/activitytab/DonationModule.kt @@ -2,6 +2,7 @@ package org.wikipedia.activitytab import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -22,6 +23,7 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import org.wikipedia.R +import org.wikipedia.compose.components.HtmlText import org.wikipedia.compose.components.WikiCard import org.wikipedia.compose.components.error.WikiErrorClickEvents import org.wikipedia.compose.components.error.WikiErrorView @@ -81,35 +83,26 @@ fun DonationModule( is UiState.Success -> { val lastDonationTime = uiState.data ?: stringResource(R.string.activity_tab_donation_unknown) Column( - modifier = Modifier - .padding(horizontal = 16.dp) - .padding(top = 16.dp, bottom = 8.dp) + modifier = Modifier.padding(16.dp) ) { - Row { + Row( + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { Icon( - modifier = Modifier - .size(16.dp), + modifier = Modifier.size(16.dp), painter = painterResource(R.drawable.outline_credit_card_heart_24), tint = WikipediaTheme.colors.primaryColor, contentDescription = null ) - Text( - modifier = Modifier.padding(start = 16.dp), + HtmlText( text = stringResource(R.string.activity_tab_donation_last_donation), - style = MaterialTheme.typography.labelMedium, + style = MaterialTheme.typography.labelMedium.copy(fontWeight = FontWeight.Normal), color = WikipediaTheme.colors.primaryColor, - fontWeight = FontWeight.SemiBold - ) - Text( - modifier = Modifier.padding(start = 4.dp, end = 16.dp), - text = stringResource(R.string.activity_tab_donation_in_app), - style = MaterialTheme.typography.bodySmall, - color = WikipediaTheme.colors.primaryColor + lineHeight = MaterialTheme.typography.labelMedium.lineHeight ) } - Text( - modifier = Modifier.padding(top = 16.dp, bottom = 8.dp), + modifier = Modifier.padding(top = 16.dp), text = lastDonationTime, style = MaterialTheme.typography.titleLarge, color = WikipediaTheme.colors.progressiveColor, diff --git a/app/src/main/java/org/wikipedia/activitytab/TopCategoriesView.kt b/app/src/main/java/org/wikipedia/activitytab/TopCategoriesView.kt index 5c0dcf07167..70f1fc1a28d 100644 --- a/app/src/main/java/org/wikipedia/activitytab/TopCategoriesView.kt +++ b/app/src/main/java/org/wikipedia/activitytab/TopCategoriesView.kt @@ -16,7 +16,6 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource -import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import org.wikipedia.R @@ -49,8 +48,7 @@ fun TopCategoriesView( horizontalArrangement = Arrangement.spacedBy(8.dp) ) { Icon( - modifier = Modifier - .size(16.dp), + modifier = Modifier.size(16.dp), painter = painterResource(R.drawable.ic_category_black_24dp), tint = WikipediaTheme.colors.primaryColor, contentDescription = null @@ -58,8 +56,7 @@ fun TopCategoriesView( Text( text = "Top categories read this month", style = MaterialTheme.typography.labelMedium, - color = WikipediaTheme.colors.primaryColor, - fontWeight = FontWeight.SemiBold + color = WikipediaTheme.colors.primaryColor ) } @@ -100,9 +97,9 @@ private fun TopCategoriesViewPreview() { .fillMaxWidth() .padding(horizontal = 16.dp), categories = listOf( - Category(2025, 1, "Category:Ancient History", "en", 1), - Category(2025, 1, "Category:World knowledge literature", "en", 1), - Category(2025, 1, "Category:Random stories of the Ancient Civilization", "en", 1), + Category(2025, 1, "Category:Ancient history", "en", 1), + Category(2025, 1, "Category:World literature", "en", 1), + Category(2025, 1, "Category:Cat breeds originating in the United States", "en", 1), ), onClick = {} ) diff --git a/app/src/main/res/values-qq/strings.xml b/app/src/main/res/values-qq/strings.xml index 25c98fe1a6d..5172aba52e8 100644 --- a/app/src/main/res/values-qq/strings.xml +++ b/app/src/main/res/values-qq/strings.xml @@ -1181,7 +1181,6 @@ Label encouraging the user to discover new articles. Label on card that shows the last donation time. Subtitle of the donation card that indicates last donation. - Subtitle of the donation card that indicates in-app donation. Title shown at the top of the activity for the file page. Button label to add image caption for the file. Button label to add image tags for the file. diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 8626892bf07..e54ffaf6841 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1199,8 +1199,7 @@ Explore Wikipedia Discover something new to read Unknown - Last donation - in app + Last donation in app]]> From d49edcca97771938b5673deb12c8ee49fcaf6e2b Mon Sep 17 00:00:00 2001 From: Dmitry Brant Date: Fri, 15 Aug 2025 09:17:46 -0400 Subject: [PATCH 18/70] Refactor a bit, and add swipe-to-refresh. --- .../activitytab/ActivityTabFragment.kt | 539 ++++-------------- .../activitytab/ActivityTabViewModel.kt | 25 +- .../wikipedia/activitytab/DonationModule.kt | 96 ++-- .../activitytab/ReadingHistoryModule.kt | 429 ++++++++++++++ .../activitytab/TopCategoriesView.kt | 3 +- app/src/main/res/values-qq/strings.xml | 1 + app/src/main/res/values/strings.xml | 1 + 7 files changed, 576 insertions(+), 518 deletions(-) create mode 100644 app/src/main/java/org/wikipedia/activitytab/ReadingHistoryModule.kt diff --git a/app/src/main/java/org/wikipedia/activitytab/ActivityTabFragment.kt b/app/src/main/java/org/wikipedia/activitytab/ActivityTabFragment.kt index 1b460d10f23..fd0e19e6ff7 100644 --- a/app/src/main/java/org/wikipedia/activitytab/ActivityTabFragment.kt +++ b/app/src/main/java/org/wikipedia/activitytab/ActivityTabFragment.kt @@ -1,69 +1,40 @@ package org.wikipedia.activitytab import android.os.Bundle -import android.text.format.DateFormat import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.background -import androidx.compose.foundation.border -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.Button -import androidx.compose.material3.ButtonDefaults -import androidx.compose.material3.Card -import androidx.compose.material3.CardDefaults -import androidx.compose.material3.CircularProgressIndicator -import androidx.compose.material3.Icon -import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Scaffold -import androidx.compose.material3.Text +import androidx.compose.material3.pulltorefresh.PullToRefreshBox +import androidx.compose.material3.pulltorefresh.PullToRefreshDefaults.Indicator +import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState 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.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Brush -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.SolidColor -import androidx.compose.ui.graphics.painter.BrushPainter -import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.ComposeView -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.font.FontFamily -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.TextUnit -import androidx.compose.ui.unit.TextUnitType import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp import androidx.core.net.toUri import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels -import coil3.compose.AsyncImage -import org.wikipedia.R import org.wikipedia.activity.BaseActivity import org.wikipedia.activity.FragmentUtil.getCallback import org.wikipedia.auth.AccountUtil import org.wikipedia.categories.CategoryActivity import org.wikipedia.categories.db.Category -import org.wikipedia.compose.ComposeColors -import org.wikipedia.compose.components.TinyBarChart import org.wikipedia.compose.components.error.WikiErrorClickEvents import org.wikipedia.compose.theme.BaseTheme import org.wikipedia.compose.theme.WikipediaTheme @@ -73,11 +44,7 @@ import org.wikipedia.page.PageTitle import org.wikipedia.settings.Prefs import org.wikipedia.theme.Theme import org.wikipedia.util.UiState -import org.wikipedia.views.imageservice.ImageService -import java.time.LocalDate import java.time.LocalDateTime -import java.time.format.DateTimeFormatter -import java.util.Locale class ActivityTabFragment : Fragment() { interface Callback { @@ -96,10 +63,7 @@ class ActivityTabFragment : Fragment() { ActivityTabScreen( userName = AccountUtil.userName, readingHistoryState = viewModel.readingHistoryState.collectAsState().value, - donationUiState = viewModel.donationUiState.collectAsState().value, - onArticlesReadClick = { callback()?.onNavigateTo(NavTab.SEARCH) }, - onArticlesSavedClick = { callback()?.onNavigateTo(NavTab.READING_LISTS) }, - onExploreClick = { callback()?.onNavigateTo(NavTab.EXPLORE) } + donationUiState = viewModel.donationUiState.collectAsState().value ) } } @@ -111,14 +75,12 @@ class ActivityTabFragment : Fragment() { viewModel.loadReadingHistory() } + @OptIn(ExperimentalMaterial3Api::class) @Composable fun ActivityTabScreen( userName: String, readingHistoryState: UiState, - donationUiState: UiState, - onArticlesReadClick: () -> Unit = {}, - onArticlesSavedClick: () -> Unit = {}, - onExploreClick: () -> Unit = {}, + donationUiState: UiState ) { Scaffold( modifier = Modifier @@ -126,415 +88,108 @@ class ActivityTabFragment : Fragment() { .background(WikipediaTheme.colors.paperColor), containerColor = WikipediaTheme.colors.paperColor ) { paddingValues -> - LazyColumn { - item { - Column( - modifier = Modifier - .fillMaxWidth() - .padding(paddingValues) - .background( - brush = Brush.verticalGradient( - colors = listOf( - WikipediaTheme.colors.paperColor, - WikipediaTheme.colors.additionColor - ) - ) - ) - ) { - ReadingHistoryModule( - modifier = Modifier.align(Alignment.CenterHorizontally), - userName = userName, - readingHistoryState = readingHistoryState, - onArticlesReadClick = onArticlesReadClick, - onArticlesSavedClick = onArticlesSavedClick, - onExploreClick = onExploreClick, - wikiErrorClickEvents = WikiErrorClickEvents( - retryClickListener = { - viewModel.loadReadingHistory() - } - ) - ) - } - } - - item { - Column( - modifier = Modifier - .fillMaxWidth() - .padding(paddingValues) - .background( - brush = Brush.verticalGradient( - colors = listOf( - WikipediaTheme.colors.paperColor, - WikipediaTheme.colors.additionColor - ) - ) - ) - ) { - // impact module - - // game module - - if (donationUiState is UiState.Success) { - // TODO: default is off. Handle this when building the configuration screen. - DonationModule( - modifier = Modifier - .fillMaxWidth() - .padding(vertical = 16.dp, horizontal = 16.dp), - uiState = donationUiState, - wikiErrorClickEvents = WikiErrorClickEvents( - retryClickListener = { - viewModel.loadDonationResults() - } - ), - onClick = { - (requireActivity() as? BaseActivity)?.launchDonateDialog( - campaignId = ActivityTabViewModel.CAMPAIGN_ID - ) - } - ) - } - } - } - - // --- new column --- + var isRefreshing by remember { mutableStateOf(false) } + val state = rememberPullToRefreshState() - // timeline module + if (readingHistoryState is UiState.Success) { + isRefreshing = false } - } - } - - // @TODO: error view and handling - @Composable - fun ReadingHistoryModule( - modifier: Modifier, - userName: String, - readingHistoryState: UiState, - onArticlesReadClick: () -> Unit = {}, - onArticlesSavedClick: () -> Unit = {}, - onExploreClick: () -> Unit = {}, - wikiErrorClickEvents: WikiErrorClickEvents? = null - ) { - Text( - text = stringResource(R.string.activity_tab_user_reading, userName), - modifier = modifier - .padding(top = 16.dp), - style = MaterialTheme.typography.titleLarge, - fontWeight = FontWeight.Medium, - textAlign = TextAlign.Center, - color = WikipediaTheme.colors.primaryColor - ) - Box( - modifier = modifier - .padding(horizontal = 8.dp, vertical = 4.dp) - .background( - color = WikipediaTheme.colors.additionColor, - shape = RoundedCornerShape(8.dp) - ) - ) { - Text( - text = stringResource(R.string.activity_tab_on_wikipedia_android).uppercase(), - modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp), - fontSize = 11.sp, - fontFamily = FontFamily.Monospace, - letterSpacing = TextUnit(0.8f, TextUnitType.Sp), - textAlign = TextAlign.Center, - color = WikipediaTheme.colors.primaryColor - ) - } - if (readingHistoryState is UiState.Loading) { - CircularProgressIndicator( - modifier = modifier.padding(vertical = 16.dp).size(48.dp), - color = WikipediaTheme.colors.progressiveColor - ) - } else if (readingHistoryState is UiState.Success) { - val readingHistory = readingHistoryState.data - val todayDate = LocalDate.now() - - Text( - text = stringResource( - R.string.activity_tab_weekly_time_spent_hm, - (readingHistory.timeSpentThisWeek / 3600), - (readingHistory.timeSpentThisWeek % 60) - ), - modifier = modifier.padding(top = 12.dp), - fontWeight = FontWeight.Medium, - textAlign = TextAlign.Center, - style = MaterialTheme.typography.headlineLarge.copy( - brush = Brush.linearGradient( - colors = listOf( - ComposeColors.Red700, - ComposeColors.Orange500, - ComposeColors.Yellow500, - ComposeColors.Blue300 - ) - ) - ), - color = WikipediaTheme.colors.primaryColor - ) - Text( - text = stringResource(R.string.activity_tab_weekly_time_spent), - modifier = modifier - .padding(top = 8.dp, bottom = 16.dp), - style = MaterialTheme.typography.labelLarge, - textAlign = TextAlign.Center, - color = WikipediaTheme.colors.primaryColor - ) - - Card( - modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp) - .clickable { - onArticlesReadClick() - }, - elevation = CardDefaults.cardElevation(defaultElevation = 0.dp), - colors = CardDefaults.cardColors( - containerColor = WikipediaTheme.colors.paperColor - ), - border = BorderStroke( - width = 1.dp, - color = WikipediaTheme.colors.borderColor - ), - shape = RoundedCornerShape(12.dp) - ) { - Row( - modifier = modifier.fillMaxWidth() - .padding(top = 16.dp, start = 16.dp, end = 16.dp) - ) { - Column( - modifier = modifier.weight(1f) - ) { - Row( - modifier = modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.spacedBy(8.dp) - ) { - Icon( - modifier = Modifier.size(16.dp), - painter = painterResource(R.drawable.ic_newsstand_24), - tint = WikipediaTheme.colors.primaryColor, - contentDescription = null - ) - Text( - text = stringResource(R.string.activity_tab_monthly_articles_read), - style = MaterialTheme.typography.labelMedium, - color = WikipediaTheme.colors.primaryColor - ) - } - if (readingHistory.lastArticleReadTime != null) { - Text( - text = if (todayDate == readingHistory.lastArticleReadTime.toLocalDate()) - readingHistory.lastArticleReadTime - .format(DateTimeFormatter.ofPattern(DateFormat.getBestDateTimePattern(Locale.getDefault(), "hhmm a"))) - else - readingHistory.lastArticleReadTime - .format(DateTimeFormatter.ofPattern(DateFormat.getBestDateTimePattern(Locale.getDefault(), "MMMM d"))), - modifier = Modifier.padding(top = 4.dp), - style = MaterialTheme.typography.bodySmall, - color = WikipediaTheme.colors.secondaryColor - ) - } - } - Icon( - modifier = Modifier.size(24.dp), - painter = painterResource(R.drawable.ic_chevron_forward_white_24dp), - tint = WikipediaTheme.colors.secondaryColor, - contentDescription = null - ) - } - - Row( - modifier = modifier.fillMaxWidth().padding(top = 6.dp, bottom = 16.dp) - ) { - Text( - text = readingHistory.articlesReadThisMonth.toString(), - modifier = Modifier.padding(start = 16.dp).align(Alignment.Bottom), - style = MaterialTheme.typography.titleLarge, - fontWeight = FontWeight.Medium, - color = WikipediaTheme.colors.primaryColor - ) - Spacer(modifier = Modifier.weight(1f)) - TinyBarChart( - values = readingHistory.articlesReadByWeek, - modifier = Modifier.padding(end = 16.dp).size(72.dp, if (readingHistory.articlesReadThisMonth == 0) 32.dp else 48.dp), - minColor = ComposeColors.Gray300, - maxColor = ComposeColors.Green600 + PullToRefreshBox( + onRefresh = { + isRefreshing = true + viewModel.loadReadingHistory() + }, + isRefreshing = isRefreshing, + state = state, + indicator = { + Indicator( + state = state, + isRefreshing = isRefreshing, + modifier = Modifier.align(Alignment.TopCenter), + containerColor = WikipediaTheme.colors.paperColor, + color = WikipediaTheme.colors.progressiveColor ) } - } - - Card( - modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp, vertical = 16.dp) - .clickable { - onArticlesSavedClick() - }, - elevation = CardDefaults.cardElevation(defaultElevation = 0.dp), - colors = CardDefaults.cardColors( - containerColor = WikipediaTheme.colors.paperColor - ), - border = BorderStroke( - width = 1.dp, - color = WikipediaTheme.colors.borderColor - ), - shape = RoundedCornerShape(12.dp) ) { - Row( - modifier = modifier.fillMaxWidth() - .padding(top = 16.dp, start = 16.dp, end = 16.dp) - ) { - Column( - modifier = modifier.weight(1f) - ) { - Row( - modifier = modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.spacedBy(8.dp) + LazyColumn { + item { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(paddingValues) + .background( + brush = Brush.verticalGradient( + colors = listOf( + WikipediaTheme.colors.paperColor, + WikipediaTheme.colors.additionColor + ) + ) + ) ) { - Icon( - modifier = Modifier.size(16.dp), - painter = painterResource(R.drawable.ic_bookmark_border_white_24dp), - tint = WikipediaTheme.colors.primaryColor, - contentDescription = null - ) - Text( - text = stringResource(R.string.activity_tab_monthly_articles_saved), - style = MaterialTheme.typography.labelMedium, - color = WikipediaTheme.colors.primaryColor - ) - } - if (readingHistory.lastArticleSavedTime != null) { - Text( - text = if (todayDate == readingHistory.lastArticleSavedTime.toLocalDate()) - readingHistory.lastArticleSavedTime - .format(DateTimeFormatter.ofPattern(DateFormat.getBestDateTimePattern(Locale.getDefault(), "hhmm a"))) - else - readingHistory.lastArticleSavedTime - .format(DateTimeFormatter.ofPattern(DateFormat.getBestDateTimePattern(Locale.getDefault(), "MMMM d"))), - modifier = Modifier.padding(top = 4.dp), - style = MaterialTheme.typography.bodySmall, - color = WikipediaTheme.colors.secondaryColor + ReadingHistoryModule( + modifier = Modifier.align(Alignment.CenterHorizontally), + userName = userName, + readingHistoryState = readingHistoryState, + onArticlesReadClick = { callback()?.onNavigateTo(NavTab.SEARCH) }, + onArticlesSavedClick = { callback()?.onNavigateTo(NavTab.READING_LISTS) }, + onExploreClick = { callback()?.onNavigateTo(NavTab.EXPLORE) }, + onCategoryItemClick = { category -> + val pageTitle = viewModel.createPageTitleForCategory(category) + startActivity( + CategoryActivity.newIntent( + requireActivity(), + pageTitle + ) + ) + }, + wikiErrorClickEvents = WikiErrorClickEvents( + retryClickListener = { + viewModel.loadReadingHistory() + } + ) ) } } - Icon( - modifier = Modifier.size(24.dp), - painter = painterResource(R.drawable.ic_chevron_forward_white_24dp), - tint = WikipediaTheme.colors.secondaryColor, - contentDescription = null - ) - } - Row( - modifier = modifier.fillMaxWidth().padding(top = 6.dp, bottom = 16.dp) - ) { - Text( - text = readingHistory.articlesSavedThisMonth.toString(), - modifier = Modifier.padding(start = 16.dp).align(Alignment.Bottom), - style = MaterialTheme.typography.titleLarge, - fontWeight = FontWeight.Medium, - color = WikipediaTheme.colors.primaryColor - ) - Spacer(modifier = Modifier.weight(1f)) - Row( - modifier = Modifier.padding(end = 16.dp) - ) { - val itemsToShow = if (readingHistory.articlesSaved.size <= 4) readingHistory.articlesSaved.size else 3 - val showOverflowItem = readingHistory.articlesSaved.size > 4 - for (i in 0 until itemsToShow) { - val url = readingHistory.articlesSaved[i].thumbUrl - if (url == null) { - Box( - modifier = Modifier.padding(start = 4.dp).size(38.dp) - .background( - color = Color.White, - shape = RoundedCornerShape(19.dp) - ).border( - 0.5.dp, - WikipediaTheme.colors.borderColor, - RoundedCornerShape(19.dp)) - ) { - Icon( - modifier = Modifier.size(24.dp).align(Alignment.Center), - painter = painterResource(R.drawable.ic_wikipedia_b), - tint = WikipediaTheme.colors.primaryColor, - contentDescription = null + item { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(paddingValues) + .background( + brush = Brush.verticalGradient( + colors = listOf( + WikipediaTheme.colors.paperColor, + WikipediaTheme.colors.additionColor + ) ) - } - } else { - val request = ImageService.getRequest(LocalContext.current, url = url) - AsyncImage( - model = request, - placeholder = BrushPainter(SolidColor(WikipediaTheme.colors.borderColor)), - error = BrushPainter(SolidColor(WikipediaTheme.colors.borderColor)), - contentScale = ContentScale.Crop, - contentDescription = null, - modifier = Modifier.padding(start = 4.dp).size(38.dp) - .clip(RoundedCornerShape(19.dp)) ) - } - } + ) { + // impact module - if (showOverflowItem) { - Box( - modifier = Modifier.padding(start = 4.dp).size(38.dp) - .background( - color = WikipediaTheme.colors.placeholderColor, - shape = RoundedCornerShape(19.dp) - ) - ) { - Text( - text = String.format(Locale.getDefault(), "+%d", readingHistory.articlesSavedThisMonth - 3), - modifier = Modifier.align(Alignment.Center), - fontWeight = FontWeight.Medium, - fontSize = 11.sp, - color = Color.White + // game module + + if (donationUiState is UiState.Success) { + // TODO: default is off. Handle this when building the configuration screen. + DonationModule( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 16.dp, horizontal = 16.dp), + uiState = donationUiState, + onClick = { + (requireActivity() as? BaseActivity)?.launchDonateDialog( + campaignId = ActivityTabViewModel.CAMPAIGN_ID + ) + } ) } } } - } - } - if (readingHistory.topCategories.isNotEmpty()) { - TopCategoriesView( - modifier = Modifier - .fillMaxWidth() - .padding(start = 16.dp, end = 16.dp, bottom = 16.dp), - categories = readingHistory.topCategories, - onClick = { - val pageTitle = viewModel.createPageTitleForCategory(it) - startActivity(CategoryActivity.newIntent(requireActivity(), pageTitle)) - } - ) - } + // --- new column --- - if (readingHistory.articlesReadThisMonth == 0 && readingHistory.articlesSavedThisMonth == 0) { - Text( - text = stringResource(R.string.activity_tab_discover_encourage), - modifier = modifier.padding(horizontal = 16.dp, vertical = 8.dp), - fontWeight = FontWeight.Medium, - fontSize = 14.sp, - color = WikipediaTheme.colors.primaryColor - ) - Button( - modifier = modifier.padding(top = 8.dp, bottom = 16.dp), - contentPadding = PaddingValues(horizontal = 18.dp), - colors = ButtonDefaults.buttonColors( - containerColor = WikipediaTheme.colors.progressiveColor, - contentColor = WikipediaTheme.colors.paperColor, - ), - onClick = { - onExploreClick() - }, - ) { - Icon( - modifier = Modifier.size(20.dp), - painter = painterResource(R.drawable.ic_globe), - tint = WikipediaTheme.colors.paperColor, - contentDescription = null - ) - Text( - modifier = Modifier.padding(start = 6.dp), - text = stringResource(R.string.activity_tab_explore_wikipedia) - ) + // timeline module } } } @@ -547,7 +202,6 @@ class ActivityTabFragment : Fragment() { BaseTheme(currentTheme = Theme.LIGHT) { ActivityTabScreen( userName = "User", - donationUiState = UiState.Success("5 days ago"), readingHistoryState = UiState.Success(ActivityTabViewModel.ReadingHistory( timeSpentThisWeek = 12345, articlesReadThisMonth = 123, @@ -566,7 +220,8 @@ class ActivityTabFragment : Fragment() { Category(2025, 1, "Category:Ancient history", "en", 1), Category(2025, 1, "Category:World literature", "en", 1), ) - )) + )), + donationUiState = UiState.Success("5 days ago") ) } } @@ -577,7 +232,6 @@ class ActivityTabFragment : Fragment() { BaseTheme(currentTheme = Theme.LIGHT) { ActivityTabScreen( userName = "User", - donationUiState = UiState.Success("Unknown"), readingHistoryState = UiState.Success(ActivityTabViewModel.ReadingHistory( timeSpentThisWeek = 0, articlesReadThisMonth = 0, @@ -587,7 +241,8 @@ class ActivityTabFragment : Fragment() { lastArticleSavedTime = null, articlesSaved = emptyList(), topCategories = emptyList() - )) + )), + donationUiState = UiState.Success("Unknown") ) } } diff --git a/app/src/main/java/org/wikipedia/activitytab/ActivityTabViewModel.kt b/app/src/main/java/org/wikipedia/activitytab/ActivityTabViewModel.kt index ecd71ffab44..d623e62749f 100644 --- a/app/src/main/java/org/wikipedia/activitytab/ActivityTabViewModel.kt +++ b/app/src/main/java/org/wikipedia/activitytab/ActivityTabViewModel.kt @@ -5,7 +5,6 @@ import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import kotlinx.coroutines.CoroutineExceptionHandler -import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow @@ -41,7 +40,6 @@ class ActivityTabViewModel(savedStateHandle: SavedStateHandle) : ViewModel() { _readingHistoryState.value = UiState.Error(throwable) }) { _readingHistoryState.value = UiState.Loading - delay(3000) val now = LocalDateTime.now().atZone(ZoneId.systemDefault()).toInstant().toEpochMilli() val weekInMillis = TimeUnit.DAYS.toMillis(7) var weekAgo = now - weekInMillis @@ -80,21 +78,16 @@ class ActivityTabViewModel(savedStateHandle: SavedStateHandle) : ViewModel() { } fun loadDonationResults() { - viewModelScope.launch(CoroutineExceptionHandler { _, throwable -> - _donationUiState.value = UiState.Error(throwable) - }) { - _donationUiState.value = UiState.Loading - val lastDonationTime = Prefs.donationResults.lastOrNull()?.dateTime?.let { - val timestampInLong = LocalDateTime.parse(it).toInstant(ZoneOffset.UTC).epochSecond - val relativeTime = DateUtils.getRelativeTimeSpanString( - timestampInLong * 1000, // Convert seconds to milliseconds - System.currentTimeMillis(), - 0L - ) - return@let relativeTime.toString() - } - _donationUiState.value = UiState.Success(lastDonationTime) + val lastDonationTime = Prefs.donationResults.lastOrNull()?.dateTime?.let { + val timestampInLong = LocalDateTime.parse(it).toInstant(ZoneOffset.UTC).epochSecond + val relativeTime = DateUtils.getRelativeTimeSpanString( + timestampInLong * 1000, // Convert seconds to milliseconds + System.currentTimeMillis(), + 0L + ) + return@let relativeTime.toString() } + _donationUiState.value = UiState.Success(lastDonationTime) } fun createPageTitleForCategory(category: Category): PageTitle { diff --git a/app/src/main/java/org/wikipedia/activitytab/DonationModule.kt b/app/src/main/java/org/wikipedia/activitytab/DonationModule.kt index fd799ca6357..f30d9078a19 100644 --- a/app/src/main/java/org/wikipedia/activitytab/DonationModule.kt +++ b/app/src/main/java/org/wikipedia/activitytab/DonationModule.kt @@ -25,8 +25,6 @@ import androidx.compose.ui.unit.dp import org.wikipedia.R import org.wikipedia.compose.components.HtmlText import org.wikipedia.compose.components.WikiCard -import org.wikipedia.compose.components.error.WikiErrorClickEvents -import org.wikipedia.compose.components.error.WikiErrorView import org.wikipedia.compose.theme.BaseTheme import org.wikipedia.compose.theme.WikipediaTheme import org.wikipedia.theme.Theme @@ -36,7 +34,6 @@ import org.wikipedia.util.UiState fun DonationModule( modifier: Modifier = Modifier, uiState: UiState, - wikiErrorClickEvents: WikiErrorClickEvents? = null, onClick: (() -> Unit)? = null ) { WikiCard( @@ -48,67 +45,48 @@ fun DonationModule( color = WikipediaTheme.colors.borderColor ) ) { - when (uiState) { - is UiState.Error -> { - Box( - modifier = modifier - .fillMaxWidth() - .height(200.dp), - contentAlignment = Alignment.Center - ) { - WikiErrorView( - modifier = Modifier - .fillMaxWidth(), - caught = uiState.error, - errorClickEvents = wikiErrorClickEvents - ) - } + if (uiState == UiState.Loading) { + Box( + modifier = modifier + .fillMaxWidth() + .height(200.dp) + ) { + CircularProgressIndicator( + modifier = Modifier + .align(Alignment.Center) + .padding(24.dp), + color = WikipediaTheme.colors.progressiveColor + ) } - - UiState.Loading -> { - Box( - modifier = modifier - .fillMaxWidth() - .height(200.dp) + } else if (uiState is UiState.Success) { + val lastDonationTime = + uiState.data ?: stringResource(R.string.activity_tab_donation_unknown) + Column( + modifier = Modifier.padding(16.dp) + ) { + Row( + horizontalArrangement = Arrangement.spacedBy(8.dp) ) { - CircularProgressIndicator( - modifier = Modifier - .align(Alignment.Center) - .padding(24.dp), - color = WikipediaTheme.colors.progressiveColor + Icon( + modifier = Modifier.size(16.dp), + painter = painterResource(R.drawable.outline_credit_card_heart_24), + tint = WikipediaTheme.colors.primaryColor, + contentDescription = null ) - } - } - - is UiState.Success -> { - val lastDonationTime = uiState.data ?: stringResource(R.string.activity_tab_donation_unknown) - Column( - modifier = Modifier.padding(16.dp) - ) { - Row( - horizontalArrangement = Arrangement.spacedBy(8.dp) - ) { - Icon( - modifier = Modifier.size(16.dp), - painter = painterResource(R.drawable.outline_credit_card_heart_24), - tint = WikipediaTheme.colors.primaryColor, - contentDescription = null - ) - HtmlText( - text = stringResource(R.string.activity_tab_donation_last_donation), - style = MaterialTheme.typography.labelMedium.copy(fontWeight = FontWeight.Normal), - color = WikipediaTheme.colors.primaryColor, - lineHeight = MaterialTheme.typography.labelMedium.lineHeight - ) - } - Text( - modifier = Modifier.padding(top = 16.dp), - text = lastDonationTime, - style = MaterialTheme.typography.titleLarge, - color = WikipediaTheme.colors.progressiveColor, - fontWeight = FontWeight.Medium + HtmlText( + text = stringResource(R.string.activity_tab_donation_last_donation), + style = MaterialTheme.typography.labelMedium.copy(fontWeight = FontWeight.Normal), + color = WikipediaTheme.colors.primaryColor, + lineHeight = MaterialTheme.typography.labelMedium.lineHeight ) } + Text( + modifier = Modifier.padding(top = 16.dp), + text = lastDonationTime, + style = MaterialTheme.typography.titleLarge, + color = WikipediaTheme.colors.progressiveColor, + fontWeight = FontWeight.Medium + ) } } } diff --git a/app/src/main/java/org/wikipedia/activitytab/ReadingHistoryModule.kt b/app/src/main/java/org/wikipedia/activitytab/ReadingHistoryModule.kt new file mode 100644 index 00000000000..e0fc6c0ee3f --- /dev/null +++ b/app/src/main/java/org/wikipedia/activitytab/ReadingHistoryModule.kt @@ -0,0 +1,429 @@ +package org.wikipedia.activitytab + +import android.text.format.DateFormat +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +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.shape.RoundedCornerShape +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.graphics.painter.BrushPainter +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.TextUnit +import androidx.compose.ui.unit.TextUnitType +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import coil3.compose.AsyncImage +import org.wikipedia.R +import org.wikipedia.categories.db.Category +import org.wikipedia.compose.ComposeColors +import org.wikipedia.compose.components.TinyBarChart +import org.wikipedia.compose.components.error.WikiErrorClickEvents +import org.wikipedia.compose.theme.WikipediaTheme +import org.wikipedia.util.UiState +import org.wikipedia.views.imageservice.ImageService +import java.time.LocalDate +import java.time.format.DateTimeFormatter +import java.util.Locale + +@Composable +fun ReadingHistoryModule( + modifier: Modifier, + userName: String, + readingHistoryState: UiState, + onArticlesReadClick: () -> Unit = {}, + onArticlesSavedClick: () -> Unit = {}, + onExploreClick: () -> Unit = {}, + onCategoryItemClick: (Category) -> Unit = {}, + wikiErrorClickEvents: WikiErrorClickEvents? = null +) { + Text( + text = stringResource(R.string.activity_tab_user_reading, userName), + modifier = modifier + .padding(top = 16.dp), + style = MaterialTheme.typography.titleLarge, + fontWeight = FontWeight.Medium, + textAlign = TextAlign.Center, + color = WikipediaTheme.colors.primaryColor + ) + Box( + modifier = modifier + .padding(horizontal = 8.dp, vertical = 4.dp) + .background( + color = WikipediaTheme.colors.additionColor, + shape = RoundedCornerShape(8.dp) + ) + ) { + Text( + text = stringResource(R.string.activity_tab_on_wikipedia_android).uppercase(), + modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp), + fontSize = 11.sp, + fontFamily = FontFamily.Monospace, + letterSpacing = TextUnit(0.8f, TextUnitType.Sp), + textAlign = TextAlign.Center, + color = WikipediaTheme.colors.primaryColor + ) + } + if (readingHistoryState is UiState.Loading) { + CircularProgressIndicator( + modifier = modifier.padding(vertical = 16.dp).size(48.dp), + color = WikipediaTheme.colors.progressiveColor + ) + } else if (readingHistoryState is UiState.Success) { + val readingHistory = readingHistoryState.data + val todayDate = LocalDate.now() + + Text( + text = stringResource( + R.string.activity_tab_weekly_time_spent_hm, + (readingHistory.timeSpentThisWeek / 3600), + (readingHistory.timeSpentThisWeek % 60) + ), + modifier = modifier.padding(top = 12.dp), + fontWeight = FontWeight.Medium, + textAlign = TextAlign.Center, + style = MaterialTheme.typography.headlineLarge.copy( + brush = Brush.linearGradient( + colors = listOf( + ComposeColors.Red700, + ComposeColors.Orange500, + ComposeColors.Yellow500, + ComposeColors.Blue300 + ) + ) + ), + color = WikipediaTheme.colors.primaryColor + ) + Text( + text = stringResource(R.string.activity_tab_weekly_time_spent), + modifier = modifier + .padding(top = 8.dp, bottom = 16.dp), + style = MaterialTheme.typography.labelLarge, + textAlign = TextAlign.Center, + color = WikipediaTheme.colors.primaryColor + ) + + Card( + modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp) + .clickable { + onArticlesReadClick() + }, + elevation = CardDefaults.cardElevation(defaultElevation = 0.dp), + colors = CardDefaults.cardColors( + containerColor = WikipediaTheme.colors.paperColor + ), + border = BorderStroke( + width = 1.dp, + color = WikipediaTheme.colors.borderColor + ), + shape = RoundedCornerShape(12.dp) + ) { + Row( + modifier = modifier.fillMaxWidth() + .padding(top = 16.dp, start = 16.dp, end = 16.dp) + ) { + Column( + modifier = modifier.weight(1f) + ) { + Row( + modifier = modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + Icon( + modifier = Modifier.size(16.dp), + painter = painterResource(R.drawable.ic_newsstand_24), + tint = WikipediaTheme.colors.primaryColor, + contentDescription = null + ) + Text( + text = stringResource(R.string.activity_tab_monthly_articles_read), + style = MaterialTheme.typography.labelMedium, + color = WikipediaTheme.colors.primaryColor + ) + } + if (readingHistory.lastArticleReadTime != null) { + Text( + text = if (todayDate == readingHistory.lastArticleReadTime.toLocalDate()) + readingHistory.lastArticleReadTime + .format( + DateTimeFormatter.ofPattern( + DateFormat.getBestDateTimePattern( + Locale.getDefault(), + "hhmm a" + ) + ) + ) + else + readingHistory.lastArticleReadTime + .format( + DateTimeFormatter.ofPattern( + DateFormat.getBestDateTimePattern( + Locale.getDefault(), + "MMMM d" + ) + ) + ), + modifier = Modifier.padding(top = 4.dp), + style = MaterialTheme.typography.bodySmall, + color = WikipediaTheme.colors.secondaryColor + ) + } + } + Icon( + modifier = Modifier.size(24.dp), + painter = painterResource(R.drawable.ic_chevron_forward_white_24dp), + tint = WikipediaTheme.colors.secondaryColor, + contentDescription = null + ) + } + + Row( + modifier = modifier.fillMaxWidth().padding(top = 6.dp, bottom = 16.dp) + ) { + Text( + text = readingHistory.articlesReadThisMonth.toString(), + modifier = Modifier.padding(start = 16.dp).align(Alignment.Bottom), + style = MaterialTheme.typography.titleLarge, + fontWeight = FontWeight.Medium, + color = WikipediaTheme.colors.primaryColor + ) + Spacer(modifier = Modifier.weight(1f)) + + TinyBarChart( + values = readingHistory.articlesReadByWeek, + modifier = Modifier.padding(end = 16.dp).size( + 72.dp, + if (readingHistory.articlesReadThisMonth == 0) 32.dp else 48.dp + ), + minColor = ComposeColors.Gray300, + maxColor = ComposeColors.Green600 + ) + } + } + + Card( + modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp, vertical = 16.dp) + .clickable { + onArticlesSavedClick() + }, + elevation = CardDefaults.cardElevation(defaultElevation = 0.dp), + colors = CardDefaults.cardColors( + containerColor = WikipediaTheme.colors.paperColor + ), + border = BorderStroke( + width = 1.dp, + color = WikipediaTheme.colors.borderColor + ), + shape = RoundedCornerShape(12.dp) + ) { + Row( + modifier = modifier.fillMaxWidth() + .padding(top = 16.dp, start = 16.dp, end = 16.dp) + ) { + Column( + modifier = modifier.weight(1f) + ) { + Row( + modifier = modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + Icon( + modifier = Modifier.size(16.dp), + painter = painterResource(R.drawable.ic_bookmark_border_white_24dp), + tint = WikipediaTheme.colors.primaryColor, + contentDescription = null + ) + Text( + text = stringResource(R.string.activity_tab_monthly_articles_saved), + style = MaterialTheme.typography.labelMedium, + color = WikipediaTheme.colors.primaryColor + ) + } + if (readingHistory.lastArticleSavedTime != null) { + Text( + text = if (todayDate == readingHistory.lastArticleSavedTime.toLocalDate()) + readingHistory.lastArticleSavedTime + .format( + DateTimeFormatter.ofPattern( + DateFormat.getBestDateTimePattern( + Locale.getDefault(), + "hhmm a" + ) + ) + ) + else + readingHistory.lastArticleSavedTime + .format( + DateTimeFormatter.ofPattern( + DateFormat.getBestDateTimePattern( + Locale.getDefault(), + "MMMM d" + ) + ) + ), + modifier = Modifier.padding(top = 4.dp), + style = MaterialTheme.typography.bodySmall, + color = WikipediaTheme.colors.secondaryColor + ) + } + } + Icon( + modifier = Modifier.size(24.dp), + painter = painterResource(R.drawable.ic_chevron_forward_white_24dp), + tint = WikipediaTheme.colors.secondaryColor, + contentDescription = null + ) + } + Row( + modifier = modifier.fillMaxWidth().padding(top = 6.dp, bottom = 16.dp) + ) { + Text( + text = readingHistory.articlesSavedThisMonth.toString(), + modifier = Modifier.padding(start = 16.dp).align(Alignment.Bottom), + style = MaterialTheme.typography.titleLarge, + fontWeight = FontWeight.Medium, + color = WikipediaTheme.colors.primaryColor + ) + Spacer(modifier = Modifier.weight(1f)) + Row( + modifier = Modifier.padding(end = 16.dp) + ) { + val itemsToShow = + if (readingHistory.articlesSaved.size <= 4) readingHistory.articlesSaved.size else 3 + val showOverflowItem = readingHistory.articlesSaved.size > 4 + + for (i in 0 until itemsToShow) { + val url = readingHistory.articlesSaved[i].thumbUrl + if (url == null) { + Box( + modifier = Modifier.padding(start = 4.dp).size(38.dp) + .background( + color = Color.White, + shape = RoundedCornerShape(19.dp) + ).border( + 0.5.dp, + WikipediaTheme.colors.borderColor, + RoundedCornerShape(19.dp) + ) + ) { + Icon( + modifier = Modifier.size(24.dp).align(Alignment.Center), + painter = painterResource(R.drawable.ic_wikipedia_b), + tint = WikipediaTheme.colors.primaryColor, + contentDescription = null + ) + } + } else { + val request = + ImageService.getRequest(LocalContext.current, url = url) + AsyncImage( + model = request, + placeholder = BrushPainter(SolidColor(WikipediaTheme.colors.borderColor)), + error = BrushPainter(SolidColor(WikipediaTheme.colors.borderColor)), + contentScale = ContentScale.Crop, + contentDescription = null, + modifier = Modifier.padding(start = 4.dp).size(38.dp) + .clip(RoundedCornerShape(19.dp)) + ) + } + } + + if (showOverflowItem) { + Box( + modifier = Modifier.padding(start = 4.dp).size(38.dp) + .background( + color = WikipediaTheme.colors.placeholderColor, + shape = RoundedCornerShape(19.dp) + ) + ) { + Text( + text = String.format( + Locale.getDefault(), + "+%d", + readingHistory.articlesSavedThisMonth - 3 + ), + modifier = Modifier.align(Alignment.Center), + fontWeight = FontWeight.Medium, + fontSize = 11.sp, + color = Color.White + ) + } + } + } + } + } + + if (readingHistory.topCategories.isNotEmpty()) { + TopCategoriesView( + modifier = Modifier + .fillMaxWidth() + .padding(start = 16.dp, end = 16.dp, bottom = 16.dp), + categories = readingHistory.topCategories, + onClick = { + onCategoryItemClick(it) + } + ) + } + + if (readingHistory.articlesReadThisMonth == 0 && readingHistory.articlesSavedThisMonth == 0) { + Text( + text = stringResource(R.string.activity_tab_discover_encourage), + modifier = modifier.padding(horizontal = 16.dp, vertical = 8.dp), + fontWeight = FontWeight.Medium, + fontSize = 14.sp, + color = WikipediaTheme.colors.primaryColor + ) + Button( + modifier = modifier.padding(top = 8.dp, bottom = 16.dp), + contentPadding = PaddingValues(horizontal = 18.dp), + colors = ButtonDefaults.buttonColors( + containerColor = WikipediaTheme.colors.progressiveColor, + contentColor = WikipediaTheme.colors.paperColor, + ), + onClick = { + onExploreClick() + }, + ) { + Icon( + modifier = Modifier.size(20.dp), + painter = painterResource(R.drawable.ic_globe), + tint = WikipediaTheme.colors.paperColor, + contentDescription = null + ) + Text( + modifier = Modifier.padding(start = 6.dp), + text = stringResource(R.string.activity_tab_explore_wikipedia) + ) + } + } + } +} diff --git a/app/src/main/java/org/wikipedia/activitytab/TopCategoriesView.kt b/app/src/main/java/org/wikipedia/activitytab/TopCategoriesView.kt index 70f1fc1a28d..b80f92b89dd 100644 --- a/app/src/main/java/org/wikipedia/activitytab/TopCategoriesView.kt +++ b/app/src/main/java/org/wikipedia/activitytab/TopCategoriesView.kt @@ -16,6 +16,7 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import org.wikipedia.R @@ -54,7 +55,7 @@ fun TopCategoriesView( contentDescription = null ) Text( - text = "Top categories read this month", + text = stringResource(R.string.activity_tab_monthly_top_categories), style = MaterialTheme.typography.labelMedium, color = WikipediaTheme.colors.primaryColor ) diff --git a/app/src/main/res/values-qq/strings.xml b/app/src/main/res/values-qq/strings.xml index 5172aba52e8..228a57e6438 100644 --- a/app/src/main/res/values-qq/strings.xml +++ b/app/src/main/res/values-qq/strings.xml @@ -1177,6 +1177,7 @@ Label underneath the weekly time spent in the app. Label on a card that lists the number of articles read this month. Label on a card that lists the number of articles saved this month to a reading list. + Label on a card that lists the top categories of articles that were read this month. Button label to go to the Explore screen. Label encouraging the user to discover new articles. Label on card that shows the last donation time. diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index e54ffaf6841..f7b01ec9ca2 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1196,6 +1196,7 @@ Time spent reading this week Articles read this month Articles saved this month + Top categories read this month Explore Wikipedia Discover something new to read Unknown From cceb390834bbed771d3710cc081b96ba519b659c Mon Sep 17 00:00:00 2001 From: Dmitry Brant Date: Fri, 15 Aug 2025 17:21:08 -0400 Subject: [PATCH 19/70] Make queries distinct. --- .../org/wikipedia/activitytab/ActivityTabViewModel.kt | 11 +++-------- .../java/org/wikipedia/history/db/HistoryEntryDao.kt | 8 ++++---- .../wikipedia/yearinreview/YearInReviewViewModel.kt | 2 +- 3 files changed, 8 insertions(+), 13 deletions(-) diff --git a/app/src/main/java/org/wikipedia/activitytab/ActivityTabViewModel.kt b/app/src/main/java/org/wikipedia/activitytab/ActivityTabViewModel.kt index d623e62749f..69c1487437d 100644 --- a/app/src/main/java/org/wikipedia/activitytab/ActivityTabViewModel.kt +++ b/app/src/main/java/org/wikipedia/activitytab/ActivityTabViewModel.kt @@ -30,11 +30,6 @@ class ActivityTabViewModel(savedStateHandle: SavedStateHandle) : ViewModel() { private val _donationUiState = MutableStateFlow>(UiState.Loading) val donationUiState: StateFlow> = _donationUiState.asStateFlow() - init { - loadReadingHistory() - loadDonationResults() - } - fun loadReadingHistory() { viewModelScope.launch(CoroutineExceptionHandler { _, throwable -> _readingHistoryState.value = UiState.Error(throwable) @@ -46,12 +41,12 @@ class ActivityTabViewModel(savedStateHandle: SavedStateHandle) : ViewModel() { val totalTimeSpent = AppDatabase.instance.historyEntryWithImageDao().getTimeSpentSinceTimeStamp(weekAgo) val thirtyDaysAgo = now - TimeUnit.DAYS.toMillis(30) - val articlesReadThisMonth = AppDatabase.instance.historyEntryDao().getTotalEntriesSince(thirtyDaysAgo) ?: 0 + val articlesReadThisMonth = AppDatabase.instance.historyEntryDao().getDistinctEntriesSince(thirtyDaysAgo) ?: 0 val articlesReadByWeek = mutableListOf() - articlesReadByWeek.add(AppDatabase.instance.historyEntryDao().getTotalEntriesSince(weekAgo) ?: 0) + articlesReadByWeek.add(AppDatabase.instance.historyEntryDao().getDistinctEntriesSince(weekAgo) ?: 0) for (i in 1..3) { weekAgo -= weekInMillis - val articlesRead = AppDatabase.instance.historyEntryDao().getHistoryCount(weekAgo, weekAgo + weekInMillis) + val articlesRead = AppDatabase.instance.historyEntryDao().getDistinctEntriesBetween(weekAgo, weekAgo + weekInMillis) articlesReadByWeek.add(articlesRead) } val mostRecentReadTime = AppDatabase.instance.historyEntryDao().getMostRecentEntry()?.timestamp?.toInstant()?.atZone(ZoneId.systemDefault())?.toLocalDateTime() diff --git a/app/src/main/java/org/wikipedia/history/db/HistoryEntryDao.kt b/app/src/main/java/org/wikipedia/history/db/HistoryEntryDao.kt index 2da4f3f57cf..ba64a923412 100644 --- a/app/src/main/java/org/wikipedia/history/db/HistoryEntryDao.kt +++ b/app/src/main/java/org/wikipedia/history/db/HistoryEntryDao.kt @@ -24,8 +24,8 @@ interface HistoryEntryDao { @Query("SELECT * FROM HistoryEntry WHERE authority = :authority AND lang = :lang AND apiTitle = :apiTitle AND timestamp = :timestamp LIMIT 1") suspend fun findEntryBy(authority: String, lang: String, apiTitle: String, timestamp: Long): HistoryEntry? - @Query("SELECT COUNT(*) FROM HistoryEntry WHERE timestamp BETWEEN :startDate AND :endDate ") - suspend fun getHistoryCount(startDate: Long?, endDate: Long?): Int + @Query("SELECT COUNT(*) FROM (SELECT DISTINCT HistoryEntry.lang, HistoryEntry.apiTitle FROM HistoryEntry WHERE timestamp BETWEEN :startDate AND :endDate)") + suspend fun getDistinctEntriesBetween(startDate: Long?, endDate: Long?): Int @Query("SELECT DISTINCT displayTitle FROM HistoryEntry LIMIT 3") suspend fun getDisplayTitles(): List @@ -39,8 +39,8 @@ interface HistoryEntryDao { @Query("DELETE FROM HistoryEntry WHERE authority = :authority AND lang = :lang AND namespace = :namespace AND apiTitle = :apiTitle") suspend fun deleteBy(authority: String, lang: String, namespace: String?, apiTitle: String) - @Query("SELECT COUNT(*) FROM HistoryEntry WHERE timestamp > :timestamp") - suspend fun getTotalEntriesSince(timestamp: Long): Int? + @Query("SELECT COUNT(*) FROM (SELECT DISTINCT HistoryEntry.lang, HistoryEntry.apiTitle FROM HistoryEntry WHERE timestamp > :timestamp)") + suspend fun getDistinctEntriesSince(timestamp: Long): Int? @Query("SELECT * FROM HistoryEntry ORDER BY timestamp DESC LIMIT 1") suspend fun getMostRecentEntry(): HistoryEntry? diff --git a/app/src/main/java/org/wikipedia/yearinreview/YearInReviewViewModel.kt b/app/src/main/java/org/wikipedia/yearinreview/YearInReviewViewModel.kt index 106a7c42602..cb9efb6d152 100644 --- a/app/src/main/java/org/wikipedia/yearinreview/YearInReviewViewModel.kt +++ b/app/src/main/java/org/wikipedia/yearinreview/YearInReviewViewModel.kt @@ -47,7 +47,7 @@ class YearInReviewViewModel() : ViewModel() { _uiScreenListState.value = Resource.Loading() val readCountJob = async { - personalizedStatistics.readCount = AppDatabase.instance.historyEntryDao().getHistoryCount(startTimeInMillis, endTimeInMillis) + personalizedStatistics.readCount = AppDatabase.instance.historyEntryDao().getDistinctEntriesBetween(startTimeInMillis, endTimeInMillis) if (personalizedStatistics.readCount >= MINIMUM_READ_COUNT) { personalizedStatistics.readCountApiTitles = AppDatabase.instance.historyEntryDao().getDisplayTitles() .map { StringUtil.fromHtml(it).toString() } From af3cf952708704c85c363155e610ecc2c7f44407 Mon Sep 17 00:00:00 2001 From: Cooltey Feng Date: Fri, 15 Aug 2025 16:40:31 -0700 Subject: [PATCH 20/70] Add loadDonationResults() back. --- .../main/java/org/wikipedia/activitytab/ActivityTabFragment.kt | 2 ++ app/src/main/java/org/wikipedia/activitytab/DonationModule.kt | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/org/wikipedia/activitytab/ActivityTabFragment.kt b/app/src/main/java/org/wikipedia/activitytab/ActivityTabFragment.kt index fd0e19e6ff7..473a4f02f27 100644 --- a/app/src/main/java/org/wikipedia/activitytab/ActivityTabFragment.kt +++ b/app/src/main/java/org/wikipedia/activitytab/ActivityTabFragment.kt @@ -73,6 +73,7 @@ class ActivityTabFragment : Fragment() { override fun onResume() { super.onResume() viewModel.loadReadingHistory() + viewModel.loadDonationResults() } @OptIn(ExperimentalMaterial3Api::class) @@ -99,6 +100,7 @@ class ActivityTabFragment : Fragment() { onRefresh = { isRefreshing = true viewModel.loadReadingHistory() + viewModel.loadDonationResults() }, isRefreshing = isRefreshing, state = state, diff --git a/app/src/main/java/org/wikipedia/activitytab/DonationModule.kt b/app/src/main/java/org/wikipedia/activitytab/DonationModule.kt index f30d9078a19..49b5a58c219 100644 --- a/app/src/main/java/org/wikipedia/activitytab/DonationModule.kt +++ b/app/src/main/java/org/wikipedia/activitytab/DonationModule.kt @@ -94,7 +94,7 @@ fun DonationModule( @Preview @Composable -private fun DonationViewPreview() { +private fun DonationModulePreview() { BaseTheme( currentTheme = Theme.LIGHT ) { From d6521a222273bc5b8bae069d485c2d088d0376bf Mon Sep 17 00:00:00 2001 From: Dmitry Brant Date: Mon, 18 Aug 2025 09:08:14 -0400 Subject: [PATCH 21/70] Create logged-out screen. --- .../activitytab/ActivityTabFragment.kt | 107 ++++++++++++++++++ .../illustration_activity_tab_logged_out.xml | 101 +++++++++++++++++ app/src/main/res/values-qq/strings.xml | 1 + app/src/main/res/values/strings.xml | 1 + 4 files changed, 210 insertions(+) create mode 100644 app/src/main/res/drawable/illustration_activity_tab_logged_out.xml diff --git a/app/src/main/java/org/wikipedia/activitytab/ActivityTabFragment.kt b/app/src/main/java/org/wikipedia/activitytab/ActivityTabFragment.kt index 473a4f02f27..2a2c31a2f35 100644 --- a/app/src/main/java/org/wikipedia/activitytab/ActivityTabFragment.kt +++ b/app/src/main/java/org/wikipedia/activitytab/ActivityTabFragment.kt @@ -4,14 +4,25 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.compose.foundation.Image import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text import androidx.compose.material3.pulltorefresh.PullToRefreshBox import androidx.compose.material3.pulltorefresh.PullToRefreshDefaults.Indicator import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState @@ -25,11 +36,15 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Brush import androidx.compose.ui.platform.ComposeView +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.core.net.toUri import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels +import org.wikipedia.R import org.wikipedia.activity.BaseActivity import org.wikipedia.activity.FragmentUtil.getCallback import org.wikipedia.auth.AccountUtil @@ -61,6 +76,7 @@ class ActivityTabFragment : Fragment() { setContent { BaseTheme { ActivityTabScreen( + isLoggedIn = AccountUtil.isLoggedIn, userName = AccountUtil.userName, readingHistoryState = viewModel.readingHistoryState.collectAsState().value, donationUiState = viewModel.donationUiState.collectAsState().value @@ -79,6 +95,7 @@ class ActivityTabFragment : Fragment() { @OptIn(ExperimentalMaterial3Api::class) @Composable fun ActivityTabScreen( + isLoggedIn: Boolean, userName: String, readingHistoryState: UiState, donationUiState: UiState @@ -96,6 +113,72 @@ class ActivityTabFragment : Fragment() { isRefreshing = false } + if (!isLoggedIn) { + Box( + modifier = Modifier + .fillMaxSize() + .padding(paddingValues) + ) { + val scrollState = rememberScrollState() + + Column( + modifier = Modifier.align(Alignment.Center).padding(horizontal = 16.dp).verticalScroll(scrollState), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Image( + modifier = Modifier.size(164.dp), + painter = painterResource(R.drawable.illustration_activity_tab_logged_out), + contentDescription = null + ) + Text( + modifier = Modifier.padding(top = 16.dp), + text = stringResource(R.string.activity_tab_logged_out_title), + style = MaterialTheme.typography.titleSmall, + textAlign = TextAlign.Center, + color = WikipediaTheme.colors.primaryColor + ) + Button( + modifier = Modifier.padding(top = 16.dp), + contentPadding = PaddingValues(horizontal = 18.dp), + colors = ButtonDefaults.buttonColors( + containerColor = WikipediaTheme.colors.progressiveColor, + contentColor = WikipediaTheme.colors.paperColor, + ), + onClick = { + // TODO + }, + ) { + Icon( + modifier = Modifier.size(20.dp), + painter = painterResource(R.drawable.ic_user_avatar), + tint = WikipediaTheme.colors.paperColor, + contentDescription = null + ) + Text( + modifier = Modifier.padding(start = 6.dp), + text = stringResource(R.string.create_account_button) + ) + } + Button( + contentPadding = PaddingValues(horizontal = 18.dp), + colors = ButtonDefaults.buttonColors( + containerColor = WikipediaTheme.colors.paperColor, + contentColor = WikipediaTheme.colors.primaryColor, + ), + onClick = { + // TODO + }, + ) { + Text( + modifier = Modifier.padding(start = 6.dp), + text = stringResource(R.string.menu_login) + ) + } + } + } + return@Scaffold + } + PullToRefreshBox( onRefresh = { isRefreshing = true @@ -203,6 +286,7 @@ class ActivityTabFragment : Fragment() { val site = WikiSite("https://en.wikipedia.org/".toUri(), "en") BaseTheme(currentTheme = Theme.LIGHT) { ActivityTabScreen( + isLoggedIn = true, userName = "User", readingHistoryState = UiState.Success(ActivityTabViewModel.ReadingHistory( timeSpentThisWeek = 12345, @@ -233,6 +317,29 @@ class ActivityTabFragment : Fragment() { fun ActivityTabScreenEmptyPreview() { BaseTheme(currentTheme = Theme.LIGHT) { ActivityTabScreen( + isLoggedIn = true, + userName = "User", + readingHistoryState = UiState.Success(ActivityTabViewModel.ReadingHistory( + timeSpentThisWeek = 0, + articlesReadThisMonth = 0, + lastArticleReadTime = null, + articlesReadByWeek = listOf(0, 0, 0, 0), + articlesSavedThisMonth = 0, + lastArticleSavedTime = null, + articlesSaved = emptyList(), + topCategories = emptyList() + )), + donationUiState = UiState.Success("Unknown") + ) + } + } + + @Preview + @Composable + fun ActivityTabScreenLoggedOutPreview() { + BaseTheme(currentTheme = Theme.LIGHT) { + ActivityTabScreen( + isLoggedIn = false, userName = "User", readingHistoryState = UiState.Success(ActivityTabViewModel.ReadingHistory( timeSpentThisWeek = 0, diff --git a/app/src/main/res/drawable/illustration_activity_tab_logged_out.xml b/app/src/main/res/drawable/illustration_activity_tab_logged_out.xml new file mode 100644 index 00000000000..80f1c5eb7ab --- /dev/null +++ b/app/src/main/res/drawable/illustration_activity_tab_logged_out.xml @@ -0,0 +1,101 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/values-qq/strings.xml b/app/src/main/res/values-qq/strings.xml index 74a9357bede..54b1e481fbc 100644 --- a/app/src/main/res/values-qq/strings.xml +++ b/app/src/main/res/values-qq/strings.xml @@ -1171,6 +1171,7 @@ Edit summary when an image caption was added. Edit summary when an image and its caption were added. Button label for viewing an edit after it is published. + Title text shown in the Activity tab when the user is not logged in. Title of the current user\'s reading activity. The %s is replaced with the user name. Subtitle of the reading activity statistics, specific to the Wikipedia Android app. Time spent reading this week, expressed in hours and minutes. The %1$d symbol is replaced with hours, and %2$d is replaced with minutes. diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 214dff9ae3b..bde2e3d2046 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1190,6 +1190,7 @@ Added caption Added image and caption View + Log in or create an account to view your activity on the Wikipedia app %s\'s reading On Wikipedia Android %1$dh %2$dm From 5d1df9e67dc79db6d7a9e0fd79569710f04075d1 Mon Sep 17 00:00:00 2001 From: Dmitry Brant Date: Mon, 18 Aug 2025 09:26:10 -0400 Subject: [PATCH 22/70] Hook into event bus for detecting login/logout. --- .../activitytab/ActivityTabFragment.kt | 27 ++++++++++++++++--- .../activitytab/ActivityTabViewModel.kt | 5 ++++ .../org/wikipedia/events/LoggedInEvent.kt | 3 +++ .../java/org/wikipedia/login/LoginActivity.kt | 4 +++ 4 files changed, 35 insertions(+), 4 deletions(-) create mode 100644 app/src/main/java/org/wikipedia/events/LoggedInEvent.kt diff --git a/app/src/main/java/org/wikipedia/activitytab/ActivityTabFragment.kt b/app/src/main/java/org/wikipedia/activitytab/ActivityTabFragment.kt index 2a2c31a2f35..1b5cf04e1d2 100644 --- a/app/src/main/java/org/wikipedia/activitytab/ActivityTabFragment.kt +++ b/app/src/main/java/org/wikipedia/activitytab/ActivityTabFragment.kt @@ -44,6 +44,11 @@ import androidx.compose.ui.unit.dp import androidx.core.net.toUri import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.launch import org.wikipedia.R import org.wikipedia.activity.BaseActivity import org.wikipedia.activity.FragmentUtil.getCallback @@ -53,7 +58,12 @@ import org.wikipedia.categories.db.Category import org.wikipedia.compose.components.error.WikiErrorClickEvents import org.wikipedia.compose.theme.BaseTheme import org.wikipedia.compose.theme.WikipediaTheme +import org.wikipedia.concurrency.FlowEventBus import org.wikipedia.dataclient.WikiSite +import org.wikipedia.events.LoggedInEvent +import org.wikipedia.events.LoggedOutEvent +import org.wikipedia.events.LoggedOutInBackgroundEvent +import org.wikipedia.login.LoginActivity import org.wikipedia.navtab.NavTab import org.wikipedia.page.PageTitle import org.wikipedia.settings.Prefs @@ -72,6 +82,16 @@ class ActivityTabFragment : Fragment() { super.onCreateView(inflater, container, savedInstanceState) Prefs.activityTabRedDotShown = true + viewLifecycleOwner.lifecycleScope.launch { + repeatOnLifecycle(Lifecycle.State.RESUMED) { + FlowEventBus.events.collectLatest { event -> + when (event) { + is LoggedInEvent, is LoggedOutEvent, is LoggedOutInBackgroundEvent -> viewModel.loadAll() + } + } + } + } + return ComposeView(requireContext()).apply { setContent { BaseTheme { @@ -88,8 +108,7 @@ class ActivityTabFragment : Fragment() { override fun onResume() { super.onResume() - viewModel.loadReadingHistory() - viewModel.loadDonationResults() + viewModel.loadAll() } @OptIn(ExperimentalMaterial3Api::class) @@ -145,7 +164,7 @@ class ActivityTabFragment : Fragment() { contentColor = WikipediaTheme.colors.paperColor, ), onClick = { - // TODO + startActivity(LoginActivity.newIntent(requireContext(), LoginActivity.SOURCE_ACTIVITY)) }, ) { Icon( @@ -166,7 +185,7 @@ class ActivityTabFragment : Fragment() { contentColor = WikipediaTheme.colors.primaryColor, ), onClick = { - // TODO + startActivity(LoginActivity.newIntent(requireContext(), LoginActivity.SOURCE_ACTIVITY, createAccountFirst = false)) }, ) { Text( diff --git a/app/src/main/java/org/wikipedia/activitytab/ActivityTabViewModel.kt b/app/src/main/java/org/wikipedia/activitytab/ActivityTabViewModel.kt index 69c1487437d..7f9912f7b13 100644 --- a/app/src/main/java/org/wikipedia/activitytab/ActivityTabViewModel.kt +++ b/app/src/main/java/org/wikipedia/activitytab/ActivityTabViewModel.kt @@ -30,6 +30,11 @@ class ActivityTabViewModel(savedStateHandle: SavedStateHandle) : ViewModel() { private val _donationUiState = MutableStateFlow>(UiState.Loading) val donationUiState: StateFlow> = _donationUiState.asStateFlow() + fun loadAll() { + loadReadingHistory() + loadDonationResults() + } + fun loadReadingHistory() { viewModelScope.launch(CoroutineExceptionHandler { _, throwable -> _readingHistoryState.value = UiState.Error(throwable) diff --git a/app/src/main/java/org/wikipedia/events/LoggedInEvent.kt b/app/src/main/java/org/wikipedia/events/LoggedInEvent.kt new file mode 100644 index 00000000000..04f1b5b5b79 --- /dev/null +++ b/app/src/main/java/org/wikipedia/events/LoggedInEvent.kt @@ -0,0 +1,3 @@ +package org.wikipedia.events + +class LoggedInEvent diff --git a/app/src/main/java/org/wikipedia/login/LoginActivity.kt b/app/src/main/java/org/wikipedia/login/LoginActivity.kt index e5eb57a304d..e4fe4abff7f 100644 --- a/app/src/main/java/org/wikipedia/login/LoginActivity.kt +++ b/app/src/main/java/org/wikipedia/login/LoginActivity.kt @@ -19,8 +19,10 @@ import org.wikipedia.auth.AccountUtil import org.wikipedia.auth.AccountUtil.updateAccount import org.wikipedia.captcha.CaptchaHandler import org.wikipedia.captcha.CaptchaResult +import org.wikipedia.concurrency.FlowEventBus import org.wikipedia.createaccount.CreateAccountActivity import org.wikipedia.databinding.ActivityLoginBinding +import org.wikipedia.events.LoggedInEvent import org.wikipedia.extensions.parcelableExtra import org.wikipedia.notifications.PollNotificationWorker import org.wikipedia.page.PageTitle @@ -199,6 +201,7 @@ class LoginActivity : BaseActivity() { PollNotificationWorker.schedulePollNotificationJob(this) Prefs.isPushNotificationOptionsSet = false updateSubscription() + FlowEventBus.post(LoggedInEvent()) finish() } @@ -299,6 +302,7 @@ class LoginActivity : BaseActivity() { const val SOURCE_LOGOUT_BACKGROUND = "logout_background" const val SOURCE_SUGGESTED_EDITS = "suggestededits" const val SOURCE_TALK = "talk" + const val SOURCE_ACTIVITY = "activity" fun newIntent(context: Context, source: String, createAccountFirst: Boolean = true): Intent { return Intent(context, LoginActivity::class.java) From 46e2a401ece679c36315fcddc608fc2892409d5f Mon Sep 17 00:00:00 2001 From: Cooltey Feng Date: Mon, 18 Aug 2025 13:17:50 -0700 Subject: [PATCH 23/70] [Activity Tab] Create separate activity for Suggested Edits (#5824) * Create separate activity for Suggested Edits * Update manifest * Update strings * code review fixes --- app/src/main/AndroidManifest.xml | 4 + .../org/wikipedia/navtab/MenuNavTabDialog.kt | 9 ++ .../SuggestedEditsTasksActivity.kt | 22 +++++ .../SuggestedEditsTasksFragment.kt | 86 +++++++++++++++++-- .../layout/fragment_suggested_edits_tasks.xml | 3 +- .../suggested_edits_tasks_container.xml | 1 - ...ted_edits_tasks_contribution_container.xml | 3 +- app/src/main/res/layout/view_main_drawer.xml | 24 +++++- .../res/menu/menu_suggested_edits_tasks.xml | 10 +++ app/src/main/res/values-qq/strings.xml | 1 + app/src/main/res/values/strings.xml | 1 + 11 files changed, 154 insertions(+), 10 deletions(-) create mode 100644 app/src/main/java/org/wikipedia/suggestededits/SuggestedEditsTasksActivity.kt create mode 100644 app/src/main/res/menu/menu_suggested_edits_tasks.xml diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index ccfa3828f1a..52bcf1b655e 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -157,6 +157,10 @@ android:name=".suggestededits.SuggestionsActivity" android:launchMode="singleTask" android:windowSoftInputMode="adjustPan" /> + () { + + override fun onUnreadNotification() { + fragment.updateNotificationDot(true) + } + + override fun createFragment(): SuggestedEditsTasksFragment { + return SuggestedEditsTasksFragment.newInstance() + } + + companion object { + fun newIntent(context: Context): Intent { + return Intent(context, SuggestedEditsTasksActivity::class.java) + } + } +} diff --git a/app/src/main/java/org/wikipedia/suggestededits/SuggestedEditsTasksFragment.kt b/app/src/main/java/org/wikipedia/suggestededits/SuggestedEditsTasksFragment.kt index fb5834dcb70..3960363530f 100644 --- a/app/src/main/java/org/wikipedia/suggestededits/SuggestedEditsTasksFragment.kt +++ b/app/src/main/java/org/wikipedia/suggestededits/SuggestedEditsTasksFragment.kt @@ -1,12 +1,16 @@ package org.wikipedia.suggestededits import android.app.Activity -import android.net.Uri import android.os.Bundle import android.view.LayoutInflater +import android.view.Menu +import android.view.MenuInflater +import android.view.MenuItem import android.view.View import android.view.ViewGroup import androidx.activity.result.contract.ActivityResultContracts +import androidx.core.net.toUri +import androidx.core.view.MenuProvider import androidx.core.view.isVisible import androidx.core.widget.NestedScrollView import androidx.fragment.app.Fragment @@ -20,6 +24,7 @@ import kotlinx.coroutines.launch import org.wikipedia.Constants import org.wikipedia.R import org.wikipedia.WikipediaApp +import org.wikipedia.activitytab.ActivityTabABTest import org.wikipedia.analytics.eventplatform.BreadCrumbLogEvent import org.wikipedia.analytics.eventplatform.ImageRecommendationsEvent import org.wikipedia.analytics.eventplatform.PatrollerExperienceEvent @@ -37,6 +42,7 @@ import org.wikipedia.events.LoggedOutEvent import org.wikipedia.login.LoginActivity import org.wikipedia.main.MainActivity import org.wikipedia.navtab.NavTab +import org.wikipedia.notifications.NotificationActivity import org.wikipedia.settings.Prefs import org.wikipedia.settings.languages.WikipediaLanguagesActivity import org.wikipedia.usercontrib.UserContribListActivity @@ -49,10 +55,11 @@ import org.wikipedia.util.ResourceUtil import org.wikipedia.util.UriUtil import org.wikipedia.views.DefaultRecyclerAdapter import org.wikipedia.views.DefaultViewHolder +import org.wikipedia.views.NotificationButtonView import java.time.LocalDateTime import java.time.ZoneId -class SuggestedEditsTasksFragment : Fragment() { +class SuggestedEditsTasksFragment : Fragment(), MenuProvider { private var _binding: FragmentSuggestedEditsTasksBinding? = null private val binding get() = _binding!! @@ -64,9 +71,13 @@ class SuggestedEditsTasksFragment : Fragment() { private lateinit var imageRecommendationsTask: SuggestedEditsTask private lateinit var vandalismPatrolTask: SuggestedEditsTask + private var notificationButtonView: NotificationButtonView? = null + private val displayedTasks = ArrayList() private val callback = TaskViewCallback() + private val inActivityAbTestGroup = ActivityTabABTest().isInTestGroup() + private val sequentialTooltipRunnable = Runnable { if (!isAdded) { return@Runnable @@ -107,6 +118,11 @@ class SuggestedEditsTasksFragment : Fragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) setupTestingButtons() + if (inActivityAbTestGroup) { + notificationButtonView = NotificationButtonView(requireContext()) + requireActivity().addMenuProvider(this, viewLifecycleOwner, Lifecycle.State.RESUMED) + } + binding.layoutContributionsContainer.contributionsContainer.setOnClickListener { startActivity(UserContribListActivity.newIntent(requireActivity(), AccountUtil.userName)) } @@ -126,6 +142,9 @@ class SuggestedEditsTasksFragment : Fragment() { } binding.suggestedEditsScrollView.setOnScrollChangeListener(NestedScrollView.OnScrollChangeListener { _, _, scrollY, _, _ -> + if (inActivityAbTestGroup) { + return@OnScrollChangeListener + } (requireActivity() as MainActivity).updateToolbarElevation(scrollY > 0) }) tasksContainer.tasksRecyclerView.layoutManager = LinearLayoutManager(context) @@ -147,8 +166,7 @@ class SuggestedEditsTasksFragment : Fragment() { launch { FlowEventBus.events.collectLatest { event -> - if (event is LoggedOutEvent && - (requireActivity() as MainActivity).isCurrentFragmentSelected(this@SuggestedEditsTasksFragment)) { + if (event is LoggedOutEvent) { refreshContents() } } @@ -158,7 +176,9 @@ class SuggestedEditsTasksFragment : Fragment() { } fun refreshContents() { - (requireActivity() as MainActivity).onTabChanged(NavTab.EDITS) + if (!inActivityAbTestGroup) { + (requireActivity() as MainActivity).onTabChanged(NavTab.EDITS) + } requireActivity().invalidateOptionsMenu() viewModel.fetchData() } @@ -168,6 +188,50 @@ class SuggestedEditsTasksFragment : Fragment() { refreshContents() } + override fun onCreateMenu(menu: Menu, inflater: MenuInflater) { + inflater.inflate(R.menu.menu_suggested_edits_tasks, menu) + } + + override fun onMenuItemSelected(menuItem: MenuItem): Boolean { + return false + } + + override fun onPrepareMenu(menu: Menu) { + notificationButtonView?.let { + val notificationMenuItem = menu.findItem(R.id.menu_notifications) + if (AccountUtil.isLoggedIn) { + notificationMenuItem.isVisible = true + it.setUnreadCount(Prefs.notificationUnreadCount) + it.setOnClickListener { + if (AccountUtil.isLoggedIn) { + startActivity(NotificationActivity.newIntent(requireActivity())) + } + } + it.contentDescription = + getString(R.string.notifications_activity_title) + notificationMenuItem.actionView = it + notificationMenuItem.expandActionView() + FeedbackUtil.setButtonTooltip(it) + } else { + notificationMenuItem.isVisible = false + } + updateNotificationDot(false) + } + } + + fun updateNotificationDot(animate: Boolean) { + notificationButtonView?.let { + if (AccountUtil.isLoggedIn && Prefs.notificationUnreadCount > 0) { + it.setUnreadCount(Prefs.notificationUnreadCount) + if (animate) { + it.runAnimation() + } + } else { + it.setUnreadCount(0) + } + } + } + override fun onDestroyView() { binding.layoutTasksContainer.tasksRecyclerView.adapter = null binding.suggestedEditsScrollView.removeCallbacks(sequentialTooltipRunnable) @@ -261,6 +325,13 @@ class SuggestedEditsTasksFragment : Fragment() { private fun setUserStatsViewsAndTooltips() { val contributionContainer = binding.layoutContributionsContainer + + if (inActivityAbTestGroup) { + contributionContainer.root.isVisible = false + binding.layoutTasksContainer.contributeSubtitleView.isVisible = false + return + } + contributionContainer.editsCountStatsView.setImageDrawable(R.drawable.ic_mode_edit_white_24dp) contributionContainer.editsCountStatsView.tooltipText = getString(R.string.suggested_edits_contributions_stat_tooltip) @@ -278,6 +349,9 @@ class SuggestedEditsTasksFragment : Fragment() { } private fun showOneTimeSequentialUserStatsTooltips() { + if (inActivityAbTestGroup) { + return + } binding.suggestedEditsScrollView.fullScroll(View.FOCUS_UP) binding.suggestedEditsScrollView.removeCallbacks(sequentialTooltipRunnable) binding.suggestedEditsScrollView.postDelayed(sequentialTooltipRunnable, 500) @@ -300,7 +374,7 @@ class SuggestedEditsTasksFragment : Fragment() { clearContents() binding.messageCard.setDisabled(getString(R.string.suggested_edits_gate_message, AccountUtil.userName)) binding.messageCard.setPositiveButton(R.string.suggested_edits_learn_more, { - UriUtil.visitInExternalBrowser(requireContext(), Uri.parse(MIN_CONTRIBUTIONS_GATE_URL)) + UriUtil.visitInExternalBrowser(requireContext(), MIN_CONTRIBUTIONS_GATE_URL.toUri()) }, true) binding.messageCard.isVisible = true return true diff --git a/app/src/main/res/layout/fragment_suggested_edits_tasks.xml b/app/src/main/res/layout/fragment_suggested_edits_tasks.xml index 5f0f7819697..aa74c2bc4c6 100644 --- a/app/src/main/res/layout/fragment_suggested_edits_tasks.xml +++ b/app/src/main/res/layout/fragment_suggested_edits_tasks.xml @@ -42,9 +42,10 @@ android:id="@+id/messageCard" android:layout_width="match_parent" android:layout_height="wrap_content" - android:layout_marginVertical="16dp" + android:layout_marginTop="16dp" android:layout_marginStart="@dimen/activity_horizontal_margin" android:layout_marginEnd="@dimen/activity_horizontal_margin" + android:layout_marginBottom="32dp" android:visibility="gone" /> + android:layout_marginTop="8dp" + android:layout_marginBottom="32dp"> + android:layout_height="wrap_content" + tools:ignore="UseCompoundDrawables,UselessParent"> + + + + + + + diff --git a/app/src/main/res/menu/menu_suggested_edits_tasks.xml b/app/src/main/res/menu/menu_suggested_edits_tasks.xml new file mode 100644 index 00000000000..898cac1feb7 --- /dev/null +++ b/app/src/main/res/menu/menu_suggested_edits_tasks.xml @@ -0,0 +1,10 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/values-qq/strings.xml b/app/src/main/res/values-qq/strings.xml index 54b1e481fbc..bc63f79527e 100644 --- a/app/src/main/res/values-qq/strings.xml +++ b/app/src/main/res/values-qq/strings.xml @@ -964,6 +964,7 @@ Button label for resetting the configuration of customize toolbar to default. Default tooltip text for customize toolbar drag handle Default tooltip text for customize toolbar item + Title of the suggested edits screen. Title of screen where the user chooses articles that are missing article descriptions to be described. Title of screen where the user chooses images that are missing captions to be captioned. Title of screen where the user chooses tags that represent an image. diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index bde2e3d2046..de4f70f0d0b 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -977,6 +977,7 @@ + Edit Describe articles Caption images Tag images From c126c8cc99cdebdb024f25cf8e07041bd0017656 Mon Sep 17 00:00:00 2001 From: Cooltey Feng Date: Mon, 18 Aug 2025 15:51:37 -0700 Subject: [PATCH 24/70] Add chevron icon to the donation module --- .../wikipedia/activitytab/DonationModule.kt | 31 +++++++++++++------ 1 file changed, 21 insertions(+), 10 deletions(-) diff --git a/app/src/main/java/org/wikipedia/activitytab/DonationModule.kt b/app/src/main/java/org/wikipedia/activitytab/DonationModule.kt index 49b5a58c219..4e6dad151a9 100644 --- a/app/src/main/java/org/wikipedia/activitytab/DonationModule.kt +++ b/app/src/main/java/org/wikipedia/activitytab/DonationModule.kt @@ -65,20 +65,31 @@ fun DonationModule( modifier = Modifier.padding(16.dp) ) { Row( - horizontalArrangement = Arrangement.spacedBy(8.dp) + modifier = Modifier.fillMaxWidth(), ) { + Row( + modifier = Modifier.weight(1f), + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + Icon( + modifier = Modifier.size(16.dp), + painter = painterResource(R.drawable.outline_credit_card_heart_24), + tint = WikipediaTheme.colors.primaryColor, + contentDescription = null + ) + HtmlText( + text = stringResource(R.string.activity_tab_donation_last_donation), + style = MaterialTheme.typography.labelMedium.copy(fontWeight = FontWeight.Normal), + color = WikipediaTheme.colors.primaryColor, + lineHeight = MaterialTheme.typography.labelMedium.lineHeight + ) + } Icon( - modifier = Modifier.size(16.dp), - painter = painterResource(R.drawable.outline_credit_card_heart_24), - tint = WikipediaTheme.colors.primaryColor, + modifier = Modifier.size(24.dp), + painter = painterResource(R.drawable.ic_chevron_forward_white_24dp), + tint = WikipediaTheme.colors.secondaryColor, contentDescription = null ) - HtmlText( - text = stringResource(R.string.activity_tab_donation_last_donation), - style = MaterialTheme.typography.labelMedium.copy(fontWeight = FontWeight.Normal), - color = WikipediaTheme.colors.primaryColor, - lineHeight = MaterialTheme.typography.labelMedium.lineHeight - ) } Text( modifier = Modifier.padding(top = 16.dp), From b5e47fea68dd01727b7294077ca61cac87cb67da Mon Sep 17 00:00:00 2001 From: William Rai <48931640+Williamrai@users.noreply.github.com> Date: Tue, 19 Aug 2025 08:16:49 -0400 Subject: [PATCH 25/70] Activity Tab Customization Screen (#5847) * - adds ActivityTabOverflowMenu, customization screen, preferences for customization * - adds edge to edge, code fixes * - removes ActivityTabOverflowMenu and use addMenuProvider - updates menu items to show icons - adds logic to show/hide modules in the activity tab screen - adds strings * - update strings position and merge fixes * - update logic to show modules in activity tab screen * - not show horizontal divider on the last module type * - string update * - string update and code fixes * - string update --- app/src/main/AndroidManifest.xml | 3 + .../ActivityTabCustomizationActivity.kt | 184 ++++++++++++++++++ .../activitytab/ActivityTabFragment.kt | 139 +++++++++---- .../main/java/org/wikipedia/settings/Prefs.kt | 6 + .../res/menu/menu_activity_tab_overflow.xml | 28 +++ app/src/main/res/values-qq/strings.xml | 11 ++ app/src/main/res/values/preference_keys.xml | 2 + app/src/main/res/values/strings.xml | 11 ++ 8 files changed, 341 insertions(+), 43 deletions(-) create mode 100644 app/src/main/java/org/wikipedia/activitytab/ActivityTabCustomizationActivity.kt create mode 100644 app/src/main/res/menu/menu_activity_tab_overflow.xml diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 52bcf1b655e..a749cf697b1 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -380,6 +380,9 @@ android:name=".readinglist.recommended.RecommendedReadingListSettingsActivity" android:windowSoftInputMode="adjustResize" /> + + diff --git a/app/src/main/java/org/wikipedia/activitytab/ActivityTabCustomizationActivity.kt b/app/src/main/java/org/wikipedia/activitytab/ActivityTabCustomizationActivity.kt new file mode 100644 index 00000000000..9add886c63d --- /dev/null +++ b/app/src/main/java/org/wikipedia/activitytab/ActivityTabCustomizationActivity.kt @@ -0,0 +1,184 @@ +package org.wikipedia.activitytab + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import androidx.activity.compose.setContent +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.safeDrawingPadding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.ListItem +import androidx.compose.material3.ListItemDefaults +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Switch +import androidx.compose.material3.SwitchDefaults +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.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import kotlinx.serialization.Serializable +import org.wikipedia.R +import org.wikipedia.activity.BaseActivity +import org.wikipedia.compose.components.WikiTopAppBar +import org.wikipedia.compose.theme.BaseTheme +import org.wikipedia.compose.theme.WikipediaTheme +import org.wikipedia.settings.Prefs +import org.wikipedia.theme.Theme +import org.wikipedia.util.DeviceUtil + +class ActivityTabCustomizationActivity : BaseActivity() { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + DeviceUtil.setEdgeToEdge(this) + setContent { + BaseTheme { + CustomizationScreen( + onBackButtonClick = { + finish() + } + ) + } + } + } + + companion object { + fun newIntent(context: Context): Intent { + return Intent(context, ActivityTabCustomizationActivity::class.java) + } + } +} + +@Composable +fun CustomizationScreen( + modifier: Modifier = Modifier, + onBackButtonClick: () -> Unit +) { + var currentModules by remember { mutableStateOf(Prefs.activityTabModules) } + + Scaffold( + modifier = modifier + .safeDrawingPadding(), + topBar = { + WikiTopAppBar( + title = stringResource(R.string.activity_tab_customize_screen_title), + onNavigationClick = onBackButtonClick + ) + }, + containerColor = WikipediaTheme.colors.backgroundColor, + content = { paddingValues -> + LazyColumn( + modifier = Modifier + .padding(paddingValues) + .padding(vertical = 24.dp) + ) { + itemsIndexed(ModuleType.entries) { index, moduleType -> + CustomizationScreenSwitch( + isChecked = currentModules.isModuleEnabled(moduleType), + title = stringResource(moduleType.displayName), + onCheckedChange = { isChecked -> + currentModules = currentModules.setModuleEnabled(moduleType, isChecked) + Prefs.activityTabModules = currentModules + } + ) + if (index < ModuleType.entries.size - 1) { + HorizontalDivider( + color = WikipediaTheme.colors.borderColor + ) + } + } + } + } + ) +} + +@Composable +private fun CustomizationScreenSwitch( + isChecked: Boolean, + title: String, + onCheckedChange: ((Boolean) -> Unit), + modifier: Modifier = Modifier +) { + ListItem( + modifier = modifier, + colors = ListItemDefaults.colors( + containerColor = WikipediaTheme.colors.paperColor + ), + headlineContent = { + Text( + text = title, + style = MaterialTheme.typography.bodyLarge, + color = WikipediaTheme.colors.primaryColor + ) + }, + trailingContent = { + Switch( + checked = isChecked, + onCheckedChange = { + onCheckedChange(it) + }, + colors = SwitchDefaults.colors( + uncheckedTrackColor = WikipediaTheme.colors.paperColor, + uncheckedThumbColor = MaterialTheme.colorScheme.outline, + uncheckedBorderColor = MaterialTheme.colorScheme.outline, + checkedTrackColor = WikipediaTheme.colors.progressiveColor, + checkedThumbColor = WikipediaTheme.colors.paperColor + ) + ) + } + ) +} + +fun ActivityTabModules.isModuleEnabled(moduleType: ModuleType): Boolean = when (moduleType) { + ModuleType.READING_HISTORY -> isReadingHistoryEnabled + ModuleType.IMPACT -> isImpactEnabled + ModuleType.GAMES -> isGamesEnabled + ModuleType.DONATIONS -> isDonationsEnabled + ModuleType.TIMELINE -> isTimelineEnabled +} + +fun ActivityTabModules.setModuleEnabled(moduleType: ModuleType, enabled: Boolean) = when (moduleType) { + ModuleType.READING_HISTORY -> copy(isReadingHistoryEnabled = enabled) + ModuleType.IMPACT -> copy(isImpactEnabled = enabled) + ModuleType.GAMES -> copy(isGamesEnabled = enabled) + ModuleType.DONATIONS -> copy(isDonationsEnabled = enabled) + ModuleType.TIMELINE -> copy(isTimelineEnabled = enabled) +} + +@Serializable +data class ActivityTabModules( + val isReadingHistoryEnabled: Boolean = true, + val isImpactEnabled: Boolean = true, + val isGamesEnabled: Boolean = true, + val isDonationsEnabled: Boolean = false, + val isTimelineEnabled: Boolean = true, +) + +enum class ModuleType(val displayName: Int) { + READING_HISTORY(R.string.activity_tab_customize_screen_reading_history_switch_title), + IMPACT(R.string.activity_tab_customize_screen_impact_switch_title), + GAMES(R.string.activity_tab_customize_screen_games_switch_title), + DONATIONS(R.string.activity_tab_customize_screen_donations_switch_title), + TIMELINE(R.string.activity_tab_customize_screen_timeline_switch_title) +} + +@Preview +@Composable +private fun CustomizationScreenPreview() { + BaseTheme( + currentTheme = Theme.LIGHT + ) { + CustomizationScreen( + onBackButtonClick = {} + ) + } +} diff --git a/app/src/main/java/org/wikipedia/activitytab/ActivityTabFragment.kt b/app/src/main/java/org/wikipedia/activitytab/ActivityTabFragment.kt index 1b5cf04e1d2..9042d0c6a08 100644 --- a/app/src/main/java/org/wikipedia/activitytab/ActivityTabFragment.kt +++ b/app/src/main/java/org/wikipedia/activitytab/ActivityTabFragment.kt @@ -2,6 +2,9 @@ package org.wikipedia.activitytab import android.os.Bundle import android.view.LayoutInflater +import android.view.Menu +import android.view.MenuInflater +import android.view.MenuItem import android.view.View import android.view.ViewGroup import androidx.compose.foundation.Image @@ -42,6 +45,7 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.core.net.toUri +import androidx.core.view.MenuProvider import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels import androidx.lifecycle.Lifecycle @@ -68,6 +72,7 @@ import org.wikipedia.navtab.NavTab import org.wikipedia.page.PageTitle import org.wikipedia.settings.Prefs import org.wikipedia.theme.Theme +import org.wikipedia.util.FeedbackUtil import org.wikipedia.util.UiState import java.time.LocalDateTime @@ -77,10 +82,20 @@ class ActivityTabFragment : Fragment() { } private val viewModel: ActivityTabViewModel by viewModels() + private val menuProvider = object : MenuProvider { + override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) { + menuInflater.inflate(R.menu.menu_activity_tab_overflow, menu) + } + + override fun onMenuItemSelected(menuItem: MenuItem): Boolean { + return handleMenuItemClick(menuItem) + } + } override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { super.onCreateView(inflater, container, savedInstanceState) Prefs.activityTabRedDotShown = true + requireActivity().addMenuProvider(menuProvider, viewLifecycleOwner) viewLifecycleOwner.lifecycleScope.launch { repeatOnLifecycle(Lifecycle.State.RESUMED) { @@ -109,6 +124,7 @@ class ActivityTabFragment : Fragment() { override fun onResume() { super.onResume() viewModel.loadAll() + requireActivity().invalidateOptionsMenu() } @OptIn(ExperimentalMaterial3Api::class) @@ -127,7 +143,7 @@ class ActivityTabFragment : Fragment() { ) { paddingValues -> var isRefreshing by remember { mutableStateOf(false) } val state = rememberPullToRefreshState() - + val modules = Prefs.activityTabModules if (readingHistoryState is UiState.Success) { isRefreshing = false } @@ -141,7 +157,8 @@ class ActivityTabFragment : Fragment() { val scrollState = rememberScrollState() Column( - modifier = Modifier.align(Alignment.Center).padding(horizontal = 16.dp).verticalScroll(scrollState), + modifier = Modifier.align(Alignment.Center).padding(horizontal = 16.dp) + .verticalScroll(scrollState), horizontalAlignment = Alignment.CenterHorizontally ) { Image( @@ -164,7 +181,12 @@ class ActivityTabFragment : Fragment() { contentColor = WikipediaTheme.colors.paperColor, ), onClick = { - startActivity(LoginActivity.newIntent(requireContext(), LoginActivity.SOURCE_ACTIVITY)) + startActivity( + LoginActivity.newIntent( + requireContext(), + LoginActivity.SOURCE_ACTIVITY + ) + ) }, ) { Icon( @@ -185,7 +207,13 @@ class ActivityTabFragment : Fragment() { contentColor = WikipediaTheme.colors.primaryColor, ), onClick = { - startActivity(LoginActivity.newIntent(requireContext(), LoginActivity.SOURCE_ACTIVITY, createAccountFirst = false)) + startActivity( + LoginActivity.newIntent( + requireContext(), + LoginActivity.SOURCE_ACTIVITY, + createAccountFirst = false + ) + ) }, ) { Text( @@ -217,45 +245,47 @@ class ActivityTabFragment : Fragment() { } ) { LazyColumn { - item { - Column( - modifier = Modifier - .fillMaxWidth() - .padding(paddingValues) - .background( - brush = Brush.verticalGradient( - colors = listOf( - WikipediaTheme.colors.paperColor, - WikipediaTheme.colors.additionColor + if (modules.isModuleEnabled(ModuleType.READING_HISTORY)) { + item { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(paddingValues) + .background( + brush = Brush.verticalGradient( + colors = listOf( + WikipediaTheme.colors.paperColor, + WikipediaTheme.colors.additionColor + ) ) ) - ) - ) { - ReadingHistoryModule( - modifier = Modifier.align(Alignment.CenterHorizontally), - userName = userName, - readingHistoryState = readingHistoryState, - onArticlesReadClick = { callback()?.onNavigateTo(NavTab.SEARCH) }, - onArticlesSavedClick = { callback()?.onNavigateTo(NavTab.READING_LISTS) }, - onExploreClick = { callback()?.onNavigateTo(NavTab.EXPLORE) }, - onCategoryItemClick = { category -> - val pageTitle = viewModel.createPageTitleForCategory(category) - startActivity( - CategoryActivity.newIntent( - requireActivity(), - pageTitle + ) { + ReadingHistoryModule( + modifier = Modifier.align(Alignment.CenterHorizontally), + userName = userName, + readingHistoryState = readingHistoryState, + onArticlesReadClick = { callback()?.onNavigateTo(NavTab.SEARCH) }, + onArticlesSavedClick = { callback()?.onNavigateTo(NavTab.READING_LISTS) }, + onExploreClick = { callback()?.onNavigateTo(NavTab.EXPLORE) }, + onCategoryItemClick = { category -> + val pageTitle = + viewModel.createPageTitleForCategory(category) + startActivity( + CategoryActivity.newIntent( + requireActivity(), + pageTitle + ) ) + }, + wikiErrorClickEvents = WikiErrorClickEvents( + retryClickListener = { + viewModel.loadReadingHistory() + } ) - }, - wikiErrorClickEvents = WikiErrorClickEvents( - retryClickListener = { - viewModel.loadReadingHistory() - } ) - ) + } } } - item { Column( modifier = Modifier @@ -270,12 +300,15 @@ class ActivityTabFragment : Fragment() { ) ) ) { - // impact module + if (modules.isModuleEnabled(ModuleType.IMPACT)) { + // @TODO: MARK_ACTIVITY_TAB + } - // game module + if (modules.isModuleEnabled(ModuleType.GAMES)) { + // @TODO: MARK_ACTIVITY_TAB + } - if (donationUiState is UiState.Success) { - // TODO: default is off. Handle this when building the configuration screen. + if (modules.isModuleEnabled(ModuleType.DONATIONS)) { DonationModule( modifier = Modifier .fillMaxWidth() @@ -291,9 +324,9 @@ class ActivityTabFragment : Fragment() { } } - // --- new column --- - - // timeline module + if (modules.isModuleEnabled(ModuleType.TIMELINE)) { + // @TODO: MARK_ACTIVITY_TAB + } } } } @@ -385,6 +418,26 @@ class ActivityTabFragment : Fragment() { } } + private fun handleMenuItemClick(menuItem: MenuItem): Boolean { + return when (menuItem.itemId) { + R.id.menu_customize_activity_tab -> { + startActivity(ActivityTabCustomizationActivity.newIntent(requireContext())) + true + } + R.id.menu_learn_more -> { + // TODO: MARK_ACTIVITY_TAB --> add mediawiki page link + true + } + R.id.menu_report_feature -> { + FeedbackUtil.composeEmail(requireContext(), + subject = getString(R.string.activity_tab_report_email_subject), + body = getString(R.string.activity_tab_report_email_body)) + true + } + else -> false + } + } + private fun callback(): Callback? { return getCallback(this, Callback::class.java) } diff --git a/app/src/main/java/org/wikipedia/settings/Prefs.kt b/app/src/main/java/org/wikipedia/settings/Prefs.kt index 90147ae1478..7b86f16c4ca 100644 --- a/app/src/main/java/org/wikipedia/settings/Prefs.kt +++ b/app/src/main/java/org/wikipedia/settings/Prefs.kt @@ -7,6 +7,7 @@ import okhttp3.logging.HttpLoggingInterceptor import org.wikipedia.BuildConfig import org.wikipedia.R import org.wikipedia.WikipediaApp +import org.wikipedia.activitytab.ActivityTabModules import org.wikipedia.analytics.SessionData import org.wikipedia.analytics.eventplatform.AppSessionEvent import org.wikipedia.analytics.eventplatform.StreamConfig @@ -839,4 +840,9 @@ object Prefs { PrefsIoUtil.getString(R.string.preference_key_donation_reminder_config, null) ) ?: DonationReminderConfig() set(types) = PrefsIoUtil.setString(R.string.preference_key_donation_reminder_config, JsonUtil.encodeToString(types)) + + var activityTabModules: ActivityTabModules + get() = JsonUtil.decodeFromString(PrefsIoUtil.getString(R.string.preference_key_activity_tab_modules, null)) + ?: ActivityTabModules() + set(modules) = PrefsIoUtil.setString(R.string.preference_key_activity_tab_modules, JsonUtil.encodeToString(modules)) } diff --git a/app/src/main/res/menu/menu_activity_tab_overflow.xml b/app/src/main/res/menu/menu_activity_tab_overflow.xml new file mode 100644 index 00000000000..e7dda08e6fe --- /dev/null +++ b/app/src/main/res/menu/menu_activity_tab_overflow.xml @@ -0,0 +1,28 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values-qq/strings.xml b/app/src/main/res/values-qq/strings.xml index bc63f79527e..27e2a1b9a0a 100644 --- a/app/src/main/res/values-qq/strings.xml +++ b/app/src/main/res/values-qq/strings.xml @@ -1184,6 +1184,17 @@ Label encouraging the user to discover new articles. Label on card that shows the last donation time. Subtitle of the donation card that indicates last donation. + Subject heading for sending a report about a problem with the Activity tab feature. + Body of email for sending a report about a problem with the Activity tab feature. + Menu label for providing feedback on the activity tab. + Menu label for visiting the information page of the activity tab. + Menu label for opening the activity tab customization screen. + Title of the switch for the Reading History module in the customize screen. + Title of the switch for the Impact module in the customize screen. + Title of the switch for the Games module in the customize screen. + Title of the switch for the Donations module in the customize screen. + Title of the switch for the Timeline module in the customize screen. + Title shown at the top of the activity for the customize screen. Title shown at the top of the activity for the file page. Button label to add image caption for the file. Button label to add image tags for the file. diff --git a/app/src/main/res/values/preference_keys.xml b/app/src/main/res/values/preference_keys.xml index 4867e1af5ad..d21d82dfb90 100644 --- a/app/src/main/res/values/preference_keys.xml +++ b/app/src/main/res/values/preference_keys.xml @@ -200,4 +200,6 @@ donationReminderDevReset donationReminderDevResetSeenDate donations + activityTabModules + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index de4f70f0d0b..7a780ba9bc9 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1203,6 +1203,17 @@ Discover something new to read Unknown Last donation in app]]> + Issue Report - Activity Tab + I have encountered a problem with Activity Tab Feature:\n- [Describe specific problem]\n\nThe behavior I would like to see is:\n- [Describe proposed solution] + Problem with feature + Learn more + Customize + Reading History + Impact + Games + Donations + Timeline + Customize From 044126e21b6efe1355414ed18e3ad6506bb29c67 Mon Sep 17 00:00:00 2001 From: Cooltey Feng Date: Tue, 19 Aug 2025 05:52:52 -0700 Subject: [PATCH 26/70] Activity Tab: Game Module (#5848) * Activity Tab: Game Module * WikiGames entry card * Lint * block * Finish game module UIs * Wire it up * Final tweak * Lint --------- Co-authored-by: Dmitry Brant --- app/src/main/java/org/wikipedia/Constants.kt | 3 +- .../activitytab/ActivityTabFragment.kt | 50 ++- .../activitytab/ActivityTabViewModel.kt | 22 ++ .../wikipedia/activitytab/WikiGamesModule.kt | 307 ++++++++++++++++++ .../wikipedia/games/db/DailyGameHistoryDao.kt | 26 ++ .../games/onthisday/OnThisDayGameViewModel.kt | 17 +- .../res/drawable/baseline_extension_24.xml | 5 + .../res/drawable/filled_family_star_24.xml | 13 + .../res/drawable/outline_motion_blur_24.xml | 5 + .../res/drawable/outline_sports_score_24.xml | 5 + app/src/main/res/values-qq/strings.xml | 7 + app/src/main/res/values/strings.xml | 7 + 12 files changed, 454 insertions(+), 13 deletions(-) create mode 100644 app/src/main/java/org/wikipedia/activitytab/WikiGamesModule.kt create mode 100644 app/src/main/res/drawable/baseline_extension_24.xml create mode 100644 app/src/main/res/drawable/filled_family_star_24.xml create mode 100644 app/src/main/res/drawable/outline_motion_blur_24.xml create mode 100644 app/src/main/res/drawable/outline_sports_score_24.xml diff --git a/app/src/main/java/org/wikipedia/Constants.kt b/app/src/main/java/org/wikipedia/Constants.kt index 0372ec7d770..80ab1f59c49 100644 --- a/app/src/main/java/org/wikipedia/Constants.kt +++ b/app/src/main/java/org/wikipedia/Constants.kt @@ -108,7 +108,8 @@ object Constants { USER_CONTRIB_ACTIVITY("userContribActivity"), EDIT_ADD_IMAGE("editAddImage"), SUGGESTED_EDITS_RECENT_EDITS("suggestedEditsRecentEdits"), - ON_THIS_DAY_GAME_ACTIVITY("onThisDayGame") + ON_THIS_DAY_GAME_ACTIVITY("onThisDayGame"), + ACTIVITY_TAB("activityTab") } enum class ImageEditType(name: String) { diff --git a/app/src/main/java/org/wikipedia/activitytab/ActivityTabFragment.kt b/app/src/main/java/org/wikipedia/activitytab/ActivityTabFragment.kt index 9042d0c6a08..64580ef8f12 100644 --- a/app/src/main/java/org/wikipedia/activitytab/ActivityTabFragment.kt +++ b/app/src/main/java/org/wikipedia/activitytab/ActivityTabFragment.kt @@ -53,7 +53,9 @@ import androidx.lifecycle.lifecycleScope import androidx.lifecycle.repeatOnLifecycle import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch +import org.wikipedia.Constants import org.wikipedia.R +import org.wikipedia.WikipediaApp import org.wikipedia.activity.BaseActivity import org.wikipedia.activity.FragmentUtil.getCallback import org.wikipedia.auth.AccountUtil @@ -67,6 +69,8 @@ import org.wikipedia.dataclient.WikiSite import org.wikipedia.events.LoggedInEvent import org.wikipedia.events.LoggedOutEvent import org.wikipedia.events.LoggedOutInBackgroundEvent +import org.wikipedia.games.onthisday.OnThisDayGameActivity +import org.wikipedia.games.onthisday.OnThisDayGameViewModel import org.wikipedia.login.LoginActivity import org.wikipedia.navtab.NavTab import org.wikipedia.page.PageTitle @@ -114,7 +118,8 @@ class ActivityTabFragment : Fragment() { isLoggedIn = AccountUtil.isLoggedIn, userName = AccountUtil.userName, readingHistoryState = viewModel.readingHistoryState.collectAsState().value, - donationUiState = viewModel.donationUiState.collectAsState().value + donationUiState = viewModel.donationUiState.collectAsState().value, + wikiGamesUiState = viewModel.wikiGamesUiState.collectAsState().value ) } } @@ -133,7 +138,8 @@ class ActivityTabFragment : Fragment() { isLoggedIn: Boolean, userName: String, readingHistoryState: UiState, - donationUiState: UiState + donationUiState: UiState, + wikiGamesUiState: UiState ) { Scaffold( modifier = Modifier @@ -229,8 +235,7 @@ class ActivityTabFragment : Fragment() { PullToRefreshBox( onRefresh = { isRefreshing = true - viewModel.loadReadingHistory() - viewModel.loadDonationResults() + viewModel.loadAll() }, isRefreshing = isRefreshing, state = state, @@ -305,7 +310,28 @@ class ActivityTabFragment : Fragment() { } if (modules.isModuleEnabled(ModuleType.GAMES)) { - // @TODO: MARK_ACTIVITY_TAB + WikiGamesModule( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 16.dp, horizontal = 16.dp), + uiState = wikiGamesUiState, + onEntryCardClick = { + requireActivity().startActivity(OnThisDayGameActivity.newIntent( + context = requireContext(), + invokeSource = Constants.InvokeSource.ACTIVITY_TAB, + wikiSite = WikipediaApp.instance.wikiSite + )) + }, + onStatsCardClick = { + // TODO: ask PM if these two actions should be the same. + requireActivity().startActivity(OnThisDayGameActivity.newIntent( + context = requireContext(), + invokeSource = Constants.InvokeSource.ACTIVITY_TAB, + + wikiSite = WikipediaApp.instance.wikiSite + )) + } + ) } if (modules.isModuleEnabled(ModuleType.DONATIONS)) { @@ -359,7 +385,13 @@ class ActivityTabFragment : Fragment() { Category(2025, 1, "Category:World literature", "en", 1), ) )), - donationUiState = UiState.Success("5 days ago") + donationUiState = UiState.Success("5 days ago"), + wikiGamesUiState = UiState.Success(OnThisDayGameViewModel.GameStatistics( + totalGamesPlayed = 10, + averageScore = 4.5, + currentStreak = 15, + bestStreak = 25 + )) ) } } @@ -381,7 +413,8 @@ class ActivityTabFragment : Fragment() { articlesSaved = emptyList(), topCategories = emptyList() )), - donationUiState = UiState.Success("Unknown") + donationUiState = UiState.Success("Unknown"), + wikiGamesUiState = UiState.Success(null) ) } } @@ -403,7 +436,8 @@ class ActivityTabFragment : Fragment() { articlesSaved = emptyList(), topCategories = emptyList() )), - donationUiState = UiState.Success("Unknown") + donationUiState = UiState.Success("Unknown"), + wikiGamesUiState = UiState.Success(null) ) } } diff --git a/app/src/main/java/org/wikipedia/activitytab/ActivityTabViewModel.kt b/app/src/main/java/org/wikipedia/activitytab/ActivityTabViewModel.kt index 7f9912f7b13..5bc2e459575 100644 --- a/app/src/main/java/org/wikipedia/activitytab/ActivityTabViewModel.kt +++ b/app/src/main/java/org/wikipedia/activitytab/ActivityTabViewModel.kt @@ -9,9 +9,11 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch +import org.wikipedia.WikipediaApp import org.wikipedia.categories.db.Category import org.wikipedia.database.AppDatabase import org.wikipedia.dataclient.WikiSite +import org.wikipedia.games.onthisday.OnThisDayGameViewModel import org.wikipedia.page.PageTitle import org.wikipedia.readinglist.database.ReadingListPage import org.wikipedia.settings.Prefs @@ -30,9 +32,13 @@ class ActivityTabViewModel(savedStateHandle: SavedStateHandle) : ViewModel() { private val _donationUiState = MutableStateFlow>(UiState.Loading) val donationUiState: StateFlow> = _donationUiState.asStateFlow() + private val _wikiGamesUiState = MutableStateFlow>(UiState.Loading) + val wikiGamesUiState: StateFlow> = _wikiGamesUiState.asStateFlow() + fun loadAll() { loadReadingHistory() loadDonationResults() + loadWikiGamesStats() } fun loadReadingHistory() { @@ -90,6 +96,22 @@ class ActivityTabViewModel(savedStateHandle: SavedStateHandle) : ViewModel() { _donationUiState.value = UiState.Success(lastDonationTime) } + fun loadWikiGamesStats() { + viewModelScope.launch(CoroutineExceptionHandler { _, throwable -> + _wikiGamesUiState.value = UiState.Error(throwable) + }) { + _wikiGamesUiState.value = UiState.Loading + val lastGameHistory = AppDatabase.instance.dailyGameHistoryDao().findLastGameHistory() + if (lastGameHistory == null) { + _wikiGamesUiState.value = UiState.Success(null) + return@launch + } + + val gamesStats = OnThisDayGameViewModel.getGameStatistics(WikipediaApp.instance.wikiSite.languageCode) + _wikiGamesUiState.value = UiState.Success(gamesStats) + } + } + fun createPageTitleForCategory(category: Category): PageTitle { return PageTitle(title = category.title, wiki = WikiSite.forLanguageCode(category.lang)) } diff --git a/app/src/main/java/org/wikipedia/activitytab/WikiGamesModule.kt b/app/src/main/java/org/wikipedia/activitytab/WikiGamesModule.kt new file mode 100644 index 00000000000..b5826bf57cd --- /dev/null +++ b/app/src/main/java/org/wikipedia/activitytab/WikiGamesModule.kt @@ -0,0 +1,307 @@ +package org.wikipedia.activitytab + +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.intl.Locale +import androidx.compose.ui.text.toLowerCase +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import org.wikipedia.R +import org.wikipedia.compose.components.HtmlText +import org.wikipedia.compose.components.WikiCard +import org.wikipedia.compose.components.error.WikiErrorClickEvents +import org.wikipedia.compose.theme.BaseTheme +import org.wikipedia.compose.theme.WikipediaTheme +import org.wikipedia.games.onthisday.OnThisDayGameViewModel +import org.wikipedia.theme.Theme +import org.wikipedia.util.UiState + +@Composable +fun WikiGamesModule( + modifier: Modifier = Modifier, + uiState: UiState, + onEntryCardClick: (() -> Unit)? = null, + onStatsCardClick: (() -> Unit)? = null, + wikiErrorClickEvents: WikiErrorClickEvents? = null +) { + if (uiState == UiState.Loading) { + Box( + modifier = modifier + .fillMaxWidth() + .height(200.dp) + ) { + CircularProgressIndicator( + modifier = Modifier + .align(Alignment.Center) + .padding(24.dp), + color = WikipediaTheme.colors.progressiveColor + ) + } + } else if (uiState is UiState.Success) { + if (uiState.data == null) { + WikiGamesEntryCard( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + onClick = onEntryCardClick + ) + } else { + WikiGamesStatsCard( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + gameStatistics = uiState.data, + onClick = onStatsCardClick + ) + } + } +} + +@Composable +fun WikiGamesStatsCard( + modifier: Modifier = Modifier, + gameStatistics: OnThisDayGameViewModel.GameStatistics, + onClick: (() -> Unit)? = null +) { + WikiCard( + modifier = modifier + .clickable(onClick = { onClick?.invoke() }), + colors = CardDefaults.cardColors( + containerColor = WikipediaTheme.colors.paperColor, + contentColor = WikipediaTheme.colors.paperColor + ), + elevation = 0.dp, + border = BorderStroke( + width = 1.dp, + color = WikipediaTheme.colors.borderColor + ) + ) { + Column( + modifier = Modifier.padding(16.dp) + ) { + Row( + modifier = Modifier.fillMaxWidth(), + ) { + Text( + modifier = Modifier.weight(1f), + text = stringResource(R.string.activity_tab_game_stats), + style = MaterialTheme.typography.labelMedium.copy( + fontWeight = FontWeight.SemiBold + ), + color = WikipediaTheme.colors.primaryColor, + lineHeight = MaterialTheme.typography.labelMedium.lineHeight + ) + Icon( + modifier = Modifier.size(24.dp), + painter = painterResource(R.drawable.ic_chevron_forward_white_24dp), + tint = WikipediaTheme.colors.secondaryColor, + contentDescription = null + ) + } + Row( + modifier = Modifier + .fillMaxWidth() + .padding(top = 16.dp), + horizontalArrangement = Arrangement.spacedBy(16.dp) + ) { + WikiGamesStatView( + modifier = Modifier.weight(1f), + iconResource = R.drawable.baseline_extension_24, + statValue = gameStatistics.totalGamesPlayed.toString(), + statLabel = stringResource(R.string.activity_tab_game_stats_played) + ) + WikiGamesStatView( + modifier = Modifier.weight(1f), + iconResource = R.drawable.outline_motion_blur_24, + statValue = gameStatistics.currentStreak.toString(), + statLabel = stringResource(R.string.activity_tab_game_stats_current_streak) + ) + } + Row( + modifier = Modifier + .fillMaxWidth() + .padding(top = 16.dp), + horizontalArrangement = Arrangement.spacedBy(16.dp) + ) { + WikiGamesStatView( + modifier = Modifier.weight(1f), + iconResource = R.drawable.filled_family_star_24, + statValue = gameStatistics.bestStreak.toString(), + statLabel = stringResource(R.string.activity_tab_game_stats_best_streak) + ) + WikiGamesStatView( + modifier = Modifier.weight(1f), + iconResource = R.drawable.outline_sports_score_24, + statValue = gameStatistics.averageScore.toString(), + statLabel = stringResource(R.string.activity_tab_game_stats_average_score) + ) + } + } + } +} + +@Composable +fun WikiGamesStatView( + modifier: Modifier, + iconResource: Int, + statValue: String, + statLabel: String +) { + Row( + modifier = modifier + ) { + Icon( + modifier = Modifier.size(28.dp), + painter = painterResource(iconResource), + tint = WikipediaTheme.colors.progressiveColor, + contentDescription = null + ) + Column( + modifier = Modifier.padding(horizontal = 12.dp) + ) { + Text( + text = statValue, + style = MaterialTheme.typography.labelLarge.copy( + fontWeight = FontWeight.SemiBold + ), + color = WikipediaTheme.colors.primaryColor + ) + Text( + text = statLabel.toLowerCase(Locale.current), + style = MaterialTheme.typography.bodySmall, + color = WikipediaTheme.colors.primaryColor + ) + } + } +} + +@Composable +fun WikiGamesEntryCard( + modifier: Modifier, + onClick: (() -> Unit)? = null +) { + WikiCard( + modifier = modifier + .clickable(onClick = { onClick?.invoke() }), + elevation = 0.dp, + colors = CardDefaults.cardColors( + containerColor = WikipediaTheme.colors.progressiveColor, + contentColor = WikipediaTheme.colors.progressiveColor + ) + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + Column( + modifier = Modifier.weight(1f) + .heightIn(min = 132.dp) + .padding(horizontal = 4.dp), + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + Text( + text = stringResource(R.string.activity_tab_game_entry_title), + style = MaterialTheme.typography.headlineSmall, + color = WikipediaTheme.colors.paperColor, + fontFamily = FontFamily.Serif + ) + HtmlText( + text = stringResource(R.string.activity_tab_game_entry_message), + style = MaterialTheme.typography.bodyLarge, + color = WikipediaTheme.colors.paperColor + ) + } + Icon( + modifier = Modifier.size(44.dp), + painter = painterResource(R.drawable.ic_today_24px), + tint = WikipediaTheme.colors.paperColor, + contentDescription = null + ) + } + } +} + +@Preview +@Composable +private fun WikiGamesEntryCardPreview() { + BaseTheme( + currentTheme = Theme.LIGHT + ) { + WikiGamesEntryCard( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + onClick = {} + ) + } +} + +@Preview +@Composable +private fun WikiGamesStatViewPreview() { + BaseTheme( + currentTheme = Theme.LIGHT + ) { + Column( + modifier = Modifier + .background(WikipediaTheme.colors.paperColor) + .padding(16.dp) + ) { + WikiGamesStatView( + modifier = Modifier, + iconResource = R.drawable.ic_today_24px, + statValue = "43", + statLabel = stringResource(R.string.activity_tab_game_stats_played) + ) + } + } +} + +@Preview +@Composable +private fun WikiGamesStatsCardPreview() { + BaseTheme( + currentTheme = Theme.LIGHT + ) { + Column( + modifier = Modifier + .background(WikipediaTheme.colors.paperColor) + .padding(16.dp) + ) { + WikiGamesStatsCard( + modifier = Modifier.fillMaxWidth(), + gameStatistics = OnThisDayGameViewModel.GameStatistics( + totalGamesPlayed = 43, + averageScore = 4.5, + currentStreak = 5, + bestStreak = 15 + ), + onClick = {} + ) + } + } +} diff --git a/app/src/main/java/org/wikipedia/games/db/DailyGameHistoryDao.kt b/app/src/main/java/org/wikipedia/games/db/DailyGameHistoryDao.kt index af0f0e41913..7f1447bba9e 100644 --- a/app/src/main/java/org/wikipedia/games/db/DailyGameHistoryDao.kt +++ b/app/src/main/java/org/wikipedia/games/db/DailyGameHistoryDao.kt @@ -62,4 +62,30 @@ interface DailyGameHistoryDao { return currentStreak } + + suspend fun getBestStreak(gameName: Int, language: String): Int { + val history = getGameHistory(gameName, language).filter { it.playType == PlayTypes.PLAYED_ON_SAME_DAY.ordinal } + if (history.isEmpty()) { + return 0 + } + + var bestStreak = 0 + var currentStreak = 0 + var expectedDate = LocalDate.now() // Start with today's date + + for (record in history) { + val recordDate = LocalDate.of(record.year, Month.of(record.month), record.day) + + if (recordDate == expectedDate) { + currentStreak++ + expectedDate = expectedDate.minusDays(1) // Move to the previous day + } else if (recordDate.isBefore(expectedDate)) { + bestStreak = maxOf(bestStreak, currentStreak) + currentStreak = 0 + expectedDate = recordDate.minusDays(1) // Reset to the day before the record date + } + } + + return maxOf(bestStreak, currentStreak) + } } diff --git a/app/src/main/java/org/wikipedia/games/onthisday/OnThisDayGameViewModel.kt b/app/src/main/java/org/wikipedia/games/onthisday/OnThisDayGameViewModel.kt index e4248330d61..fae912eda6c 100644 --- a/app/src/main/java/org/wikipedia/games/onthisday/OnThisDayGameViewModel.kt +++ b/app/src/main/java/org/wikipedia/games/onthisday/OnThisDayGameViewModel.kt @@ -411,7 +411,8 @@ class OnThisDayGameViewModel(savedStateHandle: SavedStateHandle) : ViewModel() { data class GameStatistics( val totalGamesPlayed: Int = 0, val averageScore: Double? = null, - val currentStreak: Int = 0 + val currentStreak: Int = 0, + val bestStreak: Int = 0 ) @Serializable @@ -467,10 +468,18 @@ class OnThisDayGameViewModel(savedStateHandle: SavedStateHandle) : ViewModel() { ) } + val bestStreak = async { + AppDatabase.instance.dailyGameHistoryDao().getBestStreak( + gameName = WikiGames.WHICH_CAME_FIRST.ordinal, + language = languageCode + ) + } + GameStatistics( - totalGamesPlayed.await(), - averageScore.await(), - currentStreak.await() + totalGamesPlayed = totalGamesPlayed.await(), + averageScore = averageScore.await(), + currentStreak = currentStreak.await(), + bestStreak = bestStreak.await() ) } } diff --git a/app/src/main/res/drawable/baseline_extension_24.xml b/app/src/main/res/drawable/baseline_extension_24.xml new file mode 100644 index 00000000000..8887b84ee34 --- /dev/null +++ b/app/src/main/res/drawable/baseline_extension_24.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/filled_family_star_24.xml b/app/src/main/res/drawable/filled_family_star_24.xml new file mode 100644 index 00000000000..3583cd7081c --- /dev/null +++ b/app/src/main/res/drawable/filled_family_star_24.xml @@ -0,0 +1,13 @@ + + + + + + diff --git a/app/src/main/res/drawable/outline_motion_blur_24.xml b/app/src/main/res/drawable/outline_motion_blur_24.xml new file mode 100644 index 00000000000..712476d2e04 --- /dev/null +++ b/app/src/main/res/drawable/outline_motion_blur_24.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/outline_sports_score_24.xml b/app/src/main/res/drawable/outline_sports_score_24.xml new file mode 100644 index 00000000000..631864b5370 --- /dev/null +++ b/app/src/main/res/drawable/outline_sports_score_24.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/values-qq/strings.xml b/app/src/main/res/values-qq/strings.xml index 27e2a1b9a0a..4ac0fc3d457 100644 --- a/app/src/main/res/values-qq/strings.xml +++ b/app/src/main/res/values-qq/strings.xml @@ -1183,6 +1183,13 @@ Button label to go to the Explore screen. Label encouraging the user to discover new articles. Label on card that shows the last donation time. + Title of the Wikipedia games entry card. + Message of the Wikipedia games entry card. + Title of the game stats card. + Label of the game stats that indicates how many games have been played. + Label of the game stats that indicates the current streak. + Label of the game stats that indicates the best streak. + Label of the game stats that indicates the average score of the games. Subtitle of the donation card that indicates last donation. Subject heading for sending a report about a problem with the Activity tab feature. Body of email for sending a report about a problem with the Activity tab feature. diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 7a780ba9bc9..d916c96d1bf 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1203,6 +1203,13 @@ Discover something new to read Unknown Last donation in app]]> + Which came first? + Guess which event came first on this day in history. + Game stats + Games played + Current streak + Best streak + Average score Issue Report - Activity Tab I have encountered a problem with Activity Tab Feature:\n- [Describe specific problem]\n\nThe behavior I would like to see is:\n- [Describe proposed solution] Problem with feature From 0ad216f24d553156c9a26ed559dbf2d9a9ee8837 Mon Sep 17 00:00:00 2001 From: Dmitry Brant Date: Tue, 19 Aug 2025 09:05:18 -0400 Subject: [PATCH 27/70] Tidy things up. --- .../activitytab/ActivityTabFragment.kt | 6 +++++- .../wikipedia/activitytab/WikiGamesModule.kt | 17 +++++++++-------- app/src/main/res/values-qq/strings.xml | 7 +------ app/src/main/res/values/strings.xml | 11 +++-------- 4 files changed, 18 insertions(+), 23 deletions(-) diff --git a/app/src/main/java/org/wikipedia/activitytab/ActivityTabFragment.kt b/app/src/main/java/org/wikipedia/activitytab/ActivityTabFragment.kt index 64580ef8f12..e3b7ee41ff4 100644 --- a/app/src/main/java/org/wikipedia/activitytab/ActivityTabFragment.kt +++ b/app/src/main/java/org/wikipedia/activitytab/ActivityTabFragment.kt @@ -117,6 +117,7 @@ class ActivityTabFragment : Fragment() { ActivityTabScreen( isLoggedIn = AccountUtil.isLoggedIn, userName = AccountUtil.userName, + modules = Prefs.activityTabModules, readingHistoryState = viewModel.readingHistoryState.collectAsState().value, donationUiState = viewModel.donationUiState.collectAsState().value, wikiGamesUiState = viewModel.wikiGamesUiState.collectAsState().value @@ -137,6 +138,7 @@ class ActivityTabFragment : Fragment() { fun ActivityTabScreen( isLoggedIn: Boolean, userName: String, + modules: ActivityTabModules, readingHistoryState: UiState, donationUiState: UiState, wikiGamesUiState: UiState @@ -149,7 +151,6 @@ class ActivityTabFragment : Fragment() { ) { paddingValues -> var isRefreshing by remember { mutableStateOf(false) } val state = rememberPullToRefreshState() - val modules = Prefs.activityTabModules if (readingHistoryState is UiState.Success) { isRefreshing = false } @@ -366,6 +367,7 @@ class ActivityTabFragment : Fragment() { ActivityTabScreen( isLoggedIn = true, userName = "User", + modules = ActivityTabModules(), readingHistoryState = UiState.Success(ActivityTabViewModel.ReadingHistory( timeSpentThisWeek = 12345, articlesReadThisMonth = 123, @@ -403,6 +405,7 @@ class ActivityTabFragment : Fragment() { ActivityTabScreen( isLoggedIn = true, userName = "User", + modules = ActivityTabModules(), readingHistoryState = UiState.Success(ActivityTabViewModel.ReadingHistory( timeSpentThisWeek = 0, articlesReadThisMonth = 0, @@ -426,6 +429,7 @@ class ActivityTabFragment : Fragment() { ActivityTabScreen( isLoggedIn = false, userName = "User", + modules = ActivityTabModules(), readingHistoryState = UiState.Success(ActivityTabViewModel.ReadingHistory( timeSpentThisWeek = 0, articlesReadThisMonth = 0, diff --git a/app/src/main/java/org/wikipedia/activitytab/WikiGamesModule.kt b/app/src/main/java/org/wikipedia/activitytab/WikiGamesModule.kt index b5826bf57cd..70db0403aec 100644 --- a/app/src/main/java/org/wikipedia/activitytab/WikiGamesModule.kt +++ b/app/src/main/java/org/wikipedia/activitytab/WikiGamesModule.kt @@ -21,6 +21,7 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.pluralStringResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontWeight @@ -130,13 +131,13 @@ fun WikiGamesStatsCard( modifier = Modifier.weight(1f), iconResource = R.drawable.baseline_extension_24, statValue = gameStatistics.totalGamesPlayed.toString(), - statLabel = stringResource(R.string.activity_tab_game_stats_played) + statLabel = pluralStringResource(R.plurals.on_this_day_game_stats_games_played, gameStatistics.totalGamesPlayed) ) WikiGamesStatView( modifier = Modifier.weight(1f), iconResource = R.drawable.outline_motion_blur_24, statValue = gameStatistics.currentStreak.toString(), - statLabel = stringResource(R.string.activity_tab_game_stats_current_streak) + statLabel = stringResource(R.string.on_this_day_game_stats_streak) ) } Row( @@ -155,7 +156,7 @@ fun WikiGamesStatsCard( modifier = Modifier.weight(1f), iconResource = R.drawable.outline_sports_score_24, statValue = gameStatistics.averageScore.toString(), - statLabel = stringResource(R.string.activity_tab_game_stats_average_score) + statLabel = stringResource(R.string.on_this_day_game_stats_average_score) ) } } @@ -205,7 +206,7 @@ fun WikiGamesEntryCard( WikiCard( modifier = modifier .clickable(onClick = { onClick?.invoke() }), - elevation = 0.dp, + elevation = 4.dp, colors = CardDefaults.cardColors( containerColor = WikipediaTheme.colors.progressiveColor, contentColor = WikipediaTheme.colors.progressiveColor @@ -224,13 +225,13 @@ fun WikiGamesEntryCard( verticalArrangement = Arrangement.spacedBy(8.dp) ) { Text( - text = stringResource(R.string.activity_tab_game_entry_title), + text = stringResource(R.string.on_this_day_game_title), style = MaterialTheme.typography.headlineSmall, color = WikipediaTheme.colors.paperColor, fontFamily = FontFamily.Serif ) HtmlText( - text = stringResource(R.string.activity_tab_game_entry_message), + text = stringResource(R.string.on_this_day_game_splash_message), style = MaterialTheme.typography.bodyLarge, color = WikipediaTheme.colors.paperColor ) @@ -274,8 +275,8 @@ private fun WikiGamesStatViewPreview() { WikiGamesStatView( modifier = Modifier, iconResource = R.drawable.ic_today_24px, - statValue = "43", - statLabel = stringResource(R.string.activity_tab_game_stats_played) + statValue = "42", + statLabel = pluralStringResource(R.plurals.on_this_day_game_stats_games_played, 42) ) } } diff --git a/app/src/main/res/values-qq/strings.xml b/app/src/main/res/values-qq/strings.xml index 4ac0fc3d457..4ef54041156 100644 --- a/app/src/main/res/values-qq/strings.xml +++ b/app/src/main/res/values-qq/strings.xml @@ -1183,20 +1183,15 @@ Button label to go to the Explore screen. Label encouraging the user to discover new articles. Label on card that shows the last donation time. - Title of the Wikipedia games entry card. - Message of the Wikipedia games entry card. Title of the game stats card. - Label of the game stats that indicates how many games have been played. - Label of the game stats that indicates the current streak. Label of the game stats that indicates the best streak. - Label of the game stats that indicates the average score of the games. Subtitle of the donation card that indicates last donation. Subject heading for sending a report about a problem with the Activity tab feature. Body of email for sending a report about a problem with the Activity tab feature. Menu label for providing feedback on the activity tab. Menu label for visiting the information page of the activity tab. Menu label for opening the activity tab customization screen. - Title of the switch for the Reading History module in the customize screen. + Title of the switch for the Reading history module in the customize screen. Title of the switch for the Impact module in the customize screen. Title of the switch for the Games module in the customize screen. Title of the switch for the Donations module in the customize screen. diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index d916c96d1bf..0ffa9ca1fe9 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1203,19 +1203,14 @@ Discover something new to read Unknown Last donation in app]]> - Which came first? - Guess which event came first on this day in history. Game stats - Games played - Current streak Best streak - Average score - Issue Report - Activity Tab - I have encountered a problem with Activity Tab Feature:\n- [Describe specific problem]\n\nThe behavior I would like to see is:\n- [Describe proposed solution] + Issue report - Activity Tab + I have encountered a problem with Activity Tab feature:\n- [Describe specific problem]\n\nThe behavior I would like to see is:\n- [Describe proposed solution] Problem with feature Learn more Customize - Reading History + Reading history Impact Games Donations From 5225da5155b789e38e2b545ed85867a7dbb0af28 Mon Sep 17 00:00:00 2001 From: Dmitry Brant Date: Tue, 19 Aug 2025 09:37:25 -0400 Subject: [PATCH 28/70] A bit more cleanup. --- .../activitytab/ActivityTabFragment.kt | 14 +++-- .../wikipedia/activitytab/WikiGamesModule.kt | 59 +++++++------------ 2 files changed, 31 insertions(+), 42 deletions(-) diff --git a/app/src/main/java/org/wikipedia/activitytab/ActivityTabFragment.kt b/app/src/main/java/org/wikipedia/activitytab/ActivityTabFragment.kt index e3b7ee41ff4..3d79b498316 100644 --- a/app/src/main/java/org/wikipedia/activitytab/ActivityTabFragment.kt +++ b/app/src/main/java/org/wikipedia/activitytab/ActivityTabFragment.kt @@ -12,6 +12,7 @@ import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding @@ -314,7 +315,7 @@ class ActivityTabFragment : Fragment() { WikiGamesModule( modifier = Modifier .fillMaxWidth() - .padding(vertical = 16.dp, horizontal = 16.dp), + .padding(start = 16.dp, end = 16.dp, top = 16.dp), uiState = wikiGamesUiState, onEntryCardClick = { requireActivity().startActivity(OnThisDayGameActivity.newIntent( @@ -339,7 +340,7 @@ class ActivityTabFragment : Fragment() { DonationModule( modifier = Modifier .fillMaxWidth() - .padding(vertical = 16.dp, horizontal = 16.dp), + .padding(start = 16.dp, end = 16.dp, top = 16.dp), uiState = donationUiState, onClick = { (requireActivity() as? BaseActivity)?.launchDonateDialog( @@ -348,6 +349,11 @@ class ActivityTabFragment : Fragment() { } ) } + + if (modules.isModuleEnabled(ModuleType.DONATIONS) || modules.isModuleEnabled(ModuleType.GAMES) || modules.isModuleEnabled(ModuleType.IMPACT)) { + // Add bottom padding only if at least one of the modules in this gradient box is enabled. + Spacer(modifier = Modifier.size(16.dp)) + } } } @@ -367,7 +373,7 @@ class ActivityTabFragment : Fragment() { ActivityTabScreen( isLoggedIn = true, userName = "User", - modules = ActivityTabModules(), + modules = ActivityTabModules(isDonationsEnabled = true), readingHistoryState = UiState.Success(ActivityTabViewModel.ReadingHistory( timeSpentThisWeek = 12345, articlesReadThisMonth = 123, @@ -405,7 +411,7 @@ class ActivityTabFragment : Fragment() { ActivityTabScreen( isLoggedIn = true, userName = "User", - modules = ActivityTabModules(), + modules = ActivityTabModules(isDonationsEnabled = true), readingHistoryState = UiState.Success(ActivityTabViewModel.ReadingHistory( timeSpentThisWeek = 0, articlesReadThisMonth = 0, diff --git a/app/src/main/java/org/wikipedia/activitytab/WikiGamesModule.kt b/app/src/main/java/org/wikipedia/activitytab/WikiGamesModule.kt index 70db0403aec..ea2c7544093 100644 --- a/app/src/main/java/org/wikipedia/activitytab/WikiGamesModule.kt +++ b/app/src/main/java/org/wikipedia/activitytab/WikiGamesModule.kt @@ -1,7 +1,6 @@ package org.wikipedia.activitytab import androidx.compose.foundation.BorderStroke -import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -63,16 +62,14 @@ fun WikiGamesModule( } else if (uiState is UiState.Success) { if (uiState.data == null) { WikiGamesEntryCard( - modifier = Modifier - .fillMaxWidth() - .padding(16.dp), + modifier = modifier + .fillMaxWidth(), onClick = onEntryCardClick ) } else { WikiGamesStatsCard( - modifier = Modifier - .fillMaxWidth() - .padding(16.dp), + modifier = modifier + .fillMaxWidth(), gameStatistics = uiState.data, onClick = onStatsCardClick ) @@ -253,9 +250,7 @@ private fun WikiGamesEntryCardPreview() { currentTheme = Theme.LIGHT ) { WikiGamesEntryCard( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp), + modifier = Modifier, onClick = {} ) } @@ -267,18 +262,12 @@ private fun WikiGamesStatViewPreview() { BaseTheme( currentTheme = Theme.LIGHT ) { - Column( - modifier = Modifier - .background(WikipediaTheme.colors.paperColor) - .padding(16.dp) - ) { - WikiGamesStatView( - modifier = Modifier, - iconResource = R.drawable.ic_today_24px, - statValue = "42", - statLabel = pluralStringResource(R.plurals.on_this_day_game_stats_games_played, 42) - ) - } + WikiGamesStatView( + modifier = Modifier, + iconResource = R.drawable.ic_today_24px, + statValue = "42", + statLabel = pluralStringResource(R.plurals.on_this_day_game_stats_games_played, 42) + ) } } @@ -288,21 +277,15 @@ private fun WikiGamesStatsCardPreview() { BaseTheme( currentTheme = Theme.LIGHT ) { - Column( - modifier = Modifier - .background(WikipediaTheme.colors.paperColor) - .padding(16.dp) - ) { - WikiGamesStatsCard( - modifier = Modifier.fillMaxWidth(), - gameStatistics = OnThisDayGameViewModel.GameStatistics( - totalGamesPlayed = 43, - averageScore = 4.5, - currentStreak = 5, - bestStreak = 15 - ), - onClick = {} - ) - } + WikiGamesStatsCard( + modifier = Modifier, + gameStatistics = OnThisDayGameViewModel.GameStatistics( + totalGamesPlayed = 43, + averageScore = 4.5, + currentStreak = 5, + bestStreak = 15 + ), + onClick = {} + ) } } From 8fceebd48086494af17b217a3c0f29632347732a Mon Sep 17 00:00:00 2001 From: Dmitry Brant Date: Mon, 18 Aug 2025 16:14:33 -0400 Subject: [PATCH 29/70] Beginnings of impact module. --- .../activitytab/ActivityTabFragment.kt | 18 ++++++--- .../activitytab/ActivityTabViewModel.kt | 37 ++++++++++++++++++- .../main/java/org/wikipedia/settings/Prefs.kt | 8 ++++ app/src/main/res/values/preference_keys.xml | 2 + 4 files changed, 58 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/org/wikipedia/activitytab/ActivityTabFragment.kt b/app/src/main/java/org/wikipedia/activitytab/ActivityTabFragment.kt index 3d79b498316..a1f705f7b10 100644 --- a/app/src/main/java/org/wikipedia/activitytab/ActivityTabFragment.kt +++ b/app/src/main/java/org/wikipedia/activitytab/ActivityTabFragment.kt @@ -67,6 +67,7 @@ import org.wikipedia.compose.theme.BaseTheme import org.wikipedia.compose.theme.WikipediaTheme import org.wikipedia.concurrency.FlowEventBus import org.wikipedia.dataclient.WikiSite +import org.wikipedia.dataclient.growthtasks.GrowthUserImpact import org.wikipedia.events.LoggedInEvent import org.wikipedia.events.LoggedOutEvent import org.wikipedia.events.LoggedOutInBackgroundEvent @@ -121,7 +122,8 @@ class ActivityTabFragment : Fragment() { modules = Prefs.activityTabModules, readingHistoryState = viewModel.readingHistoryState.collectAsState().value, donationUiState = viewModel.donationUiState.collectAsState().value, - wikiGamesUiState = viewModel.wikiGamesUiState.collectAsState().value + wikiGamesUiState = viewModel.wikiGamesUiState.collectAsState().value, + impactUiState = viewModel.impactUiState.collectAsState().value ) } } @@ -142,7 +144,8 @@ class ActivityTabFragment : Fragment() { modules: ActivityTabModules, readingHistoryState: UiState, donationUiState: UiState, - wikiGamesUiState: UiState + wikiGamesUiState: UiState, + impactUiState: UiState ) { Scaffold( modifier = Modifier @@ -308,7 +311,7 @@ class ActivityTabFragment : Fragment() { ) ) { if (modules.isModuleEnabled(ModuleType.IMPACT)) { - // @TODO: MARK_ACTIVITY_TAB + // TODO: zomg do something with this! } if (modules.isModuleEnabled(ModuleType.GAMES)) { @@ -399,7 +402,8 @@ class ActivityTabFragment : Fragment() { averageScore = 4.5, currentStreak = 15, bestStreak = 25 - )) + )), + impactUiState = UiState.Success(GrowthUserImpact(totalEditsCount = 12345)) ) } } @@ -423,7 +427,8 @@ class ActivityTabFragment : Fragment() { topCategories = emptyList() )), donationUiState = UiState.Success("Unknown"), - wikiGamesUiState = UiState.Success(null) + wikiGamesUiState = UiState.Success(null), + impactUiState = UiState.Success(GrowthUserImpact()) ) } } @@ -447,7 +452,8 @@ class ActivityTabFragment : Fragment() { topCategories = emptyList() )), donationUiState = UiState.Success("Unknown"), - wikiGamesUiState = UiState.Success(null) + wikiGamesUiState = UiState.Success(null), + impactUiState = UiState.Success(GrowthUserImpact()) ) } } diff --git a/app/src/main/java/org/wikipedia/activitytab/ActivityTabViewModel.kt b/app/src/main/java/org/wikipedia/activitytab/ActivityTabViewModel.kt index 5bc2e459575..47d09b6faeb 100644 --- a/app/src/main/java/org/wikipedia/activitytab/ActivityTabViewModel.kt +++ b/app/src/main/java/org/wikipedia/activitytab/ActivityTabViewModel.kt @@ -10,10 +10,14 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch import org.wikipedia.WikipediaApp +import org.wikipedia.auth.AccountUtil import org.wikipedia.categories.db.Category import org.wikipedia.database.AppDatabase +import org.wikipedia.dataclient.ServiceFactory import org.wikipedia.dataclient.WikiSite +import org.wikipedia.dataclient.growthtasks.GrowthUserImpact import org.wikipedia.games.onthisday.OnThisDayGameViewModel +import org.wikipedia.json.JsonUtil import org.wikipedia.page.PageTitle import org.wikipedia.readinglist.database.ReadingListPage import org.wikipedia.settings.Prefs @@ -24,6 +28,7 @@ import java.time.LocalDateTime import java.time.ZoneId import java.time.ZoneOffset import java.util.concurrent.TimeUnit +import kotlin.math.abs class ActivityTabViewModel(savedStateHandle: SavedStateHandle) : ViewModel() { private val _readingHistoryState = MutableStateFlow>(UiState.Loading) @@ -35,10 +40,14 @@ class ActivityTabViewModel(savedStateHandle: SavedStateHandle) : ViewModel() { private val _wikiGamesUiState = MutableStateFlow>(UiState.Loading) val wikiGamesUiState: StateFlow> = _wikiGamesUiState.asStateFlow() + private val _impactUiState = MutableStateFlow>(UiState.Loading) + val impactUiState: StateFlow> = _impactUiState.asStateFlow() + fun loadAll() { loadReadingHistory() loadDonationResults() loadWikiGamesStats() + loadImpact() } fun loadReadingHistory() { @@ -107,11 +116,37 @@ class ActivityTabViewModel(savedStateHandle: SavedStateHandle) : ViewModel() { return@launch } - val gamesStats = OnThisDayGameViewModel.getGameStatistics(WikipediaApp.instance.wikiSite.languageCode) + val gamesStats = + OnThisDayGameViewModel.getGameStatistics(WikipediaApp.instance.wikiSite.languageCode) _wikiGamesUiState.value = UiState.Success(gamesStats) } } + fun loadImpact() { + if (!AccountUtil.isLoggedIn) { + return + } + viewModelScope.launch(CoroutineExceptionHandler { _, throwable -> + _impactUiState.value = UiState.Error(throwable) + }) { + _impactUiState.value = UiState.Loading + + // The impact API is rate limited, so we cache it manually. + val now = Instant.now().epochSecond + val impact: GrowthUserImpact + if (Prefs.impactLastResponseBody.isEmpty() || abs(now - Prefs.impactLastQueryTime) > TimeUnit.DAYS.toSeconds(1)) { + Prefs.impactLastResponseBody = "" + val userId = ServiceFactory.get(WikipediaApp.instance.wikiSite).getUserInfo().query?.userInfo?.id!! + impact = ServiceFactory.getCoreRest(WikipediaApp.instance.wikiSite).getUserImpact(userId) + Prefs.impactLastResponseBody = JsonUtil.encodeToString(impact).orEmpty() + Prefs.impactLastQueryTime = now + } else { + impact = JsonUtil.decodeFromString(Prefs.impactLastResponseBody)!! + } + _impactUiState.value = UiState.Success(impact) + } + } + fun createPageTitleForCategory(category: Category): PageTitle { return PageTitle(title = category.title, wiki = WikiSite.forLanguageCode(category.lang)) } diff --git a/app/src/main/java/org/wikipedia/settings/Prefs.kt b/app/src/main/java/org/wikipedia/settings/Prefs.kt index 7b86f16c4ca..35d9c2f6d4d 100644 --- a/app/src/main/java/org/wikipedia/settings/Prefs.kt +++ b/app/src/main/java/org/wikipedia/settings/Prefs.kt @@ -835,6 +835,14 @@ object Prefs { get() = PrefsIoUtil.getBoolean(R.string.preference_key_activity_tab_red_dot_shown, false) set(value) = PrefsIoUtil.setBoolean(R.string.preference_key_activity_tab_red_dot_shown, value) + var impactLastQueryTime + get() = PrefsIoUtil.getLong(R.string.preference_key_impact_last_query_time, 0) + set(value) = PrefsIoUtil.setLong(R.string.preference_key_impact_last_query_time, value) + + var impactLastResponseBody + get() = PrefsIoUtil.getString(R.string.preference_key_impact_last_response_body, null).orEmpty() + set(value) = PrefsIoUtil.setString(R.string.preference_key_impact_last_response_body, value) + var donationReminderConfig get() = JsonUtil.decodeFromString( PrefsIoUtil.getString(R.string.preference_key_donation_reminder_config, null) diff --git a/app/src/main/res/values/preference_keys.xml b/app/src/main/res/values/preference_keys.xml index d21d82dfb90..76a3b8c391f 100644 --- a/app/src/main/res/values/preference_keys.xml +++ b/app/src/main/res/values/preference_keys.xml @@ -176,6 +176,8 @@ otdNotificationState otdGameFirstPlayedShown activityTabRedDotShown + impactLastQueryTime + impactLastResponseBody placesDefaultLocationLatLng deleteLocalDonationHistory categoryPlayground From 31c9681e332a0d3e9a1eb46120f04ad11a40e942 Mon Sep 17 00:00:00 2001 From: Dmitry Brant Date: Tue, 19 Aug 2025 14:02:34 -0400 Subject: [PATCH 30/70] Add Highlights title. --- .../activitytab/ActivityTabCustomizationActivity.kt | 2 +- .../org/wikipedia/activitytab/ActivityTabFragment.kt | 10 ++++++++++ app/src/main/res/values-qq/strings.xml | 2 +- app/src/main/res/values/strings.xml | 2 +- 4 files changed, 13 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/org/wikipedia/activitytab/ActivityTabCustomizationActivity.kt b/app/src/main/java/org/wikipedia/activitytab/ActivityTabCustomizationActivity.kt index 9add886c63d..5792397cf2d 100644 --- a/app/src/main/java/org/wikipedia/activitytab/ActivityTabCustomizationActivity.kt +++ b/app/src/main/java/org/wikipedia/activitytab/ActivityTabCustomizationActivity.kt @@ -70,7 +70,7 @@ fun CustomizationScreen( .safeDrawingPadding(), topBar = { WikiTopAppBar( - title = stringResource(R.string.activity_tab_customize_screen_title), + title = stringResource(R.string.activity_tab_menu_customize), onNavigationClick = onBackButtonClick ) }, diff --git a/app/src/main/java/org/wikipedia/activitytab/ActivityTabFragment.kt b/app/src/main/java/org/wikipedia/activitytab/ActivityTabFragment.kt index a1f705f7b10..bf25852a720 100644 --- a/app/src/main/java/org/wikipedia/activitytab/ActivityTabFragment.kt +++ b/app/src/main/java/org/wikipedia/activitytab/ActivityTabFragment.kt @@ -42,6 +42,7 @@ import androidx.compose.ui.graphics.Brush import androidx.compose.ui.platform.ComposeView import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @@ -314,6 +315,15 @@ class ActivityTabFragment : Fragment() { // TODO: zomg do something with this! } + if (modules.isModuleEnabled(ModuleType.GAMES) || modules.isModuleEnabled(ModuleType.DONATIONS)) { + Text( + modifier = Modifier.padding(start = 16.dp, end = 16.dp, top = 24.dp), + text = stringResource(R.string.activity_tab_highlights), + style = MaterialTheme.typography.titleLarge, + fontWeight = FontWeight.Medium + ) + } + if (modules.isModuleEnabled(ModuleType.GAMES)) { WikiGamesModule( modifier = Modifier diff --git a/app/src/main/res/values-qq/strings.xml b/app/src/main/res/values-qq/strings.xml index 4ef54041156..aeec1db5fb1 100644 --- a/app/src/main/res/values-qq/strings.xml +++ b/app/src/main/res/values-qq/strings.xml @@ -1191,12 +1191,12 @@ Menu label for providing feedback on the activity tab. Menu label for visiting the information page of the activity tab. Menu label for opening the activity tab customization screen. + Label for the Highlights section of the Activity tab. Title of the switch for the Reading history module in the customize screen. Title of the switch for the Impact module in the customize screen. Title of the switch for the Games module in the customize screen. Title of the switch for the Donations module in the customize screen. Title of the switch for the Timeline module in the customize screen. - Title shown at the top of the activity for the customize screen. Title shown at the top of the activity for the file page. Button label to add image caption for the file. Button label to add image tags for the file. diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 0ffa9ca1fe9..27750040c8a 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1210,12 +1210,12 @@ Problem with feature Learn more Customize + Highlights Reading history Impact Games Donations Timeline - Customize From b883e209839faa7bf522e69b66113567cb174631 Mon Sep 17 00:00:00 2001 From: Cooltey Feng Date: Wed, 20 Aug 2025 04:48:36 -0700 Subject: [PATCH 31/70] Add WikiErrorView for wikiErrorClickEvents in Activity Tab modules (#5855) * Add WikiErrorView for wikiErrorClickEvents in Activity Tab modules * Update paddings --- .../activitytab/ActivityTabFragment.kt | 9 +++-- .../activitytab/ReadingHistoryModule.kt | 16 +++++++++ .../wikipedia/activitytab/WikiGamesModule.kt | 33 +++++++++++++++++++ 3 files changed, 56 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/org/wikipedia/activitytab/ActivityTabFragment.kt b/app/src/main/java/org/wikipedia/activitytab/ActivityTabFragment.kt index bf25852a720..ba8d65c85f7 100644 --- a/app/src/main/java/org/wikipedia/activitytab/ActivityTabFragment.kt +++ b/app/src/main/java/org/wikipedia/activitytab/ActivityTabFragment.kt @@ -338,14 +338,19 @@ class ActivityTabFragment : Fragment() { )) }, onStatsCardClick = { - // TODO: ask PM if these two actions should be the same. + // TODO: link to the stats page when we have the WikiGames home page. requireActivity().startActivity(OnThisDayGameActivity.newIntent( context = requireContext(), invokeSource = Constants.InvokeSource.ACTIVITY_TAB, wikiSite = WikipediaApp.instance.wikiSite )) - } + }, + wikiErrorClickEvents = WikiErrorClickEvents( + retryClickListener = { + viewModel.loadWikiGamesStats() + } + ) ) } diff --git a/app/src/main/java/org/wikipedia/activitytab/ReadingHistoryModule.kt b/app/src/main/java/org/wikipedia/activitytab/ReadingHistoryModule.kt index e0fc6c0ee3f..f7e5ce91b9c 100644 --- a/app/src/main/java/org/wikipedia/activitytab/ReadingHistoryModule.kt +++ b/app/src/main/java/org/wikipedia/activitytab/ReadingHistoryModule.kt @@ -11,6 +11,7 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size @@ -48,6 +49,7 @@ import org.wikipedia.categories.db.Category import org.wikipedia.compose.ComposeColors import org.wikipedia.compose.components.TinyBarChart import org.wikipedia.compose.components.error.WikiErrorClickEvents +import org.wikipedia.compose.components.error.WikiErrorView import org.wikipedia.compose.theme.WikipediaTheme import org.wikipedia.util.UiState import org.wikipedia.views.imageservice.ImageService @@ -425,5 +427,19 @@ fun ReadingHistoryModule( ) } } + } else if (readingHistoryState is UiState.Error) { + Box( + modifier = modifier + .fillMaxSize() + .padding(horizontal = 16.dp, vertical = 16.dp), + contentAlignment = Alignment.Center + ) { + WikiErrorView( + modifier = Modifier + .fillMaxWidth(), + caught = readingHistoryState.error, + errorClickEvents = wikiErrorClickEvents + ) + } } } diff --git a/app/src/main/java/org/wikipedia/activitytab/WikiGamesModule.kt b/app/src/main/java/org/wikipedia/activitytab/WikiGamesModule.kt index ea2c7544093..23e52248269 100644 --- a/app/src/main/java/org/wikipedia/activitytab/WikiGamesModule.kt +++ b/app/src/main/java/org/wikipedia/activitytab/WikiGamesModule.kt @@ -6,6 +6,7 @@ import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.heightIn @@ -32,6 +33,7 @@ import org.wikipedia.R import org.wikipedia.compose.components.HtmlText import org.wikipedia.compose.components.WikiCard import org.wikipedia.compose.components.error.WikiErrorClickEvents +import org.wikipedia.compose.components.error.WikiErrorView import org.wikipedia.compose.theme.BaseTheme import org.wikipedia.compose.theme.WikipediaTheme import org.wikipedia.games.onthisday.OnThisDayGameViewModel @@ -74,6 +76,19 @@ fun WikiGamesModule( onClick = onStatsCardClick ) } + } else if (uiState is UiState.Error) { + Box( + modifier = modifier + .fillMaxSize(), + contentAlignment = Alignment.Center + ) { + WikiErrorView( + modifier = Modifier + .fillMaxWidth(), + caught = uiState.error, + errorClickEvents = wikiErrorClickEvents + ) + } } } @@ -243,6 +258,24 @@ fun WikiGamesEntryCard( } } +@Preview +@Composable +private fun WikiGamesModulePreview() { + BaseTheme( + currentTheme = Theme.LIGHT + ) { + WikiGamesModule( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + uiState = UiState.Error(Throwable("Error")), + onEntryCardClick = {}, + onStatsCardClick = {}, + wikiErrorClickEvents = null + ) + } +} + @Preview @Composable private fun WikiGamesEntryCardPreview() { From 02425512bfe26c2bd19de0796315372ac58d6fba Mon Sep 17 00:00:00 2001 From: William Rai <48931640+Williamrai@users.noreply.github.com> Date: Wed, 20 Aug 2025 10:54:50 -0400 Subject: [PATCH 32/70] - adds description view to customize screen (#5859) --- .../activitytab/ActivityTabCustomizationActivity.kt | 12 +++++++++++- app/src/main/res/values-qq/strings.xml | 1 + app/src/main/res/values/strings.xml | 1 + 3 files changed, 13 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/org/wikipedia/activitytab/ActivityTabCustomizationActivity.kt b/app/src/main/java/org/wikipedia/activitytab/ActivityTabCustomizationActivity.kt index 5792397cf2d..cbd45827255 100644 --- a/app/src/main/java/org/wikipedia/activitytab/ActivityTabCustomizationActivity.kt +++ b/app/src/main/java/org/wikipedia/activitytab/ActivityTabCustomizationActivity.kt @@ -79,8 +79,18 @@ fun CustomizationScreen( LazyColumn( modifier = Modifier .padding(paddingValues) - .padding(vertical = 24.dp) + .padding(vertical = 16.dp), ) { + item { + Text( + modifier = Modifier + .padding(horizontal = 16.dp) + .padding(bottom = 16.dp), + text = stringResource(R.string.activity_tab_menu_customize_description), + style = MaterialTheme.typography.bodySmall, + color = WikipediaTheme.colors.secondaryColor + ) + } itemsIndexed(ModuleType.entries) { index, moduleType -> CustomizationScreenSwitch( isChecked = currentModules.isModuleEnabled(moduleType), diff --git a/app/src/main/res/values-qq/strings.xml b/app/src/main/res/values-qq/strings.xml index aeec1db5fb1..051cde3c040 100644 --- a/app/src/main/res/values-qq/strings.xml +++ b/app/src/main/res/values-qq/strings.xml @@ -1191,6 +1191,7 @@ Menu label for providing feedback on the activity tab. Menu label for visiting the information page of the activity tab. Menu label for opening the activity tab customization screen. + Text shown in the Customize screen explaining how Activity tab work. Label for the Highlights section of the Activity tab. Title of the switch for the Reading history module in the customize screen. Title of the switch for the Impact module in the customize screen. diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 27750040c8a..4176a3ae8b7 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1210,6 +1210,7 @@ Problem with feature Learn more Customize + Activity tab insights are based on the primary language set in settings and is leveraging local data with the exception of edits which are public. Highlights Reading history Impact From 2a7778a54c2fbb96bf22dd4c718ec2486ef2a0be Mon Sep 17 00:00:00 2001 From: Dmitry Brant Date: Wed, 20 Aug 2025 11:05:32 -0400 Subject: [PATCH 33/70] Update copy and style of Explore Wikipedia button. --- .../org/wikipedia/activitytab/ReadingHistoryModule.kt | 11 +---------- app/src/main/res/values-qq/strings.xml | 2 +- app/src/main/res/values/strings.xml | 4 ++-- 3 files changed, 4 insertions(+), 13 deletions(-) diff --git a/app/src/main/java/org/wikipedia/activitytab/ReadingHistoryModule.kt b/app/src/main/java/org/wikipedia/activitytab/ReadingHistoryModule.kt index f7e5ce91b9c..18d94c12c0b 100644 --- a/app/src/main/java/org/wikipedia/activitytab/ReadingHistoryModule.kt +++ b/app/src/main/java/org/wikipedia/activitytab/ReadingHistoryModule.kt @@ -415,16 +415,7 @@ fun ReadingHistoryModule( onExploreClick() }, ) { - Icon( - modifier = Modifier.size(20.dp), - painter = painterResource(R.drawable.ic_globe), - tint = WikipediaTheme.colors.paperColor, - contentDescription = null - ) - Text( - modifier = Modifier.padding(start = 6.dp), - text = stringResource(R.string.activity_tab_explore_wikipedia) - ) + Text(text = stringResource(R.string.activity_tab_explore_wikipedia)) } } } else if (readingHistoryState is UiState.Error) { diff --git a/app/src/main/res/values-qq/strings.xml b/app/src/main/res/values-qq/strings.xml index 051cde3c040..3cb7df6c451 100644 --- a/app/src/main/res/values-qq/strings.xml +++ b/app/src/main/res/values-qq/strings.xml @@ -1180,7 +1180,7 @@ Label on a card that lists the number of articles read this month. Label on a card that lists the number of articles saved this month to a reading list. Label on a card that lists the top categories of articles that were read this month. - Button label to go to the Explore screen. + Button label to go to the Reading Lists screen, which will give the user further guidance for exploring Wikipedia. Label encouraging the user to discover new articles. Label on card that shows the last donation time. Title of the game stats card. diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 4176a3ae8b7..b1337425d60 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1199,8 +1199,8 @@ Articles read this month Articles saved this month Top categories read this month - Explore Wikipedia - Discover something new to read + Discover through Wikipedia + Looking for something new to read? Unknown Last donation in app]]> Game stats From d8d1f847cd64034726861377975b73b51ed1a681 Mon Sep 17 00:00:00 2001 From: Dmitry Brant Date: Wed, 20 Aug 2025 11:08:13 -0400 Subject: [PATCH 34/70] Go to Reading Lists, not Explore feed. --- .../main/java/org/wikipedia/activitytab/ActivityTabFragment.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/org/wikipedia/activitytab/ActivityTabFragment.kt b/app/src/main/java/org/wikipedia/activitytab/ActivityTabFragment.kt index ba8d65c85f7..c4f664ece22 100644 --- a/app/src/main/java/org/wikipedia/activitytab/ActivityTabFragment.kt +++ b/app/src/main/java/org/wikipedia/activitytab/ActivityTabFragment.kt @@ -277,7 +277,7 @@ class ActivityTabFragment : Fragment() { readingHistoryState = readingHistoryState, onArticlesReadClick = { callback()?.onNavigateTo(NavTab.SEARCH) }, onArticlesSavedClick = { callback()?.onNavigateTo(NavTab.READING_LISTS) }, - onExploreClick = { callback()?.onNavigateTo(NavTab.EXPLORE) }, + onExploreClick = { callback()?.onNavigateTo(NavTab.READING_LISTS) }, onCategoryItemClick = { category -> val pageTitle = viewModel.createPageTitleForCategory(category) From 62f81733301c3755120824c4d489e4c52bb0d4af Mon Sep 17 00:00:00 2001 From: Dmitry Brant Date: Wed, 20 Aug 2025 11:28:44 -0400 Subject: [PATCH 35/70] Add explicit button to unknown state of donation card. --- .../activitytab/ActivityTabFragment.kt | 6 ++- .../wikipedia/activitytab/DonationModule.kt | 49 +++++++++++++++---- .../activitytab/ReadingHistoryModule.kt | 5 +- app/src/main/res/values-qq/strings.xml | 1 + app/src/main/res/values/strings.xml | 1 + 5 files changed, 49 insertions(+), 13 deletions(-) diff --git a/app/src/main/java/org/wikipedia/activitytab/ActivityTabFragment.kt b/app/src/main/java/org/wikipedia/activitytab/ActivityTabFragment.kt index c4f664ece22..44e4b3532cf 100644 --- a/app/src/main/java/org/wikipedia/activitytab/ActivityTabFragment.kt +++ b/app/src/main/java/org/wikipedia/activitytab/ActivityTabFragment.kt @@ -209,7 +209,8 @@ class ActivityTabFragment : Fragment() { ) Text( modifier = Modifier.padding(start = 6.dp), - text = stringResource(R.string.create_account_button) + text = stringResource(R.string.create_account_button), + style = MaterialTheme.typography.labelLarge ) } Button( @@ -230,7 +231,8 @@ class ActivityTabFragment : Fragment() { ) { Text( modifier = Modifier.padding(start = 6.dp), - text = stringResource(R.string.menu_login) + text = stringResource(R.string.menu_login), + style = MaterialTheme.typography.labelLarge ) } } diff --git a/app/src/main/java/org/wikipedia/activitytab/DonationModule.kt b/app/src/main/java/org/wikipedia/activitytab/DonationModule.kt index 4e6dad151a9..15f122056bf 100644 --- a/app/src/main/java/org/wikipedia/activitytab/DonationModule.kt +++ b/app/src/main/java/org/wikipedia/activitytab/DonationModule.kt @@ -5,11 +5,14 @@ import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues 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.foundation.layout.size +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme @@ -59,8 +62,7 @@ fun DonationModule( ) } } else if (uiState is UiState.Success) { - val lastDonationTime = - uiState.data ?: stringResource(R.string.activity_tab_donation_unknown) + val lastDonationTime = uiState.data ?: stringResource(R.string.activity_tab_donation_unknown) Column( modifier = Modifier.padding(16.dp) ) { @@ -98,6 +100,29 @@ fun DonationModule( color = WikipediaTheme.colors.progressiveColor, fontWeight = FontWeight.Medium ) + if (uiState.data.isNullOrEmpty()) { + Button( + modifier = Modifier.padding(top = 16.dp).align(Alignment.CenterHorizontally), + contentPadding = PaddingValues(horizontal = 18.dp), + colors = ButtonDefaults.buttonColors( + containerColor = WikipediaTheme.colors.progressiveColor, + contentColor = WikipediaTheme.colors.paperColor, + ), + onClick = { onClick?.invoke() }, + ) { + Icon( + modifier = Modifier.size(20.dp), + painter = painterResource(R.drawable.ic_heart_24), + tint = WikipediaTheme.colors.paperColor, + contentDescription = null + ) + Text( + modifier = Modifier.padding(start = 6.dp, top = 4.dp, bottom = 4.dp), + text = stringResource(R.string.activity_tab_donation_button), + style = MaterialTheme.typography.labelLarge + ) + } + } } } } @@ -106,15 +131,19 @@ fun DonationModule( @Preview @Composable private fun DonationModulePreview() { - BaseTheme( - currentTheme = Theme.LIGHT - ) { + BaseTheme(currentTheme = Theme.LIGHT) { + DonationModule( + uiState = UiState.Success("5 days ago") + ) + } +} + +@Preview +@Composable +private fun DonationModuleEmptyPreview() { + BaseTheme(currentTheme = Theme.LIGHT) { DonationModule( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp), - uiState = UiState.Success("5 days ago"), - onClick = {} + uiState = UiState.Success(null) ) } } diff --git a/app/src/main/java/org/wikipedia/activitytab/ReadingHistoryModule.kt b/app/src/main/java/org/wikipedia/activitytab/ReadingHistoryModule.kt index 18d94c12c0b..06c216f755a 100644 --- a/app/src/main/java/org/wikipedia/activitytab/ReadingHistoryModule.kt +++ b/app/src/main/java/org/wikipedia/activitytab/ReadingHistoryModule.kt @@ -415,7 +415,10 @@ fun ReadingHistoryModule( onExploreClick() }, ) { - Text(text = stringResource(R.string.activity_tab_explore_wikipedia)) + Text( + text = stringResource(R.string.activity_tab_explore_wikipedia), + style = MaterialTheme.typography.labelLarge + ) } } } else if (readingHistoryState is UiState.Error) { diff --git a/app/src/main/res/values-qq/strings.xml b/app/src/main/res/values-qq/strings.xml index 3cb7df6c451..a9215a63d97 100644 --- a/app/src/main/res/values-qq/strings.xml +++ b/app/src/main/res/values-qq/strings.xml @@ -1182,6 +1182,7 @@ Label on a card that lists the top categories of articles that were read this month. Button label to go to the Reading Lists screen, which will give the user further guidance for exploring Wikipedia. Label encouraging the user to discover new articles. + Button label to go to the donation workflow. Label on card that shows the last donation time. Title of the game stats card. Label of the game stats that indicates the best streak. diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index b1337425d60..972bd5442a8 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1201,6 +1201,7 @@ Top categories read this month Discover through Wikipedia Looking for something new to read? + Donate to the Wikimedia Foundation Unknown Last donation in app]]> Game stats From 543dc7ae5a56c0688a63715f5fd43e0e0841d722 Mon Sep 17 00:00:00 2001 From: Dmitry Brant Date: Wed, 20 Aug 2025 11:48:34 -0400 Subject: [PATCH 36/70] Update granularity of customization based on updated design. --- .../ActivityTabCustomizationActivity.kt | 16 +++-- .../activitytab/ActivityTabFragment.kt | 4 +- .../activitytab/ReadingHistoryModule.kt | 68 +++++++++++-------- app/src/main/res/values-qq/strings.xml | 14 ++-- app/src/main/res/values/strings.xml | 12 ++-- 5 files changed, 69 insertions(+), 45 deletions(-) diff --git a/app/src/main/java/org/wikipedia/activitytab/ActivityTabCustomizationActivity.kt b/app/src/main/java/org/wikipedia/activitytab/ActivityTabCustomizationActivity.kt index cbd45827255..d8e4993eb09 100644 --- a/app/src/main/java/org/wikipedia/activitytab/ActivityTabCustomizationActivity.kt +++ b/app/src/main/java/org/wikipedia/activitytab/ActivityTabCustomizationActivity.kt @@ -149,7 +149,9 @@ private fun CustomizationScreenSwitch( } fun ActivityTabModules.isModuleEnabled(moduleType: ModuleType): Boolean = when (moduleType) { - ModuleType.READING_HISTORY -> isReadingHistoryEnabled + ModuleType.TIME_SPENT -> isTimeSpentEnabled + ModuleType.READING_INSIGHTS -> isReadingInsightsEnabled + ModuleType.EDITING_INSIGHTS -> isEditingInsightsEnabled ModuleType.IMPACT -> isImpactEnabled ModuleType.GAMES -> isGamesEnabled ModuleType.DONATIONS -> isDonationsEnabled @@ -157,7 +159,9 @@ fun ActivityTabModules.isModuleEnabled(moduleType: ModuleType): Boolean = when ( } fun ActivityTabModules.setModuleEnabled(moduleType: ModuleType, enabled: Boolean) = when (moduleType) { - ModuleType.READING_HISTORY -> copy(isReadingHistoryEnabled = enabled) + ModuleType.TIME_SPENT -> copy(isTimeSpentEnabled = enabled) + ModuleType.READING_INSIGHTS -> copy(isReadingInsightsEnabled = enabled) + ModuleType.EDITING_INSIGHTS -> copy(isEditingInsightsEnabled = enabled) ModuleType.IMPACT -> copy(isImpactEnabled = enabled) ModuleType.GAMES -> copy(isGamesEnabled = enabled) ModuleType.DONATIONS -> copy(isDonationsEnabled = enabled) @@ -166,7 +170,9 @@ fun ActivityTabModules.setModuleEnabled(moduleType: ModuleType, enabled: Boolean @Serializable data class ActivityTabModules( - val isReadingHistoryEnabled: Boolean = true, + val isTimeSpentEnabled: Boolean = true, + val isReadingInsightsEnabled: Boolean = true, + val isEditingInsightsEnabled: Boolean = true, val isImpactEnabled: Boolean = true, val isGamesEnabled: Boolean = true, val isDonationsEnabled: Boolean = false, @@ -174,7 +180,9 @@ data class ActivityTabModules( ) enum class ModuleType(val displayName: Int) { - READING_HISTORY(R.string.activity_tab_customize_screen_reading_history_switch_title), + TIME_SPENT(R.string.activity_tab_customize_screen_time_spent_switch_title), + READING_INSIGHTS(R.string.activity_tab_customize_screen_reading_insights_switch_title), + EDITING_INSIGHTS(R.string.activity_tab_customize_screen_editing_insights_switch_title), IMPACT(R.string.activity_tab_customize_screen_impact_switch_title), GAMES(R.string.activity_tab_customize_screen_games_switch_title), DONATIONS(R.string.activity_tab_customize_screen_donations_switch_title), diff --git a/app/src/main/java/org/wikipedia/activitytab/ActivityTabFragment.kt b/app/src/main/java/org/wikipedia/activitytab/ActivityTabFragment.kt index 44e4b3532cf..5e984fdeee3 100644 --- a/app/src/main/java/org/wikipedia/activitytab/ActivityTabFragment.kt +++ b/app/src/main/java/org/wikipedia/activitytab/ActivityTabFragment.kt @@ -258,7 +258,7 @@ class ActivityTabFragment : Fragment() { } ) { LazyColumn { - if (modules.isModuleEnabled(ModuleType.READING_HISTORY)) { + if (modules.isModuleEnabled(ModuleType.TIME_SPENT) || modules.isModuleEnabled(ModuleType.READING_INSIGHTS)) { item { Column( modifier = Modifier @@ -276,6 +276,8 @@ class ActivityTabFragment : Fragment() { ReadingHistoryModule( modifier = Modifier.align(Alignment.CenterHorizontally), userName = userName, + showTimeSpent = modules.isModuleEnabled(ModuleType.TIME_SPENT), + showInsights = modules.isModuleEnabled(ModuleType.READING_INSIGHTS), readingHistoryState = readingHistoryState, onArticlesReadClick = { callback()?.onNavigateTo(NavTab.SEARCH) }, onArticlesSavedClick = { callback()?.onNavigateTo(NavTab.READING_LISTS) }, diff --git a/app/src/main/java/org/wikipedia/activitytab/ReadingHistoryModule.kt b/app/src/main/java/org/wikipedia/activitytab/ReadingHistoryModule.kt index 06c216f755a..e61faedf542 100644 --- a/app/src/main/java/org/wikipedia/activitytab/ReadingHistoryModule.kt +++ b/app/src/main/java/org/wikipedia/activitytab/ReadingHistoryModule.kt @@ -13,6 +13,7 @@ import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.RoundedCornerShape @@ -61,6 +62,8 @@ import java.util.Locale fun ReadingHistoryModule( modifier: Modifier, userName: String, + showTimeSpent: Boolean, + showInsights: Boolean, readingHistoryState: UiState, onArticlesReadClick: () -> Unit = {}, onArticlesSavedClick: () -> Unit = {}, @@ -104,38 +107,45 @@ fun ReadingHistoryModule( val readingHistory = readingHistoryState.data val todayDate = LocalDate.now() - Text( - text = stringResource( - R.string.activity_tab_weekly_time_spent_hm, - (readingHistory.timeSpentThisWeek / 3600), - (readingHistory.timeSpentThisWeek % 60) - ), - modifier = modifier.padding(top = 12.dp), - fontWeight = FontWeight.Medium, - textAlign = TextAlign.Center, - style = MaterialTheme.typography.headlineLarge.copy( - brush = Brush.linearGradient( - colors = listOf( - ComposeColors.Red700, - ComposeColors.Orange500, - ComposeColors.Yellow500, - ComposeColors.Blue300 + if (showTimeSpent) { + Text( + text = stringResource( + R.string.activity_tab_weekly_time_spent_hm, + (readingHistory.timeSpentThisWeek / 3600), + (readingHistory.timeSpentThisWeek % 60) + ), + modifier = modifier.padding(top = 12.dp), + fontWeight = FontWeight.Medium, + textAlign = TextAlign.Center, + style = MaterialTheme.typography.headlineLarge.copy( + brush = Brush.linearGradient( + colors = listOf( + ComposeColors.Red700, + ComposeColors.Orange500, + ComposeColors.Yellow500, + ComposeColors.Blue300 + ) ) - ) - ), - color = WikipediaTheme.colors.primaryColor - ) - Text( - text = stringResource(R.string.activity_tab_weekly_time_spent), - modifier = modifier - .padding(top = 8.dp, bottom = 16.dp), - style = MaterialTheme.typography.labelLarge, - textAlign = TextAlign.Center, - color = WikipediaTheme.colors.primaryColor - ) + ), + color = WikipediaTheme.colors.primaryColor + ) + Text( + text = stringResource(R.string.activity_tab_weekly_time_spent), + modifier = modifier + .padding(top = 8.dp), + style = MaterialTheme.typography.labelLarge, + textAlign = TextAlign.Center, + color = WikipediaTheme.colors.primaryColor + ) + } + + if (!showInsights) { + Spacer(modifier = Modifier.height(16.dp)) + return + } Card( - modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp) + modifier = Modifier.fillMaxWidth().padding(start = 16.dp, end = 16.dp, top = 16.dp) .clickable { onArticlesReadClick() }, diff --git a/app/src/main/res/values-qq/strings.xml b/app/src/main/res/values-qq/strings.xml index a9215a63d97..76752bd6c12 100644 --- a/app/src/main/res/values-qq/strings.xml +++ b/app/src/main/res/values-qq/strings.xml @@ -1192,13 +1192,15 @@ Menu label for providing feedback on the activity tab. Menu label for visiting the information page of the activity tab. Menu label for opening the activity tab customization screen. - Text shown in the Customize screen explaining how Activity tab work. + Text shown in the Customize screen explaining how Activity tab works. Label for the Highlights section of the Activity tab. - Title of the switch for the Reading history module in the customize screen. - Title of the switch for the Impact module in the customize screen. - Title of the switch for the Games module in the customize screen. - Title of the switch for the Donations module in the customize screen. - Title of the switch for the Timeline module in the customize screen. + Title of the switch to enable total time spent insights in the Activity Tab customization screen. + Title of the switch to enable reading insights in the Activity Tab customization screen. + Title of the switch to enable editing insights in the Activity Tab customization screen. + Title of the switch for the Impact module in the Activity Tab customization screen. + Title of the switch for the Games module in the Activity Tab customization screen. + Title of the switch for the Donations module in the Activity Tab customization screen. + Title of the switch for the Timeline module in the Activity Tab customization screen. Title shown at the top of the activity for the file page. Button label to add image caption for the file. Button label to add image tags for the file. diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 972bd5442a8..245cd1d82ee 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1213,11 +1213,13 @@ Customize Activity tab insights are based on the primary language set in settings and is leveraging local data with the exception of edits which are public. Highlights - Reading history - Impact - Games - Donations - Timeline + Time spent reading + Reading insights + Editing insights + All time impact + Game stats + Last in app donation + Timeline of behavior From f6e3b2263e50f5ae4b78825bca16cb68372ed2c2 Mon Sep 17 00:00:00 2001 From: Dmitry Brant Date: Wed, 20 Aug 2025 13:14:21 -0400 Subject: [PATCH 37/70] Add/remove menu provider symmetrically. --- .../java/org/wikipedia/activitytab/ActivityTabFragment.kt | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/org/wikipedia/activitytab/ActivityTabFragment.kt b/app/src/main/java/org/wikipedia/activitytab/ActivityTabFragment.kt index 5e984fdeee3..e50a547f577 100644 --- a/app/src/main/java/org/wikipedia/activitytab/ActivityTabFragment.kt +++ b/app/src/main/java/org/wikipedia/activitytab/ActivityTabFragment.kt @@ -102,7 +102,6 @@ class ActivityTabFragment : Fragment() { override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { super.onCreateView(inflater, container, savedInstanceState) Prefs.activityTabRedDotShown = true - requireActivity().addMenuProvider(menuProvider, viewLifecycleOwner) viewLifecycleOwner.lifecycleScope.launch { repeatOnLifecycle(Lifecycle.State.RESUMED) { @@ -133,10 +132,16 @@ class ActivityTabFragment : Fragment() { override fun onResume() { super.onResume() + requireActivity().addMenuProvider(menuProvider, viewLifecycleOwner) viewModel.loadAll() requireActivity().invalidateOptionsMenu() } + override fun onPause() { + super.onPause() + requireActivity().removeMenuProvider(menuProvider) + } + @OptIn(ExperimentalMaterial3Api::class) @Composable fun ActivityTabScreen( From 5dc03be94403e297bb276ea66a642b1bbd9abd49 Mon Sep 17 00:00:00 2001 From: Cooltey Feng Date: Wed, 20 Aug 2025 22:52:05 -0700 Subject: [PATCH 38/70] use lowercase() instead of toLowercase() --- .../main/java/org/wikipedia/activitytab/WikiGamesModule.kt | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/app/src/main/java/org/wikipedia/activitytab/WikiGamesModule.kt b/app/src/main/java/org/wikipedia/activitytab/WikiGamesModule.kt index 23e52248269..d9d7c023a26 100644 --- a/app/src/main/java/org/wikipedia/activitytab/WikiGamesModule.kt +++ b/app/src/main/java/org/wikipedia/activitytab/WikiGamesModule.kt @@ -25,8 +25,6 @@ import androidx.compose.ui.res.pluralStringResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.intl.Locale -import androidx.compose.ui.text.toLowerCase import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import org.wikipedia.R @@ -202,7 +200,7 @@ fun WikiGamesStatView( color = WikipediaTheme.colors.primaryColor ) Text( - text = statLabel.toLowerCase(Locale.current), + text = statLabel.lowercase(), style = MaterialTheme.typography.bodySmall, color = WikipediaTheme.colors.primaryColor ) From ba17a0106b6890a4ab539e97dbc2c49247160e14 Mon Sep 17 00:00:00 2001 From: Cooltey Feng Date: Fri, 22 Aug 2025 22:34:59 -0700 Subject: [PATCH 39/70] Design review fixes --- .../java/org/wikipedia/activitytab/ActivityTabFragment.kt | 3 ++- .../main/java/org/wikipedia/activitytab/DonationModule.kt | 8 ++++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/org/wikipedia/activitytab/ActivityTabFragment.kt b/app/src/main/java/org/wikipedia/activitytab/ActivityTabFragment.kt index e50a547f577..b1a9f1076cd 100644 --- a/app/src/main/java/org/wikipedia/activitytab/ActivityTabFragment.kt +++ b/app/src/main/java/org/wikipedia/activitytab/ActivityTabFragment.kt @@ -329,7 +329,8 @@ class ActivityTabFragment : Fragment() { modifier = Modifier.padding(start = 16.dp, end = 16.dp, top = 24.dp), text = stringResource(R.string.activity_tab_highlights), style = MaterialTheme.typography.titleLarge, - fontWeight = FontWeight.Medium + fontWeight = FontWeight.Medium, + color = WikipediaTheme.colors.primaryColor ) } diff --git a/app/src/main/java/org/wikipedia/activitytab/DonationModule.kt b/app/src/main/java/org/wikipedia/activitytab/DonationModule.kt index 15f122056bf..4dcfb4767af 100644 --- a/app/src/main/java/org/wikipedia/activitytab/DonationModule.kt +++ b/app/src/main/java/org/wikipedia/activitytab/DonationModule.kt @@ -102,8 +102,12 @@ fun DonationModule( ) if (uiState.data.isNullOrEmpty()) { Button( - modifier = Modifier.padding(top = 16.dp).align(Alignment.CenterHorizontally), - contentPadding = PaddingValues(horizontal = 18.dp), + modifier = Modifier + .fillMaxWidth() + .padding(top = 16.dp) + .padding(horizontal = 8.dp) + .align(Alignment.CenterHorizontally), + contentPadding = PaddingValues(horizontal = 12.dp), colors = ButtonDefaults.buttonColors( containerColor = WikipediaTheme.colors.progressiveColor, contentColor = WikipediaTheme.colors.paperColor, From e5ecc9b68248830023704925a2294b683e5a0c2d Mon Sep 17 00:00:00 2001 From: Cooltey Feng Date: Fri, 22 Aug 2025 22:45:44 -0700 Subject: [PATCH 40/70] Fix padding for donation button --- .../main/java/org/wikipedia/activitytab/DonationModule.kt | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/org/wikipedia/activitytab/DonationModule.kt b/app/src/main/java/org/wikipedia/activitytab/DonationModule.kt index 4dcfb4767af..1e0003e098a 100644 --- a/app/src/main/java/org/wikipedia/activitytab/DonationModule.kt +++ b/app/src/main/java/org/wikipedia/activitytab/DonationModule.kt @@ -103,11 +103,10 @@ fun DonationModule( if (uiState.data.isNullOrEmpty()) { Button( modifier = Modifier - .fillMaxWidth() .padding(top = 16.dp) - .padding(horizontal = 8.dp) + .fillMaxWidth() .align(Alignment.CenterHorizontally), - contentPadding = PaddingValues(horizontal = 12.dp), + contentPadding = PaddingValues(horizontal = 18.dp), colors = ButtonDefaults.buttonColors( containerColor = WikipediaTheme.colors.progressiveColor, contentColor = WikipediaTheme.colors.paperColor, From c5f217e4b1681d983a239636fbf96c0c67d71c0a Mon Sep 17 00:00:00 2001 From: Dmitry Brant Date: Mon, 25 Aug 2025 16:55:36 -0400 Subject: [PATCH 41/70] A bit of design feedback. --- .../java/org/wikipedia/activitytab/ReadingHistoryModule.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/org/wikipedia/activitytab/ReadingHistoryModule.kt b/app/src/main/java/org/wikipedia/activitytab/ReadingHistoryModule.kt index e61faedf542..b65b15e295d 100644 --- a/app/src/main/java/org/wikipedia/activitytab/ReadingHistoryModule.kt +++ b/app/src/main/java/org/wikipedia/activitytab/ReadingHistoryModule.kt @@ -350,7 +350,7 @@ fun ReadingHistoryModule( Icon( modifier = Modifier.size(24.dp).align(Alignment.Center), painter = painterResource(R.drawable.ic_wikipedia_b), - tint = WikipediaTheme.colors.primaryColor, + tint = Color.Black, contentDescription = null ) } @@ -406,7 +406,7 @@ fun ReadingHistoryModule( ) } - if (readingHistory.articlesReadThisMonth == 0 && readingHistory.articlesSavedThisMonth == 0) { + if (readingHistory.articlesReadThisMonth == 0) { Text( text = stringResource(R.string.activity_tab_discover_encourage), modifier = modifier.padding(horizontal = 16.dp, vertical = 8.dp), From 1c40eeb8797c719204022580530bc91d0b826003 Mon Sep 17 00:00:00 2001 From: Cooltey Feng Date: Mon, 25 Aug 2025 16:04:07 -0700 Subject: [PATCH 42/70] Adjust the WikiGames entry card paddings --- .../org/wikipedia/activitytab/WikiGamesModule.kt | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/org/wikipedia/activitytab/WikiGamesModule.kt b/app/src/main/java/org/wikipedia/activitytab/WikiGamesModule.kt index d9d7c023a26..fbdc6eb9b06 100644 --- a/app/src/main/java/org/wikipedia/activitytab/WikiGamesModule.kt +++ b/app/src/main/java/org/wikipedia/activitytab/WikiGamesModule.kt @@ -27,6 +27,7 @@ import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp import org.wikipedia.R import org.wikipedia.compose.components.HtmlText import org.wikipedia.compose.components.WikiCard @@ -225,20 +226,21 @@ fun WikiGamesEntryCard( Row( modifier = Modifier .fillMaxWidth() - .padding(16.dp), + .padding(24.dp), horizontalArrangement = Arrangement.spacedBy(8.dp) ) { Column( modifier = Modifier.weight(1f) - .heightIn(min = 132.dp) - .padding(horizontal = 4.dp), + .heightIn(min = 116.dp), verticalArrangement = Arrangement.spacedBy(8.dp) ) { Text( text = stringResource(R.string.on_this_day_game_title), - style = MaterialTheme.typography.headlineSmall, + style = MaterialTheme.typography.headlineSmall.copy( + lineHeight = 24.sp + ), color = WikipediaTheme.colors.paperColor, - fontFamily = FontFamily.Serif + fontFamily = FontFamily.Serif, ) HtmlText( text = stringResource(R.string.on_this_day_game_splash_message), From 4ff0360be822de438595022ca66d1a84af992312 Mon Sep 17 00:00:00 2001 From: Cooltey Feng Date: Tue, 26 Aug 2025 07:01:14 -0700 Subject: [PATCH 43/70] Add proper paddings for the top categories card in Activity Tab (#5866) --- .../java/org/wikipedia/activitytab/TopCategoriesView.kt | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/org/wikipedia/activitytab/TopCategoriesView.kt b/app/src/main/java/org/wikipedia/activitytab/TopCategoriesView.kt index b80f92b89dd..830304894e4 100644 --- a/app/src/main/java/org/wikipedia/activitytab/TopCategoriesView.kt +++ b/app/src/main/java/org/wikipedia/activitytab/TopCategoriesView.kt @@ -41,11 +41,9 @@ fun TopCategoriesView( color = WikipediaTheme.colors.borderColor ) ) { - Column( - modifier = Modifier.padding(top = 16.dp) - ) { + Column { Row( - modifier = Modifier.padding(horizontal = 16.dp), + modifier = Modifier.padding(start = 16.dp, end = 16.dp, top = 16.dp, bottom = 8.dp), horizontalArrangement = Arrangement.spacedBy(8.dp) ) { Icon( From 1817733331b379d517a5c6cde71a0e85cc1a8f74 Mon Sep 17 00:00:00 2001 From: Dmitry Brant Date: Tue, 26 Aug 2025 15:08:24 -0400 Subject: [PATCH 44/70] Whoops, correctly calculate minutes. --- .../main/java/org/wikipedia/activitytab/ReadingHistoryModule.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/org/wikipedia/activitytab/ReadingHistoryModule.kt b/app/src/main/java/org/wikipedia/activitytab/ReadingHistoryModule.kt index b65b15e295d..476619cb5a5 100644 --- a/app/src/main/java/org/wikipedia/activitytab/ReadingHistoryModule.kt +++ b/app/src/main/java/org/wikipedia/activitytab/ReadingHistoryModule.kt @@ -112,7 +112,7 @@ fun ReadingHistoryModule( text = stringResource( R.string.activity_tab_weekly_time_spent_hm, (readingHistory.timeSpentThisWeek / 3600), - (readingHistory.timeSpentThisWeek % 60) + ((readingHistory.timeSpentThisWeek / 60) % 60) ), modifier = modifier.padding(top = 12.dp), fontWeight = FontWeight.Medium, From 8c4cf63628db7904adacedff7e3956e0c956d275 Mon Sep 17 00:00:00 2001 From: Cooltey Feng Date: Tue, 26 Aug 2025 12:48:58 -0700 Subject: [PATCH 45/70] Activity Tab: Display statistics from Growth impact module (#5856) * Activity Tab: Display statistics from Growth impact module * Add image assets * fix * Build line chart and layouts and add strings * Create preview and include linechart * show on the fragment * Optimize chart and bind click events * Refine the layout with better click ripple * Build contribution card * Update contribution card * Fix lint and update string * stats view for all time impact * Complete implementation * Tune up * Update logic for best streak * Add header text color * Use map for impactModule response and update the API call * Code review comments addressed * Reduce duplicated UIs * Remove maxLength --- .../activitytab/ActivityTabFragment.kt | 45 +- .../activitytab/ActivityTabViewModel.kt | 34 +- .../org/wikipedia/activitytab/ImpactModule.kt | 824 ++++++++++++++++++ .../wikipedia/compose/components/LineChart.kt | 119 +++ .../growthtasks/GrowthUserImpact.kt | 31 + .../org/wikipedia/history/HistoryEntry.kt | 1 + .../main/java/org/wikipedia/settings/Prefs.kt | 5 +- .../main/java/org/wikipedia/util/DateUtil.kt | 4 + .../main/res/drawable/baseline_stars_24.xml | 5 + .../main/res/drawable/edit_history_ooui.xml | 9 + .../res/drawable/outline_looks_one_24.xml | 5 + .../res/drawable/outline_looks_three_24.xml | 5 + .../res/drawable/outline_looks_two_24.xml | 5 + .../res/drawable/outline_trending_up_24.xml | 5 + app/src/main/res/values-qq/strings.xml | 39 + app/src/main/res/values/strings.xml | 39 + 16 files changed, 1166 insertions(+), 9 deletions(-) create mode 100644 app/src/main/java/org/wikipedia/activitytab/ImpactModule.kt create mode 100644 app/src/main/java/org/wikipedia/compose/components/LineChart.kt create mode 100644 app/src/main/res/drawable/baseline_stars_24.xml create mode 100644 app/src/main/res/drawable/edit_history_ooui.xml create mode 100644 app/src/main/res/drawable/outline_looks_one_24.xml create mode 100644 app/src/main/res/drawable/outline_looks_three_24.xml create mode 100644 app/src/main/res/drawable/outline_looks_two_24.xml create mode 100644 app/src/main/res/drawable/outline_trending_up_24.xml diff --git a/app/src/main/java/org/wikipedia/activitytab/ActivityTabFragment.kt b/app/src/main/java/org/wikipedia/activitytab/ActivityTabFragment.kt index b1a9f1076cd..49396cfd5eb 100644 --- a/app/src/main/java/org/wikipedia/activitytab/ActivityTabFragment.kt +++ b/app/src/main/java/org/wikipedia/activitytab/ActivityTabFragment.kt @@ -74,11 +74,15 @@ import org.wikipedia.events.LoggedOutEvent import org.wikipedia.events.LoggedOutInBackgroundEvent import org.wikipedia.games.onthisday.OnThisDayGameActivity import org.wikipedia.games.onthisday.OnThisDayGameViewModel +import org.wikipedia.history.HistoryEntry import org.wikipedia.login.LoginActivity import org.wikipedia.navtab.NavTab +import org.wikipedia.page.PageActivity import org.wikipedia.page.PageTitle import org.wikipedia.settings.Prefs +import org.wikipedia.suggestededits.SuggestedEditsTasksActivity import org.wikipedia.theme.Theme +import org.wikipedia.usercontrib.UserContribListActivity import org.wikipedia.util.FeedbackUtil import org.wikipedia.util.UiState import java.time.LocalDateTime @@ -321,7 +325,46 @@ class ActivityTabFragment : Fragment() { ) ) { if (modules.isModuleEnabled(ModuleType.IMPACT)) { - // TODO: zomg do something with this! + Text( + modifier = Modifier.padding(start = 16.dp, end = 16.dp, top = 24.dp), + text = stringResource(R.string.activity_tab_impact), + style = MaterialTheme.typography.titleLarge, + fontWeight = FontWeight.Medium, + color = WikipediaTheme.colors.primaryColor + ) + ImpactModule( + modifier = Modifier + .fillMaxWidth() + .padding(start = 16.dp, end = 16.dp, top = 16.dp), + uiState = impactUiState, + onPageItemClick = { + val entry = HistoryEntry( + title = it, + source = HistoryEntry.SOURCE_ACTIVITY_TAB + ) + requireActivity().startActivity(PageActivity.newIntentForNewTab( + context = requireActivity(), + entry = entry, + title = it + )) + }, + onContributionClick = { + requireActivity().startActivity(UserContribListActivity.newIntent( + context = requireActivity(), + userName = userName + )) + }, + onSuggestedEditsClick = { + requireActivity().startActivity(SuggestedEditsTasksActivity.newIntent( + context = requireActivity() + )) + }, + wikiErrorClickEvents = WikiErrorClickEvents( + retryClickListener = { + viewModel.loadImpact() + } + ) + ) } if (modules.isModuleEnabled(ModuleType.GAMES) || modules.isModuleEnabled(ModuleType.DONATIONS)) { diff --git a/app/src/main/java/org/wikipedia/activitytab/ActivityTabViewModel.kt b/app/src/main/java/org/wikipedia/activitytab/ActivityTabViewModel.kt index 47d09b6faeb..56f1fbf63d8 100644 --- a/app/src/main/java/org/wikipedia/activitytab/ActivityTabViewModel.kt +++ b/app/src/main/java/org/wikipedia/activitytab/ActivityTabViewModel.kt @@ -132,17 +132,39 @@ class ActivityTabViewModel(savedStateHandle: SavedStateHandle) : ViewModel() { _impactUiState.value = UiState.Loading // The impact API is rate limited, so we cache it manually. + val wikiSite = WikipediaApp.instance.wikiSite val now = Instant.now().epochSecond val impact: GrowthUserImpact - if (Prefs.impactLastResponseBody.isEmpty() || abs(now - Prefs.impactLastQueryTime) > TimeUnit.DAYS.toSeconds(1)) { - Prefs.impactLastResponseBody = "" - val userId = ServiceFactory.get(WikipediaApp.instance.wikiSite).getUserInfo().query?.userInfo?.id!! - impact = ServiceFactory.getCoreRest(WikipediaApp.instance.wikiSite).getUserImpact(userId) - Prefs.impactLastResponseBody = JsonUtil.encodeToString(impact).orEmpty() + val impactLastResponseBodyMap = Prefs.impactLastResponseBody.toMutableMap() + val impactResponse = impactLastResponseBodyMap[wikiSite.languageCode] + if (impactResponse.isNullOrEmpty() || abs(now - Prefs.impactLastQueryTime) > TimeUnit.DAYS.toSeconds(1)) { + val userId = ServiceFactory.get(wikiSite).getUserInfo().query?.userInfo?.id!! + impact = ServiceFactory.getCoreRest(wikiSite).getUserImpact(userId) + impactLastResponseBodyMap[wikiSite.languageCode] = JsonUtil.encodeToString(impact).orEmpty() + Prefs.impactLastResponseBody = impactLastResponseBodyMap Prefs.impactLastQueryTime = now } else { - impact = JsonUtil.decodeFromString(Prefs.impactLastResponseBody)!! + impact = JsonUtil.decodeFromString(impactResponse)!! } + + val pagesResponse = ServiceFactory.get(wikiSite).getInfoByPageIdsOrTitles( + titles = impact.topViewedArticles.keys.joinToString(separator = "|") + ) + + // Transform the response to a map of PageTitle to ArticleViews + val pageMap = pagesResponse.query?.pages?.associate { page -> + val pageTitle = PageTitle( + text = page.title, + wiki = wikiSite, + thumbUrl = page.thumbUrl(), + description = page.description, + displayText = page.displayTitle(wikiSite.languageCode) + ) + pageTitle to impact.topViewedArticles[pageTitle.text]!! + } ?: emptyMap() + + impact.topViewedArticlesWithPageTitle = pageMap + _impactUiState.value = UiState.Success(impact) } } diff --git a/app/src/main/java/org/wikipedia/activitytab/ImpactModule.kt b/app/src/main/java/org/wikipedia/activitytab/ImpactModule.kt new file mode 100644 index 00000000000..2a1f1760f13 --- /dev/null +++ b/app/src/main/java/org/wikipedia/activitytab/ImpactModule.kt @@ -0,0 +1,824 @@ +package org.wikipedia.activitytab + +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +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.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +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 +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.graphics.painter.BrushPainter +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.pluralStringResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.core.net.toUri +import coil3.compose.AsyncImage +import org.wikipedia.R +import org.wikipedia.compose.components.HtmlText +import org.wikipedia.compose.components.LineChart +import org.wikipedia.compose.components.WikiCard +import org.wikipedia.compose.components.error.WikiErrorClickEvents +import org.wikipedia.compose.components.error.WikiErrorView +import org.wikipedia.compose.theme.BaseTheme +import org.wikipedia.compose.theme.WikipediaTheme +import org.wikipedia.dataclient.WikiSite +import org.wikipedia.dataclient.growthtasks.GrowthUserImpact +import org.wikipedia.dataclient.growthtasks.GrowthUserImpact.ArticleViews +import org.wikipedia.page.PageTitle +import org.wikipedia.theme.Theme +import org.wikipedia.util.DateUtil +import org.wikipedia.util.UiState +import org.wikipedia.views.imageservice.ImageService +import java.text.NumberFormat +import java.util.Date +import java.util.Locale + +@Composable +fun ImpactModule( + modifier: Modifier = Modifier, + uiState: UiState, + onPageItemClick: (PageTitle) -> Unit, + onContributionClick: (() -> Unit), + onSuggestedEditsClick: (() -> Unit), + wikiErrorClickEvents: WikiErrorClickEvents? = null +) { + when (uiState) { + UiState.Loading -> { + Box( + modifier = modifier + .fillMaxWidth() + .height(200.dp) + ) { + CircularProgressIndicator( + modifier = Modifier + .align(Alignment.Center) + .padding(24.dp), + color = WikipediaTheme.colors.progressiveColor + ) + } + } + is UiState.Success -> { + MostViewedCard( + modifier = modifier + .fillMaxWidth(), + data = uiState.data.topViewedArticlesWithPageTitle, + onClick = { + onPageItemClick(it) + } + ) + ContributionCard( + modifier = modifier + .fillMaxWidth(), + lastEditRelativeTime = uiState.data.lastEditRelativeTime, + editsThisMonth = uiState.data.editsThisMonth, + editsLastMonth = uiState.data.editsLastMonth, + onClick = { + onContributionClick() + } + ) + AllTimeImpactCard( + modifier = modifier + .fillMaxWidth(), + totalEdits = uiState.data.totalEditsCount, + totalThanks = uiState.data.receivedThanksCount, + longestEditingStreak = uiState.data.longestEditingStreak?.totalEditCountForPeriod ?: 0, + lastEditTimestamp = uiState.data.lastEditTimestamp, + lastThirtyDaysEdits = uiState.data.lastThirtyDaysEdits, + totalPageviewsCount = uiState.data.totalPageviewsCount, + dailyTotalViews = uiState.data.dailyTotalViews, + onClick = { + onSuggestedEditsClick() + } + ) + } + + is UiState.Error -> { + Box( + modifier = modifier + .fillMaxSize(), + contentAlignment = Alignment.Center + ) { + WikiErrorView( + modifier = Modifier + .fillMaxWidth(), + caught = uiState.error, + errorClickEvents = wikiErrorClickEvents + ) + } + } + } +} + +@Composable +fun MostViewedCard( + modifier: Modifier = Modifier, + data: Map, + showSize: Int = 3, + onClick: (PageTitle) -> Unit +) { + if (data.isEmpty()) { + return + } + val formatter = remember { NumberFormat.getNumberInstance(Locale.getDefault()) } + WikiCard( + modifier = modifier, + colors = CardDefaults.cardColors( + containerColor = WikipediaTheme.colors.paperColor, + contentColor = WikipediaTheme.colors.paperColor + ), + elevation = 0.dp, + border = BorderStroke( + width = 1.dp, + color = WikipediaTheme.colors.borderColor + ) + ) { + Column { + Row( + modifier = Modifier.padding(16.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + Icon( + modifier = Modifier.size(16.dp), + painter = painterResource(R.drawable.outline_trending_up_24), + tint = WikipediaTheme.colors.primaryColor, + contentDescription = null + ) + Text( + text = stringResource(R.string.activity_tab_impact_most_viewed), + style = MaterialTheme.typography.labelMedium, + color = WikipediaTheme.colors.primaryColor + ) + } + + var index = 1 + data.forEach { (pageTitle, articleViews) -> + if (index > showSize) return@forEach + var iconResource = when (index++) { + 1 -> R.drawable.outline_looks_one_24 + 2 -> R.drawable.outline_looks_two_24 + 3 -> R.drawable.outline_looks_three_24 + else -> null + } + if (data.size <= 1) { + iconResource = null + } + Box( + modifier = Modifier + .fillMaxWidth() + .clickable(onClick = { onClick(pageTitle) }) + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + horizontalArrangement = Arrangement.spacedBy(16.dp) + ) { + iconResource?.let { + Icon( + modifier = Modifier.size(24.dp), + painter = painterResource(it), + tint = WikipediaTheme.colors.primaryColor, + contentDescription = null + ) + } + Column( + modifier = Modifier + .weight(1f) + ) { + HtmlText( + text = pageTitle.displayText, + style = MaterialTheme.typography.titleMedium.copy( + fontFamily = FontFamily.Serif + ), + color = WikipediaTheme.colors.primaryColor, + ) + pageTitle.description?.let { description -> + Text( + text = description, + style = MaterialTheme.typography.bodyMedium, + color = WikipediaTheme.colors.secondaryColor + ) + } + Row( + modifier = Modifier + .fillMaxWidth() + .padding(top = 8.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + LineChart( + map = articleViews.viewsByDay, + modifier = Modifier + .width(24.dp) + .height(6.dp), + chartSampleSize = 10, + strokeWidth = 1.dp, + strokeColor = WikipediaTheme.colors.progressiveColor + ) + Text( + modifier = Modifier + .weight(1f) + .padding(start = 8.dp), + text = formatter.format(articleViews.viewsCount), + style = MaterialTheme.typography.labelSmall, + color = WikipediaTheme.colors.progressiveColor + ) + } + } + if (pageTitle.thumbUrl != null) { + val request = + ImageService.getRequest( + LocalContext.current, + url = pageTitle.thumbUrl + ) + AsyncImage( + model = request, + placeholder = BrushPainter(SolidColor(WikipediaTheme.colors.borderColor)), + error = BrushPainter(SolidColor(WikipediaTheme.colors.borderColor)), + contentScale = ContentScale.Crop, + contentDescription = null, + modifier = Modifier + .size(56.dp) + .clip(RoundedCornerShape(8.dp)) + ) + } + } + } + if (index < data.size - 1) { + HorizontalDivider( + color = WikipediaTheme.colors.borderColor + ) + } + } + } + } +} + +@Composable +fun ContributionCard( + modifier: Modifier = Modifier, + lastEditRelativeTime: String, + editsThisMonth: Int, + editsLastMonth: Int, + onClick: (() -> Unit)? = null +) { + WikiCard( + modifier = modifier + .clickable(onClick = { onClick?.invoke() }), + colors = CardDefaults.cardColors( + containerColor = WikipediaTheme.colors.paperColor, + contentColor = WikipediaTheme.colors.paperColor + ), + elevation = 0.dp, + border = BorderStroke( + width = 1.dp, + color = WikipediaTheme.colors.borderColor + ) + ) { + Column { + Row( + modifier = Modifier.fillMaxWidth() + .padding(top = 16.dp, start = 16.dp, end = 16.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + Icon( + modifier = Modifier.size(16.dp), + painter = painterResource(R.drawable.ic_icon_user_contributions_ooui), + tint = WikipediaTheme.colors.primaryColor, + contentDescription = null + ) + Text( + modifier = Modifier.weight(1f), + text = stringResource(R.string.activity_tab_impact_contributions_this_month), + style = MaterialTheme.typography.labelMedium, + color = WikipediaTheme.colors.primaryColor + ) + Icon( + modifier = Modifier.size(24.dp), + painter = painterResource(R.drawable.ic_chevron_forward_white_24dp), + tint = WikipediaTheme.colors.secondaryColor, + contentDescription = null + ) + } + + Text( + text = lastEditRelativeTime, + modifier = Modifier.padding(start = 16.dp), + style = MaterialTheme.typography.bodySmall, + color = WikipediaTheme.colors.secondaryColor + ) + + Column( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 16.dp) + ) { + val maxEditsCount = maxOf(editsThisMonth, editsLastMonth).toFloat() + + ContributionEditsView( + modifier = Modifier + .fillMaxWidth() + .padding(top = 8.dp), + text = pluralStringResource( + R.plurals.activity_tab_impact_edits_this_month, + editsThisMonth + ).lowercase(), + edits = editsThisMonth, + maxEdits = maxEditsCount, + barColor = WikipediaTheme.colors.successColor + ) + + ContributionEditsView( + modifier = Modifier + .fillMaxWidth() + .padding(top = 8.dp), + text = pluralStringResource( + R.plurals.activity_tab_impact_edits_last_month, + editsLastMonth + ).lowercase(), + edits = editsLastMonth, + maxEdits = maxEditsCount, + barColor = WikipediaTheme.colors.borderColor + ) + } + } + } +} + +@Composable +fun AllTimeImpactCard( + modifier: Modifier = Modifier, + totalEdits: Int = 0, + totalThanks: Int = 0, + longestEditingStreak: Int = 0, + lastEditTimestamp: Long = 0, + lastThirtyDaysEdits: Map = emptyMap(), + totalPageviewsCount: Long = 0, + dailyTotalViews: Map = emptyMap(), + onClick: (() -> Unit)? = null +) { + val formatter = remember { NumberFormat.getNumberInstance(Locale.getDefault()) } + WikiCard( + modifier = modifier, + colors = CardDefaults.cardColors( + containerColor = WikipediaTheme.colors.paperColor, + contentColor = WikipediaTheme.colors.paperColor + ), + elevation = 0.dp, + border = BorderStroke( + width = 1.dp, + color = WikipediaTheme.colors.borderColor + ) + ) { + Column( + modifier = Modifier.padding(16.dp) + ) { + Text( + text = stringResource(R.string.activity_tab_impact_all_time), + style = MaterialTheme.typography.labelMedium.copy( + fontWeight = FontWeight.SemiBold + ), + color = WikipediaTheme.colors.primaryColor, + lineHeight = MaterialTheme.typography.labelMedium.lineHeight + ) + Row( + modifier = Modifier + .fillMaxWidth() + .padding(top = 16.dp), + horizontalArrangement = Arrangement.spacedBy(16.dp) + ) { + ImpactStatView( + modifier = Modifier.weight(1f), + iconResource = R.drawable.ic_mode_edit_white_24dp, + statValue = totalEdits.toString(), + statLabel = pluralStringResource(R.plurals.activity_tab_impact_total_edits, totalEdits) + ) + val bestStreakString = if (longestEditingStreak > 0) { + pluralStringResource(R.plurals.activity_tab_impact_best_streak_text, longestEditingStreak, longestEditingStreak) + } else { + "-" + } + ImpactStatView( + modifier = Modifier.weight(1f), + iconResource = R.drawable.baseline_stars_24, + statValue = bestStreakString, + statLabel = stringResource(R.string.activity_tab_impact_best_streak) + ) + } + Row( + modifier = Modifier + .fillMaxWidth() + .padding(top = 16.dp), + horizontalArrangement = Arrangement.spacedBy(16.dp) + ) { + ImpactStatView( + modifier = Modifier.weight(1f), + iconResource = R.drawable.ic_notification_thanks, + statValue = totalThanks.toString(), + statLabel = pluralStringResource(R.plurals.activity_tab_impact_thanks, totalThanks) + ) + val lastEditedDateString = if (lastEditTimestamp > 0) { + DateUtil.getMDYDateString(Date(lastEditTimestamp * 1000)) + } else { + "-" + } + ImpactStatView( + modifier = Modifier.weight(1f), + iconResource = R.drawable.edit_history_ooui, + statValue = lastEditedDateString, + statLabel = stringResource(R.string.activity_tab_impact_last_edited) + ) + } + + HorizontalDivider( + modifier = Modifier.padding(top = 16.dp), + color = WikipediaTheme.colors.borderColor + ) + + if (totalEdits == 0) { + ImpactSuggestedEditsView(onClick = onClick) + } else { + Column( + modifier = Modifier.padding(top = 16.dp) + ) { + Text( + text = stringResource(R.string.activity_tab_impact_recent_activity), + style = MaterialTheme.typography.labelMedium.copy( + fontWeight = FontWeight.SemiBold + ), + color = WikipediaTheme.colors.primaryColor, + lineHeight = MaterialTheme.typography.labelMedium.lineHeight + ) + Row( + modifier = Modifier + .fillMaxWidth() + .padding(top = 16.dp), + horizontalArrangement = Arrangement.spacedBy(4.dp), + verticalAlignment = Alignment.Bottom + ) { + val recentEditsCount = lastThirtyDaysEdits.values.sum() + Text( + text = recentEditsCount.toString(), + modifier = Modifier.align(Alignment.Bottom), + style = MaterialTheme.typography.titleLarge, + fontWeight = FontWeight.Medium, + color = WikipediaTheme.colors.primaryColor + ) + Text( + text = pluralStringResource( + R.plurals.activity_tab_impact_recent_activity_edits, + recentEditsCount + ).lowercase(), + style = MaterialTheme.typography.bodySmall.copy( + fontWeight = FontWeight.Medium, + lineHeight = 22.sp + ), + color = WikipediaTheme.colors.secondaryColor + ) + } + Row( + modifier = Modifier + .fillMaxWidth() + .padding(top = 8.dp) + ) { + val totalSize = lastThirtyDaysEdits.size + lastThirtyDaysEdits.forEach { (_, editCount) -> + val pileColor = if (editCount > 0) { + WikipediaTheme.colors.progressiveColor + } else { + WikipediaTheme.colors.borderColor + } + Box( + modifier = Modifier + .weight(1f / totalSize) + .height(24.dp) + .padding(horizontal = 1.dp) + .background(color = pileColor, shape = RoundedCornerShape(8.dp)) + ) + } + } + Row( + modifier = Modifier + .fillMaxWidth() + .padding(top = 8.dp) + ) { + Text( + modifier = Modifier.weight(1f), + text = DateUtil.getMonthOnlyDateStringFromTimeString(lastThirtyDaysEdits.keys.first()), + style = MaterialTheme.typography.bodySmall, + color = WikipediaTheme.colors.secondaryColor + ) + Text( + text = DateUtil.getMonthOnlyDateStringFromTimeString(lastThirtyDaysEdits.keys.last()), + style = MaterialTheme.typography.bodySmall, + color = WikipediaTheme.colors.secondaryColor + ) + } + + HorizontalDivider( + modifier = Modifier.padding(top = 16.dp), + color = WikipediaTheme.colors.borderColor + ) + + Row( + modifier = Modifier + .fillMaxWidth() + .padding(top = 16.dp), + horizontalArrangement = Arrangement.spacedBy(4.dp), + verticalAlignment = Alignment.Bottom + ) { + Text( + text = formatter.format(totalPageviewsCount), + modifier = Modifier.align(Alignment.Bottom), + style = MaterialTheme.typography.titleLarge, + fontWeight = FontWeight.Medium, + color = WikipediaTheme.colors.primaryColor + ) + Text( + text = pluralStringResource( + R.plurals.activity_tab_impact_recent_activity_views, + totalPageviewsCount.toInt() + ).lowercase(), + style = MaterialTheme.typography.bodySmall.copy( + fontWeight = FontWeight.Medium, + lineHeight = 22.sp + ), + color = WikipediaTheme.colors.secondaryColor + ) + } + + LineChart( + map = dailyTotalViews, + modifier = Modifier + .fillMaxWidth() + .height(32.dp) + .padding(top = 8.dp), + chartSampleSize = 90, + strokeWidth = 2.dp, + strokeColor = WikipediaTheme.colors.progressiveColor + ) + } + } + } + } +} + +@Composable +fun ImpactSuggestedEditsView( + onClick: (() -> Unit)? = null +) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Text( + modifier = Modifier.padding(bottom = 8.dp), + text = stringResource(R.string.activity_tab_impact_suggested_edits_title), + style = MaterialTheme.typography.titleSmall.copy( + fontWeight = FontWeight.Medium + ), + color = WikipediaTheme.colors.primaryColor + ) + Text( + textAlign = TextAlign.Center, + text = stringResource(R.string.activity_tab_impact_suggested_edits_message), + style = MaterialTheme.typography.bodyMedium, + color = WikipediaTheme.colors.primaryColor + ) + Button( + modifier = Modifier.padding(top = 16.dp).align(Alignment.CenterHorizontally), + contentPadding = PaddingValues(horizontal = 18.dp), + colors = ButtonDefaults.buttonColors( + containerColor = WikipediaTheme.colors.progressiveColor, + contentColor = WikipediaTheme.colors.paperColor, + ), + onClick = { onClick?.invoke() }, + ) { + Icon( + modifier = Modifier.size(20.dp), + painter = painterResource(R.drawable.ic_mode_edit_white_24dp), + tint = WikipediaTheme.colors.paperColor, + contentDescription = null + ) + Text( + modifier = Modifier.padding(start = 6.dp, top = 4.dp, bottom = 4.dp), + text = stringResource(R.string.activity_tab_impact_suggested_edits_button), + style = MaterialTheme.typography.labelLarge + ) + } + } +} + +@Composable +fun ContributionEditsView( + modifier: Modifier, + text: String, + edits: Int, + maxEdits: Float, + barColor: Color +) { + Row( + modifier = modifier, + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.Bottom + ) { + Text( + text = edits.toString(), + modifier = Modifier.padding(start = 16.dp).align(Alignment.Bottom), + style = MaterialTheme.typography.titleLarge, + fontWeight = FontWeight.Medium, + color = WikipediaTheme.colors.primaryColor + ) + Text( + text = text, + style = MaterialTheme.typography.bodySmall.copy( + fontWeight = FontWeight.Medium, + lineHeight = 22.sp + ), + color = WikipediaTheme.colors.secondaryColor + ) + } + + if (edits > 0) { + Box( + modifier = Modifier + .fillMaxWidth(edits.toFloat() / maxEdits) + .padding(horizontal = 16.dp) + .height(20.dp) + .background( + color = barColor, + shape = RoundedCornerShape(16.dp) + ) + ) + } +} + +@Composable +fun ImpactStatView( + modifier: Modifier, + iconResource: Int, + statValue: String, + statLabel: String +) { + Row( + modifier = modifier + ) { + Icon( + modifier = Modifier.size(28.dp), + painter = painterResource(iconResource), + tint = WikipediaTheme.colors.progressiveColor, + contentDescription = null + ) + Column( + modifier = Modifier.padding(horizontal = 12.dp) + ) { + Text( + text = statValue, + style = MaterialTheme.typography.labelLarge.copy( + fontWeight = FontWeight.SemiBold + ), + color = WikipediaTheme.colors.primaryColor + ) + Text( + text = statLabel.lowercase(), + style = MaterialTheme.typography.bodySmall, + color = WikipediaTheme.colors.primaryColor + ) + } + } +} + +@Preview +@Composable +private fun MostViewedCardPreview() { + BaseTheme( + currentTheme = Theme.LIGHT + ) { + val pageTitle = PageTitle( + text = "Test Article", + displayText = "Test Article", + wiki = WikiSite("en.wikipedia.org".toUri(), "en"), + description = "This is a test article", + thumbUrl = "https://example.com/thumb.jpg" + ) + val articleViews = ArticleViews( + firstEditDate = "2023-01-01", + newestEdit = "2023-10-01", + imageUrl = "https://example.com/image.jpg", + viewsCount = 1000 + ) + MostViewedCard( + data = mapOf( + pageTitle to articleViews + ), + onClick = { }, + ) + } +} + +@Preview +@Composable +private fun ContributionCardPreview() { + BaseTheme( + currentTheme = Theme.LIGHT + ) { + ContributionCard( + modifier = Modifier.fillMaxWidth(), + lastEditRelativeTime = "2 days ago", + editsThisMonth = 9, + editsLastMonth = 2, + onClick = {} + ) + } +} + +@Preview +@Composable +private fun AllTimeImpactCardPreview() { + BaseTheme( + currentTheme = Theme.LIGHT + ) { + AllTimeImpactCard( + modifier = Modifier.fillMaxWidth(), + totalEdits = 123, + totalThanks = 56, + longestEditingStreak = 15, + lastEditTimestamp = System.currentTimeMillis() - 86400000L, + lastThirtyDaysEdits = mapOf( + "2023-10-01" to 5, + "2023-10-02" to 3, + "2023-10-03" to 7, + "2023-10-04" to 0, + "2023-10-05" to 4, + "2023-10-06" to 6, + "2023-10-07" to 2, + "2023-10-08" to 1, + "2023-10-09" to 0, + "2023-10-10" to 8, + "2023-10-11" to 0, + "2023-10-12" to 3, + "2023-10-13" to 5, + "2023-10-14" to 2, + "2023-10-15" to 4, + "2023-10-16" to 0, + "2023-10-17" to 1, + "2023-10-18" to 0, + "2023-10-19" to 6, + "2023-10-20" to 2, + "2023-10-21" to 3, + "2023-10-22" to 0, + "2023-10-23" to 4, + "2023-10-24" to 5, + "2023-10-25" to 0, + "2023-10-26" to 1, + "2023-10-27" to 2, + "2023-10-28" to 0, + "2023-10-29" to 3, + "2023-10-30" to 4, + "2023-10-31" to 0 + ), + totalPageviewsCount = 7890, + dailyTotalViews = mapOf( + "2023-10-01" to 100, + "2023-10-02" to 150, + "2023-10-03" to 200, + "2023-10-04" to 120, + "2023-10-05" to 180, + "2023-10-06" to 220 + ), + onClick = {} + ) + } +} diff --git a/app/src/main/java/org/wikipedia/compose/components/LineChart.kt b/app/src/main/java/org/wikipedia/compose/components/LineChart.kt new file mode 100644 index 00000000000..5d1b4f1b52e --- /dev/null +++ b/app/src/main/java/org/wikipedia/compose/components/LineChart.kt @@ -0,0 +1,119 @@ +package org.wikipedia.compose.components + +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +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.Path +import androidx.compose.ui.graphics.StrokeCap +import androidx.compose.ui.graphics.drawscope.Stroke +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import org.wikipedia.compose.theme.BaseTheme +import org.wikipedia.compose.theme.WikipediaTheme +import org.wikipedia.theme.Theme +import kotlin.math.max + +@Composable +fun LineChart( + map: Map, + modifier: Modifier = Modifier, + chartSampleSize: Int = 10, + strokeWidth: Dp = 2.dp, + strokeColor: Color = WikipediaTheme.colors.progressiveColor +) { + if (map.isEmpty()) { + return + } + + val sortedValues = map.entries.sortedBy { it.key }.map { it.value } + + // Downsample the data to avoid the chart being too crowded in a small layout. + val downsampledList = if (sortedValues.size <= chartSampleSize) { + sortedValues + } else { + val chunkSize = sortedValues.size / chartSampleSize + sortedValues + .chunked(chunkSize) + .map { it.average().toInt() } + } + + val maxValue = downsampledList.maxOrNull() ?: 1 + val minValue = 0 + val range = max(maxValue - minValue, 1) + + Canvas( + modifier = modifier + ) { + val canvasWidth = size.width + val canvasHeight = size.height + val xScale = canvasWidth / (downsampledList.size - 1) + val yScale = canvasHeight / range + + val path = Path() + val firstY = canvasHeight - ((downsampledList.first() - minValue) * yScale) + path.moveTo(0f, firstY) + + var index = 0 + downsampledList.forEach { + if (index == 0) { + index++ // Skip the first point since we already moved to it + return@forEach + } + val x = index++ * xScale + val y = canvasHeight - ((it - minValue) * yScale) + path.lineTo(x, y) + } + + drawPath( + path = path, + color = strokeColor, + style = Stroke(width = strokeWidth.toPx(), cap = StrokeCap.Round) + ) + } +} + +@Preview(showBackground = true) +@Composable +private fun LineChartPreview() { + BaseTheme( + currentTheme = Theme.LIGHT + ) { + Box( + modifier = Modifier + .fillMaxSize(), + contentAlignment = Alignment.Center + ) { + LineChart( + map = mapOf( + "2025-01-01" to 100, + "2025-01-02" to 122, + "2025-01-03" to 100, + "2025-01-04" to 103, + "2025-01-05" to 120, + "2025-01-06" to 121, + "2025-01-07" to 110, + "2025-01-08" to 153, + "2025-01-09" to 100, + "2025-01-10" to 150, + "2025-01-11" to 160, + "2025-01-12" to 170, + "2025-01-13" to 180, + "2025-01-14" to 140, + "2025-01-15" to 130, + "2025-01-16" to 106, + "2025-01-17" to 102, + "2025-01-18" to 103, + "2025-01-19" to 95, + "2025-01-20" to 76, + ), + modifier = Modifier.fillMaxSize(), + chartSampleSize = 10, + ) + } + } +} diff --git a/app/src/main/java/org/wikipedia/dataclient/growthtasks/GrowthUserImpact.kt b/app/src/main/java/org/wikipedia/dataclient/growthtasks/GrowthUserImpact.kt index 81e2ca7a76c..ca1c65a1b36 100644 --- a/app/src/main/java/org/wikipedia/dataclient/growthtasks/GrowthUserImpact.kt +++ b/app/src/main/java/org/wikipedia/dataclient/growthtasks/GrowthUserImpact.kt @@ -1,11 +1,14 @@ package org.wikipedia.dataclient.growthtasks +import android.text.format.DateUtils import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable import kotlinx.serialization.json.JsonElement import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.decodeFromJsonElement import org.wikipedia.json.JsonUtil +import org.wikipedia.page.PageTitle +import java.time.LocalDate @Suppress("unused") @Serializable @@ -40,6 +43,34 @@ class GrowthUserImpact( val topViewedArticles: Map by lazy { if (mTopViewedArticles is JsonObject) { JsonUtil.json.decodeFromJsonElement(mTopViewedArticles) } else { emptyMap() } } val longestEditingStreak: EditStreak? by lazy { if (mLongestEditingStreak is JsonObject) { JsonUtil.json.decodeFromJsonElement(mLongestEditingStreak) } else { null } } + val groupEditsByMonth: Map by lazy { + editCountByDay.entries.groupBy { it.key.substring(0, 7) } + .mapValues { it.value.sumOf { entry -> entry.value } } + } + + var topViewedArticlesWithPageTitle: Map = emptyMap() + + val editsThisMonth by lazy { groupEditsByMonth[LocalDate.now().toString().substring(0, 7)] ?: 0 } + val editsLastMonth by lazy { groupEditsByMonth[LocalDate.now().minusMonths(1).toString().substring(0, 7)] ?: 0 } + + val lastThirtyDaysEdits by lazy { + val thirtyDaysAgo = LocalDate.now().minusDays(30) + // Some days do not have a key, fill them with 0 + val filledEditCountByDay = (0..30).associate { + val date = thirtyDaysAgo.plusDays(it.toLong()).toString() + date to (editCountByDay[date] ?: 0) + } + filledEditCountByDay + } + + val lastEditRelativeTime by lazy { + DateUtils.getRelativeTimeSpanString( + lastEditTimestamp * 1000, + System.currentTimeMillis(), + 0L + ).toString() + } + @Serializable class ArticleViews( val firstEditDate: String = "", diff --git a/app/src/main/java/org/wikipedia/history/HistoryEntry.kt b/app/src/main/java/org/wikipedia/history/HistoryEntry.kt index e68b664df97..6be73b7668d 100644 --- a/app/src/main/java/org/wikipedia/history/HistoryEntry.kt +++ b/app/src/main/java/org/wikipedia/history/HistoryEntry.kt @@ -102,5 +102,6 @@ class HistoryEntry( const val SOURCE_FEED_PLACES = 42 const val SOURCE_ON_THIS_DAY_GAME = 43 const val SOURCE_RECOMMENDED_READING_LIST = 44 + const val SOURCE_ACTIVITY_TAB = 45 } } diff --git a/app/src/main/java/org/wikipedia/settings/Prefs.kt b/app/src/main/java/org/wikipedia/settings/Prefs.kt index 35d9c2f6d4d..a8a781548cf 100644 --- a/app/src/main/java/org/wikipedia/settings/Prefs.kt +++ b/app/src/main/java/org/wikipedia/settings/Prefs.kt @@ -840,8 +840,9 @@ object Prefs { set(value) = PrefsIoUtil.setLong(R.string.preference_key_impact_last_query_time, value) var impactLastResponseBody - get() = PrefsIoUtil.getString(R.string.preference_key_impact_last_response_body, null).orEmpty() - set(value) = PrefsIoUtil.setString(R.string.preference_key_impact_last_response_body, value) + get() = JsonUtil.decodeFromString>(PrefsIoUtil.getString(R.string.preference_key_impact_last_response_body, null)) + ?: emptyMap() + set(langSupportedMap) = PrefsIoUtil.setString(R.string.preference_key_impact_last_response_body, JsonUtil.encodeToString(langSupportedMap)) var donationReminderConfig get() = JsonUtil.decodeFromString( diff --git a/app/src/main/java/org/wikipedia/util/DateUtil.kt b/app/src/main/java/org/wikipedia/util/DateUtil.kt index cb32ceb6d51..90234626f25 100644 --- a/app/src/main/java/org/wikipedia/util/DateUtil.kt +++ b/app/src/main/java/org/wikipedia/util/DateUtil.kt @@ -63,6 +63,10 @@ object DateUtil { return getDateStringWithSkeletonPattern(date, "MMMM d") } + fun getMonthOnlyDateStringFromTimeString(dateStr: String): String { + return getMonthOnlyDateString(SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).parse(dateStr)!!) + } + fun getMonthOnlyWithoutDayDateString(date: Date): String { return getDateStringWithSkeletonPattern(date, "MMMM") } diff --git a/app/src/main/res/drawable/baseline_stars_24.xml b/app/src/main/res/drawable/baseline_stars_24.xml new file mode 100644 index 00000000000..03306ee68de --- /dev/null +++ b/app/src/main/res/drawable/baseline_stars_24.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/edit_history_ooui.xml b/app/src/main/res/drawable/edit_history_ooui.xml new file mode 100644 index 00000000000..4a4448436f1 --- /dev/null +++ b/app/src/main/res/drawable/edit_history_ooui.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/outline_looks_one_24.xml b/app/src/main/res/drawable/outline_looks_one_24.xml new file mode 100644 index 00000000000..0f4c4e0ab54 --- /dev/null +++ b/app/src/main/res/drawable/outline_looks_one_24.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/outline_looks_three_24.xml b/app/src/main/res/drawable/outline_looks_three_24.xml new file mode 100644 index 00000000000..bb32e9e91a8 --- /dev/null +++ b/app/src/main/res/drawable/outline_looks_three_24.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/outline_looks_two_24.xml b/app/src/main/res/drawable/outline_looks_two_24.xml new file mode 100644 index 00000000000..d0433678b6f --- /dev/null +++ b/app/src/main/res/drawable/outline_looks_two_24.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/outline_trending_up_24.xml b/app/src/main/res/drawable/outline_trending_up_24.xml new file mode 100644 index 00000000000..c2100b4f259 --- /dev/null +++ b/app/src/main/res/drawable/outline_trending_up_24.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/values-qq/strings.xml b/app/src/main/res/values-qq/strings.xml index a8e359e3390..e8231aa041a 100644 --- a/app/src/main/res/values-qq/strings.xml +++ b/app/src/main/res/values-qq/strings.xml @@ -1191,6 +1191,45 @@ Menu label for opening the activity tab customization screen. Text shown in the Customize screen explaining how Activity tab works. Label for the Highlights section of the Activity tab. + Title of the switch for the Reading history module in the customize screen. + Label for the Impact section of the activity tab + Title of the most viewed card of the activity tab. + Title of the contributions card of the activity tab. + + Label of the contributions card that indicates the edit this month. + Label of the contributions card that indicates the edits this month. + + + Label of the contributions card that indicates the edit last month. + Label of the contributions card that indicates the edits last month. + + Title of the all time impact card of the activity tab. + + Label of the suggested edits card that indicates the number of total edits. + Label of the suggested edits card that indicates the number of total edits. + + + Label of the suggested edits card that indicates the number of thanks. + Label of the suggested edits card that indicates the number of thanks. + + + Text that indicates the length of the best streak. %d will be replaced by the number of days. + Text that indicates the length of the best streak. %d will be replaced by the number of days. + + Label of the suggested edits card that indicates the best streak + Label of the suggested edits card that indicates the last edited + Title of the suggested edits card in the impact section. + Message of the suggested edits card in the impact section. + Button label of the suggested edits card in the impact section. + Title of the recent activity section in the suggested edits card in the impact section. + + Label of the recent activity section in the suggested edits card that indicates the number of total edits. + Label of the recent activity section in the suggested edits card that indicates the number of total edits. + + + Label of the recent activity section in the suggested edits card that indicates the total views. + Label of the recent activity section in the suggested edits card that indicates the total views. + Title of the switch to enable total time spent insights in the Activity Tab customization screen. Title of the switch to enable reading insights in the Activity Tab customization screen. Title of the switch to enable editing insights in the Activity Tab customization screen. diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 245cd1d82ee..2958c180399 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1213,6 +1213,45 @@ Customize Activity tab insights are based on the primary language set in settings and is leveraging local data with the exception of edits which are public. Highlights + Reading history + Your impact + Most viewed since your edit + Contributions this month + + Edit this month + Edits this month + + + Edit last month + Edits last month + + All time impact + + Total edit + Total edits + + + Thank + Thanks + + + %d day + %d days + + Best streak + Last edited + 0 edits to articles so far + Help extend free knowledge to the world by editing topics that matter most to you. + Try out Suggested edits + Your recent activity (last 30 days) + + Edit + Edits + + + View on articles you\'ve edited + Views on articles you\'ve edited + Time spent reading Reading insights Editing insights From 7a32431e56751b51cd27fd4b92d806ff688ab79d Mon Sep 17 00:00:00 2001 From: Dmitry Brant Date: Tue, 26 Aug 2025 15:54:27 -0400 Subject: [PATCH 46/70] Reset cached impact response when logging out. --- app/src/main/java/org/wikipedia/WikipediaApp.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/src/main/java/org/wikipedia/WikipediaApp.kt b/app/src/main/java/org/wikipedia/WikipediaApp.kt index 1f70072bc6b..8b866793a67 100644 --- a/app/src/main/java/org/wikipedia/WikipediaApp.kt +++ b/app/src/main/java/org/wikipedia/WikipediaApp.kt @@ -257,6 +257,8 @@ class WikipediaApp : Application() { Prefs.tempAccountWelcomeShown = false Prefs.tempAccountCreateDay = 0L Prefs.tempAccountDialogShown = false + Prefs.impactLastQueryTime = 0 + Prefs.impactLastResponseBody = emptyMap() SharedPreferenceCookieManager.instance.clearAllCookies() MainScope().launch { AppDatabase.instance.notificationDao().deleteAll() From 3c1469663a7bb6f75bd28fcf7e1046cbe0c6805b Mon Sep 17 00:00:00 2001 From: Dmitry Brant Date: Tue, 26 Aug 2025 15:54:48 -0400 Subject: [PATCH 47/70] Tidy up strings. --- app/src/main/res/values/strings.xml | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 2958c180399..a921d5162af 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1218,21 +1218,21 @@ Most viewed since your edit Contributions this month - Edit this month - Edits this month + edit this month + edits this month - Edit last month - Edits last month + edit last month + edits last month All time impact - Total edit - Total edits + total edit + total edits - Thank - Thanks + thank + thanks %d day From 41a6eb6e1b0f823f80b8452db4f3e512009f8263 Mon Sep 17 00:00:00 2001 From: Cooltey Feng Date: Wed, 27 Aug 2025 06:33:22 -0700 Subject: [PATCH 48/70] Hide the Donation module if no donation data in the app in Acitivty Tab (#5871) --- .../ActivityTabCustomizationActivity.kt | 16 ++++++-- .../activitytab/ActivityTabFragment.kt | 2 +- .../wikipedia/activitytab/DonationModule.kt | 39 ------------------- app/src/main/res/values-qq/strings.xml | 1 - app/src/main/res/values/strings.xml | 1 - 5 files changed, 13 insertions(+), 46 deletions(-) diff --git a/app/src/main/java/org/wikipedia/activitytab/ActivityTabCustomizationActivity.kt b/app/src/main/java/org/wikipedia/activitytab/ActivityTabCustomizationActivity.kt index d8e4993eb09..5574a38b3d8 100644 --- a/app/src/main/java/org/wikipedia/activitytab/ActivityTabCustomizationActivity.kt +++ b/app/src/main/java/org/wikipedia/activitytab/ActivityTabCustomizationActivity.kt @@ -45,7 +45,9 @@ class ActivityTabCustomizationActivity : BaseActivity() { CustomizationScreen( onBackButtonClick = { finish() - } + }, + modules = Prefs.activityTabModules, + showLastDonation = Prefs.donationResults.isNotEmpty() ) } } @@ -61,9 +63,11 @@ class ActivityTabCustomizationActivity : BaseActivity() { @Composable fun CustomizationScreen( modifier: Modifier = Modifier, - onBackButtonClick: () -> Unit + onBackButtonClick: () -> Unit, + modules: ActivityTabModules, + showLastDonation: Boolean = false ) { - var currentModules by remember { mutableStateOf(Prefs.activityTabModules) } + var currentModules by remember { mutableStateOf(modules) } Scaffold( modifier = modifier @@ -92,6 +96,9 @@ fun CustomizationScreen( ) } itemsIndexed(ModuleType.entries) { index, moduleType -> + if (moduleType == ModuleType.DONATIONS && !showLastDonation) { + return@itemsIndexed + } CustomizationScreenSwitch( isChecked = currentModules.isModuleEnabled(moduleType), title = stringResource(moduleType.displayName), @@ -196,7 +203,8 @@ private fun CustomizationScreenPreview() { currentTheme = Theme.LIGHT ) { CustomizationScreen( - onBackButtonClick = {} + onBackButtonClick = {}, + modules = ActivityTabModules() ) } } diff --git a/app/src/main/java/org/wikipedia/activitytab/ActivityTabFragment.kt b/app/src/main/java/org/wikipedia/activitytab/ActivityTabFragment.kt index 49396cfd5eb..1baab70a1b1 100644 --- a/app/src/main/java/org/wikipedia/activitytab/ActivityTabFragment.kt +++ b/app/src/main/java/org/wikipedia/activitytab/ActivityTabFragment.kt @@ -407,7 +407,7 @@ class ActivityTabFragment : Fragment() { ) } - if (modules.isModuleEnabled(ModuleType.DONATIONS)) { + if (modules.isModuleEnabled(ModuleType.DONATIONS) && Prefs.donationResults.isNotEmpty()) { DonationModule( modifier = Modifier .fillMaxWidth() diff --git a/app/src/main/java/org/wikipedia/activitytab/DonationModule.kt b/app/src/main/java/org/wikipedia/activitytab/DonationModule.kt index 1e0003e098a..cc5af199eb0 100644 --- a/app/src/main/java/org/wikipedia/activitytab/DonationModule.kt +++ b/app/src/main/java/org/wikipedia/activitytab/DonationModule.kt @@ -5,14 +5,11 @@ import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.PaddingValues 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.foundation.layout.size -import androidx.compose.material3.Button -import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme @@ -100,32 +97,6 @@ fun DonationModule( color = WikipediaTheme.colors.progressiveColor, fontWeight = FontWeight.Medium ) - if (uiState.data.isNullOrEmpty()) { - Button( - modifier = Modifier - .padding(top = 16.dp) - .fillMaxWidth() - .align(Alignment.CenterHorizontally), - contentPadding = PaddingValues(horizontal = 18.dp), - colors = ButtonDefaults.buttonColors( - containerColor = WikipediaTheme.colors.progressiveColor, - contentColor = WikipediaTheme.colors.paperColor, - ), - onClick = { onClick?.invoke() }, - ) { - Icon( - modifier = Modifier.size(20.dp), - painter = painterResource(R.drawable.ic_heart_24), - tint = WikipediaTheme.colors.paperColor, - contentDescription = null - ) - Text( - modifier = Modifier.padding(start = 6.dp, top = 4.dp, bottom = 4.dp), - text = stringResource(R.string.activity_tab_donation_button), - style = MaterialTheme.typography.labelLarge - ) - } - } } } } @@ -140,13 +111,3 @@ private fun DonationModulePreview() { ) } } - -@Preview -@Composable -private fun DonationModuleEmptyPreview() { - BaseTheme(currentTheme = Theme.LIGHT) { - DonationModule( - uiState = UiState.Success(null) - ) - } -} diff --git a/app/src/main/res/values-qq/strings.xml b/app/src/main/res/values-qq/strings.xml index e8231aa041a..f0956c3fd9e 100644 --- a/app/src/main/res/values-qq/strings.xml +++ b/app/src/main/res/values-qq/strings.xml @@ -1179,7 +1179,6 @@ Label on a card that lists the top categories of articles that were read this month. Button label to go to the Reading Lists screen, which will give the user further guidance for exploring Wikipedia. Label encouraging the user to discover new articles. - Button label to go to the donation workflow. Label on card that shows the last donation time. Title of the game stats card. Label of the game stats that indicates the best streak. diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index a921d5162af..6ca0690099b 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1201,7 +1201,6 @@ Top categories read this month Discover through Wikipedia Looking for something new to read? - Donate to the Wikimedia Foundation Unknown Last donation in app]]> Game stats From 1479edbe54df437293ef7eeb3586cf8e8e981ada Mon Sep 17 00:00:00 2001 From: Cooltey Feng Date: Wed, 27 Aug 2025 06:41:20 -0700 Subject: [PATCH 49/70] Renames it (#5872) --- .../activitytab/ActivityTabFragment.kt | 42 +- .../activitytab/EditingInsightsModule.kt | 513 ++++++++++++++++++ .../org/wikipedia/activitytab/ImpactModule.kt | 435 +-------------- 3 files changed, 551 insertions(+), 439 deletions(-) create mode 100644 app/src/main/java/org/wikipedia/activitytab/EditingInsightsModule.kt diff --git a/app/src/main/java/org/wikipedia/activitytab/ActivityTabFragment.kt b/app/src/main/java/org/wikipedia/activitytab/ActivityTabFragment.kt index 1baab70a1b1..47791431295 100644 --- a/app/src/main/java/org/wikipedia/activitytab/ActivityTabFragment.kt +++ b/app/src/main/java/org/wikipedia/activitytab/ActivityTabFragment.kt @@ -324,15 +324,16 @@ class ActivityTabFragment : Fragment() { ) ) ) { - if (modules.isModuleEnabled(ModuleType.IMPACT)) { - Text( - modifier = Modifier.padding(start = 16.dp, end = 16.dp, top = 24.dp), - text = stringResource(R.string.activity_tab_impact), - style = MaterialTheme.typography.titleLarge, - fontWeight = FontWeight.Medium, - color = WikipediaTheme.colors.primaryColor - ) - ImpactModule( + Text( + modifier = Modifier.padding(start = 16.dp, end = 16.dp, top = 24.dp), + text = stringResource(R.string.activity_tab_impact), + style = MaterialTheme.typography.titleLarge, + fontWeight = FontWeight.Medium, + color = WikipediaTheme.colors.primaryColor + ) + + if (modules.isModuleEnabled(ModuleType.EDITING_INSIGHTS)) { + EditingInsightsModule( modifier = Modifier .fillMaxWidth() .padding(start = 16.dp, end = 16.dp, top = 16.dp), @@ -342,20 +343,23 @@ class ActivityTabFragment : Fragment() { title = it, source = HistoryEntry.SOURCE_ACTIVITY_TAB ) - requireActivity().startActivity(PageActivity.newIntentForNewTab( + requireActivity().startActivity( + PageActivity.newIntentForNewTab( context = requireActivity(), entry = entry, title = it )) }, onContributionClick = { - requireActivity().startActivity(UserContribListActivity.newIntent( + requireActivity().startActivity( + UserContribListActivity.newIntent( context = requireActivity(), userName = userName )) }, onSuggestedEditsClick = { - requireActivity().startActivity(SuggestedEditsTasksActivity.newIntent( + requireActivity().startActivity( + SuggestedEditsTasksActivity.newIntent( context = requireActivity() )) }, @@ -367,6 +371,20 @@ class ActivityTabFragment : Fragment() { ) } + if (modules.isModuleEnabled(ModuleType.IMPACT)) { + ImpactModule( + modifier = Modifier + .fillMaxWidth() + .padding(start = 16.dp, end = 16.dp, top = 16.dp), + uiState = impactUiState, + wikiErrorClickEvents = WikiErrorClickEvents( + retryClickListener = { + viewModel.loadImpact() + } + ) + ) + } + if (modules.isModuleEnabled(ModuleType.GAMES) || modules.isModuleEnabled(ModuleType.DONATIONS)) { Text( modifier = Modifier.padding(start = 16.dp, end = 16.dp, top = 24.dp), diff --git a/app/src/main/java/org/wikipedia/activitytab/EditingInsightsModule.kt b/app/src/main/java/org/wikipedia/activitytab/EditingInsightsModule.kt new file mode 100644 index 00000000000..1cc9f301b42 --- /dev/null +++ b/app/src/main/java/org/wikipedia/activitytab/EditingInsightsModule.kt @@ -0,0 +1,513 @@ +package org.wikipedia.activitytab + +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +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.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +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 +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.graphics.painter.BrushPainter +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.pluralStringResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.core.net.toUri +import coil3.compose.AsyncImage +import org.wikipedia.R +import org.wikipedia.compose.components.HtmlText +import org.wikipedia.compose.components.LineChart +import org.wikipedia.compose.components.WikiCard +import org.wikipedia.compose.components.error.WikiErrorClickEvents +import org.wikipedia.compose.components.error.WikiErrorView +import org.wikipedia.compose.theme.BaseTheme +import org.wikipedia.compose.theme.WikipediaTheme +import org.wikipedia.dataclient.WikiSite +import org.wikipedia.dataclient.growthtasks.GrowthUserImpact +import org.wikipedia.dataclient.growthtasks.GrowthUserImpact.ArticleViews +import org.wikipedia.page.PageTitle +import org.wikipedia.theme.Theme +import org.wikipedia.util.UiState +import org.wikipedia.views.imageservice.ImageService +import java.text.NumberFormat +import java.util.Locale + +@Composable +fun EditingInsightsModule( + modifier: Modifier = Modifier, + uiState: UiState, + onPageItemClick: (PageTitle) -> Unit, + onContributionClick: (() -> Unit), + onSuggestedEditsClick: (() -> Unit), + wikiErrorClickEvents: WikiErrorClickEvents? = null +) { + when (uiState) { + UiState.Loading -> { + Box( + modifier = modifier + .fillMaxWidth() + .height(200.dp) + ) { + CircularProgressIndicator( + modifier = Modifier + .align(Alignment.Center) + .padding(24.dp), + color = WikipediaTheme.colors.progressiveColor + ) + } + } + is UiState.Success -> { + MostViewedCard( + modifier = modifier + .fillMaxWidth(), + data = uiState.data.topViewedArticlesWithPageTitle, + onClick = { + onPageItemClick(it) + } + ) + ContributionCard( + modifier = modifier + .fillMaxWidth(), + lastEditRelativeTime = uiState.data.lastEditRelativeTime, + editsThisMonth = uiState.data.editsThisMonth, + editsLastMonth = uiState.data.editsLastMonth, + totalEdits = uiState.data.totalEditsCount, + onContributionClick = { + onContributionClick() + }, + onSuggestedEditsClick = { + onSuggestedEditsClick() + } + ) + } + + is UiState.Error -> { + Box( + modifier = modifier + .fillMaxSize(), + contentAlignment = Alignment.Center + ) { + WikiErrorView( + modifier = Modifier + .fillMaxWidth(), + caught = uiState.error, + errorClickEvents = wikiErrorClickEvents + ) + } + } + } +} + +@Composable +fun MostViewedCard( + modifier: Modifier = Modifier, + data: Map, + showSize: Int = 3, + onClick: (PageTitle) -> Unit +) { + if (data.isEmpty()) { + return + } + val formatter = remember { NumberFormat.getNumberInstance(Locale.getDefault()) } + WikiCard( + modifier = modifier, + colors = CardDefaults.cardColors( + containerColor = WikipediaTheme.colors.paperColor, + contentColor = WikipediaTheme.colors.paperColor + ), + elevation = 0.dp, + border = BorderStroke( + width = 1.dp, + color = WikipediaTheme.colors.borderColor + ) + ) { + Column { + Row( + modifier = Modifier.padding(16.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + Icon( + modifier = Modifier.size(16.dp), + painter = painterResource(R.drawable.outline_trending_up_24), + tint = WikipediaTheme.colors.primaryColor, + contentDescription = null + ) + Text( + text = stringResource(R.string.activity_tab_impact_most_viewed), + style = MaterialTheme.typography.labelMedium, + color = WikipediaTheme.colors.primaryColor + ) + } + + var index = 1 + data.forEach { (pageTitle, articleViews) -> + if (index > showSize) return@forEach + var iconResource = when (index++) { + 1 -> R.drawable.outline_looks_one_24 + 2 -> R.drawable.outline_looks_two_24 + 3 -> R.drawable.outline_looks_three_24 + else -> null + } + if (data.size <= 1) { + iconResource = null + } + Box( + modifier = Modifier + .fillMaxWidth() + .clickable(onClick = { onClick(pageTitle) }) + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + horizontalArrangement = Arrangement.spacedBy(16.dp) + ) { + iconResource?.let { + Icon( + modifier = Modifier.size(24.dp), + painter = painterResource(it), + tint = WikipediaTheme.colors.primaryColor, + contentDescription = null + ) + } + Column( + modifier = Modifier + .weight(1f) + ) { + HtmlText( + text = pageTitle.displayText, + style = MaterialTheme.typography.titleMedium.copy( + fontFamily = FontFamily.Serif + ), + color = WikipediaTheme.colors.primaryColor, + ) + pageTitle.description?.let { description -> + Text( + text = description, + style = MaterialTheme.typography.bodyMedium, + color = WikipediaTheme.colors.secondaryColor + ) + } + Row( + modifier = Modifier + .fillMaxWidth() + .padding(top = 8.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + LineChart( + map = articleViews.viewsByDay, + modifier = Modifier + .width(24.dp) + .height(6.dp), + chartSampleSize = 10, + strokeWidth = 1.dp, + strokeColor = WikipediaTheme.colors.progressiveColor + ) + Text( + modifier = Modifier + .weight(1f) + .padding(start = 8.dp), + text = formatter.format(articleViews.viewsCount), + style = MaterialTheme.typography.labelSmall, + color = WikipediaTheme.colors.progressiveColor + ) + } + } + if (pageTitle.thumbUrl != null) { + val request = + ImageService.getRequest( + LocalContext.current, + url = pageTitle.thumbUrl + ) + AsyncImage( + model = request, + placeholder = BrushPainter(SolidColor(WikipediaTheme.colors.borderColor)), + error = BrushPainter(SolidColor(WikipediaTheme.colors.borderColor)), + contentScale = ContentScale.Crop, + contentDescription = null, + modifier = Modifier + .size(56.dp) + .clip(RoundedCornerShape(8.dp)) + ) + } + } + } + if (index < data.size - 1) { + HorizontalDivider( + color = WikipediaTheme.colors.borderColor + ) + } + } + } + } +} + +@Composable +fun ContributionCard( + modifier: Modifier = Modifier, + lastEditRelativeTime: String, + editsThisMonth: Int, + editsLastMonth: Int, + totalEdits: Int = 0, + onContributionClick: (() -> Unit)? = null, + onSuggestedEditsClick: (() -> Unit)? = null +) { + WikiCard( + modifier = modifier + .clickable(onClick = { onContributionClick?.invoke() }), + colors = CardDefaults.cardColors( + containerColor = WikipediaTheme.colors.paperColor, + contentColor = WikipediaTheme.colors.paperColor + ), + elevation = 0.dp, + border = BorderStroke( + width = 1.dp, + color = WikipediaTheme.colors.borderColor + ) + ) { + Column { + Row( + modifier = Modifier.fillMaxWidth() + .padding(top = 16.dp, start = 16.dp, end = 16.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + Icon( + modifier = Modifier.size(16.dp), + painter = painterResource(R.drawable.ic_icon_user_contributions_ooui), + tint = WikipediaTheme.colors.primaryColor, + contentDescription = null + ) + Text( + modifier = Modifier.weight(1f), + text = stringResource(R.string.activity_tab_impact_contributions_this_month), + style = MaterialTheme.typography.labelMedium, + color = WikipediaTheme.colors.primaryColor + ) + Icon( + modifier = Modifier.size(24.dp), + painter = painterResource(R.drawable.ic_chevron_forward_white_24dp), + tint = WikipediaTheme.colors.secondaryColor, + contentDescription = null + ) + } + + Text( + text = lastEditRelativeTime, + modifier = Modifier.padding(start = 16.dp), + style = MaterialTheme.typography.bodySmall, + color = WikipediaTheme.colors.secondaryColor + ) + + Column( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 16.dp) + ) { + val maxEditsCount = maxOf(editsThisMonth, editsLastMonth).toFloat() + + ContributionEditsView( + modifier = Modifier + .fillMaxWidth() + .padding(top = 8.dp), + text = pluralStringResource( + R.plurals.activity_tab_impact_edits_this_month, + editsThisMonth + ).lowercase(), + edits = editsThisMonth, + maxEdits = maxEditsCount, + barColor = WikipediaTheme.colors.successColor + ) + + ContributionEditsView( + modifier = Modifier + .fillMaxWidth() + .padding(top = 8.dp), + text = pluralStringResource( + R.plurals.activity_tab_impact_edits_last_month, + editsLastMonth + ).lowercase(), + edits = editsLastMonth, + maxEdits = maxEditsCount, + barColor = WikipediaTheme.colors.borderColor + ) + } + if (totalEdits == 0) { + HorizontalDivider( + modifier = Modifier.padding(top = 16.dp), + color = WikipediaTheme.colors.borderColor + ) + SuggestedEditsView( + onClick = onSuggestedEditsClick + ) + } + } + } +} + +@Composable +fun SuggestedEditsView( + onClick: (() -> Unit)? = null +) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Text( + modifier = Modifier.padding(bottom = 8.dp), + text = stringResource(R.string.activity_tab_impact_suggested_edits_title), + style = MaterialTheme.typography.titleSmall.copy( + fontWeight = FontWeight.Medium + ), + color = WikipediaTheme.colors.primaryColor + ) + Text( + textAlign = TextAlign.Center, + text = stringResource(R.string.activity_tab_impact_suggested_edits_message), + style = MaterialTheme.typography.bodyMedium, + color = WikipediaTheme.colors.primaryColor + ) + Button( + modifier = Modifier.padding(top = 16.dp).align(Alignment.CenterHorizontally), + contentPadding = PaddingValues(horizontal = 18.dp), + colors = ButtonDefaults.buttonColors( + containerColor = WikipediaTheme.colors.progressiveColor, + contentColor = WikipediaTheme.colors.paperColor, + ), + onClick = { onClick?.invoke() }, + ) { + Icon( + modifier = Modifier.size(20.dp), + painter = painterResource(R.drawable.ic_mode_edit_white_24dp), + tint = WikipediaTheme.colors.paperColor, + contentDescription = null + ) + Text( + modifier = Modifier.padding(start = 6.dp, top = 4.dp, bottom = 4.dp), + text = stringResource(R.string.activity_tab_impact_suggested_edits_button), + style = MaterialTheme.typography.labelLarge + ) + } + } +} + +@Composable +fun ContributionEditsView( + modifier: Modifier, + text: String, + edits: Int, + maxEdits: Float, + barColor: Color +) { + Row( + modifier = modifier, + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.Bottom + ) { + Text( + text = edits.toString(), + modifier = Modifier.padding(start = 16.dp).align(Alignment.Bottom), + style = MaterialTheme.typography.titleLarge, + fontWeight = FontWeight.Medium, + color = WikipediaTheme.colors.primaryColor + ) + Text( + text = text, + style = MaterialTheme.typography.bodySmall.copy( + fontWeight = FontWeight.Medium, + lineHeight = 22.sp + ), + color = WikipediaTheme.colors.secondaryColor + ) + } + + if (edits > 0) { + Box( + modifier = Modifier + .fillMaxWidth(edits.toFloat() / maxEdits) + .padding(horizontal = 16.dp) + .height(20.dp) + .background( + color = barColor, + shape = RoundedCornerShape(16.dp) + ) + ) + } +} + +@Preview +@Composable +private fun MostViewedCardPreview() { + BaseTheme( + currentTheme = Theme.LIGHT + ) { + val pageTitle = PageTitle( + text = "Test Article", + displayText = "Test Article", + wiki = WikiSite("en.wikipedia.org".toUri(), "en"), + description = "This is a test article", + thumbUrl = "https://example.com/thumb.jpg" + ) + val articleViews = ArticleViews( + firstEditDate = "2023-01-01", + newestEdit = "2023-10-01", + imageUrl = "https://example.com/image.jpg", + viewsCount = 1000 + ) + MostViewedCard( + data = mapOf( + pageTitle to articleViews + ), + onClick = { }, + ) + } +} + +@Preview +@Composable +private fun ContributionCardPreview() { + BaseTheme( + currentTheme = Theme.LIGHT + ) { + ContributionCard( + modifier = Modifier.fillMaxWidth(), + lastEditRelativeTime = "2 days ago", + totalEdits = 9, + editsThisMonth = 9, + editsLastMonth = 2, + onContributionClick = null, + onSuggestedEditsClick = null + ) + } +} diff --git a/app/src/main/java/org/wikipedia/activitytab/ImpactModule.kt b/app/src/main/java/org/wikipedia/activitytab/ImpactModule.kt index 2a1f1760f13..16a42584c28 100644 --- a/app/src/main/java/org/wikipedia/activitytab/ImpactModule.kt +++ b/app/src/main/java/org/wikipedia/activitytab/ImpactModule.kt @@ -2,21 +2,16 @@ package org.wikipedia.activitytab import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.background -import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row 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.layout.size -import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.Button -import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.CardDefaults import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.HorizontalDivider @@ -27,39 +22,24 @@ 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 -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.SolidColor -import androidx.compose.ui.graphics.painter.BrushPainter -import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.pluralStringResource import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import androidx.core.net.toUri -import coil3.compose.AsyncImage import org.wikipedia.R -import org.wikipedia.compose.components.HtmlText import org.wikipedia.compose.components.LineChart import org.wikipedia.compose.components.WikiCard import org.wikipedia.compose.components.error.WikiErrorClickEvents import org.wikipedia.compose.components.error.WikiErrorView import org.wikipedia.compose.theme.BaseTheme import org.wikipedia.compose.theme.WikipediaTheme -import org.wikipedia.dataclient.WikiSite import org.wikipedia.dataclient.growthtasks.GrowthUserImpact -import org.wikipedia.dataclient.growthtasks.GrowthUserImpact.ArticleViews -import org.wikipedia.page.PageTitle import org.wikipedia.theme.Theme import org.wikipedia.util.DateUtil import org.wikipedia.util.UiState -import org.wikipedia.views.imageservice.ImageService import java.text.NumberFormat import java.util.Date import java.util.Locale @@ -68,9 +48,6 @@ import java.util.Locale fun ImpactModule( modifier: Modifier = Modifier, uiState: UiState, - onPageItemClick: (PageTitle) -> Unit, - onContributionClick: (() -> Unit), - onSuggestedEditsClick: (() -> Unit), wikiErrorClickEvents: WikiErrorClickEvents? = null ) { when (uiState) { @@ -89,24 +66,6 @@ fun ImpactModule( } } is UiState.Success -> { - MostViewedCard( - modifier = modifier - .fillMaxWidth(), - data = uiState.data.topViewedArticlesWithPageTitle, - onClick = { - onPageItemClick(it) - } - ) - ContributionCard( - modifier = modifier - .fillMaxWidth(), - lastEditRelativeTime = uiState.data.lastEditRelativeTime, - editsThisMonth = uiState.data.editsThisMonth, - editsLastMonth = uiState.data.editsLastMonth, - onClick = { - onContributionClick() - } - ) AllTimeImpactCard( modifier = modifier .fillMaxWidth(), @@ -116,10 +75,7 @@ fun ImpactModule( lastEditTimestamp = uiState.data.lastEditTimestamp, lastThirtyDaysEdits = uiState.data.lastThirtyDaysEdits, totalPageviewsCount = uiState.data.totalPageviewsCount, - dailyTotalViews = uiState.data.dailyTotalViews, - onClick = { - onSuggestedEditsClick() - } + dailyTotalViews = uiState.data.dailyTotalViews ) } @@ -140,241 +96,6 @@ fun ImpactModule( } } -@Composable -fun MostViewedCard( - modifier: Modifier = Modifier, - data: Map, - showSize: Int = 3, - onClick: (PageTitle) -> Unit -) { - if (data.isEmpty()) { - return - } - val formatter = remember { NumberFormat.getNumberInstance(Locale.getDefault()) } - WikiCard( - modifier = modifier, - colors = CardDefaults.cardColors( - containerColor = WikipediaTheme.colors.paperColor, - contentColor = WikipediaTheme.colors.paperColor - ), - elevation = 0.dp, - border = BorderStroke( - width = 1.dp, - color = WikipediaTheme.colors.borderColor - ) - ) { - Column { - Row( - modifier = Modifier.padding(16.dp), - horizontalArrangement = Arrangement.spacedBy(8.dp) - ) { - Icon( - modifier = Modifier.size(16.dp), - painter = painterResource(R.drawable.outline_trending_up_24), - tint = WikipediaTheme.colors.primaryColor, - contentDescription = null - ) - Text( - text = stringResource(R.string.activity_tab_impact_most_viewed), - style = MaterialTheme.typography.labelMedium, - color = WikipediaTheme.colors.primaryColor - ) - } - - var index = 1 - data.forEach { (pageTitle, articleViews) -> - if (index > showSize) return@forEach - var iconResource = when (index++) { - 1 -> R.drawable.outline_looks_one_24 - 2 -> R.drawable.outline_looks_two_24 - 3 -> R.drawable.outline_looks_three_24 - else -> null - } - if (data.size <= 1) { - iconResource = null - } - Box( - modifier = Modifier - .fillMaxWidth() - .clickable(onClick = { onClick(pageTitle) }) - ) { - Row( - modifier = Modifier - .fillMaxWidth() - .padding(16.dp), - horizontalArrangement = Arrangement.spacedBy(16.dp) - ) { - iconResource?.let { - Icon( - modifier = Modifier.size(24.dp), - painter = painterResource(it), - tint = WikipediaTheme.colors.primaryColor, - contentDescription = null - ) - } - Column( - modifier = Modifier - .weight(1f) - ) { - HtmlText( - text = pageTitle.displayText, - style = MaterialTheme.typography.titleMedium.copy( - fontFamily = FontFamily.Serif - ), - color = WikipediaTheme.colors.primaryColor, - ) - pageTitle.description?.let { description -> - Text( - text = description, - style = MaterialTheme.typography.bodyMedium, - color = WikipediaTheme.colors.secondaryColor - ) - } - Row( - modifier = Modifier - .fillMaxWidth() - .padding(top = 8.dp), - verticalAlignment = Alignment.CenterVertically, - ) { - LineChart( - map = articleViews.viewsByDay, - modifier = Modifier - .width(24.dp) - .height(6.dp), - chartSampleSize = 10, - strokeWidth = 1.dp, - strokeColor = WikipediaTheme.colors.progressiveColor - ) - Text( - modifier = Modifier - .weight(1f) - .padding(start = 8.dp), - text = formatter.format(articleViews.viewsCount), - style = MaterialTheme.typography.labelSmall, - color = WikipediaTheme.colors.progressiveColor - ) - } - } - if (pageTitle.thumbUrl != null) { - val request = - ImageService.getRequest( - LocalContext.current, - url = pageTitle.thumbUrl - ) - AsyncImage( - model = request, - placeholder = BrushPainter(SolidColor(WikipediaTheme.colors.borderColor)), - error = BrushPainter(SolidColor(WikipediaTheme.colors.borderColor)), - contentScale = ContentScale.Crop, - contentDescription = null, - modifier = Modifier - .size(56.dp) - .clip(RoundedCornerShape(8.dp)) - ) - } - } - } - if (index < data.size - 1) { - HorizontalDivider( - color = WikipediaTheme.colors.borderColor - ) - } - } - } - } -} - -@Composable -fun ContributionCard( - modifier: Modifier = Modifier, - lastEditRelativeTime: String, - editsThisMonth: Int, - editsLastMonth: Int, - onClick: (() -> Unit)? = null -) { - WikiCard( - modifier = modifier - .clickable(onClick = { onClick?.invoke() }), - colors = CardDefaults.cardColors( - containerColor = WikipediaTheme.colors.paperColor, - contentColor = WikipediaTheme.colors.paperColor - ), - elevation = 0.dp, - border = BorderStroke( - width = 1.dp, - color = WikipediaTheme.colors.borderColor - ) - ) { - Column { - Row( - modifier = Modifier.fillMaxWidth() - .padding(top = 16.dp, start = 16.dp, end = 16.dp), - horizontalArrangement = Arrangement.spacedBy(8.dp) - ) { - Icon( - modifier = Modifier.size(16.dp), - painter = painterResource(R.drawable.ic_icon_user_contributions_ooui), - tint = WikipediaTheme.colors.primaryColor, - contentDescription = null - ) - Text( - modifier = Modifier.weight(1f), - text = stringResource(R.string.activity_tab_impact_contributions_this_month), - style = MaterialTheme.typography.labelMedium, - color = WikipediaTheme.colors.primaryColor - ) - Icon( - modifier = Modifier.size(24.dp), - painter = painterResource(R.drawable.ic_chevron_forward_white_24dp), - tint = WikipediaTheme.colors.secondaryColor, - contentDescription = null - ) - } - - Text( - text = lastEditRelativeTime, - modifier = Modifier.padding(start = 16.dp), - style = MaterialTheme.typography.bodySmall, - color = WikipediaTheme.colors.secondaryColor - ) - - Column( - modifier = Modifier - .fillMaxWidth() - .padding(vertical = 16.dp) - ) { - val maxEditsCount = maxOf(editsThisMonth, editsLastMonth).toFloat() - - ContributionEditsView( - modifier = Modifier - .fillMaxWidth() - .padding(top = 8.dp), - text = pluralStringResource( - R.plurals.activity_tab_impact_edits_this_month, - editsThisMonth - ).lowercase(), - edits = editsThisMonth, - maxEdits = maxEditsCount, - barColor = WikipediaTheme.colors.successColor - ) - - ContributionEditsView( - modifier = Modifier - .fillMaxWidth() - .padding(top = 8.dp), - text = pluralStringResource( - R.plurals.activity_tab_impact_edits_last_month, - editsLastMonth - ).lowercase(), - edits = editsLastMonth, - maxEdits = maxEditsCount, - barColor = WikipediaTheme.colors.borderColor - ) - } - } - } -} - @Composable fun AllTimeImpactCard( modifier: Modifier = Modifier, @@ -384,8 +105,7 @@ fun AllTimeImpactCard( lastEditTimestamp: Long = 0, lastThirtyDaysEdits: Map = emptyMap(), totalPageviewsCount: Long = 0, - dailyTotalViews: Map = emptyMap(), - onClick: (() -> Unit)? = null + dailyTotalViews: Map = emptyMap() ) { val formatter = remember { NumberFormat.getNumberInstance(Locale.getDefault()) } WikiCard( @@ -460,14 +180,12 @@ fun AllTimeImpactCard( ) } - HorizontalDivider( - modifier = Modifier.padding(top = 16.dp), - color = WikipediaTheme.colors.borderColor - ) + if (totalEdits > 0) { + HorizontalDivider( + modifier = Modifier.padding(top = 16.dp), + color = WikipediaTheme.colors.borderColor + ) - if (totalEdits == 0) { - ImpactSuggestedEditsView(onClick = onClick) - } else { Column( modifier = Modifier.padding(top = 16.dp) ) { @@ -593,98 +311,6 @@ fun AllTimeImpactCard( } } -@Composable -fun ImpactSuggestedEditsView( - onClick: (() -> Unit)? = null -) { - Column( - modifier = Modifier - .fillMaxWidth() - .padding(16.dp), - horizontalAlignment = Alignment.CenterHorizontally, - ) { - Text( - modifier = Modifier.padding(bottom = 8.dp), - text = stringResource(R.string.activity_tab_impact_suggested_edits_title), - style = MaterialTheme.typography.titleSmall.copy( - fontWeight = FontWeight.Medium - ), - color = WikipediaTheme.colors.primaryColor - ) - Text( - textAlign = TextAlign.Center, - text = stringResource(R.string.activity_tab_impact_suggested_edits_message), - style = MaterialTheme.typography.bodyMedium, - color = WikipediaTheme.colors.primaryColor - ) - Button( - modifier = Modifier.padding(top = 16.dp).align(Alignment.CenterHorizontally), - contentPadding = PaddingValues(horizontal = 18.dp), - colors = ButtonDefaults.buttonColors( - containerColor = WikipediaTheme.colors.progressiveColor, - contentColor = WikipediaTheme.colors.paperColor, - ), - onClick = { onClick?.invoke() }, - ) { - Icon( - modifier = Modifier.size(20.dp), - painter = painterResource(R.drawable.ic_mode_edit_white_24dp), - tint = WikipediaTheme.colors.paperColor, - contentDescription = null - ) - Text( - modifier = Modifier.padding(start = 6.dp, top = 4.dp, bottom = 4.dp), - text = stringResource(R.string.activity_tab_impact_suggested_edits_button), - style = MaterialTheme.typography.labelLarge - ) - } - } -} - -@Composable -fun ContributionEditsView( - modifier: Modifier, - text: String, - edits: Int, - maxEdits: Float, - barColor: Color -) { - Row( - modifier = modifier, - horizontalArrangement = Arrangement.spacedBy(8.dp), - verticalAlignment = Alignment.Bottom - ) { - Text( - text = edits.toString(), - modifier = Modifier.padding(start = 16.dp).align(Alignment.Bottom), - style = MaterialTheme.typography.titleLarge, - fontWeight = FontWeight.Medium, - color = WikipediaTheme.colors.primaryColor - ) - Text( - text = text, - style = MaterialTheme.typography.bodySmall.copy( - fontWeight = FontWeight.Medium, - lineHeight = 22.sp - ), - color = WikipediaTheme.colors.secondaryColor - ) - } - - if (edits > 0) { - Box( - modifier = Modifier - .fillMaxWidth(edits.toFloat() / maxEdits) - .padding(horizontal = 16.dp) - .height(20.dp) - .background( - color = barColor, - shape = RoundedCornerShape(16.dp) - ) - ) - } -} - @Composable fun ImpactStatView( modifier: Modifier, @@ -720,50 +346,6 @@ fun ImpactStatView( } } -@Preview -@Composable -private fun MostViewedCardPreview() { - BaseTheme( - currentTheme = Theme.LIGHT - ) { - val pageTitle = PageTitle( - text = "Test Article", - displayText = "Test Article", - wiki = WikiSite("en.wikipedia.org".toUri(), "en"), - description = "This is a test article", - thumbUrl = "https://example.com/thumb.jpg" - ) - val articleViews = ArticleViews( - firstEditDate = "2023-01-01", - newestEdit = "2023-10-01", - imageUrl = "https://example.com/image.jpg", - viewsCount = 1000 - ) - MostViewedCard( - data = mapOf( - pageTitle to articleViews - ), - onClick = { }, - ) - } -} - -@Preview -@Composable -private fun ContributionCardPreview() { - BaseTheme( - currentTheme = Theme.LIGHT - ) { - ContributionCard( - modifier = Modifier.fillMaxWidth(), - lastEditRelativeTime = "2 days ago", - editsThisMonth = 9, - editsLastMonth = 2, - onClick = {} - ) - } -} - @Preview @Composable private fun AllTimeImpactCardPreview() { @@ -817,8 +399,7 @@ private fun AllTimeImpactCardPreview() { "2023-10-04" to 120, "2023-10-05" to 180, "2023-10-06" to 220 - ), - onClick = {} + ) ) } } From 98d7879d8153d4d4ab0780266f811b5d18057b78 Mon Sep 17 00:00:00 2001 From: Dmitry Brant Date: Wed, 27 Aug 2025 09:43:29 -0400 Subject: [PATCH 50/70] Hide Impact title in the correct cases. --- .../activitytab/ActivityTabFragment.kt | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/org/wikipedia/activitytab/ActivityTabFragment.kt b/app/src/main/java/org/wikipedia/activitytab/ActivityTabFragment.kt index 47791431295..5bb0a03b194 100644 --- a/app/src/main/java/org/wikipedia/activitytab/ActivityTabFragment.kt +++ b/app/src/main/java/org/wikipedia/activitytab/ActivityTabFragment.kt @@ -324,13 +324,15 @@ class ActivityTabFragment : Fragment() { ) ) ) { - Text( - modifier = Modifier.padding(start = 16.dp, end = 16.dp, top = 24.dp), - text = stringResource(R.string.activity_tab_impact), - style = MaterialTheme.typography.titleLarge, - fontWeight = FontWeight.Medium, - color = WikipediaTheme.colors.primaryColor - ) + if (modules.isModuleEnabled(ModuleType.EDITING_INSIGHTS) || modules.isModuleEnabled(ModuleType.IMPACT)) { + Text( + modifier = Modifier.padding(start = 16.dp, end = 16.dp, top = 24.dp), + text = stringResource(R.string.activity_tab_impact), + style = MaterialTheme.typography.titleLarge, + fontWeight = FontWeight.Medium, + color = WikipediaTheme.colors.primaryColor + ) + } if (modules.isModuleEnabled(ModuleType.EDITING_INSIGHTS)) { EditingInsightsModule( @@ -439,7 +441,7 @@ class ActivityTabFragment : Fragment() { ) } - if (modules.isModuleEnabled(ModuleType.DONATIONS) || modules.isModuleEnabled(ModuleType.GAMES) || modules.isModuleEnabled(ModuleType.IMPACT)) { + if (modules.isModuleEnabled(ModuleType.DONATIONS) || modules.isModuleEnabled(ModuleType.GAMES) || modules.isModuleEnabled(ModuleType.EDITING_INSIGHTS) || modules.isModuleEnabled(ModuleType.IMPACT)) { // Add bottom padding only if at least one of the modules in this gradient box is enabled. Spacer(modifier = Modifier.size(16.dp)) } From 979551912c763b8e1f06a3814f3ef4dca3aadf98 Mon Sep 17 00:00:00 2001 From: Dmitry Brant Date: Wed, 27 Aug 2025 10:04:24 -0400 Subject: [PATCH 51/70] Add overflow menu item for clearing history. --- .../activitytab/ActivityTabFragment.kt | 7 +++++ .../org/wikipedia/history/HistoryFragment.kt | 29 +++++++++++++++---- .../org/wikipedia/history/HistoryViewModel.kt | 5 +--- .../res/menu/menu_activity_tab_overflow.xml | 5 ++++ app/src/main/res/values/strings.xml | 2 +- 5 files changed, 38 insertions(+), 10 deletions(-) diff --git a/app/src/main/java/org/wikipedia/activitytab/ActivityTabFragment.kt b/app/src/main/java/org/wikipedia/activitytab/ActivityTabFragment.kt index 5bb0a03b194..2515e790c8e 100644 --- a/app/src/main/java/org/wikipedia/activitytab/ActivityTabFragment.kt +++ b/app/src/main/java/org/wikipedia/activitytab/ActivityTabFragment.kt @@ -75,6 +75,7 @@ import org.wikipedia.events.LoggedOutInBackgroundEvent import org.wikipedia.games.onthisday.OnThisDayGameActivity import org.wikipedia.games.onthisday.OnThisDayGameViewModel import org.wikipedia.history.HistoryEntry +import org.wikipedia.history.HistoryFragment import org.wikipedia.login.LoginActivity import org.wikipedia.navtab.NavTab import org.wikipedia.page.PageActivity @@ -562,6 +563,12 @@ class ActivityTabFragment : Fragment() { startActivity(ActivityTabCustomizationActivity.newIntent(requireContext())) true } + R.id.menu_clear_history -> { + HistoryFragment.clearAllHistory(requireContext(), lifecycleScope) { + viewModel.loadAll() + } + true + } R.id.menu_learn_more -> { // TODO: MARK_ACTIVITY_TAB --> add mediawiki page link true diff --git a/app/src/main/java/org/wikipedia/history/HistoryFragment.kt b/app/src/main/java/org/wikipedia/history/HistoryFragment.kt index a49bcbb9985..4939e3aaf01 100644 --- a/app/src/main/java/org/wikipedia/history/HistoryFragment.kt +++ b/app/src/main/java/org/wikipedia/history/HistoryFragment.kt @@ -23,11 +23,15 @@ import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import com.google.android.material.dialog.MaterialAlertDialogBuilder +import kotlinx.coroutines.CoroutineExceptionHandler +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch import org.wikipedia.BackPressedHandler import org.wikipedia.Constants import org.wikipedia.R import org.wikipedia.WikipediaApp import org.wikipedia.activity.FragmentUtil +import org.wikipedia.database.AppDatabase import org.wikipedia.databinding.FragmentHistoryBinding import org.wikipedia.main.MainActivity import org.wikipedia.main.MainFragment @@ -273,11 +277,9 @@ class HistoryFragment : Fragment(), BackPressedHandler { } clearHistoryButton.setOnClickListener { if (selectedEntries.isEmpty()) { - MaterialAlertDialogBuilder(requireContext()) - .setTitle(R.string.dialog_title_clear_history) - .setMessage(R.string.dialog_message_clear_history) - .setPositiveButton(R.string.dialog_message_clear_history_yes) { _, _ -> viewModel.deleteAllHistoryItems() } - .setNegativeButton(R.string.dialog_message_clear_history_no, null).show() + clearAllHistory(requireContext(), lifecycleScope) { + viewModel.afterDeleteAllHistoryItems() + } } else { deleteSelectedPages() } @@ -445,6 +447,23 @@ class HistoryFragment : Fragment(), BackPressedHandler { private const val VIEW_TYPE_HEADER = 1 private const val VIEW_TYPE_ITEM = 2 + fun clearAllHistory(context: Context, coroutineScope: CoroutineScope, action: () -> Unit) { + MaterialAlertDialogBuilder(context) + .setTitle(R.string.dialog_title_clear_history) + .setMessage(R.string.dialog_message_clear_history) + .setPositiveButton(R.string.dialog_message_clear_history_yes) { _, _ -> + coroutineScope.launch( + CoroutineExceptionHandler { _, t -> L.e(t) } + ) { + AppDatabase.instance.historyEntryDao().deleteAll() + AppDatabase.instance.pageImagesDao().deleteAll() + AppDatabase.instance.categoryDao().deleteAll() + action() + } + } + .setNegativeButton(R.string.dialog_message_clear_history_no, null).show() + } + fun newInstance(): HistoryFragment { return HistoryFragment() } diff --git a/app/src/main/java/org/wikipedia/history/HistoryViewModel.kt b/app/src/main/java/org/wikipedia/history/HistoryViewModel.kt index 9111863d54f..85669bd5a54 100644 --- a/app/src/main/java/org/wikipedia/history/HistoryViewModel.kt +++ b/app/src/main/java/org/wikipedia/history/HistoryViewModel.kt @@ -51,11 +51,8 @@ class HistoryViewModel : ViewModel() { } } - fun deleteAllHistoryItems() { + fun afterDeleteAllHistoryItems() { viewModelScope.launch(handler) { - AppDatabase.instance.historyEntryDao().deleteAll() - AppDatabase.instance.pageImagesDao().deleteAll() - AppDatabase.instance.categoryDao().deleteAll() historyItems.postValue(Resource.Success(emptyList())) } } diff --git a/app/src/main/res/menu/menu_activity_tab_overflow.xml b/app/src/main/res/menu/menu_activity_tab_overflow.xml index e7dda08e6fe..3dd8f86eb2d 100644 --- a/app/src/main/res/menu/menu_activity_tab_overflow.xml +++ b/app/src/main/res/menu/menu_activity_tab_overflow.xml @@ -18,6 +18,11 @@ android:icon="@drawable/baseline_info_24" android:title="@string/activity_tab_menu_info" app:iconTint="?attr/secondary_color" /> + Clear browsing history - This will delete all of your browsing history, and close any currently open tabs. Are you sure? + This will delete all of your browsing history. Are you sure? Yes No Share via From b92b0f0f68ecdc817b2ac3a97137ed0efe8c78c4 Mon Sep 17 00:00:00 2001 From: Cooltey Feng Date: Thu, 28 Aug 2025 15:55:43 -0700 Subject: [PATCH 52/70] Update to use onClick for wikiCard --- .../main/java/org/wikipedia/activitytab/DonationModule.kt | 7 +++---- .../org/wikipedia/activitytab/EditingInsightsModule.kt | 6 +++--- .../main/java/org/wikipedia/activitytab/WikiGamesModule.kt | 6 +++--- 3 files changed, 9 insertions(+), 10 deletions(-) diff --git a/app/src/main/java/org/wikipedia/activitytab/DonationModule.kt b/app/src/main/java/org/wikipedia/activitytab/DonationModule.kt index cc5af199eb0..875668fd3b2 100644 --- a/app/src/main/java/org/wikipedia/activitytab/DonationModule.kt +++ b/app/src/main/java/org/wikipedia/activitytab/DonationModule.kt @@ -1,7 +1,6 @@ package org.wikipedia.activitytab import androidx.compose.foundation.BorderStroke -import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -37,13 +36,13 @@ fun DonationModule( onClick: (() -> Unit)? = null ) { WikiCard( - modifier = modifier - .clickable(onClick = { onClick?.invoke() }), + modifier = modifier, elevation = 0.dp, border = BorderStroke( width = 1.dp, color = WikipediaTheme.colors.borderColor - ) + ), + onClick = onClick ) { if (uiState == UiState.Loading) { Box( diff --git a/app/src/main/java/org/wikipedia/activitytab/EditingInsightsModule.kt b/app/src/main/java/org/wikipedia/activitytab/EditingInsightsModule.kt index 1cc9f301b42..aed1d8f981e 100644 --- a/app/src/main/java/org/wikipedia/activitytab/EditingInsightsModule.kt +++ b/app/src/main/java/org/wikipedia/activitytab/EditingInsightsModule.kt @@ -283,8 +283,7 @@ fun ContributionCard( onSuggestedEditsClick: (() -> Unit)? = null ) { WikiCard( - modifier = modifier - .clickable(onClick = { onContributionClick?.invoke() }), + modifier = modifier, colors = CardDefaults.cardColors( containerColor = WikipediaTheme.colors.paperColor, contentColor = WikipediaTheme.colors.paperColor @@ -293,7 +292,8 @@ fun ContributionCard( border = BorderStroke( width = 1.dp, color = WikipediaTheme.colors.borderColor - ) + ), + onClick = onContributionClick ) { Column { Row( diff --git a/app/src/main/java/org/wikipedia/activitytab/WikiGamesModule.kt b/app/src/main/java/org/wikipedia/activitytab/WikiGamesModule.kt index fbdc6eb9b06..3690f534d82 100644 --- a/app/src/main/java/org/wikipedia/activitytab/WikiGamesModule.kt +++ b/app/src/main/java/org/wikipedia/activitytab/WikiGamesModule.kt @@ -98,8 +98,7 @@ fun WikiGamesStatsCard( onClick: (() -> Unit)? = null ) { WikiCard( - modifier = modifier - .clickable(onClick = { onClick?.invoke() }), + modifier = modifier, colors = CardDefaults.cardColors( containerColor = WikipediaTheme.colors.paperColor, contentColor = WikipediaTheme.colors.paperColor @@ -108,7 +107,8 @@ fun WikiGamesStatsCard( border = BorderStroke( width = 1.dp, color = WikipediaTheme.colors.borderColor - ) + ), + onClick = onClick ) { Column( modifier = Modifier.padding(16.dp) From 8d0cb5fcb15fd4140302f69b26b8228dff27356e Mon Sep 17 00:00:00 2001 From: Cooltey Feng Date: Fri, 29 Aug 2025 07:27:10 -0700 Subject: [PATCH 53/70] Use WikiCard for ReadingHistoryModule cards and consolidate layouts (#5882) --- .../activitytab/EditingInsightsModule.kt | 48 +-- .../activitytab/ReadingHistoryModule.kt | 349 +++++++++++++----- .../activitytab/TopCategoriesView.kt | 106 ------ 3 files changed, 285 insertions(+), 218 deletions(-) delete mode 100644 app/src/main/java/org/wikipedia/activitytab/TopCategoriesView.kt diff --git a/app/src/main/java/org/wikipedia/activitytab/EditingInsightsModule.kt b/app/src/main/java/org/wikipedia/activitytab/EditingInsightsModule.kt index aed1d8f981e..e88465236b2 100644 --- a/app/src/main/java/org/wikipedia/activitytab/EditingInsightsModule.kt +++ b/app/src/main/java/org/wikipedia/activitytab/EditingInsightsModule.kt @@ -298,21 +298,34 @@ fun ContributionCard( Column { Row( modifier = Modifier.fillMaxWidth() - .padding(top = 16.dp, start = 16.dp, end = 16.dp), - horizontalArrangement = Arrangement.spacedBy(8.dp) + .padding(top = 16.dp, start = 16.dp, end = 16.dp) ) { - Icon( - modifier = Modifier.size(16.dp), - painter = painterResource(R.drawable.ic_icon_user_contributions_ooui), - tint = WikipediaTheme.colors.primaryColor, - contentDescription = null - ) - Text( - modifier = Modifier.weight(1f), - text = stringResource(R.string.activity_tab_impact_contributions_this_month), - style = MaterialTheme.typography.labelMedium, - color = WikipediaTheme.colors.primaryColor - ) + Column( + modifier = Modifier.weight(1f) + ) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + Icon( + modifier = Modifier.size(16.dp), + painter = painterResource(R.drawable.ic_icon_user_contributions_ooui), + tint = WikipediaTheme.colors.primaryColor, + contentDescription = null + ) + Text( + text = stringResource(R.string.activity_tab_impact_contributions_this_month), + style = MaterialTheme.typography.labelMedium, + color = WikipediaTheme.colors.primaryColor + ) + } + Text( + text = lastEditRelativeTime, + modifier = Modifier.padding(top = 4.dp), + style = MaterialTheme.typography.bodySmall, + color = WikipediaTheme.colors.secondaryColor + ) + } Icon( modifier = Modifier.size(24.dp), painter = painterResource(R.drawable.ic_chevron_forward_white_24dp), @@ -321,13 +334,6 @@ fun ContributionCard( ) } - Text( - text = lastEditRelativeTime, - modifier = Modifier.padding(start = 16.dp), - style = MaterialTheme.typography.bodySmall, - color = WikipediaTheme.colors.secondaryColor - ) - Column( modifier = Modifier .fillMaxWidth() diff --git a/app/src/main/java/org/wikipedia/activitytab/ReadingHistoryModule.kt b/app/src/main/java/org/wikipedia/activitytab/ReadingHistoryModule.kt index 476619cb5a5..ee9b8ebcc83 100644 --- a/app/src/main/java/org/wikipedia/activitytab/ReadingHistoryModule.kt +++ b/app/src/main/java/org/wikipedia/activitytab/ReadingHistoryModule.kt @@ -19,9 +19,8 @@ import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults -import androidx.compose.material3.Card -import androidx.compose.material3.CardDefaults import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text @@ -40,18 +39,26 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.TextUnit import androidx.compose.ui.unit.TextUnitType import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import androidx.core.net.toUri import coil3.compose.AsyncImage import org.wikipedia.R import org.wikipedia.categories.db.Category import org.wikipedia.compose.ComposeColors import org.wikipedia.compose.components.TinyBarChart +import org.wikipedia.compose.components.WikiCard import org.wikipedia.compose.components.error.WikiErrorClickEvents import org.wikipedia.compose.components.error.WikiErrorView +import org.wikipedia.compose.theme.BaseTheme import org.wikipedia.compose.theme.WikipediaTheme +import org.wikipedia.dataclient.WikiSite +import org.wikipedia.page.PageTitle +import org.wikipedia.theme.Theme +import org.wikipedia.util.StringUtil import org.wikipedia.util.UiState import org.wikipedia.views.imageservice.ImageService import java.time.LocalDate @@ -144,30 +151,110 @@ fun ReadingHistoryModule( return } - Card( - modifier = Modifier.fillMaxWidth().padding(start = 16.dp, end = 16.dp, top = 16.dp) - .clickable { - onArticlesReadClick() + ArticleReadThisMonthCard( + modifier = modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 16.dp), + readingHistory = readingHistory, + todayDate = todayDate, + onClick = { + onArticlesReadClick() + } + ) + + ArticleSavedThisMonthCard( + modifier = modifier + .fillMaxWidth() + .padding(horizontal = 16.dp) + .padding(bottom = 16.dp), + readingHistory = readingHistory, + todayDate = todayDate, + onClick = { + onArticlesSavedClick() + } + ) + + if (readingHistory.topCategories.isNotEmpty()) { + TopCategoriesCard( + modifier = Modifier + .fillMaxWidth() + .padding(start = 16.dp, end = 16.dp, bottom = 16.dp), + categories = readingHistory.topCategories, + onClick = { + onCategoryItemClick(it) + } + ) + } + + if (readingHistory.articlesReadThisMonth == 0) { + Text( + text = stringResource(R.string.activity_tab_discover_encourage), + modifier = modifier.padding(horizontal = 16.dp, vertical = 8.dp), + fontWeight = FontWeight.Medium, + fontSize = 14.sp, + color = WikipediaTheme.colors.primaryColor + ) + Button( + modifier = modifier.padding(top = 8.dp, bottom = 16.dp), + contentPadding = PaddingValues(horizontal = 18.dp), + colors = ButtonDefaults.buttonColors( + containerColor = WikipediaTheme.colors.progressiveColor, + contentColor = WikipediaTheme.colors.paperColor, + ), + onClick = { + onExploreClick() }, - elevation = CardDefaults.cardElevation(defaultElevation = 0.dp), - colors = CardDefaults.cardColors( - containerColor = WikipediaTheme.colors.paperColor - ), - border = BorderStroke( - width = 1.dp, - color = WikipediaTheme.colors.borderColor - ), - shape = RoundedCornerShape(12.dp) + ) { + Text( + text = stringResource(R.string.activity_tab_explore_wikipedia), + style = MaterialTheme.typography.labelLarge + ) + } + } + } else if (readingHistoryState is UiState.Error) { + Box( + modifier = modifier + .fillMaxSize() + .padding(horizontal = 16.dp, vertical = 16.dp), + contentAlignment = Alignment.Center + ) { + WikiErrorView( + modifier = Modifier + .fillMaxWidth(), + caught = readingHistoryState.error, + errorClickEvents = wikiErrorClickEvents + ) + } + } +} + +@Composable +private fun ArticleReadThisMonthCard( + modifier: Modifier, + readingHistory: ActivityTabViewModel.ReadingHistory, + todayDate: LocalDate, + onClick: () -> Unit +) { + WikiCard( + modifier = modifier, + elevation = 0.dp, + border = BorderStroke( + width = 1.dp, + color = WikipediaTheme.colors.borderColor + ), + onClick = onClick + ) { + Column( + modifier = Modifier.padding(16.dp) ) { Row( - modifier = modifier.fillMaxWidth() - .padding(top = 16.dp, start = 16.dp, end = 16.dp) + modifier = Modifier.fillMaxWidth() ) { Column( - modifier = modifier.weight(1f) + modifier = Modifier.weight(1f) ) { Row( - modifier = modifier.fillMaxWidth(), + modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(8.dp) ) { Icon( @@ -219,11 +306,11 @@ fun ReadingHistoryModule( } Row( - modifier = modifier.fillMaxWidth().padding(top = 6.dp, bottom = 16.dp) + modifier = Modifier.fillMaxWidth().padding(top = 6.dp) ) { Text( text = readingHistory.articlesReadThisMonth.toString(), - modifier = Modifier.padding(start = 16.dp).align(Alignment.Bottom), + modifier = Modifier.align(Alignment.Bottom), style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.Medium, color = WikipediaTheme.colors.primaryColor @@ -232,7 +319,7 @@ fun ReadingHistoryModule( TinyBarChart( values = readingHistory.articlesReadByWeek, - modifier = Modifier.padding(end = 16.dp).size( + modifier = Modifier.size( 72.dp, if (readingHistory.articlesReadThisMonth == 0) 32.dp else 48.dp ), @@ -241,31 +328,36 @@ fun ReadingHistoryModule( ) } } + } +} - Card( - modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp, vertical = 16.dp) - .clickable { - onArticlesSavedClick() - }, - elevation = CardDefaults.cardElevation(defaultElevation = 0.dp), - colors = CardDefaults.cardColors( - containerColor = WikipediaTheme.colors.paperColor - ), - border = BorderStroke( - width = 1.dp, - color = WikipediaTheme.colors.borderColor - ), - shape = RoundedCornerShape(12.dp) +@Composable +private fun ArticleSavedThisMonthCard( + modifier: Modifier, + readingHistory: ActivityTabViewModel.ReadingHistory, + todayDate: LocalDate, + onClick: () -> Unit +) { + WikiCard( + modifier = modifier, + elevation = 0.dp, + border = BorderStroke( + width = 1.dp, + color = WikipediaTheme.colors.borderColor + ), + onClick = onClick + ) { + Column( + modifier = Modifier.padding(16.dp) ) { Row( - modifier = modifier.fillMaxWidth() - .padding(top = 16.dp, start = 16.dp, end = 16.dp) + modifier = Modifier.fillMaxWidth() ) { Column( - modifier = modifier.weight(1f) + modifier = Modifier.weight(1f) ) { Row( - modifier = modifier.fillMaxWidth(), + modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(8.dp) ) { Icon( @@ -316,19 +408,17 @@ fun ReadingHistoryModule( ) } Row( - modifier = modifier.fillMaxWidth().padding(top = 6.dp, bottom = 16.dp) + modifier = Modifier.fillMaxWidth().padding(top = 6.dp) ) { Text( text = readingHistory.articlesSavedThisMonth.toString(), - modifier = Modifier.padding(start = 16.dp).align(Alignment.Bottom), + modifier = Modifier.align(Alignment.Bottom), style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.Medium, color = WikipediaTheme.colors.primaryColor ) Spacer(modifier = Modifier.weight(1f)) - Row( - modifier = Modifier.padding(end = 16.dp) - ) { + Row { val itemsToShow = if (readingHistory.articlesSaved.size <= 4) readingHistory.articlesSaved.size else 3 val showOverflowItem = readingHistory.articlesSaved.size > 4 @@ -393,57 +483,134 @@ fun ReadingHistoryModule( } } } + } +} - if (readingHistory.topCategories.isNotEmpty()) { - TopCategoriesView( - modifier = Modifier - .fillMaxWidth() - .padding(start = 16.dp, end = 16.dp, bottom = 16.dp), - categories = readingHistory.topCategories, - onClick = { - onCategoryItemClick(it) - } - ) - } - - if (readingHistory.articlesReadThisMonth == 0) { - Text( - text = stringResource(R.string.activity_tab_discover_encourage), - modifier = modifier.padding(horizontal = 16.dp, vertical = 8.dp), - fontWeight = FontWeight.Medium, - fontSize = 14.sp, - color = WikipediaTheme.colors.primaryColor - ) - Button( - modifier = modifier.padding(top = 8.dp, bottom = 16.dp), - contentPadding = PaddingValues(horizontal = 18.dp), - colors = ButtonDefaults.buttonColors( - containerColor = WikipediaTheme.colors.progressiveColor, - contentColor = WikipediaTheme.colors.paperColor, - ), - onClick = { - onExploreClick() - }, +@Composable +fun TopCategoriesCard( + modifier: Modifier = Modifier, + categories: List, + onClick: (Category) -> Unit +) { + WikiCard( + modifier = modifier, + elevation = 0.dp, + border = BorderStroke( + width = 1.dp, + color = WikipediaTheme.colors.borderColor + ) + ) { + Column { + Row( + modifier = Modifier.padding(start = 16.dp, end = 16.dp, top = 16.dp, bottom = 8.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp) ) { + Icon( + modifier = Modifier.size(16.dp), + painter = painterResource(R.drawable.ic_category_black_24dp), + tint = WikipediaTheme.colors.primaryColor, + contentDescription = null + ) Text( - text = stringResource(R.string.activity_tab_explore_wikipedia), - style = MaterialTheme.typography.labelLarge + text = stringResource(R.string.activity_tab_monthly_top_categories), + style = MaterialTheme.typography.labelMedium, + color = WikipediaTheme.colors.primaryColor ) } + + categories.forEachIndexed { index, value -> + Box( + modifier = Modifier + .fillMaxWidth() + .clickable(onClick = { onClick(value) }) + ) { + Text( + modifier = Modifier + .padding(horizontal = 32.dp, vertical = 16.dp), + text = StringUtil.removeNamespace(value.title), + style = MaterialTheme.typography.bodyLarge, + color = WikipediaTheme.colors.primaryColor + ) + } + + if (index < categories.size - 1) { + HorizontalDivider( + modifier = Modifier.padding(horizontal = 16.dp), + color = WikipediaTheme.colors.borderColor + ) + } + } } - } else if (readingHistoryState is UiState.Error) { - Box( - modifier = modifier - .fillMaxSize() - .padding(horizontal = 16.dp, vertical = 16.dp), - contentAlignment = Alignment.Center - ) { - WikiErrorView( - modifier = Modifier - .fillMaxWidth(), - caught = readingHistoryState.error, - errorClickEvents = wikiErrorClickEvents - ) - } + } +} + +@Preview +@Composable +private fun ArticleReadThisMonthCardPreview() { + BaseTheme(currentTheme = Theme.LIGHT) { + ArticleReadThisMonthCard( + modifier = Modifier + .padding(20.dp), + readingHistory = ActivityTabViewModel.ReadingHistory( + articlesReadThisMonth = 42, + articlesSavedThisMonth = 8, + timeSpentThisWeek = 3661, + lastArticleReadTime = LocalDate.now().atStartOfDay(), + lastArticleSavedTime = LocalDate.now().atStartOfDay(), + articlesReadByWeek = listOf(5, 10, 8, 12), + articlesSaved = listOf(), + topCategories = listOf() + ), + todayDate = LocalDate.now(), + onClick = {} + ) + } +} + +@Preview +@Composable +private fun ArticleSavedThisMonthCardPreview() { + val wikiSite = WikiSite("en.wikipedia.org".toUri(), "en") + BaseTheme(currentTheme = Theme.LIGHT) { + ArticleSavedThisMonthCard( + modifier = Modifier + .padding(20.dp), + readingHistory = ActivityTabViewModel.ReadingHistory( + articlesReadThisMonth = 42, + articlesSavedThisMonth = 8, + timeSpentThisWeek = 3661, + lastArticleReadTime = LocalDate.now().atStartOfDay(), + lastArticleSavedTime = LocalDate.now().atStartOfDay(), + articlesReadByWeek = listOf(5, 10, 8, 12), + articlesSaved = listOf( + PageTitle("Title1", wikiSite), + PageTitle("Title2", wikiSite), + PageTitle("Title3", wikiSite), + ), + topCategories = listOf() + ), + todayDate = LocalDate.now(), + onClick = {} + ) + } +} + +@Preview +@Composable +private fun TopCategoriesViewPreview() { + BaseTheme( + currentTheme = Theme.LIGHT + ) { + TopCategoriesCard( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + categories = listOf( + Category(2025, 1, "Category:Ancient history", "en", 1), + Category(2025, 1, "Category:World literature", "en", 1), + Category(2025, 1, "Category:Cat breeds originating in the United States", "en", 1), + ), + onClick = {} + ) } } diff --git a/app/src/main/java/org/wikipedia/activitytab/TopCategoriesView.kt b/app/src/main/java/org/wikipedia/activitytab/TopCategoriesView.kt deleted file mode 100644 index 830304894e4..00000000000 --- a/app/src/main/java/org/wikipedia/activitytab/TopCategoriesView.kt +++ /dev/null @@ -1,106 +0,0 @@ -package org.wikipedia.activitytab - -import androidx.compose.foundation.BorderStroke -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.material3.HorizontalDivider -import androidx.compose.material3.Icon -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import org.wikipedia.R -import org.wikipedia.categories.db.Category -import org.wikipedia.compose.components.WikiCard -import org.wikipedia.compose.theme.BaseTheme -import org.wikipedia.compose.theme.WikipediaTheme -import org.wikipedia.theme.Theme -import org.wikipedia.util.StringUtil - -@Composable -fun TopCategoriesView( - modifier: Modifier = Modifier, - categories: List, - onClick: (Category) -> Unit -) { - WikiCard( - modifier = modifier, - elevation = 0.dp, - border = BorderStroke( - width = 1.dp, - color = WikipediaTheme.colors.borderColor - ) - ) { - Column { - Row( - modifier = Modifier.padding(start = 16.dp, end = 16.dp, top = 16.dp, bottom = 8.dp), - horizontalArrangement = Arrangement.spacedBy(8.dp) - ) { - Icon( - modifier = Modifier.size(16.dp), - painter = painterResource(R.drawable.ic_category_black_24dp), - tint = WikipediaTheme.colors.primaryColor, - contentDescription = null - ) - Text( - text = stringResource(R.string.activity_tab_monthly_top_categories), - style = MaterialTheme.typography.labelMedium, - color = WikipediaTheme.colors.primaryColor - ) - } - - categories.forEachIndexed { index, value -> - Box( - modifier = Modifier - .fillMaxWidth() - .clickable(onClick = { onClick(value) }) - ) { - Text( - modifier = Modifier - .padding(horizontal = 32.dp, vertical = 16.dp), - text = StringUtil.removeNamespace(value.title), - style = MaterialTheme.typography.bodyLarge, - color = WikipediaTheme.colors.primaryColor - ) - } - - if (index < categories.size - 1) { - HorizontalDivider( - modifier = Modifier.padding(horizontal = 16.dp), - color = WikipediaTheme.colors.borderColor - ) - } - } - } - } -} - -@Preview -@Composable -private fun TopCategoriesViewPreview() { - BaseTheme( - currentTheme = Theme.LIGHT - ) { - TopCategoriesView( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp), - categories = listOf( - Category(2025, 1, "Category:Ancient history", "en", 1), - Category(2025, 1, "Category:World literature", "en", 1), - Category(2025, 1, "Category:Cat breeds originating in the United States", "en", 1), - ), - onClick = {} - ) - } -} From 974cfd50d4e5cdd288809fe3fffc8f67f75a1602 Mon Sep 17 00:00:00 2001 From: Cooltey Feng Date: Tue, 2 Sep 2025 06:49:55 -0700 Subject: [PATCH 54/70] Consolidate ActivityTabModules and ModuleType classes (#5886) --- .../ActivityTabCustomizationActivity.kt | 42 ----------------- .../activitytab/ActivityTabModules.kt | 45 +++++++++++++++++++ 2 files changed, 45 insertions(+), 42 deletions(-) create mode 100644 app/src/main/java/org/wikipedia/activitytab/ActivityTabModules.kt diff --git a/app/src/main/java/org/wikipedia/activitytab/ActivityTabCustomizationActivity.kt b/app/src/main/java/org/wikipedia/activitytab/ActivityTabCustomizationActivity.kt index 5574a38b3d8..9c963741ca7 100644 --- a/app/src/main/java/org/wikipedia/activitytab/ActivityTabCustomizationActivity.kt +++ b/app/src/main/java/org/wikipedia/activitytab/ActivityTabCustomizationActivity.kt @@ -25,7 +25,6 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import kotlinx.serialization.Serializable import org.wikipedia.R import org.wikipedia.activity.BaseActivity import org.wikipedia.compose.components.WikiTopAppBar @@ -155,47 +154,6 @@ private fun CustomizationScreenSwitch( ) } -fun ActivityTabModules.isModuleEnabled(moduleType: ModuleType): Boolean = when (moduleType) { - ModuleType.TIME_SPENT -> isTimeSpentEnabled - ModuleType.READING_INSIGHTS -> isReadingInsightsEnabled - ModuleType.EDITING_INSIGHTS -> isEditingInsightsEnabled - ModuleType.IMPACT -> isImpactEnabled - ModuleType.GAMES -> isGamesEnabled - ModuleType.DONATIONS -> isDonationsEnabled - ModuleType.TIMELINE -> isTimelineEnabled -} - -fun ActivityTabModules.setModuleEnabled(moduleType: ModuleType, enabled: Boolean) = when (moduleType) { - ModuleType.TIME_SPENT -> copy(isTimeSpentEnabled = enabled) - ModuleType.READING_INSIGHTS -> copy(isReadingInsightsEnabled = enabled) - ModuleType.EDITING_INSIGHTS -> copy(isEditingInsightsEnabled = enabled) - ModuleType.IMPACT -> copy(isImpactEnabled = enabled) - ModuleType.GAMES -> copy(isGamesEnabled = enabled) - ModuleType.DONATIONS -> copy(isDonationsEnabled = enabled) - ModuleType.TIMELINE -> copy(isTimelineEnabled = enabled) -} - -@Serializable -data class ActivityTabModules( - val isTimeSpentEnabled: Boolean = true, - val isReadingInsightsEnabled: Boolean = true, - val isEditingInsightsEnabled: Boolean = true, - val isImpactEnabled: Boolean = true, - val isGamesEnabled: Boolean = true, - val isDonationsEnabled: Boolean = false, - val isTimelineEnabled: Boolean = true, -) - -enum class ModuleType(val displayName: Int) { - TIME_SPENT(R.string.activity_tab_customize_screen_time_spent_switch_title), - READING_INSIGHTS(R.string.activity_tab_customize_screen_reading_insights_switch_title), - EDITING_INSIGHTS(R.string.activity_tab_customize_screen_editing_insights_switch_title), - IMPACT(R.string.activity_tab_customize_screen_impact_switch_title), - GAMES(R.string.activity_tab_customize_screen_games_switch_title), - DONATIONS(R.string.activity_tab_customize_screen_donations_switch_title), - TIMELINE(R.string.activity_tab_customize_screen_timeline_switch_title) -} - @Preview @Composable private fun CustomizationScreenPreview() { diff --git a/app/src/main/java/org/wikipedia/activitytab/ActivityTabModules.kt b/app/src/main/java/org/wikipedia/activitytab/ActivityTabModules.kt new file mode 100644 index 00000000000..7f4f55e026c --- /dev/null +++ b/app/src/main/java/org/wikipedia/activitytab/ActivityTabModules.kt @@ -0,0 +1,45 @@ +package org.wikipedia.activitytab + +import kotlinx.serialization.Serializable +import org.wikipedia.R + +@Serializable +data class ActivityTabModules( + val isTimeSpentEnabled: Boolean = true, + val isReadingInsightsEnabled: Boolean = true, + val isEditingInsightsEnabled: Boolean = true, + val isImpactEnabled: Boolean = true, + val isGamesEnabled: Boolean = true, + val isDonationsEnabled: Boolean = false, + val isTimelineEnabled: Boolean = true, +) { + fun isModuleEnabled(moduleType: ModuleType): Boolean = when (moduleType) { + ModuleType.TIME_SPENT -> isTimeSpentEnabled + ModuleType.READING_INSIGHTS -> isReadingInsightsEnabled + ModuleType.EDITING_INSIGHTS -> isEditingInsightsEnabled + ModuleType.IMPACT -> isImpactEnabled + ModuleType.GAMES -> isGamesEnabled + ModuleType.DONATIONS -> isDonationsEnabled + ModuleType.TIMELINE -> isTimelineEnabled + } + + fun setModuleEnabled(moduleType: ModuleType, enabled: Boolean) = when (moduleType) { + ModuleType.TIME_SPENT -> copy(isTimeSpentEnabled = enabled) + ModuleType.READING_INSIGHTS -> copy(isReadingInsightsEnabled = enabled) + ModuleType.EDITING_INSIGHTS -> copy(isEditingInsightsEnabled = enabled) + ModuleType.IMPACT -> copy(isImpactEnabled = enabled) + ModuleType.GAMES -> copy(isGamesEnabled = enabled) + ModuleType.DONATIONS -> copy(isDonationsEnabled = enabled) + ModuleType.TIMELINE -> copy(isTimelineEnabled = enabled) + } +} + +enum class ModuleType(val displayName: Int) { + TIME_SPENT(R.string.activity_tab_customize_screen_time_spent_switch_title), + READING_INSIGHTS(R.string.activity_tab_customize_screen_reading_insights_switch_title), + EDITING_INSIGHTS(R.string.activity_tab_customize_screen_editing_insights_switch_title), + IMPACT(R.string.activity_tab_customize_screen_impact_switch_title), + GAMES(R.string.activity_tab_customize_screen_games_switch_title), + DONATIONS(R.string.activity_tab_customize_screen_donations_switch_title), + TIMELINE(R.string.activity_tab_customize_screen_timeline_switch_title) +} From f5056a092057db501c2780f5031eda73b9c00e6d Mon Sep 17 00:00:00 2001 From: Cooltey Feng Date: Tue, 2 Sep 2025 07:29:17 -0700 Subject: [PATCH 55/70] Show a retry button for the WikiErrorView for generic error messages (#5887) * Show a retry button for WikiError view if necessary * No donation reminder --- .../activitytab/EditingInsightsModule.kt | 3 +- .../org/wikipedia/activitytab/ImpactModule.kt | 3 +- .../activitytab/ReadingHistoryModule.kt | 3 +- .../wikipedia/activitytab/WikiGamesModule.kt | 3 +- .../components/error/ComposeWikiErrorView.kt | 39 ++++++++++++++----- 5 files changed, 38 insertions(+), 13 deletions(-) diff --git a/app/src/main/java/org/wikipedia/activitytab/EditingInsightsModule.kt b/app/src/main/java/org/wikipedia/activitytab/EditingInsightsModule.kt index e88465236b2..05cb6226178 100644 --- a/app/src/main/java/org/wikipedia/activitytab/EditingInsightsModule.kt +++ b/app/src/main/java/org/wikipedia/activitytab/EditingInsightsModule.kt @@ -121,7 +121,8 @@ fun EditingInsightsModule( modifier = Modifier .fillMaxWidth(), caught = uiState.error, - errorClickEvents = wikiErrorClickEvents + errorClickEvents = wikiErrorClickEvents, + retryForGenericError = true ) } } diff --git a/app/src/main/java/org/wikipedia/activitytab/ImpactModule.kt b/app/src/main/java/org/wikipedia/activitytab/ImpactModule.kt index 16a42584c28..79cd075f3dc 100644 --- a/app/src/main/java/org/wikipedia/activitytab/ImpactModule.kt +++ b/app/src/main/java/org/wikipedia/activitytab/ImpactModule.kt @@ -89,7 +89,8 @@ fun ImpactModule( modifier = Modifier .fillMaxWidth(), caught = uiState.error, - errorClickEvents = wikiErrorClickEvents + errorClickEvents = wikiErrorClickEvents, + retryForGenericError = true ) } } diff --git a/app/src/main/java/org/wikipedia/activitytab/ReadingHistoryModule.kt b/app/src/main/java/org/wikipedia/activitytab/ReadingHistoryModule.kt index ee9b8ebcc83..c63c54bf733 100644 --- a/app/src/main/java/org/wikipedia/activitytab/ReadingHistoryModule.kt +++ b/app/src/main/java/org/wikipedia/activitytab/ReadingHistoryModule.kt @@ -222,7 +222,8 @@ fun ReadingHistoryModule( modifier = Modifier .fillMaxWidth(), caught = readingHistoryState.error, - errorClickEvents = wikiErrorClickEvents + errorClickEvents = wikiErrorClickEvents, + retryForGenericError = true ) } } diff --git a/app/src/main/java/org/wikipedia/activitytab/WikiGamesModule.kt b/app/src/main/java/org/wikipedia/activitytab/WikiGamesModule.kt index 3690f534d82..f14110e06fd 100644 --- a/app/src/main/java/org/wikipedia/activitytab/WikiGamesModule.kt +++ b/app/src/main/java/org/wikipedia/activitytab/WikiGamesModule.kt @@ -85,7 +85,8 @@ fun WikiGamesModule( modifier = Modifier .fillMaxWidth(), caught = uiState.error, - errorClickEvents = wikiErrorClickEvents + errorClickEvents = wikiErrorClickEvents, + retryForGenericError = true ) } } diff --git a/app/src/main/java/org/wikipedia/compose/components/error/ComposeWikiErrorView.kt b/app/src/main/java/org/wikipedia/compose/components/error/ComposeWikiErrorView.kt index eb03ef14996..1c154d89471 100644 --- a/app/src/main/java/org/wikipedia/compose/components/error/ComposeWikiErrorView.kt +++ b/app/src/main/java/org/wikipedia/compose/components/error/ComposeWikiErrorView.kt @@ -51,8 +51,9 @@ fun WikiErrorView( caught: Throwable?, pageTitle: PageTitle? = null, errorClickEvents: WikiErrorClickEvents? = null, + retryForGenericError: Boolean = false ) { - val errorType = getErrorType(caught, pageTitle) + val errorType = getErrorType(caught, pageTitle, retryForGenericError) val context = LocalContext.current val errorMessage = when { @@ -164,12 +165,13 @@ private fun getClickEventForErrorType( is ComposeErrorType.Generic -> wikiErrorClickEvents?.backClickListener is ComposeErrorType.LoggedOut -> wikiErrorClickEvents?.loginClickListener is ComposeErrorType.Empty -> wikiErrorClickEvents?.nextClickListener + is ComposeErrorType.Retry, is ComposeErrorType.Offline, is ComposeErrorType.Timeout -> wikiErrorClickEvents?.retryClickListener } } -private fun getErrorType(caught: Throwable?, pageTitle: PageTitle?): ComposeErrorType { +private fun getErrorType(caught: Throwable?, pageTitle: PageTitle?, retryForGenericError: Boolean): ComposeErrorType { caught?.let { when { is404(it) -> { @@ -190,15 +192,15 @@ private fun getErrorType(caught: Throwable?, pageTitle: PageTitle?): ComposeErro } } } - return ComposeErrorType.Generic() + return if (retryForGenericError) ComposeErrorType.Retry() else ComposeErrorType.Generic() } sealed class ComposeErrorType( - @DrawableRes val icon: Int, - @StringRes val text: Int, - @StringRes val buttonText: Int, + @param:DrawableRes val icon: Int, + @param:StringRes val text: Int, + @param:StringRes val buttonText: Int, val hasFooterText: Boolean = false, - @StringRes val footerText: Int = 0, + @param:StringRes val footerText: Int = 0, ) { class UserPageMissing : ComposeErrorType( icon = R.drawable.ic_userpage_error_icon, @@ -241,6 +243,12 @@ sealed class ComposeErrorType( text = R.string.error_message_generic, buttonText = R.string.error_back ) + + class Retry : ComposeErrorType( + icon = R.drawable.ic_error_black_24dp, + text = R.string.error_message_generic, + buttonText = R.string.offline_load_error_retry + ) } data class WikiErrorClickEvents( @@ -250,9 +258,9 @@ data class WikiErrorClickEvents( var loginClickListener: (() -> Unit)? = null, ) -@Preview +@Preview(showBackground = true) @Composable -private fun WikiErrorViewPreview() { +private fun WikiErrorViewGenericPreview() { BaseTheme( currentTheme = Theme.DARK ) { @@ -261,3 +269,16 @@ private fun WikiErrorViewPreview() { ) } } + +@Preview(showBackground = true) +@Composable +private fun WikiErrorViewRetryPreview() { + BaseTheme( + currentTheme = Theme.DARK + ) { + WikiErrorView( + caught = Exception(), + retryForGenericError = true + ) + } +} From dc9142faf5629f9cda4576d0af7958bf82259902 Mon Sep 17 00:00:00 2001 From: Cooltey Feng Date: Wed, 3 Sep 2025 05:35:34 -0700 Subject: [PATCH 56/70] Activity tab: update design and logic for suggested edits card (#5891) --- .../activitytab/EditingInsightsModule.kt | 127 ++++++++++-------- 1 file changed, 73 insertions(+), 54 deletions(-) diff --git a/app/src/main/java/org/wikipedia/activitytab/EditingInsightsModule.kt b/app/src/main/java/org/wikipedia/activitytab/EditingInsightsModule.kt index 05cb6226178..fc9d63e8473 100644 --- a/app/src/main/java/org/wikipedia/activitytab/EditingInsightsModule.kt +++ b/app/src/main/java/org/wikipedia/activitytab/EditingInsightsModule.kt @@ -87,6 +87,15 @@ fun EditingInsightsModule( } } is UiState.Success -> { + if (uiState.data.totalEditsCount == 0) { + SuggestedEditsCard( + modifier = modifier.fillMaxWidth(), + onClick = { + onSuggestedEditsClick() + } + ) + return + } MostViewedCard( modifier = modifier .fillMaxWidth(), @@ -101,12 +110,8 @@ fun EditingInsightsModule( lastEditRelativeTime = uiState.data.lastEditRelativeTime, editsThisMonth = uiState.data.editsThisMonth, editsLastMonth = uiState.data.editsLastMonth, - totalEdits = uiState.data.totalEditsCount, onContributionClick = { onContributionClick() - }, - onSuggestedEditsClick = { - onSuggestedEditsClick() } ) } @@ -279,9 +284,7 @@ fun ContributionCard( lastEditRelativeTime: String, editsThisMonth: Int, editsLastMonth: Int, - totalEdits: Int = 0, onContributionClick: (() -> Unit)? = null, - onSuggestedEditsClick: (() -> Unit)? = null ) { WikiCard( modifier = modifier, @@ -368,63 +371,68 @@ fun ContributionCard( barColor = WikipediaTheme.colors.borderColor ) } - if (totalEdits == 0) { - HorizontalDivider( - modifier = Modifier.padding(top = 16.dp), - color = WikipediaTheme.colors.borderColor - ) - SuggestedEditsView( - onClick = onSuggestedEditsClick - ) - } } } } @Composable -fun SuggestedEditsView( +fun SuggestedEditsCard( + modifier: Modifier = Modifier, onClick: (() -> Unit)? = null ) { - Column( - modifier = Modifier - .fillMaxWidth() - .padding(16.dp), - horizontalAlignment = Alignment.CenterHorizontally, + WikiCard( + modifier = modifier, + colors = CardDefaults.cardColors( + containerColor = WikipediaTheme.colors.paperColor, + contentColor = WikipediaTheme.colors.paperColor + ), + elevation = 0.dp, + border = BorderStroke( + width = 1.dp, + color = WikipediaTheme.colors.borderColor + ), ) { - Text( - modifier = Modifier.padding(bottom = 8.dp), - text = stringResource(R.string.activity_tab_impact_suggested_edits_title), - style = MaterialTheme.typography.titleSmall.copy( - fontWeight = FontWeight.Medium - ), - color = WikipediaTheme.colors.primaryColor - ) - Text( - textAlign = TextAlign.Center, - text = stringResource(R.string.activity_tab_impact_suggested_edits_message), - style = MaterialTheme.typography.bodyMedium, - color = WikipediaTheme.colors.primaryColor - ) - Button( - modifier = Modifier.padding(top = 16.dp).align(Alignment.CenterHorizontally), - contentPadding = PaddingValues(horizontal = 18.dp), - colors = ButtonDefaults.buttonColors( - containerColor = WikipediaTheme.colors.progressiveColor, - contentColor = WikipediaTheme.colors.paperColor, - ), - onClick = { onClick?.invoke() }, + Column( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + horizontalAlignment = Alignment.CenterHorizontally, ) { - Icon( - modifier = Modifier.size(20.dp), - painter = painterResource(R.drawable.ic_mode_edit_white_24dp), - tint = WikipediaTheme.colors.paperColor, - contentDescription = null + Text( + modifier = Modifier.padding(bottom = 8.dp), + text = stringResource(R.string.activity_tab_impact_suggested_edits_title), + style = MaterialTheme.typography.titleSmall.copy( + fontWeight = FontWeight.Medium + ), + color = WikipediaTheme.colors.primaryColor ) Text( - modifier = Modifier.padding(start = 6.dp, top = 4.dp, bottom = 4.dp), - text = stringResource(R.string.activity_tab_impact_suggested_edits_button), - style = MaterialTheme.typography.labelLarge + textAlign = TextAlign.Center, + text = stringResource(R.string.activity_tab_impact_suggested_edits_message), + style = MaterialTheme.typography.bodyMedium, + color = WikipediaTheme.colors.primaryColor ) + Button( + modifier = Modifier.padding(top = 16.dp).align(Alignment.CenterHorizontally), + contentPadding = PaddingValues(horizontal = 18.dp), + colors = ButtonDefaults.buttonColors( + containerColor = WikipediaTheme.colors.progressiveColor, + contentColor = WikipediaTheme.colors.paperColor, + ), + onClick = { onClick?.invoke() }, + ) { + Icon( + modifier = Modifier.size(20.dp), + painter = painterResource(R.drawable.ic_mode_edit_white_24dp), + tint = WikipediaTheme.colors.paperColor, + contentDescription = null + ) + Text( + modifier = Modifier.padding(start = 6.dp, top = 4.dp, bottom = 4.dp), + text = stringResource(R.string.activity_tab_impact_suggested_edits_button), + style = MaterialTheme.typography.labelLarge + ) + } } } } @@ -510,11 +518,22 @@ private fun ContributionCardPreview() { ContributionCard( modifier = Modifier.fillMaxWidth(), lastEditRelativeTime = "2 days ago", - totalEdits = 9, editsThisMonth = 9, editsLastMonth = 2, - onContributionClick = null, - onSuggestedEditsClick = null + onContributionClick = null + ) + } +} + +@Preview +@Composable +private fun SuggestedEditsCardPreview() { + BaseTheme( + currentTheme = Theme.LIGHT + ) { + SuggestedEditsCard( + modifier = Modifier.fillMaxWidth(), + onClick = {} ) } } From d9cb45c0bb6b5327a9e7a529a22bddbefff0eb84 Mon Sep 17 00:00:00 2001 From: Cooltey Feng Date: Wed, 3 Sep 2025 05:44:17 -0700 Subject: [PATCH 57/70] Activity Tab: empty state for no modules enabled (#5890) --- .../activitytab/ActivityTabFragment.kt | 69 ++++++++++++ .../activitytab/ActivityTabModules.kt | 2 + .../illustration_activity_tab_empty.xml | 101 ++++++++++++++++++ app/src/main/res/values-qq/strings.xml | 2 + app/src/main/res/values/strings.xml | 2 + 5 files changed, 176 insertions(+) create mode 100644 app/src/main/res/drawable/illustration_activity_tab_empty.xml diff --git a/app/src/main/java/org/wikipedia/activitytab/ActivityTabFragment.kt b/app/src/main/java/org/wikipedia/activitytab/ActivityTabFragment.kt index 2515e790c8e..e55cdd556fa 100644 --- a/app/src/main/java/org/wikipedia/activitytab/ActivityTabFragment.kt +++ b/app/src/main/java/org/wikipedia/activitytab/ActivityTabFragment.kt @@ -250,6 +250,42 @@ class ActivityTabFragment : Fragment() { return@Scaffold } + if (modules.noModulesEnabled()) { + Box( + modifier = Modifier + .fillMaxSize() + .padding(paddingValues) + ) { + val scrollState = rememberScrollState() + Column( + modifier = Modifier.align(Alignment.Center).padding(horizontal = 16.dp) + .verticalScroll(scrollState), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Image( + modifier = Modifier.size(164.dp), + painter = painterResource(R.drawable.illustration_activity_tab_empty), + contentDescription = null + ) + Text( + modifier = Modifier.padding(top = 16.dp), + text = stringResource(R.string.activity_tab_customize_screen_no_modules_title), + style = MaterialTheme.typography.titleSmall, + textAlign = TextAlign.Center, + color = WikipediaTheme.colors.primaryColor + ) + Text( + modifier = Modifier.padding(top = 4.dp), + text = stringResource(R.string.activity_tab_customize_screen_no_modules_message), + style = MaterialTheme.typography.bodyMedium, + textAlign = TextAlign.Center, + color = WikipediaTheme.colors.primaryColor + ) + } + return@Scaffold + } + } + PullToRefreshBox( onRefresh = { isRefreshing = true @@ -547,6 +583,39 @@ class ActivityTabFragment : Fragment() { } } + @Preview + @Composable + fun ActivityTabNoModulesPreview() { + BaseTheme(currentTheme = Theme.LIGHT) { + ActivityTabScreen( + isLoggedIn = true, + userName = "User", + modules = ActivityTabModules( + isTimeSpentEnabled = false, + isReadingInsightsEnabled = false, + isEditingInsightsEnabled = false, + isImpactEnabled = false, + isGamesEnabled = false, + isDonationsEnabled = false, + isTimelineEnabled = false + ), + readingHistoryState = UiState.Success(ActivityTabViewModel.ReadingHistory( + timeSpentThisWeek = 0, + articlesReadThisMonth = 0, + lastArticleReadTime = null, + articlesReadByWeek = listOf(0, 0, 0, 0), + articlesSavedThisMonth = 0, + lastArticleSavedTime = null, + articlesSaved = emptyList(), + topCategories = emptyList() + )), + donationUiState = UiState.Success("Unknown"), + wikiGamesUiState = UiState.Success(null), + impactUiState = UiState.Success(GrowthUserImpact()) + ) + } + } + companion object { fun newInstance(): ActivityTabFragment { return ActivityTabFragment().apply { diff --git a/app/src/main/java/org/wikipedia/activitytab/ActivityTabModules.kt b/app/src/main/java/org/wikipedia/activitytab/ActivityTabModules.kt index 7f4f55e026c..5d1f7c9be1d 100644 --- a/app/src/main/java/org/wikipedia/activitytab/ActivityTabModules.kt +++ b/app/src/main/java/org/wikipedia/activitytab/ActivityTabModules.kt @@ -32,6 +32,8 @@ data class ActivityTabModules( ModuleType.DONATIONS -> copy(isDonationsEnabled = enabled) ModuleType.TIMELINE -> copy(isTimelineEnabled = enabled) } + + fun noModulesEnabled() = !(isTimeSpentEnabled || isReadingInsightsEnabled || isEditingInsightsEnabled || isImpactEnabled || isGamesEnabled || isDonationsEnabled || isTimelineEnabled) } enum class ModuleType(val displayName: Int) { diff --git a/app/src/main/res/drawable/illustration_activity_tab_empty.xml b/app/src/main/res/drawable/illustration_activity_tab_empty.xml new file mode 100644 index 00000000000..a226e62baab --- /dev/null +++ b/app/src/main/res/drawable/illustration_activity_tab_empty.xml @@ -0,0 +1,101 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/values-qq/strings.xml b/app/src/main/res/values-qq/strings.xml index f0956c3fd9e..874afc9f213 100644 --- a/app/src/main/res/values-qq/strings.xml +++ b/app/src/main/res/values-qq/strings.xml @@ -1236,6 +1236,8 @@ Title of the switch for the Games module in the Activity Tab customization screen. Title of the switch for the Donations module in the Activity Tab customization screen. Title of the switch for the Timeline module in the Activity Tab customization screen. + Title of the empty state when all modules are turned off. + Message of the empty state when all modules are turned off. Title shown at the top of the activity for the file page. Button label to add image caption for the file. Button label to add image tags for the file. diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index c5b686ced01..2bc655048bf 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1258,6 +1258,8 @@ Game stats Last in app donation Timeline of behavior + Your Activity Tab modules are off + Turn on Activity Tab modules to see your reading history, editing history, and more. From 297471c77419722e5de1e8163dbbb92bf4f6f645 Mon Sep 17 00:00:00 2001 From: Dmitry Brant Date: Wed, 3 Sep 2025 09:10:26 -0400 Subject: [PATCH 58/70] Clean up module enable/visibility logic. --- .../ActivityTabCustomizationActivity.kt | 6 ++--- .../activitytab/ActivityTabFragment.kt | 25 ++++++++++--------- .../activitytab/ActivityTabModules.kt | 7 +++++- 3 files changed, 22 insertions(+), 16 deletions(-) diff --git a/app/src/main/java/org/wikipedia/activitytab/ActivityTabCustomizationActivity.kt b/app/src/main/java/org/wikipedia/activitytab/ActivityTabCustomizationActivity.kt index 9c963741ca7..6f5eb62700c 100644 --- a/app/src/main/java/org/wikipedia/activitytab/ActivityTabCustomizationActivity.kt +++ b/app/src/main/java/org/wikipedia/activitytab/ActivityTabCustomizationActivity.kt @@ -46,7 +46,7 @@ class ActivityTabCustomizationActivity : BaseActivity() { finish() }, modules = Prefs.activityTabModules, - showLastDonation = Prefs.donationResults.isNotEmpty() + haveAtLeastOneDonation = Prefs.donationResults.isNotEmpty() ) } } @@ -64,7 +64,7 @@ fun CustomizationScreen( modifier: Modifier = Modifier, onBackButtonClick: () -> Unit, modules: ActivityTabModules, - showLastDonation: Boolean = false + haveAtLeastOneDonation: Boolean = false ) { var currentModules by remember { mutableStateOf(modules) } @@ -95,7 +95,7 @@ fun CustomizationScreen( ) } itemsIndexed(ModuleType.entries) { index, moduleType -> - if (moduleType == ModuleType.DONATIONS && !showLastDonation) { + if (moduleType == ModuleType.DONATIONS && !haveAtLeastOneDonation) { return@itemsIndexed } CustomizationScreenSwitch( diff --git a/app/src/main/java/org/wikipedia/activitytab/ActivityTabFragment.kt b/app/src/main/java/org/wikipedia/activitytab/ActivityTabFragment.kt index e55cdd556fa..91b8913eb1b 100644 --- a/app/src/main/java/org/wikipedia/activitytab/ActivityTabFragment.kt +++ b/app/src/main/java/org/wikipedia/activitytab/ActivityTabFragment.kt @@ -169,6 +169,7 @@ class ActivityTabFragment : Fragment() { if (readingHistoryState is UiState.Success) { isRefreshing = false } + val haveAtLeastOneDonation = Prefs.donationResults.isNotEmpty() if (!isLoggedIn) { Box( @@ -250,7 +251,7 @@ class ActivityTabFragment : Fragment() { return@Scaffold } - if (modules.noModulesEnabled()) { + if (modules.noModulesVisible(haveAtLeastOneDonation)) { Box( modifier = Modifier .fillMaxSize() @@ -304,7 +305,7 @@ class ActivityTabFragment : Fragment() { } ) { LazyColumn { - if (modules.isModuleEnabled(ModuleType.TIME_SPENT) || modules.isModuleEnabled(ModuleType.READING_INSIGHTS)) { + if (modules.isModuleVisible(ModuleType.TIME_SPENT) || modules.isModuleVisible(ModuleType.READING_INSIGHTS)) { item { Column( modifier = Modifier @@ -322,8 +323,8 @@ class ActivityTabFragment : Fragment() { ReadingHistoryModule( modifier = Modifier.align(Alignment.CenterHorizontally), userName = userName, - showTimeSpent = modules.isModuleEnabled(ModuleType.TIME_SPENT), - showInsights = modules.isModuleEnabled(ModuleType.READING_INSIGHTS), + showTimeSpent = modules.isModuleVisible(ModuleType.TIME_SPENT), + showInsights = modules.isModuleVisible(ModuleType.READING_INSIGHTS), readingHistoryState = readingHistoryState, onArticlesReadClick = { callback()?.onNavigateTo(NavTab.SEARCH) }, onArticlesSavedClick = { callback()?.onNavigateTo(NavTab.READING_LISTS) }, @@ -361,7 +362,7 @@ class ActivityTabFragment : Fragment() { ) ) ) { - if (modules.isModuleEnabled(ModuleType.EDITING_INSIGHTS) || modules.isModuleEnabled(ModuleType.IMPACT)) { + if (modules.isModuleVisible(ModuleType.EDITING_INSIGHTS) || modules.isModuleVisible(ModuleType.IMPACT)) { Text( modifier = Modifier.padding(start = 16.dp, end = 16.dp, top = 24.dp), text = stringResource(R.string.activity_tab_impact), @@ -371,7 +372,7 @@ class ActivityTabFragment : Fragment() { ) } - if (modules.isModuleEnabled(ModuleType.EDITING_INSIGHTS)) { + if (modules.isModuleVisible(ModuleType.EDITING_INSIGHTS)) { EditingInsightsModule( modifier = Modifier .fillMaxWidth() @@ -410,7 +411,7 @@ class ActivityTabFragment : Fragment() { ) } - if (modules.isModuleEnabled(ModuleType.IMPACT)) { + if (modules.isModuleVisible(ModuleType.IMPACT)) { ImpactModule( modifier = Modifier .fillMaxWidth() @@ -424,7 +425,7 @@ class ActivityTabFragment : Fragment() { ) } - if (modules.isModuleEnabled(ModuleType.GAMES) || modules.isModuleEnabled(ModuleType.DONATIONS)) { + if (modules.isModuleVisible(ModuleType.GAMES) || modules.isModuleVisible(ModuleType.DONATIONS)) { Text( modifier = Modifier.padding(start = 16.dp, end = 16.dp, top = 24.dp), text = stringResource(R.string.activity_tab_highlights), @@ -434,7 +435,7 @@ class ActivityTabFragment : Fragment() { ) } - if (modules.isModuleEnabled(ModuleType.GAMES)) { + if (modules.isModuleVisible(ModuleType.GAMES)) { WikiGamesModule( modifier = Modifier .fillMaxWidth() @@ -464,7 +465,7 @@ class ActivityTabFragment : Fragment() { ) } - if (modules.isModuleEnabled(ModuleType.DONATIONS) && Prefs.donationResults.isNotEmpty()) { + if (modules.isModuleVisible(ModuleType.DONATIONS, haveAtLeastOneDonation)) { DonationModule( modifier = Modifier .fillMaxWidth() @@ -478,14 +479,14 @@ class ActivityTabFragment : Fragment() { ) } - if (modules.isModuleEnabled(ModuleType.DONATIONS) || modules.isModuleEnabled(ModuleType.GAMES) || modules.isModuleEnabled(ModuleType.EDITING_INSIGHTS) || modules.isModuleEnabled(ModuleType.IMPACT)) { + if (modules.isModuleVisible(ModuleType.DONATIONS, haveAtLeastOneDonation) || modules.isModuleVisible(ModuleType.GAMES) || modules.isModuleVisible(ModuleType.EDITING_INSIGHTS) || modules.isModuleEnabled(ModuleType.IMPACT)) { // Add bottom padding only if at least one of the modules in this gradient box is enabled. Spacer(modifier = Modifier.size(16.dp)) } } } - if (modules.isModuleEnabled(ModuleType.TIMELINE)) { + if (modules.isModuleVisible(ModuleType.TIMELINE)) { // @TODO: MARK_ACTIVITY_TAB } } diff --git a/app/src/main/java/org/wikipedia/activitytab/ActivityTabModules.kt b/app/src/main/java/org/wikipedia/activitytab/ActivityTabModules.kt index 5d1f7c9be1d..99b79c0abe1 100644 --- a/app/src/main/java/org/wikipedia/activitytab/ActivityTabModules.kt +++ b/app/src/main/java/org/wikipedia/activitytab/ActivityTabModules.kt @@ -33,7 +33,12 @@ data class ActivityTabModules( ModuleType.TIMELINE -> copy(isTimelineEnabled = enabled) } - fun noModulesEnabled() = !(isTimeSpentEnabled || isReadingInsightsEnabled || isEditingInsightsEnabled || isImpactEnabled || isGamesEnabled || isDonationsEnabled || isTimelineEnabled) + fun isModuleVisible(moduleType: ModuleType, haveAtLeastOneDonation: Boolean = false): Boolean = when (moduleType) { + ModuleType.DONATIONS -> isModuleEnabled(moduleType) && haveAtLeastOneDonation + else -> isModuleEnabled(moduleType) + } + + fun noModulesVisible(haveAtLeastOneDonation: Boolean = false) = ModuleType.entries.all { !isModuleVisible(it, haveAtLeastOneDonation) } } enum class ModuleType(val displayName: Int) { From a0d29614ecac16220f4d14b69cfc5b8e0739a4fe Mon Sep 17 00:00:00 2001 From: Dmitry Brant Date: Wed, 3 Sep 2025 10:25:47 -0400 Subject: [PATCH 59/70] Update suggested edit verbiage. --- app/src/main/res/values/strings.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 2bc655048bf..3ddd450e652 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1239,9 +1239,9 @@ Best streak Last edited - 0 edits to articles so far + 0 edits to articles recently Help extend free knowledge to the world by editing topics that matter most to you. - Try out Suggested edits + Make an edit Your recent activity (last 30 days) Edit From 1a05a8e515e88ee83b8f1c1bddf675c84e64f1e5 Mon Sep 17 00:00:00 2001 From: William Rai <48931640+Williamrai@users.noreply.github.com> Date: Wed, 3 Sep 2025 13:19:23 -0400 Subject: [PATCH 60/70] Activity tab timeline (#5863) * - adds activity tab timeline ui, repository, extension functions and other logics * - adds paging source - adds database function to support time range - adds a flow for paging source with custom separator function to add date separator - updates getUserContrib parameter to use ucdir for retrieving newest items first - adds paging for jetpack compose * - adds date support * - adds date support * - adds cursor based pagination - adds interface to support multiple sources easily - code fixes/updates * - moves code to separate files * - adds support for click events - adds new items and functions to support click events * - adds refresh onResume for activity timeline * - code fix * - code fix * - removes Todo * - moves timeline classes into its own package * - updates code to show timeline when user logs in from activity tab screen * - code fixes * - ui/code fixes * - code fixes * - adds todo * - updates font family * - code/ui fixes * - rename TimelineModule.kt * - code fixes * - add logic to retrieve page info for missing pages - code fixes * - code fixes * - minor bug fix * - logic fix to include ignored contributions * - code fix * - adds empty state view for timeline, ui fixes for dark mode and code fixes --------- Co-authored-by: Cooltey Feng --- app/build.gradle | 1 + .../activitytab/ActivityTabFragment.kt | 128 ++++++++- .../activitytab/ActivityTabViewModel.kt | 70 +++++ .../activitytab/timeline/TimelineModule.kt | 244 ++++++++++++++++++ .../timeline/TimelinePagingSource.kt | 42 +++ .../timeline/TimelineRepository.kt | 196 ++++++++++++++ .../java/org/wikipedia/dataclient/Service.kt | 3 +- .../java/org/wikipedia/extensions/Date.kt | 17 ++ .../history/db/HistoryEntryWithImage.kt | 3 +- .../history/db/HistoryEntryWithImageDao.kt | 15 +- .../readinglist/db/ReadingListPageDao.kt | 3 + .../main/java/org/wikipedia/util/DateUtil.kt | 32 ++- .../res/drawable/outline_difference_24.xml | 5 + app/src/main/res/values-qq/strings.xml | 4 + app/src/main/res/values/strings.xml | 4 + gradle/libs.versions.toml | 2 + 16 files changed, 756 insertions(+), 13 deletions(-) create mode 100644 app/src/main/java/org/wikipedia/activitytab/timeline/TimelineModule.kt create mode 100644 app/src/main/java/org/wikipedia/activitytab/timeline/TimelinePagingSource.kt create mode 100644 app/src/main/java/org/wikipedia/activitytab/timeline/TimelineRepository.kt create mode 100644 app/src/main/java/org/wikipedia/extensions/Date.kt create mode 100644 app/src/main/res/drawable/outline_difference_24.xml diff --git a/app/build.gradle b/app/build.gradle index 1a86719c48c..f476f4ad00b 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -207,6 +207,7 @@ dependencies { implementation libs.photoview implementation libs.balloon implementation libs.retrofit2.kotlinx.serialization.converter + implementation(libs.paging.compose) implementation libs.android.sdk implementation libs.android.plugin.annotation.v9 diff --git a/app/src/main/java/org/wikipedia/activitytab/ActivityTabFragment.kt b/app/src/main/java/org/wikipedia/activitytab/ActivityTabFragment.kt index 91b8913eb1b..974e56be756 100644 --- a/app/src/main/java/org/wikipedia/activitytab/ActivityTabFragment.kt +++ b/app/src/main/java/org/wikipedia/activitytab/ActivityTabFragment.kt @@ -15,6 +15,7 @@ import androidx.compose.foundation.layout.PaddingValues 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.layout.size import androidx.compose.foundation.lazy.LazyColumn @@ -22,6 +23,7 @@ import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme @@ -53,13 +55,22 @@ import androidx.fragment.app.viewModels import androidx.lifecycle.Lifecycle import androidx.lifecycle.lifecycleScope import androidx.lifecycle.repeatOnLifecycle +import androidx.paging.LoadState +import androidx.paging.PagingData +import androidx.paging.compose.collectAsLazyPagingItems +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.launch import org.wikipedia.Constants import org.wikipedia.R import org.wikipedia.WikipediaApp import org.wikipedia.activity.BaseActivity import org.wikipedia.activity.FragmentUtil.getCallback +import org.wikipedia.activitytab.timeline.ActivitySource +import org.wikipedia.activitytab.timeline.TimelineDateSeparator +import org.wikipedia.activitytab.timeline.TimelineModule +import org.wikipedia.activitytab.timeline.TimelineModuleEmptyView import org.wikipedia.auth.AccountUtil import org.wikipedia.categories.CategoryActivity import org.wikipedia.categories.db.Category @@ -69,6 +80,7 @@ import org.wikipedia.compose.theme.WikipediaTheme import org.wikipedia.concurrency.FlowEventBus import org.wikipedia.dataclient.WikiSite import org.wikipedia.dataclient.growthtasks.GrowthUserImpact +import org.wikipedia.diff.ArticleEditDetailsActivity import org.wikipedia.events.LoggedInEvent import org.wikipedia.events.LoggedOutEvent import org.wikipedia.events.LoggedOutInBackgroundEvent @@ -128,7 +140,8 @@ class ActivityTabFragment : Fragment() { readingHistoryState = viewModel.readingHistoryState.collectAsState().value, donationUiState = viewModel.donationUiState.collectAsState().value, wikiGamesUiState = viewModel.wikiGamesUiState.collectAsState().value, - impactUiState = viewModel.impactUiState.collectAsState().value + impactUiState = viewModel.impactUiState.collectAsState().value, + timelineFlow = viewModel.timelineFlow ) } } @@ -156,8 +169,10 @@ class ActivityTabFragment : Fragment() { readingHistoryState: UiState, donationUiState: UiState, wikiGamesUiState: UiState, - impactUiState: UiState + impactUiState: UiState, + timelineFlow: Flow> ) { + val timelineItems = timelineFlow.collectAsLazyPagingItems() Scaffold( modifier = Modifier .fillMaxSize() @@ -290,6 +305,7 @@ class ActivityTabFragment : Fragment() { PullToRefreshBox( onRefresh = { isRefreshing = true + timelineItems.refresh() viewModel.loadAll() }, isRefreshing = isRefreshing, @@ -487,7 +503,101 @@ class ActivityTabFragment : Fragment() { } if (modules.isModuleVisible(ModuleType.TIMELINE)) { - // @TODO: MARK_ACTIVITY_TAB + if (timelineItems.itemCount == 0) { + item { + TimelineModuleEmptyView( + modifier = Modifier.align(Alignment.Center) + .padding(horizontal = 16.dp) + .padding(top = 32.dp, bottom = 52.dp) + ) + } + return@LazyColumn + } + items( + count = timelineItems.itemCount, + ) { index -> + when (val displayItem = timelineItems[index]) { + is TimelineDisplayItem.DateSeparator -> { + TimelineDateSeparator( + date = displayItem.date, + modifier = Modifier + .padding(horizontal = 16.dp) + .padding(top = 22.dp, bottom = 8.dp) + ) + } + is TimelineDisplayItem.TimelineEntry -> { + TimelineModule( + timelineItem = displayItem.item, + onItemClick = { item -> + when (item.activitySource) { + ActivitySource.EDIT -> { + startActivity( + ArticleEditDetailsActivity.newIntent( + requireContext(), + PageTitle( + item.apiTitle, + viewModel.wikiSite, + item.thumbnailUrl, + item.description, + item.displayTitle + ), item.pageId, revisionTo = item.id + ) + ) + } + + ActivitySource.BOOKMARKED -> { + val title = item.toPageTitle() + val entry = HistoryEntry( + title, + HistoryEntry.SOURCE_INTERNAL_LINK + ) + startActivity( + PageActivity.newIntentForCurrentTab( + requireContext(), + entry, + entry.title + ) + ) + } + + ActivitySource.LINK, ActivitySource.SEARCH -> { + val entry = item.toHistoryEntry() + startActivity( + PageActivity.newIntentForCurrentTab( + requireContext(), + entry, + entry.title + ) + ) + } + + else -> {} + } + } + ) + Spacer(modifier = Modifier.height(6.dp)) + } + null -> {} + } + } + + when (timelineItems.loadState.append) { + LoadState.Loading -> { + item { + Box( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 16.dp), + contentAlignment = Alignment.Center + ) { + CircularProgressIndicator( + color = WikipediaTheme.colors.progressiveColor + ) + } + } + } + else -> {} + } } } } @@ -529,7 +639,8 @@ class ActivityTabFragment : Fragment() { currentStreak = 15, bestStreak = 25 )), - impactUiState = UiState.Success(GrowthUserImpact(totalEditsCount = 12345)) + impactUiState = UiState.Success(GrowthUserImpact(totalEditsCount = 12345)), + timelineFlow = emptyFlow() ) } } @@ -554,7 +665,8 @@ class ActivityTabFragment : Fragment() { )), donationUiState = UiState.Success("Unknown"), wikiGamesUiState = UiState.Success(null), - impactUiState = UiState.Success(GrowthUserImpact()) + impactUiState = UiState.Success(GrowthUserImpact()), + timelineFlow = emptyFlow() ) } } @@ -579,7 +691,8 @@ class ActivityTabFragment : Fragment() { )), donationUiState = UiState.Success("Unknown"), wikiGamesUiState = UiState.Success(null), - impactUiState = UiState.Success(GrowthUserImpact()) + impactUiState = UiState.Success(GrowthUserImpact()), + timelineFlow = emptyFlow() ) } } @@ -612,7 +725,8 @@ class ActivityTabFragment : Fragment() { )), donationUiState = UiState.Success("Unknown"), wikiGamesUiState = UiState.Success(null), - impactUiState = UiState.Success(GrowthUserImpact()) + impactUiState = UiState.Success(GrowthUserImpact()), + timelineFlow = emptyFlow() ) } } diff --git a/app/src/main/java/org/wikipedia/activitytab/ActivityTabViewModel.kt b/app/src/main/java/org/wikipedia/activitytab/ActivityTabViewModel.kt index 56f1fbf63d8..94bdd14fd2b 100644 --- a/app/src/main/java/org/wikipedia/activitytab/ActivityTabViewModel.kt +++ b/app/src/main/java/org/wikipedia/activitytab/ActivityTabViewModel.kt @@ -4,18 +4,33 @@ import android.text.format.DateUtils import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import androidx.paging.Pager +import androidx.paging.PagingConfig +import androidx.paging.cachedIn +import androidx.paging.insertSeparators +import androidx.paging.map import kotlinx.coroutines.CoroutineExceptionHandler import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch +import org.wikipedia.Constants import org.wikipedia.WikipediaApp +import org.wikipedia.activitytab.timeline.HistoryEntryPagingSource +import org.wikipedia.activitytab.timeline.ReadingListPagingSource +import org.wikipedia.activitytab.timeline.TimelineItem +import org.wikipedia.activitytab.timeline.TimelinePagingSource +import org.wikipedia.activitytab.timeline.TimelineSource +import org.wikipedia.activitytab.timeline.UserContribPagingSource import org.wikipedia.auth.AccountUtil import org.wikipedia.categories.db.Category import org.wikipedia.database.AppDatabase +import org.wikipedia.dataclient.Service import org.wikipedia.dataclient.ServiceFactory import org.wikipedia.dataclient.WikiSite import org.wikipedia.dataclient.growthtasks.GrowthUserImpact +import org.wikipedia.extensions.toLocalDate import org.wikipedia.games.onthisday.OnThisDayGameViewModel import org.wikipedia.json.JsonUtil import org.wikipedia.page.PageTitle @@ -27,10 +42,21 @@ import java.time.LocalDate import java.time.LocalDateTime import java.time.ZoneId import java.time.ZoneOffset +import java.util.Date import java.util.concurrent.TimeUnit import kotlin.math.abs class ActivityTabViewModel(savedStateHandle: SavedStateHandle) : ViewModel() { + var langCode = Prefs.userContribFilterLangCode + + val wikiSite get(): WikiSite { + return when (langCode) { + Constants.WIKI_CODE_COMMONS -> WikiSite(Service.COMMONS_URL) + Constants.WIKI_CODE_WIKIDATA -> WikiSite(Service.WIKIDATA_URL) + else -> WikiSite.forLanguageCode(langCode) + } + } + private val _readingHistoryState = MutableStateFlow>(UiState.Loading) val readingHistoryState: StateFlow> = _readingHistoryState.asStateFlow() @@ -40,6 +66,33 @@ class ActivityTabViewModel(savedStateHandle: SavedStateHandle) : ViewModel() { private val _wikiGamesUiState = MutableStateFlow>(UiState.Loading) val wikiGamesUiState: StateFlow> = _wikiGamesUiState.asStateFlow() + private var currentTimelinePagingSource: TimelinePagingSource? = null + + val timelineFlow = Pager( + config = PagingConfig( + pageSize = 150, + prefetchDistance = 20 + ), + pagingSourceFactory = { TimelinePagingSource( + createTimelineSources() + ).also { + currentTimelinePagingSource = it + } } + ).flow.cachedIn(viewModelScope) + .map { pagingData -> + pagingData.insertSeparators { before, after -> + if (before == null && after != null) TimelineDisplayItem.DateSeparator(after.timestamp) + else if (before != null && after != null && before.timestamp.toLocalDate() != after.timestamp.toLocalDate()) { + TimelineDisplayItem.DateSeparator(after.timestamp) + } else null + }.map { item -> + when (item) { + is TimelineItem -> TimelineDisplayItem.TimelineEntry(item) + else -> item as TimelineDisplayItem + } + } + } + private val _impactUiState = MutableStateFlow>(UiState.Loading) val impactUiState: StateFlow> = _impactUiState.asStateFlow() @@ -48,6 +101,11 @@ class ActivityTabViewModel(savedStateHandle: SavedStateHandle) : ViewModel() { loadDonationResults() loadWikiGamesStats() loadImpact() + refreshTimeline() + } + + private fun refreshTimeline() { + currentTimelinePagingSource?.invalidate() } fun loadReadingHistory() { @@ -173,6 +231,13 @@ class ActivityTabViewModel(savedStateHandle: SavedStateHandle) : ViewModel() { return PageTitle(title = category.title, wiki = WikiSite.forLanguageCode(category.lang)) } + private fun createTimelineSources(): List { + val historyEntryPagingSource = HistoryEntryPagingSource(AppDatabase.instance.historyEntryWithImageDao()) + val userContribPagingSource = UserContribPagingSource(wikiSite, AccountUtil.userName, AppDatabase.instance.historyEntryWithImageDao()) + val readingListPagingSource = ReadingListPagingSource(AppDatabase.instance.readingListPageDao()) + return listOf(historyEntryPagingSource, readingListPagingSource, userContribPagingSource) + } + class ReadingHistory( val timeSpentThisWeek: Long, val articlesReadThisMonth: Int, @@ -188,3 +253,8 @@ class ActivityTabViewModel(savedStateHandle: SavedStateHandle) : ViewModel() { const val CAMPAIGN_ID = "appmenu_activity" } } + +sealed class TimelineDisplayItem { + data class DateSeparator(val date: Date) : TimelineDisplayItem() + data class TimelineEntry(val item: TimelineItem) : TimelineDisplayItem() +} diff --git a/app/src/main/java/org/wikipedia/activitytab/timeline/TimelineModule.kt b/app/src/main/java/org/wikipedia/activitytab/timeline/TimelineModule.kt new file mode 100644 index 00000000000..456710e3207 --- /dev/null +++ b/app/src/main/java/org/wikipedia/activitytab/timeline/TimelineModule.kt @@ -0,0 +1,244 @@ +package org.wikipedia.activitytab.timeline + +import androidx.compose.foundation.Image +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +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.shape.RoundedCornerShape +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.graphics.painter.BrushPainter +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import coil3.compose.AsyncImage +import org.wikipedia.R +import org.wikipedia.compose.components.HtmlText +import org.wikipedia.compose.theme.BaseTheme +import org.wikipedia.compose.theme.WikipediaTheme +import org.wikipedia.extensions.isToday +import org.wikipedia.extensions.isYesterday +import org.wikipedia.theme.Theme +import org.wikipedia.util.DateUtil +import org.wikipedia.views.imageservice.ImageService +import java.util.Date + +// @TODO: MARK_ACTIVITY_TAB retrieve description and thumbnail for contributions through API +@Composable +fun TimelineModule( + modifier: Modifier = Modifier, + timelineItem: TimelineItem, + onItemClick: (TimelineItem) -> Unit +) { + Row( + modifier = Modifier + .fillMaxWidth() + .clickable(onClick = { onItemClick(timelineItem) }) + .padding(horizontal = 16.dp, vertical = 8.dp), + horizontalArrangement = Arrangement.spacedBy(16.dp), + verticalAlignment = Alignment.CenterVertically + ) { + val icon = when (timelineItem.activitySource) { + ActivitySource.EDIT -> R.drawable.ic_mode_edit_white_24dp + ActivitySource.SEARCH -> R.drawable.search_bold + ActivitySource.LINK -> R.drawable.ic_link_black_24dp + ActivitySource.BOOKMARKED -> R.drawable.ic_bookmark_white_24dp + null -> null + } + if (icon != null) { + Icon( + painter = painterResource(icon), + tint = WikipediaTheme.colors.primaryColor, + contentDescription = null + ) + } + Column( + modifier = Modifier + .weight(1f), + verticalArrangement = Arrangement.Center + ) { + HtmlText( + text = timelineItem.displayTitle, + style = MaterialTheme.typography.titleMedium.copy( + fontFamily = FontFamily.Serif, + color = WikipediaTheme.colors.primaryColor + ), + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + + // Description + if (!timelineItem.description.isNullOrEmpty()) { + Text( + text = timelineItem.description, + style = MaterialTheme.typography.bodyMedium, + color = WikipediaTheme.colors.secondaryColor, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + } + + if (timelineItem.activitySource == ActivitySource.EDIT) { + Button( + modifier = modifier.padding(top = 8.dp), + contentPadding = PaddingValues(horizontal = 18.dp), + colors = ButtonDefaults.buttonColors( + containerColor = WikipediaTheme.colors.additionColor, + contentColor = WikipediaTheme.colors.secondaryColor, + ), + onClick = { onItemClick(timelineItem) }, + ) { + Icon( + modifier = Modifier.size(20.dp), + painter = painterResource(R.drawable.outline_difference_24), + tint = WikipediaTheme.colors.secondaryColor, + contentDescription = null + ) + Text( + modifier = Modifier.padding(start = 6.dp), + text = "View changes" + ) + } + } + } + if (timelineItem.thumbnailUrl != null) { + val request = + ImageService.getRequest(LocalContext.current, url = timelineItem.thumbnailUrl) + AsyncImage( + model = request, + placeholder = BrushPainter(SolidColor(WikipediaTheme.colors.borderColor)), + error = BrushPainter(SolidColor(WikipediaTheme.colors.borderColor)), + contentScale = ContentScale.Crop, + contentDescription = null, + modifier = Modifier + .size(56.dp) + .clip(RoundedCornerShape(8.dp)) + ) + } + } +} + +@Composable +fun TimelineModuleEmptyView(modifier: Modifier = Modifier) { + Column( + modifier = modifier, + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text( + modifier = Modifier + .fillMaxWidth() + .padding(bottom = 48.dp), + text = stringResource(R.string.activity_tab_timeline_today), + fontWeight = FontWeight.Bold, + style = MaterialTheme.typography.titleLarge, + color = WikipediaTheme.colors.primaryColor + ) + Image( + modifier = Modifier + .size(164.dp), + painter = painterResource(R.drawable.illustration_activity_tab_empty), + contentDescription = null + ) + Text( + modifier = Modifier.padding(top = 16.dp), + text = stringResource(R.string.activity_tab_timeline_empty_state_title), + style = MaterialTheme.typography.titleSmall, + textAlign = TextAlign.Center, + color = WikipediaTheme.colors.primaryColor + ) + Text( + modifier = Modifier.padding(top = 4.dp), + text = stringResource(R.string.activity_tab_timeline_empty_state_message), + style = MaterialTheme.typography.bodyMedium, + textAlign = TextAlign.Center, + color = WikipediaTheme.colors.primaryColor + ) + } +} + +// Date Separator Composable +@Composable +fun TimelineDateSeparator( + date: Date, + modifier: Modifier = Modifier +) { + val dateText = when { + date.isToday() -> stringResource(R.string.activity_tab_timeline_today) + date.isYesterday() -> stringResource(R.string.activity_tab_timeline_yesterday) + else -> DateUtil.toRelativeDateString(date) + } + + Column( + modifier = modifier + .fillMaxWidth() + ) { + Text( + text = dateText, + style = MaterialTheme.typography.titleLarge, + fontWeight = FontWeight.Bold, + color = WikipediaTheme.colors.primaryColor + ) + Text( + text = DateUtil.getMMMMdYYYY(date, false), + style = MaterialTheme.typography.bodySmall, + color = WikipediaTheme.colors.secondaryColor + ) + } +} + +@Preview +@Composable +private fun TimelineItemPreview() { + BaseTheme( + currentTheme = Theme.LIGHT + ) { + TimelineModule( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 8.dp), + timelineItem = TimelineItem( + id = 1, + pageId = 1, + displayTitle = "1980s professional wrestling boxing", + description = "Era of professional wrestling", + thumbnailUrl = "", + timestamp = Date(), + source = 1, + activitySource = ActivitySource.EDIT + ), + onItemClick = {} + ) + } +} + +@Preview +@Composable +private fun TimelineDateSeparatorPreview() { + BaseTheme( + currentTheme = Theme.LIGHT + ) { + TimelineDateSeparator( + date = Date() + ) + } +} diff --git a/app/src/main/java/org/wikipedia/activitytab/timeline/TimelinePagingSource.kt b/app/src/main/java/org/wikipedia/activitytab/timeline/TimelinePagingSource.kt new file mode 100644 index 00000000000..5557beb2112 --- /dev/null +++ b/app/src/main/java/org/wikipedia/activitytab/timeline/TimelinePagingSource.kt @@ -0,0 +1,42 @@ +package org.wikipedia.activitytab.timeline + +import androidx.paging.PagingSource +import androidx.paging.PagingState + +class TimelinePagingSource( + private val sources: List +) : PagingSource() { + override fun getRefreshKey(state: PagingState): TimelinePageKey? { + return null + } + + override suspend fun load(params: LoadParams): LoadResult { + return try { + val key = params.key ?: TimelinePageKey() + val allItems = mutableListOf() + val newCursors = mutableMapOf() + + sources.forEach { source -> + val sourceName = source.id + val cursor = key.cursors[sourceName] + if (cursor != null || key.cursors.isEmpty()) { + val (items, nextCursor) = source.fetch(params.loadSize, cursor) + allItems.addAll(items) + nextCursor?.let { newCursors[sourceName] = it } + } + } + + val merged = allItems + .distinctBy { it.id } + .sortedByDescending { it.timestamp } + + LoadResult.Page( + data = merged, + prevKey = null, + nextKey = if (newCursors.isEmpty()) null else TimelinePageKey(newCursors) + ) + } catch (e: Exception) { + LoadResult.Error(e) + } + } +} diff --git a/app/src/main/java/org/wikipedia/activitytab/timeline/TimelineRepository.kt b/app/src/main/java/org/wikipedia/activitytab/timeline/TimelineRepository.kt new file mode 100644 index 00000000000..9ded1761ee8 --- /dev/null +++ b/app/src/main/java/org/wikipedia/activitytab/timeline/TimelineRepository.kt @@ -0,0 +1,196 @@ +package org.wikipedia.activitytab.timeline + +import org.wikipedia.dataclient.ServiceFactory +import org.wikipedia.dataclient.WikiSite +import org.wikipedia.history.HistoryEntry +import org.wikipedia.history.HistoryEntry.Companion.SOURCE_SEARCH +import org.wikipedia.history.db.HistoryEntryWithImageDao +import org.wikipedia.page.Namespace +import org.wikipedia.page.PageTitle +import org.wikipedia.readinglist.db.ReadingListPageDao +import java.time.ZoneId +import java.util.Date + +interface TimelineSource { + val id: String + suspend fun fetch(pageSize: Int, cursor: Cursor?): Pair, Cursor?> +} + +class HistoryEntryPagingSource( + private val dao: HistoryEntryWithImageDao +) : TimelineSource { + + override val id: String = "history_entry" + + override suspend fun fetch(pageSize: Int, cursor: Cursor?): Pair, Cursor?> { + val offset = (cursor as? Cursor.HistoryEntryCursor)?.offset ?: 0 + val items = dao.getHistoryEntriesWithOffset(pageSize, offset).map { + TimelineItem( + id = it.id, + pageId = 0, + authority = it.authority, + apiTitle = it.apiTitle, + displayTitle = it.displayTitle, + description = it.description, + thumbnailUrl = it.imageName, + timestamp = it.timestamp, + namespace = it.namespace, + lang = it.lang, + source = it.source, + activitySource = when (it.source) { + SOURCE_SEARCH -> ActivitySource.SEARCH + else -> ActivitySource.LINK + } + ) + } + val nextCursor = + if (items.size < pageSize) null else Cursor.HistoryEntryCursor(offset + items.size) + return items to nextCursor + } +} + +class UserContribPagingSource( + private val wikiSite: WikiSite, + private val userName: String, + private val historyEntryWithImageDao: HistoryEntryWithImageDao +) : TimelineSource { + + private val MAX_BATCH_SIZE = 50 + + override val id: String = "user_contrib" + + override suspend fun fetch(pageSize: Int, cursor: Cursor?): Pair, Cursor?> { + val token = (cursor as? Cursor.UserContribCursor)?.token + val service = ServiceFactory.get(wikiSite) + val userContribResponse = service.getUserContrib(username = userName, maxCount = pageSize, ns = null, filter = null, uccontinue = token, ucdir = "older") + + val missingPageInfoIds = mutableListOf() + val timelineItemsByPageId = mutableMapOf() + userContribResponse.query?.userContributions?.forEach { contribution -> + // pageId for article namespace and revid for other namespace as key because they can have similar pageId for example (User talk namespace) + // Only check database cache for article namespace article + val savedHistoryItem = if (contribution.ns == Namespace.MAIN.code()) historyEntryWithImageDao.getHistoryItemWIthImage(contribution.title).firstOrNull() else null + val keyForMap = savedHistoryItem?.id ?: contribution.revid + val timelineItem = TimelineItem( + id = contribution.revid, + pageId = contribution.pageid, + apiTitle = contribution.title, + displayTitle = contribution.title, + description = savedHistoryItem?.description, + thumbnailUrl = savedHistoryItem?.imageName, + timestamp = Date.from(contribution.parsedDateTime.atZone(ZoneId.systemDefault()).toInstant()), + activitySource = ActivitySource.EDIT, + source = -1 + ) + timelineItemsByPageId[keyForMap] = timelineItem + // only fetch page info for contribution in article namespace + if (contribution.ns == Namespace.MAIN.code() && savedHistoryItem == null) { + missingPageInfoIds.add(contribution.pageid) + } + } + + // Fetching missing page info in batches + missingPageInfoIds.chunked(MAX_BATCH_SIZE).forEach { batch -> + val pages = service.getInfoByPageIdsOrTitles(pageIds = batch.joinToString(separator = "|")).query?.pages.orEmpty() + pages.forEach { page -> + timelineItemsByPageId[page.pageId.toLong()]?.let { existingItem -> + timelineItemsByPageId[page.pageId.toLong()] = existingItem.copy( + description = page.description, + thumbnailUrl = page.thumbUrl() + ) + } + } + } + + val items = timelineItemsByPageId.values.sortedByDescending { it.timestamp } + + val nextCursor = userContribResponse.continuation?.ucContinuation?.let { Cursor.UserContribCursor(it) } + return items to nextCursor + } +} + +class ReadingListPagingSource( + val dao: ReadingListPageDao +) : TimelineSource { + + override val id: String = "reading_list" + + override suspend fun fetch(pageSize: Int, cursor: Cursor?): Pair, Cursor?> { + val offset = (cursor as? Cursor.ReadingListCursor)?.offset ?: 0 + val items = dao.getPagesWithLimitOffset(pageSize, offset).map { + TimelineItem( + id = it.mtime + it.atime + it.id, + pageId = 0, + apiTitle = it.apiTitle, + displayTitle = it.displayTitle, + description = it.description, + thumbnailUrl = it.thumbUrl, + timestamp = Date(it.mtime), + wiki = WikiSite.forLanguageCode(it.lang), + activitySource = ActivitySource.BOOKMARKED, + source = -1 + ) + } + val nextCursor = + if (items.size < pageSize) null else Cursor.HistoryEntryCursor(offset + items.size) + return items to nextCursor + } +} + +// Data Models +enum class ActivitySource { + EDIT, SEARCH, LINK, BOOKMARKED +} + +data class TimelineItem( + val id: Long, + val pageId: Int, + val description: String?, + val thumbnailUrl: String?, + val timestamp: Date, + val source: Int, + val activitySource: ActivitySource?, + var authority: String = "", + var lang: String = "", + var apiTitle: String = "", + var displayTitle: String = "", + var namespace: String = "", + val wiki: WikiSite? = null +) { + fun toHistoryEntry(): HistoryEntry { + val entry = HistoryEntry( + authority = authority, + lang = lang, + apiTitle = apiTitle, + displayTitle = displayTitle, + id = id, + namespace = namespace, + timestamp = timestamp, + source = source + ) + entry.title.thumbUrl = thumbnailUrl + entry.title.description = description + + return entry + } + + fun toPageTitle(): PageTitle { + return PageTitle( + apiTitle, + wiki!!, + thumbnailUrl, + description, + displayTitle + ) + } +} + +sealed class Cursor { + data class UserContribCursor(val token: String?) : Cursor() + data class HistoryEntryCursor(val offset: Int) : Cursor() + data class ReadingListCursor(val offset: Int) : Cursor() +} + +data class TimelinePageKey( + val cursors: Map = emptyMap() +) diff --git a/app/src/main/java/org/wikipedia/dataclient/Service.kt b/app/src/main/java/org/wikipedia/dataclient/Service.kt index 3cc8dc704e5..de72d024b9b 100644 --- a/app/src/main/java/org/wikipedia/dataclient/Service.kt +++ b/app/src/main/java/org/wikipedia/dataclient/Service.kt @@ -438,7 +438,8 @@ interface Service { @Query("uclimit") maxCount: Int, @Query("ucnamespace") ns: String?, @Query("ucshow") filter: String?, - @Query("uccontinue") uccontinue: String? + @Query("uccontinue") uccontinue: String?, + @Query("ucdir") ucdir: String? = null ): MwQueryResponse @GET(MW_API_PREFIX + "action=query&list=usercontribs&meta=userinfo&uiprop=editcount") diff --git a/app/src/main/java/org/wikipedia/extensions/Date.kt b/app/src/main/java/org/wikipedia/extensions/Date.kt new file mode 100644 index 00000000000..32678b62b9d --- /dev/null +++ b/app/src/main/java/org/wikipedia/extensions/Date.kt @@ -0,0 +1,17 @@ +package org.wikipedia.extensions + +import java.time.LocalDate +import java.time.ZoneId +import java.util.Date + +fun Date.toLocalDate(): LocalDate { + return this.toInstant().atZone(ZoneId.systemDefault()).toLocalDate() +} + +fun Date.isToday(): Boolean { + return this.toLocalDate() == LocalDate.now() +} + +fun Date.isYesterday(): Boolean { + return this.toLocalDate() == LocalDate.now().minusDays(1) +} diff --git a/app/src/main/java/org/wikipedia/history/db/HistoryEntryWithImage.kt b/app/src/main/java/org/wikipedia/history/db/HistoryEntryWithImage.kt index a690b1d0aee..615a36472da 100644 --- a/app/src/main/java/org/wikipedia/history/db/HistoryEntryWithImage.kt +++ b/app/src/main/java/org/wikipedia/history/db/HistoryEntryWithImage.kt @@ -14,5 +14,6 @@ data class HistoryEntryWithImage( val imageName: String?, val description: String?, val geoLat: Double?, - val geoLon: Double? + val geoLon: Double?, + val id: Long, ) diff --git a/app/src/main/java/org/wikipedia/history/db/HistoryEntryWithImageDao.kt b/app/src/main/java/org/wikipedia/history/db/HistoryEntryWithImageDao.kt index ebce65badba..840fed510f0 100644 --- a/app/src/main/java/org/wikipedia/history/db/HistoryEntryWithImageDao.kt +++ b/app/src/main/java/org/wikipedia/history/db/HistoryEntryWithImageDao.kt @@ -17,10 +17,15 @@ import java.util.TimeZone @Dao interface HistoryEntryWithImageDao { + @Query("SELECT HistoryEntry.*, PageImage.imageName, PageImage.description, PageImage.geoLat, PageImage.geoLon, PageImage.timeSpentSec FROM HistoryEntry LEFT OUTER JOIN PageImage ON (HistoryEntry.namespace = PageImage.namespace AND HistoryEntry.apiTitle = PageImage.apiTitle AND HistoryEntry.lang = PageImage.lang) INNER JOIN (SELECT lang, apiTitle, MAX(timestamp) as max_timestamp FROM HistoryEntry GROUP BY lang, apiTitle) LatestEntries ON HistoryEntry.apiTitle = LatestEntries.apiTitle AND HistoryEntry.timestamp = LatestEntries.max_timestamp ORDER BY timestamp DESC LIMIT :limit OFFSET :offset") + suspend fun getHistoryEntriesWithOffset( + limit: Int, + offset: Int + ): List // TODO: convert to PagingSource. // https://developer.android.com/topic/libraries/architecture/paging/v3-overview - @Query("SELECT HistoryEntry.*, PageImage.imageName, PageImage.description, PageImage.geoLat, PageImage.geoLon, PageImage.timeSpentSec FROM HistoryEntry LEFT OUTER JOIN PageImage ON (HistoryEntry.namespace = PageImage.namespace AND HistoryEntry.apiTitle = PageImage.apiTitle AND HistoryEntry.lang = PageImage.lang) INNER JOIN(SELECT lang, apiTitle, MAX(timestamp) as max_timestamp FROM HistoryEntry GROUP BY lang, apiTitle) LatestEntries ON HistoryEntry.apiTitle = LatestEntries.apiTitle AND HistoryEntry.timestamp = LatestEntries.max_timestamp WHERE UPPER(HistoryEntry.displayTitle) LIKE UPPER(:term) ESCAPE '\\' ORDER BY timestamp DESC") + @Query("SELECT HistoryEntry.*, PageImage.imageName, PageImage.description, PageImage.geoLat, PageImage.geoLon, PageImage.timeSpentSec FROM HistoryEntry LEFT OUTER JOIN PageImage ON (HistoryEntry.namespace = PageImage.namespace AND HistoryEntry.apiTitle = PageImage.apiTitle AND HistoryEntry.lang = PageImage.lang) INNER JOIN (SELECT lang, apiTitle, MAX(timestamp) as max_timestamp FROM HistoryEntry GROUP BY lang, apiTitle) LatestEntries ON HistoryEntry.apiTitle = LatestEntries.apiTitle AND HistoryEntry.timestamp = LatestEntries.max_timestamp WHERE UPPER(HistoryEntry.displayTitle) LIKE UPPER(:term) ESCAPE '\\' ORDER BY timestamp DESC") @RewriteQueriesToDropUnusedColumns suspend fun findEntriesBySearchTerm(term: String): List @@ -84,6 +89,12 @@ interface HistoryEntryWithImageDao { return entries.map { toHistoryEntry(it) } } + suspend fun getHistoryItemWIthImage(searchTerm: String): List { + val normalizedQuery = StringUtils.stripAccents(searchTerm).replace("\\", "\\\\") + .replace("%", "\\%").replace("_", "\\_") + return findEntriesBySearchTerm("%$normalizedQuery%") + } + private fun normalizedQuery(searchQuery: String): String { return StringUtils.stripAccents(searchQuery).lowercase(Locale.getDefault()) .replace("\\", "\\\\") @@ -97,7 +108,7 @@ interface HistoryEntryWithImageDao { lang = entryWithImage.lang, apiTitle = entryWithImage.apiTitle, displayTitle = entryWithImage.displayTitle, - id = 0, + id = entryWithImage.id, namespace = entryWithImage.namespace, timestamp = entryWithImage.timestamp, source = entryWithImage.source diff --git a/app/src/main/java/org/wikipedia/readinglist/db/ReadingListPageDao.kt b/app/src/main/java/org/wikipedia/readinglist/db/ReadingListPageDao.kt index 60f49e7c7da..35b896922d5 100644 --- a/app/src/main/java/org/wikipedia/readinglist/db/ReadingListPageDao.kt +++ b/app/src/main/java/org/wikipedia/readinglist/db/ReadingListPageDao.kt @@ -95,6 +95,9 @@ interface ReadingListPageDao { @Query("SELECT * FROM ReadingListPage ORDER BY mtime DESC LIMIT 1") suspend fun getMostRecentSavedPage(): ReadingListPage? + @Query("SELECT * FROM ReadingListPage ORDER BY mtime DESC LIMIT :limit OFFSET :offset") + suspend fun getPagesWithLimitOffset(limit: Int, offset: Int): List + suspend fun getAllPagesToBeSaved() = getPagesByStatus(ReadingListPage.STATUS_QUEUE_FOR_SAVE, true) suspend fun getAllPagesToBeForcedSave() = getPagesByStatus(ReadingListPage.STATUS_QUEUE_FOR_FORCED_SAVE, true) diff --git a/app/src/main/java/org/wikipedia/util/DateUtil.kt b/app/src/main/java/org/wikipedia/util/DateUtil.kt index 90234626f25..85028ef2fee 100644 --- a/app/src/main/java/org/wikipedia/util/DateUtil.kt +++ b/app/src/main/java/org/wikipedia/util/DateUtil.kt @@ -8,6 +8,7 @@ import org.wikipedia.R import org.wikipedia.WikipediaApp import org.wikipedia.extensions.getResources import org.wikipedia.extensions.getString +import org.wikipedia.extensions.toLocalDate import org.wikipedia.feed.model.UtcDate import java.text.SimpleDateFormat import java.time.Instant @@ -18,6 +19,8 @@ import java.time.ZoneOffset import java.time.ZonedDateTime import java.time.format.DateTimeFormatter import java.time.format.FormatStyle +import java.time.format.TextStyle +import java.time.temporal.ChronoUnit import java.time.temporal.TemporalAccessor import java.util.Calendar import java.util.Date @@ -67,6 +70,10 @@ object DateUtil { return getMonthOnlyDateString(SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).parse(dateStr)!!) } + fun getMonthOnlyDateString(date: LocalDate): String { + return getDateStringWithSkeletonPattern(date, "MMMM d") + } + fun getMonthOnlyWithoutDayDateString(date: Date): String { return getDateStringWithSkeletonPattern(date, "MMMM") } @@ -79,8 +86,8 @@ object DateUtil { return getCachedDateFormat("yyyyMMdd", Locale.ROOT, true).format(date) } - fun getMMMMdYYYY(date: Date): String { - return getCachedDateFormat("MMMM d, yyyy", Locale.getDefault(), true).format(date) + fun getMMMMdYYYY(date: Date, utc: Boolean = true): String { + return getCachedDateFormat("MMMM d, yyyy", Locale.getDefault(), utc).format(date) } private fun getExtraShortDateString(date: Date): String { @@ -207,4 +214,25 @@ object DateUtil { val zonedDateTime = ZonedDateTime.ofInstant(Instant.ofEpochMilli(epochMilli), zoneId) return zonedDateTime.year } + + fun toRelativeDateString(date: Date): String { + val localDate = date.toLocalDate() + val now = LocalDate.now() + val daysDiff = ChronoUnit.DAYS.between(localDate, now) + + return when { + daysDiff < 7 -> { + val dayOfWeek = localDate.dayOfWeek.getDisplayName(TextStyle.FULL, Locale.getDefault()) + dayOfWeek + } + + localDate.year == now.year -> { + DateUtil.getMonthOnlyDateString(localDate) + } + + else -> { + DateUtil.getShortDateString(localDate) + } + } + } } diff --git a/app/src/main/res/drawable/outline_difference_24.xml b/app/src/main/res/drawable/outline_difference_24.xml new file mode 100644 index 00000000000..13241cadfae --- /dev/null +++ b/app/src/main/res/drawable/outline_difference_24.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/values-qq/strings.xml b/app/src/main/res/values-qq/strings.xml index 874afc9f213..d8334fc26a5 100644 --- a/app/src/main/res/values-qq/strings.xml +++ b/app/src/main/res/values-qq/strings.xml @@ -1238,6 +1238,10 @@ Title of the switch for the Timeline module in the Activity Tab customization screen. Title of the empty state when all modules are turned off. Message of the empty state when all modules are turned off. + Date separator label for today\'s entries in activity timeline. + Date separator label for yesterday\'s entries in activity timeline. + Title displayed when the timeline is empty. + Message shown when there are no items in the timeline. Title shown at the top of the activity for the file page. Button label to add image caption for the file. Button label to add image tags for the file. diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 3ddd450e652..d9800b8fd4a 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1260,6 +1260,10 @@ Timeline of behavior Your Activity Tab modules are off Turn on Activity Tab modules to see your reading history, editing history, and more. + Today + Yesterday + Nothing to show + Start reading and editing to build your history diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 939165f58f1..c7ac22d732e 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -34,6 +34,7 @@ mockitoVersion = "5.2.0" navigationCompose = "2.9.3" okHttpVersion = "5.1.0" orchestrator = "1.6.1" +pagingCompose = "3.3.6" pagingRuntimeKtx = "3.3.6" paletteKtx = "1.0.0" photoview = "1.0.3" @@ -102,6 +103,7 @@ mockwebserver = { module = "com.squareup.okhttp3:mockwebserver", version.ref = " okhttp-tls = { module = "com.squareup.okhttp3:okhttp-tls", version.ref = "okHttpVersion" } okhttp3-logging-interceptor = { module = "com.squareup.okhttp3:logging-interceptor", version.ref = "okHttpVersion" } okhttp3-okhttp = { module = "com.squareup.okhttp3:okhttp", version.ref = "okHttpVersion" } +paging-compose = { module = "androidx.paging:paging-compose", version.ref = "pagingCompose" } palette-ktx = { module = "androidx.palette:palette-ktx", version.ref = "paletteKtx" } paging-runtime-ktx = { module = "androidx.paging:paging-runtime-ktx", version.ref = "pagingRuntimeKtx" } photoview = { module = "io.getstream:photoview", version.ref = "photoview" } From 418b14552b6e2a264279e5cb92c000494d167fd4 Mon Sep 17 00:00:00 2001 From: Cooltey Feng Date: Wed, 3 Sep 2025 13:08:42 -0700 Subject: [PATCH 61/70] Empty state of Activity tab update (#5893) --- .../activitytab/ActivityTabFragment.kt | 17 +++++++---------- .../wikipedia/compose/components/HtmlText.kt | 5 ++++- app/src/main/res/values-qq/strings.xml | 3 +-- app/src/main/res/values/strings.xml | 3 +-- 4 files changed, 13 insertions(+), 15 deletions(-) diff --git a/app/src/main/java/org/wikipedia/activitytab/ActivityTabFragment.kt b/app/src/main/java/org/wikipedia/activitytab/ActivityTabFragment.kt index 974e56be756..80e047fbec3 100644 --- a/app/src/main/java/org/wikipedia/activitytab/ActivityTabFragment.kt +++ b/app/src/main/java/org/wikipedia/activitytab/ActivityTabFragment.kt @@ -74,6 +74,7 @@ import org.wikipedia.activitytab.timeline.TimelineModuleEmptyView import org.wikipedia.auth.AccountUtil import org.wikipedia.categories.CategoryActivity import org.wikipedia.categories.db.Category +import org.wikipedia.compose.components.HtmlText import org.wikipedia.compose.components.error.WikiErrorClickEvents import org.wikipedia.compose.theme.BaseTheme import org.wikipedia.compose.theme.WikipediaTheme @@ -283,19 +284,15 @@ class ActivityTabFragment : Fragment() { painter = painterResource(R.drawable.illustration_activity_tab_empty), contentDescription = null ) - Text( - modifier = Modifier.padding(top = 16.dp), - text = stringResource(R.string.activity_tab_customize_screen_no_modules_title), - style = MaterialTheme.typography.titleSmall, - textAlign = TextAlign.Center, - color = WikipediaTheme.colors.primaryColor - ) - Text( - modifier = Modifier.padding(top = 4.dp), + HtmlText( + modifier = Modifier.padding(vertical = 16.dp), text = stringResource(R.string.activity_tab_customize_screen_no_modules_message), style = MaterialTheme.typography.bodyMedium, textAlign = TextAlign.Center, - color = WikipediaTheme.colors.primaryColor + color = WikipediaTheme.colors.primaryColor, + linkInteractionListener = { + startActivity(ActivityTabCustomizationActivity.newIntent(requireContext())) + } ) } return@Scaffold diff --git a/app/src/main/java/org/wikipedia/compose/components/HtmlText.kt b/app/src/main/java/org/wikipedia/compose/components/HtmlText.kt index 92dee2d2a3e..83fd52d22f0 100644 --- a/app/src/main/java/org/wikipedia/compose/components/HtmlText.kt +++ b/app/src/main/java/org/wikipedia/compose/components/HtmlText.kt @@ -9,6 +9,7 @@ import androidx.compose.ui.text.LinkInteractionListener import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.TextLinkStyles import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.TextUnit @@ -37,7 +38,8 @@ fun HtmlText( maxLines: Int = Int.MAX_VALUE, overflow: TextOverflow = TextOverflow.Ellipsis, lineHeight: TextUnit = 1.6.em, - linkInteractionListener: LinkInteractionListener? = null + linkInteractionListener: LinkInteractionListener? = null, + textAlign: TextAlign = TextAlign.Start ) { Text( modifier = modifier, @@ -51,6 +53,7 @@ fun HtmlText( color = color, maxLines = maxLines, overflow = overflow, + textAlign = textAlign ) } diff --git a/app/src/main/res/values-qq/strings.xml b/app/src/main/res/values-qq/strings.xml index d8334fc26a5..10d75043ded 100644 --- a/app/src/main/res/values-qq/strings.xml +++ b/app/src/main/res/values-qq/strings.xml @@ -1236,8 +1236,7 @@ Title of the switch for the Games module in the Activity Tab customization screen. Title of the switch for the Donations module in the Activity Tab customization screen. Title of the switch for the Timeline module in the Activity Tab customization screen. - Title of the empty state when all modules are turned off. - Message of the empty state when all modules are turned off. + Message of the empty state when all modules are turned off. (Please preserve the anchor tag in its exact form) Date separator label for today\'s entries in activity timeline. Date separator label for yesterday\'s entries in activity timeline. Title displayed when the timeline is empty. diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index d9800b8fd4a..11374445f73 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1258,8 +1258,7 @@ Game stats Last in app donation Timeline of behavior - Your Activity Tab modules are off - Turn on Activity Tab modules to see your reading history, editing history, and more. + Switch them on to see updates in this tab.]]> Today Yesterday Nothing to show From 1968787b5ecab1c3fa88e30ebaec9fe5274abda5 Mon Sep 17 00:00:00 2001 From: Cooltey Feng Date: Wed, 3 Sep 2025 13:09:22 -0700 Subject: [PATCH 62/70] Update Contributions this month logic for design updates (#5894) --- .../activitytab/EditingInsightsModule.kt | 136 +++++++++--------- app/src/main/res/values/strings.xml | 2 +- 2 files changed, 70 insertions(+), 68 deletions(-) diff --git a/app/src/main/java/org/wikipedia/activitytab/EditingInsightsModule.kt b/app/src/main/java/org/wikipedia/activitytab/EditingInsightsModule.kt index fc9d63e8473..09bb959545f 100644 --- a/app/src/main/java/org/wikipedia/activitytab/EditingInsightsModule.kt +++ b/app/src/main/java/org/wikipedia/activitytab/EditingInsightsModule.kt @@ -87,15 +87,6 @@ fun EditingInsightsModule( } } is UiState.Success -> { - if (uiState.data.totalEditsCount == 0) { - SuggestedEditsCard( - modifier = modifier.fillMaxWidth(), - onClick = { - onSuggestedEditsClick() - } - ) - return - } MostViewedCard( modifier = modifier .fillMaxWidth(), @@ -112,6 +103,9 @@ fun EditingInsightsModule( editsLastMonth = uiState.data.editsLastMonth, onContributionClick = { onContributionClick() + }, + onSuggestedEditsClick = { + onSuggestedEditsClick() } ) } @@ -285,6 +279,7 @@ fun ContributionCard( editsThisMonth: Int, editsLastMonth: Int, onContributionClick: (() -> Unit)? = null, + onSuggestedEditsClick: (() -> Unit)? = null ) { WikiCard( modifier = modifier, @@ -323,12 +318,14 @@ fun ContributionCard( color = WikipediaTheme.colors.primaryColor ) } - Text( - text = lastEditRelativeTime, - modifier = Modifier.padding(top = 4.dp), - style = MaterialTheme.typography.bodySmall, - color = WikipediaTheme.colors.secondaryColor - ) + if (editsThisMonth > 0) { + Text( + text = lastEditRelativeTime, + modifier = Modifier.padding(top = 4.dp), + style = MaterialTheme.typography.bodySmall, + color = WikipediaTheme.colors.secondaryColor + ) + } } Icon( modifier = Modifier.size(24.dp), @@ -371,68 +368,70 @@ fun ContributionCard( barColor = WikipediaTheme.colors.borderColor ) } + + if (editsThisMonth == 0) { + + HorizontalDivider( + Modifier.padding(horizontal = 16.dp), + color = WikipediaTheme.colors.borderColor + ) + + SuggestedEditsView( + modifier = modifier.fillMaxWidth(), + onClick = { + onSuggestedEditsClick?.invoke() + } + ) + } } } } @Composable -fun SuggestedEditsCard( +fun SuggestedEditsView( modifier: Modifier = Modifier, onClick: (() -> Unit)? = null ) { - WikiCard( - modifier = modifier, - colors = CardDefaults.cardColors( - containerColor = WikipediaTheme.colors.paperColor, - contentColor = WikipediaTheme.colors.paperColor - ), - elevation = 0.dp, - border = BorderStroke( - width = 1.dp, - color = WikipediaTheme.colors.borderColor - ), + Column( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + horizontalAlignment = Alignment.CenterHorizontally, ) { - Column( - modifier = Modifier - .fillMaxWidth() - .padding(16.dp), - horizontalAlignment = Alignment.CenterHorizontally, + Text( + modifier = Modifier.padding(bottom = 8.dp), + text = stringResource(R.string.activity_tab_impact_suggested_edits_title), + style = MaterialTheme.typography.titleSmall.copy( + fontWeight = FontWeight.Medium + ), + color = WikipediaTheme.colors.primaryColor + ) + Text( + textAlign = TextAlign.Center, + text = stringResource(R.string.activity_tab_impact_suggested_edits_message), + style = MaterialTheme.typography.bodyMedium, + color = WikipediaTheme.colors.primaryColor + ) + Button( + modifier = Modifier.padding(top = 16.dp).align(Alignment.CenterHorizontally), + contentPadding = PaddingValues(horizontal = 18.dp), + colors = ButtonDefaults.buttonColors( + containerColor = WikipediaTheme.colors.progressiveColor, + contentColor = WikipediaTheme.colors.paperColor, + ), + onClick = { onClick?.invoke() }, ) { - Text( - modifier = Modifier.padding(bottom = 8.dp), - text = stringResource(R.string.activity_tab_impact_suggested_edits_title), - style = MaterialTheme.typography.titleSmall.copy( - fontWeight = FontWeight.Medium - ), - color = WikipediaTheme.colors.primaryColor + Icon( + modifier = Modifier.size(20.dp), + painter = painterResource(R.drawable.ic_mode_edit_white_24dp), + tint = WikipediaTheme.colors.paperColor, + contentDescription = null ) Text( - textAlign = TextAlign.Center, - text = stringResource(R.string.activity_tab_impact_suggested_edits_message), - style = MaterialTheme.typography.bodyMedium, - color = WikipediaTheme.colors.primaryColor + modifier = Modifier.padding(start = 6.dp, top = 4.dp, bottom = 4.dp), + text = stringResource(R.string.activity_tab_impact_suggested_edits_button), + style = MaterialTheme.typography.labelLarge ) - Button( - modifier = Modifier.padding(top = 16.dp).align(Alignment.CenterHorizontally), - contentPadding = PaddingValues(horizontal = 18.dp), - colors = ButtonDefaults.buttonColors( - containerColor = WikipediaTheme.colors.progressiveColor, - contentColor = WikipediaTheme.colors.paperColor, - ), - onClick = { onClick?.invoke() }, - ) { - Icon( - modifier = Modifier.size(20.dp), - painter = painterResource(R.drawable.ic_mode_edit_white_24dp), - tint = WikipediaTheme.colors.paperColor, - contentDescription = null - ) - Text( - modifier = Modifier.padding(start = 6.dp, top = 4.dp, bottom = 4.dp), - text = stringResource(R.string.activity_tab_impact_suggested_edits_button), - style = MaterialTheme.typography.labelLarge - ) - } } } } @@ -527,13 +526,16 @@ private fun ContributionCardPreview() { @Preview @Composable -private fun SuggestedEditsCardPreview() { +private fun ContributionCardWithSuggestedEditsPreview() { BaseTheme( currentTheme = Theme.LIGHT ) { - SuggestedEditsCard( + ContributionCard( modifier = Modifier.fillMaxWidth(), - onClick = {} + lastEditRelativeTime = "2024 Sep 1", + editsThisMonth = 0, + editsLastMonth = 0, + onContributionClick = null ) } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 11374445f73..75bff7d7510 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1240,7 +1240,7 @@ Best streak Last edited 0 edits to articles recently - Help extend free knowledge to the world by editing topics that matter most to you. + Looks like you haven\'t made an edit this month. Extend free knowledge by editing topics that matter most to you. Make an edit Your recent activity (last 30 days) From 85e822af23e12524fc7dd412f923b6d9beafa09c Mon Sep 17 00:00:00 2001 From: Cooltey Feng Date: Wed, 3 Sep 2025 13:10:19 -0700 Subject: [PATCH 63/70] Update WikiGames entry button for the Game module (#5895) --- .../activitytab/ActivityTabFragment.kt | 2 +- .../wikipedia/activitytab/WikiGamesModule.kt | 232 +++++++----------- app/src/main/res/values-qq/strings.xml | 1 + app/src/main/res/values/strings.xml | 1 + 4 files changed, 97 insertions(+), 139 deletions(-) diff --git a/app/src/main/java/org/wikipedia/activitytab/ActivityTabFragment.kt b/app/src/main/java/org/wikipedia/activitytab/ActivityTabFragment.kt index 80e047fbec3..1c3bd021b08 100644 --- a/app/src/main/java/org/wikipedia/activitytab/ActivityTabFragment.kt +++ b/app/src/main/java/org/wikipedia/activitytab/ActivityTabFragment.kt @@ -454,7 +454,7 @@ class ActivityTabFragment : Fragment() { .fillMaxWidth() .padding(start = 16.dp, end = 16.dp, top = 16.dp), uiState = wikiGamesUiState, - onEntryCardClick = { + onPlayGameCardClick = { requireActivity().startActivity(OnThisDayGameActivity.newIntent( context = requireContext(), invokeSource = Constants.InvokeSource.ACTIVITY_TAB, diff --git a/app/src/main/java/org/wikipedia/activitytab/WikiGamesModule.kt b/app/src/main/java/org/wikipedia/activitytab/WikiGamesModule.kt index f14110e06fd..2b6c3845278 100644 --- a/app/src/main/java/org/wikipedia/activitytab/WikiGamesModule.kt +++ b/app/src/main/java/org/wikipedia/activitytab/WikiGamesModule.kt @@ -1,17 +1,18 @@ package org.wikipedia.activitytab import androidx.compose.foundation.BorderStroke -import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.CardDefaults import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.Icon @@ -23,13 +24,10 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.pluralStringResource import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp import org.wikipedia.R -import org.wikipedia.compose.components.HtmlText import org.wikipedia.compose.components.WikiCard import org.wikipedia.compose.components.error.WikiErrorClickEvents import org.wikipedia.compose.components.error.WikiErrorView @@ -43,51 +41,52 @@ import org.wikipedia.util.UiState fun WikiGamesModule( modifier: Modifier = Modifier, uiState: UiState, - onEntryCardClick: (() -> Unit)? = null, + onPlayGameCardClick: (() -> Unit)? = null, onStatsCardClick: (() -> Unit)? = null, wikiErrorClickEvents: WikiErrorClickEvents? = null ) { - if (uiState == UiState.Loading) { - Box( - modifier = modifier - .fillMaxWidth() - .height(200.dp) - ) { - CircularProgressIndicator( - modifier = Modifier - .align(Alignment.Center) - .padding(24.dp), - color = WikipediaTheme.colors.progressiveColor - ) - } - } else if (uiState is UiState.Success) { - if (uiState.data == null) { - WikiGamesEntryCard( + when (uiState) { + UiState.Loading -> { + Box( modifier = modifier - .fillMaxWidth(), - onClick = onEntryCardClick - ) - } else { + .fillMaxWidth() + .height(200.dp) + ) { + CircularProgressIndicator( + modifier = Modifier + .align(Alignment.Center) + .padding(24.dp), + color = WikipediaTheme.colors.progressiveColor + ) + } + } + is UiState.Success -> { WikiGamesStatsCard( modifier = modifier .fillMaxWidth(), - gameStatistics = uiState.data, - onClick = onStatsCardClick + totalGamesPlayed = uiState.data?.totalGamesPlayed ?: 0, + currentStreak = uiState.data?.currentStreak ?: 0, + bestStreak = uiState.data?.bestStreak ?: 0, + averageScore = uiState.data?.averageScore ?: 0.0, + onStatsCardClick = onStatsCardClick, + onPlayGameCardClick = onPlayGameCardClick ) } - } else if (uiState is UiState.Error) { - Box( - modifier = modifier - .fillMaxSize(), - contentAlignment = Alignment.Center - ) { - WikiErrorView( - modifier = Modifier - .fillMaxWidth(), - caught = uiState.error, - errorClickEvents = wikiErrorClickEvents, - retryForGenericError = true - ) + + is UiState.Error -> { + Box( + modifier = modifier + .fillMaxSize(), + contentAlignment = Alignment.Center + ) { + WikiErrorView( + modifier = Modifier + .fillMaxWidth(), + caught = uiState.error, + errorClickEvents = wikiErrorClickEvents, + retryForGenericError = true + ) + } } } } @@ -95,8 +94,12 @@ fun WikiGamesModule( @Composable fun WikiGamesStatsCard( modifier: Modifier = Modifier, - gameStatistics: OnThisDayGameViewModel.GameStatistics, - onClick: (() -> Unit)? = null + totalGamesPlayed: Int = 0, + currentStreak: Int = 0, + bestStreak: Int = 0, + averageScore: Double = 0.0, + onStatsCardClick: (() -> Unit)? = null, + onPlayGameCardClick: (() -> Unit)? = null ) { WikiCard( modifier = modifier, @@ -109,7 +112,7 @@ fun WikiGamesStatsCard( width = 1.dp, color = WikipediaTheme.colors.borderColor ), - onClick = onClick + onClick = onStatsCardClick ) { Column( modifier = Modifier.padding(16.dp) @@ -142,13 +145,13 @@ fun WikiGamesStatsCard( WikiGamesStatView( modifier = Modifier.weight(1f), iconResource = R.drawable.baseline_extension_24, - statValue = gameStatistics.totalGamesPlayed.toString(), - statLabel = pluralStringResource(R.plurals.on_this_day_game_stats_games_played, gameStatistics.totalGamesPlayed) + statValue = totalGamesPlayed.toString(), + statLabel = pluralStringResource(R.plurals.on_this_day_game_stats_games_played, totalGamesPlayed) ) WikiGamesStatView( modifier = Modifier.weight(1f), iconResource = R.drawable.outline_motion_blur_24, - statValue = gameStatistics.currentStreak.toString(), + statValue = if (currentStreak == 0) "-" else currentStreak.toString(), statLabel = stringResource(R.string.on_this_day_game_stats_streak) ) } @@ -161,16 +164,33 @@ fun WikiGamesStatsCard( WikiGamesStatView( modifier = Modifier.weight(1f), iconResource = R.drawable.filled_family_star_24, - statValue = gameStatistics.bestStreak.toString(), + statValue = if (bestStreak == 0) "-" else bestStreak.toString(), statLabel = stringResource(R.string.activity_tab_game_stats_best_streak) ) WikiGamesStatView( modifier = Modifier.weight(1f), iconResource = R.drawable.outline_sports_score_24, - statValue = gameStatistics.averageScore.toString(), + statValue = if (averageScore == 0.0) "-" else averageScore.toString(), statLabel = stringResource(R.string.on_this_day_game_stats_average_score) ) } + if (totalGamesPlayed == 0) { + Button( + modifier = Modifier.padding(top = 16.dp).align(Alignment.CenterHorizontally), + contentPadding = PaddingValues(horizontal = 18.dp), + colors = ButtonDefaults.buttonColors( + containerColor = WikipediaTheme.colors.progressiveColor, + contentColor = WikipediaTheme.colors.paperColor, + ), + onClick = { onPlayGameCardClick?.invoke() }, + ) { + Text( + modifier = Modifier.padding(start = 6.dp, top = 4.dp, bottom = 4.dp), + text = stringResource(R.string.activity_tab_play_wiki_games), + style = MaterialTheme.typography.labelLarge + ) + } + } } } } @@ -210,56 +230,7 @@ fun WikiGamesStatView( } } -@Composable -fun WikiGamesEntryCard( - modifier: Modifier, - onClick: (() -> Unit)? = null -) { - WikiCard( - modifier = modifier - .clickable(onClick = { onClick?.invoke() }), - elevation = 4.dp, - colors = CardDefaults.cardColors( - containerColor = WikipediaTheme.colors.progressiveColor, - contentColor = WikipediaTheme.colors.progressiveColor - ) - ) { - Row( - modifier = Modifier - .fillMaxWidth() - .padding(24.dp), - horizontalArrangement = Arrangement.spacedBy(8.dp) - ) { - Column( - modifier = Modifier.weight(1f) - .heightIn(min = 116.dp), - verticalArrangement = Arrangement.spacedBy(8.dp) - ) { - Text( - text = stringResource(R.string.on_this_day_game_title), - style = MaterialTheme.typography.headlineSmall.copy( - lineHeight = 24.sp - ), - color = WikipediaTheme.colors.paperColor, - fontFamily = FontFamily.Serif, - ) - HtmlText( - text = stringResource(R.string.on_this_day_game_splash_message), - style = MaterialTheme.typography.bodyLarge, - color = WikipediaTheme.colors.paperColor - ) - } - Icon( - modifier = Modifier.size(44.dp), - painter = painterResource(R.drawable.ic_today_24px), - tint = WikipediaTheme.colors.paperColor, - contentDescription = null - ) - } - } -} - -@Preview +@Preview(showBackground = true) @Composable private fun WikiGamesModulePreview() { BaseTheme( @@ -269,8 +240,15 @@ private fun WikiGamesModulePreview() { modifier = Modifier .fillMaxWidth() .padding(16.dp), - uiState = UiState.Error(Throwable("Error")), - onEntryCardClick = {}, + uiState = UiState.Success( + OnThisDayGameViewModel.GameStatistics( + totalGamesPlayed = 43, + averageScore = 4.5, + currentStreak = 5, + bestStreak = 15 + ) + ), + onPlayGameCardClick = {}, onStatsCardClick = {}, wikiErrorClickEvents = null ) @@ -279,47 +257,25 @@ private fun WikiGamesModulePreview() { @Preview @Composable -private fun WikiGamesEntryCardPreview() { +private fun WikiGamesModuleWithPlayButtonPreview() { BaseTheme( currentTheme = Theme.LIGHT ) { - WikiGamesEntryCard( - modifier = Modifier, - onClick = {} - ) - } -} - -@Preview -@Composable -private fun WikiGamesStatViewPreview() { - BaseTheme( - currentTheme = Theme.LIGHT - ) { - WikiGamesStatView( - modifier = Modifier, - iconResource = R.drawable.ic_today_24px, - statValue = "42", - statLabel = pluralStringResource(R.plurals.on_this_day_game_stats_games_played, 42) - ) - } -} - -@Preview -@Composable -private fun WikiGamesStatsCardPreview() { - BaseTheme( - currentTheme = Theme.LIGHT - ) { - WikiGamesStatsCard( - modifier = Modifier, - gameStatistics = OnThisDayGameViewModel.GameStatistics( - totalGamesPlayed = 43, - averageScore = 4.5, - currentStreak = 5, - bestStreak = 15 + WikiGamesModule( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + uiState = UiState.Success( + OnThisDayGameViewModel.GameStatistics( + totalGamesPlayed = 0, + averageScore = 0.0, + currentStreak = 0, + bestStreak = 0 + ) ), - onClick = {} + onPlayGameCardClick = {}, + onStatsCardClick = {}, + wikiErrorClickEvents = null ) } } diff --git a/app/src/main/res/values-qq/strings.xml b/app/src/main/res/values-qq/strings.xml index 10d75043ded..77603051995 100644 --- a/app/src/main/res/values-qq/strings.xml +++ b/app/src/main/res/values-qq/strings.xml @@ -1182,6 +1182,7 @@ Label on card that shows the last donation time. Title of the game stats card. Label of the game stats that indicates the best streak. + Button label to go to the WikiGames. Subtitle of the donation card that indicates last donation. Subject heading for sending a report about a problem with the Activity tab feature. Body of email for sending a report about a problem with the Activity tab feature. diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 75bff7d7510..6c7d8d5e7db 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1205,6 +1205,7 @@ Last donation in app]]> Game stats Best streak + Play WikiGames Issue report - Activity Tab I have encountered a problem with Activity Tab feature:\n- [Describe specific problem]\n\nThe behavior I would like to see is:\n- [Describe proposed solution] Problem with feature From f8dedf62195530084ef8b1b96c259638f1cc6742 Mon Sep 17 00:00:00 2001 From: Dmitry Brant Date: Wed, 3 Sep 2025 16:11:00 -0400 Subject: [PATCH 64/70] Update button colors. --- .../org/wikipedia/activitytab/ActivityTabFragment.kt | 12 +++++++++--- .../wikipedia/activitytab/EditingInsightsModule.kt | 4 ++-- .../wikipedia/activitytab/ReadingHistoryModule.kt | 2 +- .../org/wikipedia/activitytab/WikiGamesModule.kt | 3 ++- 4 files changed, 14 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/org/wikipedia/activitytab/ActivityTabFragment.kt b/app/src/main/java/org/wikipedia/activitytab/ActivityTabFragment.kt index 1c3bd021b08..4ab99b37b0c 100644 --- a/app/src/main/java/org/wikipedia/activitytab/ActivityTabFragment.kt +++ b/app/src/main/java/org/wikipedia/activitytab/ActivityTabFragment.kt @@ -41,6 +41,7 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.ComposeView import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource @@ -138,6 +139,7 @@ class ActivityTabFragment : Fragment() { isLoggedIn = AccountUtil.isLoggedIn, userName = AccountUtil.userName, modules = Prefs.activityTabModules, + haveAtLeastOneDonation = Prefs.donationResults.isNotEmpty(), readingHistoryState = viewModel.readingHistoryState.collectAsState().value, donationUiState = viewModel.donationUiState.collectAsState().value, wikiGamesUiState = viewModel.wikiGamesUiState.collectAsState().value, @@ -167,6 +169,7 @@ class ActivityTabFragment : Fragment() { isLoggedIn: Boolean, userName: String, modules: ActivityTabModules, + haveAtLeastOneDonation: Boolean, readingHistoryState: UiState, donationUiState: UiState, wikiGamesUiState: UiState, @@ -185,7 +188,6 @@ class ActivityTabFragment : Fragment() { if (readingHistoryState is UiState.Success) { isRefreshing = false } - val haveAtLeastOneDonation = Prefs.donationResults.isNotEmpty() if (!isLoggedIn) { Box( @@ -217,7 +219,7 @@ class ActivityTabFragment : Fragment() { contentPadding = PaddingValues(horizontal = 18.dp), colors = ButtonDefaults.buttonColors( containerColor = WikipediaTheme.colors.progressiveColor, - contentColor = WikipediaTheme.colors.paperColor, + contentColor = Color.White, ), onClick = { startActivity( @@ -231,7 +233,7 @@ class ActivityTabFragment : Fragment() { Icon( modifier = Modifier.size(20.dp), painter = painterResource(R.drawable.ic_user_avatar), - tint = WikipediaTheme.colors.paperColor, + tint = Color.White, contentDescription = null ) Text( @@ -610,6 +612,7 @@ class ActivityTabFragment : Fragment() { isLoggedIn = true, userName = "User", modules = ActivityTabModules(isDonationsEnabled = true), + haveAtLeastOneDonation = true, readingHistoryState = UiState.Success(ActivityTabViewModel.ReadingHistory( timeSpentThisWeek = 12345, articlesReadThisMonth = 123, @@ -650,6 +653,7 @@ class ActivityTabFragment : Fragment() { isLoggedIn = true, userName = "User", modules = ActivityTabModules(isDonationsEnabled = true), + haveAtLeastOneDonation = false, readingHistoryState = UiState.Success(ActivityTabViewModel.ReadingHistory( timeSpentThisWeek = 0, articlesReadThisMonth = 0, @@ -676,6 +680,7 @@ class ActivityTabFragment : Fragment() { isLoggedIn = false, userName = "User", modules = ActivityTabModules(), + haveAtLeastOneDonation = false, readingHistoryState = UiState.Success(ActivityTabViewModel.ReadingHistory( timeSpentThisWeek = 0, articlesReadThisMonth = 0, @@ -710,6 +715,7 @@ class ActivityTabFragment : Fragment() { isDonationsEnabled = false, isTimelineEnabled = false ), + haveAtLeastOneDonation = true, readingHistoryState = UiState.Success(ActivityTabViewModel.ReadingHistory( timeSpentThisWeek = 0, articlesReadThisMonth = 0, diff --git a/app/src/main/java/org/wikipedia/activitytab/EditingInsightsModule.kt b/app/src/main/java/org/wikipedia/activitytab/EditingInsightsModule.kt index 09bb959545f..832bf5613b9 100644 --- a/app/src/main/java/org/wikipedia/activitytab/EditingInsightsModule.kt +++ b/app/src/main/java/org/wikipedia/activitytab/EditingInsightsModule.kt @@ -417,14 +417,14 @@ fun SuggestedEditsView( contentPadding = PaddingValues(horizontal = 18.dp), colors = ButtonDefaults.buttonColors( containerColor = WikipediaTheme.colors.progressiveColor, - contentColor = WikipediaTheme.colors.paperColor, + contentColor = Color.White, ), onClick = { onClick?.invoke() }, ) { Icon( modifier = Modifier.size(20.dp), painter = painterResource(R.drawable.ic_mode_edit_white_24dp), - tint = WikipediaTheme.colors.paperColor, + tint = Color.White, contentDescription = null ) Text( diff --git a/app/src/main/java/org/wikipedia/activitytab/ReadingHistoryModule.kt b/app/src/main/java/org/wikipedia/activitytab/ReadingHistoryModule.kt index c63c54bf733..4c7a58fe17c 100644 --- a/app/src/main/java/org/wikipedia/activitytab/ReadingHistoryModule.kt +++ b/app/src/main/java/org/wikipedia/activitytab/ReadingHistoryModule.kt @@ -199,7 +199,7 @@ fun ReadingHistoryModule( contentPadding = PaddingValues(horizontal = 18.dp), colors = ButtonDefaults.buttonColors( containerColor = WikipediaTheme.colors.progressiveColor, - contentColor = WikipediaTheme.colors.paperColor, + contentColor = Color.White, ), onClick = { onExploreClick() diff --git a/app/src/main/java/org/wikipedia/activitytab/WikiGamesModule.kt b/app/src/main/java/org/wikipedia/activitytab/WikiGamesModule.kt index 2b6c3845278..81a6329068f 100644 --- a/app/src/main/java/org/wikipedia/activitytab/WikiGamesModule.kt +++ b/app/src/main/java/org/wikipedia/activitytab/WikiGamesModule.kt @@ -21,6 +21,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.graphics.Color import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.pluralStringResource import androidx.compose.ui.res.stringResource @@ -180,7 +181,7 @@ fun WikiGamesStatsCard( contentPadding = PaddingValues(horizontal = 18.dp), colors = ButtonDefaults.buttonColors( containerColor = WikipediaTheme.colors.progressiveColor, - contentColor = WikipediaTheme.colors.paperColor, + contentColor = Color.White, ), onClick = { onPlayGameCardClick?.invoke() }, ) { From c2fe4b9d9a57c5ae8019cac93bc526d2f8b8a4b8 Mon Sep 17 00:00:00 2001 From: William Rai <48931640+Williamrai@users.noreply.github.com> Date: Wed, 3 Sep 2025 17:42:06 -0400 Subject: [PATCH 65/70] Activity tab onboarding (#5883) * - adds onboarding activity and views * - refines ui * - adds new resources - adds onboarding preference and logic to show onboarding activity * - replace raw string with string resource * - code/ui fixes * - ui fixes * - ui fixes --- app/src/main/AndroidManifest.xml | 3 + .../ActivityTabOnboardingActivity.kt | 222 ++++++++++++++++++ .../java/org/wikipedia/main/MainFragment.kt | 16 +- .../main/java/org/wikipedia/settings/Prefs.kt | 4 + .../main/res/drawable/ic_outline_lock_24.xml | 5 + .../ic_outline_stadia_controller_24.xml | 5 + app/src/main/res/values-qq/strings.xml | 9 + app/src/main/res/values/preference_keys.xml | 1 + app/src/main/res/values/strings.xml | 10 + .../main/res/xml/developer_preferences.xml | 4 + 10 files changed, 278 insertions(+), 1 deletion(-) create mode 100644 app/src/main/java/org/wikipedia/activitytab/ActivityTabOnboardingActivity.kt create mode 100644 app/src/main/res/drawable/ic_outline_lock_24.xml create mode 100644 app/src/main/res/drawable/ic_outline_stadia_controller_24.xml diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index a749cf697b1..458d085d352 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -383,6 +383,9 @@ + + diff --git a/app/src/main/java/org/wikipedia/activitytab/ActivityTabOnboardingActivity.kt b/app/src/main/java/org/wikipedia/activitytab/ActivityTabOnboardingActivity.kt new file mode 100644 index 00000000000..b642c8a0bbc --- /dev/null +++ b/app/src/main/java/org/wikipedia/activitytab/ActivityTabOnboardingActivity.kt @@ -0,0 +1,222 @@ +package org.wikipedia.activitytab + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import androidx.activity.compose.setContent +import androidx.compose.foundation.BorderStroke +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.layout.padding +import androidx.compose.foundation.layout.safeDrawingPadding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Icon +import androidx.compose.material3.ListItem +import androidx.compose.material3.ListItemDefaults +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import org.wikipedia.R +import org.wikipedia.activity.BaseActivity +import org.wikipedia.compose.theme.BaseTheme +import org.wikipedia.compose.theme.WikipediaTheme +import org.wikipedia.settings.Prefs +import org.wikipedia.theme.Theme +import org.wikipedia.util.DeviceUtil + +private val onboardingItems = listOf( + OnboardingItem( + icon = R.drawable.ic_newsstand_24, + title = R.string.activity_tab_onboarding_reading_patterns_title, + subTitle = R.string.activity_tab_onboarding_reading_patterns_message + ), + OnboardingItem( + icon = R.drawable.ic_mode_edit_white_24dp, + title = R.string.activity_tab_onboarding_impact_title, + subTitle = R.string.activity_tab_onboarding_impact_message + ), + OnboardingItem( + icon = R.drawable.ic_outline_stadia_controller_24, + title = R.string.activity_tab_onboarding_engage_title, + subTitle = R.string.activity_tab_onboarding_engage_message + ), + OnboardingItem( + icon = R.drawable.ic_outline_lock_24, + title = R.string.activity_tab_onboarding_stay_in_control_title, + subTitle = R.string.activity_tab_onboarding_stay_in_control_message + ) +) + +class ActivityTabOnboardingActivity : BaseActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + DeviceUtil.setEdgeToEdge(this) + setContent { + BaseTheme { + OnboardingScreen( + onboardingItems = onboardingItems, + onLearnMoreClick = { + // TODO: MARK_ACTIVITY_TAB waiting for mediawiki page link + Prefs.isActivityTabOnboardingShown = true + }, + onContinueClick = { + Prefs.isActivityTabOnboardingShown = true + setResult(RESULT_OK) + finish() + } + ) + } + } + } + + companion object { + fun newIntent(context: Context): Intent { + return Intent(context, ActivityTabOnboardingActivity::class.java) + } + } +} + +@Composable +fun OnboardingScreen( + modifier: Modifier = Modifier, + onboardingItems: List, + onLearnMoreClick: () -> Unit, + onContinueClick: () -> Unit +) { + Scaffold( + modifier = modifier + .safeDrawingPadding(), + containerColor = WikipediaTheme.colors.paperColor, + bottomBar = { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 16.dp), + horizontalArrangement = Arrangement.spacedBy( + space = 24.dp, + alignment = Alignment.CenterHorizontally + ) + ) { + Button( + modifier = Modifier + .weight(1f), + border = BorderStroke( + width = 1.dp, + color = WikipediaTheme.colors.borderColor + ), + colors = ButtonDefaults.buttonColors( + containerColor = WikipediaTheme.colors.paperColor + ), + onClick = onLearnMoreClick + ) { + Text( + text = stringResource(R.string.activity_tab_menu_info), + style = MaterialTheme.typography.labelLarge, + color = WikipediaTheme.colors.progressiveColor + ) + } + + Button( + modifier = Modifier + .weight(1f), + colors = ButtonDefaults.buttonColors( + containerColor = WikipediaTheme.colors.progressiveColor + ), + onClick = onContinueClick + ) { + Text( + text = stringResource(R.string.onboarding_continue), + style = MaterialTheme.typography.labelLarge, + color = WikipediaTheme.colors.paperColor + ) + } + } + } + ) { paddingValues -> + Column( + modifier = Modifier + .fillMaxSize() + .padding(paddingValues) + .verticalScroll(rememberScrollState()), + ) { + Text( + modifier = Modifier + .fillMaxWidth() + .padding(top = 58.dp, bottom = 32.dp), + textAlign = TextAlign.Center, + text = stringResource(R.string.activity_tab_onboarding_screen_title), + style = MaterialTheme.typography.headlineLarge.copy(fontWeight = FontWeight.Medium), + color = WikipediaTheme.colors.primaryColor + ) + + onboardingItems.forEach { onboardingItem -> + ListItem( + modifier = Modifier + .padding(horizontal = 8.dp) + .padding(bottom = 16.dp), + colors = ListItemDefaults.colors( + containerColor = WikipediaTheme.colors.paperColor + ), + headlineContent = { + Text( + text = stringResource(onboardingItem.title), + style = MaterialTheme.typography.titleMedium.copy(fontWeight = FontWeight.SemiBold), + color = WikipediaTheme.colors.primaryColor + ) + }, + supportingContent = { + Text( + text = stringResource(onboardingItem.subTitle), + style = MaterialTheme.typography.bodyMedium, + color = WikipediaTheme.colors.secondaryColor + ) + }, + leadingContent = { + Icon( + modifier = Modifier + .padding(top = 2.dp), + painter = painterResource(onboardingItem.icon), + tint = WikipediaTheme.colors.progressiveColor, + contentDescription = null + ) + } + ) + } + } + } +} + +data class OnboardingItem( + val icon: Int, + val title: Int, + val subTitle: Int +) + +@Preview +@Composable +private fun OnboardingScreenPreview() { + BaseTheme( + currentTheme = Theme.LIGHT + ) { + OnboardingScreen( + onboardingItems = onboardingItems, + onLearnMoreClick = {}, + onContinueClick = {} + ) + } +} diff --git a/app/src/main/java/org/wikipedia/main/MainFragment.kt b/app/src/main/java/org/wikipedia/main/MainFragment.kt index a0227726c19..90fcbc36fd5 100644 --- a/app/src/main/java/org/wikipedia/main/MainFragment.kt +++ b/app/src/main/java/org/wikipedia/main/MainFragment.kt @@ -21,6 +21,7 @@ import androidx.activity.result.contract.ActivityResultContracts import androidx.core.content.ContextCompat import androidx.core.view.MenuProvider import androidx.core.view.descendants +import androidx.core.view.get import androidx.core.view.isVisible import androidx.fragment.app.Fragment import androidx.lifecycle.Lifecycle @@ -39,6 +40,7 @@ import org.wikipedia.activity.BaseActivity import org.wikipedia.activity.FragmentUtil.getCallback import org.wikipedia.activitytab.ActivityTabABTest import org.wikipedia.activitytab.ActivityTabFragment +import org.wikipedia.activitytab.ActivityTabOnboardingActivity import org.wikipedia.analytics.eventplatform.ReadingListsAnalyticsHelper import org.wikipedia.auth.AccountUtil import org.wikipedia.commons.FilePageActivity @@ -123,6 +125,12 @@ class MainFragment : Fragment(), BackPressedHandler, MenuProvider, FeedFragment. } } + private val activityTabOnboardingLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> + if (result.resultCode == Activity.RESULT_OK) { + goToTab(NavTab.EDITS) + } + } + val currentFragment get() = (binding.mainViewPager.adapter as NavTabFragmentPagerAdapter).getFragmentAt(binding.mainViewPager.currentItem) override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { @@ -157,6 +165,12 @@ class MainFragment : Fragment(), BackPressedHandler, MenuProvider, FeedFragment. val shouldShowRedDotForRecommendedReadingList = (!Prefs.isRecommendedReadingListOnboardingShown) || (Prefs.isRecommendedReadingListEnabled && Prefs.isNewRecommendedReadingListGenerated) binding.mainNavTabLayout.setOverlayDot(NavTab.READING_LISTS, shouldShowRedDotForRecommendedReadingList) binding.mainNavTabLayout.setOnItemSelectedListener { item -> + if (item.order == NavTab.EDITS.code()) { + if (ActivityTabABTest().isInTestGroup() && !Prefs.isActivityTabOnboardingShown) { + activityTabOnboardingLauncher.launch(ActivityTabOnboardingActivity.newIntent(requireContext())) + return@setOnItemSelectedListener false + } + } if (item.order == NavTab.MORE.code()) { ExclusiveBottomSheetPresenter.show(childFragmentManager, MenuNavTabDialog.newInstance()) return@setOnItemSelectedListener false @@ -539,7 +553,7 @@ class MainFragment : Fragment(), BackPressedHandler, MenuProvider, FeedFragment. } private fun goToTab(tab: NavTab) { - binding.mainNavTabLayout.selectedItemId = binding.mainNavTabLayout.menu.getItem(tab.code()).itemId + binding.mainNavTabLayout.selectedItemId = binding.mainNavTabLayout.menu[tab.code()].itemId } private fun refreshContents() { diff --git a/app/src/main/java/org/wikipedia/settings/Prefs.kt b/app/src/main/java/org/wikipedia/settings/Prefs.kt index a8a781548cf..53e247b0b7e 100644 --- a/app/src/main/java/org/wikipedia/settings/Prefs.kt +++ b/app/src/main/java/org/wikipedia/settings/Prefs.kt @@ -854,4 +854,8 @@ object Prefs { get() = JsonUtil.decodeFromString(PrefsIoUtil.getString(R.string.preference_key_activity_tab_modules, null)) ?: ActivityTabModules() set(modules) = PrefsIoUtil.setString(R.string.preference_key_activity_tab_modules, JsonUtil.encodeToString(modules)) + + var isActivityTabOnboardingShown + get() = PrefsIoUtil.getBoolean(R.string.preference_key_activity_tab_onboarding_shown, false) + set(value) = PrefsIoUtil.setBoolean(R.string.preference_key_activity_tab_onboarding_shown, value) } diff --git a/app/src/main/res/drawable/ic_outline_lock_24.xml b/app/src/main/res/drawable/ic_outline_lock_24.xml new file mode 100644 index 00000000000..6c17e850744 --- /dev/null +++ b/app/src/main/res/drawable/ic_outline_lock_24.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_outline_stadia_controller_24.xml b/app/src/main/res/drawable/ic_outline_stadia_controller_24.xml new file mode 100644 index 00000000000..b1e43341396 --- /dev/null +++ b/app/src/main/res/drawable/ic_outline_stadia_controller_24.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/values-qq/strings.xml b/app/src/main/res/values-qq/strings.xml index 77603051995..230020e5b32 100644 --- a/app/src/main/res/values-qq/strings.xml +++ b/app/src/main/res/values-qq/strings.xml @@ -1242,6 +1242,15 @@ Date separator label for yesterday\'s entries in activity timeline. Title displayed when the timeline is empty. Message shown when there are no items in the timeline. + Title for the activity tab onboarding screen. + Title for the section about user\'s reading habits. + Short explanation about user\'s reading habits. + Title for the section about user\'s impact. + Short explanation about the user\'s impact. + Title for the section about about ways to engage with Wikipedia.. + Short explanation about the stats, saved articles, and other activities that connect the user more deeply with Wikipedia. + Title for the section about privacy and control. + Short explanation about privacy and control. Title shown at the top of the activity for the file page. Button label to add image caption for the file. Button label to add image tags for the file. diff --git a/app/src/main/res/values/preference_keys.xml b/app/src/main/res/values/preference_keys.xml index 76a3b8c391f..eaa2a56cd64 100644 --- a/app/src/main/res/values/preference_keys.xml +++ b/app/src/main/res/values/preference_keys.xml @@ -203,5 +203,6 @@ donationReminderDevResetSeenDate donations activityTabModules + activityTabOnboardingShown diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 6c7d8d5e7db..869e2537d9e 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1264,6 +1264,16 @@ Yesterday Nothing to show Start reading and editing to build your history + Reading patterns + See how much time you\'ve spent reading and which articles or topics you\'ve explored over time. + Introducing Activity + Impact highlights + Discover insights about your contributions and the reach of the knowledge you\'ve shared. + More ways to engage + Explore stats for games, saved articles, and other activities that connect you more deeply with Wikipedia. + Stay in control + Choose which modules to display. All personal data stays private on your device and browsing history can be cleared at anytime. + diff --git a/app/src/main/res/xml/developer_preferences.xml b/app/src/main/res/xml/developer_preferences.xml index ad0cfcfcd70..de5d1e458b2 100644 --- a/app/src/main/res/xml/developer_preferences.xml +++ b/app/src/main/res/xml/developer_preferences.xml @@ -559,5 +559,9 @@ android:key="@string/preference_key_activity_tab_red_dot_shown" android:title="@string/preference_key_activity_tab_red_dot_shown" /> + + From 6b9ee751e5c7a200e85a32f74a57177c1969f9c5 Mon Sep 17 00:00:00 2001 From: Cooltey Feng Date: Thu, 4 Sep 2025 05:41:31 -0700 Subject: [PATCH 66/70] Rename wikiSite to wikiSiteForTimeline for specific usage (#5898) --- .../activitytab/ActivityTabFragment.kt | 2 +- .../activitytab/ActivityTabViewModel.kt | 23 ++++++++----------- .../timeline/TimelineRepository.kt | 4 ++-- 3 files changed, 13 insertions(+), 16 deletions(-) diff --git a/app/src/main/java/org/wikipedia/activitytab/ActivityTabFragment.kt b/app/src/main/java/org/wikipedia/activitytab/ActivityTabFragment.kt index 4ab99b37b0c..b60e6ecbf9c 100644 --- a/app/src/main/java/org/wikipedia/activitytab/ActivityTabFragment.kt +++ b/app/src/main/java/org/wikipedia/activitytab/ActivityTabFragment.kt @@ -535,7 +535,7 @@ class ActivityTabFragment : Fragment() { requireContext(), PageTitle( item.apiTitle, - viewModel.wikiSite, + viewModel.wikiSiteForTimeline, item.thumbnailUrl, item.description, item.displayTitle diff --git a/app/src/main/java/org/wikipedia/activitytab/ActivityTabViewModel.kt b/app/src/main/java/org/wikipedia/activitytab/ActivityTabViewModel.kt index 94bdd14fd2b..300c6370a0b 100644 --- a/app/src/main/java/org/wikipedia/activitytab/ActivityTabViewModel.kt +++ b/app/src/main/java/org/wikipedia/activitytab/ActivityTabViewModel.kt @@ -1,7 +1,6 @@ package org.wikipedia.activitytab import android.text.format.DateUtils -import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import androidx.paging.Pager @@ -46,17 +45,7 @@ import java.util.Date import java.util.concurrent.TimeUnit import kotlin.math.abs -class ActivityTabViewModel(savedStateHandle: SavedStateHandle) : ViewModel() { - var langCode = Prefs.userContribFilterLangCode - - val wikiSite get(): WikiSite { - return when (langCode) { - Constants.WIKI_CODE_COMMONS -> WikiSite(Service.COMMONS_URL) - Constants.WIKI_CODE_WIKIDATA -> WikiSite(Service.WIKIDATA_URL) - else -> WikiSite.forLanguageCode(langCode) - } - } - +class ActivityTabViewModel() : ViewModel() { private val _readingHistoryState = MutableStateFlow>(UiState.Loading) val readingHistoryState: StateFlow> = _readingHistoryState.asStateFlow() @@ -68,6 +57,14 @@ class ActivityTabViewModel(savedStateHandle: SavedStateHandle) : ViewModel() { private var currentTimelinePagingSource: TimelinePagingSource? = null + val wikiSiteForTimeline get(): WikiSite { + val langCode = Prefs.userContribFilterLangCode + return when (langCode) { + Constants.WIKI_CODE_COMMONS -> WikiSite(Service.COMMONS_URL) + Constants.WIKI_CODE_WIKIDATA -> WikiSite(Service.WIKIDATA_URL) + else -> WikiSite.forLanguageCode(langCode) + } + } val timelineFlow = Pager( config = PagingConfig( pageSize = 150, @@ -233,7 +230,7 @@ class ActivityTabViewModel(savedStateHandle: SavedStateHandle) : ViewModel() { private fun createTimelineSources(): List { val historyEntryPagingSource = HistoryEntryPagingSource(AppDatabase.instance.historyEntryWithImageDao()) - val userContribPagingSource = UserContribPagingSource(wikiSite, AccountUtil.userName, AppDatabase.instance.historyEntryWithImageDao()) + val userContribPagingSource = UserContribPagingSource(wikiSiteForTimeline, AccountUtil.userName, AppDatabase.instance.historyEntryWithImageDao()) val readingListPagingSource = ReadingListPagingSource(AppDatabase.instance.readingListPageDao()) return listOf(historyEntryPagingSource, readingListPagingSource, userContribPagingSource) } diff --git a/app/src/main/java/org/wikipedia/activitytab/timeline/TimelineRepository.kt b/app/src/main/java/org/wikipedia/activitytab/timeline/TimelineRepository.kt index 9ded1761ee8..262f59a18b7 100644 --- a/app/src/main/java/org/wikipedia/activitytab/timeline/TimelineRepository.kt +++ b/app/src/main/java/org/wikipedia/activitytab/timeline/TimelineRepository.kt @@ -55,7 +55,7 @@ class UserContribPagingSource( private val historyEntryWithImageDao: HistoryEntryWithImageDao ) : TimelineSource { - private val MAX_BATCH_SIZE = 50 + private val maxBatchSize = 50 override val id: String = "user_contrib" @@ -90,7 +90,7 @@ class UserContribPagingSource( } // Fetching missing page info in batches - missingPageInfoIds.chunked(MAX_BATCH_SIZE).forEach { batch -> + missingPageInfoIds.chunked(maxBatchSize).forEach { batch -> val pages = service.getInfoByPageIdsOrTitles(pageIds = batch.joinToString(separator = "|")).query?.pages.orEmpty() pages.forEach { page -> timelineItemsByPageId[page.pageId.toLong()]?.let { existingItem -> From 4ec1d5aeaf03ebea0eff7f6c628077dd0fc8e84c Mon Sep 17 00:00:00 2001 From: Cooltey Feng Date: Thu, 4 Sep 2025 10:41:46 -0700 Subject: [PATCH 67/70] Use string resource for view changes button and some minor code updates (#5901) * Use string resource for view changes button and some minor code updates * More thumbnail url update --- .../org/wikipedia/activitytab/EditingInsightsModule.kt | 2 +- .../org/wikipedia/activitytab/ReadingHistoryModule.kt | 2 +- .../org/wikipedia/activitytab/timeline/TimelineModule.kt | 8 ++++---- app/src/main/res/values-qq/strings.xml | 1 + app/src/main/res/values/strings.xml | 1 + 5 files changed, 8 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/org/wikipedia/activitytab/EditingInsightsModule.kt b/app/src/main/java/org/wikipedia/activitytab/EditingInsightsModule.kt index 832bf5613b9..f31889bf483 100644 --- a/app/src/main/java/org/wikipedia/activitytab/EditingInsightsModule.kt +++ b/app/src/main/java/org/wikipedia/activitytab/EditingInsightsModule.kt @@ -243,7 +243,7 @@ fun MostViewedCard( ) } } - if (pageTitle.thumbUrl != null) { + if (!pageTitle.thumbUrl.isNullOrEmpty()) { val request = ImageService.getRequest( LocalContext.current, diff --git a/app/src/main/java/org/wikipedia/activitytab/ReadingHistoryModule.kt b/app/src/main/java/org/wikipedia/activitytab/ReadingHistoryModule.kt index 4c7a58fe17c..f6b5a244d7e 100644 --- a/app/src/main/java/org/wikipedia/activitytab/ReadingHistoryModule.kt +++ b/app/src/main/java/org/wikipedia/activitytab/ReadingHistoryModule.kt @@ -426,7 +426,7 @@ private fun ArticleSavedThisMonthCard( for (i in 0 until itemsToShow) { val url = readingHistory.articlesSaved[i].thumbUrl - if (url == null) { + if (url.isNullOrEmpty()) { Box( modifier = Modifier.padding(start = 4.dp).size(38.dp) .background( diff --git a/app/src/main/java/org/wikipedia/activitytab/timeline/TimelineModule.kt b/app/src/main/java/org/wikipedia/activitytab/timeline/TimelineModule.kt index 456710e3207..42b3b98c76e 100644 --- a/app/src/main/java/org/wikipedia/activitytab/timeline/TimelineModule.kt +++ b/app/src/main/java/org/wikipedia/activitytab/timeline/TimelineModule.kt @@ -116,12 +116,12 @@ fun TimelineModule( ) Text( modifier = Modifier.padding(start = 6.dp), - text = "View changes" + text = stringResource(R.string.activity_tab_timeline_view_changes_button) ) } } } - if (timelineItem.thumbnailUrl != null) { + if (!timelineItem.thumbnailUrl.isNullOrEmpty()) { val request = ImageService.getRequest(LocalContext.current, url = timelineItem.thumbnailUrl) AsyncImage( @@ -206,7 +206,7 @@ fun TimelineDateSeparator( } } -@Preview +@Preview(showBackground = true) @Composable private fun TimelineItemPreview() { BaseTheme( @@ -231,7 +231,7 @@ private fun TimelineItemPreview() { } } -@Preview +@Preview(showBackground = true) @Composable private fun TimelineDateSeparatorPreview() { BaseTheme( diff --git a/app/src/main/res/values-qq/strings.xml b/app/src/main/res/values-qq/strings.xml index 230020e5b32..aa912c27049 100644 --- a/app/src/main/res/values-qq/strings.xml +++ b/app/src/main/res/values-qq/strings.xml @@ -1242,6 +1242,7 @@ Date separator label for yesterday\'s entries in activity timeline. Title displayed when the timeline is empty. Message shown when there are no items in the timeline. + Button label to view edit changes in the timeline. Title for the activity tab onboarding screen. Title for the section about user\'s reading habits. Short explanation about user\'s reading habits. diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 869e2537d9e..59a9971043a 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1264,6 +1264,7 @@ Yesterday Nothing to show Start reading and editing to build your history + View changes Reading patterns See how much time you\'ve spent reading and which articles or topics you\'ve explored over time. Introducing Activity From bb437bd4177fd984e3fa9bfc1b1ac6fcd6225bce Mon Sep 17 00:00:00 2001 From: Cooltey Feng Date: Thu, 4 Sep 2025 10:57:36 -0700 Subject: [PATCH 68/70] Change the usergrowth request limitation from 1 day to 12 hours for (#5903) Activity Tab --- .../main/java/org/wikipedia/activitytab/ActivityTabViewModel.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/org/wikipedia/activitytab/ActivityTabViewModel.kt b/app/src/main/java/org/wikipedia/activitytab/ActivityTabViewModel.kt index 300c6370a0b..7d5ae9955dc 100644 --- a/app/src/main/java/org/wikipedia/activitytab/ActivityTabViewModel.kt +++ b/app/src/main/java/org/wikipedia/activitytab/ActivityTabViewModel.kt @@ -192,7 +192,7 @@ class ActivityTabViewModel() : ViewModel() { val impact: GrowthUserImpact val impactLastResponseBodyMap = Prefs.impactLastResponseBody.toMutableMap() val impactResponse = impactLastResponseBodyMap[wikiSite.languageCode] - if (impactResponse.isNullOrEmpty() || abs(now - Prefs.impactLastQueryTime) > TimeUnit.DAYS.toSeconds(1)) { + if (impactResponse.isNullOrEmpty() || abs(now - Prefs.impactLastQueryTime) > TimeUnit.HOURS.toSeconds(12)) { val userId = ServiceFactory.get(wikiSite).getUserInfo().query?.userInfo?.id!! impact = ServiceFactory.getCoreRest(wikiSite).getUserImpact(userId) impactLastResponseBodyMap[wikiSite.languageCode] = JsonUtil.encodeToString(impact).orEmpty() From 021f39c1e26f4df111f4fdb724e69595caf55cf6 Mon Sep 17 00:00:00 2001 From: Dmitry Brant Date: Thu, 4 Sep 2025 18:49:46 -0400 Subject: [PATCH 69/70] Update copy of menu item. --- app/src/main/res/values/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 59a9971043a..b52b960e7b4 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -30,7 +30,7 @@ Go back Go back Next - Clear history + Clear reading history %s removed from history %d articles removed from history Undo From 73e84d5c083e8e936d0e40603a9e8a6fd303778d Mon Sep 17 00:00:00 2001 From: Dmitry Brant Date: Thu, 4 Sep 2025 19:13:29 -0400 Subject: [PATCH 70/70] Create space for url string. --- .../java/org/wikipedia/activitytab/ActivityTabFragment.kt | 3 ++- .../wikipedia/activitytab/ActivityTabOnboardingActivity.kt | 4 +++- app/src/main/res/values/strings_no_translate.xml | 1 + 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/org/wikipedia/activitytab/ActivityTabFragment.kt b/app/src/main/java/org/wikipedia/activitytab/ActivityTabFragment.kt index b60e6ecbf9c..6565b6206dc 100644 --- a/app/src/main/java/org/wikipedia/activitytab/ActivityTabFragment.kt +++ b/app/src/main/java/org/wikipedia/activitytab/ActivityTabFragment.kt @@ -100,6 +100,7 @@ import org.wikipedia.theme.Theme import org.wikipedia.usercontrib.UserContribListActivity import org.wikipedia.util.FeedbackUtil import org.wikipedia.util.UiState +import org.wikipedia.util.UriUtil import java.time.LocalDateTime class ActivityTabFragment : Fragment() { @@ -757,7 +758,7 @@ class ActivityTabFragment : Fragment() { true } R.id.menu_learn_more -> { - // TODO: MARK_ACTIVITY_TAB --> add mediawiki page link + UriUtil.visitInExternalBrowser(requireActivity(), getString(R.string.activity_tab_url).toUri()) true } R.id.menu_report_feature -> { diff --git a/app/src/main/java/org/wikipedia/activitytab/ActivityTabOnboardingActivity.kt b/app/src/main/java/org/wikipedia/activitytab/ActivityTabOnboardingActivity.kt index b642c8a0bbc..b816b661df5 100644 --- a/app/src/main/java/org/wikipedia/activitytab/ActivityTabOnboardingActivity.kt +++ b/app/src/main/java/org/wikipedia/activitytab/ActivityTabOnboardingActivity.kt @@ -31,6 +31,7 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import androidx.core.net.toUri import org.wikipedia.R import org.wikipedia.activity.BaseActivity import org.wikipedia.compose.theme.BaseTheme @@ -38,6 +39,7 @@ import org.wikipedia.compose.theme.WikipediaTheme import org.wikipedia.settings.Prefs import org.wikipedia.theme.Theme import org.wikipedia.util.DeviceUtil +import org.wikipedia.util.UriUtil private val onboardingItems = listOf( OnboardingItem( @@ -71,7 +73,7 @@ class ActivityTabOnboardingActivity : BaseActivity() { OnboardingScreen( onboardingItems = onboardingItems, onLearnMoreClick = { - // TODO: MARK_ACTIVITY_TAB waiting for mediawiki page link + UriUtil.visitInExternalBrowser(this, getString(R.string.activity_tab_url).toUri()) Prefs.isActivityTabOnboardingShown = true }, onContinueClick = { diff --git a/app/src/main/res/values/strings_no_translate.xml b/app/src/main/res/values/strings_no_translate.xml index 12701e7bd5c..12621b8955e 100644 --- a/app/src/main/res/values/strings_no_translate.xml +++ b/app/src/main/res/values/strings_no_translate.xml @@ -28,6 +28,7 @@ https://www.mediawiki.org/wiki/Wikimedia_Apps/Team/iOS/Personalized_Wikipedia_Year_in_Review/How_your_data_is_used https://www.mediawiki.org/wiki/Wikimedia_Apps/Team/Android/Rabbit_Holes https://www.mediawiki.org/wiki/Wikimedia_Apps/Team/Android/Customizable_Donation_Reminder_Experiment + TODO @string/wikimedia