Skip to content

[Feature branch] Recommended reading list #5567

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 31 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
838ed17
[Feature breanch] Recommended Reading list
cooltey May 16, 2025
1c0ea26
Add preferences and enum classes for Recommended Reading list (#5569)
cooltey May 16, 2025
46a1638
Make default status to disable
cooltey May 16, 2025
c1d2291
Naming of string item
cooltey May 16, 2025
9e4ce3b
Update preference format a little bit
cooltey May 17, 2025
22e4a5a
Merge branch 'main' into recommended-reading-list-design
Williamrai May 19, 2025
a248baa
Merge branch 'main' into recommended-reading-list-design
cooltey May 19, 2025
a05c2dc
Merge branch 'main' into recommended-reading-list-design
Williamrai May 20, 2025
40c7422
Create database table for Recommended Reading list (#5556)
cooltey May 20, 2025
4afdd24
Merge branch 'main' into recommended-reading-list-design
cooltey May 20, 2025
4c211ae
Merge branch 'main' into recommended-reading-list-design
cooltey May 20, 2025
a5b9727
Merge branch 'main' into recommended-reading-list-design
cooltey May 21, 2025
ee05a61
Create onboarding message card for Recommended Reading list (#5583)
cooltey May 22, 2025
21299dc
Merge branch 'main' into recommended-reading-list-design
cooltey May 22, 2025
9c1c777
Create recurring task for Recommended Reading list (#5570)
cooltey May 22, 2025
fdf11af
Follow-up to recurring task. (#5589)
dbrant May 22, 2025
f1334b8
Merge branch 'main' into recommended-reading-list-design
Williamrai May 23, 2025
0d2fe5b
Merge branch 'main' into recommended-reading-list-design
Williamrai May 23, 2025
cd1fcce
Merge branch 'main' into recommended-reading-list-design
cooltey May 27, 2025
570dc42
Merge branch 'main' into recommended-reading-list-design
dbrant May 28, 2025
6aaeed3
Merge branch 'main' into recommended-reading-list-design
Williamrai May 28, 2025
5c31c9b
Build screen for selecting the source type for Recommended Reading li…
cooltey May 28, 2025
ab18750
Update the logic of showing onboarding card for Recommended reading l…
cooltey May 29, 2025
d7f8e12
Merge branch 'main' into recommended-reading-list-design
cooltey May 29, 2025
160b8bf
Merge branch 'main' into recommended-reading-list-design
Williamrai May 29, 2025
ec33a16
Ab test Recommended reading list (#5600)
Williamrai May 29, 2025
123eaf9
Discover reading list setting (#5564)
Williamrai May 29, 2025
53ec5fe
Follow-up: Discover settings for Recommended reading list (#5602)
cooltey May 30, 2025
0be29c5
- parameter rename and logic update (#5603)
Williamrai May 30, 2025
d8699c0
Merge branch 'main' into recommended-reading-list-design
cooltey May 30, 2025
f3ab3c3
Merge branch 'main' into recommended-reading-list-design
Williamrai Jun 2, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
727 changes: 727 additions & 0 deletions app/schemas/org.wikipedia.database.AppDatabase/30.json

Large diffs are not rendered by default.

7 changes: 6 additions & 1 deletion app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -362,9 +362,14 @@

<activity
android:name=".games.onthisday.OnThisDayGameActivity"/>

<activity
android:name=".settings.dev.playground.CategoryDeveloperPlayGround"/>
android:name=".readinglist.recommended.RecommendedReadingListOnboardingActivity"/>

<activity
android:name=".settings.dev.playground.CategoryDeveloperPlayGround"/>
<activity
android:name=".readinglist.recommended.RecommendedReadingListSettingsActivity"/>
<provider
android:name=".WikipediaFileProvider"
android:authorities="${applicationId}.fileprovider"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package org.wikipedia.analytics.eventplatform

import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import org.wikipedia.WikipediaApp
import org.wikipedia.json.JsonUtil

object RecommendedReadingListEvent {

fun submit(
action: String,
activeInterface: String,
groupName: String,
wikiId: String = WikipediaApp.instance.appOrSystemLanguageCode
) {
val actionData = ActionData(
rrlGroup = groupName
)

EventPlatformClient.submit(
AppInteractionEvent(
action,
activeInterface,
action_data = JsonUtil.encodeToString(actionData).orEmpty(),
primary_language = WikipediaApp.instance.languageState.appLanguageCode,
wiki_id = wikiId,
streamName = "app_rabbit_holes"
)
)
}

@Serializable
class ActionData(
@SerialName("rrl_group")
val rrlGroup: String
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package org.wikipedia.compose.components

import androidx.compose.material3.AlertDialog
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import org.wikipedia.compose.theme.WikipediaTheme

@Composable
fun WikipediaAlertDialog(
title: String,
message: String,
confirmButtonText: String,
dismissButtonText: String,
onDismissRequest: () -> Unit,
onConfirmButtonClick: () -> Unit,
onDismissButtonClick: () -> Unit,
modifier: Modifier = Modifier
) {
AlertDialog(
modifier = modifier,
containerColor = WikipediaTheme.colors.paperColor,
title = {
Text(
text = title,
color = WikipediaTheme.colors.primaryColor
)
},
text = {
Text(
text = message,
color = WikipediaTheme.colors.secondaryColor
)
},
onDismissRequest = onDismissRequest,
confirmButton = {
TextButton(
colors = ButtonDefaults.textButtonColors(
contentColor = WikipediaTheme.colors.progressiveColor
),
onClick = onConfirmButtonClick
) {
Text(confirmButtonText)
}
},
dismissButton = {
TextButton(
colors = ButtonDefaults.textButtonColors(
contentColor = WikipediaTheme.colors.progressiveColor
),
onClick = onDismissButtonClick
) {
Text(dismissButtonText)
}
}
)
}
13 changes: 13 additions & 0 deletions app/src/main/java/org/wikipedia/compose/extensions/Modifier.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import androidx.compose.animation.core.Easing
import androidx.compose.animation.core.FastOutSlowInEasing
import androidx.compose.animation.core.animate
import androidx.compose.animation.core.tween
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Scaffold
Expand Down Expand Up @@ -60,6 +61,18 @@ fun Modifier.pulse(
}
}

fun Modifier.noRippleClickable(
enabled: Boolean = true,
onClickLabel: String? = null,
onClick: () -> Unit
): Modifier = clickable(
enabled = enabled,
indication = null,
interactionSource = null,
onClickLabel = onClickLabel,
onClick = onClick
)

@Preview
@Composable
private fun PreviewPulse() {
Expand Down
26 changes: 23 additions & 3 deletions app/src/main/java/org/wikipedia/database/AppDatabase.kt
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,10 @@ import org.wikipedia.pageimages.db.PageImage
import org.wikipedia.pageimages.db.PageImageDao
import org.wikipedia.readinglist.database.ReadingList
import org.wikipedia.readinglist.database.ReadingListPage
import org.wikipedia.readinglist.database.RecommendedPage
import org.wikipedia.readinglist.db.ReadingListDao
import org.wikipedia.readinglist.db.ReadingListPageDao
import org.wikipedia.readinglist.db.RecommendedPageDao
import org.wikipedia.search.db.RecentSearch
import org.wikipedia.search.db.RecentSearchDao
import org.wikipedia.staticdata.MainPageNameData
Expand All @@ -35,7 +37,7 @@ import org.wikipedia.talk.db.TalkTemplate
import org.wikipedia.talk.db.TalkTemplateDao

const val DATABASE_NAME = "wikipedia.db"
const val DATABASE_VERSION = 29
const val DATABASE_VERSION = 30

@Database(
entities = [
Expand All @@ -50,7 +52,8 @@ const val DATABASE_VERSION = 29
Notification::class,
TalkTemplate::class,
Category::class,
DailyGameHistory::class
DailyGameHistory::class,
RecommendedPage::class
],
version = DATABASE_VERSION
)
Expand All @@ -75,6 +78,7 @@ abstract class AppDatabase : RoomDatabase() {
abstract fun talkTemplateDao(): TalkTemplateDao
abstract fun categoryDao(): CategoryDao
abstract fun dailyGameHistoryDao(): DailyGameHistoryDao
abstract fun recommendedPageDao(): RecommendedPageDao

companion object {
val MIGRATION_19_20 = object : Migration(19, 20) {
Expand Down Expand Up @@ -297,12 +301,28 @@ abstract class AppDatabase : RoomDatabase() {
")")
}
}
val MIGRATION_29_30 = object : Migration(29, 30) {
override fun migrate(db: SupportSQLiteDatabase) {
db.execSQL("CREATE TABLE IF NOT EXISTS RecommendedPage (" +
" id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL," +
" wiki TEXT NOT NULL," +
" lang TEXT NOT NULL DEFAULT 'en'," +
" namespace INTEGER NOT NULL," +
" timestamp INTEGER NOT NULL," +
" apiTitle TEXT NOT NULL," +
" displayTitle TEXT NOT NULL," +
" description TEXT," +
" thumbUrl TEXT," +
" status INTEGER NOT NULL DEFAULT 0" +
")")
}
}

val instance: AppDatabase by lazy(LazyThreadSafetyMode.SYNCHRONIZED) {
Room.databaseBuilder(WikipediaApp.instance, AppDatabase::class.java, DATABASE_NAME)
.addMigrations(MIGRATION_19_20, MIGRATION_20_21, MIGRATION_21_22, MIGRATION_22_23,
MIGRATION_23_24, MIGRATION_24_25, MIGRATION_25_26, MIGRATION_26_27,
MIGRATION_26_28, MIGRATION_27_28, MIGRATION_28_29)
MIGRATION_26_28, MIGRATION_27_28, MIGRATION_28_29, MIGRATION_29_30)
.allowMainThreadQueries() // TODO: remove after migration
.fallbackToDestructiveMigration()
.build()
Expand Down
1 change: 1 addition & 0 deletions app/src/main/java/org/wikipedia/dataclient/Service.kt
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ interface Service {
@Query("gsrsearch") searchTerm: String?,
@Query("gsrlimit") gsrLimit: Int,
@Query("pilimit") piLimit: Int,
@Query("gsroffset") gsrOffset: Int? = null,
): MwQueryResponse

// ------- Miscellaneous -------
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,15 @@ interface HistoryEntryDao {
@Query("SELECT * FROM HistoryEntry WHERE authority = :authority AND lang = :lang AND apiTitle = :apiTitle LIMIT 1")
suspend fun findEntryBy(authority: String, lang: String, apiTitle: String): HistoryEntry?

@Query("SELECT * FROM HistoryEntry ORDER BY RANDOM() DESC LIMIT :limit")
suspend fun getHistoryEntriesByRandom(limit: Int): List<HistoryEntry>

@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")
suspend fun getHistoryCount(): Int

@Query("DELETE FROM HistoryEntry")
suspend fun deleteAll()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ import org.wikipedia.page.PageActivity
import org.wikipedia.page.PageAvailableOfflineHandler
import org.wikipedia.readinglist.database.ReadingList
import org.wikipedia.readinglist.database.ReadingListPage
import org.wikipedia.readinglist.recommended.RecommendedReadingListAbTest
import org.wikipedia.readinglist.recommended.RecommendedReadingListOnboardingActivity
import org.wikipedia.readinglist.sync.ReadingListSyncAdapter
import org.wikipedia.readinglist.sync.ReadingListSyncEvent
import org.wikipedia.settings.Prefs
Expand Down Expand Up @@ -783,8 +785,24 @@ class ReadingListsFragment : Fragment(), SortReadingListsDialog.Callback, Readin
binding.onboardingView.isVisible = false
return
}
if ((AccountUtil.isLoggedIn && !AccountUtil.isTemporaryAccount) && !Prefs.isReadingListSyncEnabled &&
if (RecommendedReadingListAbTest().isTestGroupUser() &&
!Prefs.isRecommendedReadingListOnboardingShown && !Prefs.isRecommendedReadingListEnabled) {
binding.onboardingView.setMessageLabel(getString(R.string.recommended_reading_list_onboarding_card_new))
binding.onboardingView.setMessageTitle(getString(R.string.recommended_reading_list_onboarding_card_title))
binding.onboardingView.setMessageText(getString(R.string.recommended_reading_list_onboarding_card_message))
binding.onboardingView.setPositiveButton(R.string.recommended_reading_list_onboarding_card_positive_button, {
startActivity(RecommendedReadingListOnboardingActivity.newIntent(requireContext()))
}, true)
binding.onboardingView.setNegativeButton(R.string.recommended_reading_list_onboarding_card_negative_button, {
binding.onboardingView.isVisible = false
Prefs.isRecommendedReadingListOnboardingShown = true
updateEmptyState(null)
FeedbackUtil.showMessage(this@ReadingListsFragment, getString(R.string.recommended_reading_list_onboarding_card_negative_snackbar))
}, false)
binding.onboardingView.isVisible = true
} else if ((AccountUtil.isLoggedIn && !AccountUtil.isTemporaryAccount) && !Prefs.isReadingListSyncEnabled &&
Prefs.isReadingListSyncReminderEnabled && !RemoteConfig.config.disableReadingListSync) {
binding.onboardingView.setMessageLabel(null)
binding.onboardingView.setMessageTitle(getString(R.string.reading_lists_sync_reminder_title))
binding.onboardingView.setMessageText(StringUtil.fromHtml(getString(R.string.reading_lists_sync_reminder_text)).toString())
binding.onboardingView.setImageResource(ResourceUtil.getThemedAttributeId(requireContext(), R.attr.sync_reading_list_prompt_drawable), true)
Expand All @@ -795,6 +813,7 @@ class ReadingListsFragment : Fragment(), SortReadingListsDialog.Callback, Readin
}, false)
binding.onboardingView.isVisible = true
} else if ((!AccountUtil.isLoggedIn || AccountUtil.isTemporaryAccount) && Prefs.isReadingListLoginReminderEnabled && !RemoteConfig.config.disableReadingListSync) {
binding.onboardingView.setMessageLabel(null)
binding.onboardingView.setMessageTitle(getString(R.string.reading_list_login_reminder_title))
binding.onboardingView.setMessageText(getString(R.string.reading_lists_login_reminder_text))
binding.onboardingView.setImageResource(ResourceUtil.getThemedAttributeId(requireContext(), R.attr.sync_reading_list_prompt_drawable), true)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package org.wikipedia.readinglist.database

import androidx.room.Entity
import androidx.room.PrimaryKey
import org.wikipedia.dataclient.WikiSite
import org.wikipedia.page.Namespace
import java.util.Date

@Entity
data class RecommendedPage(
@PrimaryKey(autoGenerate = true) val id: Int = 0,
val wiki: WikiSite,
val lang: String = "en",
val namespace: Namespace,
val timestamp: Date = Date(),
var apiTitle: String,
var displayTitle: String,
var description: String? = null,
var thumbUrl: String? = null,
var status: Int = 0 // 0 = new, 1 = expired
)
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ interface ReadingListPageDao {
@Query("SELECT * FROM ReadingListPage")
fun getAllPages(): List<ReadingListPage>

@Query("SELECT COUNT(*) FROM ReadingListPage")
suspend fun getPagesCount(): Int

@Query("SELECT * FROM ReadingListPage WHERE id = :id")
fun getPageById(id: Long): ReadingListPage?

Expand All @@ -56,6 +59,9 @@ interface ReadingListPageDao {
fun getPagesByParams(wiki: WikiSite, lang: String, ns: Namespace,
apiTitle: String, excludedStatus: Long): List<ReadingListPage>

@Query("SELECT * FROM ReadingListPage ORDER BY RANDOM() DESC LIMIT :limit")
suspend fun getPagesByRandom(limit: Int): List<ReadingListPage>

@Query("SELECT * FROM ReadingListPage WHERE listId = :listId AND status != :excludedStatus")
fun getPagesByListId(listId: Long, excludedStatus: Long): List<ReadingListPage>

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package org.wikipedia.readinglist.db

import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import androidx.room.Update
import org.wikipedia.dataclient.WikiSite
import org.wikipedia.readinglist.database.RecommendedPage

@Dao
interface RecommendedPageDao {
@Query("SELECT * FROM RecommendedPage WHERE status = 0 ORDER BY timestamp DESC")
suspend fun getNewRecommendedPages(): List<RecommendedPage>

@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertAll(recommendedPages: List<RecommendedPage>)

@Update
suspend fun updateAll(recommendedPages: List<RecommendedPage>)

@Query("SELECT COUNT(*) FROM RecommendedPage WHERE apiTitle = :apiTitle AND wiki = :wiki")
suspend fun findIfAny(apiTitle: String, wiki: WikiSite): Int

@Query("DELETE FROM RecommendedPage")
suspend fun deleteAll()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package org.wikipedia.readinglist.recommended

import org.wikipedia.analytics.ABTest
import org.wikipedia.util.ReleaseUtil

class RecommendedReadingListAbTest : ABTest("recommendedReadingList", GROUP_SIZE_2) {
fun getGroupName(): String {
return when (group) {
GROUP_2 -> "b" // test group
else -> "a" // control
}
}

fun isTestGroupUser(): Boolean {
return ReleaseUtil.isPreBetaRelease || group == GROUP_2
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package org.wikipedia.readinglist.recommended

import android.content.Context
import android.content.Intent
import androidx.fragment.app.Fragment
import org.wikipedia.activity.SingleFragmentActivity

class RecommendedReadingListOnboardingActivity : SingleFragmentActivity<Fragment>() {

public override fun createFragment(): Fragment {
val fromSetting = intent.getBooleanExtra(EXTRA_FROM_SETTING, false)
val startFromSourceSelection = intent.getBooleanExtra(EXTRA_START_FROM_SOURCE_SELECTION, true)
return if (startFromSourceSelection) {
RecommendedReadingListSourceFragment.newInstance(fromSetting)
} else {
// TODO: add this for the article interests screen when it is ready
Fragment()
}
}

companion object {

private const val EXTRA_START_FROM_SOURCE_SELECTION = "startFromSourceSelection"
private const val EXTRA_FROM_SETTING = "fromSetting"

fun newIntent(context: Context, startFromSourceSelection: Boolean = true, fromSetting: Boolean = false): Intent {
return Intent(context, RecommendedReadingListOnboardingActivity::class.java).apply {
putExtra(EXTRA_START_FROM_SOURCE_SELECTION, startFromSourceSelection)
putExtra(EXTRA_FROM_SETTING, fromSetting)
}
}
}
}
Loading
Loading