Commit 2cb7e37a authored by MozLando's avatar MozLando
Browse files

Merge #5125

5125: Closes #4398: Add reader state to browser-state r=pocmo a=csadilek

Closing #4398 to bring in the state required for reader mode so we can port it over soon. Nothing new here, following the existing pattern, and as discussed in https://github.com/mozilla-mobile/android-components/issues/4398#issuecomment-531128780

. 
Co-authored-by: default avatarChristian Sadilek <christian.sadilek@gmail.com>
parents dcf5287e cd83f961
......@@ -27,6 +27,7 @@ import mozilla.components.browser.state.action.ContentAction.UpdateThumbnailActi
import mozilla.components.browser.state.action.ContentAction.UpdateTitleAction
import mozilla.components.browser.state.action.ContentAction.UpdateUrlAction
import mozilla.components.browser.state.action.CustomTabListAction.RemoveCustomTabAction
import mozilla.components.browser.state.action.ReaderAction
import mozilla.components.browser.state.action.TabListAction.AddTabAction
import mozilla.components.browser.state.action.TrackingProtectionAction
import mozilla.components.browser.state.state.CustomTabConfig
......@@ -474,6 +475,7 @@ class Session(
*/
var readerable: Boolean by Delegates.observable(false) { _, _, new ->
notifyObservers { onReaderableStateUpdated(this@Session, new) }
store?.syncDispatch(ReaderAction.UpdateReaderableAction(id, new))
}
/**
......@@ -481,6 +483,7 @@ class Session(
*/
var readerMode: Boolean by Delegates.observable(false) { _, old, new ->
notifyObservers(old, new) { onReaderModeChanged(this@Session, new) }
store?.syncDispatch(ReaderAction.UpdateReaderActiveAction(id, new))
}
/**
......
......@@ -7,6 +7,7 @@ package mozilla.components.browser.session.ext
import mozilla.components.browser.session.Session
import mozilla.components.browser.state.state.ContentState
import mozilla.components.browser.state.state.CustomTabSessionState
import mozilla.components.browser.state.state.ReaderState
import mozilla.components.browser.state.state.SecurityInfoState
import mozilla.components.browser.state.state.TabSessionState
import mozilla.components.browser.state.state.TrackingProtectionState
......@@ -16,7 +17,13 @@ import mozilla.components.browser.state.state.content.FindResultState
* Create a matching [TabSessionState] from a [Session].
*/
fun Session.toTabSessionState(): TabSessionState {
return TabSessionState(id, toContentState(), toTrackingProtectionState(), parentId = parentId)
return TabSessionState(
id,
toContentState(),
toTrackingProtectionState(),
parentId = parentId,
readerState = toReaderState()
)
}
/**
......@@ -40,6 +47,13 @@ private fun Session.toContentState(): ContentState {
)
}
/**
* Creates a matching [ReaderState] from a [Session]
*/
fun Session.toReaderState(): ReaderState {
return ReaderState(readerable, readerMode)
}
/**
* Creates a matching [SecurityInfoState] from a [Session.SecurityInfo]
*/
......
......@@ -875,4 +875,31 @@ class SessionManagerMigrationTest {
assertTrue(session.findResults.isEmpty())
assertTrue(store.state.findTab("session")!!.content.findResults.isEmpty())
}
@Test
fun `Updating reader state`() {
val store = BrowserStore()
val manager = SessionManager(engine = mock(), store = store)
val session = Session(id = "session", initialUrl = "https://www.mozilla.org")
manager.add(session)
assertFalse(session.readerable)
assertFalse(session.readerMode)
assertFalse(store.state.findTab("session")!!.readerState.active)
assertFalse(store.state.findTab("session")!!.readerState.readerable)
session.readerable = true
assertTrue(store.state.findTab("session")!!.readerState.readerable)
assertFalse(store.state.findTab("session")!!.readerState.active)
session.readerMode = true
assertTrue(store.state.findTab("session")!!.readerState.active)
assertTrue(store.state.findTab("session")!!.readerState.readerable)
session.readerable = false
session.readerMode = false
assertFalse(store.state.findTab("session")!!.readerState.active)
assertFalse(store.state.findTab("session")!!.readerState.readerable)
}
}
......@@ -17,6 +17,7 @@ import mozilla.components.browser.session.ext.toSecurityInfoState
import mozilla.components.browser.session.ext.toTabSessionState
import mozilla.components.browser.state.action.ContentAction
import mozilla.components.browser.state.action.CustomTabListAction
import mozilla.components.browser.state.action.ReaderAction
import mozilla.components.browser.state.action.TabListAction
import mozilla.components.browser.state.state.CustomTabConfig
import mozilla.components.browser.state.store.BrowserStore
......@@ -1009,6 +1010,19 @@ class SessionTest {
assertTrue(session.readerable)
}
@Test
fun `action is dispatched when readerable state changes`() {
val store: BrowserStore = mock()
`when`(store.dispatch(any())).thenReturn(mock())
val session = Session("https://www.mozilla.org")
session.store = store
session.readerable = true
verify(store).dispatch(ReaderAction.UpdateReaderableAction(session.id, true))
verifyNoMoreInteractions(store)
}
@Test
fun `observer is notified when reader mode state changes`() {
val observer = mock(Session.Observer::class.java)
......@@ -1026,6 +1040,19 @@ class SessionTest {
assertTrue(session.readerMode)
}
@Test
fun `action is dispatched when reader mode state changes`() {
val store: BrowserStore = mock()
`when`(store.dispatch(any())).thenReturn(mock())
val session = Session("https://www.mozilla.org")
session.store = store
session.readerMode = true
verify(store).dispatch(ReaderAction.UpdateReaderActiveAction(session.id, true))
verifyNoMoreInteractions(store)
}
@Test
fun `observer is notified when recording devices change`() {
val observer = mock(Session.Observer::class.java)
......
......@@ -27,6 +27,19 @@ class SessionExtensionsTest {
assertEquals(tabState.parentId, session.parentId)
}
@Test
fun `toTabSessionState - Can convert tab session with reader state`() {
val session = Session("https://mozilla.org")
session.readerable = true
session.readerMode = true
val tabState = session.toTabSessionState()
assertEquals(tabState.id, session.id)
assertEquals(tabState.content.url, session.url)
assertEquals(tabState.readerState.readerable, true)
assertEquals(tabState.readerState.active, true)
}
@Test
fun `toCustomTabSessionState - Can convert custom tab session`() {
val session = Session("https://mozilla.org")
......
......@@ -9,6 +9,7 @@ import mozilla.components.browser.state.state.BrowserState
import mozilla.components.browser.state.state.ContentState
import mozilla.components.browser.state.state.CustomTabSessionState
import mozilla.components.browser.state.state.EngineState
import mozilla.components.browser.state.state.ReaderState
import mozilla.components.browser.state.state.SecurityInfoState
import mozilla.components.browser.state.state.SessionState
import mozilla.components.browser.state.state.TabSessionState
......@@ -313,3 +314,19 @@ sealed class EngineAction : BrowserAction() {
val engineSessionState: EngineSessionState
) : EngineAction()
}
/**
* [BrowserAction] implementations related to updating the [ReaderState] of a single [TabSessionState] inside
* [BrowserState].
*/
sealed class ReaderAction : BrowserAction() {
/**
* Updates the [ReaderState.readerable] flag.
*/
data class UpdateReaderableAction(val tabId: String, val readerable: Boolean) : ReaderAction()
/**
* Updates the [ReaderState.active] flag.
*/
data class UpdateReaderActiveAction(val tabId: String, val active: Boolean) : ReaderAction()
}
......@@ -8,6 +8,7 @@ import mozilla.components.browser.state.action.BrowserAction
import mozilla.components.browser.state.action.ContentAction
import mozilla.components.browser.state.action.CustomTabListAction
import mozilla.components.browser.state.action.EngineAction
import mozilla.components.browser.state.action.ReaderAction
import mozilla.components.browser.state.action.SystemAction
import mozilla.components.browser.state.action.TabListAction
import mozilla.components.browser.state.action.TrackingProtectionAction
......@@ -27,10 +28,11 @@ internal object BrowserStateReducer {
return when (action) {
is ContentAction -> ContentStateReducer.reduce(state, action)
is CustomTabListAction -> CustomTabListReducer.reduce(state, action)
is EngineAction -> EngineStateReducer.reduce(state, action)
is ReaderAction -> ReaderStateReducer.reduce(state, action)
is SystemAction -> SystemReducer.reduce(state, action)
is TabListAction -> TabListReducer.reduce(state, action)
is TrackingProtectionAction -> TrackingProtectionStateReducer.reduce(state, action)
is EngineAction -> EngineStateReducer.reduce(state, action)
is WebExtensionAction -> WebExtensionReducer.reduce(state, action)
}
}
......
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package mozilla.components.browser.state.reducer
import mozilla.components.browser.state.action.EngineAction
import mozilla.components.browser.state.action.ReaderAction
import mozilla.components.browser.state.state.BrowserState
import mozilla.components.browser.state.state.ReaderState
internal object ReaderStateReducer {
/**
* [EngineAction] Reducer function for modifying a specific [ReaderState]
* of a [BrowserState].
*/
fun reduce(state: BrowserState, action: ReaderAction): BrowserState = when (action) {
is ReaderAction.UpdateReaderableAction -> state.copyWithReaderState(action.tabId) {
it.copy(readerable = action.readerable)
}
is ReaderAction.UpdateReaderActiveAction -> state.copyWithReaderState(action.tabId) {
it.copy(active = action.active)
}
}
}
private fun BrowserState.copyWithReaderState(
tabId: String,
update: (ReaderState) -> ReaderState
): BrowserState {
return copy(
tabs = tabs.map { current ->
if (current.id == tabId) {
current.copy(readerState = update.invoke(current.readerState))
} else {
current
}
}
)
}
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package mozilla.components.browser.state.state
/**
* Value type that represents the state of reader mode/view.
*
* @property readerable whether or not the current page can be transformed to
* be displayed in a reader view.
* @property active whether or not reader view is active.
*/
data class ReaderState(
val readerable: Boolean = false,
val active: Boolean = false
)
......@@ -16,8 +16,9 @@ import java.util.UUID
* parent. The parent tab is usually the tab that initiated opening this
* tab (e.g. the user clicked a link with target="_blank" or selected
* "open in new tab" or a "window.open" was triggered).
* @property extensionState a map of web extension ids and extensions, that contains the overridden
* values for this tab.
* @property extensionState a map of web extension ids to extensions,
* that contains the overridden values for this tab.
* @property readerState the [ReaderState] of this tab.
*/
data class TabSessionState(
override val id: String = UUID.randomUUID().toString(),
......@@ -25,23 +26,27 @@ data class TabSessionState(
override val trackingProtection: TrackingProtectionState = TrackingProtectionState(),
override val engineState: EngineState = EngineState(),
val parentId: String? = null,
override val extensionState: Map<String, WebExtensionState> = emptyMap()
override val extensionState: Map<String, WebExtensionState> = emptyMap(),
val readerState: ReaderState = ReaderState()
) : SessionState
/**
* Convenient function for creating a tab.
*/
@Suppress("LongParameterList")
fun createTab(
url: String,
private: Boolean = false,
id: String = UUID.randomUUID().toString(),
parent: TabSessionState? = null,
extensions: Map<String, WebExtensionState> = emptyMap()
extensions: Map<String, WebExtensionState> = emptyMap(),
readerState: ReaderState = ReaderState()
): TabSessionState {
return TabSessionState(
id = id,
content = ContentState(url, private),
parentId = parent?.id,
extensionState = extensions
extensionState = extensions,
readerState = readerState
)
}
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package mozilla.components.browser.state.action
import mozilla.components.browser.state.selector.findTab
import mozilla.components.browser.state.state.BrowserState
import mozilla.components.browser.state.state.TabSessionState
import mozilla.components.browser.state.state.createTab
import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.support.test.ext.joinBlocking
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
class ReaderActionTest {
private lateinit var tab: TabSessionState
private lateinit var store: BrowserStore
@Before
fun setUp() {
tab = createTab("https://www.mozilla.org")
store = BrowserStore(
initialState = BrowserState(
tabs = listOf(tab)
)
)
}
private fun tabState(): TabSessionState = store.state.findTab(tab.id)!!
private fun readerState() = tabState().readerState
@Test
fun `UpdateReaderableAction - Updates readerable flag of ReaderState`() {
assertFalse(readerState().readerable)
store.dispatch(ReaderAction.UpdateReaderableAction(tabId = tab.id, readerable = true))
.joinBlocking()
assertTrue(readerState().readerable)
store.dispatch(ReaderAction.UpdateReaderableAction(tabId = tab.id, readerable = false))
.joinBlocking()
assertFalse(readerState().readerable)
}
@Test
fun `UpdateReaderActiveAction - Updates active flag of ReaderState`() {
assertFalse(readerState().active)
store.dispatch(ReaderAction.UpdateReaderActiveAction(tabId = tab.id, active = true))
.joinBlocking()
assertTrue(readerState().active)
store.dispatch(ReaderAction.UpdateReaderActiveAction(tabId = tab.id, active = false))
.joinBlocking()
assertFalse(readerState().active)
}
}
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment