Skip to content
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

Rabbit Holes (feature branch) #5114

Merged
merged 28 commits into from
Nov 22, 2024
Merged
Show file tree
Hide file tree
Changes from 24 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
8015282
Rabbit Holes (feature branch)
dbrant Nov 12, 2024
942ac4f
Add missing stuff.
dbrant Nov 12, 2024
f2915c6
Merge branch 'main' into rabbit_holes_design
dbrant Nov 12, 2024
e09d69e
Merge branch 'main' into rabbit_holes_design
dbrant Nov 13, 2024
df77219
Lint.
dbrant Nov 13, 2024
26df4d2
Show survey after saving suggested reading list.
dbrant Nov 13, 2024
403e7f6
Use last two history entries for suggested reading list.
dbrant Nov 13, 2024
5197ce3
Rejigger arrangement of buttons.
dbrant Nov 13, 2024
1c1155d
Merge branch 'main' into rabbit_holes_design
dbrant Nov 14, 2024
ed39b5e
Whoops.
dbrant Nov 14, 2024
c016b74
Merge branch 'rabbit_holes_design' of github.com:wikimedia/apps-andro…
dbrant Nov 14, 2024
f01c17c
Deduplicate.
dbrant Nov 14, 2024
dbd4fab
Tweak strings.
dbrant Nov 14, 2024
8ad52ae
Merge branch 'main' into rabbit_holes_design
dbrant Nov 14, 2024
24a6f25
Merge branch 'main' into rabbit_holes_design
dbrant Nov 19, 2024
098faea
Wire up most of analytics.
dbrant Nov 19, 2024
ac32d73
Design feedback for reading lists.
dbrant Nov 21, 2024
0b71850
Further design feedback.
dbrant Nov 21, 2024
6ef9c26
Merge branch 'main' into rabbit_holes_design
dbrant Nov 22, 2024
0725c99
Exclude Main Page from potential sources of suggestions.
dbrant Nov 22, 2024
7a3f8d0
Also exclude Main Page from search suggestions.
dbrant Nov 22, 2024
6360ca5
Fix dark mode issue.
dbrant Nov 22, 2024
af80e5d
Deduplicate recent searches vs suggestion.
dbrant Nov 22, 2024
4ab693d
Touch up analytics.
dbrant Nov 22, 2024
cd92e84
Prevent showing survey twice.
dbrant Nov 22, 2024
8580cc3
Send events only when/where experiment is active.
dbrant Nov 22, 2024
236b14a
Simplify a bit.
dbrant Nov 22, 2024
755d985
A bit more feedback.
dbrant Nov 22, 2024
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
2 changes: 2 additions & 0 deletions app/src/main/java/org/wikipedia/Constants.kt
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,8 @@ object Constants {
USER_CONTRIB_ACTIVITY("userContribActivity"),
EDIT_ADD_IMAGE("editAddImage"),
SUGGESTED_EDITS_RECENT_EDITS("suggestedEditsRecentEdits"),
RABBIT_HOLE_SEARCH("rabbitHoleSearch"),
RABBIT_HOLE_READING_LIST("rabbitHoleReadingList")
}

enum class ImageEditType(name: String) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package org.wikipedia.analytics.eventplatform

import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import org.wikipedia.WikipediaApp
import org.wikipedia.analytics.metricsplatform.RabbitHolesAnalyticsHelper
import org.wikipedia.json.JsonUtil

