From 0333c79f107b34eba21e69f3abeb10be0bbcac04 Mon Sep 17 00:00:00 2001 From: williamrai Date: Tue, 8 Jul 2025 17:11:59 -0400 Subject: [PATCH 01/18] - refactor step1 --- .../java/org/wikipedia/page/PageActivity.kt | 23 +- .../java/org/wikipedia/page/PageFragment.kt | 320 ++++++++++------- .../wikipedia/page/PageFragmentLoadState.kt | 71 ++-- .../page/pageload/PageDataFetcher.kt | 99 +++++ .../org/wikipedia/page/pageload/PageLoad.kt | 38 ++ .../org/wikipedia/page/pageload/PageLoader.kt | 338 ++++++++++++++++++ .../java/org/wikipedia/page/pageload/Test2.kt | 308 ++++++++++++++++ 7 files changed, 1006 insertions(+), 191 deletions(-) create mode 100644 app/src/main/java/org/wikipedia/page/pageload/PageDataFetcher.kt create mode 100644 app/src/main/java/org/wikipedia/page/pageload/PageLoad.kt create mode 100644 app/src/main/java/org/wikipedia/page/pageload/PageLoader.kt create mode 100644 app/src/main/java/org/wikipedia/page/pageload/Test2.kt diff --git a/app/src/main/java/org/wikipedia/page/PageActivity.kt b/app/src/main/java/org/wikipedia/page/PageActivity.kt index 8de31ec3a85..1b38ca319e0 100644 --- a/app/src/main/java/org/wikipedia/page/PageActivity.kt +++ b/app/src/main/java/org/wikipedia/page/PageActivity.kt @@ -62,6 +62,7 @@ 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.pageload.PageLoadOptions import org.wikipedia.page.tabs.TabActivity import org.wikipedia.readinglist.ReadingListActivity import org.wikipedia.readinglist.ReadingListMode @@ -119,7 +120,7 @@ class PageActivity : BaseActivity(), PageFragment.Callback, LinkPreviewDialog.Lo // and reload the page... pageFragment.model.title?.let { title -> pageFragment.model.curEntry?.let { entry -> - pageFragment.loadPage(title, entry, pushBackStack = false, squashBackstack = false, isRefresh = true) +// pageFragment.loadPage(title, entry, pushBackStack = false, squashBackstack = false, isRefresh = true) } } } @@ -613,13 +614,21 @@ class PageActivity : BaseActivity(), PageFragment.Callback, LinkPreviewDialog.Lo // Close the link preview, if one is open. hideLinkPreview() onPageCloseActionMode() - when (position) { - TabPosition.CURRENT_TAB -> pageFragment.loadPage(pageTitle, entry, pushBackStack = true, squashBackstack = false) - TabPosition.CURRENT_TAB_SQUASH -> pageFragment.loadPage(pageTitle, entry, pushBackStack = true, squashBackstack = true) - TabPosition.NEW_TAB_BACKGROUND -> pageFragment.openInNewBackgroundTab(pageTitle, entry) - TabPosition.NEW_TAB_FOREGROUND -> pageFragment.openInNewForegroundTab(pageTitle, entry) - else -> pageFragment.openFromExistingTab(pageTitle, entry) + val options = when(position) { + TabPosition.CURRENT_TAB -> PageLoadOptions(tabPosition = position) + TabPosition.CURRENT_TAB_SQUASH -> PageLoadOptions(tabPosition = position, squashBackStack = true) + TabPosition.NEW_TAB_BACKGROUND -> PageLoadOptions(tabPosition = position) + TabPosition.NEW_TAB_FOREGROUND -> PageLoadOptions(tabPosition = position) + TabPosition.EXISTING_TAB -> PageLoadOptions(tabPosition = position) } + pageFragment.loadPage(pageTitle, entry, options) +// when (position) { +// TabPosition.CURRENT_TAB -> pageFragment.loadPage(pageTitle, entry, pushBackStack = true, squashBackstack = false) +// TabPosition.CURRENT_TAB_SQUASH -> pageFragment.loadPage(pageTitle, entry, pushBackStack = true, squashBackstack = true) +// TabPosition.NEW_TAB_BACKGROUND -> pageFragment.openInNewBackgroundTab(pageTitle, entry) +// TabPosition.NEW_TAB_FOREGROUND -> pageFragment.openInNewForegroundTab(pageTitle, entry) +// else -> pageFragment.openFromExistingTab(pageTitle, entry) +// } } } diff --git a/app/src/main/java/org/wikipedia/page/PageFragment.kt b/app/src/main/java/org/wikipedia/page/PageFragment.kt index 89f4a3d226b..fdeae978b84 100644 --- a/app/src/main/java/org/wikipedia/page/PageFragment.kt +++ b/app/src/main/java/org/wikipedia/page/PageFragment.kt @@ -82,11 +82,15 @@ import org.wikipedia.main.MainActivity import org.wikipedia.media.AvPlayer import org.wikipedia.navtab.NavTab import org.wikipedia.notifications.PollNotificationWorker +import org.wikipedia.page.PageActivity.TabPosition import org.wikipedia.page.action.PageActionItem import org.wikipedia.page.campaign.CampaignDialog import org.wikipedia.page.edithistory.EditHistoryListActivity import org.wikipedia.page.issues.PageIssuesDialog import org.wikipedia.page.leadimages.LeadImagesHandler +import org.wikipedia.page.pageload.PageLoadOptions +import org.wikipedia.page.pageload.PageLoadRequest +import org.wikipedia.page.pageload.PageLoader import org.wikipedia.page.references.PageReferences import org.wikipedia.page.references.ReferenceDialog import org.wikipedia.page.shareafact.ShareHandler @@ -152,16 +156,16 @@ class PageFragment : Fragment(), BackPressedHandler, CommunicationBridge.Communi private val pageRefreshListener = OnRefreshListener { refreshPage() } private val pageActionItemCallback = PageActionItemCallback() + private lateinit var pageLoader: PageLoader private lateinit var bridge: CommunicationBridge private lateinit var leadImagesHandler: LeadImagesHandler - private lateinit var pageFragmentLoadState: PageFragmentLoadState private lateinit var bottomBarHideHandler: ViewHideHandler internal var articleInteractionEvent: ArticleInteractionEvent? = null internal var metricsPlatformArticleEventToolbarInteraction = ArticleToolbarInteraction(this) - private var pageRefreshed = false - private var errorState = false + var pageRefreshed = false + var errorState = false private var scrolledUpForThemeChange = false - private var references: PageReferences? = null + var references: PageReferences? = null private var avPlayer: AvPlayer? = null private var avCallback: AvCallback? = null private var sections: MutableList
? = null @@ -180,9 +184,9 @@ class PageFragment : Fragment(), BackPressedHandler, CommunicationBridge.Communi lateinit var editHandler: EditHandler var revision = 0L - private val shouldCreateNewTab get() = currentTab.backStack.isNotEmpty() - private val backgroundTabPosition get() = 0.coerceAtLeast(foregroundTabPosition - 1) - private val foregroundTabPosition get() = app.tabList.size + val shouldCreateNewTab get() = currentTab.backStack.isNotEmpty() + val backgroundTabPosition get() = 0.coerceAtLeast(foregroundTabPosition - 1) + val foregroundTabPosition get() = app.tabList.size private val tabLayoutOffsetParams get() = LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, binding.pageActionsTabLayout.height) val currentTab get() = app.tabList.last() val title get() = model.title @@ -237,11 +241,12 @@ class PageFragment : Fragment(), BackPressedHandler, CommunicationBridge.Communi } } + editHandler = EditHandler(this, bridge) sidePanelHandler = SidePanelHandler(this, bridge) leadImagesHandler = LeadImagesHandler(this, webView, binding.pageHeaderView, callback()) shareHandler = ShareHandler(this, bridge) - pageFragmentLoadState = PageFragmentLoadState(model, this, webView, bridge, leadImagesHandler, currentTab) + pageLoader = PageLoader(this, webView, bridge, leadImagesHandler, currentTab) if (callback() != null) { LongPressHandler(webView, HistoryEntry.SOURCE_INTERNAL_LINK, PageContainerLongPressHandler(this)) @@ -282,9 +287,10 @@ class PageFragment : Fragment(), BackPressedHandler, CommunicationBridge.Communi } activeTimer.pause() addTimeSpentReading(activeTimer.elapsedSec) - pageFragmentLoadState.updateCurrentBackStackItem() + + pageLoader.updateCurrentBackStackItem() app.commitTabState() - val time = if (app.tabList.size >= 1 && !pageFragmentLoadState.backStackEmpty()) System.currentTimeMillis() else 0 + val time = if (app.tabList.size >= 1 && !pageLoader.backStackEmpty()) System.currentTimeMillis() else 0 Prefs.pageLastShown = time articleInteractionEvent?.pause() metricsPlatformArticleEventToolbarInteraction.pause() @@ -310,7 +316,7 @@ class PageFragment : Fragment(), BackPressedHandler, CommunicationBridge.Communi // if the screen orientation changes, then re-layout the lead image container, // but only if we've finished fetching the page. if (!bridge.isLoading && !errorState) { - pageFragmentLoadState.onConfigurationChanged() + pageLoader.onConfigurationChanged() } } @@ -321,7 +327,7 @@ class PageFragment : Fragment(), BackPressedHandler, CommunicationBridge.Communi sidePanelHandler.hide() return true } - if (pageFragmentLoadState.goBack()) { + if (pageLoader.goBack()) { return true } // if the current tab can no longer go back, then close the tab before exiting @@ -508,11 +514,11 @@ class PageFragment : Fragment(), BackPressedHandler, CommunicationBridge.Communi if (position < app.tabList.size - 1) { val tab = app.tabList.removeAt(position) app.tabList.add(tab) - pageFragmentLoadState.setTab(tab) + pageLoader.setTab(tab) } if (app.tabCount > 0) { app.tabList.last().squashBackstack() - pageFragmentLoadState.loadFromBackStack() + pageLoader.loadFromBackStack() } } @@ -521,49 +527,49 @@ class PageFragment : Fragment(), BackPressedHandler, CommunicationBridge.Communi title == it.backStackPositionTitle }?.let { app.tabList.indexOf(it) } ?: -1 } - private fun openInNewTab(title: PageTitle, entry: HistoryEntry, position: Int) { - val selectedTabPosition = selectedTabPosition(title) - if (selectedTabPosition >= 0) { - setCurrentTabAndReset(selectedTabPosition) - return - } - if (shouldCreateNewTab) { - // create a new tab - val tab = Tab() - val isForeground = position == foregroundTabPosition - // if the requested position is at the top, then make its backstack current - if (isForeground) { - pageFragmentLoadState.setTab(tab) - } - // put this tab in the requested position - app.tabList.add(position, tab) - trimTabCount() - // add the requested page to its backstack - tab.backStack.add(PageBackStackItem(title, entry)) - if (!isForeground) { - lifecycleScope.launch(CoroutineExceptionHandler { _, t -> L.e(t) }) { - ServiceFactory.get(title.wikiSite).getInfoByPageIdsOrTitles(null, title.prefixedText) - .query?.firstPage()?.let { page -> - WikipediaApp.instance.tabList.find { it.backStackPositionTitle == title }?.backStackPositionTitle?.apply { - thumbUrl = page.thumbUrl() - description = page.description - } - } - } - } - requireActivity().invalidateOptionsMenu() - } else { - pageFragmentLoadState.setTab(currentTab) - currentTab.backStack.add(PageBackStackItem(title, entry)) - } - } - - private fun dismissBottomSheet() { +// private fun openInNewTab(title: PageTitle, entry: HistoryEntry, position: Int) { +// val selectedTabPosition = selectedTabPosition(title) +// if (selectedTabPosition >= 0) { +// setCurrentTabAndReset(selectedTabPosition) +// return +// } +// if (shouldCreateNewTab) { +// // create a new tab +// val tab = Tab() +// val isForeground = position == foregroundTabPosition +// // if the requested position is at the top, then make its backstack current +// if (isForeground) { +// pageFragmentLoadState.setTab(tab) +// } +// // put this tab in the requested position +// app.tabList.add(position, tab) +// trimTabCount() +// // add the requested page to its backstack +// tab.backStack.add(PageBackStackItem(title, entry)) +// if (!isForeground) { +// lifecycleScope.launch(CoroutineExceptionHandler { _, t -> L.e(t) }) { +// ServiceFactory.get(title.wikiSite).getInfoByPageIdsOrTitles(null, title.prefixedText) +// .query?.firstPage()?.let { page -> +// WikipediaApp.instance.tabList.find { it.backStackPositionTitle == title }?.backStackPositionTitle?.apply { +// thumbUrl = page.thumbUrl() +// description = page.description +// } +// } +// } +// } +// requireActivity().invalidateOptionsMenu() +// } else { +// pageFragmentLoadState.setTab(currentTab) +// currentTab.backStack.add(PageBackStackItem(title, entry)) +// } +// } + + fun dismissBottomSheet() { ExclusiveBottomSheetPresenter.dismiss(childFragmentManager) callback()?.onPageDismissBottomSheet() } - private fun updateProgressBar(visible: Boolean) { + fun updateProgressBar(visible: Boolean) { callback()?.onPageUpdateProgressBar(visible) } @@ -878,9 +884,9 @@ class PageFragment : Fragment(), BackPressedHandler, CommunicationBridge.Communi } fun reloadFromBackstack(forceReload: Boolean = true) { - if (pageFragmentLoadState.setTab(currentTab) || forceReload) { - if (!pageFragmentLoadState.backStackEmpty()) { - pageFragmentLoadState.loadFromBackStack() + if (pageLoader.setTab(currentTab) || forceReload) { + if (!pageLoader.backStackEmpty()) { + pageLoader.loadFromBackStack() } else { callback()?.onPageLoadMainPageInForegroundTab() } @@ -930,85 +936,117 @@ class PageFragment : Fragment(), BackPressedHandler, CommunicationBridge.Communi fun openInNewBackgroundTab(title: PageTitle, entry: HistoryEntry) { if (app.tabCount == 0) { - openInNewTab(title, entry, foregroundTabPosition) - pageFragmentLoadState.loadFromBackStack() - } else { - openInNewTab(title, entry, backgroundTabPosition) - (requireActivity() as PageActivity).animateTabsButton() - } - } +// openInNewTab(title, entry, foregroundTabPosition) +// pageFragmentLoadState.loadFromBackStack() + loadPage(title, entry, PageLoadOptions(tabPosition = TabPosition.NEW_TAB_FOREGROUND)) - fun openInNewForegroundTab(title: PageTitle, entry: HistoryEntry) { - openInNewTab(title, entry, foregroundTabPosition) - pageFragmentLoadState.loadFromBackStack() - } - - fun openFromExistingTab(title: PageTitle, entry: HistoryEntry) { - val selectedTabPosition = selectedTabPosition(title) - - if (selectedTabPosition == -1) { - loadPage(title, entry, pushBackStack = true, squashBackstack = false) - return - } - setCurrentTabAndReset(selectedTabPosition) - } - - fun loadPage(title: PageTitle, entry: HistoryEntry, pushBackStack: Boolean, squashBackstack: Boolean, isRefresh: Boolean = false) { - // is the new title the same as what's already being displayed? - if (currentTab.backStack.isNotEmpty() && - title == currentTab.backStack[currentTab.backStackPosition].title) { - if (model.page == null || isRefresh) { - pageFragmentLoadState.loadFromBackStack() - } else if (!title.fragment.isNullOrEmpty()) { - scrollToSection(title.fragment!!) - } - return - } - if (squashBackstack) { - if (app.tabCount > 0) { - app.tabList.last().clearBackstack() - } - } - loadPage(title, entry, pushBackStack, 0, isRefresh) - } - - fun loadPage(title: PageTitle, entry: HistoryEntry, pushBackStack: Boolean, stagedScrollY: Int, isRefresh: Boolean = false) { - // clear the title in case the previous page load had failed. - clearActivityActionBarTitle() - - if (ExclusiveBottomSheetPresenter.getCurrentBottomSheet(childFragmentManager) !is ThemeChooserDialog) { - dismissBottomSheet() - } - - if (AccountUtil.isLoggedIn) { - // explicitly check notifications for the current user - PollNotificationWorker.schedulePollNotificationJob(requireContext()) - } - - EventPlatformClient.AssociationController.beginNewPageView() - - // update the time spent reading of the current page, before loading the new one - addTimeSpentReading(activeTimer.elapsedSec) - activeTimer.reset() - callback()?.onPageSetToolbarElevationEnabled(false) - sidePanelHandler.setEnabled(false) - errorState = false - binding.pageError.visibility = View.GONE - model.title = title - model.curEntry = entry - model.page = null - model.readingListPage = null - model.forceNetwork = isRefresh - webView.visibility = View.VISIBLE - binding.pageActionsTabLayout.visibility = View.VISIBLE - binding.pageActionsTabLayout.enableAllTabs() - updateProgressBar(true) - pageRefreshed = isRefresh - references = null - revision = 0 - pageFragmentLoadState.load(pushBackStack) - scrollTriggerListener.stagedScrollY = stagedScrollY - } + } else { +// openInNewTab(title, entry, backgroundTabPosition) +// (requireActivity() as PageActivity).animateTabsButton() + loadPage(title, entry, PageLoadOptions(tabPosition = TabPosition.NEW_TAB_BACKGROUND)) + + } + } + +// fun openInNewForegroundTab(title: PageTitle, entry: HistoryEntry) { +//// openInNewTab(title, entry, foregroundTabPosition) +//// pageFragmentLoadState.loadFromBackStack() +// loadPage(title, entry, PageLoadOptions(tabPosition = TabPosition.NEW_TAB_FOREGROUND)) +// +// } +// +// fun openFromExistingTab(title: PageTitle, entry: HistoryEntry) { +//// val selectedTabPosition = selectedTabPosition(title) +//// +//// if (selectedTabPosition == -1) { +//// loadPage(title, entry, pushBackStack = true, squashBackstack = false) +//// return +//// } +//// setCurrentTabAndReset(selectedTabPosition) +// loadPage(title, entry, PageLoadOptions(tabPosition = TabPosition.EXISTING_TAB)) +// } + + fun loadPage(title: PageTitle, entry: HistoryEntry, options: PageLoadOptions = PageLoadOptions()) { + val request = PageLoadRequest(title, entry, options) + pageLoader.loadPage(request) + } + +// fun loadPage(title: PageTitle, entry: HistoryEntry, pushBackStack: Boolean, squashBackstack: Boolean, isRefresh: Boolean = false) { +// // is the new title the same as what's already being displayed? +// // done +// if (currentTab.backStack.isNotEmpty() && +// title == currentTab.backStack[currentTab.backStackPosition].title) { +// if (model.page == null || isRefresh) { +// pageFragmentLoadState.loadFromBackStack() +// } else if (!title.fragment.isNullOrEmpty()) { +// scrollToSection(title.fragment!!) +// } +// return +// } +// +// // done +// if (squashBackstack) { +// if (app.tabCount > 0) { +// app.tabList.last().clearBackstack() +// } +// } +// loadPage(title, entry, pushBackStack, 0, isRefresh) +// } + +// fun loadPage(title: PageTitle, entry: HistoryEntry, pushBackStack: Boolean, stagedScrollY: Int, isRefresh: Boolean = false) { +// // clear the title in case the previous page load had failed. +// // done +// clearActivityActionBarTitle() +// +// // done +// if (ExclusiveBottomSheetPresenter.getCurrentBottomSheet(childFragmentManager) !is ThemeChooserDialog) { +// dismissBottomSheet() +// } +// +// // not done +// if (AccountUtil.isLoggedIn) { +// // explicitly check notifications for the current user +// PollNotificationWorker.schedulePollNotificationJob(requireContext()) +// } +// +// // not done +// EventPlatformClient.AssociationController.beginNewPageView() +// +// // update the time spent reading of the current page, before loading the new one +// // not done +// addTimeSpentReading(activeTimer.elapsedSec) +// activeTimer.reset() +// +// // done +// updateProgressBar(true) +// callback()?.onPageSetToolbarElevationEnabled(false) +// sidePanelHandler.setEnabled(false) +// +// // done +// errorState = false +// binding.pageError.visibility = View.GONE +// webView.visibility = View.VISIBLE +// binding.pageActionsTabLayout.visibility = View.VISIBLE +// binding.pageActionsTabLayout.enableAllTabs() +// +// // done +// model.title = title +// model.curEntry = entry +// model.page = null +// model.readingListPage = null +// model.forceNetwork = isRefresh +// +// // done +// pageRefreshed = isRefresh +// references = null +// revision = 0 +// +// // main load state +// pageFragmentLoadState.load(pushBackStack) +// +// // @TODO: not applied +// scrollTriggerListener.stagedScrollY = stagedScrollY +// } fun updateFontSize() { webView.settings.defaultFontSize = app.getFontSize().toInt() @@ -1119,7 +1157,7 @@ class PageFragment : Fragment(), BackPressedHandler, CommunicationBridge.Communi } } - private fun scrollToSection(sectionAnchor: String) { + fun scrollToSection(sectionAnchor: String) { if (!isAdded) { return } @@ -1164,12 +1202,18 @@ class PageFragment : Fragment(), BackPressedHandler, CommunicationBridge.Communi binding.pageActionsTabLayout.enableAllTabs() errorState = false model.curEntry = HistoryEntry(title, HistoryEntry.SOURCE_HISTORY) - loadPage(title, entry, false, stagedScrollY, app.isOnline) + // loadPage(title, entry, false, stagedScrollY, app.isOnline) + loadPage(title, entry, PageLoadOptions( + pushbackStack = false, + isRefresh = app.isOnline, + stagedScrollY = stagedScrollY + )) } } } - private fun clearActivityActionBarTitle() { + + fun clearActivityActionBarTitle() { val currentActivity = requireActivity() if (currentActivity is PageActivity) { currentActivity.clearActionBarTitle() @@ -1202,7 +1246,7 @@ class PageFragment : Fragment(), BackPressedHandler, CommunicationBridge.Communi } fun goForward() { - pageFragmentLoadState.goForward() + pageLoader.goForward() } fun showBottomSheet(dialog: BottomSheetDialogFragment) { diff --git a/app/src/main/java/org/wikipedia/page/PageFragmentLoadState.kt b/app/src/main/java/org/wikipedia/page/PageFragmentLoadState.kt index eae06a82c71..064845f0d4f 100644 --- a/app/src/main/java/org/wikipedia/page/PageFragmentLoadState.kt +++ b/app/src/main/java/org/wikipedia/page/PageFragmentLoadState.kt @@ -42,6 +42,7 @@ class PageFragmentLoadState(private var model: PageViewModel, private var currentTab: Tab) { fun load(pushBackStack: Boolean) { + // done if (pushBackStack && model.title != null && model.curEntry != null) { // update the topmost entry in the backstack, before we start overwriting things. updateCurrentBackStackItem() @@ -50,17 +51,6 @@ class PageFragmentLoadState(private var model: PageViewModel, pageLoad() } - fun loadFromBackStack() { - if (currentTab.backStack.isEmpty()) { - return - } - val item = currentTab.backStack[currentTab.backStackPosition] - // display the page based on the backstack item, stage the scrollY position based on - // the backstack item. - fragment.loadPage(item.title, item.historyEntry, false, item.scrollY) - L.d("Loaded page " + item.title.displayText + " from backstack") - } - fun updateCurrentBackStackItem() { if (currentTab.backStack.isEmpty()) { return @@ -73,41 +63,6 @@ class PageFragmentLoadState(private var model: PageViewModel, } } - fun setTab(tab: Tab): Boolean { - val isDifferent = tab != currentTab - currentTab = tab - return isDifferent - } - - fun goBack(): Boolean { - if (currentTab.canGoBack()) { - currentTab.moveBack() - if (!backStackEmpty()) { - loadFromBackStack() - return true - } - } - return false - } - - fun goForward(): Boolean { - if (currentTab.canGoForward()) { - currentTab.moveForward() - loadFromBackStack() - return true - } - return false - } - - fun backStackEmpty(): Boolean { - return currentTab.backStack.isEmpty() - } - - fun onConfigurationChanged() { - leadImagesHandler.loadLeadImage() - bridge.execute(JavaScriptActionHandler.setTopMargin(leadImagesHandler.topMargin)) - } - private fun commonSectionFetchOnCatch(caught: Throwable) { if (!fragment.isAdded) { return @@ -122,16 +77,23 @@ class PageFragmentLoadState(private var model: PageViewModel, L.e("Page details network error: ", throwable) commonSectionFetchOnCatch(throwable) }) { + // not done model.readingListPage = AppDatabase.instance.readingListPageDao().findPageInAnyList(title) + // done fragment.updateQuickActionsAndMenuOptions() fragment.requireActivity().invalidateOptionsMenu() fragment.callback()?.onPageUpdateProgressBar(true) + model.page = null + + // done val delayLoadHtml = title.prefixedText.contains(":") if (!delayLoadHtml) { bridge.resetHtml(title) } + + // done if (title.namespace() === Namespace.SPECIAL) { // Short-circuit the entire process of fetching the Summary, since Special: pages // are not supported in RestBase. @@ -142,11 +104,13 @@ class PageFragmentLoadState(private var model: PageViewModel, return@launch } + // done val pageSummaryRequest = async { ServiceFactory.getRest(title.wikiSite).getSummaryResponse(title.prefixedText, cacheControl = model.cacheControl.toString(), saveHeader = if (model.isInReadingList) OfflineCacheInterceptor.SAVE_HEADER_SAVE else null, langHeader = title.wikiSite.languageCode, titleHeader = UriUtil.encodeURL(title.prefixedText)) } + // done val makeWatchRequest = WikipediaApp.instance.isOnline && AccountUtil.isLoggedIn val watchedRequest = async { if (makeWatchRequest) { @@ -157,6 +121,7 @@ class PageFragmentLoadState(private var model: PageViewModel, MwQueryResponse() } } + // done val categoriesRequest = async { if (!makeWatchRequest && WikipediaApp.instance.isOnline) { ServiceFactory.get(title.wikiSite).getCategoriesProps(title.text) @@ -167,29 +132,43 @@ class PageFragmentLoadState(private var model: PageViewModel, val pageSummaryResponse = pageSummaryRequest.await() val watchedResponse = watchedRequest.await() val categoriesResponse = categoriesRequest.await() + // done val isWatched = watchedResponse.query?.firstPage()?.watched == true val hasWatchlistExpiry = watchedResponse.query?.firstPage()?.hasWatchlistExpiry() == true if (pageSummaryResponse.body() == null) { throw RuntimeException("Summary response was invalid.") } + // done val redirectedFrom = if (pageSummaryResponse.raw().priorResponse?.isRedirect == true) model.title?.displayText else null + + // partial done createPageModel(pageSummaryResponse, isWatched, hasWatchlistExpiry) + + // not done if (OfflineCacheInterceptor.SAVE_HEADER_SAVE == pageSummaryResponse.headers()[OfflineCacheInterceptor.SAVE_HEADER]) { showPageOfflineMessage(pageSummaryResponse.headers().getInstant("date")) } + // done val categoryList = (categoriesResponse.query ?: watchedResponse.query)?.firstPage()?.categories?.map { category -> Category(title = category.title, lang = title.wikiSite.languageCode) }.orEmpty() + + // not done if (categoryList.isNotEmpty()) { AppDatabase.instance.categoryDao().upsertAll(categoryList) } + + // not done if (delayLoadHtml) { bridge.resetHtml(title) } + + // done fragment.onPageMetadataLoaded(redirectedFrom) + // not done if (AnonymousNotificationHelper.shouldCheckAnonNotifications(watchedResponse)) { checkAnonNotifications(title) } diff --git a/app/src/main/java/org/wikipedia/page/pageload/PageDataFetcher.kt b/app/src/main/java/org/wikipedia/page/pageload/PageDataFetcher.kt new file mode 100644 index 00000000000..f2845dd7dae --- /dev/null +++ b/app/src/main/java/org/wikipedia/page/pageload/PageDataFetcher.kt @@ -0,0 +1,99 @@ +package org.wikipedia.page.pageload + +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.mwapi.MwQueryResponse +import org.wikipedia.dataclient.okhttp.OfflineCacheInterceptor +import org.wikipedia.dataclient.page.PageSummary +import org.wikipedia.history.HistoryEntry +import org.wikipedia.notifications.AnonymousNotificationHelper +import org.wikipedia.page.PageTitle +import org.wikipedia.util.UriUtil +import retrofit2.Response + +class PageDataFetcher { + + suspend fun fetchPage(title: PageTitle, entry: HistoryEntry, forceNetwork: Boolean = false): PageResult { + return try { + val cacheControl = if (forceNetwork) "no-cache" else "default" + + val pageSummary = fetchPageSummary(title, cacheControl) + val watchStatus = fetchWatchStatus(title) + val categories = fetchCategories(title, watchStatus.myQueryResponse) + + if (pageSummary.body() == null) { + throw RuntimeException("Summary response was invalid.") + } + + PageResult.Success( + pageSummaryResponse = pageSummary, + categories = categories, + isWatched = watchStatus.isWatched, + hasWatchlistExpiry = watchStatus.hasWatchlistExpiry, + redirectedFrom = if (pageSummary.raw().priorResponse?.isRedirect == true) title.displayText else null + ) + } catch (e: Exception) { + PageResult.Error(e) + } + } + private suspend fun fetchPageSummary(title: PageTitle, cacheControl: String): Response { + return ServiceFactory.getRest(title.wikiSite).getSummaryResponse( + title = title.prefixedText, + cacheControl = cacheControl, + saveHeader = if (isInReadingList(title)) OfflineCacheInterceptor.SAVE_HEADER_SAVE else null, + langHeader = title.wikiSite.languageCode, + titleHeader = UriUtil.encodeURL(title.prefixedText) + ) + } + + private suspend fun fetchWatchStatus(title: PageTitle): WatchStatus { + return if (WikipediaApp.instance.isOnline && AccountUtil.isLoggedIn) { + val response = ServiceFactory.get(title.wikiSite).getWatchedStatusWithCategories(title.prefixedText) + val page = response.query?.firstPage() + WatchStatus( + isWatched = page?.watched == true, + hasWatchlistExpiry = page?.hasWatchlistExpiry() == true, + myQueryResponse = response + ) + } else if (WikipediaApp.instance.isOnline && !AccountUtil.isLoggedIn) { + val response = AnonymousNotificationHelper.observableForAnonUserInfo(title.wikiSite) + WatchStatus(false,false, response) + } else { + WatchStatus(false, false, MwQueryResponse()) + } + } + + private suspend fun fetchCategories(title: PageTitle, watchResponse: MwQueryResponse): List { + return if (WikipediaApp.instance.isOnline) { + val response = ServiceFactory.get(title.wikiSite).getCategoriesProps(title.text) + (response.query ?: watchResponse.query)?.firstPage()?.categories?.map { category -> + Category(title = category.title, lang = title.wikiSite.languageCode) + } ?: emptyList() + } else emptyList() + } + + private suspend fun isInReadingList(title: PageTitle): Boolean { + return AppDatabase.instance.readingListPageDao().findPageInAnyList(title) != null + } +} + +data class WatchStatus( + val isWatched: Boolean, + val hasWatchlistExpiry: Boolean, + val myQueryResponse: MwQueryResponse +) + +sealed class PageResult { + data class Success( + val pageSummaryResponse: Response, + val categories: List, + val isWatched: Boolean, + val hasWatchlistExpiry: Boolean, + val redirectedFrom: String? + ) : PageResult() + + data class Error(val throwable: Throwable) : PageResult() +} \ No newline at end of file diff --git a/app/src/main/java/org/wikipedia/page/pageload/PageLoad.kt b/app/src/main/java/org/wikipedia/page/pageload/PageLoad.kt new file mode 100644 index 00000000000..fd543409302 --- /dev/null +++ b/app/src/main/java/org/wikipedia/page/pageload/PageLoad.kt @@ -0,0 +1,38 @@ +package org.wikipedia.page.pageload + +import org.wikipedia.categories.db.Category +import org.wikipedia.dataclient.page.PageSummary +import org.wikipedia.history.HistoryEntry +import org.wikipedia.page.Page +import org.wikipedia.page.PageActivity +import org.wikipedia.page.PageTitle +import retrofit2.Response + +data class PageLoadRequest( + val title: PageTitle, + val entry: HistoryEntry, + val options: PageLoadOptions = PageLoadOptions() +) + +data class PageLoadOptions( + val pushbackStack: Boolean = true, + val squashBackStack: Boolean = false, + val isRefresh: Boolean = false, + val stagedScrollY: Int = 0, + val tabPosition: PageActivity.TabPosition = PageActivity.TabPosition.CURRENT_TAB +) + +sealed class LoadType { + object CurrentTab: LoadType() + object NewForegroundTab: LoadType() + object NewBackgroundTab: LoadType() + object ExistingTab: LoadType() + data class WithScrollPosition(val scrollY: Int): LoadType() +} + +sealed class LoadState { + object Idle: LoadState() + object Loading: LoadState() + data class Success(val result: PageResult? = null): LoadState() + data class Error(val throwable: Throwable): LoadState() +} \ No newline at end of file diff --git a/app/src/main/java/org/wikipedia/page/pageload/PageLoader.kt b/app/src/main/java/org/wikipedia/page/pageload/PageLoader.kt new file mode 100644 index 00000000000..ed7c8bfb76f --- /dev/null +++ b/app/src/main/java/org/wikipedia/page/pageload/PageLoader.kt @@ -0,0 +1,338 @@ +package org.wikipedia.page.pageload + +import android.view.View +import androidx.lifecycle.lifecycleScope +import kotlinx.coroutines.CoroutineExceptionHandler +import kotlinx.coroutines.launch +import org.wikipedia.Constants +import org.wikipedia.WikipediaApp +import org.wikipedia.bridge.CommunicationBridge +import org.wikipedia.bridge.JavaScriptActionHandler +import org.wikipedia.dataclient.ServiceFactory +import org.wikipedia.history.HistoryEntry +import org.wikipedia.page.Namespace +import org.wikipedia.page.PageActivity +import org.wikipedia.page.PageBackStackItem +import org.wikipedia.page.PageFragment +import org.wikipedia.page.PageTitle +import org.wikipedia.page.PageViewModel +import org.wikipedia.page.leadimages.LeadImagesHandler +import org.wikipedia.page.tabs.Tab +import org.wikipedia.util.log.L +import org.wikipedia.views.ObservableWebView +import kotlin.text.get + +class PageLoader( + private val fragment: PageFragment, + private val webView: ObservableWebView, + private val bridge: CommunicationBridge, + private val leadImagesHandler: LeadImagesHandler, + private var currentTab: Tab +) { + private val dataFetcher = PageDataFetcher() + private val app = WikipediaApp.instance + + fun loadPage(request: PageLoadRequest) { + when (determineLoadType(request)) { + is LoadType.CurrentTab -> loadInCurrentTab(request) + is LoadType.NewForegroundTab -> loadInNewForegroundTab(request) + is LoadType.NewBackgroundTab -> loadInNewBackgroundTab(request) + is LoadType.ExistingTab -> loadInExistingTab(request) + is LoadType.WithScrollPosition -> loadWithScrollPosition(request) + } + } + + private fun loadInCurrentTab(request: PageLoadRequest) { + // is the new title the same as what's already being displayed? + if (fragment.currentTab.backStack.isNotEmpty() && + request.title == fragment.currentTab.backStack[fragment.currentTab.backStackPosition].title) { + if (fragment.model.page == null || request.options.isRefresh) { + loadFromBackStack() + } else if (!request.title.fragment.isNullOrEmpty()) { + fragment.scrollToSection(request.title.fragment!!) + } + return + } + + prepareForLoad(request) + executeLoad(request) + } + + fun backStackEmpty(): Boolean { + return currentTab.backStack.isEmpty() + } + + fun goBack(): Boolean { + if (currentTab.canGoBack()) { + currentTab.moveBack() + if (!backStackEmpty()) { + loadFromBackStack() + return true + } + } + return false + } + + fun goForward(): Boolean { + if (currentTab.canGoForward()) { + currentTab.moveForward() + loadFromBackStack() + return true + } + return false + } + + private fun loadInNewForegroundTab(request: PageLoadRequest) { + prepareForLoad(request) + createNewTab(request, isForeground = true) + executeLoad(request) + } + + private fun loadInNewBackgroundTab(request: PageLoadRequest) { + val isForeground = if (app.tabCount == 0) true else false + prepareForLoad(request) + createNewTab(request, isForeground = isForeground) + (fragment.requireActivity() as PageActivity).animateTabsButton() + executeLoad(request) + } + + private fun loadInExistingTab(request: PageLoadRequest) { + val existingTabPosition = selectedTabPosition(request.title) + if (existingTabPosition >= 0) { + switchToExistingTab(existingTabPosition) + } else { + loadInCurrentTab(request) + } + } + + private fun loadWithScrollPosition(request: PageLoadRequest) { + prepareForLoad(request) + executeLoad(request) + } + + private fun createNewTab(request: PageLoadRequest, isForeground: Boolean) { + val selectedTabPosition = selectedTabPosition(request.title) + if (selectedTabPosition >= 0) { + switchToExistingTab(selectedTabPosition) + return + } + + if (fragment.shouldCreateNewTab) { + val tab = Tab() + val position = if (isForeground) fragment.foregroundTabPosition else fragment.backgroundTabPosition + + if (isForeground) { + setTab(tab) + } + + app.tabList.add(position, tab) + trimTabCount() + + tab.backStack.add(PageBackStackItem(request.title, request.entry)) + if (!isForeground) { + // Load metadata for background tab + loadBackgroundTabMetadata(request.title) + } + fragment.requireActivity().invalidateOptionsMenu() + }else { + setTab(fragment.currentTab) + fragment.currentTab.backStack.add(PageBackStackItem(request.title, request.entry)) + } + } + + fun setTab(tab: Tab): Boolean { + val isDifferent = tab != currentTab + currentTab = tab + return isDifferent + } + + private fun loadBackgroundTabMetadata(title: PageTitle) { + fragment.lifecycleScope.launch(CoroutineExceptionHandler { _, t -> L.e(t) }) { + ServiceFactory.get(title.wikiSite) + .getInfoByPageIdsOrTitles(null, title.prefixedText) + .query?.firstPage()?.let { page -> + app.tabList.find { it.backStackPositionTitle == title } + ?.backStackPositionTitle?.apply { + thumbUrl = page.thumbUrl() + description = page.description + } + } + } + } + + private fun trimTabCount() { + while (app.tabList.size > Constants.MAX_TABS) { + app.tabList.removeAt(0) + } + } + + private fun switchToExistingTab(position: Int) { + if (position < app.tabList.size - 1) { + val tab = app.tabList.removeAt(position) + app.tabList.add(tab) + setTab(tab) + } + + if (app.tabCount > 0) { + app.tabList.last().squashBackstack() + loadFromBackStack() + } + } + + fun loadFromBackStack() { + if (currentTab.backStack.isEmpty()) { + return + } + val item = currentTab.backStack[currentTab.backStackPosition] + // display the page based on the backstack item, stage the scrollY position based on + // the backstack item. + //fragment.loadPage(item.title, item.historyEntry, false, item.scrollY) + L.d("Loaded page " + item.title.displayText + " from backstack") + } + + private fun prepareForLoad(request: PageLoadRequest) { + fragment.clearActivityActionBarTitle() + fragment.dismissBottomSheet() + + if (request.options.squashBackStack) { + if (app.tabCount > 0) { + app.tabList.last().clearBackstack() + } + } + + fragment.updateProgressBar(true) + fragment.sidePanelHandler.setEnabled(false) + fragment.callback()?.onPageSetToolbarElevationEnabled(false) + + // Update model + fragment.model.title = request.title + fragment.model.curEntry = request.entry + fragment.model.page = null + fragment.model.readingListPage = null + fragment.model.forceNetwork = request.options.isRefresh + + // Clear previous state + fragment.errorState = false + fragment.binding.pageError.visibility = View.GONE + fragment.webView.visibility = View.VISIBLE + fragment.binding.pageActionsTabLayout.visibility = View.VISIBLE + fragment.binding.pageActionsTabLayout.enableAllTabs() + + // Reset references and other state + fragment.references = null + fragment.revision = 0 + fragment.pageRefreshed = request.options.isRefresh + } + + private fun executeLoad(request: PageLoadRequest) { + if (request.options.pushbackStack) { + updateCurrentBackStackItem() + currentTab.pushBackStackItem(PageBackStackItem(request.title, request.entry)) + } + + if (request.title.namespace() == Namespace.SPECIAL) { + handleSpecialPage(request) + return + } + + fragment.lifecycleScope.launch(CoroutineExceptionHandler { _, throwable -> + L.e("Page details network error: ", throwable) + handleLoadError(throwable) + }) { + updateLoadingState(LoadState.Loading) + val result = dataFetcher.fetchPage( + request.title, + request.entry, + request.options.isRefresh + ) + when (result) { + is PageResult.Success -> { + updateLoadingState(LoadState.Success()) + handleLoadSuccess(result, request) + } + is PageResult.Error -> { + + } + } + } + } + + + fun updateCurrentBackStackItem() { + if (currentTab.backStack.isEmpty()) { + return + } + val item = currentTab.backStack[currentTab.backStackPosition] + item.scrollY = webView.scrollY + fragment.model.title?.let { + item.title.description = it.description + item.title.thumbUrl = it.thumbUrl + } + } + + fun handleSpecialPage(request: PageLoadRequest) { + bridge.resetHtml(request.title) + leadImagesHandler.loadLeadImage() + fragment.requireActivity().invalidateOptionsMenu() + fragment.onPageMetadataLoaded() + } + + private fun handleLoadSuccess(result: PageResult.Success, request: PageLoadRequest) { + val response = result.pageSummaryResponse + val pageSummary = response.body() + val page = pageSummary?.toPage(fragment.model.title) + fragment.model.page = page + fragment.model.isWatched = result.isWatched + fragment.model.hasWatchlistExpiry = result.hasWatchlistExpiry + fragment.model.title = page?.title + + // Load page content + if (!request.title.prefixedText.contains(":")) { + bridge.resetHtml(request.title) + } + fragment.updateQuickActionsAndMenuOptions() + fragment.requireActivity().invalidateOptionsMenu() + + leadImagesHandler.loadLeadImage() + fragment.onPageMetadataLoaded(result.redirectedFrom) + } + + + private fun handleLoadError(error: Throwable) { + if (!fragment.isAdded) { + return + } + fragment.requireActivity().invalidateOptionsMenu() + fragment.onPageLoadError(error) + } + + private fun updateLoadingState(state: LoadState) { + when (state) { + is LoadState.Loading -> fragment.updateProgressBar(true) + is LoadState.Success -> fragment.updateProgressBar(false) + is LoadState.Error -> fragment.updateProgressBar(false) + else -> {} + } + } + + private fun determineLoadType(request: PageLoadRequest): LoadType { + return when { + request.options.stagedScrollY > 0 -> LoadType.WithScrollPosition(request.options.stagedScrollY) + request.options.tabPosition == PageActivity.TabPosition.NEW_TAB_FOREGROUND -> LoadType.NewForegroundTab + request.options.tabPosition == PageActivity.TabPosition.NEW_TAB_BACKGROUND -> LoadType.NewBackgroundTab + request.options.tabPosition == PageActivity.TabPosition.EXISTING_TAB -> LoadType.ExistingTab + else -> LoadType.CurrentTab + } + } + + private fun selectedTabPosition(title: PageTitle): Int { + return app.tabList.firstOrNull { it.backStackPositionTitle != null && + title == it.backStackPositionTitle }?.let { app.tabList.indexOf(it) } ?: -1 + } + + fun onConfigurationChanged() { + leadImagesHandler.loadLeadImage() + bridge.execute(JavaScriptActionHandler.setTopMargin(leadImagesHandler.topMargin)) + } + +} \ No newline at end of file diff --git a/app/src/main/java/org/wikipedia/page/pageload/Test2.kt b/app/src/main/java/org/wikipedia/page/pageload/Test2.kt new file mode 100644 index 00000000000..a4ada0f9af8 --- /dev/null +++ b/app/src/main/java/org/wikipedia/page/pageload/Test2.kt @@ -0,0 +1,308 @@ +//package org.wikipedia.page.pageload +// +//import org.wikipedia.WikipediaApp +//import org.wikipedia.bridge.CommunicationBridge +//import org.wikipedia.page.PageFragment +//import org.wikipedia.page.leadimages.LeadImagesHandler +//import org.wikipedia.views.ObservableWebView +// +//class PageLoader2( +// private val fragment: PageFragment, +// private val webView: ObservableWebView, +// private val bridge: CommunicationBridge, +// private val leadImagesHandler: LeadImagesHandler +//) { +// private val dataFetcher = PageDataFetcher() +// private val app = WikipediaApp.instance +// +// fun loadPage(request: PageLoadRequest) { +// when (determineLoadType(request)) { +// is LoadType.CurrentTab -> loadInCurrentTab(request) +// is LoadType.NewForegroundTab -> loadInNewForegroundTab(request) +// is LoadType.NewBackgroundTab -> loadInNewBackgroundTab(request) +// is LoadType.ExistingTab -> loadInExistingTab(request) +// is LoadType.WithScrollPosition -> loadWithScrollPosition(request) +// } +// } +// +// private fun loadInCurrentTab(request: PageLoadRequest) { +// if (isSamePageLoaded(request)) { +// handleSamePageLoad(request) +// return +// } +// +// prepareForLoad(request) +// executeLoad(request) +// } +// +// private fun loadInNewForegroundTab(request: PageLoadRequest) { +// createNewTab(request, foreground = true) +// executeLoad(request) +// } +// +// private fun loadInNewBackgroundTab(request: PageLoadRequest) { +// createNewTab(request, foreground = false) +// animateTabsButton() +// } +// +// private fun loadInExistingTab(request: PageLoadRequest) { +// val existingTabPosition = findExistingTab(request.title) +// if (existingTabPosition >= 0) { +// switchToExistingTab(existingTabPosition) +// } else { +// loadInCurrentTab(request) +// } +// } +// +// private fun loadWithScrollPosition(request: PageLoadRequest) { +// prepareForLoad(request) +// executeLoad(request) +// } +// +// private fun prepareForLoad(request: PageLoadRequest) { +// fragment.clearActivityActionBarTitle() +// fragment.dismissBottomSheet() +// +// if (request.options.squashBackStack) { +// squashBackstack() +// } +// +// fragment.updateProgressBar(true) +// fragment.sidePanelHandler.setEnabled(false) +// fragment.callback()?.onPageSetToolbarElevationEnabled(false) +// +// // Update model +// fragment.model.title = request.title +// fragment.model.curEntry = request.entry +// fragment.model.page = null +// fragment.model.readingListPage = null +// fragment.model.forceNetwork = request.options.isRefresh +// +// // Clear previous state +// fragment.errorState = false +// fragment.binding.pageError.visibility = View.GONE +// fragment.webView.visibility = View.VISIBLE +// fragment.binding.pageActionsTabLayout.visibility = View.VISIBLE +// fragment.binding.pageActionsTabLayout.enableAllTabs() +// +// // Reset references and other state +// fragment.references = null +// fragment.revision = 0 +// fragment.pageRefreshed = request.options.isRefresh +// } +// +// private fun executeLoad(request: PageLoadRequest) { +// if (request.options.pushBackStack) { +// pushToBackstack(request.title, request.entry) +// } +// +// // Handle special pages +// if (request.title.namespace() === Namespace.SPECIAL) { +// handleSpecialPage(request) +// return +// } +// +// // Regular page loading +// fragment.lifecycleScope.launch(CoroutineExceptionHandler { _, throwable -> +// L.e("Page load error: ", throwable) +// handleLoadError(throwable, request) +// }) { +// try { +// updateLoadingState(LoadState.Loading) +// +// val result = dataFetcher.fetchPage( +// request.title, +// request.entry, +// request.options.isRefresh +// ) +// +// when (result) { +// is PageDataFetcher.PageResult.Success -> { +// updateLoadingState(LoadState.Success(result.page)) +// handleLoadSuccess(result, request) +// } +// is PageDataFetcher.PageResult.Error -> { +// updateLoadingState(LoadState.Error(result.throwable)) +// handleLoadError(result.throwable, request) +// } +// } +// } catch (e: Exception) { +// updateLoadingState(LoadState.Error(e)) +// handleLoadError(e, request) +// } +// } +// } +// +// private fun handleLoadSuccess(result: PageDataFetcher.PageResult.Success, request: PageLoadRequest) { +// // Update model +// fragment.model.page = result.page +// fragment.model.isWatched = result.isWatched +// fragment.model.hasWatchlistExpiry = result.hasWatchlistExpiry +// fragment.model.title = result.page.title +// +// // Update UI +// fragment.binding.pageRefreshContainer.isEnabled = true +// fragment.binding.pageRefreshContainer.isRefreshing = false +// +// // Load page content +// if (!request.title.prefixedText.contains(":")) { +// bridge.resetHtml(request.title) +// } +// +// leadImagesHandler.loadLeadImage() +// fragment.onPageMetadataLoaded(result.redirectedFrom) +// +// // Handle scroll position +// if (request.options.stagedScrollY > 0) { +// fragment.scrollTriggerListener.stagedScrollY = request.options.stagedScrollY +// } +// +// // Handle fragments +// request.title.fragment?.let { fragment -> +// if (fragment.isNotEmpty()) { +// handleFragmentScroll(fragment) +// } +// } +// +// fragment.updateQuickActionsAndMenuOptions() +// fragment.requireActivity().invalidateOptionsMenu() +// } +// +// private fun handleLoadError(error: Throwable, request: PageLoadRequest) { +// fragment.onPageLoadError(error) +// } +// +// private fun handleSpecialPage(request: PageLoadRequest) { +// bridge.resetHtml(request.title) +// leadImagesHandler.loadLeadImage() +// fragment.requireActivity().invalidateOptionsMenu() +// fragment.onPageMetadataLoaded() +// } +// +// private fun isSamePageLoaded(request: PageLoadRequest): Boolean { +// return fragment.currentTab.backStack.isNotEmpty() && +// request.title == fragment.currentTab.backStack[fragment.currentTab.backStackPosition].title +// } +// +// private fun handleSamePageLoad(request: PageLoadRequest) { +// if (fragment.model.page == null || request.options.isRefresh) { +// fragment.pageFragmentLoadState.loadFromBackStack() +// } else if (!request.title.fragment.isNullOrEmpty()) { +// fragment.scrollToSection(request.title.fragment!!) +// } +// } +// +// private fun createNewTab(request: PageLoadRequest, foreground: Boolean) { +// val selectedTabPosition = findExistingTab(request.title) +// if (selectedTabPosition >= 0) { +// switchToExistingTab(selectedTabPosition) +// return +// } +// +// if (fragment.shouldCreateNewTab) { +// val tab = Tab() +// val position = if (foreground) fragment.foregroundTabPosition else fragment.backgroundTabPosition +// +// if (foreground) { +// fragment.pageFragmentLoadState.setTab(tab) +// } +// +// app.tabList.add(position, tab) +// trimTabCount() +// +// tab.backStack.add(PageBackStackItem(request.title, request.entry)) +// +// if (!foreground) { +// // Load metadata for background tab +// loadBackgroundTabMetadata(request.title) +// } +// +// fragment.requireActivity().invalidateOptionsMenu() +// } else { +// fragment.pageFragmentLoadState.setTab(fragment.currentTab) +// fragment.currentTab.backStack.add(PageBackStackItem(request.title, request.entry)) +// } +// } +// +// private fun findExistingTab(title: PageTitle): Int { +// return app.tabList.indexOfFirst { tab -> +// tab.backStackPositionTitle != null && title == tab.backStackPositionTitle +// } +// } +// +// private fun switchToExistingTab(position: Int) { +// if (position < app.tabList.size - 1) { +// val tab = app.tabList.removeAt(position) +// app.tabList.add(tab) +// fragment.pageFragmentLoadState.setTab(tab) +// } +// +// if (app.tabCount > 0) { +// app.tabList.last().squashBackstack() +// fragment.pageFragmentLoadState.loadFromBackStack() +// } +// } +// +// private fun pushToBackstack(title: PageTitle, entry: HistoryEntry) { +// fragment.pageFragmentLoadState.updateCurrentBackStackItem() +// fragment.currentTab.pushBackStackItem(PageBackStackItem(title, entry)) +// } +// +// private fun squashBackstack() { +// if (app.tabCount > 0) { +// app.tabList.last().clearBackstack() +// } +// } +// +// private fun trimTabCount() { +// while (app.tabList.size > Constants.MAX_TABS) { +// app.tabList.removeAt(0) +// } +// } +// +// private fun loadBackgroundTabMetadata(title: PageTitle) { +// fragment.lifecycleScope.launch(CoroutineExceptionHandler { _, t -> L.e(t) }) { +// ServiceFactory.get(title.wikiSite) +// .getInfoByPageIdsOrTitles(null, title.prefixedText) +// .query?.firstPage()?.let { page -> +// app.tabList.find { it.backStackPositionTitle == title } +// ?.backStackPositionTitle?.apply { +// thumbUrl = page.thumbUrl() +// description = page.description +// } +// } +// } +// } +// +// private fun animateTabsButton() { +// (fragment.requireActivity() as PageActivity).animateTabsButton() +// } +// +// private fun handleFragmentScroll(fragment: String) { +// val scrollDelay = 100L +// webView.postDelayed({ +// if (this.fragment.isAdded) { +// this.fragment.scrollToSection(fragment) +// } +// }, scrollDelay) +// } +// +// private fun determineLoadType(request: PageLoadRequest): LoadType { +// return when { +// request.options.stagedScrollY > 0 -> LoadType.WithScrollPosition(request.options.stagedScrollY) +// request.options.tabPosition == TabPosition.NEW_TAB_FOREGROUND -> LoadType.NewForegroundTab +// request.options.tabPosition == TabPosition.NEW_TAB_BACKGROUND -> LoadType.NewBackgroundTab +// request.options.tabPosition == TabPosition.EXISTING_TAB -> LoadType.ExistingTab +// else -> LoadType.CurrentTab +// } +// } +// +// private fun updateLoadingState(state: LoadState) { +// when (state) { +// is LoadState.Loading -> fragment.updateProgressBar(true) +// is LoadState.Success -> fragment.updateProgressBar(false) +// is LoadState.Error -> fragment.updateProgressBar(false) +// else -> {} +// } +// } +//} From 84f1f872698cf68e460864f4361a3e19a9f75d80 Mon Sep 17 00:00:00 2001 From: williamrai Date: Tue, 8 Jul 2025 17:31:31 -0400 Subject: [PATCH 02/18] - refactor code fixes --- app/src/main/java/org/wikipedia/page/PageActivity.kt | 2 +- app/src/main/java/org/wikipedia/page/PageFragment.kt | 4 ++-- .../main/java/org/wikipedia/page/pageload/PageLoader.kt | 8 ++++++-- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/org/wikipedia/page/PageActivity.kt b/app/src/main/java/org/wikipedia/page/PageActivity.kt index 1b38ca319e0..c283f8c1785 100644 --- a/app/src/main/java/org/wikipedia/page/PageActivity.kt +++ b/app/src/main/java/org/wikipedia/page/PageActivity.kt @@ -618,7 +618,7 @@ class PageActivity : BaseActivity(), PageFragment.Callback, LinkPreviewDialog.Lo TabPosition.CURRENT_TAB -> PageLoadOptions(tabPosition = position) TabPosition.CURRENT_TAB_SQUASH -> PageLoadOptions(tabPosition = position, squashBackStack = true) TabPosition.NEW_TAB_BACKGROUND -> PageLoadOptions(tabPosition = position) - TabPosition.NEW_TAB_FOREGROUND -> PageLoadOptions(tabPosition = position) + TabPosition.NEW_TAB_FOREGROUND -> PageLoadOptions(tabPosition = position, pushbackStack = false) TabPosition.EXISTING_TAB -> PageLoadOptions(tabPosition = position) } pageFragment.loadPage(pageTitle, entry, options) diff --git a/app/src/main/java/org/wikipedia/page/PageFragment.kt b/app/src/main/java/org/wikipedia/page/PageFragment.kt index fdeae978b84..6b5bc1e6087 100644 --- a/app/src/main/java/org/wikipedia/page/PageFragment.kt +++ b/app/src/main/java/org/wikipedia/page/PageFragment.kt @@ -152,7 +152,7 @@ class PageFragment : Fragment(), BackPressedHandler, CommunicationBridge.Communi val binding get() = _binding!! private val activeTimer = ActiveTimer() - private val scrollTriggerListener = WebViewScrollTriggerListener() + val scrollTriggerListener = WebViewScrollTriggerListener() private val pageRefreshListener = OnRefreshListener { refreshPage() } private val pageActionItemCallback = PageActionItemCallback() @@ -1311,7 +1311,7 @@ class PageFragment : Fragment(), BackPressedHandler, CommunicationBridge.Communi } } - private inner class WebViewScrollTriggerListener : ObservableWebView.OnContentHeightChangedListener { + inner class WebViewScrollTriggerListener : ObservableWebView.OnContentHeightChangedListener { var stagedScrollY = 0 override fun onContentHeightChanged(contentHeight: Int) { if (stagedScrollY > 0 && contentHeight * DimenUtil.densityScalar - webView.height > stagedScrollY) { diff --git a/app/src/main/java/org/wikipedia/page/pageload/PageLoader.kt b/app/src/main/java/org/wikipedia/page/pageload/PageLoader.kt index ed7c8bfb76f..59c2fed67b1 100644 --- a/app/src/main/java/org/wikipedia/page/pageload/PageLoader.kt +++ b/app/src/main/java/org/wikipedia/page/pageload/PageLoader.kt @@ -186,7 +186,7 @@ class PageLoader( val item = currentTab.backStack[currentTab.backStackPosition] // display the page based on the backstack item, stage the scrollY position based on // the backstack item. - //fragment.loadPage(item.title, item.historyEntry, false, item.scrollY) + loadPage(request = PageLoadRequest(title = item.title, entry = item.historyEntry, options = PageLoadOptions(pushbackStack = false, stagedScrollY = item.scrollY))) L.d("Loaded page " + item.title.displayText + " from backstack") } @@ -286,10 +286,14 @@ class PageLoader( fragment.model.hasWatchlistExpiry = result.hasWatchlistExpiry fragment.model.title = page?.title - // Load page content if (!request.title.prefixedText.contains(":")) { bridge.resetHtml(request.title) } + + if (request.options.stagedScrollY > 0) { + fragment.scrollTriggerListener.stagedScrollY = request.options.stagedScrollY + } + fragment.updateQuickActionsAndMenuOptions() fragment.requireActivity().invalidateOptionsMenu() From c2d4a829d82a82aa475b11e3bd08ac9cdb753b6b Mon Sep 17 00:00:00 2001 From: williamrai Date: Wed, 9 Jul 2025 10:33:26 -0400 Subject: [PATCH 03/18] - refactor code fixes step 3 --- .../java/org/wikipedia/page/PageActivity.kt | 2 +- .../page/PageContainerLongPressHandler.kt | 5 +- .../java/org/wikipedia/page/PageFragment.kt | 174 +--------- .../wikipedia/page/PageFragmentLoadState.kt | 2 - .../page/pageload/PageDataFetcher.kt | 4 +- .../org/wikipedia/page/pageload/PageLoad.kt | 25 +- .../org/wikipedia/page/pageload/PageLoader.kt | 40 ++- .../java/org/wikipedia/page/pageload/Test2.kt | 308 ------------------ 8 files changed, 41 insertions(+), 519 deletions(-) delete mode 100644 app/src/main/java/org/wikipedia/page/pageload/Test2.kt diff --git a/app/src/main/java/org/wikipedia/page/PageActivity.kt b/app/src/main/java/org/wikipedia/page/PageActivity.kt index c283f8c1785..7bef9d7c4cf 100644 --- a/app/src/main/java/org/wikipedia/page/PageActivity.kt +++ b/app/src/main/java/org/wikipedia/page/PageActivity.kt @@ -614,7 +614,7 @@ class PageActivity : BaseActivity(), PageFragment.Callback, LinkPreviewDialog.Lo // Close the link preview, if one is open. hideLinkPreview() onPageCloseActionMode() - val options = when(position) { + val options = when (position) { TabPosition.CURRENT_TAB -> PageLoadOptions(tabPosition = position) TabPosition.CURRENT_TAB_SQUASH -> PageLoadOptions(tabPosition = position, squashBackStack = true) TabPosition.NEW_TAB_BACKGROUND -> PageLoadOptions(tabPosition = position) diff --git a/app/src/main/java/org/wikipedia/page/PageContainerLongPressHandler.kt b/app/src/main/java/org/wikipedia/page/PageContainerLongPressHandler.kt index 470c239cf2d..c27317443f0 100644 --- a/app/src/main/java/org/wikipedia/page/PageContainerLongPressHandler.kt +++ b/app/src/main/java/org/wikipedia/page/PageContainerLongPressHandler.kt @@ -3,17 +3,18 @@ package org.wikipedia.page import org.wikipedia.Constants.InvokeSource import org.wikipedia.LongPressHandler.WebViewMenuCallback import org.wikipedia.history.HistoryEntry +import org.wikipedia.page.pageload.PageLoadOptions import org.wikipedia.readinglist.ReadingListBehaviorsUtil import org.wikipedia.readinglist.database.ReadingListPage class PageContainerLongPressHandler(private val fragment: PageFragment) : WebViewMenuCallback { override fun onOpenLink(entry: HistoryEntry) { - fragment.loadPage(entry.title, entry) + fragment.onPageLoadPage(entry.title, entry) } override fun onOpenInNewTab(entry: HistoryEntry) { - fragment.openInNewBackgroundTab(entry.title, entry) + fragment.loadPage(entry.title, entry, options = PageLoadOptions(tabPosition = PageActivity.TabPosition.NEW_TAB_BACKGROUND)) } override fun onAddRequest(entry: HistoryEntry, addToDefault: Boolean) { diff --git a/app/src/main/java/org/wikipedia/page/PageFragment.kt b/app/src/main/java/org/wikipedia/page/PageFragment.kt index 6b5bc1e6087..e9d7defd24a 100644 --- a/app/src/main/java/org/wikipedia/page/PageFragment.kt +++ b/app/src/main/java/org/wikipedia/page/PageFragment.kt @@ -50,7 +50,6 @@ import org.wikipedia.activity.FragmentUtil.getCallback import org.wikipedia.analytics.eventplatform.ArticleFindInPageInteractionEvent import org.wikipedia.analytics.eventplatform.ArticleInteractionEvent import org.wikipedia.analytics.eventplatform.DonorExperienceEvent -import org.wikipedia.analytics.eventplatform.EventPlatformClient import org.wikipedia.analytics.eventplatform.PlacesEvent import org.wikipedia.analytics.eventplatform.WatchlistAnalyticsHelper import org.wikipedia.analytics.metricsplatform.ArticleFindInPageInteraction @@ -64,7 +63,6 @@ import org.wikipedia.database.AppDatabase import org.wikipedia.databinding.FragmentPageBinding import org.wikipedia.databinding.GroupFindReferencesInPageBinding import org.wikipedia.dataclient.RestService -import org.wikipedia.dataclient.ServiceFactory import org.wikipedia.dataclient.WikiSite import org.wikipedia.dataclient.donate.CampaignCollection import org.wikipedia.dataclient.mwapi.MwQueryPage @@ -81,8 +79,6 @@ import org.wikipedia.login.LoginActivity import org.wikipedia.main.MainActivity import org.wikipedia.media.AvPlayer import org.wikipedia.navtab.NavTab -import org.wikipedia.notifications.PollNotificationWorker -import org.wikipedia.page.PageActivity.TabPosition import org.wikipedia.page.action.PageActionItem import org.wikipedia.page.campaign.CampaignDialog import org.wikipedia.page.edithistory.EditHistoryListActivity @@ -94,7 +90,6 @@ import org.wikipedia.page.pageload.PageLoader import org.wikipedia.page.references.PageReferences import org.wikipedia.page.references.ReferenceDialog import org.wikipedia.page.shareafact.ShareHandler -import org.wikipedia.page.tabs.Tab import org.wikipedia.places.PlacesActivity import org.wikipedia.readinglist.LongPressMenu import org.wikipedia.readinglist.ReadingListBehaviorsUtil @@ -241,7 +236,6 @@ class PageFragment : Fragment(), BackPressedHandler, CommunicationBridge.Communi } } - editHandler = EditHandler(this, bridge) sidePanelHandler = SidePanelHandler(this, bridge) leadImagesHandler = LeadImagesHandler(this, webView, binding.pageHeaderView, callback()) @@ -508,62 +502,6 @@ class PageFragment : Fragment(), BackPressedHandler, CommunicationBridge.Communi } } - private fun setCurrentTabAndReset(position: Int) { - // move the selected tab to the bottom of the list, and navigate to it! - // (but only if it's a different tab than the one currently in view! - if (position < app.tabList.size - 1) { - val tab = app.tabList.removeAt(position) - app.tabList.add(tab) - pageLoader.setTab(tab) - } - if (app.tabCount > 0) { - app.tabList.last().squashBackstack() - pageLoader.loadFromBackStack() - } - } - - private fun selectedTabPosition(title: PageTitle): Int { - return app.tabList.firstOrNull { it.backStackPositionTitle != null && - title == it.backStackPositionTitle }?.let { app.tabList.indexOf(it) } ?: -1 - } - -// private fun openInNewTab(title: PageTitle, entry: HistoryEntry, position: Int) { -// val selectedTabPosition = selectedTabPosition(title) -// if (selectedTabPosition >= 0) { -// setCurrentTabAndReset(selectedTabPosition) -// return -// } -// if (shouldCreateNewTab) { -// // create a new tab -// val tab = Tab() -// val isForeground = position == foregroundTabPosition -// // if the requested position is at the top, then make its backstack current -// if (isForeground) { -// pageFragmentLoadState.setTab(tab) -// } -// // put this tab in the requested position -// app.tabList.add(position, tab) -// trimTabCount() -// // add the requested page to its backstack -// tab.backStack.add(PageBackStackItem(title, entry)) -// if (!isForeground) { -// lifecycleScope.launch(CoroutineExceptionHandler { _, t -> L.e(t) }) { -// ServiceFactory.get(title.wikiSite).getInfoByPageIdsOrTitles(null, title.prefixedText) -// .query?.firstPage()?.let { page -> -// WikipediaApp.instance.tabList.find { it.backStackPositionTitle == title }?.backStackPositionTitle?.apply { -// thumbUrl = page.thumbUrl() -// description = page.description -// } -// } -// } -// } -// requireActivity().invalidateOptionsMenu() -// } else { -// pageFragmentLoadState.setTab(currentTab) -// currentTab.backStack.add(PageBackStackItem(title, entry)) -// } -// } - fun dismissBottomSheet() { ExclusiveBottomSheetPresenter.dismiss(childFragmentManager) callback()?.onPageDismissBottomSheet() @@ -934,120 +872,11 @@ class PageFragment : Fragment(), BackPressedHandler, CommunicationBridge.Communi bridge.execute(JavaScriptActionHandler.setFooter(model)) } - fun openInNewBackgroundTab(title: PageTitle, entry: HistoryEntry) { - if (app.tabCount == 0) { -// openInNewTab(title, entry, foregroundTabPosition) -// pageFragmentLoadState.loadFromBackStack() - loadPage(title, entry, PageLoadOptions(tabPosition = TabPosition.NEW_TAB_FOREGROUND)) - - } else { -// openInNewTab(title, entry, backgroundTabPosition) -// (requireActivity() as PageActivity).animateTabsButton() - loadPage(title, entry, PageLoadOptions(tabPosition = TabPosition.NEW_TAB_BACKGROUND)) - - } - } - -// fun openInNewForegroundTab(title: PageTitle, entry: HistoryEntry) { -//// openInNewTab(title, entry, foregroundTabPosition) -//// pageFragmentLoadState.loadFromBackStack() -// loadPage(title, entry, PageLoadOptions(tabPosition = TabPosition.NEW_TAB_FOREGROUND)) -// -// } -// -// fun openFromExistingTab(title: PageTitle, entry: HistoryEntry) { -//// val selectedTabPosition = selectedTabPosition(title) -//// -//// if (selectedTabPosition == -1) { -//// loadPage(title, entry, pushBackStack = true, squashBackstack = false) -//// return -//// } -//// setCurrentTabAndReset(selectedTabPosition) -// loadPage(title, entry, PageLoadOptions(tabPosition = TabPosition.EXISTING_TAB)) -// } - fun loadPage(title: PageTitle, entry: HistoryEntry, options: PageLoadOptions = PageLoadOptions()) { val request = PageLoadRequest(title, entry, options) pageLoader.loadPage(request) } -// fun loadPage(title: PageTitle, entry: HistoryEntry, pushBackStack: Boolean, squashBackstack: Boolean, isRefresh: Boolean = false) { -// // is the new title the same as what's already being displayed? -// // done -// if (currentTab.backStack.isNotEmpty() && -// title == currentTab.backStack[currentTab.backStackPosition].title) { -// if (model.page == null || isRefresh) { -// pageFragmentLoadState.loadFromBackStack() -// } else if (!title.fragment.isNullOrEmpty()) { -// scrollToSection(title.fragment!!) -// } -// return -// } -// -// // done -// if (squashBackstack) { -// if (app.tabCount > 0) { -// app.tabList.last().clearBackstack() -// } -// } -// loadPage(title, entry, pushBackStack, 0, isRefresh) -// } - -// fun loadPage(title: PageTitle, entry: HistoryEntry, pushBackStack: Boolean, stagedScrollY: Int, isRefresh: Boolean = false) { -// // clear the title in case the previous page load had failed. -// // done -// clearActivityActionBarTitle() -// -// // done -// if (ExclusiveBottomSheetPresenter.getCurrentBottomSheet(childFragmentManager) !is ThemeChooserDialog) { -// dismissBottomSheet() -// } -// -// // not done -// if (AccountUtil.isLoggedIn) { -// // explicitly check notifications for the current user -// PollNotificationWorker.schedulePollNotificationJob(requireContext()) -// } -// -// // not done -// EventPlatformClient.AssociationController.beginNewPageView() -// -// // update the time spent reading of the current page, before loading the new one -// // not done -// addTimeSpentReading(activeTimer.elapsedSec) -// activeTimer.reset() -// -// // done -// updateProgressBar(true) -// callback()?.onPageSetToolbarElevationEnabled(false) -// sidePanelHandler.setEnabled(false) -// -// // done -// errorState = false -// binding.pageError.visibility = View.GONE -// webView.visibility = View.VISIBLE -// binding.pageActionsTabLayout.visibility = View.VISIBLE -// binding.pageActionsTabLayout.enableAllTabs() -// -// // done -// model.title = title -// model.curEntry = entry -// model.page = null -// model.readingListPage = null -// model.forceNetwork = isRefresh -// -// // done -// pageRefreshed = isRefresh -// references = null -// revision = 0 -// -// // main load state -// pageFragmentLoadState.load(pushBackStack) -// -// // @TODO: not applied -// scrollTriggerListener.stagedScrollY = stagedScrollY -// } - fun updateFontSize() { webView.settings.defaultFontSize = app.getFontSize().toInt() } @@ -1212,7 +1041,6 @@ class PageFragment : Fragment(), BackPressedHandler, CommunicationBridge.Communi } } - fun clearActivityActionBarTitle() { val currentActivity = requireActivity() if (currentActivity is PageActivity) { @@ -1253,7 +1081,7 @@ class PageFragment : Fragment(), BackPressedHandler, CommunicationBridge.Communi ExclusiveBottomSheetPresenter.show(childFragmentManager, dialog) } - fun loadPage(title: PageTitle, entry: HistoryEntry) { + fun onPageLoadPage(title: PageTitle, entry: HistoryEntry) { callback()?.onPageLoadPage(title, entry) } diff --git a/app/src/main/java/org/wikipedia/page/PageFragmentLoadState.kt b/app/src/main/java/org/wikipedia/page/PageFragmentLoadState.kt index 064845f0d4f..c5cc224f87f 100644 --- a/app/src/main/java/org/wikipedia/page/PageFragmentLoadState.kt +++ b/app/src/main/java/org/wikipedia/page/PageFragmentLoadState.kt @@ -12,7 +12,6 @@ import org.wikipedia.analytics.eventplatform.ArticleLinkPreviewInteractionEvent import org.wikipedia.analytics.metricsplatform.ArticleLinkPreviewInteraction import org.wikipedia.auth.AccountUtil import org.wikipedia.bridge.CommunicationBridge -import org.wikipedia.bridge.JavaScriptActionHandler import org.wikipedia.categories.db.Category import org.wikipedia.database.AppDatabase import org.wikipedia.dataclient.ServiceFactory @@ -159,7 +158,6 @@ class PageFragmentLoadState(private var model: PageViewModel, AppDatabase.instance.categoryDao().upsertAll(categoryList) } - // not done if (delayLoadHtml) { bridge.resetHtml(title) diff --git a/app/src/main/java/org/wikipedia/page/pageload/PageDataFetcher.kt b/app/src/main/java/org/wikipedia/page/pageload/PageDataFetcher.kt index f2845dd7dae..eea2a9acf6e 100644 --- a/app/src/main/java/org/wikipedia/page/pageload/PageDataFetcher.kt +++ b/app/src/main/java/org/wikipedia/page/pageload/PageDataFetcher.kt @@ -60,7 +60,7 @@ class PageDataFetcher { ) } else if (WikipediaApp.instance.isOnline && !AccountUtil.isLoggedIn) { val response = AnonymousNotificationHelper.observableForAnonUserInfo(title.wikiSite) - WatchStatus(false,false, response) + WatchStatus(false, false, response) } else { WatchStatus(false, false, MwQueryResponse()) } @@ -96,4 +96,4 @@ sealed class PageResult { ) : PageResult() data class Error(val throwable: Throwable) : PageResult() -} \ No newline at end of file +} diff --git a/app/src/main/java/org/wikipedia/page/pageload/PageLoad.kt b/app/src/main/java/org/wikipedia/page/pageload/PageLoad.kt index fd543409302..c3b0add55e8 100644 --- a/app/src/main/java/org/wikipedia/page/pageload/PageLoad.kt +++ b/app/src/main/java/org/wikipedia/page/pageload/PageLoad.kt @@ -1,12 +1,8 @@ package org.wikipedia.page.pageload -import org.wikipedia.categories.db.Category -import org.wikipedia.dataclient.page.PageSummary import org.wikipedia.history.HistoryEntry -import org.wikipedia.page.Page import org.wikipedia.page.PageActivity import org.wikipedia.page.PageTitle -import retrofit2.Response data class PageLoadRequest( val title: PageTitle, @@ -19,20 +15,21 @@ data class PageLoadOptions( val squashBackStack: Boolean = false, val isRefresh: Boolean = false, val stagedScrollY: Int = 0, + val shouldLoadFromBackStack: Boolean = false, val tabPosition: PageActivity.TabPosition = PageActivity.TabPosition.CURRENT_TAB ) sealed class LoadType { - object CurrentTab: LoadType() - object NewForegroundTab: LoadType() - object NewBackgroundTab: LoadType() - object ExistingTab: LoadType() - data class WithScrollPosition(val scrollY: Int): LoadType() + object CurrentTab : LoadType() + object NewForegroundTab : LoadType() + object NewBackgroundTab : LoadType() + object ExistingTab : LoadType() + object FromBackStack : LoadType() } sealed class LoadState { - object Idle: LoadState() - object Loading: LoadState() - data class Success(val result: PageResult? = null): LoadState() - data class Error(val throwable: Throwable): LoadState() -} \ No newline at end of file + object Idle : LoadState() + object Loading : LoadState() + data class Success(val result: PageResult? = null) : LoadState() + data class Error(val throwable: Throwable) : LoadState() +} diff --git a/app/src/main/java/org/wikipedia/page/pageload/PageLoader.kt b/app/src/main/java/org/wikipedia/page/pageload/PageLoader.kt index 59c2fed67b1..4fc65b5ca21 100644 --- a/app/src/main/java/org/wikipedia/page/pageload/PageLoader.kt +++ b/app/src/main/java/org/wikipedia/page/pageload/PageLoader.kt @@ -9,18 +9,15 @@ import org.wikipedia.WikipediaApp import org.wikipedia.bridge.CommunicationBridge import org.wikipedia.bridge.JavaScriptActionHandler import org.wikipedia.dataclient.ServiceFactory -import org.wikipedia.history.HistoryEntry import org.wikipedia.page.Namespace import org.wikipedia.page.PageActivity import org.wikipedia.page.PageBackStackItem import org.wikipedia.page.PageFragment import org.wikipedia.page.PageTitle -import org.wikipedia.page.PageViewModel import org.wikipedia.page.leadimages.LeadImagesHandler import org.wikipedia.page.tabs.Tab import org.wikipedia.util.log.L import org.wikipedia.views.ObservableWebView -import kotlin.text.get class PageLoader( private val fragment: PageFragment, @@ -38,7 +35,7 @@ class PageLoader( is LoadType.NewForegroundTab -> loadInNewForegroundTab(request) is LoadType.NewBackgroundTab -> loadInNewBackgroundTab(request) is LoadType.ExistingTab -> loadInExistingTab(request) - is LoadType.WithScrollPosition -> loadWithScrollPosition(request) + is LoadType.FromBackStack -> loadWithScrollPosition(request) } } @@ -89,11 +86,14 @@ class PageLoader( } private fun loadInNewBackgroundTab(request: PageLoadRequest) { - val isForeground = if (app.tabCount == 0) true else false - prepareForLoad(request) - createNewTab(request, isForeground = isForeground) - (fragment.requireActivity() as PageActivity).animateTabsButton() - executeLoad(request) + val isForeground = app.tabCount == 0 + if (isForeground) { + createNewTab(request, isForeground = true) + loadFromBackStack() + } else { + createNewTab(request, isForeground = false) + (fragment.requireActivity() as PageActivity).animateTabsButton() + } } private fun loadInExistingTab(request: PageLoadRequest) { @@ -134,7 +134,7 @@ class PageLoader( loadBackgroundTabMetadata(request.title) } fragment.requireActivity().invalidateOptionsMenu() - }else { + } else { setTab(fragment.currentTab) fragment.currentTab.backStack.add(PageBackStackItem(request.title, request.entry)) } @@ -186,7 +186,15 @@ class PageLoader( val item = currentTab.backStack[currentTab.backStackPosition] // display the page based on the backstack item, stage the scrollY position based on // the backstack item. - loadPage(request = PageLoadRequest(title = item.title, entry = item.historyEntry, options = PageLoadOptions(pushbackStack = false, stagedScrollY = item.scrollY))) + loadPage(request = PageLoadRequest( + title = item.title, + entry = item.historyEntry, + options = PageLoadOptions( + pushbackStack = false, + stagedScrollY = item.scrollY, + shouldLoadFromBackStack = true + )) + ) L.d("Loaded page " + item.title.displayText + " from backstack") } @@ -251,13 +259,13 @@ class PageLoader( handleLoadSuccess(result, request) } is PageResult.Error -> { - + updateLoadingState(LoadState.Error(result.throwable)) + handleLoadError(result.throwable) } } } } - fun updateCurrentBackStackItem() { if (currentTab.backStack.isEmpty()) { return @@ -301,7 +309,6 @@ class PageLoader( fragment.onPageMetadataLoaded(result.redirectedFrom) } - private fun handleLoadError(error: Throwable) { if (!fragment.isAdded) { return @@ -321,7 +328,7 @@ class PageLoader( private fun determineLoadType(request: PageLoadRequest): LoadType { return when { - request.options.stagedScrollY > 0 -> LoadType.WithScrollPosition(request.options.stagedScrollY) + request.options.shouldLoadFromBackStack -> LoadType.FromBackStack request.options.tabPosition == PageActivity.TabPosition.NEW_TAB_FOREGROUND -> LoadType.NewForegroundTab request.options.tabPosition == PageActivity.TabPosition.NEW_TAB_BACKGROUND -> LoadType.NewBackgroundTab request.options.tabPosition == PageActivity.TabPosition.EXISTING_TAB -> LoadType.ExistingTab @@ -338,5 +345,4 @@ class PageLoader( leadImagesHandler.loadLeadImage() bridge.execute(JavaScriptActionHandler.setTopMargin(leadImagesHandler.topMargin)) } - -} \ No newline at end of file +} diff --git a/app/src/main/java/org/wikipedia/page/pageload/Test2.kt b/app/src/main/java/org/wikipedia/page/pageload/Test2.kt deleted file mode 100644 index a4ada0f9af8..00000000000 --- a/app/src/main/java/org/wikipedia/page/pageload/Test2.kt +++ /dev/null @@ -1,308 +0,0 @@ -//package org.wikipedia.page.pageload -// -//import org.wikipedia.WikipediaApp -//import org.wikipedia.bridge.CommunicationBridge -//import org.wikipedia.page.PageFragment -//import org.wikipedia.page.leadimages.LeadImagesHandler -//import org.wikipedia.views.ObservableWebView -// -//class PageLoader2( -// private val fragment: PageFragment, -// private val webView: ObservableWebView, -// private val bridge: CommunicationBridge, -// private val leadImagesHandler: LeadImagesHandler -//) { -// private val dataFetcher = PageDataFetcher() -// private val app = WikipediaApp.instance -// -// fun loadPage(request: PageLoadRequest) { -// when (determineLoadType(request)) { -// is LoadType.CurrentTab -> loadInCurrentTab(request) -// is LoadType.NewForegroundTab -> loadInNewForegroundTab(request) -// is LoadType.NewBackgroundTab -> loadInNewBackgroundTab(request) -// is LoadType.ExistingTab -> loadInExistingTab(request) -// is LoadType.WithScrollPosition -> loadWithScrollPosition(request) -// } -// } -// -// private fun loadInCurrentTab(request: PageLoadRequest) { -// if (isSamePageLoaded(request)) { -// handleSamePageLoad(request) -// return -// } -// -// prepareForLoad(request) -// executeLoad(request) -// } -// -// private fun loadInNewForegroundTab(request: PageLoadRequest) { -// createNewTab(request, foreground = true) -// executeLoad(request) -// } -// -// private fun loadInNewBackgroundTab(request: PageLoadRequest) { -// createNewTab(request, foreground = false) -// animateTabsButton() -// } -// -// private fun loadInExistingTab(request: PageLoadRequest) { -// val existingTabPosition = findExistingTab(request.title) -// if (existingTabPosition >= 0) { -// switchToExistingTab(existingTabPosition) -// } else { -// loadInCurrentTab(request) -// } -// } -// -// private fun loadWithScrollPosition(request: PageLoadRequest) { -// prepareForLoad(request) -// executeLoad(request) -// } -// -// private fun prepareForLoad(request: PageLoadRequest) { -// fragment.clearActivityActionBarTitle() -// fragment.dismissBottomSheet() -// -// if (request.options.squashBackStack) { -// squashBackstack() -// } -// -// fragment.updateProgressBar(true) -// fragment.sidePanelHandler.setEnabled(false) -// fragment.callback()?.onPageSetToolbarElevationEnabled(false) -// -// // Update model -// fragment.model.title = request.title -// fragment.model.curEntry = request.entry -// fragment.model.page = null -// fragment.model.readingListPage = null -// fragment.model.forceNetwork = request.options.isRefresh -// -// // Clear previous state -// fragment.errorState = false -// fragment.binding.pageError.visibility = View.GONE -// fragment.webView.visibility = View.VISIBLE -// fragment.binding.pageActionsTabLayout.visibility = View.VISIBLE -// fragment.binding.pageActionsTabLayout.enableAllTabs() -// -// // Reset references and other state -// fragment.references = null -// fragment.revision = 0 -// fragment.pageRefreshed = request.options.isRefresh -// } -// -// private fun executeLoad(request: PageLoadRequest) { -// if (request.options.pushBackStack) { -// pushToBackstack(request.title, request.entry) -// } -// -// // Handle special pages -// if (request.title.namespace() === Namespace.SPECIAL) { -// handleSpecialPage(request) -// return -// } -// -// // Regular page loading -// fragment.lifecycleScope.launch(CoroutineExceptionHandler { _, throwable -> -// L.e("Page load error: ", throwable) -// handleLoadError(throwable, request) -// }) { -// try { -// updateLoadingState(LoadState.Loading) -// -// val result = dataFetcher.fetchPage( -// request.title, -// request.entry, -// request.options.isRefresh -// ) -// -// when (result) { -// is PageDataFetcher.PageResult.Success -> { -// updateLoadingState(LoadState.Success(result.page)) -// handleLoadSuccess(result, request) -// } -// is PageDataFetcher.PageResult.Error -> { -// updateLoadingState(LoadState.Error(result.throwable)) -// handleLoadError(result.throwable, request) -// } -// } -// } catch (e: Exception) { -// updateLoadingState(LoadState.Error(e)) -// handleLoadError(e, request) -// } -// } -// } -// -// private fun handleLoadSuccess(result: PageDataFetcher.PageResult.Success, request: PageLoadRequest) { -// // Update model -// fragment.model.page = result.page -// fragment.model.isWatched = result.isWatched -// fragment.model.hasWatchlistExpiry = result.hasWatchlistExpiry -// fragment.model.title = result.page.title -// -// // Update UI -// fragment.binding.pageRefreshContainer.isEnabled = true -// fragment.binding.pageRefreshContainer.isRefreshing = false -// -// // Load page content -// if (!request.title.prefixedText.contains(":")) { -// bridge.resetHtml(request.title) -// } -// -// leadImagesHandler.loadLeadImage() -// fragment.onPageMetadataLoaded(result.redirectedFrom) -// -// // Handle scroll position -// if (request.options.stagedScrollY > 0) { -// fragment.scrollTriggerListener.stagedScrollY = request.options.stagedScrollY -// } -// -// // Handle fragments -// request.title.fragment?.let { fragment -> -// if (fragment.isNotEmpty()) { -// handleFragmentScroll(fragment) -// } -// } -// -// fragment.updateQuickActionsAndMenuOptions() -// fragment.requireActivity().invalidateOptionsMenu() -// } -// -// private fun handleLoadError(error: Throwable, request: PageLoadRequest) { -// fragment.onPageLoadError(error) -// } -// -// private fun handleSpecialPage(request: PageLoadRequest) { -// bridge.resetHtml(request.title) -// leadImagesHandler.loadLeadImage() -// fragment.requireActivity().invalidateOptionsMenu() -// fragment.onPageMetadataLoaded() -// } -// -// private fun isSamePageLoaded(request: PageLoadRequest): Boolean { -// return fragment.currentTab.backStack.isNotEmpty() && -// request.title == fragment.currentTab.backStack[fragment.currentTab.backStackPosition].title -// } -// -// private fun handleSamePageLoad(request: PageLoadRequest) { -// if (fragment.model.page == null || request.options.isRefresh) { -// fragment.pageFragmentLoadState.loadFromBackStack() -// } else if (!request.title.fragment.isNullOrEmpty()) { -// fragment.scrollToSection(request.title.fragment!!) -// } -// } -// -// private fun createNewTab(request: PageLoadRequest, foreground: Boolean) { -// val selectedTabPosition = findExistingTab(request.title) -// if (selectedTabPosition >= 0) { -// switchToExistingTab(selectedTabPosition) -// return -// } -// -// if (fragment.shouldCreateNewTab) { -// val tab = Tab() -// val position = if (foreground) fragment.foregroundTabPosition else fragment.backgroundTabPosition -// -// if (foreground) { -// fragment.pageFragmentLoadState.setTab(tab) -// } -// -// app.tabList.add(position, tab) -// trimTabCount() -// -// tab.backStack.add(PageBackStackItem(request.title, request.entry)) -// -// if (!foreground) { -// // Load metadata for background tab -// loadBackgroundTabMetadata(request.title) -// } -// -// fragment.requireActivity().invalidateOptionsMenu() -// } else { -// fragment.pageFragmentLoadState.setTab(fragment.currentTab) -// fragment.currentTab.backStack.add(PageBackStackItem(request.title, request.entry)) -// } -// } -// -// private fun findExistingTab(title: PageTitle): Int { -// return app.tabList.indexOfFirst { tab -> -// tab.backStackPositionTitle != null && title == tab.backStackPositionTitle -// } -// } -// -// private fun switchToExistingTab(position: Int) { -// if (position < app.tabList.size - 1) { -// val tab = app.tabList.removeAt(position) -// app.tabList.add(tab) -// fragment.pageFragmentLoadState.setTab(tab) -// } -// -// if (app.tabCount > 0) { -// app.tabList.last().squashBackstack() -// fragment.pageFragmentLoadState.loadFromBackStack() -// } -// } -// -// private fun pushToBackstack(title: PageTitle, entry: HistoryEntry) { -// fragment.pageFragmentLoadState.updateCurrentBackStackItem() -// fragment.currentTab.pushBackStackItem(PageBackStackItem(title, entry)) -// } -// -// private fun squashBackstack() { -// if (app.tabCount > 0) { -// app.tabList.last().clearBackstack() -// } -// } -// -// private fun trimTabCount() { -// while (app.tabList.size > Constants.MAX_TABS) { -// app.tabList.removeAt(0) -// } -// } -// -// private fun loadBackgroundTabMetadata(title: PageTitle) { -// fragment.lifecycleScope.launch(CoroutineExceptionHandler { _, t -> L.e(t) }) { -// ServiceFactory.get(title.wikiSite) -// .getInfoByPageIdsOrTitles(null, title.prefixedText) -// .query?.firstPage()?.let { page -> -// app.tabList.find { it.backStackPositionTitle == title } -// ?.backStackPositionTitle?.apply { -// thumbUrl = page.thumbUrl() -// description = page.description -// } -// } -// } -// } -// -// private fun animateTabsButton() { -// (fragment.requireActivity() as PageActivity).animateTabsButton() -// } -// -// private fun handleFragmentScroll(fragment: String) { -// val scrollDelay = 100L -// webView.postDelayed({ -// if (this.fragment.isAdded) { -// this.fragment.scrollToSection(fragment) -// } -// }, scrollDelay) -// } -// -// private fun determineLoadType(request: PageLoadRequest): LoadType { -// return when { -// request.options.stagedScrollY > 0 -> LoadType.WithScrollPosition(request.options.stagedScrollY) -// request.options.tabPosition == TabPosition.NEW_TAB_FOREGROUND -> LoadType.NewForegroundTab -// request.options.tabPosition == TabPosition.NEW_TAB_BACKGROUND -> LoadType.NewBackgroundTab -// request.options.tabPosition == TabPosition.EXISTING_TAB -> LoadType.ExistingTab -// else -> LoadType.CurrentTab -// } -// } -// -// private fun updateLoadingState(state: LoadState) { -// when (state) { -// is LoadState.Loading -> fragment.updateProgressBar(true) -// is LoadState.Success -> fragment.updateProgressBar(false) -// is LoadState.Error -> fragment.updateProgressBar(false) -// else -> {} -// } -// } -//} From ae06ef4d25e414f52acdaf6e62833a16036180c6 Mon Sep 17 00:00:00 2001 From: williamrai Date: Wed, 9 Jul 2025 17:30:09 -0400 Subject: [PATCH 04/18] - refactor code fixes step 4 (converting to viewModel) --- .../java/org/wikipedia/page/PageActivity.kt | 7 - .../java/org/wikipedia/page/PageFragment.kt | 74 ++++- .../page/pageload/PageDataFetcher.kt | 31 +- .../org/wikipedia/page/pageload/PageLoad.kt | 10 +- .../page/pageload/PageLoadViewModel.kt | 279 ++++++++++++++++++ .../org/wikipedia/page/pageload/PageLoader.kt | 25 +- 6 files changed, 397 insertions(+), 29 deletions(-) create mode 100644 app/src/main/java/org/wikipedia/page/pageload/PageLoadViewModel.kt diff --git a/app/src/main/java/org/wikipedia/page/PageActivity.kt b/app/src/main/java/org/wikipedia/page/PageActivity.kt index 7bef9d7c4cf..2db776451b7 100644 --- a/app/src/main/java/org/wikipedia/page/PageActivity.kt +++ b/app/src/main/java/org/wikipedia/page/PageActivity.kt @@ -622,13 +622,6 @@ class PageActivity : BaseActivity(), PageFragment.Callback, LinkPreviewDialog.Lo TabPosition.EXISTING_TAB -> PageLoadOptions(tabPosition = position) } pageFragment.loadPage(pageTitle, entry, options) -// when (position) { -// TabPosition.CURRENT_TAB -> pageFragment.loadPage(pageTitle, entry, pushBackStack = true, squashBackstack = false) -// TabPosition.CURRENT_TAB_SQUASH -> pageFragment.loadPage(pageTitle, entry, pushBackStack = true, squashBackstack = true) -// TabPosition.NEW_TAB_BACKGROUND -> pageFragment.openInNewBackgroundTab(pageTitle, entry) -// TabPosition.NEW_TAB_FOREGROUND -> pageFragment.openInNewForegroundTab(pageTitle, entry) -// else -> pageFragment.openFromExistingTab(pageTitle, entry) -// } } } diff --git a/app/src/main/java/org/wikipedia/page/PageFragment.kt b/app/src/main/java/org/wikipedia/page/PageFragment.kt index e9d7defd24a..45d987249e1 100644 --- a/app/src/main/java/org/wikipedia/page/PageFragment.kt +++ b/app/src/main/java/org/wikipedia/page/PageFragment.kt @@ -27,6 +27,7 @@ import androidx.core.app.ActivityOptionsCompat import androidx.core.view.forEach import androidx.core.widget.TextViewCompat import androidx.fragment.app.Fragment +import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.lifecycleScope import androidx.swiperefreshlayout.widget.SwipeRefreshLayout.OnRefreshListener import com.google.android.material.bottomsheet.BottomSheetDialogFragment @@ -84,8 +85,10 @@ import org.wikipedia.page.campaign.CampaignDialog import org.wikipedia.page.edithistory.EditHistoryListActivity import org.wikipedia.page.issues.PageIssuesDialog import org.wikipedia.page.leadimages.LeadImagesHandler +import org.wikipedia.page.pageload.LoadState import org.wikipedia.page.pageload.PageLoadOptions import org.wikipedia.page.pageload.PageLoadRequest +import org.wikipedia.page.pageload.PageLoadViewModel import org.wikipedia.page.pageload.PageLoader import org.wikipedia.page.references.PageReferences import org.wikipedia.page.references.ReferenceDialog @@ -117,6 +120,7 @@ import org.wikipedia.wiktionary.WiktionaryDialog import java.time.Duration import java.time.Instant +// @TODO: offline test, visit article, save article turn airplane mode, chinese variant test, and other language test class PageFragment : Fragment(), BackPressedHandler, CommunicationBridge.CommunicationBridgeListener, ThemeChooserDialog.Callback, ReferenceDialog.Callback, WiktionaryDialog.Callback, WatchlistExpiryDialog.Callback { @@ -188,6 +192,7 @@ class PageFragment : Fragment(), BackPressedHandler, CommunicationBridge.Communi val page get() = model.page val isLoading get() = bridge.isLoading val leadImageEditLang get() = leadImagesHandler.callToActionEditLang + private lateinit var pageLoadViewModel: PageLoadViewModel override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { _binding = FragmentPageBinding.inflate(inflater, container, false) @@ -236,6 +241,7 @@ class PageFragment : Fragment(), BackPressedHandler, CommunicationBridge.Communi } } + pageLoadViewModel = ViewModelProvider(this, PageLoadViewModel.Factory)[PageLoadViewModel::class.java] editHandler = EditHandler(this, bridge) sidePanelHandler = SidePanelHandler(this, bridge) leadImagesHandler = LeadImagesHandler(this, webView, binding.pageHeaderView, callback()) @@ -249,6 +255,71 @@ class PageFragment : Fragment(), BackPressedHandler, CommunicationBridge.Communi if (shouldLoadFromBackstack(activity) || savedInstanceState != null) { reloadFromBackstack() } + setupObservers() + } + + private fun setupObservers() { + viewLifecycleOwner.lifecycleScope.launch { + pageLoadViewModel.loadState.collect { state -> + when (state) { + is LoadState.Loading -> { + clearActivityActionBarTitle() + dismissBottomSheet() + + updateProgressBar(true) + sidePanelHandler.setEnabled(false) + callback()?.onPageSetToolbarElevationEnabled(false) + + + // Clear previous state + errorState = false + binding.pageError.visibility = View.GONE + webView.visibility = View.VISIBLE + binding.pageActionsTabLayout.visibility = View.VISIBLE + binding.pageActionsTabLayout.enableAllTabs() + + // Reset references and other state + references = null + revision = 0 + pageRefreshed = state.isRefresh + } + is LoadState.Success -> { + // when new tab is created + if (state.isNewTabCreated) { + requireActivity().invalidateOptionsMenu() + } + if (state.loadedFromBackground) { + (requireActivity() as PageActivity).animateTabsButton() + return@collect + } + if (!state.title.prefixedText.contains(":")) { + bridge.resetHtml(state.title) + } + scrollTriggerListener.stagedScrollY = state.stagedScrollY + updateQuickActionsAndMenuOptions() + requireActivity().invalidateOptionsMenu() + leadImagesHandler.loadLeadImage() + onPageMetadataLoaded(state.result?.redirectedFrom) + + } + is LoadState.Error -> {} + LoadState.Idle -> {} + is LoadState.SpecialPage -> { + bridge.resetHtml(state.request.title) + leadImagesHandler.loadLeadImage() + requireActivity().invalidateOptionsMenu() + onPageMetadataLoaded() + } + } + } + } + viewLifecycleOwner.lifecycleScope.launch { + pageLoadViewModel.currentPageModel.collect { pageViewModel -> + pageViewModel?.let { + model = it + } + } + } } override fun onSaveInstanceState(outState: Bundle) { @@ -874,7 +945,8 @@ class PageFragment : Fragment(), BackPressedHandler, CommunicationBridge.Communi fun loadPage(title: PageTitle, entry: HistoryEntry, options: PageLoadOptions = PageLoadOptions()) { val request = PageLoadRequest(title, entry, options) - pageLoader.loadPage(request) + // pageLoader.loadPage(request) + pageLoadViewModel.loadPage(request, webView.scrollY) } fun updateFontSize() { diff --git a/app/src/main/java/org/wikipedia/page/pageload/PageDataFetcher.kt b/app/src/main/java/org/wikipedia/page/pageload/PageDataFetcher.kt index eea2a9acf6e..e6ad37856c3 100644 --- a/app/src/main/java/org/wikipedia/page/pageload/PageDataFetcher.kt +++ b/app/src/main/java/org/wikipedia/page/pageload/PageDataFetcher.kt @@ -17,27 +17,24 @@ import retrofit2.Response class PageDataFetcher { suspend fun fetchPage(title: PageTitle, entry: HistoryEntry, forceNetwork: Boolean = false): PageResult { - return try { - val cacheControl = if (forceNetwork) "no-cache" else "default" - val pageSummary = fetchPageSummary(title, cacheControl) - val watchStatus = fetchWatchStatus(title) - val categories = fetchCategories(title, watchStatus.myQueryResponse) + val cacheControl = if (forceNetwork) "no-cache" else "default" - if (pageSummary.body() == null) { - throw RuntimeException("Summary response was invalid.") - } + val pageSummary = fetchPageSummary(title, cacheControl) + val watchStatus = fetchWatchStatus(title) + val categories = fetchCategories(title, watchStatus.myQueryResponse) - PageResult.Success( - pageSummaryResponse = pageSummary, - categories = categories, - isWatched = watchStatus.isWatched, - hasWatchlistExpiry = watchStatus.hasWatchlistExpiry, - redirectedFrom = if (pageSummary.raw().priorResponse?.isRedirect == true) title.displayText else null - ) - } catch (e: Exception) { - PageResult.Error(e) + if (pageSummary.body() == null) { + throw RuntimeException("Summary response was invalid.") } + + return PageResult.Success( + pageSummaryResponse = pageSummary, + categories = categories, + isWatched = watchStatus.isWatched, + hasWatchlistExpiry = watchStatus.hasWatchlistExpiry, + redirectedFrom = if (pageSummary.raw().priorResponse?.isRedirect == true) title.displayText else null + ) } private suspend fun fetchPageSummary(title: PageTitle, cacheControl: String): Response { return ServiceFactory.getRest(title.wikiSite).getSummaryResponse( diff --git a/app/src/main/java/org/wikipedia/page/pageload/PageLoad.kt b/app/src/main/java/org/wikipedia/page/pageload/PageLoad.kt index c3b0add55e8..fb116c40e43 100644 --- a/app/src/main/java/org/wikipedia/page/pageload/PageLoad.kt +++ b/app/src/main/java/org/wikipedia/page/pageload/PageLoad.kt @@ -29,7 +29,13 @@ sealed class LoadType { sealed class LoadState { object Idle : LoadState() - object Loading : LoadState() - data class Success(val result: PageResult? = null) : LoadState() + data class SpecialPage(val request: PageLoadRequest): LoadState() + data class Loading(val isRefresh: Boolean = false) : LoadState() + data class Success( + val result: PageResult.Success? = null, + val isNewTabCreated: Boolean = false, + val title: PageTitle, + val stagedScrollY: Int = 0, + val loadedFromBackground: Boolean = false) : LoadState() data class Error(val throwable: Throwable) : LoadState() } diff --git a/app/src/main/java/org/wikipedia/page/pageload/PageLoadViewModel.kt b/app/src/main/java/org/wikipedia/page/pageload/PageLoadViewModel.kt new file mode 100644 index 00000000000..6d40c638ec7 --- /dev/null +++ b/app/src/main/java/org/wikipedia/page/pageload/PageLoadViewModel.kt @@ -0,0 +1,279 @@ +package org.wikipedia.page.pageload + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.ViewModelProvider.AndroidViewModelFactory.Companion.APPLICATION_KEY +import androidx.lifecycle.viewModelScope +import androidx.lifecycle.viewmodel.initializer +import androidx.lifecycle.viewmodel.viewModelFactory +import kotlinx.coroutines.CoroutineExceptionHandler +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch +import org.wikipedia.Constants +import org.wikipedia.WikipediaApp +import org.wikipedia.database.AppDatabase +import org.wikipedia.dataclient.ServiceFactory +import org.wikipedia.page.Namespace +import org.wikipedia.page.PageActivity +import org.wikipedia.page.PageBackStackItem +import org.wikipedia.page.PageTitle +import org.wikipedia.page.PageViewModel +import org.wikipedia.page.tabs.Tab +import org.wikipedia.util.log.L + +class PageLoadViewModel(private val app: WikipediaApp) : ViewModel() { + + private val _loadState = MutableStateFlow(LoadState.Idle) + val loadState: StateFlow = _loadState.asStateFlow() + + private val _progressVisible = MutableStateFlow(false) + val progressVisible: StateFlow = _progressVisible.asStateFlow() + + private val _currentPageModel = MutableStateFlow(null) + val currentPageModel: StateFlow = _currentPageModel.asStateFlow() + + private val _isRefreshing = MutableStateFlow(false) + val isRefreshing: StateFlow = _isRefreshing.asStateFlow() + + private val _errorState = MutableStateFlow(null) + val errorState: StateFlow = _errorState.asStateFlow() + + // Internal state + private var currentTab: Tab = app.tabList.last() + private val pageDataFetcher = PageDataFetcher() + val foregroundTabPosition get() = app.tabList.size + val backgroundTabPosition get() = 0.coerceAtLeast(foregroundTabPosition - 1) + + fun loadPage(request: PageLoadRequest, webScrollY: Int = 0) { + viewModelScope.launch(CoroutineExceptionHandler {_, throwable -> + handleError(throwable) + }) { + try { + _loadState.value = LoadState.Loading(request.options.isRefresh) + _progressVisible.value = true + _errorState.value = null + + when (determineLoadType(request)) { + LoadType.CurrentTab -> {} + LoadType.ExistingTab -> {} + LoadType.FromBackStack -> loadFromBackStack(request, webScrollY) + LoadType.NewBackgroundTab -> loadInNewBackgroundTab(request) + LoadType.NewForegroundTab -> loadInNewForegroundTab(request, webScrollY) + } + } catch (e: Exception) { + handleError(e) + } finally { + _progressVisible.value = false + } + } + } + + fun setTab(tab: Tab): Boolean { + val isDifferent = tab != currentTab + currentTab = tab + return isDifferent + } + + fun clearError() { + _errorState.value = null + } + + fun backStackEmpty(): Boolean { + return currentTab.backStack.isEmpty() + } + + fun updateCurrentBackStackItem(scrollY: Int) { + if (currentTab.backStack.isEmpty()) { + return + } + val item = currentTab.backStack[currentTab.backStackPosition] + item.scrollY = scrollY + _currentPageModel.value?.title?.let { + item.title.description = it.description + item.title.thumbUrl = it.thumbUrl + } + } + + private suspend fun loadFromBackStack(request: PageLoadRequest, webScrollY: Int) { + if (request.options.pushbackStack) { + // update the topmost entry in the backstack, before we start overwriting things. + updateCurrentBackStackItem(webScrollY) + currentTab.pushBackStackItem(PageBackStackItem(request.title, request.entry)) + } + loadPageData(request, false) + } + + private suspend fun loadInNewForegroundTab(request: PageLoadRequest, webScrollY: Int) { + if (request.options.squashBackStack) { + if (app.tabCount > 0) { + app.tabList.last().clearBackstack() + } + } + if (request.options.pushbackStack) { + // update the topmost entry in the backstack, before we start overwriting things. + updateCurrentBackStackItem(webScrollY) + currentTab.pushBackStackItem(PageBackStackItem(request.title, request.entry)) + } + val isNewTabCreated = createNewTab(request, isForeground = true) + loadPageData(request, isNewTabCreated) + } + + private fun loadInNewBackgroundTab(request: PageLoadRequest) { + val isForeground = app.tabCount == 0 + if (isForeground) { + createNewTab(request, isForeground = true) + loadFromBackStack() + } else { + val isNewTabCreated = createNewTab(request, isForeground = false) + _loadState.value = LoadState.Success(title = request.title, isNewTabCreated = isNewTabCreated, loadedFromBackground = true) + } + } + + fun loadFromBackStack() { + if (currentTab.backStack.isEmpty()) { + return + } + val item = currentTab.backStack[currentTab.backStackPosition] + // display the page based on the backstack item, stage the scrollY position based on + // the backstack item. + loadPage(request = PageLoadRequest( + title = item.title, + entry = item.historyEntry, + options = PageLoadOptions( + pushbackStack = false, + stagedScrollY = item.scrollY, + shouldLoadFromBackStack = true, + )) + ) + L.d("Loaded page " + item.title.displayText + " from backstack") + } + + private suspend fun loadPageData(request: PageLoadRequest, isNewTabCreated: Boolean) { + val result = pageDataFetcher.fetchPage( + title = request.title, + entry = request.entry, + forceNetwork = request.options.isRefresh + ) + when (result) { + is PageResult.Success -> { + val pageModel = createPageModel(request, result) + if (request.title.namespace() == Namespace.SPECIAL) { + _loadState.value = LoadState.SpecialPage(request) + return + } + _currentPageModel.value = pageModel + _loadState.value = LoadState.Success(result, isNewTabCreated, request.title, request.options.stagedScrollY) + } + is PageResult.Error -> { + handleError(result.throwable) + } + } + } + + private fun loadBackgroundTabMetadata(title: PageTitle) { + viewModelScope.launch(CoroutineExceptionHandler { _, t -> L.e(t) }) { + ServiceFactory.get(title.wikiSite) + .getInfoByPageIdsOrTitles(null, title.prefixedText) + .query?.firstPage()?.let { page -> + app.tabList.find { it.backStackPositionTitle == title } + ?.backStackPositionTitle?.apply { + thumbUrl = page.thumbUrl() + description = page.description + } + } + } + } + + private fun createNewTab(request: PageLoadRequest, isForeground: Boolean): Boolean { + val existingTabPosition = selectedTabPosition(request.title) + if (existingTabPosition >= 0) { + switchToExistingTab(existingTabPosition) + return false + } + + val shouldCreateNewTab = currentTab.backStack.isNotEmpty() + if (shouldCreateNewTab) { + val tab = Tab() + val position = if (isForeground) foregroundTabPosition else backgroundTabPosition + if (isForeground) { + setTab(tab) + } + app.tabList.add(position, tab) + trimTabCount() + + tab.backStack.add(PageBackStackItem(request.title, request.entry)) + if (!isForeground) { + // Load metadata for background tab + loadBackgroundTabMetadata(request.title) + } + return true + } else { + setTab(currentTab) + currentTab.backStack.add(PageBackStackItem(request.title, request.entry)) + return false + } + } + + private fun switchToExistingTab(position: Int) { + if (position < app.tabList.size - 1) { + val tab = app.tabList.removeAt(position) + app.tabList.add(tab) + setTab(tab) + } + + if (app.tabCount > 0) { + app.tabList.last().squashBackstack() + loadFromBackStack() + } + } + + private fun trimTabCount() { + while (app.tabList.size > Constants.MAX_TABS) { + app.tabList.removeAt(0) + } + } + + private fun selectedTabPosition(title: PageTitle): Int { + return app.tabList.firstOrNull { it.backStackPositionTitle != null && + title == it.backStackPositionTitle }?.let { app.tabList.indexOf(it) } ?: -1 + } + + private suspend fun createPageModel(request: PageLoadRequest, result: PageResult.Success): PageViewModel { + return PageViewModel().apply { + title = request.title + curEntry = request.entry + forceNetwork = request.options.isRefresh + readingListPage = AppDatabase.instance.readingListPageDao().findPageInAnyList(request.title) + page = result.pageSummaryResponse.body()?.toPage(title) + isWatched = result.isWatched + hasWatchlistExpiry = result.hasWatchlistExpiry + } + } + + private fun handleError(throwable: Throwable) { + L.e(throwable) + _errorState.value = throwable + _loadState.value = LoadState.Error(throwable) + } + + private fun determineLoadType(request: PageLoadRequest): LoadType { + return when { + request.options.shouldLoadFromBackStack -> LoadType.FromBackStack + request.options.tabPosition == PageActivity.TabPosition.NEW_TAB_FOREGROUND -> LoadType.NewForegroundTab + request.options.tabPosition == PageActivity.TabPosition.NEW_TAB_BACKGROUND -> LoadType.NewBackgroundTab + request.options.tabPosition == PageActivity.TabPosition.EXISTING_TAB -> LoadType.ExistingTab + else -> LoadType.CurrentTab + } + } + + companion object { + val Factory: ViewModelProvider.Factory = viewModelFactory { + initializer { + val app = this[APPLICATION_KEY] as WikipediaApp + PageLoadViewModel(app) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/wikipedia/page/pageload/PageLoader.kt b/app/src/main/java/org/wikipedia/page/pageload/PageLoader.kt index 4fc65b5ca21..7121f63639e 100644 --- a/app/src/main/java/org/wikipedia/page/pageload/PageLoader.kt +++ b/app/src/main/java/org/wikipedia/page/pageload/PageLoader.kt @@ -199,19 +199,23 @@ class PageLoader( } private fun prepareForLoad(request: PageLoadRequest) { + // added to fragment fragment.clearActivityActionBarTitle() fragment.dismissBottomSheet() + // done added to load function if (request.options.squashBackStack) { if (app.tabCount > 0) { app.tabList.last().clearBackstack() } } + // added to fragment fragment.updateProgressBar(true) fragment.sidePanelHandler.setEnabled(false) fragment.callback()?.onPageSetToolbarElevationEnabled(false) + // done // Update model fragment.model.title = request.title fragment.model.curEntry = request.entry @@ -219,6 +223,7 @@ class PageLoader( fragment.model.readingListPage = null fragment.model.forceNetwork = request.options.isRefresh + // done // Clear previous state fragment.errorState = false fragment.binding.pageError.visibility = View.GONE @@ -226,6 +231,7 @@ class PageLoader( fragment.binding.pageActionsTabLayout.visibility = View.VISIBLE fragment.binding.pageActionsTabLayout.enableAllTabs() + // done // Reset references and other state fragment.references = null fragment.revision = 0 @@ -233,21 +239,24 @@ class PageLoader( } private fun executeLoad(request: PageLoadRequest) { + // added to load function if (request.options.pushbackStack) { updateCurrentBackStackItem() currentTab.pushBackStackItem(PageBackStackItem(request.title, request.entry)) } + // added to on success state because this check should run before if (request.title.namespace() == Namespace.SPECIAL) { handleSpecialPage(request) return } + // done fragment.lifecycleScope.launch(CoroutineExceptionHandler { _, throwable -> L.e("Page details network error: ", throwable) handleLoadError(throwable) }) { - updateLoadingState(LoadState.Loading) + updateLoadingState(LoadState.Loading()) val result = dataFetcher.fetchPage( request.title, request.entry, @@ -255,7 +264,7 @@ class PageLoader( ) when (result) { is PageResult.Success -> { - updateLoadingState(LoadState.Success()) + //updateLoadingState(LoadState.Success()) handleLoadSuccess(result, request) } is PageResult.Error -> { @@ -286,6 +295,8 @@ class PageLoader( } private fun handleLoadSuccess(result: PageResult.Success, request: PageLoadRequest) { + // done in createPageModel function + // data update val response = result.pageSummaryResponse val pageSummary = response.body() val page = pageSummary?.toPage(fragment.model.title) @@ -294,17 +305,27 @@ class PageLoader( fragment.model.hasWatchlistExpiry = result.hasWatchlistExpiry fragment.model.title = page?.title + + // in sucess state on fragment + // ui update if (!request.title.prefixedText.contains(":")) { bridge.resetHtml(request.title) } + // in sucess state on fragment + // ui update + // @TODO: scrollY is received from the item when loading back from stack done if (request.options.stagedScrollY > 0) { fragment.scrollTriggerListener.stagedScrollY = request.options.stagedScrollY } + // in sucess state on fragment + // ui update fragment.updateQuickActionsAndMenuOptions() fragment.requireActivity().invalidateOptionsMenu() + // in sucess state on fragment + // ui update leadImagesHandler.loadLeadImage() fragment.onPageMetadataLoaded(result.redirectedFrom) } From d98719f58d52e6830f91884a332e37cfb0f33c06 Mon Sep 17 00:00:00 2001 From: williamrai Date: Thu, 10 Jul 2025 10:12:59 -0400 Subject: [PATCH 05/18] - refactor code fixes step 5 (converting to viewModel) --- .../java/org/wikipedia/page/PageActivity.kt | 2 +- .../java/org/wikipedia/page/PageFragment.kt | 4 ++ .../org/wikipedia/page/pageload/PageLoad.kt | 3 +- .../page/pageload/PageLoadViewModel.kt | 45 ++++++++++++------- 4 files changed, 37 insertions(+), 17 deletions(-) diff --git a/app/src/main/java/org/wikipedia/page/PageActivity.kt b/app/src/main/java/org/wikipedia/page/PageActivity.kt index 2db776451b7..46f54247c8b 100644 --- a/app/src/main/java/org/wikipedia/page/PageActivity.kt +++ b/app/src/main/java/org/wikipedia/page/PageActivity.kt @@ -619,7 +619,7 @@ class PageActivity : BaseActivity(), PageFragment.Callback, LinkPreviewDialog.Lo TabPosition.CURRENT_TAB_SQUASH -> PageLoadOptions(tabPosition = position, squashBackStack = true) TabPosition.NEW_TAB_BACKGROUND -> PageLoadOptions(tabPosition = position) TabPosition.NEW_TAB_FOREGROUND -> PageLoadOptions(tabPosition = position, pushbackStack = false) - TabPosition.EXISTING_TAB -> PageLoadOptions(tabPosition = position) + TabPosition.EXISTING_TAB -> PageLoadOptions(tabPosition = position, pushbackStack = true, squashBackStack = true) } pageFragment.loadPage(pageTitle, entry, options) } diff --git a/app/src/main/java/org/wikipedia/page/PageFragment.kt b/app/src/main/java/org/wikipedia/page/PageFragment.kt index 45d987249e1..473fd32bc95 100644 --- a/app/src/main/java/org/wikipedia/page/PageFragment.kt +++ b/app/src/main/java/org/wikipedia/page/PageFragment.kt @@ -292,6 +292,10 @@ class PageFragment : Fragment(), BackPressedHandler, CommunicationBridge.Communi (requireActivity() as PageActivity).animateTabsButton() return@collect } + if (state.sectionAnchor != null) { + scrollToSection(state.sectionAnchor) + return@collect + } if (!state.title.prefixedText.contains(":")) { bridge.resetHtml(state.title) } diff --git a/app/src/main/java/org/wikipedia/page/pageload/PageLoad.kt b/app/src/main/java/org/wikipedia/page/pageload/PageLoad.kt index fb116c40e43..11f4fc86dcd 100644 --- a/app/src/main/java/org/wikipedia/page/pageload/PageLoad.kt +++ b/app/src/main/java/org/wikipedia/page/pageload/PageLoad.kt @@ -36,6 +36,7 @@ sealed class LoadState { val isNewTabCreated: Boolean = false, val title: PageTitle, val stagedScrollY: Int = 0, - val loadedFromBackground: Boolean = false) : LoadState() + val loadedFromBackground: Boolean = false, + val sectionAnchor: String? = null) : LoadState() data class Error(val throwable: Throwable) : LoadState() } diff --git a/app/src/main/java/org/wikipedia/page/pageload/PageLoadViewModel.kt b/app/src/main/java/org/wikipedia/page/pageload/PageLoadViewModel.kt index 6d40c638ec7..8446115c8fd 100644 --- a/app/src/main/java/org/wikipedia/page/pageload/PageLoadViewModel.kt +++ b/app/src/main/java/org/wikipedia/page/pageload/PageLoadViewModel.kt @@ -51,13 +51,9 @@ class PageLoadViewModel(private val app: WikipediaApp) : ViewModel() { handleError(throwable) }) { try { - _loadState.value = LoadState.Loading(request.options.isRefresh) - _progressVisible.value = true - _errorState.value = null - when (determineLoadType(request)) { - LoadType.CurrentTab -> {} - LoadType.ExistingTab -> {} + LoadType.CurrentTab -> loadInCurrentTab(request, webScrollY) + LoadType.ExistingTab -> loadInExistingTab(request) LoadType.FromBackStack -> loadFromBackStack(request, webScrollY) LoadType.NewBackgroundTab -> loadInNewBackgroundTab(request) LoadType.NewForegroundTab -> loadInNewForegroundTab(request, webScrollY) @@ -96,30 +92,46 @@ class PageLoadViewModel(private val app: WikipediaApp) : ViewModel() { } } - private suspend fun loadFromBackStack(request: PageLoadRequest, webScrollY: Int) { - if (request.options.pushbackStack) { - // update the topmost entry in the backstack, before we start overwriting things. - updateCurrentBackStackItem(webScrollY) - currentTab.pushBackStackItem(PageBackStackItem(request.title, request.entry)) + private suspend fun loadInExistingTab(request: PageLoadRequest) { + val selectedTabPosition = selectedTabPosition(request.title) + if (selectedTabPosition == -1) { + loadPageData(request) } - loadPageData(request, false) } - private suspend fun loadInNewForegroundTab(request: PageLoadRequest, webScrollY: Int) { + private suspend fun loadInCurrentTab(request: PageLoadRequest, webScrollY: Int) { + val model = currentPageModel.value + if (currentTab.backStack.isNotEmpty() && + request.title == currentTab.backStack[currentTab.backStackPosition].title) { + if (model?.page == null || request.options.isRefresh) { + loadFromBackStack() + } else if (!request.title.fragment.isNullOrEmpty()) { + _loadState.value = LoadState.Success(title = request.title, sectionAnchor = request.title.fragment) + } + return + } if (request.options.squashBackStack) { if (app.tabCount > 0) { app.tabList.last().clearBackstack() } } + loadFromBackStack(request, webScrollY) + } + + private suspend fun loadFromBackStack(request: PageLoadRequest, webScrollY: Int, isNewTabCreated: Boolean = false) { if (request.options.pushbackStack) { // update the topmost entry in the backstack, before we start overwriting things. updateCurrentBackStackItem(webScrollY) currentTab.pushBackStackItem(PageBackStackItem(request.title, request.entry)) } - val isNewTabCreated = createNewTab(request, isForeground = true) loadPageData(request, isNewTabCreated) } + private suspend fun loadInNewForegroundTab(request: PageLoadRequest, webScrollY: Int) { + val isNewTabCreated = createNewTab(request, isForeground = true) + loadFromBackStack(request, webScrollY, isNewTabCreated) + } + private fun loadInNewBackgroundTab(request: PageLoadRequest) { val isForeground = app.tabCount == 0 if (isForeground) { @@ -150,7 +162,10 @@ class PageLoadViewModel(private val app: WikipediaApp) : ViewModel() { L.d("Loaded page " + item.title.displayText + " from backstack") } - private suspend fun loadPageData(request: PageLoadRequest, isNewTabCreated: Boolean) { + private suspend fun loadPageData(request: PageLoadRequest, isNewTabCreated: Boolean = false) { + _loadState.value = LoadState.Loading(request.options.isRefresh) + _progressVisible.value = true + _errorState.value = null val result = pageDataFetcher.fetchPage( title = request.title, entry = request.entry, From 0d10fdc59468257690082a0f044dce4a67ee4416 Mon Sep 17 00:00:00 2001 From: williamrai Date: Thu, 10 Jul 2025 11:40:01 -0400 Subject: [PATCH 06/18] - refactor code fixes step 6(converting to viewModel) --- .../java/org/wikipedia/page/PageFragment.kt | 137 +++++++++--------- .../org/wikipedia/page/pageload/PageLoad.kt | 12 +- .../page/pageload/PageLoadViewModel.kt | 104 ++++++------- .../org/wikipedia/page/pageload/PageLoader.kt | 15 +- 4 files changed, 127 insertions(+), 141 deletions(-) diff --git a/app/src/main/java/org/wikipedia/page/PageFragment.kt b/app/src/main/java/org/wikipedia/page/PageFragment.kt index 473fd32bc95..f5ba4576e2b 100644 --- a/app/src/main/java/org/wikipedia/page/PageFragment.kt +++ b/app/src/main/java/org/wikipedia/page/PageFragment.kt @@ -85,9 +85,9 @@ import org.wikipedia.page.campaign.CampaignDialog import org.wikipedia.page.edithistory.EditHistoryListActivity import org.wikipedia.page.issues.PageIssuesDialog import org.wikipedia.page.leadimages.LeadImagesHandler -import org.wikipedia.page.pageload.LoadState import org.wikipedia.page.pageload.PageLoadOptions import org.wikipedia.page.pageload.PageLoadRequest +import org.wikipedia.page.pageload.PageLoadUiState import org.wikipedia.page.pageload.PageLoadViewModel import org.wikipedia.page.pageload.PageLoader import org.wikipedia.page.references.PageReferences @@ -258,74 +258,6 @@ class PageFragment : Fragment(), BackPressedHandler, CommunicationBridge.Communi setupObservers() } - private fun setupObservers() { - viewLifecycleOwner.lifecycleScope.launch { - pageLoadViewModel.loadState.collect { state -> - when (state) { - is LoadState.Loading -> { - clearActivityActionBarTitle() - dismissBottomSheet() - - updateProgressBar(true) - sidePanelHandler.setEnabled(false) - callback()?.onPageSetToolbarElevationEnabled(false) - - - // Clear previous state - errorState = false - binding.pageError.visibility = View.GONE - webView.visibility = View.VISIBLE - binding.pageActionsTabLayout.visibility = View.VISIBLE - binding.pageActionsTabLayout.enableAllTabs() - - // Reset references and other state - references = null - revision = 0 - pageRefreshed = state.isRefresh - } - is LoadState.Success -> { - // when new tab is created - if (state.isNewTabCreated) { - requireActivity().invalidateOptionsMenu() - } - if (state.loadedFromBackground) { - (requireActivity() as PageActivity).animateTabsButton() - return@collect - } - if (state.sectionAnchor != null) { - scrollToSection(state.sectionAnchor) - return@collect - } - if (!state.title.prefixedText.contains(":")) { - bridge.resetHtml(state.title) - } - scrollTriggerListener.stagedScrollY = state.stagedScrollY - updateQuickActionsAndMenuOptions() - requireActivity().invalidateOptionsMenu() - leadImagesHandler.loadLeadImage() - onPageMetadataLoaded(state.result?.redirectedFrom) - - } - is LoadState.Error -> {} - LoadState.Idle -> {} - is LoadState.SpecialPage -> { - bridge.resetHtml(state.request.title) - leadImagesHandler.loadLeadImage() - requireActivity().invalidateOptionsMenu() - onPageMetadataLoaded() - } - } - } - } - viewLifecycleOwner.lifecycleScope.launch { - pageLoadViewModel.currentPageModel.collect { pageViewModel -> - pageViewModel?.let { - model = it - } - } - } - } - override fun onSaveInstanceState(outState: Bundle) { super.onSaveInstanceState(outState) outState.putBoolean(ARG_THEME_CHANGE_SCROLLED, scrolledUpForThemeChange) @@ -1424,6 +1356,73 @@ class PageFragment : Fragment(), BackPressedHandler, CommunicationBridge.Communi } } + // UI observers + private fun setupObservers() { + viewLifecycleOwner.lifecycleScope.launch { + pageLoadViewModel.pageLoadState.collect { state -> + when (state.uiState) { + is PageLoadUiState.Loading -> handleLoadingState(state.uiState) + is PageLoadUiState.Success -> handleSuccessPageLoadingState(state.uiState) + is PageLoadUiState.SpecialPage -> handleSpecialLoadingPage(state.uiState) + is PageLoadUiState.Error -> {} + } + if (state.isTabCreated) { + requireActivity().invalidateOptionsMenu() + } + if (state.currentPageModel != null) { + model = state.currentPageModel + } + } + } + } + + private fun handleLoadingState(state: PageLoadUiState.Loading) { + clearActivityActionBarTitle() + dismissBottomSheet() + + updateProgressBar(true) + sidePanelHandler.setEnabled(false) + callback()?.onPageSetToolbarElevationEnabled(false) + + // Clear previous state + errorState = false + binding.pageError.visibility = View.GONE + webView.visibility = View.VISIBLE + binding.pageActionsTabLayout.visibility = View.VISIBLE + binding.pageActionsTabLayout.enableAllTabs() + + // Reset references and other state + references = null + revision = 0 + pageRefreshed = state.isRefresh + } + + private fun handleSuccessPageLoadingState(state: PageLoadUiState.Success) { + when { + state.loadedFromBackground -> { + (requireActivity() as PageActivity).animateTabsButton() + return + } + state.sectionAnchor != null -> { + scrollToSection(state.sectionAnchor) + return + } + !state.title.prefixedText.contains(":") -> bridge.resetHtml(state.title) + } + scrollTriggerListener.stagedScrollY = state.stagedScrollY + updateQuickActionsAndMenuOptions() + requireActivity().invalidateOptionsMenu() + leadImagesHandler.loadLeadImage() + onPageMetadataLoaded(state.result?.redirectedFrom) + } + + private fun handleSpecialLoadingPage(state: PageLoadUiState.SpecialPage) { + bridge.resetHtml(state.request.title) + leadImagesHandler.loadLeadImage() + requireActivity().invalidateOptionsMenu() + onPageMetadataLoaded() + } + companion object { private const val ARG_THEME_CHANGE_SCROLLED = "themeChangeScrolled" private val REFRESH_SPINNER_ADDITIONAL_OFFSET = (16 * DimenUtil.densityScalar).toInt() diff --git a/app/src/main/java/org/wikipedia/page/pageload/PageLoad.kt b/app/src/main/java/org/wikipedia/page/pageload/PageLoad.kt index 11f4fc86dcd..abe2e6201ed 100644 --- a/app/src/main/java/org/wikipedia/page/pageload/PageLoad.kt +++ b/app/src/main/java/org/wikipedia/page/pageload/PageLoad.kt @@ -27,16 +27,14 @@ sealed class LoadType { object FromBackStack : LoadType() } -sealed class LoadState { - object Idle : LoadState() - data class SpecialPage(val request: PageLoadRequest): LoadState() - data class Loading(val isRefresh: Boolean = false) : LoadState() +sealed class PageLoadUiState { + data class SpecialPage(val request: PageLoadRequest) : PageLoadUiState() + data class Loading(val isRefresh: Boolean = false) : PageLoadUiState() data class Success( val result: PageResult.Success? = null, - val isNewTabCreated: Boolean = false, val title: PageTitle, val stagedScrollY: Int = 0, val loadedFromBackground: Boolean = false, - val sectionAnchor: String? = null) : LoadState() - data class Error(val throwable: Throwable) : LoadState() + val sectionAnchor: String? = null) : PageLoadUiState() + data class Error(val throwable: Throwable) : PageLoadUiState() } diff --git a/app/src/main/java/org/wikipedia/page/pageload/PageLoadViewModel.kt b/app/src/main/java/org/wikipedia/page/pageload/PageLoadViewModel.kt index 8446115c8fd..d0ab61f047f 100644 --- a/app/src/main/java/org/wikipedia/page/pageload/PageLoadViewModel.kt +++ b/app/src/main/java/org/wikipedia/page/pageload/PageLoadViewModel.kt @@ -10,6 +10,7 @@ import kotlinx.coroutines.CoroutineExceptionHandler import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import org.wikipedia.Constants import org.wikipedia.WikipediaApp @@ -25,20 +26,8 @@ import org.wikipedia.util.log.L class PageLoadViewModel(private val app: WikipediaApp) : ViewModel() { - private val _loadState = MutableStateFlow(LoadState.Idle) - val loadState: StateFlow = _loadState.asStateFlow() - - private val _progressVisible = MutableStateFlow(false) - val progressVisible: StateFlow = _progressVisible.asStateFlow() - - private val _currentPageModel = MutableStateFlow(null) - val currentPageModel: StateFlow = _currentPageModel.asStateFlow() - - private val _isRefreshing = MutableStateFlow(false) - val isRefreshing: StateFlow = _isRefreshing.asStateFlow() - - private val _errorState = MutableStateFlow(null) - val errorState: StateFlow = _errorState.asStateFlow() + private val _pageLoadState = MutableStateFlow(PageLoadState()) + val pageLoadState: StateFlow = _pageLoadState.asStateFlow() // Internal state private var currentTab: Tab = app.tabList.last() @@ -47,21 +36,15 @@ class PageLoadViewModel(private val app: WikipediaApp) : ViewModel() { val backgroundTabPosition get() = 0.coerceAtLeast(foregroundTabPosition - 1) fun loadPage(request: PageLoadRequest, webScrollY: Int = 0) { - viewModelScope.launch(CoroutineExceptionHandler {_, throwable -> + viewModelScope.launch(CoroutineExceptionHandler { _, throwable -> handleError(throwable) }) { - try { - when (determineLoadType(request)) { - LoadType.CurrentTab -> loadInCurrentTab(request, webScrollY) - LoadType.ExistingTab -> loadInExistingTab(request) - LoadType.FromBackStack -> loadFromBackStack(request, webScrollY) - LoadType.NewBackgroundTab -> loadInNewBackgroundTab(request) - LoadType.NewForegroundTab -> loadInNewForegroundTab(request, webScrollY) - } - } catch (e: Exception) { - handleError(e) - } finally { - _progressVisible.value = false + when (determineLoadType(request)) { + LoadType.CurrentTab -> loadInCurrentTab(request, webScrollY) + LoadType.ExistingTab -> loadInExistingTab(request) + LoadType.FromBackStack -> loadPageData(request) + LoadType.NewBackgroundTab -> loadInNewBackgroundTab(request) + LoadType.NewForegroundTab -> loadInNewForegroundTab(request, webScrollY) } } } @@ -72,10 +55,6 @@ class PageLoadViewModel(private val app: WikipediaApp) : ViewModel() { return isDifferent } - fun clearError() { - _errorState.value = null - } - fun backStackEmpty(): Boolean { return currentTab.backStack.isEmpty() } @@ -86,7 +65,7 @@ class PageLoadViewModel(private val app: WikipediaApp) : ViewModel() { } val item = currentTab.backStack[currentTab.backStackPosition] item.scrollY = scrollY - _currentPageModel.value?.title?.let { + _pageLoadState.value.currentPageModel?.title?.let { item.title.description = it.description item.title.thumbUrl = it.thumbUrl } @@ -100,13 +79,15 @@ class PageLoadViewModel(private val app: WikipediaApp) : ViewModel() { } private suspend fun loadInCurrentTab(request: PageLoadRequest, webScrollY: Int) { - val model = currentPageModel.value + val model = _pageLoadState.value.currentPageModel if (currentTab.backStack.isNotEmpty() && request.title == currentTab.backStack[currentTab.backStackPosition].title) { if (model?.page == null || request.options.isRefresh) { loadFromBackStack() } else if (!request.title.fragment.isNullOrEmpty()) { - _loadState.value = LoadState.Success(title = request.title, sectionAnchor = request.title.fragment) + _pageLoadState.update { + it.copy(uiState = PageLoadUiState.Success(title = request.title, sectionAnchor = request.title.fragment)) + } } return } @@ -115,21 +96,22 @@ class PageLoadViewModel(private val app: WikipediaApp) : ViewModel() { app.tabList.last().clearBackstack() } } - loadFromBackStack(request, webScrollY) - } - - private suspend fun loadFromBackStack(request: PageLoadRequest, webScrollY: Int, isNewTabCreated: Boolean = false) { if (request.options.pushbackStack) { // update the topmost entry in the backstack, before we start overwriting things. updateCurrentBackStackItem(webScrollY) currentTab.pushBackStackItem(PageBackStackItem(request.title, request.entry)) } - loadPageData(request, isNewTabCreated) + loadPageData(request) } - private suspend fun loadInNewForegroundTab(request: PageLoadRequest, webScrollY: Int) { - val isNewTabCreated = createNewTab(request, isForeground = true) - loadFromBackStack(request, webScrollY, isNewTabCreated) + private fun loadInNewForegroundTab(request: PageLoadRequest, webScrollY: Int) { + createNewTab(request, isForeground = true) + if (request.options.pushbackStack) { + // update the topmost entry in the backstack, before we start overwriting things. + updateCurrentBackStackItem(webScrollY) + currentTab.pushBackStackItem(PageBackStackItem(request.title, request.entry)) + } + loadFromBackStack() } private fun loadInNewBackgroundTab(request: PageLoadRequest) { @@ -138,8 +120,10 @@ class PageLoadViewModel(private val app: WikipediaApp) : ViewModel() { createNewTab(request, isForeground = true) loadFromBackStack() } else { - val isNewTabCreated = createNewTab(request, isForeground = false) - _loadState.value = LoadState.Success(title = request.title, isNewTabCreated = isNewTabCreated, loadedFromBackground = true) + createNewTab(request, isForeground = false) + _pageLoadState.update { + it.copy(uiState = PageLoadUiState.Success(title = request.title, loadedFromBackground = true)) + } } } @@ -163,9 +147,7 @@ class PageLoadViewModel(private val app: WikipediaApp) : ViewModel() { } private suspend fun loadPageData(request: PageLoadRequest, isNewTabCreated: Boolean = false) { - _loadState.value = LoadState.Loading(request.options.isRefresh) - _progressVisible.value = true - _errorState.value = null + _pageLoadState.update { it.copy(uiState = PageLoadUiState.Loading(request.options.isRefresh)) } val result = pageDataFetcher.fetchPage( title = request.title, entry = request.entry, @@ -175,11 +157,15 @@ class PageLoadViewModel(private val app: WikipediaApp) : ViewModel() { is PageResult.Success -> { val pageModel = createPageModel(request, result) if (request.title.namespace() == Namespace.SPECIAL) { - _loadState.value = LoadState.SpecialPage(request) + _pageLoadState.update { it.copy(uiState = PageLoadUiState.SpecialPage(request)) } return } - _currentPageModel.value = pageModel - _loadState.value = LoadState.Success(result, isNewTabCreated, request.title, request.options.stagedScrollY) + _pageLoadState.update { + it.copy( + uiState = PageLoadUiState.Success(result, request.title, request.options.stagedScrollY), + currentPageModel = pageModel + ) + } } is PageResult.Error -> { handleError(result.throwable) @@ -201,11 +187,11 @@ class PageLoadViewModel(private val app: WikipediaApp) : ViewModel() { } } - private fun createNewTab(request: PageLoadRequest, isForeground: Boolean): Boolean { + private fun createNewTab(request: PageLoadRequest, isForeground: Boolean) { val existingTabPosition = selectedTabPosition(request.title) if (existingTabPosition >= 0) { switchToExistingTab(existingTabPosition) - return false + return } val shouldCreateNewTab = currentTab.backStack.isNotEmpty() @@ -223,11 +209,10 @@ class PageLoadViewModel(private val app: WikipediaApp) : ViewModel() { // Load metadata for background tab loadBackgroundTabMetadata(request.title) } - return true + _pageLoadState.update { it.copy(isTabCreated = true) } } else { setTab(currentTab) currentTab.backStack.add(PageBackStackItem(request.title, request.entry)) - return false } } @@ -269,8 +254,7 @@ class PageLoadViewModel(private val app: WikipediaApp) : ViewModel() { private fun handleError(throwable: Throwable) { L.e(throwable) - _errorState.value = throwable - _loadState.value = LoadState.Error(throwable) + _pageLoadState.update { it.copy(uiState = PageLoadUiState.Error(throwable)) } } private fun determineLoadType(request: PageLoadRequest): LoadType { @@ -291,4 +275,10 @@ class PageLoadViewModel(private val app: WikipediaApp) : ViewModel() { } } } -} \ No newline at end of file + + data class PageLoadState( + val uiState: PageLoadUiState = PageLoadUiState.Loading(), + val currentPageModel: PageViewModel? = null, + val isTabCreated: Boolean = false + ) +} diff --git a/app/src/main/java/org/wikipedia/page/pageload/PageLoader.kt b/app/src/main/java/org/wikipedia/page/pageload/PageLoader.kt index 7121f63639e..3bd7702ff54 100644 --- a/app/src/main/java/org/wikipedia/page/pageload/PageLoader.kt +++ b/app/src/main/java/org/wikipedia/page/pageload/PageLoader.kt @@ -256,7 +256,7 @@ class PageLoader( L.e("Page details network error: ", throwable) handleLoadError(throwable) }) { - updateLoadingState(LoadState.Loading()) + updateLoadingState(PageLoadUiState.Loading()) val result = dataFetcher.fetchPage( request.title, request.entry, @@ -264,11 +264,11 @@ class PageLoader( ) when (result) { is PageResult.Success -> { - //updateLoadingState(LoadState.Success()) + // updateLoadingState(LoadState.Success()) handleLoadSuccess(result, request) } is PageResult.Error -> { - updateLoadingState(LoadState.Error(result.throwable)) + updateLoadingState(PageLoadUiState.Error(result.throwable)) handleLoadError(result.throwable) } } @@ -305,7 +305,6 @@ class PageLoader( fragment.model.hasWatchlistExpiry = result.hasWatchlistExpiry fragment.model.title = page?.title - // in sucess state on fragment // ui update if (!request.title.prefixedText.contains(":")) { @@ -338,11 +337,11 @@ class PageLoader( fragment.onPageLoadError(error) } - private fun updateLoadingState(state: LoadState) { + private fun updateLoadingState(state: PageLoadUiState) { when (state) { - is LoadState.Loading -> fragment.updateProgressBar(true) - is LoadState.Success -> fragment.updateProgressBar(false) - is LoadState.Error -> fragment.updateProgressBar(false) + is PageLoadUiState.Loading -> fragment.updateProgressBar(true) + is PageLoadUiState.Success -> fragment.updateProgressBar(false) + is PageLoadUiState.Error -> fragment.updateProgressBar(false) else -> {} } } From 2b5f418a7de56caa686f9f088099cce7c0766d73 Mon Sep 17 00:00:00 2001 From: williamrai Date: Thu, 10 Jul 2025 16:03:56 -0400 Subject: [PATCH 07/18] - refactor code fixes step 7(converting to viewModel) --- .../java/org/wikipedia/page/PageFragment.kt | 14 ++-- .../org/wikipedia/page/pageload/PageLoad.kt | 1 + .../page/pageload/PageLoadViewModel.kt | 65 ++++++++++++++++++- 3 files changed, 72 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/org/wikipedia/page/PageFragment.kt b/app/src/main/java/org/wikipedia/page/PageFragment.kt index f5ba4576e2b..df5f956cb4d 100644 --- a/app/src/main/java/org/wikipedia/page/PageFragment.kt +++ b/app/src/main/java/org/wikipedia/page/PageFragment.kt @@ -1360,18 +1360,18 @@ class PageFragment : Fragment(), BackPressedHandler, CommunicationBridge.Communi private fun setupObservers() { viewLifecycleOwner.lifecycleScope.launch { pageLoadViewModel.pageLoadState.collect { state -> - when (state.uiState) { - is PageLoadUiState.Loading -> handleLoadingState(state.uiState) - is PageLoadUiState.Success -> handleSuccessPageLoadingState(state.uiState) - is PageLoadUiState.SpecialPage -> handleSpecialLoadingPage(state.uiState) - is PageLoadUiState.Error -> {} - } if (state.isTabCreated) { requireActivity().invalidateOptionsMenu() } if (state.currentPageModel != null) { model = state.currentPageModel } + when (state.uiState) { + is PageLoadUiState.Loading -> handleLoadingState(state.uiState) + is PageLoadUiState.Success -> handleSuccessPageLoadingState(state.uiState) + is PageLoadUiState.SpecialPage -> handleSpecialLoadingPage(state.uiState) + is PageLoadUiState.Error -> {} + } } } } @@ -1409,6 +1409,8 @@ class PageFragment : Fragment(), BackPressedHandler, CommunicationBridge.Communi } !state.title.prefixedText.contains(":") -> bridge.resetHtml(state.title) } + pageLoadViewModel.updateTabListToPreventZHVariantIssue(model.title) + pageLoadViewModel.saveInformationToDatabase(model, state.result?.pageSummaryResponse?.body()) scrollTriggerListener.stagedScrollY = state.stagedScrollY updateQuickActionsAndMenuOptions() requireActivity().invalidateOptionsMenu() diff --git a/app/src/main/java/org/wikipedia/page/pageload/PageLoad.kt b/app/src/main/java/org/wikipedia/page/pageload/PageLoad.kt index abe2e6201ed..27501165603 100644 --- a/app/src/main/java/org/wikipedia/page/pageload/PageLoad.kt +++ b/app/src/main/java/org/wikipedia/page/pageload/PageLoad.kt @@ -3,6 +3,7 @@ package org.wikipedia.page.pageload import org.wikipedia.history.HistoryEntry import org.wikipedia.page.PageActivity import org.wikipedia.page.PageTitle +import org.wikipedia.page.PageViewModel data class PageLoadRequest( val title: PageTitle, diff --git a/app/src/main/java/org/wikipedia/page/pageload/PageLoadViewModel.kt b/app/src/main/java/org/wikipedia/page/pageload/PageLoadViewModel.kt index d0ab61f047f..295aced7216 100644 --- a/app/src/main/java/org/wikipedia/page/pageload/PageLoadViewModel.kt +++ b/app/src/main/java/org/wikipedia/page/pageload/PageLoadViewModel.kt @@ -14,8 +14,12 @@ import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import org.wikipedia.Constants import org.wikipedia.WikipediaApp +import org.wikipedia.analytics.eventplatform.ArticleLinkPreviewInteractionEvent +import org.wikipedia.analytics.metricsplatform.ArticleLinkPreviewInteraction import org.wikipedia.database.AppDatabase import org.wikipedia.dataclient.ServiceFactory +import org.wikipedia.dataclient.page.PageSummary +import org.wikipedia.history.HistoryEntry import org.wikipedia.page.Namespace import org.wikipedia.page.PageActivity import org.wikipedia.page.PageBackStackItem @@ -71,11 +75,54 @@ class PageLoadViewModel(private val app: WikipediaApp) : ViewModel() { } } + fun updateTabListToPreventZHVariantIssue(title: PageTitle?) { + if (title == null) return + WikipediaApp.instance.tabList.getOrNull(WikipediaApp.instance.tabCount - 1)?.setBackStackPositionTitle(title) + } + + fun saveInformationToDatabase( + currentPageModel: PageViewModel?, + pageSummary: PageSummary? + ) { + val pageModel = currentPageModel ?: return + val title = currentPageModel.title ?: return + + viewModelScope.launch { + pageModel.curEntry?.let { + val entry = HistoryEntry( + title, + it.source, + timestamp = it.timestamp + ).apply { + referrer = it.referrer + prevId = it.prevId + } + pageModel.curEntry = entry + // Insert and/or update this history entry in the DB + AppDatabase.instance.historyEntryDao().upsert(entry).run { + pageModel.curEntry?.id = this + } + + // Update metadata in the DB + AppDatabase.instance.pageImagesDao().upsertForMetadata(entry, title.thumbUrl, title.description, pageSummary?.coordinates?.latitude, pageSummary?.coordinates?.longitude) + + // And finally, count this as a page view. + WikipediaApp.instance.appSessionEvent.pageViewed(entry) + ArticleLinkPreviewInteractionEvent(title.wikiSite.dbName(), pageSummary?.pageId ?: 0, entry.source).logNavigate() + + // @TODO: requires fragment + // ArticleLinkPreviewInteraction(fragment, entry.source).logNavigate() + } + } + } + private suspend fun loadInExistingTab(request: PageLoadRequest) { val selectedTabPosition = selectedTabPosition(request.title) if (selectedTabPosition == -1) { loadPageData(request) + return } + switchToExistingTab(selectedTabPosition) } private suspend fun loadInCurrentTab(request: PageLoadRequest, webScrollY: Int) { @@ -242,13 +289,27 @@ class PageLoadViewModel(private val app: WikipediaApp) : ViewModel() { private suspend fun createPageModel(request: PageLoadRequest, result: PageResult.Success): PageViewModel { return PageViewModel().apply { - title = request.title curEntry = request.entry forceNetwork = request.options.isRefresh readingListPage = AppDatabase.instance.readingListPageDao().findPageInAnyList(request.title) - page = result.pageSummaryResponse.body()?.toPage(title) + val response = result.pageSummaryResponse + val pageSummary = response.body() + page = pageSummary?.toPage(request.title) + title = page?.title isWatched = result.isWatched hasWatchlistExpiry = result.hasWatchlistExpiry + title?.let { + if (!response.raw().request.url.fragment.isNullOrEmpty()) { + it.fragment = response.raw().request.url.fragment + } + if (it.description.isNullOrEmpty()) { + WikipediaApp.instance.appSessionEvent.noDescription() + } + if (!it.isMainPage) { + it.displayText = page?.displayTitle.orEmpty() + } + it.thumbUrl = pageSummary?.thumbnailUrl + } } } From ac7f826b46368e35ddcc0b3beea4c911dcbe7d59 Mon Sep 17 00:00:00 2001 From: williamrai Date: Fri, 11 Jul 2025 14:05:21 -0400 Subject: [PATCH 08/18] - refactor code fixes step 8(converting to viewModel), separating the api calls --- .../java/org/wikipedia/page/PageFragment.kt | 97 ++++++++--- .../java/org/wikipedia/page/PageViewModel.kt | 36 ++-- .../page/pageload/PageDataFetcher.kt | 13 +- .../org/wikipedia/page/pageload/PageLoad.kt | 7 +- .../page/pageload/PageLoadViewModel.kt | 163 +++++++++++------- 5 files changed, 208 insertions(+), 108 deletions(-) diff --git a/app/src/main/java/org/wikipedia/page/PageFragment.kt b/app/src/main/java/org/wikipedia/page/PageFragment.kt index df5f956cb4d..51062fa316d 100644 --- a/app/src/main/java/org/wikipedia/page/PageFragment.kt +++ b/app/src/main/java/org/wikipedia/page/PageFragment.kt @@ -51,6 +51,7 @@ import org.wikipedia.activity.FragmentUtil.getCallback import org.wikipedia.analytics.eventplatform.ArticleFindInPageInteractionEvent import org.wikipedia.analytics.eventplatform.ArticleInteractionEvent import org.wikipedia.analytics.eventplatform.DonorExperienceEvent +import org.wikipedia.analytics.eventplatform.EventPlatformClient import org.wikipedia.analytics.eventplatform.PlacesEvent import org.wikipedia.analytics.eventplatform.WatchlistAnalyticsHelper import org.wikipedia.analytics.metricsplatform.ArticleFindInPageInteraction @@ -80,6 +81,7 @@ import org.wikipedia.login.LoginActivity import org.wikipedia.main.MainActivity import org.wikipedia.media.AvPlayer import org.wikipedia.navtab.NavTab +import org.wikipedia.notifications.PollNotificationWorker import org.wikipedia.page.action.PageActionItem import org.wikipedia.page.campaign.CampaignDialog import org.wikipedia.page.edithistory.EditHistoryListActivity @@ -89,7 +91,6 @@ import org.wikipedia.page.pageload.PageLoadOptions import org.wikipedia.page.pageload.PageLoadRequest import org.wikipedia.page.pageload.PageLoadUiState import org.wikipedia.page.pageload.PageLoadViewModel -import org.wikipedia.page.pageload.PageLoader import org.wikipedia.page.references.PageReferences import org.wikipedia.page.references.ReferenceDialog import org.wikipedia.page.shareafact.ShareHandler @@ -108,6 +109,7 @@ import org.wikipedia.util.ImageUrlUtil import org.wikipedia.util.ResourceUtil import org.wikipedia.util.ShareUtil import org.wikipedia.util.ThrowableUtil +import org.wikipedia.util.UiState import org.wikipedia.util.UriUtil import org.wikipedia.util.log.L import org.wikipedia.views.ObservableWebView @@ -155,7 +157,7 @@ class PageFragment : Fragment(), BackPressedHandler, CommunicationBridge.Communi private val pageRefreshListener = OnRefreshListener { refreshPage() } private val pageActionItemCallback = PageActionItemCallback() - private lateinit var pageLoader: PageLoader + private lateinit var bridge: CommunicationBridge private lateinit var leadImagesHandler: LeadImagesHandler private lateinit var bottomBarHideHandler: ViewHideHandler @@ -246,14 +248,13 @@ class PageFragment : Fragment(), BackPressedHandler, CommunicationBridge.Communi sidePanelHandler = SidePanelHandler(this, bridge) leadImagesHandler = LeadImagesHandler(this, webView, binding.pageHeaderView, callback()) shareHandler = ShareHandler(this, bridge) - pageLoader = PageLoader(this, webView, bridge, leadImagesHandler, currentTab) if (callback() != null) { LongPressHandler(webView, HistoryEntry.SOURCE_INTERNAL_LINK, PageContainerLongPressHandler(this)) } if (shouldLoadFromBackstack(activity) || savedInstanceState != null) { - reloadFromBackstack() + //reloadFromBackstack() } setupObservers() } @@ -289,9 +290,9 @@ class PageFragment : Fragment(), BackPressedHandler, CommunicationBridge.Communi activeTimer.pause() addTimeSpentReading(activeTimer.elapsedSec) - pageLoader.updateCurrentBackStackItem() + pageLoadViewModel.updateCurrentBackStackItem(webView.scrollY) app.commitTabState() - val time = if (app.tabList.size >= 1 && !pageLoader.backStackEmpty()) System.currentTimeMillis() else 0 + val time = if (app.tabList.size >= 1 && !pageLoadViewModel.backStackEmpty()) System.currentTimeMillis() else 0 Prefs.pageLastShown = time articleInteractionEvent?.pause() metricsPlatformArticleEventToolbarInteraction.pause() @@ -317,7 +318,8 @@ class PageFragment : Fragment(), BackPressedHandler, CommunicationBridge.Communi // if the screen orientation changes, then re-layout the lead image container, // but only if we've finished fetching the page. if (!bridge.isLoading && !errorState) { - pageLoader.onConfigurationChanged() + leadImagesHandler.loadLeadImage() + bridge.execute(JavaScriptActionHandler.setTopMargin(leadImagesHandler.topMargin)) } } @@ -328,7 +330,8 @@ class PageFragment : Fragment(), BackPressedHandler, CommunicationBridge.Communi sidePanelHandler.hide() return true } - if (pageLoader.goBack()) { + + if (pageLoadViewModel.goBack()) { return true } // if the current tab can no longer go back, then close the tab before exiting @@ -829,9 +832,9 @@ class PageFragment : Fragment(), BackPressedHandler, CommunicationBridge.Communi } fun reloadFromBackstack(forceReload: Boolean = true) { - if (pageLoader.setTab(currentTab) || forceReload) { - if (!pageLoader.backStackEmpty()) { - pageLoader.loadFromBackStack() + if (pageLoadViewModel.setTab(currentTab) || forceReload) { + if (!pageLoadViewModel.backStackEmpty()) { + pageLoadViewModel.loadFromBackStack() } else { callback()?.onPageLoadMainPageInForegroundTab() } @@ -881,7 +884,6 @@ class PageFragment : Fragment(), BackPressedHandler, CommunicationBridge.Communi fun loadPage(title: PageTitle, entry: HistoryEntry, options: PageLoadOptions = PageLoadOptions()) { val request = PageLoadRequest(title, entry, options) - // pageLoader.loadPage(request) pageLoadViewModel.loadPage(request, webView.scrollY) } @@ -1082,7 +1084,7 @@ class PageFragment : Fragment(), BackPressedHandler, CommunicationBridge.Communi } fun goForward() { - pageLoader.goForward() + pageLoadViewModel.goForward() } fun showBottomSheet(dialog: BottomSheetDialogFragment) { @@ -1359,17 +1361,52 @@ class PageFragment : Fragment(), BackPressedHandler, CommunicationBridge.Communi // UI observers private fun setupObservers() { viewLifecycleOwner.lifecycleScope.launch { - pageLoadViewModel.pageLoadState.collect { state -> - if (state.isTabCreated) { + pageLoadViewModel.animateType.collect { type -> + println("orange --> animate buttons ${type.animateButtons}") + if (type.animateButtons) { requireActivity().invalidateOptionsMenu() + (requireActivity() as PageActivity).animateTabsButton() + } + if (type.sectionAnchor != null) { + scrollToSection(type.sectionAnchor) + } + } + } + + viewLifecycleOwner.lifecycleScope.launch { + pageLoadViewModel.watchResponseState.collect { state -> + println("orange --> watchResponseState") + when (state) { + is UiState.Error -> {} + UiState.Loading -> {} + is UiState.Success -> pageLoadViewModel.updateWatchStatusInModel(state.data) } - if (state.currentPageModel != null) { - model = state.currentPageModel + } + } + + viewLifecycleOwner.lifecycleScope.launch { + pageLoadViewModel.categories.collect { state -> + println("orange --> categories state") + when (state) { + is UiState.Error -> {} + UiState.Loading -> {} + is UiState.Success -> {} } - when (state.uiState) { - is PageLoadUiState.Loading -> handleLoadingState(state.uiState) - is PageLoadUiState.Success -> handleSuccessPageLoadingState(state.uiState) - is PageLoadUiState.SpecialPage -> handleSpecialLoadingPage(state.uiState) + } + } + + viewLifecycleOwner.lifecycleScope.launch { + pageLoadViewModel.currentPageViewModel.collect { + model = it + } + } + + viewLifecycleOwner.lifecycleScope.launch { + pageLoadViewModel.pageLoadState.collect { uiState -> + when (uiState) { + is PageLoadUiState.Loading -> handleLoadingState(uiState) + is PageLoadUiState.Success -> handleSuccessPageLoadingState(uiState) + is PageLoadUiState.SpecialPage -> handleSpecialLoadingPage(uiState) is PageLoadUiState.Error -> {} } } @@ -1395,14 +1432,20 @@ class PageFragment : Fragment(), BackPressedHandler, CommunicationBridge.Communi references = null revision = 0 pageRefreshed = state.isRefresh + + if (AccountUtil.isLoggedIn) { + // explicitly check notifications for the current user + PollNotificationWorker.schedulePollNotificationJob(requireContext()) + } + + EventPlatformClient.AssociationController.beginNewPageView() + + addTimeSpentReading(activeTimer.elapsedSec) + activeTimer.reset() } private fun handleSuccessPageLoadingState(state: PageLoadUiState.Success) { when { - state.loadedFromBackground -> { - (requireActivity() as PageActivity).animateTabsButton() - return - } state.sectionAnchor != null -> { scrollToSection(state.sectionAnchor) return @@ -1410,12 +1453,12 @@ class PageFragment : Fragment(), BackPressedHandler, CommunicationBridge.Communi !state.title.prefixedText.contains(":") -> bridge.resetHtml(state.title) } pageLoadViewModel.updateTabListToPreventZHVariantIssue(model.title) - pageLoadViewModel.saveInformationToDatabase(model, state.result?.pageSummaryResponse?.body()) + pageLoadViewModel.saveInformationToDatabase(model, state.result) scrollTriggerListener.stagedScrollY = state.stagedScrollY updateQuickActionsAndMenuOptions() requireActivity().invalidateOptionsMenu() leadImagesHandler.loadLeadImage() - onPageMetadataLoaded(state.result?.redirectedFrom) + onPageMetadataLoaded(state.redirectedFrom) } private fun handleSpecialLoadingPage(state: PageLoadUiState.SpecialPage) { diff --git a/app/src/main/java/org/wikipedia/page/PageViewModel.kt b/app/src/main/java/org/wikipedia/page/PageViewModel.kt index c945216acc2..53fedeafdb4 100644 --- a/app/src/main/java/org/wikipedia/page/PageViewModel.kt +++ b/app/src/main/java/org/wikipedia/page/PageViewModel.kt @@ -4,20 +4,30 @@ import org.wikipedia.dataclient.okhttp.OkHttpConnectionFactory import org.wikipedia.history.HistoryEntry import org.wikipedia.readinglist.database.ReadingListPage -class PageViewModel { - - var page: Page? = null - var title: PageTitle? = null - var curEntry: HistoryEntry? = null - var readingListPage: ReadingListPage? = null - var hasWatchlistExpiry = false - var isWatched = false - var forceNetwork = false - var isReadMoreLoaded = false +data class PageViewModel( + var page: Page? = null, + var title: PageTitle? = null, + var curEntry: HistoryEntry? = null, + var readingListPage: ReadingListPage? = null, + var hasWatchlistExpiry: Boolean = false, + var isWatched: Boolean = false, + var forceNetwork: Boolean = false, + var isReadMoreLoaded: Boolean = false +) { + // Computed properties remain in the class body val isInReadingList get() = readingListPage != null - val cacheControl get() = if (forceNetwork) OkHttpConnectionFactory.CACHE_CONTROL_FORCE_NETWORK else OkHttpConnectionFactory.CACHE_CONTROL_NONE + + val cacheControl get() = if (forceNetwork) + OkHttpConnectionFactory.CACHE_CONTROL_FORCE_NETWORK + else + OkHttpConnectionFactory.CACHE_CONTROL_NONE + val shouldLoadAsMobileWeb get() = title?.run { namespace() === Namespace.SPECIAL || isMainPage } ?: run { false } || - page?.run { pageProperties.namespace !== Namespace.MAIN && pageProperties.namespace !== Namespace.USER && - pageProperties.namespace !== Namespace.PROJECT && pageProperties.namespace !== Namespace.DRAFT || isMainPage } ?: run { false } + page?.run { + pageProperties.namespace !== Namespace.MAIN && + pageProperties.namespace !== Namespace.USER && + pageProperties.namespace !== Namespace.PROJECT && + pageProperties.namespace !== Namespace.DRAFT || isMainPage + } ?: run { false } } diff --git a/app/src/main/java/org/wikipedia/page/pageload/PageDataFetcher.kt b/app/src/main/java/org/wikipedia/page/pageload/PageDataFetcher.kt index e6ad37856c3..630af213622 100644 --- a/app/src/main/java/org/wikipedia/page/pageload/PageDataFetcher.kt +++ b/app/src/main/java/org/wikipedia/page/pageload/PageDataFetcher.kt @@ -36,7 +36,8 @@ class PageDataFetcher { redirectedFrom = if (pageSummary.raw().priorResponse?.isRedirect == true) title.displayText else null ) } - private suspend fun fetchPageSummary(title: PageTitle, cacheControl: String): Response { + + suspend fun fetchPageSummary(title: PageTitle, cacheControl: String): Response { return ServiceFactory.getRest(title.wikiSite).getSummaryResponse( title = title.prefixedText, cacheControl = cacheControl, @@ -46,7 +47,7 @@ class PageDataFetcher { ) } - private suspend fun fetchWatchStatus(title: PageTitle): WatchStatus { + suspend fun fetchWatchStatus(title: PageTitle): WatchStatus { return if (WikipediaApp.instance.isOnline && AccountUtil.isLoggedIn) { val response = ServiceFactory.get(title.wikiSite).getWatchedStatusWithCategories(title.prefixedText) val page = response.query?.firstPage() @@ -63,7 +64,7 @@ class PageDataFetcher { } } - private suspend fun fetchCategories(title: PageTitle, watchResponse: MwQueryResponse): List { + suspend fun fetchCategories(title: PageTitle, watchResponse: MwQueryResponse): List { return if (WikipediaApp.instance.isOnline) { val response = ServiceFactory.get(title.wikiSite).getCategoriesProps(title.text) (response.query ?: watchResponse.query)?.firstPage()?.categories?.map { category -> @@ -78,9 +79,9 @@ class PageDataFetcher { } data class WatchStatus( - val isWatched: Boolean, - val hasWatchlistExpiry: Boolean, - val myQueryResponse: MwQueryResponse + val isWatched: Boolean = false, + val hasWatchlistExpiry: Boolean = false, + val myQueryResponse: MwQueryResponse = MwQueryResponse() ) sealed class PageResult { diff --git a/app/src/main/java/org/wikipedia/page/pageload/PageLoad.kt b/app/src/main/java/org/wikipedia/page/pageload/PageLoad.kt index 27501165603..49dea3ddeb8 100644 --- a/app/src/main/java/org/wikipedia/page/pageload/PageLoad.kt +++ b/app/src/main/java/org/wikipedia/page/pageload/PageLoad.kt @@ -1,5 +1,6 @@ package org.wikipedia.page.pageload +import org.wikipedia.dataclient.page.PageSummary import org.wikipedia.history.HistoryEntry import org.wikipedia.page.PageActivity import org.wikipedia.page.PageTitle @@ -32,10 +33,10 @@ sealed class PageLoadUiState { data class SpecialPage(val request: PageLoadRequest) : PageLoadUiState() data class Loading(val isRefresh: Boolean = false) : PageLoadUiState() data class Success( - val result: PageResult.Success? = null, + val result: PageSummary? = null, val title: PageTitle, val stagedScrollY: Int = 0, - val loadedFromBackground: Boolean = false, - val sectionAnchor: String? = null) : PageLoadUiState() + val sectionAnchor: String? = null, + val redirectedFrom: String?) : PageLoadUiState() data class Error(val throwable: Throwable) : PageLoadUiState() } diff --git a/app/src/main/java/org/wikipedia/page/pageload/PageLoadViewModel.kt b/app/src/main/java/org/wikipedia/page/pageload/PageLoadViewModel.kt index 295aced7216..c1e222aab66 100644 --- a/app/src/main/java/org/wikipedia/page/pageload/PageLoadViewModel.kt +++ b/app/src/main/java/org/wikipedia/page/pageload/PageLoadViewModel.kt @@ -7,31 +7,47 @@ import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewmodel.initializer import androidx.lifecycle.viewmodel.viewModelFactory import kotlinx.coroutines.CoroutineExceptionHandler +import kotlinx.coroutines.async +import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import org.wikipedia.Constants import org.wikipedia.WikipediaApp import org.wikipedia.analytics.eventplatform.ArticleLinkPreviewInteractionEvent -import org.wikipedia.analytics.metricsplatform.ArticleLinkPreviewInteraction +import org.wikipedia.categories.db.Category import org.wikipedia.database.AppDatabase import org.wikipedia.dataclient.ServiceFactory import org.wikipedia.dataclient.page.PageSummary import org.wikipedia.history.HistoryEntry -import org.wikipedia.page.Namespace import org.wikipedia.page.PageActivity import org.wikipedia.page.PageBackStackItem import org.wikipedia.page.PageTitle import org.wikipedia.page.PageViewModel import org.wikipedia.page.tabs.Tab +import org.wikipedia.util.UiState import org.wikipedia.util.log.L +import retrofit2.Response class PageLoadViewModel(private val app: WikipediaApp) : ViewModel() { - private val _pageLoadState = MutableStateFlow(PageLoadState()) - val pageLoadState: StateFlow = _pageLoadState.asStateFlow() + private val _pageLoadState = MutableStateFlow(PageLoadUiState.Loading()) + val pageLoadState = _pageLoadState.asStateFlow() + + private val _watchResponseState = MutableStateFlow>(UiState.Loading) + val watchResponseState = _watchResponseState.asStateFlow() + + private val _categories = MutableStateFlow>>(UiState.Loading) + val categories = _categories.asStateFlow() + + private val _animateType = MutableSharedFlow(replay = 1) + val animateType = _animateType + + private val _currentPageViewModel = MutableStateFlow(PageViewModel()) + val currentPageViewModel = _currentPageViewModel.asStateFlow() // Internal state private var currentTab: Tab = app.tabList.last() @@ -46,7 +62,14 @@ class PageLoadViewModel(private val app: WikipediaApp) : ViewModel() { when (determineLoadType(request)) { LoadType.CurrentTab -> loadInCurrentTab(request, webScrollY) LoadType.ExistingTab -> loadInExistingTab(request) - LoadType.FromBackStack -> loadPageData(request) + LoadType.FromBackStack -> { + if (request.options.pushbackStack) { + // update the topmost entry in the backstack, before we start overwriting things. + updateCurrentBackStackItem(webScrollY) + currentTab.pushBackStackItem(PageBackStackItem(request.title, request.entry)) + } + loadPageData(request) + } LoadType.NewBackgroundTab -> loadInNewBackgroundTab(request) LoadType.NewForegroundTab -> loadInNewForegroundTab(request, webScrollY) } @@ -69,7 +92,7 @@ class PageLoadViewModel(private val app: WikipediaApp) : ViewModel() { } val item = currentTab.backStack[currentTab.backStackPosition] item.scrollY = scrollY - _pageLoadState.value.currentPageModel?.title?.let { + _currentPageViewModel.value.title?.let { item.title.description = it.description item.title.thumbUrl = it.thumbUrl } @@ -116,25 +139,52 @@ class PageLoadViewModel(private val app: WikipediaApp) : ViewModel() { } } + fun goForward(): Boolean { + if (currentTab.canGoForward()) { + currentTab.moveForward() + loadFromBackStack() + return true + } + return false + } + + fun goBack(): Boolean { + if (currentTab.canGoBack()) { + currentTab.moveBack() + if (!backStackEmpty()) { + loadFromBackStack() + return true + } + } + return false + } + + fun updateWatchStatusInModel(watchStatus: WatchStatus) { + _currentPageViewModel.update { currentModel -> + currentModel.copy( + isWatched = watchStatus.isWatched, + hasWatchlistExpiry = watchStatus.hasWatchlistExpiry + ) + } + } + private suspend fun loadInExistingTab(request: PageLoadRequest) { val selectedTabPosition = selectedTabPosition(request.title) if (selectedTabPosition == -1) { - loadPageData(request) + //loadPageData(request) return } switchToExistingTab(selectedTabPosition) } - private suspend fun loadInCurrentTab(request: PageLoadRequest, webScrollY: Int) { - val model = _pageLoadState.value.currentPageModel + private fun loadInCurrentTab(request: PageLoadRequest, webScrollY: Int) { + val model = _currentPageViewModel.value if (currentTab.backStack.isNotEmpty() && request.title == currentTab.backStack[currentTab.backStackPosition].title) { - if (model?.page == null || request.options.isRefresh) { + if (model.page == null || request.options.isRefresh) { loadFromBackStack() } else if (!request.title.fragment.isNullOrEmpty()) { - _pageLoadState.update { - it.copy(uiState = PageLoadUiState.Success(title = request.title, sectionAnchor = request.title.fragment)) - } + _animateType.tryEmit(Animate(sectionAnchor = request.title.fragment)) } return } @@ -152,25 +202,18 @@ class PageLoadViewModel(private val app: WikipediaApp) : ViewModel() { } private fun loadInNewForegroundTab(request: PageLoadRequest, webScrollY: Int) { - createNewTab(request, isForeground = true) - if (request.options.pushbackStack) { - // update the topmost entry in the backstack, before we start overwriting things. - updateCurrentBackStackItem(webScrollY) - currentTab.pushBackStackItem(PageBackStackItem(request.title, request.entry)) - } + createOrReuseExistingTab(request, isForeground = true) loadFromBackStack() } private fun loadInNewBackgroundTab(request: PageLoadRequest) { val isForeground = app.tabCount == 0 if (isForeground) { - createNewTab(request, isForeground = true) + createOrReuseExistingTab(request, isForeground = true) loadFromBackStack() } else { - createNewTab(request, isForeground = false) - _pageLoadState.update { - it.copy(uiState = PageLoadUiState.Success(title = request.title, loadedFromBackground = true)) - } + createOrReuseExistingTab(request, isForeground = false) + _animateType.tryEmit(Animate(animateButtons = true)) } } @@ -193,30 +236,33 @@ class PageLoadViewModel(private val app: WikipediaApp) : ViewModel() { L.d("Loaded page " + item.title.displayText + " from backstack") } - private suspend fun loadPageData(request: PageLoadRequest, isNewTabCreated: Boolean = false) { - _pageLoadState.update { it.copy(uiState = PageLoadUiState.Loading(request.options.isRefresh)) } - val result = pageDataFetcher.fetchPage( - title = request.title, - entry = request.entry, - forceNetwork = request.options.isRefresh - ) - when (result) { - is PageResult.Success -> { - val pageModel = createPageModel(request, result) - if (request.title.namespace() == Namespace.SPECIAL) { - _pageLoadState.update { it.copy(uiState = PageLoadUiState.SpecialPage(request)) } - return - } - _pageLoadState.update { - it.copy( - uiState = PageLoadUiState.Success(result, request.title, request.options.stagedScrollY), - currentPageModel = pageModel - ) - } - } - is PageResult.Error -> { - handleError(result.throwable) + fun loadPageData(request: PageLoadRequest) { + _pageLoadState.value = PageLoadUiState.Loading() + viewModelScope.launch { + val pageSummary = async { + val cacheControl = if (request.options.isRefresh) "no-cache" else "default" + pageDataFetcher.fetchPageSummary(request.title, cacheControl) + }.await() + pageSummary.body()?.let { value -> + val pageModel = createPageModel(request, pageSummary) + _currentPageViewModel.value = pageModel + _pageLoadState.value = PageLoadUiState.Success( + result = value, + title = request.title, + stagedScrollY = request.options.stagedScrollY, + redirectedFrom = if (pageSummary.raw().priorResponse?.isRedirect == true) request.title.displayText else null + ) } + + val watchStatus = async { + pageDataFetcher.fetchWatchStatus(request.title) + }.await() + _watchResponseState.value = UiState.Success(watchStatus) + + val categories = async { + pageDataFetcher.fetchCategories(request.title, watchStatus.myQueryResponse) + }.await() + _categories.value = UiState.Success(categories) } } @@ -234,13 +280,12 @@ class PageLoadViewModel(private val app: WikipediaApp) : ViewModel() { } } - private fun createNewTab(request: PageLoadRequest, isForeground: Boolean) { + private fun createOrReuseExistingTab(request: PageLoadRequest, isForeground: Boolean) { val existingTabPosition = selectedTabPosition(request.title) if (existingTabPosition >= 0) { switchToExistingTab(existingTabPosition) return } - val shouldCreateNewTab = currentTab.backStack.isNotEmpty() if (shouldCreateNewTab) { val tab = Tab() @@ -256,7 +301,6 @@ class PageLoadViewModel(private val app: WikipediaApp) : ViewModel() { // Load metadata for background tab loadBackgroundTabMetadata(request.title) } - _pageLoadState.update { it.copy(isTabCreated = true) } } else { setTab(currentTab) currentTab.backStack.add(PageBackStackItem(request.title, request.entry)) @@ -287,17 +331,19 @@ class PageLoadViewModel(private val app: WikipediaApp) : ViewModel() { title == it.backStackPositionTitle }?.let { app.tabList.indexOf(it) } ?: -1 } - private suspend fun createPageModel(request: PageLoadRequest, result: PageResult.Success): PageViewModel { + private suspend fun createPageModel(request: PageLoadRequest, response: Response): PageViewModel { return PageViewModel().apply { curEntry = request.entry forceNetwork = request.options.isRefresh readingListPage = AppDatabase.instance.readingListPageDao().findPageInAnyList(request.title) - val response = result.pageSummaryResponse val pageSummary = response.body() page = pageSummary?.toPage(request.title) title = page?.title - isWatched = result.isWatched - hasWatchlistExpiry = result.hasWatchlistExpiry + + // from other api calls, need to update this later in the model +// isWatched = result.isWatched +// hasWatchlistExpiry = result.hasWatchlistExpiry + title?.let { if (!response.raw().request.url.fragment.isNullOrEmpty()) { it.fragment = response.raw().request.url.fragment @@ -315,7 +361,7 @@ class PageLoadViewModel(private val app: WikipediaApp) : ViewModel() { private fun handleError(throwable: Throwable) { L.e(throwable) - _pageLoadState.update { it.copy(uiState = PageLoadUiState.Error(throwable)) } + _pageLoadState.value = PageLoadUiState.Error(throwable) } private fun determineLoadType(request: PageLoadRequest): LoadType { @@ -337,9 +383,8 @@ class PageLoadViewModel(private val app: WikipediaApp) : ViewModel() { } } - data class PageLoadState( - val uiState: PageLoadUiState = PageLoadUiState.Loading(), - val currentPageModel: PageViewModel? = null, - val isTabCreated: Boolean = false + data class Animate( + val animateButtons: Boolean = false, + val sectionAnchor: String? = null ) } From 8085ce349d5109df4cc9e1fb03ea75e9ea787ba7 Mon Sep 17 00:00:00 2001 From: williamrai Date: Fri, 11 Jul 2025 14:15:31 -0400 Subject: [PATCH 09/18] - refactor code fixes step 8(converting to viewModel), separating the api calls - codes fixes --- .../java/org/wikipedia/page/PageFragment.kt | 3 +- .../page/pageload/PageDataFetcher.kt | 2 +- .../org/wikipedia/page/pageload/PageLoad.kt | 1 - .../page/pageload/PageLoadViewModel.kt | 6 +- .../org/wikipedia/page/pageload/PageLoader.kt | 368 ------------------ 5 files changed, 4 insertions(+), 376 deletions(-) delete mode 100644 app/src/main/java/org/wikipedia/page/pageload/PageLoader.kt diff --git a/app/src/main/java/org/wikipedia/page/PageFragment.kt b/app/src/main/java/org/wikipedia/page/PageFragment.kt index 281704de5ff..ef1b6b8c075 100644 --- a/app/src/main/java/org/wikipedia/page/PageFragment.kt +++ b/app/src/main/java/org/wikipedia/page/PageFragment.kt @@ -160,7 +160,6 @@ class PageFragment : Fragment(), BackPressedHandler, CommunicationBridge.Communi } private val pageActionItemCallback = PageActionItemCallback() - private lateinit var bridge: CommunicationBridge private lateinit var leadImagesHandler: LeadImagesHandler private lateinit var bottomBarHideHandler: ViewHideHandler @@ -257,7 +256,7 @@ class PageFragment : Fragment(), BackPressedHandler, CommunicationBridge.Communi } if (shouldLoadFromBackstack(activity) || savedInstanceState != null) { - //reloadFromBackstack() + reloadFromBackstack() } setupObservers() } diff --git a/app/src/main/java/org/wikipedia/page/pageload/PageDataFetcher.kt b/app/src/main/java/org/wikipedia/page/pageload/PageDataFetcher.kt index 630af213622..ed2195708dc 100644 --- a/app/src/main/java/org/wikipedia/page/pageload/PageDataFetcher.kt +++ b/app/src/main/java/org/wikipedia/page/pageload/PageDataFetcher.kt @@ -57,7 +57,7 @@ class PageDataFetcher { myQueryResponse = response ) } else if (WikipediaApp.instance.isOnline && !AccountUtil.isLoggedIn) { - val response = AnonymousNotificationHelper.observableForAnonUserInfo(title.wikiSite) + val response = AnonymousNotificationHelper.maybeGetAnonUserInfo(title.wikiSite) WatchStatus(false, false, response) } else { WatchStatus(false, false, MwQueryResponse()) diff --git a/app/src/main/java/org/wikipedia/page/pageload/PageLoad.kt b/app/src/main/java/org/wikipedia/page/pageload/PageLoad.kt index 49dea3ddeb8..5b27713e7a4 100644 --- a/app/src/main/java/org/wikipedia/page/pageload/PageLoad.kt +++ b/app/src/main/java/org/wikipedia/page/pageload/PageLoad.kt @@ -4,7 +4,6 @@ import org.wikipedia.dataclient.page.PageSummary import org.wikipedia.history.HistoryEntry import org.wikipedia.page.PageActivity import org.wikipedia.page.PageTitle -import org.wikipedia.page.PageViewModel data class PageLoadRequest( val title: PageTitle, diff --git a/app/src/main/java/org/wikipedia/page/pageload/PageLoadViewModel.kt b/app/src/main/java/org/wikipedia/page/pageload/PageLoadViewModel.kt index c1e222aab66..f280c62c8f9 100644 --- a/app/src/main/java/org/wikipedia/page/pageload/PageLoadViewModel.kt +++ b/app/src/main/java/org/wikipedia/page/pageload/PageLoadViewModel.kt @@ -10,8 +10,6 @@ import kotlinx.coroutines.CoroutineExceptionHandler import kotlinx.coroutines.async import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch @@ -171,7 +169,7 @@ class PageLoadViewModel(private val app: WikipediaApp) : ViewModel() { private suspend fun loadInExistingTab(request: PageLoadRequest) { val selectedTabPosition = selectedTabPosition(request.title) if (selectedTabPosition == -1) { - //loadPageData(request) + loadPageData(request) return } switchToExistingTab(selectedTabPosition) @@ -237,7 +235,7 @@ class PageLoadViewModel(private val app: WikipediaApp) : ViewModel() { } fun loadPageData(request: PageLoadRequest) { - _pageLoadState.value = PageLoadUiState.Loading() + _pageLoadState.value = PageLoadUiState.Loading() viewModelScope.launch { val pageSummary = async { val cacheControl = if (request.options.isRefresh) "no-cache" else "default" diff --git a/app/src/main/java/org/wikipedia/page/pageload/PageLoader.kt b/app/src/main/java/org/wikipedia/page/pageload/PageLoader.kt deleted file mode 100644 index 3bd7702ff54..00000000000 --- a/app/src/main/java/org/wikipedia/page/pageload/PageLoader.kt +++ /dev/null @@ -1,368 +0,0 @@ -package org.wikipedia.page.pageload - -import android.view.View -import androidx.lifecycle.lifecycleScope -import kotlinx.coroutines.CoroutineExceptionHandler -import kotlinx.coroutines.launch -import org.wikipedia.Constants -import org.wikipedia.WikipediaApp -import org.wikipedia.bridge.CommunicationBridge -import org.wikipedia.bridge.JavaScriptActionHandler -import org.wikipedia.dataclient.ServiceFactory -import org.wikipedia.page.Namespace -import org.wikipedia.page.PageActivity -import org.wikipedia.page.PageBackStackItem -import org.wikipedia.page.PageFragment -import org.wikipedia.page.PageTitle -import org.wikipedia.page.leadimages.LeadImagesHandler -import org.wikipedia.page.tabs.Tab -import org.wikipedia.util.log.L -import org.wikipedia.views.ObservableWebView - -class PageLoader( - private val fragment: PageFragment, - private val webView: ObservableWebView, - private val bridge: CommunicationBridge, - private val leadImagesHandler: LeadImagesHandler, - private var currentTab: Tab -) { - private val dataFetcher = PageDataFetcher() - private val app = WikipediaApp.instance - - fun loadPage(request: PageLoadRequest) { - when (determineLoadType(request)) { - is LoadType.CurrentTab -> loadInCurrentTab(request) - is LoadType.NewForegroundTab -> loadInNewForegroundTab(request) - is LoadType.NewBackgroundTab -> loadInNewBackgroundTab(request) - is LoadType.ExistingTab -> loadInExistingTab(request) - is LoadType.FromBackStack -> loadWithScrollPosition(request) - } - } - - private fun loadInCurrentTab(request: PageLoadRequest) { - // is the new title the same as what's already being displayed? - if (fragment.currentTab.backStack.isNotEmpty() && - request.title == fragment.currentTab.backStack[fragment.currentTab.backStackPosition].title) { - if (fragment.model.page == null || request.options.isRefresh) { - loadFromBackStack() - } else if (!request.title.fragment.isNullOrEmpty()) { - fragment.scrollToSection(request.title.fragment!!) - } - return - } - - prepareForLoad(request) - executeLoad(request) - } - - fun backStackEmpty(): Boolean { - return currentTab.backStack.isEmpty() - } - - fun goBack(): Boolean { - if (currentTab.canGoBack()) { - currentTab.moveBack() - if (!backStackEmpty()) { - loadFromBackStack() - return true - } - } - return false - } - - fun goForward(): Boolean { - if (currentTab.canGoForward()) { - currentTab.moveForward() - loadFromBackStack() - return true - } - return false - } - - private fun loadInNewForegroundTab(request: PageLoadRequest) { - prepareForLoad(request) - createNewTab(request, isForeground = true) - executeLoad(request) - } - - private fun loadInNewBackgroundTab(request: PageLoadRequest) { - val isForeground = app.tabCount == 0 - if (isForeground) { - createNewTab(request, isForeground = true) - loadFromBackStack() - } else { - createNewTab(request, isForeground = false) - (fragment.requireActivity() as PageActivity).animateTabsButton() - } - } - - private fun loadInExistingTab(request: PageLoadRequest) { - val existingTabPosition = selectedTabPosition(request.title) - if (existingTabPosition >= 0) { - switchToExistingTab(existingTabPosition) - } else { - loadInCurrentTab(request) - } - } - - private fun loadWithScrollPosition(request: PageLoadRequest) { - prepareForLoad(request) - executeLoad(request) - } - - private fun createNewTab(request: PageLoadRequest, isForeground: Boolean) { - val selectedTabPosition = selectedTabPosition(request.title) - if (selectedTabPosition >= 0) { - switchToExistingTab(selectedTabPosition) - return - } - - if (fragment.shouldCreateNewTab) { - val tab = Tab() - val position = if (isForeground) fragment.foregroundTabPosition else fragment.backgroundTabPosition - - if (isForeground) { - setTab(tab) - } - - app.tabList.add(position, tab) - trimTabCount() - - tab.backStack.add(PageBackStackItem(request.title, request.entry)) - if (!isForeground) { - // Load metadata for background tab - loadBackgroundTabMetadata(request.title) - } - fragment.requireActivity().invalidateOptionsMenu() - } else { - setTab(fragment.currentTab) - fragment.currentTab.backStack.add(PageBackStackItem(request.title, request.entry)) - } - } - - fun setTab(tab: Tab): Boolean { - val isDifferent = tab != currentTab - currentTab = tab - return isDifferent - } - - private fun loadBackgroundTabMetadata(title: PageTitle) { - fragment.lifecycleScope.launch(CoroutineExceptionHandler { _, t -> L.e(t) }) { - ServiceFactory.get(title.wikiSite) - .getInfoByPageIdsOrTitles(null, title.prefixedText) - .query?.firstPage()?.let { page -> - app.tabList.find { it.backStackPositionTitle == title } - ?.backStackPositionTitle?.apply { - thumbUrl = page.thumbUrl() - description = page.description - } - } - } - } - - private fun trimTabCount() { - while (app.tabList.size > Constants.MAX_TABS) { - app.tabList.removeAt(0) - } - } - - private fun switchToExistingTab(position: Int) { - if (position < app.tabList.size - 1) { - val tab = app.tabList.removeAt(position) - app.tabList.add(tab) - setTab(tab) - } - - if (app.tabCount > 0) { - app.tabList.last().squashBackstack() - loadFromBackStack() - } - } - - fun loadFromBackStack() { - if (currentTab.backStack.isEmpty()) { - return - } - val item = currentTab.backStack[currentTab.backStackPosition] - // display the page based on the backstack item, stage the scrollY position based on - // the backstack item. - loadPage(request = PageLoadRequest( - title = item.title, - entry = item.historyEntry, - options = PageLoadOptions( - pushbackStack = false, - stagedScrollY = item.scrollY, - shouldLoadFromBackStack = true - )) - ) - L.d("Loaded page " + item.title.displayText + " from backstack") - } - - private fun prepareForLoad(request: PageLoadRequest) { - // added to fragment - fragment.clearActivityActionBarTitle() - fragment.dismissBottomSheet() - - // done added to load function - if (request.options.squashBackStack) { - if (app.tabCount > 0) { - app.tabList.last().clearBackstack() - } - } - - // added to fragment - fragment.updateProgressBar(true) - fragment.sidePanelHandler.setEnabled(false) - fragment.callback()?.onPageSetToolbarElevationEnabled(false) - - // done - // Update model - fragment.model.title = request.title - fragment.model.curEntry = request.entry - fragment.model.page = null - fragment.model.readingListPage = null - fragment.model.forceNetwork = request.options.isRefresh - - // done - // Clear previous state - fragment.errorState = false - fragment.binding.pageError.visibility = View.GONE - fragment.webView.visibility = View.VISIBLE - fragment.binding.pageActionsTabLayout.visibility = View.VISIBLE - fragment.binding.pageActionsTabLayout.enableAllTabs() - - // done - // Reset references and other state - fragment.references = null - fragment.revision = 0 - fragment.pageRefreshed = request.options.isRefresh - } - - private fun executeLoad(request: PageLoadRequest) { - // added to load function - if (request.options.pushbackStack) { - updateCurrentBackStackItem() - currentTab.pushBackStackItem(PageBackStackItem(request.title, request.entry)) - } - - // added to on success state because this check should run before - if (request.title.namespace() == Namespace.SPECIAL) { - handleSpecialPage(request) - return - } - - // done - fragment.lifecycleScope.launch(CoroutineExceptionHandler { _, throwable -> - L.e("Page details network error: ", throwable) - handleLoadError(throwable) - }) { - updateLoadingState(PageLoadUiState.Loading()) - val result = dataFetcher.fetchPage( - request.title, - request.entry, - request.options.isRefresh - ) - when (result) { - is PageResult.Success -> { - // updateLoadingState(LoadState.Success()) - handleLoadSuccess(result, request) - } - is PageResult.Error -> { - updateLoadingState(PageLoadUiState.Error(result.throwable)) - handleLoadError(result.throwable) - } - } - } - } - - fun updateCurrentBackStackItem() { - if (currentTab.backStack.isEmpty()) { - return - } - val item = currentTab.backStack[currentTab.backStackPosition] - item.scrollY = webView.scrollY - fragment.model.title?.let { - item.title.description = it.description - item.title.thumbUrl = it.thumbUrl - } - } - - fun handleSpecialPage(request: PageLoadRequest) { - bridge.resetHtml(request.title) - leadImagesHandler.loadLeadImage() - fragment.requireActivity().invalidateOptionsMenu() - fragment.onPageMetadataLoaded() - } - - private fun handleLoadSuccess(result: PageResult.Success, request: PageLoadRequest) { - // done in createPageModel function - // data update - val response = result.pageSummaryResponse - val pageSummary = response.body() - val page = pageSummary?.toPage(fragment.model.title) - fragment.model.page = page - fragment.model.isWatched = result.isWatched - fragment.model.hasWatchlistExpiry = result.hasWatchlistExpiry - fragment.model.title = page?.title - - // in sucess state on fragment - // ui update - if (!request.title.prefixedText.contains(":")) { - bridge.resetHtml(request.title) - } - - // in sucess state on fragment - // ui update - // @TODO: scrollY is received from the item when loading back from stack done - if (request.options.stagedScrollY > 0) { - fragment.scrollTriggerListener.stagedScrollY = request.options.stagedScrollY - } - - // in sucess state on fragment - // ui update - fragment.updateQuickActionsAndMenuOptions() - fragment.requireActivity().invalidateOptionsMenu() - - // in sucess state on fragment - // ui update - leadImagesHandler.loadLeadImage() - fragment.onPageMetadataLoaded(result.redirectedFrom) - } - - private fun handleLoadError(error: Throwable) { - if (!fragment.isAdded) { - return - } - fragment.requireActivity().invalidateOptionsMenu() - fragment.onPageLoadError(error) - } - - private fun updateLoadingState(state: PageLoadUiState) { - when (state) { - is PageLoadUiState.Loading -> fragment.updateProgressBar(true) - is PageLoadUiState.Success -> fragment.updateProgressBar(false) - is PageLoadUiState.Error -> fragment.updateProgressBar(false) - else -> {} - } - } - - private fun determineLoadType(request: PageLoadRequest): LoadType { - return when { - request.options.shouldLoadFromBackStack -> LoadType.FromBackStack - request.options.tabPosition == PageActivity.TabPosition.NEW_TAB_FOREGROUND -> LoadType.NewForegroundTab - request.options.tabPosition == PageActivity.TabPosition.NEW_TAB_BACKGROUND -> LoadType.NewBackgroundTab - request.options.tabPosition == PageActivity.TabPosition.EXISTING_TAB -> LoadType.ExistingTab - else -> LoadType.CurrentTab - } - } - - private fun selectedTabPosition(title: PageTitle): Int { - return app.tabList.firstOrNull { it.backStackPositionTitle != null && - title == it.backStackPositionTitle }?.let { app.tabList.indexOf(it) } ?: -1 - } - - fun onConfigurationChanged() { - leadImagesHandler.loadLeadImage() - bridge.execute(JavaScriptActionHandler.setTopMargin(leadImagesHandler.topMargin)) - } -} From ebb4871343f58499868864ec2a60878468e5c10e Mon Sep 17 00:00:00 2001 From: williamrai Date: Fri, 11 Jul 2025 17:15:07 -0400 Subject: [PATCH 10/18] - refactor code fixes step 9(converting to viewModel), separating the api calls - codes fixes - adds categories db code, fixes refresh calls and code for sending events --- .../java/org/wikipedia/page/PageActivity.kt | 6 +-- .../java/org/wikipedia/page/PageFragment.kt | 32 +++++++++---- .../org/wikipedia/page/pageload/PageLoad.kt | 4 +- .../page/pageload/PageLoadViewModel.kt | 46 +++++++++++-------- 4 files changed, 55 insertions(+), 33 deletions(-) diff --git a/app/src/main/java/org/wikipedia/page/PageActivity.kt b/app/src/main/java/org/wikipedia/page/PageActivity.kt index 46f54247c8b..bcfd89ddd80 100644 --- a/app/src/main/java/org/wikipedia/page/PageActivity.kt +++ b/app/src/main/java/org/wikipedia/page/PageActivity.kt @@ -120,7 +120,7 @@ class PageActivity : BaseActivity(), PageFragment.Callback, LinkPreviewDialog.Lo // and reload the page... pageFragment.model.title?.let { title -> pageFragment.model.curEntry?.let { entry -> -// pageFragment.loadPage(title, entry, pushBackStack = false, squashBackstack = false, isRefresh = true) + pageFragment.loadPage(title, entry, options = PageLoadOptions(pushBackStack = false, squashBackStack = false, isRefresh = true)) } } } @@ -618,8 +618,8 @@ class PageActivity : BaseActivity(), PageFragment.Callback, LinkPreviewDialog.Lo TabPosition.CURRENT_TAB -> PageLoadOptions(tabPosition = position) TabPosition.CURRENT_TAB_SQUASH -> PageLoadOptions(tabPosition = position, squashBackStack = true) TabPosition.NEW_TAB_BACKGROUND -> PageLoadOptions(tabPosition = position) - TabPosition.NEW_TAB_FOREGROUND -> PageLoadOptions(tabPosition = position, pushbackStack = false) - TabPosition.EXISTING_TAB -> PageLoadOptions(tabPosition = position, pushbackStack = true, squashBackStack = true) + TabPosition.NEW_TAB_FOREGROUND -> PageLoadOptions(tabPosition = position, pushBackStack = false) + TabPosition.EXISTING_TAB -> PageLoadOptions(tabPosition = position, pushBackStack = true, squashBackStack = true) } pageFragment.loadPage(pageTitle, entry, options) } diff --git a/app/src/main/java/org/wikipedia/page/PageFragment.kt b/app/src/main/java/org/wikipedia/page/PageFragment.kt index ef1b6b8c075..34cd2659ba6 100644 --- a/app/src/main/java/org/wikipedia/page/PageFragment.kt +++ b/app/src/main/java/org/wikipedia/page/PageFragment.kt @@ -50,11 +50,13 @@ import org.wikipedia.WikipediaApp import org.wikipedia.activity.FragmentUtil.getCallback import org.wikipedia.analytics.eventplatform.ArticleFindInPageInteractionEvent import org.wikipedia.analytics.eventplatform.ArticleInteractionEvent +import org.wikipedia.analytics.eventplatform.ArticleLinkPreviewInteractionEvent import org.wikipedia.analytics.eventplatform.DonorExperienceEvent import org.wikipedia.analytics.eventplatform.EventPlatformClient import org.wikipedia.analytics.eventplatform.PlacesEvent import org.wikipedia.analytics.eventplatform.WatchlistAnalyticsHelper import org.wikipedia.analytics.metricsplatform.ArticleFindInPageInteraction +import org.wikipedia.analytics.metricsplatform.ArticleLinkPreviewInteraction import org.wikipedia.analytics.metricsplatform.ArticleToolbarInteraction import org.wikipedia.auth.AccountUtil import org.wikipedia.bridge.CommunicationBridge @@ -1043,9 +1045,9 @@ class PageFragment : Fragment(), BackPressedHandler, CommunicationBridge.Communi binding.pageActionsTabLayout.enableAllTabs() errorState = false model.curEntry = HistoryEntry(title, HistoryEntry.SOURCE_HISTORY) - // loadPage(title, entry, false, stagedScrollY, app.isOnline) + loadPage(title, entry, options = PageLoadOptions(pushBackStack = false, stagedScrollY = stagedScrollY, isRefresh = app.isOnline)) loadPage(title, entry, PageLoadOptions( - pushbackStack = false, + pushBackStack = false, isRefresh = app.isOnline, stagedScrollY = stagedScrollY )) @@ -1392,7 +1394,9 @@ class PageFragment : Fragment(), BackPressedHandler, CommunicationBridge.Communi when (state) { is UiState.Error -> {} UiState.Loading -> {} - is UiState.Success -> {} + is UiState.Success -> { + pageLoadViewModel.saveCategories(state.data) + } } } } @@ -1404,9 +1408,9 @@ class PageFragment : Fragment(), BackPressedHandler, CommunicationBridge.Communi } viewLifecycleOwner.lifecycleScope.launch { - pageLoadViewModel.pageLoadState.collect { uiState -> + pageLoadViewModel.pageLoadUiState.collect { uiState -> when (uiState) { - is PageLoadUiState.Loading -> handleLoadingState(uiState) + is PageLoadUiState.LoadingPrep -> handleLoadingState(uiState) is PageLoadUiState.Success -> handleSuccessPageLoadingState(uiState) is PageLoadUiState.SpecialPage -> handleSpecialLoadingPage(uiState) is PageLoadUiState.Error -> {} @@ -1415,7 +1419,7 @@ class PageFragment : Fragment(), BackPressedHandler, CommunicationBridge.Communi } } - private fun handleLoadingState(state: PageLoadUiState.Loading) { + private fun handleLoadingState(state: PageLoadUiState.LoadingPrep) { clearActivityActionBarTitle() dismissBottomSheet() @@ -1444,6 +1448,9 @@ class PageFragment : Fragment(), BackPressedHandler, CommunicationBridge.Communi addTimeSpentReading(activeTimer.elapsedSec) activeTimer.reset() + + updateQuickActionsAndMenuOptions() + requireActivity().invalidateOptionsMenu() } private fun handleSuccessPageLoadingState(state: PageLoadUiState.Success) { @@ -1454,11 +1461,18 @@ class PageFragment : Fragment(), BackPressedHandler, CommunicationBridge.Communi } !state.title.prefixedText.contains(":") -> bridge.resetHtml(state.title) } + updateProgressBar(false) pageLoadViewModel.updateTabListToPreventZHVariantIssue(model.title) - pageLoadViewModel.saveInformationToDatabase(model, state.result) + + if (model.title != null && state.result != null) { + pageLoadViewModel.saveInformationToDatabase(model, state.result, sendEvent = { entry -> + WikipediaApp.instance.appSessionEvent.pageViewed(entry) + ArticleLinkPreviewInteractionEvent(model.title!!.wikiSite.dbName(), state.result.pageId, entry.source).logNavigate() + ArticleLinkPreviewInteraction(this, entry.source).logNavigate() + }) + } + scrollTriggerListener.stagedScrollY = state.stagedScrollY - updateQuickActionsAndMenuOptions() - requireActivity().invalidateOptionsMenu() leadImagesHandler.loadLeadImage() onPageMetadataLoaded(state.redirectedFrom) } diff --git a/app/src/main/java/org/wikipedia/page/pageload/PageLoad.kt b/app/src/main/java/org/wikipedia/page/pageload/PageLoad.kt index 5b27713e7a4..9b9019c81d7 100644 --- a/app/src/main/java/org/wikipedia/page/pageload/PageLoad.kt +++ b/app/src/main/java/org/wikipedia/page/pageload/PageLoad.kt @@ -12,7 +12,7 @@ data class PageLoadRequest( ) data class PageLoadOptions( - val pushbackStack: Boolean = true, + val pushBackStack: Boolean = true, val squashBackStack: Boolean = false, val isRefresh: Boolean = false, val stagedScrollY: Int = 0, @@ -30,7 +30,7 @@ sealed class LoadType { sealed class PageLoadUiState { data class SpecialPage(val request: PageLoadRequest) : PageLoadUiState() - data class Loading(val isRefresh: Boolean = false) : PageLoadUiState() + data class LoadingPrep(val isRefresh: Boolean = false, val title: PageTitle? = null) : PageLoadUiState() data class Success( val result: PageSummary? = null, val title: PageTitle, diff --git a/app/src/main/java/org/wikipedia/page/pageload/PageLoadViewModel.kt b/app/src/main/java/org/wikipedia/page/pageload/PageLoadViewModel.kt index f280c62c8f9..0f95eae2107 100644 --- a/app/src/main/java/org/wikipedia/page/pageload/PageLoadViewModel.kt +++ b/app/src/main/java/org/wikipedia/page/pageload/PageLoadViewModel.kt @@ -15,12 +15,12 @@ import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import org.wikipedia.Constants import org.wikipedia.WikipediaApp -import org.wikipedia.analytics.eventplatform.ArticleLinkPreviewInteractionEvent import org.wikipedia.categories.db.Category import org.wikipedia.database.AppDatabase import org.wikipedia.dataclient.ServiceFactory import org.wikipedia.dataclient.page.PageSummary import org.wikipedia.history.HistoryEntry +import org.wikipedia.page.Namespace import org.wikipedia.page.PageActivity import org.wikipedia.page.PageBackStackItem import org.wikipedia.page.PageTitle @@ -32,8 +32,8 @@ import retrofit2.Response class PageLoadViewModel(private val app: WikipediaApp) : ViewModel() { - private val _pageLoadState = MutableStateFlow(PageLoadUiState.Loading()) - val pageLoadState = _pageLoadState.asStateFlow() + private val _pageLoadUiState = MutableStateFlow(PageLoadUiState.LoadingPrep()) + val pageLoadUiState = _pageLoadUiState.asStateFlow() private val _watchResponseState = MutableStateFlow>(UiState.Loading) val watchResponseState = _watchResponseState.asStateFlow() @@ -61,7 +61,7 @@ class PageLoadViewModel(private val app: WikipediaApp) : ViewModel() { LoadType.CurrentTab -> loadInCurrentTab(request, webScrollY) LoadType.ExistingTab -> loadInExistingTab(request) LoadType.FromBackStack -> { - if (request.options.pushbackStack) { + if (request.options.pushBackStack) { // update the topmost entry in the backstack, before we start overwriting things. updateCurrentBackStackItem(webScrollY) currentTab.pushBackStackItem(PageBackStackItem(request.title, request.entry)) @@ -101,12 +101,20 @@ class PageLoadViewModel(private val app: WikipediaApp) : ViewModel() { WikipediaApp.instance.tabList.getOrNull(WikipediaApp.instance.tabCount - 1)?.setBackStackPositionTitle(title) } + fun saveCategories(categories: List) { + viewModelScope.launch { + if (categories.isNotEmpty()) { + AppDatabase.instance.categoryDao().upsertAll(categories) + } + } + } + fun saveInformationToDatabase( - currentPageModel: PageViewModel?, - pageSummary: PageSummary? + pageModel: PageViewModel, + pageSummary: PageSummary, + sendEvent: (HistoryEntry) -> Unit ) { - val pageModel = currentPageModel ?: return - val title = currentPageModel.title ?: return + val title = pageModel.title ?: return viewModelScope.launch { pageModel.curEntry?.let { @@ -125,14 +133,10 @@ class PageLoadViewModel(private val app: WikipediaApp) : ViewModel() { } // Update metadata in the DB - AppDatabase.instance.pageImagesDao().upsertForMetadata(entry, title.thumbUrl, title.description, pageSummary?.coordinates?.latitude, pageSummary?.coordinates?.longitude) + AppDatabase.instance.pageImagesDao().upsertForMetadata(entry, title.thumbUrl, title.description, pageSummary.coordinates?.latitude, pageSummary.coordinates?.longitude) // And finally, count this as a page view. - WikipediaApp.instance.appSessionEvent.pageViewed(entry) - ArticleLinkPreviewInteractionEvent(title.wikiSite.dbName(), pageSummary?.pageId ?: 0, entry.source).logNavigate() - - // @TODO: requires fragment - // ArticleLinkPreviewInteraction(fragment, entry.source).logNavigate() + sendEvent(entry) } } } @@ -191,7 +195,7 @@ class PageLoadViewModel(private val app: WikipediaApp) : ViewModel() { app.tabList.last().clearBackstack() } } - if (request.options.pushbackStack) { + if (request.options.pushBackStack) { // update the topmost entry in the backstack, before we start overwriting things. updateCurrentBackStackItem(webScrollY) currentTab.pushBackStackItem(PageBackStackItem(request.title, request.entry)) @@ -226,7 +230,7 @@ class PageLoadViewModel(private val app: WikipediaApp) : ViewModel() { title = item.title, entry = item.historyEntry, options = PageLoadOptions( - pushbackStack = false, + pushBackStack = false, stagedScrollY = item.scrollY, shouldLoadFromBackStack = true, )) @@ -235,7 +239,11 @@ class PageLoadViewModel(private val app: WikipediaApp) : ViewModel() { } fun loadPageData(request: PageLoadRequest) { - _pageLoadState.value = PageLoadUiState.Loading() + _pageLoadUiState.value = PageLoadUiState.LoadingPrep(isRefresh = request.options.isRefresh, title = request.title) + if (request.title.namespace() == Namespace.SPECIAL) { + _pageLoadUiState.value = PageLoadUiState.SpecialPage(request) + return + } viewModelScope.launch { val pageSummary = async { val cacheControl = if (request.options.isRefresh) "no-cache" else "default" @@ -244,7 +252,7 @@ class PageLoadViewModel(private val app: WikipediaApp) : ViewModel() { pageSummary.body()?.let { value -> val pageModel = createPageModel(request, pageSummary) _currentPageViewModel.value = pageModel - _pageLoadState.value = PageLoadUiState.Success( + _pageLoadUiState.value = PageLoadUiState.Success( result = value, title = request.title, stagedScrollY = request.options.stagedScrollY, @@ -359,7 +367,7 @@ class PageLoadViewModel(private val app: WikipediaApp) : ViewModel() { private fun handleError(throwable: Throwable) { L.e(throwable) - _pageLoadState.value = PageLoadUiState.Error(throwable) + _pageLoadUiState.value = PageLoadUiState.Error(throwable) } private fun determineLoadType(request: PageLoadRequest): LoadType { From 8c6298cca6306f3827eb3119e3bee49b8bc34a7b Mon Sep 17 00:00:00 2001 From: williamrai Date: Mon, 14 Jul 2025 17:16:04 -0400 Subject: [PATCH 11/18] - code fixes --- .../java/org/wikipedia/page/PageFragment.kt | 27 +++++---- .../page/pageload/PageLoadViewModel.kt | 56 +++++++++---------- 2 files changed, 41 insertions(+), 42 deletions(-) diff --git a/app/src/main/java/org/wikipedia/page/PageFragment.kt b/app/src/main/java/org/wikipedia/page/PageFragment.kt index 34cd2659ba6..36f3470920f 100644 --- a/app/src/main/java/org/wikipedia/page/PageFragment.kt +++ b/app/src/main/java/org/wikipedia/page/PageFragment.kt @@ -41,6 +41,7 @@ import kotlinx.serialization.json.float import kotlinx.serialization.json.jsonArray import kotlinx.serialization.json.jsonObject import kotlinx.serialization.json.jsonPrimitive +import okio.IOException import org.wikipedia.BackPressedHandler import org.wikipedia.Constants import org.wikipedia.Constants.InvokeSource @@ -189,9 +190,6 @@ class PageFragment : Fragment(), BackPressedHandler, CommunicationBridge.Communi lateinit var editHandler: EditHandler var revision = 0L - private val shouldCreateNewTab get() = currentTab.backStack.isNotEmpty() - private val backgroundTabPosition get() = 0.coerceAtLeast(foregroundTabPosition - 1) - private val foregroundTabPosition get() = app.tabList.size private val tabLayoutOffsetParams get() = LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, binding.pageActionsTabLayout.height) val currentTab get() = app.tabList.last() val title get() = model.title @@ -1366,7 +1364,6 @@ class PageFragment : Fragment(), BackPressedHandler, CommunicationBridge.Communi private fun setupObservers() { viewLifecycleOwner.lifecycleScope.launch { pageLoadViewModel.animateType.collect { type -> - println("orange --> animate buttons ${type.animateButtons}") if (type.animateButtons) { requireActivity().invalidateOptionsMenu() (requireActivity() as PageActivity).animateTabsButton() @@ -1379,24 +1376,32 @@ class PageFragment : Fragment(), BackPressedHandler, CommunicationBridge.Communi viewLifecycleOwner.lifecycleScope.launch { pageLoadViewModel.watchResponseState.collect { state -> - println("orange --> watchResponseState") when (state) { - is UiState.Error -> {} - UiState.Loading -> {} + is UiState.Error -> { + if (state.error !is IOException) { + L.w("Ignoring network error while fetching watched status.") + onPageLoadError(state.error) + } + } is UiState.Success -> pageLoadViewModel.updateWatchStatusInModel(state.data) + UiState.Loading -> updateProgressBar(true) } } } viewLifecycleOwner.lifecycleScope.launch { pageLoadViewModel.categories.collect { state -> - println("orange --> categories state") when (state) { - is UiState.Error -> {} - UiState.Loading -> {} + is UiState.Error -> { + if (state.error !is IOException) { + L.w("Ignoring network error while fetching categories.") + onPageLoadError(state.error) + } + } is UiState.Success -> { pageLoadViewModel.saveCategories(state.data) } + UiState.Loading -> updateProgressBar(true) } } } @@ -1413,7 +1418,7 @@ class PageFragment : Fragment(), BackPressedHandler, CommunicationBridge.Communi is PageLoadUiState.LoadingPrep -> handleLoadingState(uiState) is PageLoadUiState.Success -> handleSuccessPageLoadingState(uiState) is PageLoadUiState.SpecialPage -> handleSpecialLoadingPage(uiState) - is PageLoadUiState.Error -> {} + is PageLoadUiState.Error -> onPageLoadError(uiState.throwable) } } } diff --git a/app/src/main/java/org/wikipedia/page/pageload/PageLoadViewModel.kt b/app/src/main/java/org/wikipedia/page/pageload/PageLoadViewModel.kt index 0f95eae2107..7fd8f736153 100644 --- a/app/src/main/java/org/wikipedia/page/pageload/PageLoadViewModel.kt +++ b/app/src/main/java/org/wikipedia/page/pageload/PageLoadViewModel.kt @@ -7,7 +7,6 @@ import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewmodel.initializer import androidx.lifecycle.viewmodel.viewModelFactory import kotlinx.coroutines.CoroutineExceptionHandler -import kotlinx.coroutines.async import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow @@ -54,23 +53,20 @@ class PageLoadViewModel(private val app: WikipediaApp) : ViewModel() { val backgroundTabPosition get() = 0.coerceAtLeast(foregroundTabPosition - 1) fun loadPage(request: PageLoadRequest, webScrollY: Int = 0) { - viewModelScope.launch(CoroutineExceptionHandler { _, throwable -> - handleError(throwable) - }) { - when (determineLoadType(request)) { - LoadType.CurrentTab -> loadInCurrentTab(request, webScrollY) - LoadType.ExistingTab -> loadInExistingTab(request) - LoadType.FromBackStack -> { - if (request.options.pushBackStack) { - // update the topmost entry in the backstack, before we start overwriting things. - updateCurrentBackStackItem(webScrollY) - currentTab.pushBackStackItem(PageBackStackItem(request.title, request.entry)) - } - loadPageData(request) + val loadType = determineLoadType(request) + when (loadType) { + LoadType.CurrentTab -> loadInCurrentTab(request, webScrollY) + LoadType.ExistingTab -> loadInExistingTab(request) + LoadType.FromBackStack -> { + if (request.options.pushBackStack) { + // update the topmost entry in the backstack, before we start overwriting things. + updateCurrentBackStackItem(webScrollY) + currentTab.pushBackStackItem(PageBackStackItem(request.title, request.entry)) } - LoadType.NewBackgroundTab -> loadInNewBackgroundTab(request) - LoadType.NewForegroundTab -> loadInNewForegroundTab(request, webScrollY) + loadPageData(request) } + LoadType.NewBackgroundTab -> loadInNewBackgroundTab(request) + LoadType.NewForegroundTab -> loadInNewForegroundTab(request, webScrollY) } } @@ -170,7 +166,7 @@ class PageLoadViewModel(private val app: WikipediaApp) : ViewModel() { } } - private suspend fun loadInExistingTab(request: PageLoadRequest) { + private fun loadInExistingTab(request: PageLoadRequest) { val selectedTabPosition = selectedTabPosition(request.title) if (selectedTabPosition == -1) { loadPageData(request) @@ -244,11 +240,11 @@ class PageLoadViewModel(private val app: WikipediaApp) : ViewModel() { _pageLoadUiState.value = PageLoadUiState.SpecialPage(request) return } - viewModelScope.launch { - val pageSummary = async { - val cacheControl = if (request.options.isRefresh) "no-cache" else "default" - pageDataFetcher.fetchPageSummary(request.title, cacheControl) - }.await() + viewModelScope.launch(CoroutineExceptionHandler { _, throwable -> + handleError(throwable) + }) { + val cacheControl = if (request.options.isRefresh) "no-cache" else "default" + val pageSummary = pageDataFetcher.fetchPageSummary(request.title, cacheControl) pageSummary.body()?.let { value -> val pageModel = createPageModel(request, pageSummary) _currentPageViewModel.value = pageModel @@ -259,16 +255,14 @@ class PageLoadViewModel(private val app: WikipediaApp) : ViewModel() { redirectedFrom = if (pageSummary.raw().priorResponse?.isRedirect == true) request.title.displayText else null ) } - - val watchStatus = async { - pageDataFetcher.fetchWatchStatus(request.title) - }.await() - _watchResponseState.value = UiState.Success(watchStatus) - - val categories = async { - pageDataFetcher.fetchCategories(request.title, watchStatus.myQueryResponse) - }.await() + } + viewModelScope.launch(CoroutineExceptionHandler { _, throwable -> + handleError(throwable) + }) { + val watchStatus = pageDataFetcher.fetchWatchStatus(request.title) + val categories = pageDataFetcher.fetchCategories(request.title, watchStatus.myQueryResponse) _categories.value = UiState.Success(categories) + _watchResponseState.value = UiState.Success(watchStatus) } } From fb5144687889b7fa29f835dacbc5d2a8eaa85149 Mon Sep 17 00:00:00 2001 From: williamrai Date: Tue, 15 Jul 2025 10:34:13 -0400 Subject: [PATCH 12/18] - refactors api load function --- .../page/pageload/PageDataFetcher.kt | 76 ++++++++----------- .../page/pageload/PageLoadViewModel.kt | 58 ++++++++------ 2 files changed, 69 insertions(+), 65 deletions(-) diff --git a/app/src/main/java/org/wikipedia/page/pageload/PageDataFetcher.kt b/app/src/main/java/org/wikipedia/page/pageload/PageDataFetcher.kt index ed2195708dc..acb48b79e2a 100644 --- a/app/src/main/java/org/wikipedia/page/pageload/PageDataFetcher.kt +++ b/app/src/main/java/org/wikipedia/page/pageload/PageDataFetcher.kt @@ -1,5 +1,6 @@ package org.wikipedia.page.pageload +import okio.IOException import org.wikipedia.WikipediaApp import org.wikipedia.auth.AccountUtil import org.wikipedia.categories.db.Category @@ -8,35 +9,14 @@ import org.wikipedia.dataclient.ServiceFactory import org.wikipedia.dataclient.mwapi.MwQueryResponse import org.wikipedia.dataclient.okhttp.OfflineCacheInterceptor import org.wikipedia.dataclient.page.PageSummary -import org.wikipedia.history.HistoryEntry import org.wikipedia.notifications.AnonymousNotificationHelper import org.wikipedia.page.PageTitle +import org.wikipedia.util.Resource import org.wikipedia.util.UriUtil import retrofit2.Response class PageDataFetcher { - suspend fun fetchPage(title: PageTitle, entry: HistoryEntry, forceNetwork: Boolean = false): PageResult { - - val cacheControl = if (forceNetwork) "no-cache" else "default" - - val pageSummary = fetchPageSummary(title, cacheControl) - val watchStatus = fetchWatchStatus(title) - val categories = fetchCategories(title, watchStatus.myQueryResponse) - - if (pageSummary.body() == null) { - throw RuntimeException("Summary response was invalid.") - } - - return PageResult.Success( - pageSummaryResponse = pageSummary, - categories = categories, - isWatched = watchStatus.isWatched, - hasWatchlistExpiry = watchStatus.hasWatchlistExpiry, - redirectedFrom = if (pageSummary.raw().priorResponse?.isRedirect == true) title.displayText else null - ) - } - suspend fun fetchPageSummary(title: PageTitle, cacheControl: String): Response { return ServiceFactory.getRest(title.wikiSite).getSummaryResponse( title = title.prefixedText, @@ -47,30 +27,40 @@ class PageDataFetcher { ) } - suspend fun fetchWatchStatus(title: PageTitle): WatchStatus { - return if (WikipediaApp.instance.isOnline && AccountUtil.isLoggedIn) { - val response = ServiceFactory.get(title.wikiSite).getWatchedStatusWithCategories(title.prefixedText) - val page = response.query?.firstPage() - WatchStatus( - isWatched = page?.watched == true, - hasWatchlistExpiry = page?.hasWatchlistExpiry() == true, - myQueryResponse = response - ) - } else if (WikipediaApp.instance.isOnline && !AccountUtil.isLoggedIn) { - val response = AnonymousNotificationHelper.maybeGetAnonUserInfo(title.wikiSite) - WatchStatus(false, false, response) - } else { - WatchStatus(false, false, MwQueryResponse()) + suspend fun fetchWatchStatus(title: PageTitle): Resource { + try { + val watchStatus = if (WikipediaApp.instance.isOnline && AccountUtil.isLoggedIn) { + val response = ServiceFactory.get(title.wikiSite).getWatchedStatusWithCategories(title.prefixedText) + val page = response.query?.firstPage() + WatchStatus( + isWatched = page?.watched == true, + hasWatchlistExpiry = page?.hasWatchlistExpiry() == true, + myQueryResponse = response + ) + } else if (WikipediaApp.instance.isOnline && !AccountUtil.isLoggedIn) { + val response = AnonymousNotificationHelper.maybeGetAnonUserInfo(title.wikiSite) + WatchStatus(false, false, response) + } else { + WatchStatus(false, false, MwQueryResponse()) + } + return Resource.Success(watchStatus) + } catch (e: IOException) { + return Resource.Error(e) } } - suspend fun fetchCategories(title: PageTitle, watchResponse: MwQueryResponse): List { - return if (WikipediaApp.instance.isOnline) { - val response = ServiceFactory.get(title.wikiSite).getCategoriesProps(title.text) - (response.query ?: watchResponse.query)?.firstPage()?.categories?.map { category -> - Category(title = category.title, lang = title.wikiSite.languageCode) - } ?: emptyList() - } else emptyList() + suspend fun fetchCategories(title: PageTitle, watchResponse: MwQueryResponse): Resource> { + try { + val categories = if (WikipediaApp.instance.isOnline) { + val response = ServiceFactory.get(title.wikiSite).getCategoriesProps(title.text) + (response.query ?: watchResponse.query)?.firstPage()?.categories?.map { category -> + Category(title = category.title, lang = title.wikiSite.languageCode) + } ?: emptyList() + } else emptyList() + return Resource.Success(categories) + } catch (e: IOException) { + return Resource.Error(e) + } } private suspend fun isInReadingList(title: PageTitle): Boolean { diff --git a/app/src/main/java/org/wikipedia/page/pageload/PageLoadViewModel.kt b/app/src/main/java/org/wikipedia/page/pageload/PageLoadViewModel.kt index 7fd8f736153..98e33db3460 100644 --- a/app/src/main/java/org/wikipedia/page/pageload/PageLoadViewModel.kt +++ b/app/src/main/java/org/wikipedia/page/pageload/PageLoadViewModel.kt @@ -7,6 +7,7 @@ import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewmodel.initializer import androidx.lifecycle.viewmodel.viewModelFactory import kotlinx.coroutines.CoroutineExceptionHandler +import kotlinx.coroutines.async import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow @@ -25,6 +26,7 @@ import org.wikipedia.page.PageBackStackItem import org.wikipedia.page.PageTitle import org.wikipedia.page.PageViewModel import org.wikipedia.page.tabs.Tab +import org.wikipedia.util.Resource import org.wikipedia.util.UiState import org.wikipedia.util.log.L import retrofit2.Response @@ -244,25 +246,41 @@ class PageLoadViewModel(private val app: WikipediaApp) : ViewModel() { handleError(throwable) }) { val cacheControl = if (request.options.isRefresh) "no-cache" else "default" - val pageSummary = pageDataFetcher.fetchPageSummary(request.title, cacheControl) - pageSummary.body()?.let { value -> - val pageModel = createPageModel(request, pageSummary) - _currentPageViewModel.value = pageModel - _pageLoadUiState.value = PageLoadUiState.Success( - result = value, - title = request.title, - stagedScrollY = request.options.stagedScrollY, - redirectedFrom = if (pageSummary.raw().priorResponse?.isRedirect == true) request.title.displayText else null - ) - } + val pageSummaryDeferred = async { pageDataFetcher.fetchPageSummary(request.title, cacheControl) } + val watchStatusDeferred = async { pageDataFetcher.fetchWatchStatus(request.title) } + + val pageSummary = pageSummaryDeferred.await() + handlePageSummary(pageSummary, request) + + val watchStatus = watchStatusDeferred.await() + handleWatchStatusAndFetchCategories(watchStatus, request.title) } - viewModelScope.launch(CoroutineExceptionHandler { _, throwable -> - handleError(throwable) - }) { - val watchStatus = pageDataFetcher.fetchWatchStatus(request.title) - val categories = pageDataFetcher.fetchCategories(request.title, watchStatus.myQueryResponse) - _categories.value = UiState.Success(categories) - _watchResponseState.value = UiState.Success(watchStatus) + } + + private suspend fun handlePageSummary(pageSummary: Response, request: PageLoadRequest) { + pageSummary.body()?.let { value -> + val pageModel = createPageModel(request, pageSummary) + _currentPageViewModel.value = pageModel + _pageLoadUiState.value = PageLoadUiState.Success( + result = value, + title = request.title, + stagedScrollY = request.options.stagedScrollY, + redirectedFrom = if (pageSummary.raw().priorResponse?.isRedirect == true) request.title.displayText else null + ) + } + } + + private suspend fun handleWatchStatusAndFetchCategories(watchStatus: Resource, title: PageTitle) { + when (watchStatus) { + is Resource.Success -> { + _watchResponseState.value = UiState.Success(watchStatus.data) + val categoriesStatus = pageDataFetcher.fetchCategories(title, watchStatus.data.myQueryResponse) + when (categoriesStatus) { + is Resource.Success -> { _categories.value = UiState.Success(categoriesStatus.data) } + is Resource.Error -> { _categories.value = UiState.Error(categoriesStatus.throwable) } + } + } + is Resource.Error -> _watchResponseState.value = UiState.Error(watchStatus.throwable) } } @@ -340,10 +358,6 @@ class PageLoadViewModel(private val app: WikipediaApp) : ViewModel() { page = pageSummary?.toPage(request.title) title = page?.title - // from other api calls, need to update this later in the model -// isWatched = result.isWatched -// hasWatchlistExpiry = result.hasWatchlistExpiry - title?.let { if (!response.raw().request.url.fragment.isNullOrEmpty()) { it.fragment = response.raw().request.url.fragment From 343b7f31364f387aa07b022aaf2632aff99bfecf Mon Sep 17 00:00:00 2001 From: williamrai Date: Tue, 15 Jul 2025 14:57:33 -0400 Subject: [PATCH 13/18] - removes unused code --- .../java/org/wikipedia/page/PageFragment.kt | 1 - .../wikipedia/page/PageFragmentLoadState.kt | 270 ------------------ 2 files changed, 271 deletions(-) delete mode 100644 app/src/main/java/org/wikipedia/page/PageFragmentLoadState.kt diff --git a/app/src/main/java/org/wikipedia/page/PageFragment.kt b/app/src/main/java/org/wikipedia/page/PageFragment.kt index 36f3470920f..f008c7d1d31 100644 --- a/app/src/main/java/org/wikipedia/page/PageFragment.kt +++ b/app/src/main/java/org/wikipedia/page/PageFragment.kt @@ -125,7 +125,6 @@ import org.wikipedia.wiktionary.WiktionaryDialog import java.time.Duration import java.time.Instant -// @TODO: offline test, visit article, save article turn airplane mode, chinese variant test, and other language test class PageFragment : Fragment(), BackPressedHandler, CommunicationBridge.CommunicationBridgeListener, ThemeChooserDialog.Callback, ReferenceDialog.Callback, WiktionaryDialog.Callback, WatchlistExpiryDialog.Callback { diff --git a/app/src/main/java/org/wikipedia/page/PageFragmentLoadState.kt b/app/src/main/java/org/wikipedia/page/PageFragmentLoadState.kt deleted file mode 100644 index c277efa5a40..00000000000 --- a/app/src/main/java/org/wikipedia/page/PageFragmentLoadState.kt +++ /dev/null @@ -1,270 +0,0 @@ -package org.wikipedia.page - -import android.widget.Toast -import androidx.lifecycle.lifecycleScope -import kotlinx.coroutines.CoroutineExceptionHandler -import kotlinx.coroutines.MainScope -import kotlinx.coroutines.async -import kotlinx.coroutines.launch -import org.wikipedia.R -import org.wikipedia.WikipediaApp -import org.wikipedia.analytics.eventplatform.ArticleLinkPreviewInteractionEvent -import org.wikipedia.analytics.metricsplatform.ArticleLinkPreviewInteraction -import org.wikipedia.auth.AccountUtil -import org.wikipedia.bridge.CommunicationBridge -import org.wikipedia.categories.db.Category -import org.wikipedia.database.AppDatabase -import org.wikipedia.dataclient.ServiceFactory -import org.wikipedia.dataclient.mwapi.MwQueryResponse -import org.wikipedia.dataclient.okhttp.OfflineCacheInterceptor -import org.wikipedia.dataclient.page.PageSummary -import org.wikipedia.history.HistoryEntry -import org.wikipedia.notifications.AnonymousNotificationHelper -import org.wikipedia.page.leadimages.LeadImagesHandler -import org.wikipedia.page.tabs.Tab -import org.wikipedia.settings.Prefs -import org.wikipedia.staticdata.UserTalkAliasData -import org.wikipedia.util.DateUtil -import org.wikipedia.util.UriUtil -import org.wikipedia.util.log.L -import org.wikipedia.views.ObservableWebView -import retrofit2.Response -import java.io.IOException -import java.time.Instant -import java.time.LocalDate -import java.time.ZoneId - -class PageFragmentLoadState(private var model: PageViewModel, - private var fragment: PageFragment, - private var webView: ObservableWebView, - private var bridge: CommunicationBridge, - private var leadImagesHandler: LeadImagesHandler, - private var currentTab: Tab) { - - fun load(pushBackStack: Boolean) { - // done - if (pushBackStack && model.title != null && model.curEntry != null) { - // update the topmost entry in the backstack, before we start overwriting things. - updateCurrentBackStackItem() - currentTab.pushBackStackItem(PageBackStackItem(model.title!!, model.curEntry!!)) - } - pageLoad() - } - - fun updateCurrentBackStackItem() { - if (currentTab.backStack.isEmpty()) { - return - } - val item = currentTab.backStack[currentTab.backStackPosition] - item.scrollY = webView.scrollY - model.title?.let { - item.title.description = it.description - item.title.thumbUrl = it.thumbUrl - } - } - - private fun commonSectionFetchOnCatch(caught: Throwable) { - if (!fragment.isAdded) { - return - } - fragment.requireActivity().invalidateOptionsMenu() - fragment.onPageLoadError(caught) - } - - private fun pageLoad() { - model.title?.let { title -> - fragment.lifecycleScope.launch(CoroutineExceptionHandler { _, throwable -> - L.e("Page details network error: ", throwable) - commonSectionFetchOnCatch(throwable) - }) { - // not done - model.readingListPage = AppDatabase.instance.readingListPageDao().findPageInAnyList(title) - - // done - fragment.updateQuickActionsAndMenuOptions() - fragment.requireActivity().invalidateOptionsMenu() - fragment.callback()?.onPageUpdateProgressBar(true) - - model.page = null - - // done - val delayLoadHtml = title.prefixedText.contains(":") - if (!delayLoadHtml) { - bridge.resetHtml(title) - } - - // done - if (title.namespace() === Namespace.SPECIAL) { - // Short-circuit the entire process of fetching the Summary, since Special: pages - // are not supported in RestBase. - bridge.resetHtml(title) - leadImagesHandler.loadLeadImage() - fragment.requireActivity().invalidateOptionsMenu() - fragment.onPageMetadataLoaded() - return@launch - } - - // done - val pageSummaryRequest = async { - ServiceFactory.getRest(title.wikiSite).getSummaryResponse(title.prefixedText, cacheControl = model.cacheControl.toString(), - saveHeader = if (model.isInReadingList) OfflineCacheInterceptor.SAVE_HEADER_SAVE else null, - langHeader = title.wikiSite.languageCode, titleHeader = UriUtil.encodeURL(title.prefixedText)) - } - // done - val makeWatchRequest = WikipediaApp.instance.isOnline && AccountUtil.isLoggedIn - val watchedRequest = async { - try { - if (makeWatchRequest) { - ServiceFactory.get(title.wikiSite) - .getWatchedStatusWithCategories(title.prefixedText) - } else if (WikipediaApp.instance.isOnline && !AccountUtil.isLoggedIn) { - AnonymousNotificationHelper.maybeGetAnonUserInfo(title.wikiSite) - } else { - MwQueryResponse() - } - } catch (_: IOException) { - L.w("Ignoring network error while fetching watched status.") - MwQueryResponse() - } - } - // done - val categoriesRequest = async { - try { - if (!makeWatchRequest && WikipediaApp.instance.isOnline) { - ServiceFactory.get(title.wikiSite).getCategoriesProps(title.text) - } else { - MwQueryResponse() - } - } catch (_: IOException) { - L.w("Ignoring network error while fetching categories.") - MwQueryResponse() - } - } - val pageSummaryResponse = pageSummaryRequest.await() - val watchedResponse = watchedRequest.await() - val categoriesResponse = categoriesRequest.await() - // done - val isWatched = watchedResponse.query?.firstPage()?.watched == true - val hasWatchlistExpiry = watchedResponse.query?.firstPage()?.hasWatchlistExpiry() == true - if (pageSummaryResponse.body() == null) { - throw RuntimeException("Summary response was invalid.") - } - // done - val redirectedFrom = if (pageSummaryResponse.raw().priorResponse?.isRedirect == true) model.title?.displayText else null - - // partial done - createPageModel(pageSummaryResponse, isWatched, hasWatchlistExpiry) - - // not done - if (OfflineCacheInterceptor.SAVE_HEADER_SAVE == pageSummaryResponse.headers()[OfflineCacheInterceptor.SAVE_HEADER]) { - showPageOfflineMessage(pageSummaryResponse.headers().getInstant("date")) - } - - // done - val categoryList = (categoriesResponse.query ?: watchedResponse.query)?.firstPage()?.categories?.map { category -> - Category(title = category.title, lang = title.wikiSite.languageCode) - }.orEmpty() - - // not done - if (categoryList.isNotEmpty()) { - AppDatabase.instance.categoryDao().upsertAll(categoryList) - } - - // not done - if (delayLoadHtml) { - bridge.resetHtml(title) - } - - // done - fragment.onPageMetadataLoaded(redirectedFrom) - - // not done - if (AnonymousNotificationHelper.shouldCheckAnonNotifications(watchedResponse)) { - checkAnonNotifications(title) - } - } - } - } - - private fun checkAnonNotifications(title: PageTitle) { - fragment.lifecycleScope.launch(CoroutineExceptionHandler { _, throwable -> - L.e(throwable) - }) { - val response = ServiceFactory.get(title.wikiSite) - .getLastModified(UserTalkAliasData.valueFor(title.wikiSite.languageCode) + ":" + Prefs.lastAnonUserWithMessages) - if (AnonymousNotificationHelper.anonTalkPageHasRecentMessage(response, title)) { - fragment.showAnonNotification() - } - } - } - - private fun showPageOfflineMessage(dateHeader: Instant?) { - if (!fragment.isAdded || dateHeader == null) { - return - } - val localDate = LocalDate.ofInstant(dateHeader, ZoneId.systemDefault()) - val dateStr = DateUtil.getShortDateString(localDate) - Toast.makeText(fragment.requireContext().applicationContext, - fragment.getString(R.string.page_offline_notice_last_date, dateStr), - Toast.LENGTH_LONG).show() - } - - private fun createPageModel(response: Response, - isWatched: Boolean, - hasWatchlistExpiry: Boolean) { - if (!fragment.isAdded || response.body() == null) { - return - } - val pageSummary = response.body() - val page = pageSummary?.toPage(model.title) - model.page = page - model.isWatched = isWatched - model.hasWatchlistExpiry = hasWatchlistExpiry - model.title = page?.title - model.title?.let { title -> - if (!response.raw().request.url.fragment.isNullOrEmpty()) { - title.fragment = response.raw().request.url.fragment - } - if (title.description.isNullOrEmpty()) { - WikipediaApp.instance.appSessionEvent.noDescription() - } - if (!title.isMainPage) { - title.displayText = page?.displayTitle.orEmpty() - } - title.thumbUrl = pageSummary?.thumbnailUrl - leadImagesHandler.loadLeadImage() - fragment.requireActivity().invalidateOptionsMenu() - - // Update our tab list to prevent ZH variants issue. - WikipediaApp.instance.tabList.getOrNull(WikipediaApp.instance.tabCount - 1)?.setBackStackPositionTitle(title) - - // Update our history entry, in case the Title was changed (i.e. normalized) - model.curEntry?.let { - val entry = HistoryEntry( - title, - it.source, - timestamp = it.timestamp - ).apply { - referrer = it.referrer - prevId = it.prevId - } - model.curEntry = entry - - MainScope().launch { - // Insert and/or update this history entry in the DB - AppDatabase.instance.historyEntryDao().upsert(entry).run { - model.curEntry?.id = this - } - - // Update metadata in the DB - AppDatabase.instance.pageImagesDao().upsertForMetadata(entry, title.thumbUrl, title.description, pageSummary?.coordinates?.latitude, pageSummary?.coordinates?.longitude) - } - - // And finally, count this as a page view. - WikipediaApp.instance.appSessionEvent.pageViewed(entry) - ArticleLinkPreviewInteractionEvent(title.wikiSite.dbName(), pageSummary?.pageId ?: 0, entry.source).logNavigate() - ArticleLinkPreviewInteraction(fragment, entry.source).logNavigate() - } - } - } -} From a8dc11e631c9e6ae64bf3810881ab89cc17eee1e Mon Sep 17 00:00:00 2001 From: williamrai Date: Tue, 15 Jul 2025 15:00:12 -0400 Subject: [PATCH 14/18] - code fix --- app/src/main/java/org/wikipedia/page/PageFragment.kt | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/app/src/main/java/org/wikipedia/page/PageFragment.kt b/app/src/main/java/org/wikipedia/page/PageFragment.kt index f008c7d1d31..aff31e1619f 100644 --- a/app/src/main/java/org/wikipedia/page/PageFragment.kt +++ b/app/src/main/java/org/wikipedia/page/PageFragment.kt @@ -1084,10 +1084,6 @@ class PageFragment : Fragment(), BackPressedHandler, CommunicationBridge.Communi } } - fun goForward() { - pageLoadViewModel.goForward() - } - fun showBottomSheet(dialog: BottomSheetDialogFragment) { ExclusiveBottomSheetPresenter.show(childFragmentManager, dialog) } @@ -1353,7 +1349,7 @@ class PageFragment : Fragment(), BackPressedHandler, CommunicationBridge.Communi } override fun forwardClick() { - goForward() + pageLoadViewModel.goForward() articleInteractionEvent?.logForwardClick() metricsPlatformArticleEventToolbarInteraction.logForwardClick() } From 24c003e629b6a071e2eb28bdc351ed37cd5e6beb Mon Sep 17 00:00:00 2001 From: williamrai Date: Tue, 15 Jul 2025 15:19:17 -0400 Subject: [PATCH 15/18] - code fix --- .../main/java/org/wikipedia/page/pageload/PageDataFetcher.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/org/wikipedia/page/pageload/PageDataFetcher.kt b/app/src/main/java/org/wikipedia/page/pageload/PageDataFetcher.kt index acb48b79e2a..c64147ad4f4 100644 --- a/app/src/main/java/org/wikipedia/page/pageload/PageDataFetcher.kt +++ b/app/src/main/java/org/wikipedia/page/pageload/PageDataFetcher.kt @@ -1,6 +1,5 @@ package org.wikipedia.page.pageload -import okio.IOException import org.wikipedia.WikipediaApp import org.wikipedia.auth.AccountUtil import org.wikipedia.categories.db.Category @@ -14,6 +13,7 @@ import org.wikipedia.page.PageTitle import org.wikipedia.util.Resource import org.wikipedia.util.UriUtil import retrofit2.Response +import java.io.IOException class PageDataFetcher { From dcae5dc628b7dea9161c91c372a097fcf66c64d8 Mon Sep 17 00:00:00 2001 From: williamrai Date: Tue, 15 Jul 2025 15:20:20 -0400 Subject: [PATCH 16/18] - code fix --- app/src/main/java/org/wikipedia/page/PageFragment.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/org/wikipedia/page/PageFragment.kt b/app/src/main/java/org/wikipedia/page/PageFragment.kt index aff31e1619f..15697403834 100644 --- a/app/src/main/java/org/wikipedia/page/PageFragment.kt +++ b/app/src/main/java/org/wikipedia/page/PageFragment.kt @@ -41,7 +41,6 @@ import kotlinx.serialization.json.float import kotlinx.serialization.json.jsonArray import kotlinx.serialization.json.jsonObject import kotlinx.serialization.json.jsonPrimitive -import okio.IOException import org.wikipedia.BackPressedHandler import org.wikipedia.Constants import org.wikipedia.Constants.InvokeSource @@ -122,6 +121,7 @@ import org.wikipedia.watchlist.WatchlistExpiry import org.wikipedia.watchlist.WatchlistExpiryDialog import org.wikipedia.watchlist.WatchlistViewModel import org.wikipedia.wiktionary.WiktionaryDialog +import java.io.IOException import java.time.Duration import java.time.Instant From 0d6a0d749cd4146bb5a4f6e47bb268dd9ff13a10 Mon Sep 17 00:00:00 2001 From: williamrai Date: Wed, 16 Jul 2025 11:42:25 -0400 Subject: [PATCH 17/18] - code fixes --- .../main/java/org/wikipedia/page/PageFragment.kt | 6 ++---- .../wikipedia/page/pageload/PageDataFetcher.kt | 4 ++-- .../wikipedia/page/pageload/PageLoadViewModel.kt | 16 ++-------------- 3 files changed, 6 insertions(+), 20 deletions(-) diff --git a/app/src/main/java/org/wikipedia/page/PageFragment.kt b/app/src/main/java/org/wikipedia/page/PageFragment.kt index 15697403834..1059822fd89 100644 --- a/app/src/main/java/org/wikipedia/page/PageFragment.kt +++ b/app/src/main/java/org/wikipedia/page/PageFragment.kt @@ -27,7 +27,7 @@ import androidx.core.app.ActivityOptionsCompat import androidx.core.view.forEach import androidx.core.widget.TextViewCompat import androidx.fragment.app.Fragment -import androidx.lifecycle.ViewModelProvider +import androidx.fragment.app.viewModels import androidx.lifecycle.lifecycleScope import androidx.swiperefreshlayout.widget.SwipeRefreshLayout.OnRefreshListener import com.google.android.material.bottomsheet.BottomSheetDialogFragment @@ -195,7 +195,7 @@ class PageFragment : Fragment(), BackPressedHandler, CommunicationBridge.Communi val page get() = model.page val isLoading get() = bridge.isLoading val leadImageEditLang get() = leadImagesHandler.callToActionEditLang - private lateinit var pageLoadViewModel: PageLoadViewModel + private val pageLoadViewModel: PageLoadViewModel by viewModels() override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { _binding = FragmentPageBinding.inflate(inflater, container, false) @@ -243,8 +243,6 @@ class PageFragment : Fragment(), BackPressedHandler, CommunicationBridge.Communi model.isReadMoreLoaded = true } } - - pageLoadViewModel = ViewModelProvider(this, PageLoadViewModel.Factory)[PageLoadViewModel::class.java] editHandler = EditHandler(this, bridge) sidePanelHandler = SidePanelHandler(this, bridge) leadImagesHandler = LeadImagesHandler(this, webView, binding.pageHeaderView, callback()) diff --git a/app/src/main/java/org/wikipedia/page/pageload/PageDataFetcher.kt b/app/src/main/java/org/wikipedia/page/pageload/PageDataFetcher.kt index c64147ad4f4..f3dffe1922e 100644 --- a/app/src/main/java/org/wikipedia/page/pageload/PageDataFetcher.kt +++ b/app/src/main/java/org/wikipedia/page/pageload/PageDataFetcher.kt @@ -39,9 +39,9 @@ class PageDataFetcher { ) } else if (WikipediaApp.instance.isOnline && !AccountUtil.isLoggedIn) { val response = AnonymousNotificationHelper.maybeGetAnonUserInfo(title.wikiSite) - WatchStatus(false, false, response) + WatchStatus(isWatched = false, hasWatchlistExpiry = false, myQueryResponse = response) } else { - WatchStatus(false, false, MwQueryResponse()) + WatchStatus(isWatched = false, hasWatchlistExpiry = false, myQueryResponse = MwQueryResponse()) } return Resource.Success(watchStatus) } catch (e: IOException) { diff --git a/app/src/main/java/org/wikipedia/page/pageload/PageLoadViewModel.kt b/app/src/main/java/org/wikipedia/page/pageload/PageLoadViewModel.kt index 98e33db3460..0c5d4bd5574 100644 --- a/app/src/main/java/org/wikipedia/page/pageload/PageLoadViewModel.kt +++ b/app/src/main/java/org/wikipedia/page/pageload/PageLoadViewModel.kt @@ -1,11 +1,7 @@ package org.wikipedia.page.pageload import androidx.lifecycle.ViewModel -import androidx.lifecycle.ViewModelProvider -import androidx.lifecycle.ViewModelProvider.AndroidViewModelFactory.Companion.APPLICATION_KEY import androidx.lifecycle.viewModelScope -import androidx.lifecycle.viewmodel.initializer -import androidx.lifecycle.viewmodel.viewModelFactory import kotlinx.coroutines.CoroutineExceptionHandler import kotlinx.coroutines.async import kotlinx.coroutines.flow.MutableSharedFlow @@ -31,7 +27,8 @@ import org.wikipedia.util.UiState import org.wikipedia.util.log.L import retrofit2.Response -class PageLoadViewModel(private val app: WikipediaApp) : ViewModel() { +class PageLoadViewModel : ViewModel() { + private val app = WikipediaApp.instance private val _pageLoadUiState = MutableStateFlow(PageLoadUiState.LoadingPrep()) val pageLoadUiState = _pageLoadUiState.asStateFlow() @@ -388,15 +385,6 @@ class PageLoadViewModel(private val app: WikipediaApp) : ViewModel() { } } - companion object { - val Factory: ViewModelProvider.Factory = viewModelFactory { - initializer { - val app = this[APPLICATION_KEY] as WikipediaApp - PageLoadViewModel(app) - } - } - } - data class Animate( val animateButtons: Boolean = false, val sectionAnchor: String? = null From 95aece102624eec0fe7260dee2871e908fa917f1 Mon Sep 17 00:00:00 2001 From: williamrai Date: Wed, 16 Jul 2025 14:29:35 -0400 Subject: [PATCH 18/18] - code fixes/ fetching logic changes --- .../page/pageload/PageDataFetcher.kt | 14 +++--- .../page/pageload/PageLoadViewModel.kt | 43 +++++++++++++------ 2 files changed, 36 insertions(+), 21 deletions(-) diff --git a/app/src/main/java/org/wikipedia/page/pageload/PageDataFetcher.kt b/app/src/main/java/org/wikipedia/page/pageload/PageDataFetcher.kt index f3dffe1922e..6146ebe01fc 100644 --- a/app/src/main/java/org/wikipedia/page/pageload/PageDataFetcher.kt +++ b/app/src/main/java/org/wikipedia/page/pageload/PageDataFetcher.kt @@ -12,6 +12,7 @@ import org.wikipedia.notifications.AnonymousNotificationHelper import org.wikipedia.page.PageTitle import org.wikipedia.util.Resource import org.wikipedia.util.UriUtil +import org.wikipedia.util.log.L import retrofit2.Response import java.io.IOException @@ -45,20 +46,17 @@ class PageDataFetcher { } return Resource.Success(watchStatus) } catch (e: IOException) { + L.w("Ignoring network error while fetching watched status.") return Resource.Error(e) } } - suspend fun fetchCategories(title: PageTitle, watchResponse: MwQueryResponse): Resource> { + suspend fun fetchCategories(title: PageTitle): Resource { try { - val categories = if (WikipediaApp.instance.isOnline) { - val response = ServiceFactory.get(title.wikiSite).getCategoriesProps(title.text) - (response.query ?: watchResponse.query)?.firstPage()?.categories?.map { category -> - Category(title = category.title, lang = title.wikiSite.languageCode) - } ?: emptyList() - } else emptyList() - return Resource.Success(categories) + val response = ServiceFactory.get(title.wikiSite).getCategoriesProps(title.text) + return Resource.Success(response) } catch (e: IOException) { + L.w("Ignoring network error while fetching categories.") return Resource.Error(e) } } diff --git a/app/src/main/java/org/wikipedia/page/pageload/PageLoadViewModel.kt b/app/src/main/java/org/wikipedia/page/pageload/PageLoadViewModel.kt index 0c5d4bd5574..dfc785faf4d 100644 --- a/app/src/main/java/org/wikipedia/page/pageload/PageLoadViewModel.kt +++ b/app/src/main/java/org/wikipedia/page/pageload/PageLoadViewModel.kt @@ -3,7 +3,6 @@ package org.wikipedia.page.pageload import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import kotlinx.coroutines.CoroutineExceptionHandler -import kotlinx.coroutines.async import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow @@ -11,9 +10,11 @@ import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import org.wikipedia.Constants 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.mwapi.MwQueryResponse import org.wikipedia.dataclient.page.PageSummary import org.wikipedia.history.HistoryEntry import org.wikipedia.page.Namespace @@ -243,14 +244,17 @@ class PageLoadViewModel : ViewModel() { handleError(throwable) }) { val cacheControl = if (request.options.isRefresh) "no-cache" else "default" - val pageSummaryDeferred = async { pageDataFetcher.fetchPageSummary(request.title, cacheControl) } - val watchStatusDeferred = async { pageDataFetcher.fetchWatchStatus(request.title) } - - val pageSummary = pageSummaryDeferred.await() + val pageSummary = pageDataFetcher.fetchPageSummary(request.title, cacheControl) handlePageSummary(pageSummary, request) - val watchStatus = watchStatusDeferred.await() - handleWatchStatusAndFetchCategories(watchStatus, request.title) + val canMakeWatchRequest = WikipediaApp.instance.isOnline && AccountUtil.isLoggedIn + if (canMakeWatchRequest) { + val watchStatus = pageDataFetcher.fetchWatchStatus(request.title) + handleWatchStatus(watchStatus, request.title) + } else if (WikipediaApp.instance.isOnline) { + val categoriesStatus = pageDataFetcher.fetchCategories(request.title) + handleCategoriesStatus(categoriesStatus, request.title) + } } } @@ -267,20 +271,33 @@ class PageLoadViewModel : ViewModel() { } } - private suspend fun handleWatchStatusAndFetchCategories(watchStatus: Resource, title: PageTitle) { + private fun handleWatchStatus(watchStatus: Resource, title: PageTitle) { when (watchStatus) { is Resource.Success -> { _watchResponseState.value = UiState.Success(watchStatus.data) - val categoriesStatus = pageDataFetcher.fetchCategories(title, watchStatus.data.myQueryResponse) - when (categoriesStatus) { - is Resource.Success -> { _categories.value = UiState.Success(categoriesStatus.data) } - is Resource.Error -> { _categories.value = UiState.Error(categoriesStatus.throwable) } - } + val categories = unwrapCategories(watchStatus.data.myQueryResponse, title) + _categories.value = UiState.Success(categories) } is Resource.Error -> _watchResponseState.value = UiState.Error(watchStatus.throwable) } } + private fun handleCategoriesStatus(categoriesStatus: Resource, title: PageTitle) { + when (categoriesStatus) { + is Resource.Success -> { + val categories = unwrapCategories(categoriesStatus.data, title) + _categories.value = UiState.Success(categories) + } + is Resource.Error -> { _categories.value = UiState.Error(categoriesStatus.throwable) } + } + } + + private fun unwrapCategories(response: MwQueryResponse, title: PageTitle): List { + return response.query?.firstPage()?.categories?.map { category -> + Category(title = category.title, lang = title.wikiSite.languageCode) + } ?: emptyList() + } + private fun loadBackgroundTabMetadata(title: PageTitle) { viewModelScope.launch(CoroutineExceptionHandler { _, t -> L.e(t) }) { ServiceFactory.get(title.wikiSite)