Merge pull request #211 from omnivore-app/feature/reader-progress-tracking-ios

Reader progress tracking ios
This commit is contained in:
Satindar Dhillon 2022-03-09 18:12:57 -08:00 committed by GitHub
commit b68fd1d487
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 90 additions and 28 deletions

View file

@ -270,6 +270,9 @@ import Views
}
}
.listStyle(PlainListStyle())
.onAppear {
viewModel.sendProgressUpdates = false
}
}
}
@ -347,6 +350,9 @@ import Views
LoadingSection()
}
}
.onAppear {
viewModel.sendProgressUpdates = true
}
}
}

View file

@ -12,6 +12,7 @@ final class HomeFeedViewModel: ObservableObject {
@Published var isLoading = false
@Published var showPushNotificationPrimer = false
var cursor: String?
var sendProgressUpdates = false
// These are used to make sure we handle search result
// responses in the right order
@ -158,4 +159,11 @@ final class HomeFeedViewModel: ObservableObject {
)
.store(in: &subscriptions)
}
func updateProgress(itemID: String, progress: Double) {
guard sendProgressUpdates, let item = items.first(where: { $0.id == itemID }) else { return }
if let index = items.firstIndex(of: item) {
items[index].readingProgress = progress
}
}
}

View file

@ -87,10 +87,12 @@ final class LinkItemDetailViewModel: ObservableObject {
rawAuthCookie: rawAuthCookie
)
newWebAppWrapperViewModel.performActionSubject.sink { action in
newWebAppWrapperViewModel.performActionSubject.sink { [weak self] action in
switch action {
case let .shareHighlight(highlightID):
print("show share modal for highlight with id: \(highlightID)")
case let .updateReadingProgess(progress: progress):
self?.homeFeedViewModel.updateProgress(itemID: self?.item.id ?? "", progress: Double(progress))
}
}
.store(in: &newWebAppWrapperViewModel.subscriptions)

View file

@ -6,6 +6,7 @@ import WebKit
public final class WebAppWrapperViewModel: ObservableObject {
public enum Action {
case shareHighlight(highlightID: String)
case updateReadingProgess(progress: Int)
}
public var subscriptions = Set<AnyCancellable>()
@ -86,6 +87,12 @@ public struct WebAppWrapperView: View {
if message.name == WebViewAction.highlightAction.rawValue {
handleHighlightAction(message: message)
}
if message.name == WebViewAction.readingProgressUpdate.rawValue {
guard let messageBody = message.body as? [String: Double] else { return }
guard let progress = messageBody["progress"] else { return }
viewModel.performActionSubject.send(.updateReadingProgess(progress: Int(progress)))
}
}
private func handleHighlightAction(message: WKScriptMessage) {

View file

@ -4,6 +4,7 @@ import WebKit
/// The names on the javascript side must match for an action to be handled.
enum WebViewAction: String, CaseIterable {
case highlightAction
case readingProgressUpdate
}
final class WebView: WKWebView {

View file

@ -1,7 +1,6 @@
export {}
declare global {
// eslint-disable-next-line functional/prefer-type-literal
interface Window {
webkit?: Webkit
MathJax?: MathJax
@ -24,6 +23,7 @@ declare type Webkit = {
declare type MessageHandlers = {
viewerAction?: WebKitMessageHandler
highlightAction?: WebKitMessageHandler
readingProgressUpdate?: WebKitMessageHandler
}
declare type WebKitMessageHandler = {

View file

@ -1,6 +1,12 @@
import { PageMetaData, PageMetaDataProps } from '../patterns/PageMetaData'
import { Box } from '../elements/LayoutPrimitives'
import { ReactNode, MutableRefObject, useEffect, useContext, useState } from 'react'
import {
ReactNode,
MutableRefObject,
useEffect,
useContext,
useState,
} from 'react'
import { PrimaryHeader } from './../patterns/PrimaryHeader'
import { useGetViewerQuery } from '../../lib/networking/queries/useGetViewerQuery'
import { navigationCommands } from '../../lib/keyboardShortcuts/navigationShortcuts'
@ -11,7 +17,6 @@ import { ConfirmationModal } from '../patterns/ConfirmationModal'
import { KeyboardShortcutListModal } from './KeyboardShortcutListModal'
import { logoutMutation } from '../../lib/networking/mutations/logoutMutation'
type PrimaryLayoutProps = {
children: ReactNode
pageTestId: string
@ -25,7 +30,8 @@ export function PrimaryLayout(props: PrimaryLayoutProps): JSX.Element {
const { viewerData } = useGetViewerQuery()
const router = useRouter()
const [showLogoutConfirmation, setShowLogoutConfirmation] = useState(false)
const [showKeyboardCommandsModal, setShowKeyboardCommandsModal] = useState(false)
const [showKeyboardCommandsModal, setShowKeyboardCommandsModal] =
useState(false)
useKeyboardShortcuts(navigationCommands(router))

View file

@ -36,27 +36,39 @@ export function Article(props: ArticleProps): JSX.Element {
props.initialAnchorIndex
)
const [
shouldScrollToInitialPosition,
setShouldScrollToInitialPosition,
] = useState(true)
const [shouldScrollToInitialPosition, setShouldScrollToInitialPosition] =
useState(true)
const articleContentRef = useRef<HTMLDivElement | null>(null)
useReadingProgressAnchor(articleContentRef, setReadingAnchorIndex)
const debouncedReadingProgress = useDebounce(readingProgress, 1000);
const debouncedReadingAnchorIndex = useDebounce(readingAnchorIndex, 1000);
const debouncedReadingProgress = useDebounce(readingProgress, 1000)
const debouncedReadingAnchorIndex = useDebounce(readingAnchorIndex, 1000)
useEffect(() => {
(async () => {
;(async () => {
await articleReadingProgressMutation({
id: props.articleId,
readingProgressPercent: readingProgress,
readingProgressPercent: debouncedReadingProgress,
readingProgressAnchorIndex: readingAnchorIndex,
})
})()
}, [props.articleId, debouncedReadingProgress, debouncedReadingAnchorIndex])
}, [
props.articleId,
debouncedReadingProgress,
debouncedReadingAnchorIndex,
readingAnchorIndex,
])
// Post message to webkit so apple app embeds get progress updates
useEffect(() => {
if (typeof window?.webkit != 'undefined') {
window.webkit.messageHandlers.readingProgressUpdate?.postMessage({
progress: debouncedReadingProgress,
})
}
}, [readingProgress, debouncedReadingProgress])
const setScrollWatchedElement = useScrollWatcher(
(changeset: ScrollOffsetChangeset) => {
@ -67,6 +79,12 @@ export function Article(props: ArticleProps): JSX.Element {
scrollContainer.scrollHeight
setReadingProgress(newReadingProgress * 100)
} else if (window && window.document.scrollingElement) {
const newReadingProgress =
window.scrollY / window.document.scrollingElement.scrollHeight
const adjustedReadingProgress =
newReadingProgress > 0.92 ? 1 : newReadingProgress
setReadingProgress(adjustedReadingProgress * 100)
}
},
1000
@ -182,16 +200,19 @@ export function Article(props: ArticleProps): JSX.Element {
return (
<>
<link rel="stylesheet" href={`https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.4.0/styles/${highlightTheme}.min.css`} />
<link
rel="stylesheet"
href={`https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.4.0/styles/${highlightTheme}.min.css`}
/>
<Box
ref={articleContentRef}
css={{
maxWidth: '100%',
}}
className="article-inner-css"
dangerouslySetInnerHTML={{
__html: props.content,
}}
ref={articleContentRef}
css={{
maxWidth: '100%',
}}
className="article-inner-css"
dangerouslySetInnerHTML={{
__html: props.content,
}}
/>
</>
)

View file

@ -6,7 +6,11 @@ import type { Highlight } from '../../../lib/networking/fragments/highlightFragm
import { deleteHighlightMutation } from '../../../lib/networking/mutations/deleteHighlightMutation'
import { shareHighlightToFeedMutation } from '../../../lib/networking/mutations/shareHighlightToFeedMutation'
import { shareHighlightCommentMutation } from '../../../lib/networking/mutations/updateShareHighlightCommentMutation'
import { highlightIdAttribute, highlightNoteIdAttribute, SelectionAttributes } from '../../../lib/highlights/highlightHelpers'
import {
highlightIdAttribute,
highlightNoteIdAttribute,
SelectionAttributes,
} from '../../../lib/highlights/highlightHelpers'
import { HighlightBar, HighlightAction } from '../../patterns/HighlightBar'
import { removeHighlights } from '../../../lib/highlights/deleteHighlight'
import { createHighlight } from '../../../lib/highlights/createHighlight'
@ -49,7 +53,9 @@ export function HighlightsLayer(props: HighlightsLayerProps): JSX.Element {
const [highlightModalAction, setHighlightModalAction] =
useState<HighlightActionProps>({ highlightModalAction: 'none' })
const [highlightLocations, setHighlightLocations] = useState<HighlightLocation[]>([])
const [highlightLocations, setHighlightLocations] = useState<
HighlightLocation[]
>([])
const focusedHighlightMousePos = useRef({ pageX: 0, pageY: 0 })
const [focusedHighlight, setFocusedHighlight] = useState<
@ -175,7 +181,10 @@ export function HighlightsLayer(props: HighlightsLayerProps): JSX.Element {
[props.highlightBarDisabled]
)
const createHighlightFromSelection = async (selection: SelectionAttributes, note?: string): Promise<Highlight | undefined> => {
const createHighlightFromSelection = async (
selection: SelectionAttributes,
note?: string
): Promise<Highlight | undefined> => {
const result = await createHighlight({
selection: selection,
articleId: props.articleId,
@ -206,7 +215,10 @@ export function HighlightsLayer(props: HighlightsLayerProps): JSX.Element {
if (!selectionData) {
return
}
const result = await createHighlightFromSelection(selectionData, annotation)
const result = await createHighlightFromSelection(
selectionData,
annotation
)
if (!result) {
toast.error('Error saving highlight', { position: 'bottom-right' })
}

View file

@ -67,7 +67,6 @@ function AppArticleEmbedContent(
if (articleData) {
return (
<Box
ref={scrollRef}
css={{
overflowY: 'auto',
height: '100%',