object RabbitHolesEvent {
fun submit(
action: String,
activeInterface: String,
source: String? = null,
feedbackSelect: String? = null,
feedbackText: String? = null,
wikiId: String = WikipediaApp.instance.appOrSystemLanguageCode
) {
EventPlatformClient.submit(
AppInteractionEvent(
action,
activeInterface,
JsonUtil.encodeToString(ActionData(
groupAssigned = RabbitHolesAnalyticsHelper.abcTest.getGroupName(),
source = source,
feedbackText = feedbackText,
feedbackSelect = feedbackSelect
)).orEmpty(),
WikipediaApp.instance.languageState.appLanguageCode,
wikiId,
"app_rabbit_holes"
)
)
}

@Serializable
class ActionData(
@SerialName("group_assigned") val groupAssigned: String? = null,
@SerialName("source") val source: String? = null,
@SerialName("feedback_select") val feedbackSelect: String? = null,
@SerialName("feedback_text") val feedbackText: String? = null,
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package org.wikipedia.analytics.metricsplatform

import org.wikipedia.analytics.ABTest

class RabbitHolesABCTest : ABTest("rabbitHoles", GROUP_SIZE_3) {
fun getGroupName(): String {
return when (group) {
GROUP_2 -> "search"
GROUP_3 -> "reading_list"
else -> "control"
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package org.wikipedia.analytics.metricsplatform

import org.wikipedia.util.GeoUtil
import org.wikipedia.util.ReleaseUtil
import java.time.LocalDate

object RabbitHolesAnalyticsHelper {
val abcTest = RabbitHolesABCTest()

private val enabledCountries = listOf(
// sub-saharan africa
"AO", "BJ", "BW", "IO", "BF", "BI", "CV", "CM", "CF", "TD", "KM", "CG", "IC", "CD", "DJ", "GQ", "ER",
"SZ", "ET", "GA", "GM", "GH", "GN", "GW", "KE", "LS", "LR", "MG", "MW", "ML", "MR", "MU", "YT", "MZ",
"NA", "NE", "NG", "RE", "RW", "SH", "ST", "SN", "SC", "SL", "SO", "ZA", "SS", "TG", "UG", "TZ", "ZM",
"ZW",
// south asia
"IN", "PK", "BD", "LK", "MV", "NP", "BT", "AF"
)

val rabbitHolesEnabled get() = ReleaseUtil.isPreBetaRelease ||
(enabledCountries.contains(GeoUtil.geoIPCountry.orEmpty()) &&
LocalDate.now() <= LocalDate.of(2024, 12, 31))
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ class MwQueryPage {
@SerialName("pageprops") val pageProps: PageProps? = null
@SerialName("entityterms") val entityTerms: EntityTerms? = null

private val ns = 0
val ns = 0
val coordinates: List<Coordinates>? = null
private val thumbnail: Thumbnail? = null
val varianttitles: Map<String, String>? = null
Expand Down
2 changes: 2 additions & 0 deletions app/src/main/java/org/wikipedia/history/HistoryEntry.kt
Original file line number Diff line number Diff line change
Expand Up @@ -100,5 +100,7 @@ class HistoryEntry(
const val SOURCE_SINGLE_WEBVIEW = 40
const val SOURCE_SUGGESTED_EDITS_RECENT_EDITS = 41
const val SOURCE_FEED_PLACES = 42
const val SOURCE_RABBIT_HOLE_SEARCH = 43
const val SOURCE_RABBIT_HOLE_READING_LIST = 44
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ 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 WHERE lang = :lang ORDER BY timestamp DESC LIMIT :count")
suspend fun getLastHistoryEntries(lang: String, count: Int): List<HistoryEntry>

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

Expand Down
132 changes: 131 additions & 1 deletion app/src/main/java/org/wikipedia/page/PageActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -29,22 +29,30 @@ import androidx.lifecycle.repeatOnLifecycle
import androidx.preference.PreferenceManager
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.snackbar.Snackbar
import kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import kotlinx.serialization.json.encodeToJsonElement
import org.wikipedia.Constants
import org.wikipedia.Constants.InvokeSource
import org.wikipedia.R
import org.wikipedia.WikipediaApp
import org.wikipedia.activity.BaseActivity
import org.wikipedia.activity.SingleWebViewActivity
import org.wikipedia.analytics.ABTest
import org.wikipedia.analytics.eventplatform.ArticleLinkPreviewInteractionEvent
import org.wikipedia.analytics.eventplatform.BreadCrumbLogEvent
import org.wikipedia.analytics.eventplatform.DonorExperienceEvent
import org.wikipedia.analytics.eventplatform.RabbitHolesEvent
import org.wikipedia.analytics.metricsplatform.ArticleLinkPreviewInteraction
import org.wikipedia.analytics.metricsplatform.RabbitHolesAnalyticsHelper
import org.wikipedia.auth.AccountUtil
import org.wikipedia.commons.FilePageActivity
import org.wikipedia.concurrency.FlowEventBus
import org.wikipedia.database.AppDatabase
import org.wikipedia.databinding.ActivityPageBinding
import org.wikipedia.dataclient.ServiceFactory
import org.wikipedia.dataclient.WikiSite
import org.wikipedia.dataclient.donate.CampaignCollection
import org.wikipedia.dataclient.mwapi.MwQueryPage
Expand All @@ -58,13 +66,15 @@ import org.wikipedia.events.ChangeTextSizeEvent
import org.wikipedia.extensions.parcelableExtra
import org.wikipedia.gallery.GalleryActivity
import org.wikipedia.history.HistoryEntry
import org.wikipedia.json.JsonUtil
import org.wikipedia.language.LangLinksActivity
import org.wikipedia.navtab.NavTab
import org.wikipedia.notifications.AnonymousNotificationHelper
import org.wikipedia.notifications.NotificationActivity
import org.wikipedia.page.linkpreview.LinkPreviewDialog
import org.wikipedia.page.tabs.TabActivity
import org.wikipedia.readinglist.ReadingListActivity
import org.wikipedia.readinglist.ReadingListsShareHelper
import org.wikipedia.search.SearchActivity
import org.wikipedia.settings.Prefs
import org.wikipedia.staticdata.MainPageNameData
Expand All @@ -84,9 +94,11 @@ import org.wikipedia.util.UriUtil
import org.wikipedia.util.log.L
import org.wikipedia.views.FrameLayoutNavMenuTriggerer
import org.wikipedia.views.ObservableWebView
import org.wikipedia.views.SurveyDialog
import org.wikipedia.views.ViewUtil
import org.wikipedia.watchlist.WatchlistExpiry
import java.util.Locale
import java.util.concurrent.TimeUnit

class PageActivity : BaseActivity(), PageFragment.Callback, LinkPreviewDialog.LoadPageCallback, FrameLayoutNavMenuTriggerer.Callback {

Expand All @@ -104,6 +116,7 @@ class PageActivity : BaseActivity(), PageFragment.Callback, LinkPreviewDialog.Lo
private val isCabOpen get() = currentActionModes.isNotEmpty()
private var exclusiveTooltipRunnable: Runnable? = null
private var isTooltipShowing = false
private var suggestedSearchTerm: String? = null

private val requestEditSectionLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
if (it.resultCode == EditHandler.RESULT_REFRESH_PAGE) {
Expand Down Expand Up @@ -179,6 +192,13 @@ class PageActivity : BaseActivity(), PageFragment.Callback, LinkPreviewDialog.Lo
}
}

private val requestSuggestedReadingListLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
if (it.resultCode == RESULT_CANCELED) {
RabbitHolesEvent.submit("impression", "reading_list_warn")
FeedbackUtil.showMessage(this, R.string.suggested_reading_list_back_cancel)
}
}

public override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
PreferenceManager.setDefaultValues(this, R.xml.preferences, false)
Expand Down Expand Up @@ -214,7 +234,8 @@ class PageActivity : BaseActivity(), PageFragment.Callback, LinkPreviewDialog.Lo
binding.pageToolbarButtonSearch.setOnClickListener {
pageFragment.articleInteractionEvent?.logSearchWikipediaClick()
pageFragment.metricsPlatformArticleEventToolbarInteraction.logSearchWikipediaClick()
startActivity(SearchActivity.newIntent(this@PageActivity, InvokeSource.TOOLBAR, null))
startActivity(SearchActivity.newIntent(this@PageActivity, InvokeSource.TOOLBAR, null,
suggestedSearchQuery = suggestedSearchTerm))
}
binding.pageToolbarButtonTabs.updateTabCount(false)
binding.pageToolbarButtonTabs.setOnClickListener {
Expand Down Expand Up @@ -397,6 +418,8 @@ class PageActivity : BaseActivity(), PageFragment.Callback, LinkPreviewDialog.Lo
override fun onPageLoadComplete() {
removeTransitionAnimState()
maybeShowThemeTooltip()

maybeStartRabbitHole()
}

override fun onPageDismissBottomSheet() {
Expand Down Expand Up @@ -576,6 +599,9 @@ class PageActivity : BaseActivity(), PageFragment.Callback, LinkPreviewDialog.Lo
* foreground tab.
*/
private fun loadPage(pageTitle: PageTitle?, entry: HistoryEntry?, position: TabPosition) {

binding.pageToolbarButtonSearch.setText(R.string.search_hint)

if (isDestroyed || pageTitle == null || entry == null) {
return
}
Expand Down Expand Up @@ -794,6 +820,110 @@ class PageActivity : BaseActivity(), PageFragment.Callback, LinkPreviewDialog.Lo
}
}

private fun maybeStartRabbitHole() {
if (!RabbitHolesAnalyticsHelper.rabbitHolesEnabled || RabbitHolesAnalyticsHelper.abcTest.group == ABTest.GROUP_1) {
return
}
lifecycleScope.launch(CoroutineExceptionHandler { _, t ->
L.e(t)
}) {
pageFragment.title?.let { title ->
if (RabbitHolesAnalyticsHelper.abcTest.group == ABTest.GROUP_2) {
if (title.displayText != MainPageNameData.valueFor(title.wikiSite.languageCode)) {
val response = ServiceFactory.get(title.wikiSite).searchMoreLike("morelike:${title.prefixedText}", 3, 3)
response.query?.pages?.firstOrNull()?.let { page ->
applySuggestedSearchTerm(page.displayTitle(title.wikiSite.languageCode))
}
}
} else if (RabbitHolesAnalyticsHelper.abcTest.group == ABTest.GROUP_3 && !Prefs.suggestedReadingListDialogShown) {
val historyEntries = AppDatabase.instance.historyEntryDao().getLastHistoryEntries(title.wikiSite.languageCode, 2)
.filter { it.displayTitle != MainPageNameData.valueFor(title.wikiSite.languageCode) }
if (historyEntries.size < 2) {
return@launch
}

val pages = mutableListOf<MwQueryPage>()
historyEntries.forEach { entry ->
val response = ServiceFactory.get(title.wikiSite).searchMoreLike("morelike:${entry.apiTitle}", 10, 10)
response.query?.pages?.filter { it.title != historyEntries[0].apiTitle && it.title != historyEntries[1].apiTitle }?.take(5)?.let { pages.addAll(it) }
}

if (pages.isNotEmpty()) {
applySuggestedReadingList(historyEntries[0], historyEntries[1], pages)

Prefs.suggestedReadingListDialogShown = true
RabbitHolesEvent.submit("impression", "reading_list_prompt")

MaterialAlertDialogBuilder(this@PageActivity)
.setTitle(R.string.suggested_reading_list_dialog_title)
.setMessage(R.string.suggested_reading_list_dialog_body)
.setPositiveButton(R.string.suggested_reading_list_dialog_positive) { _, _ ->
RabbitHolesEvent.submit("enter_click", "reading_list_prompt")
requestSuggestedReadingListLauncher.launch(ReadingListActivity.newIntent(this@PageActivity, true, suggestedList = true))
}
.setNegativeButton(R.string.suggested_reading_list_dialog_negative) { _, _ ->
RabbitHolesEvent.submit("ignore_click", "reading_list_prompt")
FeedbackUtil.showMessage(this@PageActivity, R.string.suggested_reading_list_later_snackbar)
}
.show()
}
}
}
}
maybeShowRabbitHolesSurvey()
}

private fun applySuggestedSearchTerm(term: String) {
suggestedSearchTerm = term
binding.pageToolbarButtonSearch.text = term
}

private fun applySuggestedReadingList(basedOnTitle1: HistoryEntry, basedOnTitle2: HistoryEntry, pages: List<MwQueryPage>) {
val listItems = pages.map {
JsonUtil.json.encodeToJsonElement(
ReadingListsShareHelper.ExportedReadingListPage(
basedOnTitle1.title.wikiSite.languageCode,
it.displayTitle(basedOnTitle1.title.wikiSite.languageCode),
it.ns,
it.description,
it.thumbUrl()
)
)
}
val readingList = ReadingListsShareHelper.ExportedReadingList(
list = mapOf(basedOnTitle1.title.wikiSite.languageCode to listItems),
name = getString(R.string.suggested_reading_list_title),
description = getString(R.string.suggested_reading_list_description_multi,
StringUtil.fromHtml(basedOnTitle1.title.displayText).toString(),
StringUtil.fromHtml(basedOnTitle2.title.displayText).toString())
)
Prefs.importReadingListsDialogShown = false
Prefs.suggestedReadingListsData = JsonUtil.encodeToString(readingList)
}

private fun maybeShowRabbitHolesSurvey() {
if (Prefs.suggestedContentSurveyShown) {
return
}
lifecycleScope.launch(CoroutineExceptionHandler { _, t ->
L.e(t)
}) {
delay(TimeUnit.SECONDS.toMillis(if (ReleaseUtil.isDevRelease) 1L else 10L))
pageFragment.historyEntry?.let {
if (it.source == HistoryEntry.SOURCE_RABBIT_HOLE_SEARCH) {
Prefs.suggestedContentSurveyShown = true
SurveyDialog.showFeedbackOptionsDialog(
this@PageActivity,
titleId = R.string.rabbit_holes_survey_dialog_title,
messageId = R.string.rabbit_holes_survey_dialog_body,
snackbarMessageId = R.string.survey_dialog_submitted_snackbar,
invokeSource = InvokeSource.RABBIT_HOLE_SEARCH
)
}
}
}
}

companion object {
private const val LANGUAGE_CODE_BUNDLE_KEY = "language"
private const val EXCEPTION_MESSAGE_WEBVIEW = "webview"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,23 +35,29 @@ class ReadingListActivity : SingleFragmentActivity<ReadingListFragment>() {
super.onBackPressed()
if (intent.getBooleanExtra(EXTRA_READING_LIST_PREVIEW, false)) {
ReadingListsAnalyticsHelper.logReceiveCancel(this, fragment.readingList)
} else if (intent.getBooleanExtra(EXTRA_READING_LIST_SUGGESTED, false)) {
setResult(RESULT_CANCELED)
}
}

companion object {
private const val EXTRA_READING_LIST_TITLE = "readingListTitle"
const val EXTRA_READING_LIST_ID = "readingListId"
const val EXTRA_READING_LIST_PREVIEW = "previewReadingList"
const val EXTRA_READING_LIST_SUGGESTED = "suggestedReadingList"
const val EXTRA_READING_LIST_SUGGESTED_SAVE = "suggestedReadingListSave"

fun newIntent(context: Context, list: ReadingList): Intent {
return Intent(context, ReadingListActivity::class.java)
.putExtra(EXTRA_READING_LIST_TITLE, list.title)
.putExtra(EXTRA_READING_LIST_ID, list.id)
}

fun newIntent(context: Context, preview: Boolean): Intent {
fun newIntent(context: Context, preview: Boolean, suggestedList: Boolean = false, suggestedListSave: Boolean = false): Intent {
return Intent(context, ReadingListActivity::class.java)
.putExtra(EXTRA_READING_LIST_PREVIEW, preview)
.putExtra(EXTRA_READING_LIST_SUGGESTED, suggestedList)
.putExtra(EXTRA_READING_LIST_SUGGESTED_SAVE, suggestedListSave)
}
}
}
Loading
Loading