Merge pull request #297 from omnivore-app/article-mutations
Separate the mutations out of lower level components
This commit is contained in:
@ -8,7 +8,7 @@ struct WebReader: UIViewRepresentable {
|
||||
let articleContent: ArticleContent
|
||||
let item: FeedItem
|
||||
let openLinkAction: (URL) -> Void
|
||||
let webViewActionHandler: (WKScriptMessage) -> Void
|
||||
let webViewActionHandler: (WKScriptMessage, WKScriptMessageReplyHandler?) -> Void
|
||||
let navBarVisibilityRatioUpdater: (Double) -> Void
|
||||
let authToken: String
|
||||
let appEnv: AppEnvironment
|
||||
@ -60,6 +60,8 @@ struct WebReader: UIViewRepresentable {
|
||||
|
||||
webView.configuration.userContentController.add(webView, name: "viewerAction")
|
||||
|
||||
webView.configuration.userContentController.addScriptMessageHandler(context.coordinator, contentWorld: .page, name: "articleAction")
|
||||
|
||||
context.coordinator.linkHandler = openLinkAction
|
||||
context.coordinator.webViewActionHandler = webViewActionHandler
|
||||
context.coordinator.updateNavBarVisibilityRatio = navBarVisibilityRatioUpdater
|
||||
|
||||
@ -5,36 +5,6 @@ import SwiftUI
|
||||
import Views
|
||||
import WebKit
|
||||
|
||||
struct SafariWebLink: Identifiable {
|
||||
let id: UUID
|
||||
let url: URL
|
||||
}
|
||||
|
||||
// TODO: load highlights
|
||||
final class WebReaderViewModel: ObservableObject {
|
||||
@Published var isLoading = false
|
||||
@Published var articleContent: ArticleContent?
|
||||
|
||||
var subscriptions = Set<AnyCancellable>()
|
||||
|
||||
func loadContent(dataService: DataService, slug: String) {
|
||||
isLoading = true
|
||||
|
||||
guard let viewer = dataService.currentViewer else { return }
|
||||
|
||||
dataService.articleContentPublisher(username: viewer.username, slug: slug).sink(
|
||||
receiveCompletion: { [weak self] completion in
|
||||
guard case .failure = completion else { return }
|
||||
self?.isLoading = false
|
||||
},
|
||||
receiveValue: { [weak self] articleContent in
|
||||
self?.articleContent = articleContent
|
||||
}
|
||||
)
|
||||
.store(in: &subscriptions)
|
||||
}
|
||||
}
|
||||
|
||||
struct WebReaderContainerView: View {
|
||||
let item: FeedItem
|
||||
let homeFeedViewModel: HomeFeedViewModel
|
||||
@ -62,7 +32,24 @@ struct WebReaderContainerView: View {
|
||||
)
|
||||
}
|
||||
|
||||
func webViewActionHandler(message: WKScriptMessage) {
|
||||
func webViewActionHandler(message: WKScriptMessage, replyHandler: WKScriptMessageReplyHandler?) {
|
||||
if message.name == WebViewAction.readingProgressUpdate.rawValue {
|
||||
let messageBody = message.body as? [String: Double]
|
||||
|
||||
if let messageBody = messageBody, let progress = messageBody["progress"] {
|
||||
homeFeedViewModel.updateProgress(itemID: item.id, progress: Double(progress))
|
||||
}
|
||||
}
|
||||
|
||||
if let replyHandler = replyHandler {
|
||||
viewModel.webViewActionWithReplyHandler(
|
||||
message: message,
|
||||
replyHandler: replyHandler,
|
||||
dataService: dataService
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
if message.name == WebViewAction.highlightAction.rawValue {
|
||||
handleHighlightAction(message: message)
|
||||
}
|
||||
|
||||
@ -46,8 +46,8 @@ struct WebReaderContent {
|
||||
}
|
||||
|
||||
window.omnivoreArticle = {
|
||||
id: "test",
|
||||
linkId: "test",
|
||||
id: "\(item.id)",
|
||||
linkId: "\(item.id)",
|
||||
slug: "test-slug",
|
||||
createdAt: new Date().toISOString(),
|
||||
savedAt: new Date().toISOString(),
|
||||
|
||||
@ -7,8 +7,10 @@ import Utils
|
||||
import Views
|
||||
import WebKit
|
||||
|
||||
typealias WKScriptMessageReplyHandler = (Any?, String?) -> Void
|
||||
|
||||
final class WebReaderCoordinator: NSObject {
|
||||
var webViewActionHandler: (WKScriptMessage) -> Void = { _ in }
|
||||
var webViewActionHandler: (WKScriptMessage, WKScriptMessageReplyHandler?) -> Void = { _, _ in }
|
||||
var linkHandler: (URL) -> Void = { _ in }
|
||||
var needsReload = true
|
||||
var lastSavedAnnotationID: UUID?
|
||||
@ -34,7 +36,17 @@ final class WebReaderCoordinator: NSObject {
|
||||
|
||||
extension WebReaderCoordinator: WKScriptMessageHandler {
|
||||
func userContentController(_: WKUserContentController, didReceive message: WKScriptMessage) {
|
||||
webViewActionHandler(message)
|
||||
webViewActionHandler(message, nil)
|
||||
}
|
||||
}
|
||||
|
||||
extension WebReaderCoordinator: WKScriptMessageHandlerWithReply {
|
||||
func userContentController(
|
||||
_: WKUserContentController,
|
||||
didReceive message: WKScriptMessage,
|
||||
replyHandler: @escaping (Any?, String?) -> Void
|
||||
) {
|
||||
webViewActionHandler(message, replyHandler)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -0,0 +1,165 @@
|
||||
import Combine
|
||||
import Models
|
||||
import Services
|
||||
import SwiftUI
|
||||
import WebKit
|
||||
|
||||
struct SafariWebLink: Identifiable {
|
||||
let id: UUID
|
||||
let url: URL
|
||||
}
|
||||
|
||||
final class WebReaderViewModel: ObservableObject {
|
||||
@Published var isLoading = false
|
||||
@Published var articleContent: ArticleContent?
|
||||
|
||||
var subscriptions = Set<AnyCancellable>()
|
||||
|
||||
func loadContent(dataService: DataService, slug: String) {
|
||||
isLoading = true
|
||||
|
||||
guard let viewer = dataService.currentViewer else { return }
|
||||
|
||||
dataService.articleContentPublisher(username: viewer.username, slug: slug).sink(
|
||||
receiveCompletion: { [weak self] completion in
|
||||
guard case .failure = completion else { return }
|
||||
self?.isLoading = false
|
||||
},
|
||||
receiveValue: { [weak self] articleContent in
|
||||
self?.articleContent = articleContent
|
||||
}
|
||||
)
|
||||
.store(in: &subscriptions)
|
||||
}
|
||||
|
||||
func createHighlight(
|
||||
messageBody: [String: Any],
|
||||
replyHandler: @escaping WKScriptMessageReplyHandler,
|
||||
dataService: DataService
|
||||
) {
|
||||
dataService.createHighlightPublisher(
|
||||
shortId: messageBody["shortId"] as? String ?? "",
|
||||
highlightID: messageBody["id"] as? String ?? "",
|
||||
quote: messageBody["quote"] as? String ?? "",
|
||||
patch: messageBody["patch"] as? String ?? "",
|
||||
articleId: messageBody["articleId"] as? String ?? ""
|
||||
)
|
||||
.sink { completion in
|
||||
guard case .failure = completion else { return }
|
||||
replyHandler(["result": false], nil)
|
||||
} receiveValue: { _ in
|
||||
replyHandler(["result": true], nil)
|
||||
}
|
||||
.store(in: &subscriptions)
|
||||
}
|
||||
|
||||
func deleteHighlight(
|
||||
messageBody: [String: Any],
|
||||
replyHandler: @escaping WKScriptMessageReplyHandler,
|
||||
dataService: DataService
|
||||
) {
|
||||
dataService.deleteHighlightPublisher(
|
||||
highlightId: messageBody["highlightId"] as? String ?? ""
|
||||
)
|
||||
.sink { completion in
|
||||
guard case .failure = completion else { return }
|
||||
replyHandler(["result": false], nil)
|
||||
} receiveValue: { _ in
|
||||
replyHandler(["result": true], nil)
|
||||
}
|
||||
.store(in: &subscriptions)
|
||||
}
|
||||
|
||||
func mergeHighlight(
|
||||
messageBody: [String: Any],
|
||||
replyHandler: @escaping WKScriptMessageReplyHandler,
|
||||
dataService: DataService
|
||||
) {
|
||||
dataService.mergeHighlightPublisher(
|
||||
shortId: messageBody["shortId"] as? String ?? "",
|
||||
highlightID: messageBody["id"] as? String ?? "",
|
||||
quote: messageBody["quote"] as? String ?? "",
|
||||
patch: messageBody["patch"] as? String ?? "",
|
||||
articleId: messageBody["articleId"] as? String ?? "",
|
||||
overlapHighlightIdList: messageBody["overlapHighlightIdList"] as? [String] ?? []
|
||||
)
|
||||
.sink { completion in
|
||||
guard case .failure = completion else { return }
|
||||
replyHandler(["result": false], nil)
|
||||
} receiveValue: { _ in
|
||||
replyHandler(["result": true], nil)
|
||||
}
|
||||
.store(in: &subscriptions)
|
||||
}
|
||||
|
||||
func updateHighlight(
|
||||
messageBody: [String: Any],
|
||||
replyHandler: @escaping WKScriptMessageReplyHandler,
|
||||
dataService: DataService
|
||||
) {
|
||||
dataService.updateHighlightAttributesPublisher(
|
||||
highlightID: messageBody["highlightId"] as? String ?? "",
|
||||
annotation: messageBody["annotation"] as? String ?? "",
|
||||
sharedAt: nil
|
||||
)
|
||||
.sink { completion in
|
||||
guard case .failure = completion else { return }
|
||||
replyHandler(["result": false], nil)
|
||||
} receiveValue: { _ in
|
||||
replyHandler(["result": true], nil)
|
||||
}
|
||||
.store(in: &subscriptions)
|
||||
}
|
||||
|
||||
func updateReadingProgress(
|
||||
messageBody: [String: Any],
|
||||
replyHandler: @escaping WKScriptMessageReplyHandler,
|
||||
dataService: DataService
|
||||
) {
|
||||
let itemID = messageBody["id"] as? String
|
||||
let readingProgress = messageBody["readingProgressPercent"] as? Double
|
||||
let anchorIndex = messageBody["readingProgressAnchorIndex"] as? Int
|
||||
|
||||
guard let itemID = itemID, let readingProgress = readingProgress, let anchorIndex = anchorIndex else {
|
||||
replyHandler(["result": false], nil)
|
||||
return
|
||||
}
|
||||
|
||||
dataService.updateArticleReadingProgressPublisher(
|
||||
itemID: itemID,
|
||||
readingProgress: readingProgress,
|
||||
anchorIndex: anchorIndex
|
||||
)
|
||||
.sink { completion in
|
||||
guard case .failure = completion else { return }
|
||||
replyHandler(["result": false], nil)
|
||||
} receiveValue: { _ in
|
||||
replyHandler(["result": true], nil)
|
||||
}
|
||||
.store(in: &subscriptions)
|
||||
}
|
||||
|
||||
func webViewActionWithReplyHandler(
|
||||
message: WKScriptMessage,
|
||||
replyHandler: @escaping WKScriptMessageReplyHandler,
|
||||
dataService: DataService
|
||||
) {
|
||||
guard let messageBody = message.body as? [String: Any] else { return }
|
||||
guard let actionID = messageBody["actionID"] as? String else { return }
|
||||
|
||||
switch actionID {
|
||||
case "deleteHighlight":
|
||||
deleteHighlight(messageBody: messageBody, replyHandler: replyHandler, dataService: dataService)
|
||||
case "createHighlight":
|
||||
createHighlight(messageBody: messageBody, replyHandler: replyHandler, dataService: dataService)
|
||||
case "mergeHighlight":
|
||||
mergeHighlight(messageBody: messageBody, replyHandler: replyHandler, dataService: dataService)
|
||||
case "updateHighlight":
|
||||
updateHighlight(messageBody: messageBody, replyHandler: replyHandler, dataService: dataService)
|
||||
case "articleReadingProgress":
|
||||
updateReadingProgress(messageBody: messageBody, replyHandler: replyHandler, dataService: dataService)
|
||||
default:
|
||||
replyHandler(nil, "Unknown actionID: \(actionID)")
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -10,8 +10,6 @@ public struct ArticleContent {
|
||||
) {
|
||||
self.htmlContent = htmlContent
|
||||
self.highlights = highlights
|
||||
|
||||
print(highlightsJSONString)
|
||||
}
|
||||
|
||||
public var highlightsJSONString: String {
|
||||
|
||||
@ -9,7 +9,8 @@ public extension DataService {
|
||||
highlightID: String,
|
||||
quote: String,
|
||||
patch: String,
|
||||
articleId: String
|
||||
articleId: String,
|
||||
annotation: String? = nil
|
||||
) -> AnyPublisher<String, BasicError> {
|
||||
enum MutationResult {
|
||||
case saved(id: String)
|
||||
@ -28,7 +29,12 @@ public extension DataService {
|
||||
let mutation = Selection.Mutation {
|
||||
try $0.createHighlight(
|
||||
input: InputObjects.CreateHighlightInput(
|
||||
id: highlightID, shortId: shortId, articleId: articleId, patch: patch, quote: quote
|
||||
id: highlightID,
|
||||
shortId: shortId,
|
||||
articleId: articleId,
|
||||
patch: patch,
|
||||
quote: quote,
|
||||
annotation: OptionalArgument(annotation)
|
||||
),
|
||||
selection: selection
|
||||
)
|
||||
|
||||
File diff suppressed because one or more lines are too long
@ -6,8 +6,17 @@ import { applyStoredTheme } from '@omnivore/web/lib/themeUpdater'
|
||||
import '@omnivore/web/styles/globals.css'
|
||||
import '@omnivore/web/styles/articleInnerStyling.css'
|
||||
|
||||
const mutation = async (name, input) => {
|
||||
const result = await window?.webkit?.messageHandlers.articleAction?.postMessage({
|
||||
actionID: name,
|
||||
...input
|
||||
})
|
||||
console.log('action result', result, result.result)
|
||||
return result.result
|
||||
}
|
||||
|
||||
const App = () => {
|
||||
applyStoredTheme(false) // false to skip serevr sync
|
||||
applyStoredTheme(false)
|
||||
|
||||
return (
|
||||
<>
|
||||
@ -32,6 +41,13 @@ const App = () => {
|
||||
highlightsBaseURL="https://example.com"
|
||||
fontSize={window.fontSize ?? 18}
|
||||
margin={0}
|
||||
articleMutations={{
|
||||
createHighlightMutation: (input) => mutation('createHighlight', input),
|
||||
deleteHighlightMutation: (highlightId) => mutation('deleteHighlight', { highlightId }),
|
||||
mergeHighlightMutation: (input) => mutation('mergeHighlight', input),
|
||||
updateHighlightMutation: (input) => mutation('updateHighlight', input),
|
||||
articleReadingProgressMutation: (input) => mutation('articleReadingProgress', input),
|
||||
}}
|
||||
/>
|
||||
</VStack>
|
||||
</Box>
|
||||
|
||||
@ -12,11 +12,11 @@ import {
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react'
|
||||
import { articleReadingProgressMutation } from '../../../lib/networking/mutations/articleReadingProgressMutation'
|
||||
import { Tweet } from 'react-twitter-widgets'
|
||||
import { render } from 'react-dom'
|
||||
import { isDarkTheme } from '../../../lib/themeUpdater'
|
||||
import { debounce } from 'lodash'
|
||||
import { ArticleMutations } from '../../../lib/articleActions'
|
||||
|
||||
export type ArticleProps = {
|
||||
articleId: string
|
||||
@ -24,6 +24,7 @@ export type ArticleProps = {
|
||||
initialAnchorIndex: number
|
||||
initialReadingProgress?: number
|
||||
scrollElementRef: MutableRefObject<HTMLDivElement | null>
|
||||
articleMutations: ArticleMutations
|
||||
}
|
||||
|
||||
export function Article(props: ArticleProps): JSX.Element {
|
||||
@ -64,7 +65,7 @@ export function Article(props: ArticleProps): JSX.Element {
|
||||
useEffect(() => {
|
||||
;(async () => {
|
||||
if (!readingProgress) return
|
||||
await articleReadingProgressMutation({
|
||||
await props.articleMutations.articleReadingProgressMutation({
|
||||
id: props.articleId,
|
||||
// round reading progress to 100% if more than that
|
||||
readingProgressPercent: readingProgress > 100 ? 100 : readingProgress,
|
||||
|
||||
@ -19,10 +19,12 @@ import { updateThemeLocally } from '../../../lib/themeUpdater'
|
||||
import { EditLabelsModal } from './EditLabelsModal'
|
||||
import Script from 'next/script'
|
||||
import { useRouter } from 'next/router'
|
||||
import { ArticleMutations } from '../../../lib/articleActions'
|
||||
|
||||
type ArticleContainerProps = {
|
||||
viewerUsername: string
|
||||
article: ArticleAttributes
|
||||
articleMutations: ArticleMutations
|
||||
scrollElementRef: MutableRefObject<HTMLDivElement | null>
|
||||
isAppleAppEmbed: boolean
|
||||
highlightBarDisabled: boolean
|
||||
@ -187,6 +189,7 @@ export function ArticleContainer(props: ArticleContainerProps): JSX.Element {
|
||||
content={props.article.content}
|
||||
initialAnchorIndex={props.article.readingProgressAnchorIndex}
|
||||
scrollElementRef={props.scrollElementRef}
|
||||
articleMutations={props.articleMutations}
|
||||
/>
|
||||
<Button
|
||||
style="ghost"
|
||||
@ -216,6 +219,7 @@ export function ArticleContainer(props: ArticleContainerProps): JSX.Element {
|
||||
showNotesSidebar={showNotesSidebar}
|
||||
highlightsBaseURL={props.highlightsBaseURL}
|
||||
setShowNotesSidebar={setShowNotesSidebar}
|
||||
articleMutations={props.articleMutations}
|
||||
/>
|
||||
{showReportIssuesModal ? (
|
||||
<ReportIssuesModal
|
||||
|
||||
@ -3,7 +3,6 @@ import { makeHighlightStartEndOffset } from '../../../lib/highlights/highlightGe
|
||||
import type { HighlightLocation } from '../../../lib/highlights/highlightGenerator'
|
||||
import { useSelection } from '../../../lib/highlights/useSelection'
|
||||
import type { Highlight } from '../../../lib/networking/fragments/highlightFragment'
|
||||
import { deleteHighlightMutation } from '../../../lib/networking/mutations/deleteHighlightMutation'
|
||||
import { shareHighlightToFeedMutation } from '../../../lib/networking/mutations/shareHighlightToFeedMutation'
|
||||
import { shareHighlightCommentMutation } from '../../../lib/networking/mutations/updateShareHighlightCommentMutation'
|
||||
import {
|
||||
@ -18,9 +17,9 @@ import { HighlightNoteModal } from './HighlightNoteModal'
|
||||
import { ShareHighlightModal } from './ShareHighlightModal'
|
||||
import { HighlightPostToFeedModal } from './HighlightPostToFeedModal'
|
||||
import { HighlightsModal } from './HighlightsModal'
|
||||
import { updateHighlightMutation } from '../../../lib/networking/mutations/updateHighlightMutation'
|
||||
import { useCanShareNative } from '../../../lib/hooks/useCanShareNative'
|
||||
import toast from 'react-hot-toast'
|
||||
import { ArticleMutations } from '../../../lib/articleActions'
|
||||
|
||||
type HighlightsLayerProps = {
|
||||
viewerUsername: string
|
||||
@ -33,6 +32,7 @@ type HighlightsLayerProps = {
|
||||
showNotesSidebar: boolean
|
||||
highlightsBaseURL: string
|
||||
setShowNotesSidebar: React.Dispatch<React.SetStateAction<boolean>>
|
||||
articleMutations: ArticleMutations
|
||||
}
|
||||
|
||||
type HighlightModalAction = 'none' | 'addComment' | 'postToFeed' | 'share'
|
||||
@ -88,7 +88,7 @@ export function HighlightsLayer(props: HighlightsLayerProps): JSX.Element {
|
||||
const highlightId = id || focusedHighlight?.id
|
||||
if (!highlightId) return
|
||||
|
||||
const didDeleteHighlight = await deleteHighlightMutation(highlightId)
|
||||
const didDeleteHighlight = await props.articleMutations.deleteHighlightMutation(highlightId)
|
||||
|
||||
if (didDeleteHighlight) {
|
||||
removeHighlights(
|
||||
@ -191,7 +191,7 @@ export function HighlightsLayer(props: HighlightsLayerProps): JSX.Element {
|
||||
existingHighlights: highlights,
|
||||
highlightStartEndOffsets: highlightLocations,
|
||||
annotation: note,
|
||||
})
|
||||
}, props.articleMutations)
|
||||
|
||||
if (!result.highlights || result.highlights.length == 0) {
|
||||
// TODO: show an error message
|
||||
@ -407,7 +407,7 @@ export function HighlightsLayer(props: HighlightsLayerProps): JSX.Element {
|
||||
if (focusedHighlight) {
|
||||
const annotation = event.annotation ?? ''
|
||||
|
||||
const result = await updateHighlightMutation({
|
||||
const result = await props.articleMutations.updateHighlightMutation({
|
||||
highlightId: focusedHighlight.id,
|
||||
annotation: event.annotation ?? '',
|
||||
})
|
||||
|
||||
14
packages/web/lib/articleActions.tsx
Normal file
14
packages/web/lib/articleActions.tsx
Normal file
@ -0,0 +1,14 @@
|
||||
import { Highlight } from "./networking/fragments/highlightFragment"
|
||||
import { ArticleReadingProgressMutationInput } from "./networking/mutations/articleReadingProgressMutation"
|
||||
import { CreateHighlightInput } from "./networking/mutations/createHighlightMutation"
|
||||
import { MergeHighlightInput, MergeHighlightOutput } from "./networking/mutations/mergeHighlightMutation"
|
||||
import { UpdateHighlightInput } from "./networking/mutations/updateHighlightMutation"
|
||||
|
||||
|
||||
export type ArticleMutations = {
|
||||
createHighlightMutation: (input: CreateHighlightInput) => Promise<Highlight | undefined>
|
||||
deleteHighlightMutation: (highlightId: string) => Promise<boolean>
|
||||
mergeHighlightMutation: (input: MergeHighlightInput) => Promise<MergeHighlightOutput | undefined>
|
||||
updateHighlightMutation: (input: UpdateHighlightInput) => Promise<string | undefined>
|
||||
articleReadingProgressMutation: (input: ArticleReadingProgressMutationInput) => Promise<boolean>
|
||||
}
|
||||
@ -9,9 +9,8 @@ import {
|
||||
import type { HighlightLocation } from './highlightGenerator'
|
||||
import { extendRangeToWordBoundaries } from './normalizeHighlightRange'
|
||||
import type { Highlight } from '../networking/fragments/highlightFragment'
|
||||
import { createHighlightMutation } from '../networking/mutations/createHighlightMutation'
|
||||
import { removeHighlights } from './deleteHighlight'
|
||||
import { mergeHighlightMutation } from '../networking/mutations/mergeHighlightMutation'
|
||||
import { ArticleMutations } from '../articleActions'
|
||||
|
||||
type CreateHighlightInput = {
|
||||
selection: SelectionAttributes
|
||||
@ -28,7 +27,8 @@ type CreateHighlightOutput = {
|
||||
}
|
||||
|
||||
export async function createHighlight(
|
||||
input: CreateHighlightInput
|
||||
input: CreateHighlightInput,
|
||||
articleMutations: ArticleMutations
|
||||
): Promise<CreateHighlightOutput> {
|
||||
|
||||
if (!input.selection.selection) {
|
||||
@ -89,7 +89,7 @@ export async function createHighlight(
|
||||
let keptHighlights = input.existingHighlights
|
||||
|
||||
if (shouldMerge) {
|
||||
const result = await mergeHighlightMutation({
|
||||
const result = await articleMutations.mergeHighlightMutation({
|
||||
...newHighlightAttributes,
|
||||
overlapHighlightIdList: input.selection.overlapHighlights,
|
||||
})
|
||||
@ -99,7 +99,7 @@ export async function createHighlight(
|
||||
($0) => !input.selection.overlapHighlights.includes($0.id)
|
||||
)
|
||||
} else {
|
||||
highlight = await createHighlightMutation(newHighlightAttributes)
|
||||
highlight = await articleMutations.createHighlightMutation(newHighlightAttributes)
|
||||
}
|
||||
|
||||
if (highlight) {
|
||||
|
||||
@ -2,7 +2,11 @@ import { diff_match_patch as DiffMatchPatch } from 'diff-match-patch'
|
||||
import { RefObject } from 'react'
|
||||
import type { Highlight } from '../networking/fragments/highlightFragment'
|
||||
import { interpolationSearch } from './interpolationSearch'
|
||||
import { highlightIdAttribute, highlightNoteIdAttribute } from './highlightHelpers'
|
||||
import {
|
||||
highlightIdAttribute,
|
||||
highlightNoteIdAttribute,
|
||||
noteImage,
|
||||
} from './highlightHelpers'
|
||||
|
||||
const highlightTag = 'omnivore_highlight'
|
||||
const highlightClassname = 'highlight'
|
||||
@ -10,7 +14,8 @@ const highlightWithNoteClassName = 'highlight_with_note'
|
||||
const articleContainerId = 'article-container'
|
||||
export const maxHighlightLength = 2000
|
||||
|
||||
const nonParagraphTagsRegEx = /^(a|b|basefont|bdo|big|em|font|i|s|small|span|strike|strong|su[bp]|tt|u|code|mark)$/i
|
||||
const nonParagraphTagsRegEx =
|
||||
/^(a|b|basefont|bdo|big|em|font|i|s|small|span|strike|strong|su[bp]|tt|u|code|mark)$/i
|
||||
const highlightContentRegex = new RegExp(
|
||||
`<${highlightTag}>([\\s\\S]*)<\\/${highlightTag}>`,
|
||||
'i'
|
||||
@ -47,10 +52,8 @@ export type HighlightNodeAttributes = {
|
||||
export function makeHighlightStartEndOffset(
|
||||
highlight: Highlight
|
||||
): HighlightLocation {
|
||||
const {
|
||||
startLocation: highlightTextStart,
|
||||
endLocation: highlightTextEnd,
|
||||
} = nodeAttributesFromHighlight(highlight)
|
||||
const { startLocation: highlightTextStart, endLocation: highlightTextEnd } =
|
||||
nodeAttributesFromHighlight(highlight)
|
||||
return {
|
||||
id: highlight.id,
|
||||
start: highlightTextStart,
|
||||
@ -129,7 +132,9 @@ export function makeHighlightNodeAttributes(
|
||||
}
|
||||
|
||||
const newHighlightSpan = document.createElement('span')
|
||||
newHighlightSpan.className = withNote ? highlightWithNoteClassName : highlightClassname
|
||||
newHighlightSpan.className = withNote
|
||||
? highlightWithNoteClassName
|
||||
: highlightClassname
|
||||
newHighlightSpan.setAttribute(highlightIdAttribute, id)
|
||||
customColor &&
|
||||
newHighlightSpan.setAttribute(
|
||||
@ -146,13 +151,18 @@ export function makeHighlightNodeAttributes(
|
||||
}
|
||||
if (withNote && lastElement) {
|
||||
lastElement.classList.add('last_element')
|
||||
const button = document.createElement('img')
|
||||
button.className = 'highlight_note_button'
|
||||
button.src = '/static/icons/highlight-note-icon.svg'
|
||||
button.alt = 'Add note'
|
||||
button.setAttribute(highlightNoteIdAttribute, id)
|
||||
|
||||
lastElement.appendChild(button)
|
||||
const svg = noteImage()
|
||||
svg.setAttribute(highlightNoteIdAttribute, id)
|
||||
|
||||
const ctr = document.createElement('div')
|
||||
ctr.className = 'highlight_note_button'
|
||||
ctr.appendChild(svg)
|
||||
ctr.setAttribute(highlightNoteIdAttribute, id)
|
||||
ctr.setAttribute('width', '14px')
|
||||
ctr.setAttribute('height', '14px')
|
||||
|
||||
lastElement.appendChild(ctr)
|
||||
}
|
||||
|
||||
return {
|
||||
@ -199,9 +209,8 @@ export function generateDiffPatch(range: Range): string {
|
||||
|
||||
export function wrapHighlightTagAroundRange(range: Range): [number, number] {
|
||||
const patch = generateDiffPatch(range)
|
||||
const { highlightTextStart, highlightTextEnd } = selectionOffsetsFromPatch(
|
||||
patch
|
||||
)
|
||||
const { highlightTextStart, highlightTextEnd } =
|
||||
selectionOffsetsFromPatch(patch)
|
||||
return [highlightTextStart, highlightTextEnd]
|
||||
}
|
||||
|
||||
@ -290,11 +299,7 @@ const selectionOffsetsFromPatch = (
|
||||
}
|
||||
}
|
||||
|
||||
export function getPrefixAndSuffix({
|
||||
patch,
|
||||
}: {
|
||||
patch: string
|
||||
}): {
|
||||
export function getPrefixAndSuffix({ patch }: { patch: string }): {
|
||||
prefix: string
|
||||
suffix: string
|
||||
highlightTextStart: number
|
||||
@ -305,9 +310,8 @@ export function getPrefixAndSuffix({
|
||||
if (!patch) throw new Error('Invalid patch')
|
||||
const { textNodes } = getArticleTextNodes()
|
||||
|
||||
const { highlightTextStart, highlightTextEnd } = selectionOffsetsFromPatch(
|
||||
patch
|
||||
)
|
||||
const { highlightTextStart, highlightTextEnd } =
|
||||
selectionOffsetsFromPatch(patch)
|
||||
// Searching for the starting text node using interpolation search algorithm
|
||||
const textNodeIndex = interpolationSearch(
|
||||
textNodes.map(({ startIndex: startIndex }) => startIndex),
|
||||
@ -360,9 +364,11 @@ const fillHighlight = ({
|
||||
highlightTextStart: number
|
||||
highlightTextEnd: number
|
||||
}): FillNodeResponse => {
|
||||
const { node, startIndex: startIndex, startsParagraph } = textNodes[
|
||||
startingTextNodeIndex
|
||||
]
|
||||
const {
|
||||
node,
|
||||
startIndex: startIndex,
|
||||
startsParagraph,
|
||||
} = textNodes[startingTextNodeIndex]
|
||||
const text = node.nodeValue || ''
|
||||
|
||||
const textBeforeHighlightLenght = highlightTextStart - startIndex
|
||||
|
||||
@ -24,3 +24,23 @@ export function getHighlightNoteButton(highlightId: string): Element[] {
|
||||
document.querySelectorAll(`[${highlightNoteIdAttribute}='${highlightId}']`)
|
||||
)
|
||||
}
|
||||
|
||||
export function noteImage(): SVGSVGElement {
|
||||
const svgURI = 'http://www.w3.org/2000/svg'
|
||||
const svg = document.createElementNS(svgURI, 'svg')
|
||||
svg.setAttribute('viewBox', '0 0 14 14')
|
||||
svg.setAttribute('width', '14')
|
||||
svg.setAttribute('height', '14')
|
||||
svg.setAttribute('fill', 'none')
|
||||
|
||||
const path = document.createElementNS(svgURI, 'path')
|
||||
path.setAttribute(
|
||||
'd',
|
||||
'M1 5.66602C1 3.7804 1 2.83759 1.58579 2.2518C2.17157 1.66602 3.11438 1.66602 5 1.66602H9C10.8856 1.66602 11.8284 1.66602 12.4142 2.2518C13 2.83759 13 3.7804 13 5.66602V7.66601C13 9.55163 13 10.4944 12.4142 11.0802C11.8284 11.666 10.8856 11.666 9 11.666H4.63014C4.49742 11.666 4.43106 11.666 4.36715 11.6701C3.92582 11.6984 3.50632 11.8722 3.17425 12.1642C3.12616 12.2065 3.07924 12.2534 2.98539 12.3473V12.3473C2.75446 12.5782 2.639 12.6937 2.55914 12.7475C1.96522 13.1481 1.15512 12.8125 1.01838 12.1093C1 12.0148 1 11.8515 1 11.5249V5.66602Z'
|
||||
)
|
||||
path.setAttribute('stroke', 'rgba(255, 210, 52, 0.8)')
|
||||
path.setAttribute('stroke-width', '1.8')
|
||||
path.setAttribute('stroke-linejoin', 'round')
|
||||
svg.appendChild(path)
|
||||
return svg
|
||||
}
|
||||
|
||||
@ -136,7 +136,7 @@ export function useSelection(
|
||||
document.removeEventListener('touchend', handleFinishTouch)
|
||||
document.removeEventListener('contextmenu', handleFinishTouch)
|
||||
}
|
||||
}, [JSON.stringify(highlightLocations), handleFinishTouch, disabled])
|
||||
}, [highlightLocations, handleFinishTouch, disabled])
|
||||
|
||||
return [selectionAttributes, setSelectionAttributes]
|
||||
}
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { gql } from 'graphql-request'
|
||||
import { gqlFetcher } from '../networkHelpers'
|
||||
|
||||
type ArticleReadingProgressMutationInput = {
|
||||
export type ArticleReadingProgressMutationInput = {
|
||||
id: string
|
||||
readingProgressPercent: number
|
||||
readingProgressAnchorIndex: number
|
||||
|
||||
@ -2,7 +2,7 @@ import { gql } from 'graphql-request'
|
||||
import { gqlFetcher } from '../networkHelpers'
|
||||
import { Highlight } from './../fragments/highlightFragment'
|
||||
|
||||
type CreateHighlightInput = {
|
||||
export type CreateHighlightInput = {
|
||||
prefix: string
|
||||
suffix: string
|
||||
quote: string
|
||||
|
||||
@ -2,7 +2,7 @@ import { gql } from 'graphql-request'
|
||||
import { gqlFetcher } from '../networkHelpers'
|
||||
import { Highlight } from './../fragments/highlightFragment'
|
||||
|
||||
type MergeHighlightInput = {
|
||||
export type MergeHighlightInput = {
|
||||
id: string
|
||||
shortId: string
|
||||
articleId: string
|
||||
@ -14,7 +14,7 @@ type MergeHighlightInput = {
|
||||
overlapHighlightIdList: string[]
|
||||
}
|
||||
|
||||
type MergeHighlightOutput = {
|
||||
export type MergeHighlightOutput = {
|
||||
mergeHighlight: InnerMergeHighlightOutput
|
||||
}
|
||||
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { gql } from 'graphql-request'
|
||||
import { gqlFetcher } from '../networkHelpers'
|
||||
|
||||
type UpdateHighlightInput = {
|
||||
export type UpdateHighlightInput = {
|
||||
highlightId: string
|
||||
annotation?: string
|
||||
sharedAt?: string
|
||||
|
||||
@ -13,6 +13,11 @@ import dynamic from 'next/dynamic'
|
||||
import { useGetUserPreferences } from '../../../lib/networking/queries/useGetUserPreferences'
|
||||
import { webBaseURL } from '../../../lib/appConfig'
|
||||
import { Toaster } from 'react-hot-toast'
|
||||
import { createHighlightMutation } from '../../../lib/networking/mutations/createHighlightMutation'
|
||||
import { deleteHighlightMutation } from '../../../lib/networking/mutations/deleteHighlightMutation'
|
||||
import { mergeHighlightMutation } from '../../../lib/networking/mutations/mergeHighlightMutation'
|
||||
import { articleReadingProgressMutation } from '../../../lib/networking/mutations/articleReadingProgressMutation'
|
||||
import { updateHighlightMutation } from '../../../lib/networking/mutations/updateHighlightMutation'
|
||||
|
||||
const PdfArticleContainerNoSSR = dynamic<PdfArticleContainerProps>(
|
||||
() => import('./../../../components/templates/article/PdfArticleContainer'),
|
||||
@ -70,6 +75,13 @@ export default function Home(): JSX.Element {
|
||||
viewerUsername={viewerData.me?.profile?.username}
|
||||
highlightsBaseURL={`${webBaseURL}/${viewerData.me?.profile?.username}/${slug}/highlights`}
|
||||
fontSize={preferencesData?.fontSize}
|
||||
articleMutations={{
|
||||
createHighlightMutation,
|
||||
deleteHighlightMutation,
|
||||
mergeHighlightMutation,
|
||||
updateHighlightMutation,
|
||||
articleReadingProgressMutation,
|
||||
}}
|
||||
/>
|
||||
</VStack>
|
||||
)}
|
||||
|
||||
@ -7,6 +7,11 @@ import { webBaseURL } from '../../../../lib/appConfig'
|
||||
import { LoadingView } from '../../../../components/patterns/LoadingView'
|
||||
import { cookieValue } from '../../../../lib/cookieHelpers'
|
||||
import { applyStoredTheme } from '../../../../lib/themeUpdater'
|
||||
import { createHighlightMutation } from '../../../../lib/networking/mutations/createHighlightMutation'
|
||||
import { deleteHighlightMutation } from '../../../../lib/networking/mutations/deleteHighlightMutation'
|
||||
import { mergeHighlightMutation } from '../../../../lib/networking/mutations/mergeHighlightMutation'
|
||||
import { updateHighlightMutation } from '../../../../lib/networking/mutations/updateHighlightMutation'
|
||||
import { articleReadingProgressMutation } from '../../../../lib/networking/mutations/articleReadingProgressMutation'
|
||||
|
||||
type AppArticleEmbedContentProps = {
|
||||
slug: string
|
||||
@ -88,6 +93,13 @@ function AppArticleEmbedContent(
|
||||
fontSize={props.fontSize}
|
||||
margin={props.margin}
|
||||
fontFamily={props.fontFamily}
|
||||
articleMutations={{
|
||||
createHighlightMutation,
|
||||
deleteHighlightMutation,
|
||||
mergeHighlightMutation,
|
||||
updateHighlightMutation,
|
||||
articleReadingProgressMutation,
|
||||
}}
|
||||
/>
|
||||
</VStack>
|
||||
</Box>
|
||||
|
||||
Reference in New Issue
Block a user