Merge pull request #295 from omnivore-app/feature/webview-highlights

Web Reader Highlights [Apple]
This commit is contained in:
Satindar Dhillon 2022-03-22 12:05:17 -07:00 committed by GitHub
commit 42e2beb53d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 462 additions and 441 deletions

View file

@ -57,7 +57,8 @@ public final class PDFViewerViewModel: ObservableObject {
prefix: nil,
suffix: nil,
patch: patch,
annotation: nil
annotation: nil,
createdByMe: true
)
)
@ -94,7 +95,8 @@ public final class PDFViewerViewModel: ObservableObject {
prefix: nil,
suffix: nil,
patch: patch,
annotation: nil
annotation: nil,
createdByMe: true
)
)

View file

@ -148,7 +148,7 @@ struct LinkItemDetailView: View {
if viewModel.item.isPDF {
fixedNavBarReader
} else if FeatureFlag.useLocalWebView {
WebReaderContainerView(item: viewModel.item)
WebReaderContainerView(item: viewModel.item, homeFeedViewModel: viewModel.homeFeedViewModel)
} else {
hidingNavBarReader
}

View file

@ -1,250 +1,11 @@
import Combine
import Models
import Services
import SwiftUI
import UIKit
import Utils
import Views
import WebKit
struct SafariWebLink: Identifiable {
let id: UUID
let url: URL
}
final class WebReaderViewModel: ObservableObject {
@Published var isLoading = false
@Published var htmlContent: String?
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] htmlContent in
self?.htmlContent = htmlContent
}
)
.store(in: &subscriptions)
}
}
struct WebReaderContainerView: View {
let item: FeedItem
@State private var showFontSizePopover = false
@State var showHighlightAnnotationModal = false
@State var safariWebLink: SafariWebLink?
@State private var navBarVisibilityRatio = 1.0
@State private var showDeleteConfirmation = false
@State private var showOverlay = true
@State var increaseFontActionID: UUID?
@State var decreaseFontActionID: UUID?
@EnvironmentObject var dataService: DataService
@EnvironmentObject var authenticator: Authenticator
@Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
@StateObject var viewModel = WebReaderViewModel()
var fontAdjustmentPopoverView: some View {
FontSizeAdjustmentPopoverView(
increaseFontAction: { increaseFontActionID = UUID() },
decreaseFontAction: { decreaseFontActionID = UUID() }
)
}
var navBariOS14: some View {
HStack(alignment: .center) {
Button(
action: { self.presentationMode.wrappedValue.dismiss() },
label: {
Image(systemName: "chevron.backward")
.font(.appTitleTwo)
.foregroundColor(.appGrayTextContrast)
.padding(.horizontal)
}
)
.scaleEffect(navBarVisibilityRatio)
Spacer()
Button(
action: { showFontSizePopover.toggle() },
label: {
Image(systemName: "textformat.size")
.font(.appTitleTwo)
}
)
.padding(.horizontal)
.scaleEffect(navBarVisibilityRatio)
}
.frame(height: readerViewNavBarHeight * navBarVisibilityRatio)
.opacity(navBarVisibilityRatio)
.background(Color.systemBackground)
.onTapGesture {
showFontSizePopover = false
}
}
@available(macOS 12.0, *)
@available(iOS 15.0, *)
var navBar: some View {
HStack(alignment: .center) {
Button(
action: { self.presentationMode.wrappedValue.dismiss() },
label: {
Image(systemName: "chevron.backward")
.font(.appTitleTwo)
.foregroundColor(.appGrayTextContrast)
.padding(.horizontal)
}
)
.scaleEffect(navBarVisibilityRatio)
Spacer()
Button(
action: { showFontSizePopover.toggle() },
label: {
Image(systemName: "textformat.size")
.font(.appTitleTwo)
}
)
.padding(.horizontal)
.scaleEffect(navBarVisibilityRatio)
Menu(
content: {
Group {
Button(
action: {}, // ,viewModel.handleArchiveAction(dataService: dataService) },
label: {
Label(
item.isArchived ? "Unarchive" : "Archive",
systemImage: item.isArchived ? "tray.and.arrow.down.fill" : "archivebox"
)
}
)
Button(
action: { showDeleteConfirmation = true },
label: { Label("Delete", systemImage: "trash") }
)
}
},
label: {
Image.profile
.padding(.horizontal)
.scaleEffect(navBarVisibilityRatio)
}
)
}
.frame(height: readerViewNavBarHeight * navBarVisibilityRatio)
.opacity(navBarVisibilityRatio)
.background(Color.systemBackground)
.onTapGesture {
showFontSizePopover = false
}
.alert("Are you sure?", isPresented: $showDeleteConfirmation) {
Button("Remove Link", role: .destructive) {
// viewModel.handleDeleteAction(dataService: dataService)
}
Button("Cancel", role: .cancel, action: {})
}
}
var body: some View {
ZStack {
if let htmlContent = viewModel.htmlContent {
WebReader(
htmlContent: htmlContent,
item: item,
openLinkAction: { url in print(url) },
webViewActionHandler: { _ in },
navBarVisibilityRatioUpdater: {
if $0 < 1 {
showFontSizePopover = false
}
navBarVisibilityRatio = $0
},
authToken: authenticator.authToken ?? "",
appEnv: dataService.appEnvironment,
increaseFontActionID: $increaseFontActionID,
decreaseFontActionID: $decreaseFontActionID,
annotationSaveTransactionID: nil
)
.overlay(
Group {
if showOverlay {
Color.systemBackground
.transition(.opacity)
.onAppear {
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(250)) {
withAnimation(.linear(duration: 0.2)) {
showOverlay = false
}
}
}
}
}
)
} else {
Color.clear
.contentShape(Rectangle())
.onAppear {
if !viewModel.isLoading {
viewModel.loadContent(dataService: dataService, slug: item.slug)
}
}
}
if showFontSizePopover {
VStack {
Color.clear
.contentShape(Rectangle())
.frame(height: LinkItemDetailView.navBarHeight)
HStack {
Spacer()
fontAdjustmentPopoverView
.background(Color.appButtonBackground)
.cornerRadius(8)
.padding(.trailing, 44)
}
Spacer()
}
.background(
Color.clear
.contentShape(Rectangle())
.onTapGesture {
showFontSizePopover = false
}
)
}
if #available(iOS 15.0, *) {
VStack(spacing: 0) {
navBar
Spacer()
}
.navigationBarHidden(true)
} else {
VStack(spacing: 0) {
navBariOS14
Spacer()
}
.navigationBarHidden(true)
}
}.onDisappear {
// Clear the shared webview content when exiting
WebViewManager.shared().loadHTMLString("<html></html>", baseURL: nil)
}
.navigationBarHidden(true)
}
}
// TODO: implement things WebAppWrapperView does
struct WebReader: UIViewRepresentable {
let htmlContent: String
let articleContent: ArticleContent
let item: FeedItem
let openLinkAction: (URL) -> Void
let webViewActionHandler: (WKScriptMessage) -> Void
@ -254,9 +15,8 @@ struct WebReader: UIViewRepresentable {
@Binding var increaseFontActionID: UUID?
@Binding var decreaseFontActionID: UUID?
@State var annotationSaveTransactionID: UUID?
@State private var annotation = String()
@Binding var annotationSaveTransactionID: UUID?
@Binding var annotation: String
func makeCoordinator() -> WebReaderCoordinator {
WebReaderCoordinator()
@ -268,12 +28,12 @@ struct WebReader: UIViewRepresentable {
}
func makeUIView(context: Context) -> WKWebView {
let webView = WebViewManager.create()
let webView = WebViewManager.shared()
let contentController = WKUserContentController()
webView.loadHTMLString(
WebReaderContent(
htmlContent: htmlContent,
articleContent: articleContent,
item: item,
authToken: authToken,
isDark: UITraitCollection.current.userInterfaceStyle == .dark,
@ -292,23 +52,14 @@ struct WebReader: UIViewRepresentable {
webView.scrollView.contentInset.top = readerViewNavBarHeight
webView.scrollView.verticalScrollIndicatorInsets.top = readerViewNavBarHeight
webView.configuration.userContentController.removeAllScriptMessageHandlers()
for action in WebViewAction.allCases {
webView.configuration.userContentController.add(context.coordinator, name: action.rawValue)
}
webView.configuration.userContentController.add(webView, name: "viewerAction")
// webView.configureForOmnivoreAppEmbed(
// config: WebViewConfig(
// url: dataService.appEnvironment.webAppBaseURL,
// themeId: UITraitCollection.current.userInterfaceStyle == .dark ? "Gray" : "LightGray",
// margin: 0,
// fontSize: fontSize(),
// fontFamily: "inter",
// rawAuthCookie: rawAuthCookie
// )
// )
context.coordinator.linkHandler = openLinkAction
context.coordinator.webViewActionHandler = webViewActionHandler
context.coordinator.updateNavBarVisibilityRatio = navBarVisibilityRatioUpdater

View file

@ -0,0 +1,298 @@
import Combine
import Models
import Services
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
@State private var showFontSizePopover = false
@State var showHighlightAnnotationModal = false
@State var safariWebLink: SafariWebLink?
@State private var navBarVisibilityRatio = 1.0
@State private var showDeleteConfirmation = false
@State private var showOverlay = true
@State var increaseFontActionID: UUID?
@State var decreaseFontActionID: UUID?
@State var annotationSaveTransactionID: UUID?
@State var annotation = String()
@EnvironmentObject var dataService: DataService
@EnvironmentObject var authenticator: Authenticator
@Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
@StateObject var viewModel = WebReaderViewModel()
var fontAdjustmentPopoverView: some View {
FontSizeAdjustmentPopoverView(
increaseFontAction: { increaseFontActionID = UUID() },
decreaseFontAction: { decreaseFontActionID = UUID() }
)
}
func webViewActionHandler(message: WKScriptMessage) {
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 }
homeFeedViewModel.updateProgress(itemID: item.id, progress: Double(progress))
}
}
private func handleHighlightAction(message: WKScriptMessage) {
guard let messageBody = message.body as? [String: String] else { return }
guard let actionID = messageBody["actionID"] else { return }
switch actionID {
case "annotate":
annotation = messageBody["annotation"] ?? ""
showHighlightAnnotationModal = true
default:
break
}
}
var navBariOS14: some View {
HStack(alignment: .center) {
Button(
action: { self.presentationMode.wrappedValue.dismiss() },
label: {
Image(systemName: "chevron.backward")
.font(.appTitleTwo)
.foregroundColor(.appGrayTextContrast)
.padding(.horizontal)
}
)
.scaleEffect(navBarVisibilityRatio)
Spacer()
Button(
action: { showFontSizePopover.toggle() },
label: {
Image(systemName: "textformat.size")
.font(.appTitleTwo)
}
)
.padding(.horizontal)
.scaleEffect(navBarVisibilityRatio)
}
.frame(height: readerViewNavBarHeight * navBarVisibilityRatio)
.opacity(navBarVisibilityRatio)
.background(Color.systemBackground)
.onTapGesture {
showFontSizePopover = false
}
}
@available(macOS 12.0, *)
@available(iOS 15.0, *)
var navBar: some View {
HStack(alignment: .center) {
Button(
action: { self.presentationMode.wrappedValue.dismiss() },
label: {
Image(systemName: "chevron.backward")
.font(.appTitleTwo)
.foregroundColor(.appGrayTextContrast)
.padding(.horizontal)
}
)
.scaleEffect(navBarVisibilityRatio)
Spacer()
Button(
action: { showFontSizePopover.toggle() },
label: {
Image(systemName: "textformat.size")
.font(.appTitleTwo)
}
)
.padding(.horizontal)
.scaleEffect(navBarVisibilityRatio)
Menu(
content: {
Group {
Button(
action: {
homeFeedViewModel.setLinkArchived(
dataService: dataService,
linkId: item.id,
archived: !item.isArchived
)
},
label: {
Label(
item.isArchived ? "Unarchive" : "Archive",
systemImage: item.isArchived ? "tray.and.arrow.down.fill" : "archivebox"
)
}
)
Button(
action: { showDeleteConfirmation = true },
label: { Label("Delete", systemImage: "trash") }
)
}
},
label: {
Image.profile
.padding(.horizontal)
.scaleEffect(navBarVisibilityRatio)
}
)
}
.frame(height: readerViewNavBarHeight * navBarVisibilityRatio)
.opacity(navBarVisibilityRatio)
.background(Color.systemBackground)
.onTapGesture {
showFontSizePopover = false
}
.alert("Are you sure?", isPresented: $showDeleteConfirmation) {
Button("Remove Link", role: .destructive) {
homeFeedViewModel.removeLink(dataService: dataService, linkId: item.id)
}
Button("Cancel", role: .cancel, action: {})
}
}
var body: some View {
ZStack {
if let articleContent = viewModel.articleContent {
WebReader(
articleContent: articleContent,
item: item,
openLinkAction: {
#if os(macOS)
NSWorkspace.shared.open($0)
#elseif os(iOS)
safariWebLink = SafariWebLink(id: UUID(), url: $0)
#endif
},
webViewActionHandler: webViewActionHandler,
navBarVisibilityRatioUpdater: {
if $0 < 1 {
showFontSizePopover = false
}
navBarVisibilityRatio = $0
},
authToken: authenticator.authToken ?? "",
appEnv: dataService.appEnvironment,
increaseFontActionID: $increaseFontActionID,
decreaseFontActionID: $decreaseFontActionID,
annotationSaveTransactionID: $annotationSaveTransactionID,
annotation: $annotation
)
.overlay(
Group {
if showOverlay {
Color.systemBackground
.transition(.opacity)
.onAppear {
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(250)) {
withAnimation(.linear(duration: 0.2)) {
showOverlay = false
}
}
}
}
}
)
.sheet(item: $safariWebLink) {
SafariView(url: $0.url)
}
.sheet(isPresented: $showHighlightAnnotationModal) {
HighlightAnnotationSheet(
annotation: $annotation,
onSave: {
annotationSaveTransactionID = UUID()
showHighlightAnnotationModal = false
},
onCancel: {
showHighlightAnnotationModal = false
}
)
}
} else {
Color.clear
.contentShape(Rectangle())
.onAppear {
if !viewModel.isLoading {
viewModel.loadContent(dataService: dataService, slug: item.slug)
}
}
}
if showFontSizePopover {
VStack {
Color.clear
.contentShape(Rectangle())
.frame(height: LinkItemDetailView.navBarHeight)
HStack {
Spacer()
fontAdjustmentPopoverView
.background(Color.appButtonBackground)
.cornerRadius(8)
.padding(.trailing, 44)
}
Spacer()
}
.background(
Color.clear
.contentShape(Rectangle())
.onTapGesture {
showFontSizePopover = false
}
)
}
if #available(iOS 15.0, *) {
VStack(spacing: 0) {
navBar
Spacer()
}
.navigationBarHidden(true)
} else {
VStack(spacing: 0) {
navBariOS14
Spacer()
}
.navigationBarHidden(true)
}
}.onDisappear {
// Clear the shared webview content when exiting
WebViewManager.shared().loadHTMLString("<html></html>", baseURL: nil)
}
.navigationBarHidden(true)
}
}

View file

@ -4,14 +4,14 @@ import Utils
struct WebReaderContent {
let textFontSize: Int
let content: String
let articleContent: ArticleContent
let item: FeedItem
let themeKey: String
let authToken: String
let appEnv: AppEnvironment
init(
htmlContent: String,
articleContent: ArticleContent,
item: FeedItem,
authToken: String,
isDark: Bool,
@ -19,7 +19,7 @@ struct WebReaderContent {
appEnv: AppEnvironment
) {
self.textFontSize = fontSize
self.content = htmlContent
self.articleContent = articleContent
self.item = item
self.themeKey = isDark ? "Gray" : "LightGray"
self.authToken = authToken
@ -36,36 +36,35 @@ struct WebReaderContent {
<meta name='viewport' content='width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, user-scalable=no' />
</head>
<body>
<div id="root">
<script type="text/javascript">
window.omnivoreEnv = {
"NEXT_PUBLIC_APP_ENV": "\(appEnv.rawValue)",
"NEXT_PUBLIC_\(appEnv.rawValue.uppercased())_BASE_URL": "\(appEnv.webAppBaseURL.absoluteString)",
"NEXT_PUBLIC_\(appEnv.rawValue.uppercased())_SERVER_BASE_URL": "\(appEnv.serverBaseURL.absoluteString)",
"NEXT_PUBLIC_\(appEnv.rawValue.uppercased())_HIGHLIGHTS_BASE_URL": "\(appEnv.highlightsServerBaseURL.absoluteString)"
}
<div id="root" />
<script type="text/javascript">
window.omnivoreEnv = {
"NEXT_PUBLIC_APP_ENV": "\(appEnv.rawValue)",
"NEXT_PUBLIC_\(appEnv.rawValue.uppercased())_BASE_URL": "\(appEnv.webAppBaseURL.absoluteString)",
"NEXT_PUBLIC_\(appEnv.rawValue.uppercased())_SERVER_BASE_URL": "\(appEnv.serverBaseURL.absoluteString)",
"NEXT_PUBLIC_\(appEnv.rawValue.uppercased())_HIGHLIGHTS_BASE_URL": "\(appEnv.highlightsServerBaseURL.absoluteString)"
}
window.omnivoreArticle = {
id: "test",
linkId: "test",
slug: "test-slug",
createdAt: new Date().toISOString(),
savedAt: new Date().toISOString(),
url: "https://example.com",
title: `\(item.title)`,
content: `\(content)`,
originalArticleUrl: "https://example.com",
contentReader: "WEB",
readingProgressPercent: \(item.readingProgress),
readingProgressAnchorIndex: \(item.readingProgressAnchor),
highlights: [],
}
window.omnivoreArticle = {
id: "test",
linkId: "test",
slug: "test-slug",
createdAt: new Date().toISOString(),
savedAt: new Date().toISOString(),
url: "https://example.com",
title: `\(item.title)`,
content: `\(articleContent.htmlContent)`,
originalArticleUrl: "https://example.com",
contentReader: "WEB",
readingProgressPercent: \(item.readingProgress),
readingProgressAnchorIndex: \(item.readingProgressAnchor),
highlights: \(articleContent.highlightsJSONString),
}
window.fontSize = \(textFontSize)
window.localStorage.setItem("authToken", "\(authToken)")
window.localStorage.setItem("theme", "\(themeKey)")
</script>
</div>
window.fontSize = \(textFontSize)
window.localStorage.setItem("authToken", "\(authToken)")
window.localStorage.setItem("theme", "\(themeKey)")
</script>
<script src="bundle.js"></script>
</body>
</html>

View file

@ -114,31 +114,3 @@ extension WebReaderCoordinator: WKNavigationDelegate {
}
}
#endif
struct WebViewConfig {
let url: URL
let themeId: String
let margin: Int
let fontSize: Int
let fontFamily: String
let rawAuthCookie: String?
}
extension WKWebView {
func configureForOmnivoreAppEmbed(config: WebViewConfig) {
// Set cookies to pass article preferences to web view
injectCookie(cookieString: "theme=\(config.themeId); Max-Age=31536000;", url: config.url)
injectCookie(cookieString: "margin=\(config.margin); Max-Age=31536000;", url: config.url)
injectCookie(cookieString: "fontSize=\(config.fontSize); Max-Age=31536000;", url: config.url)
injectCookie(cookieString: "fontFamily=\(config.fontFamily); Max-Age=31536000;", url: config.url)
injectCookie(cookieString: config.rawAuthCookie, url: config.url)
}
func injectCookie(cookieString: String?, url: URL) {
if let cookieString = cookieString {
for cookie in HTTPCookie.cookies(withResponseHeaderFields: ["Set-Cookie": cookieString], for: url) {
configuration.websiteDataStore.httpCookieStore.setCookie(cookie) {}
}
}
}
}

View file

@ -0,0 +1,22 @@
import Foundation
public struct ArticleContent {
public let htmlContent: String
public let highlights: [Highlight]
public init(
htmlContent: String,
highlights: [Highlight]
) {
self.htmlContent = htmlContent
self.highlights = highlights
print(highlightsJSONString)
}
public var highlightsJSONString: String {
let jsonData = try? JSONEncoder().encode(highlights)
guard let jsonData = jsonData else { return "[]" }
return String(data: jsonData, encoding: .utf8) ?? "[]"
}
}

View file

@ -1,6 +1,6 @@
import Foundation
public struct Highlight: Identifiable, Hashable {
public struct Highlight: Identifiable, Hashable, Codable {
public let id: String
public let shortId: String
public let quote: String
@ -8,6 +8,9 @@ public struct Highlight: Identifiable, Hashable {
public let suffix: String?
public let patch: String
public let annotation: String?
public let createdAt: Date?
public let updatedAt: Date?
public let createdByMe: Bool
public init(
id: String,
@ -16,7 +19,10 @@ public struct Highlight: Identifiable, Hashable {
prefix: String?,
suffix: String?,
patch: String,
annotation: String?
annotation: String?,
createdByMe: Bool,
createdAt: Date? = nil,
updatedAt: Date? = nil
) {
self.id = id
self.shortId = shortId
@ -25,5 +31,8 @@ public struct Highlight: Identifiable, Hashable {
self.suffix = suffix
self.patch = patch
self.annotation = annotation
self.createdAt = createdAt
self.updatedAt = updatedAt
self.createdByMe = createdByMe
}
}

View file

@ -4,14 +4,30 @@ import Models
import SwiftGraphQL
public extension DataService {
func articleContentPublisher(username: String, slug: String) -> AnyPublisher<String, ServerError> {
func articleContentPublisher(username: String, slug: String) -> AnyPublisher<ArticleContent, ServerError> {
enum QueryResult {
case success(result: String)
case success(result: ArticleContent)
case error(error: String)
}
let highlightSelection = Selection.Highlight {
Highlight(
id: try $0.id(),
shortId: try $0.shortId(),
quote: try $0.quote(),
prefix: try $0.prefix(),
suffix: try $0.suffix(),
patch: try $0.patch(),
annotation: try $0.annotation(),
createdByMe: try $0.createdByMe()
)
}
let articleSelection = Selection.Article {
try $0.content()
ArticleContent(
htmlContent: try $0.content(),
highlights: try $0.highlights(selection: highlightSelection.list)
)
}
let selection = Selection<QueryResult, Unions.ArticleResult> {

View file

@ -18,7 +18,8 @@ public extension DataService {
prefix: try $0.prefix(),
suffix: try $0.suffix(),
patch: try $0.patch(),
annotation: try $0.annotation()
annotation: try $0.annotation(),
createdByMe: try $0.createdByMe()
)
}

View file

@ -1,13 +1,13 @@
import Introspect
import SwiftUI
struct HighlightAnnotationSheet: View {
public struct HighlightAnnotationSheet: View {
@Binding var annotation: String
let onSave: () -> Void
let onCancel: () -> Void
init(
public init(
annotation: Binding<String>,
onSave: @escaping () -> Void,
onCancel: @escaping () -> Void
@ -17,7 +17,7 @@ struct HighlightAnnotationSheet: View {
self.onCancel = onCancel
}
var body: some View {
public var body: some View {
VStack {
HStack {
Button("Cancel", action: onCancel)

View file

@ -114,21 +114,32 @@ public struct WebAppWrapperView: View {
}
#if os(iOS)
struct SafariView: UIViewControllerRepresentable {
public struct SafariView: UIViewControllerRepresentable {
let url: URL
func makeUIViewController(context _: UIViewControllerRepresentableContext<SafariView>) -> SFSafariViewController {
public init(url: URL) {
self.url = url
}
public func makeUIViewController(
context _: UIViewControllerRepresentableContext<SafariView>
) -> SFSafariViewController {
SFSafariViewController(url: url)
}
// swiftlint:disable:next line_length
func updateUIViewController(_: SFSafariViewController, context _: UIViewControllerRepresentableContext<SafariView>) {}
public func updateUIViewController(_: SFSafariViewController, context _: UIViewControllerRepresentableContext<SafariView>) {}
}
#elseif os(macOS)
struct SafariView: View {
public struct SafariView: View {
let url: URL
var body: some View {
public init(url: URL) {
self.url = url
}
public var body: some View {
Color.clear
}
}

File diff suppressed because one or more lines are too long

View file

@ -1,26 +0,0 @@
{
"parser": "@typescript-eslint/parser",
"parserOptions": {
"project": "tsconfig.json"
},
"plugins": ["@typescript-eslint", "functional"],
"extends": [
"next/core-web-vitals",
"plugin:@typescript-eslint/recommended",
"plugin:@typescript-eslint/eslint-recommended",
"prettier",
"plugin:functional/no-object-orientation"
],
"root": true,
"env": {
"es6": true,
"browser": true,
"jest": true,
"node": true
},
"ignorePatterns": ["next.config.js", "jest.config.js"],
"rules": {
"functional/no-mixed-type": 0,
"react/react-in-jsx-scope": 0
}
}

View file

@ -1,9 +0,0 @@
export {}
declare global {
interface Window {
omnivoreArticle?: any
omnivoreEnv?: any
fontSize?: number
}
}

View file

@ -2,18 +2,20 @@
<html>
<head>
<meta charset="utf-8" />
<meta name='viewport' content='width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, user-scalable=no' />
<meta
name="viewport"
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, user-scalable=no"
/>
</head>
<body>
<div id="root">
<!-- {{INJECT_SWIFT_STRING_HERE!!}} -->
<script type="text/javascript">
function loadEnv() {
window.omnivoreEnv = {
"NEXT_PUBLIC_APP_ENV":"local",
"NEXT_PUBLIC_LOCAL_BASE_URL":"http://localhost:3000",
"NEXT_PUBLIC_LOCAL_SERVER_BASE_URL":"http://localhost:4000",
"NEXT_PUBLIC_LOCAL_HIGHLIGHTS_BASE_URL":"http://localhost:4000"
NEXT_PUBLIC_APP_ENV: 'local',
NEXT_PUBLIC_LOCAL_BASE_URL: 'http://localhost:3000',
NEXT_PUBLIC_LOCAL_SERVER_BASE_URL: 'http://localhost:4000',
NEXT_PUBLIC_LOCAL_HIGHLIGHTS_BASE_URL: 'http://localhost:4000',
}
}
@ -23,19 +25,19 @@
<p data-omnivore-anchor-idx="3">I didn't have time to write a short email so I scheduled a long meeting.</p><p data-omnivore-anchor-idx="4">
<a href="https://twitter.com/jacksonh" data-omnivore-anchor-idx="5">jacksonh</a> Jackson Harper <a href="https://twitter.com/jacksonh/status/1069798258141159425" data-omnivore-anchor-idx="6">December 4, 2018, 3:39 AM UTC</a>
</p></div></div>
`;
`
window.omnivoreArticle = {
id: "test",
linkId: "test",
slug: "test-slug",
id: 'test',
linkId: 'test',
slug: 'test-slug',
createdAt: new Date().toISOString(),
savedAt: new Date().toISOString(),
url: "https://example.com",
title: "Test Article",
url: 'https://example.com',
title: 'Test Article',
content: CONTENT,
originalArticleUrl: "https://example.com",
contentReader: "WEB",
originalArticleUrl: 'https://example.com',
contentReader: 'WEB',
readingProgressPercent: 0,
readingProgressAnchorIndex: 0,
highlights: [],
@ -48,4 +50,4 @@
</div>
<script src="bundle.js"></script>
</body>
</html>
</html>

View file

@ -2,7 +2,6 @@ import React from 'react'
import ReactDOM from 'react-dom'
import { Box, VStack } from '@omnivore/web/components/elements/LayoutPrimitives'
import { ArticleContainer } from '@omnivore/web/components/templates/article/ArticleContainer'
import { ArticleAttributes } from '@omnivore/web/lib/networking/queries/useGetArticleQuery'
import { applyStoredTheme } from '@omnivore/web/lib/themeUpdater'
import '@omnivore/web/styles/globals.css'
import '@omnivore/web/styles/articleInnerStyling.css'
@ -26,7 +25,7 @@ const App = () => {
>
<ArticleContainer
viewerUsername="test"
article={window.omnivoreArticle as ArticleAttributes}
article={window.omnivoreArticle}
scrollElementRef={React.createRef()}
isAppleAppEmbed={true}
highlightBarDisabled={true}

View file

@ -1,17 +0,0 @@
{
"compilerOptions": {
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"allowSyntheticDefaultImports": true,
"skipLibCheck": true,
"esModuleInterop": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react"
},
"include": ["additional.d.ts", "src"]
}

View file

@ -1,19 +1,13 @@
import path from "path"
import glob from 'glob'
import path from 'path'
import { DefinePlugin } from 'webpack'
import { Configuration } from "webpack"
import { Configuration } from 'webpack'
import { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer'
const analyze = process.env.ANALYZE
const config: Configuration = {
entry: {
bundle: './src/index.tsx',
fonts: [
...glob.sync('../web/public/static/fonts/Inter/*'),
...glob.sync('../web/public/static/fonts/SFMono/*')
],
bundle: './src/index.jsx',
},
module: {
rules: [
@ -21,40 +15,37 @@ const config: Configuration = {
test: /\.(ts|js)x?$/,
exclude: /node_modules/,
use: {
loader: "babel-loader",
loader: 'babel-loader',
options: {
presets: [
"@babel/preset-env",
["@babel/preset-react", {"runtime": "automatic"}],
"@babel/preset-typescript",
'@babel/preset-env',
['@babel/preset-react', { runtime: 'automatic' }],
'@babel/preset-typescript',
],
},
},
},
{
test: /\.css$/i,
use: ['style-loader', {
loader: 'css-loader',
options: { url: false }
}, {
// We want paths like `/static/fonts/Inter/Inter.woff2 to become
// `Inter.woff2` which is how they will be bundled on iOS.
loader: 'string-replace-loader',
options: {
search: '\(\'/static/fonts/.*/(.*)\'\)',
replace(_s: string, _p: string, group: string) {
return group
},
flags: 'g',
use: [
'style-loader',
{
loader: 'css-loader',
options: { url: false },
},
}],
},
{
test: /.(ttf|otf|woff(2)?)(\?[a-z0-9]+)?$/,
type: 'asset/resource',
generator: {
filename: '[path][name][ext]'
}
{
// We want paths like `/static/fonts/Inter/Inter.woff2 to become
// `Inter.woff2` which is how they will be bundled on iOS.
loader: 'string-replace-loader',
options: {
search: "('/static/fonts/.*/(.*)')",
replace(_s: string, _p: string, group: string) {
return group
},
flags: 'g',
},
},
],
},
],
},
@ -65,15 +56,15 @@ const config: Configuration = {
new BundleAnalyzerPlugin({
openAnalyzer: !!analyze,
analyzerMode: 'static',
})
}),
],
resolve: {
extensions: [".tsx", ".ts", ".js"],
extensions: ['.tsx', '.ts', '.js'],
},
output: {
path: path.resolve(__dirname, "build"),
path: path.resolve(__dirname, 'build'),
filename: '[name].js',
},
};
}
export default config;
export default config