diff --git a/app/src/main/java/org/wikipedia/page/PageActivity.kt b/app/src/main/java/org/wikipedia/page/PageActivity.kt index 8de31ec3a85..bcfd89ddd80 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, options = PageLoadOptions(pushBackStack = false, squashBackStack = false, isRefresh = true)) } } } @@ -613,13 +614,14 @@ 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, 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/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 fa301a97dc2..af431ec057b 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.fragment.app.viewModels import androidx.lifecycle.lifecycleScope import androidx.swiperefreshlayout.widget.SwipeRefreshLayout.OnRefreshListener import com.google.android.material.bottomsheet.BottomSheetDialogFragment @@ -49,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 @@ -64,7 +67,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 @@ -87,10 +89,13 @@ 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.PageLoadUiState +import org.wikipedia.page.pageload.PageLoadViewModel 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 @@ -106,6 +111,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 @@ -115,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 @@ -157,7 +164,6 @@ class PageFragment : Fragment(), BackPressedHandler, CommunicationBridge.Communi 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) @@ -183,15 +189,13 @@ 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 val page get() = model.page val isLoading get() = bridge.isLoading val leadImageEditLang get() = leadImagesHandler.callToActionEditLang + private val pageLoadViewModel: PageLoadViewModel by viewModels() override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { _binding = FragmentPageBinding.inflate(inflater, container, false) @@ -239,12 +243,10 @@ class PageFragment : Fragment(), BackPressedHandler, CommunicationBridge.Communi model.isReadMoreLoaded = true } } - 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) if (callback() != null) { LongPressHandler(webView, HistoryEntry.SOURCE_INTERNAL_LINK, PageContainerLongPressHandler(this)) @@ -253,6 +255,7 @@ class PageFragment : Fragment(), BackPressedHandler, CommunicationBridge.Communi if (shouldLoadFromBackstack(activity) || savedInstanceState != null) { reloadFromBackstack() } + setupObservers() } override fun onSaveInstanceState(outState: Bundle) { @@ -285,9 +288,10 @@ class PageFragment : Fragment(), BackPressedHandler, CommunicationBridge.Communi } activeTimer.pause() addTimeSpentReading(activeTimer.elapsedSec) - pageFragmentLoadState.updateCurrentBackStackItem() + + pageLoadViewModel.updateCurrentBackStackItem(webView.scrollY) app.commitTabState() - val time = if (app.tabList.size >= 1 && !pageFragmentLoadState.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() @@ -313,7 +317,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) { - pageFragmentLoadState.onConfigurationChanged() + leadImagesHandler.loadLeadImage() + bridge.execute(JavaScriptActionHandler.setTopMargin(leadImagesHandler.topMargin)) } } @@ -324,7 +329,8 @@ class PageFragment : Fragment(), BackPressedHandler, CommunicationBridge.Communi sidePanelHandler.hide() return true } - if (pageFragmentLoadState.goBack()) { + + if (pageLoadViewModel.goBack()) { return true } // if the current tab can no longer go back, then close the tab before exiting @@ -505,68 +511,12 @@ 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) - pageFragmentLoadState.setTab(tab) - } - if (app.tabCount > 0) { - app.tabList.last().squashBackstack() - pageFragmentLoadState.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)) - } - } - - private fun dismissBottomSheet() { + fun dismissBottomSheet() { ExclusiveBottomSheetPresenter.dismiss(childFragmentManager) callback()?.onPageDismissBottomSheet() } - private fun updateProgressBar(visible: Boolean) { + fun updateProgressBar(visible: Boolean) { callback()?.onPageUpdateProgressBar(visible) } @@ -881,9 +831,9 @@ class PageFragment : Fragment(), BackPressedHandler, CommunicationBridge.Communi } fun reloadFromBackstack(forceReload: Boolean = true) { - if (pageFragmentLoadState.setTab(currentTab) || forceReload) { - if (!pageFragmentLoadState.backStackEmpty()) { - pageFragmentLoadState.loadFromBackStack() + if (pageLoadViewModel.setTab(currentTab) || forceReload) { + if (!pageLoadViewModel.backStackEmpty()) { + pageLoadViewModel.loadFromBackStack() } else { callback()?.onPageLoadMainPageInForegroundTab() } @@ -931,86 +881,9 @@ 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() - } else { - openInNewTab(title, entry, backgroundTabPosition) - (requireActivity() as PageActivity).animateTabsButton() - } - } - - 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 + fun loadPage(title: PageTitle, entry: HistoryEntry, options: PageLoadOptions = PageLoadOptions()) { + val request = PageLoadRequest(title, entry, options) + pageLoadViewModel.loadPage(request, webView.scrollY) } fun updateFontSize() { @@ -1122,7 +995,7 @@ class PageFragment : Fragment(), BackPressedHandler, CommunicationBridge.Communi } } - private fun scrollToSection(sectionAnchor: String) { + fun scrollToSection(sectionAnchor: String) { if (!isAdded) { return } @@ -1167,12 +1040,17 @@ 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, + isRefresh = app.isOnline, + stagedScrollY = stagedScrollY + )) } } } - private fun clearActivityActionBarTitle() { + fun clearActivityActionBarTitle() { val currentActivity = requireActivity() if (currentActivity is PageActivity) { currentActivity.clearActionBarTitle() @@ -1204,15 +1082,11 @@ class PageFragment : Fragment(), BackPressedHandler, CommunicationBridge.Communi } } - fun goForward() { - pageFragmentLoadState.goForward() - } - fun showBottomSheet(dialog: BottomSheetDialogFragment) { ExclusiveBottomSheetPresenter.show(childFragmentManager, dialog) } - fun loadPage(title: PageTitle, entry: HistoryEntry) { + fun onPageLoadPage(title: PageTitle, entry: HistoryEntry) { callback()?.onPageLoadPage(title, entry) } @@ -1270,7 +1144,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) { @@ -1473,12 +1347,141 @@ class PageFragment : Fragment(), BackPressedHandler, CommunicationBridge.Communi } override fun forwardClick() { - goForward() + pageLoadViewModel.goForward() articleInteractionEvent?.logForwardClick() metricsPlatformArticleEventToolbarInteraction.logForwardClick() } } + // UI observers + private fun setupObservers() { + viewLifecycleOwner.lifecycleScope.launch { + pageLoadViewModel.animateType.collect { type -> + if (type.animateButtons) { + requireActivity().invalidateOptionsMenu() + (requireActivity() as PageActivity).animateTabsButton() + } + if (type.sectionAnchor != null) { + scrollToSection(type.sectionAnchor) + } + } + } + + viewLifecycleOwner.lifecycleScope.launch { + pageLoadViewModel.watchResponseState.collect { state -> + when (state) { + 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 -> + when (state) { + 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) + } + } + } + + viewLifecycleOwner.lifecycleScope.launch { + pageLoadViewModel.currentPageViewModel.collect { + model = it + } + } + + viewLifecycleOwner.lifecycleScope.launch { + pageLoadViewModel.pageLoadUiState.collect { uiState -> + when (uiState) { + is PageLoadUiState.LoadingPrep -> handleLoadingState(uiState) + is PageLoadUiState.Success -> handleSuccessPageLoadingState(uiState) + is PageLoadUiState.SpecialPage -> handleSpecialLoadingPage(uiState) + is PageLoadUiState.Error -> onPageLoadError(uiState.throwable) + } + } + } + } + + private fun handleLoadingState(state: PageLoadUiState.LoadingPrep) { + 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 + + if (AccountUtil.isLoggedIn) { + // explicitly check notifications for the current user + PollNotificationWorker.schedulePollNotificationJob(requireContext()) + } + + EventPlatformClient.AssociationController.beginNewPageView() + + addTimeSpentReading(activeTimer.elapsedSec) + activeTimer.reset() + + updateQuickActionsAndMenuOptions() + requireActivity().invalidateOptionsMenu() + } + + private fun handleSuccessPageLoadingState(state: PageLoadUiState.Success) { + when { + state.sectionAnchor != null -> { + scrollToSection(state.sectionAnchor) + return + } + !state.title.prefixedText.contains(":") -> bridge.resetHtml(state.title) + } + updateProgressBar(false) + pageLoadViewModel.updateTabListToPreventZHVariantIssue(model.title) + + 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 + leadImagesHandler.loadLeadImage() + onPageMetadataLoaded(state.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/PageFragmentLoadState.kt b/app/src/main/java/org/wikipedia/page/PageFragmentLoadState.kt deleted file mode 100644 index 40e93e7f658..00000000000 --- a/app/src/main/java/org/wikipedia/page/PageFragmentLoadState.kt +++ /dev/null @@ -1,293 +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.bridge.JavaScriptActionHandler -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) { - 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 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 - } - val item = currentTab.backStack[currentTab.backStackPosition] - item.scrollY = webView.scrollY - model.title?.let { - item.title.description = it.description - item.title.thumbUrl = it.thumbUrl - } - } - - 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 - } - 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) - }) { - model.readingListPage = AppDatabase.instance.readingListPageDao().findPageInAnyList(title) - - fragment.updateQuickActionsAndMenuOptions() - fragment.requireActivity().invalidateOptionsMenu() - fragment.callback()?.onPageUpdateProgressBar(true) - model.page = null - val delayLoadHtml = title.prefixedText.contains(":") - if (!delayLoadHtml) { - bridge.resetHtml(title) - } - 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 - } - - 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)) - } - 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() - } - } - 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() - val isWatched = watchedResponse.query?.firstPage()?.watched == true - val hasWatchlistExpiry = watchedResponse.query?.firstPage()?.hasWatchlistExpiry() == true - if (pageSummaryResponse.body() == null) { - throw RuntimeException("Summary response was invalid.") - } - val redirectedFrom = if (pageSummaryResponse.raw().priorResponse?.isRedirect == true) model.title?.displayText else null - createPageModel(pageSummaryResponse, isWatched, hasWatchlistExpiry) - if (OfflineCacheInterceptor.SAVE_HEADER_SAVE == pageSummaryResponse.headers()[OfflineCacheInterceptor.SAVE_HEADER]) { - showPageOfflineMessage(pageSummaryResponse.headers().getInstant("date")) - } - - val categoryList = (categoriesResponse.query ?: watchedResponse.query)?.firstPage()?.categories?.map { category -> - Category(title = category.title, lang = title.wikiSite.languageCode) - }.orEmpty() - if (categoryList.isNotEmpty()) { - AppDatabase.instance.categoryDao().upsertAll(categoryList) - } - - if (delayLoadHtml) { - bridge.resetHtml(title) - } - fragment.onPageMetadataLoaded(redirectedFrom) - - 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() - } - } - } -} 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 new file mode 100644 index 00000000000..6146ebe01fc --- /dev/null +++ b/app/src/main/java/org/wikipedia/page/pageload/PageDataFetcher.kt @@ -0,0 +1,85 @@ +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.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 + +class PageDataFetcher { + + 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) + ) + } + + 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(isWatched = false, hasWatchlistExpiry = false, myQueryResponse = response) + } else { + WatchStatus(isWatched = false, hasWatchlistExpiry = false, myQueryResponse = MwQueryResponse()) + } + 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): Resource { + try { + 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) + } + } + + private suspend fun isInReadingList(title: PageTitle): Boolean { + return AppDatabase.instance.readingListPageDao().findPageInAnyList(title) != null + } +} + +data class WatchStatus( + val isWatched: Boolean = false, + val hasWatchlistExpiry: Boolean = false, + val myQueryResponse: MwQueryResponse = 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() +} 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..9b9019c81d7 --- /dev/null +++ b/app/src/main/java/org/wikipedia/page/pageload/PageLoad.kt @@ -0,0 +1,41 @@ +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 + +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 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() + object FromBackStack : LoadType() +} + +sealed class PageLoadUiState { + data class SpecialPage(val request: PageLoadRequest) : PageLoadUiState() + data class LoadingPrep(val isRefresh: Boolean = false, val title: PageTitle? = null) : PageLoadUiState() + data class Success( + val result: PageSummary? = null, + val title: PageTitle, + val stagedScrollY: Int = 0, + 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 new file mode 100644 index 00000000000..dfc785faf4d --- /dev/null +++ b/app/src/main/java/org/wikipedia/page/pageload/PageLoadViewModel.kt @@ -0,0 +1,409 @@ +package org.wikipedia.page.pageload + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.CoroutineExceptionHandler +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +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.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 +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.Resource +import org.wikipedia.util.UiState +import org.wikipedia.util.log.L +import retrofit2.Response + +class PageLoadViewModel : ViewModel() { + private val app = WikipediaApp.instance + + private val _pageLoadUiState = MutableStateFlow(PageLoadUiState.LoadingPrep()) + val pageLoadUiState = _pageLoadUiState.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() + private val pageDataFetcher = PageDataFetcher() + val foregroundTabPosition get() = app.tabList.size + val backgroundTabPosition get() = 0.coerceAtLeast(foregroundTabPosition - 1) + + fun loadPage(request: PageLoadRequest, webScrollY: Int = 0) { + 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)) + } + loadPageData(request) + } + LoadType.NewBackgroundTab -> loadInNewBackgroundTab(request) + LoadType.NewForegroundTab -> loadInNewForegroundTab(request, webScrollY) + } + } + + fun setTab(tab: Tab): Boolean { + val isDifferent = tab != currentTab + currentTab = tab + return isDifferent + } + + 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 + _currentPageViewModel.value.title?.let { + item.title.description = it.description + item.title.thumbUrl = it.thumbUrl + } + } + + fun updateTabListToPreventZHVariantIssue(title: PageTitle?) { + if (title == null) return + 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( + pageModel: PageViewModel, + pageSummary: PageSummary, + sendEvent: (HistoryEntry) -> Unit + ) { + val title = pageModel.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. + sendEvent(entry) + } + } + } + + 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 fun loadInExistingTab(request: PageLoadRequest) { + val selectedTabPosition = selectedTabPosition(request.title) + if (selectedTabPosition == -1) { + loadPageData(request) + return + } + switchToExistingTab(selectedTabPosition) + } + + 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) { + loadFromBackStack() + } else if (!request.title.fragment.isNullOrEmpty()) { + _animateType.tryEmit(Animate(sectionAnchor = request.title.fragment)) + } + return + } + 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)) + } + loadPageData(request) + } + + private fun loadInNewForegroundTab(request: PageLoadRequest, webScrollY: Int) { + createOrReuseExistingTab(request, isForeground = true) + loadFromBackStack() + } + + private fun loadInNewBackgroundTab(request: PageLoadRequest) { + val isForeground = app.tabCount == 0 + if (isForeground) { + createOrReuseExistingTab(request, isForeground = true) + loadFromBackStack() + } else { + createOrReuseExistingTab(request, isForeground = false) + _animateType.tryEmit(Animate(animateButtons = 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") + } + + fun loadPageData(request: PageLoadRequest) { + _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(CoroutineExceptionHandler { _, throwable -> + handleError(throwable) + }) { + val cacheControl = if (request.options.isRefresh) "no-cache" else "default" + val pageSummary = pageDataFetcher.fetchPageSummary(request.title, cacheControl) + handlePageSummary(pageSummary, request) + + 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) + } + } + } + + 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 fun handleWatchStatus(watchStatus: Resource, title: PageTitle) { + when (watchStatus) { + is Resource.Success -> { + _watchResponseState.value = UiState.Success(watchStatus.data) + 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) + .getInfoByPageIdsOrTitles(null, title.prefixedText) + .query?.firstPage()?.let { page -> + app.tabList.find { it.backStackPositionTitle == title } + ?.backStackPositionTitle?.apply { + thumbUrl = page.thumbUrl() + description = page.description + } + } + } + } + + 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() + 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) + } + } else { + setTab(currentTab) + currentTab.backStack.add(PageBackStackItem(request.title, request.entry)) + } + } + + 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, response: Response): PageViewModel { + return PageViewModel().apply { + curEntry = request.entry + forceNetwork = request.options.isRefresh + readingListPage = AppDatabase.instance.readingListPageDao().findPageInAnyList(request.title) + val pageSummary = response.body() + page = pageSummary?.toPage(request.title) + title = page?.title + + 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 + } + } + } + + private fun handleError(throwable: Throwable) { + L.e(throwable) + _pageLoadUiState.value = PageLoadUiState.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 + } + } + + data class Animate( + val animateButtons: Boolean = false, + val sectionAnchor: String? = null + ) +}