diff --git a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift index b74242259..04a07ba01 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift @@ -270,6 +270,9 @@ import Views } } .listStyle(PlainListStyle()) + .onAppear { + viewModel.sendProgressUpdates = false + } } } @@ -347,6 +350,9 @@ import Views LoadingSection() } } + .onAppear { + viewModel.sendProgressUpdates = true + } } } diff --git a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift index 1d7c11fed..e0ebabb8d 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift @@ -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 + } + } } diff --git a/apple/OmnivoreKit/Sources/App/Views/LinkItemDetailView.swift b/apple/OmnivoreKit/Sources/App/Views/LinkItemDetailView.swift index 3f0d9cead..36b19523f 100644 --- a/apple/OmnivoreKit/Sources/App/Views/LinkItemDetailView.swift +++ b/apple/OmnivoreKit/Sources/App/Views/LinkItemDetailView.swift @@ -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) diff --git a/apple/OmnivoreKit/Sources/Views/Article/WebAppWrapperView.swift b/apple/OmnivoreKit/Sources/Views/Article/WebAppWrapperView.swift index 6f704e6b1..7c16c83a8 100644 --- a/apple/OmnivoreKit/Sources/Views/Article/WebAppWrapperView.swift +++ b/apple/OmnivoreKit/Sources/Views/Article/WebAppWrapperView.swift @@ -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() @@ -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) { diff --git a/apple/OmnivoreKit/Sources/Views/Article/WebView.swift b/apple/OmnivoreKit/Sources/Views/Article/WebView.swift index dd802b68e..44931a57d 100644 --- a/apple/OmnivoreKit/Sources/Views/Article/WebView.swift +++ b/apple/OmnivoreKit/Sources/Views/Article/WebView.swift @@ -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 { diff --git a/packages/web/additional.d.ts b/packages/web/additional.d.ts index 552a77414..c121550ba 100644 --- a/packages/web/additional.d.ts +++ b/packages/web/additional.d.ts @@ -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 = { diff --git a/packages/web/components/templates/PrimaryLayout.tsx b/packages/web/components/templates/PrimaryLayout.tsx index e1a6128d8..17757cacd 100644 --- a/packages/web/components/templates/PrimaryLayout.tsx +++ b/packages/web/components/templates/PrimaryLayout.tsx @@ -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)) diff --git a/packages/web/components/templates/article/Article.tsx b/packages/web/components/templates/article/Article.tsx index e4b44f47d..2f729a42b 100644 --- a/packages/web/components/templates/article/Article.tsx +++ b/packages/web/components/templates/article/Article.tsx @@ -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(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 ( <> - + ) diff --git a/packages/web/components/templates/article/HighlightsLayer.tsx b/packages/web/components/templates/article/HighlightsLayer.tsx index 5f9938acc..714b5ae3e 100644 --- a/packages/web/components/templates/article/HighlightsLayer.tsx +++ b/packages/web/components/templates/article/HighlightsLayer.tsx @@ -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({ highlightModalAction: 'none' }) - const [highlightLocations, setHighlightLocations] = useState([]) + 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 => { + const createHighlightFromSelection = async ( + selection: SelectionAttributes, + note?: string + ): Promise => { 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' }) } diff --git a/packages/web/pages/app/[username]/[slug]/index.tsx b/packages/web/pages/app/[username]/[slug]/index.tsx index e08eb4c61..7f871ab5f 100644 --- a/packages/web/pages/app/[username]/[slug]/index.tsx +++ b/packages/web/pages/app/[username]/[slug]/index.tsx @@ -67,7 +67,6 @@ function AppArticleEmbedContent( if (articleData) { return (