mirror of
https://github.com/omnivore-app/omnivore.git
synced 2026-03-11 08:54:26 +00:00
Merge commit '42e2beb53d89b333611f215e63d366034905bba6' into OMN-189
This commit is contained in:
commit
2593596833
29 changed files with 604 additions and 462 deletions
|
|
@ -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
|
||||
)
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,14 @@ final class ProfileContainerViewModel: ObservableObject {
|
|||
|
||||
var subscriptions = Set<AnyCancellable>()
|
||||
|
||||
var appVersionString: String {
|
||||
if let appVersion = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String {
|
||||
return "Omnivore Version \(appVersion)"
|
||||
} else {
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func loadProfileData(dataService: DataService) {
|
||||
dataService.viewerPublisher().sink(
|
||||
receiveCompletion: { _ in },
|
||||
|
|
@ -81,7 +89,7 @@ struct ProfileView: View {
|
|||
#endif
|
||||
}
|
||||
|
||||
Section {
|
||||
Section(footer: Text(viewModel.appVersionString)) {
|
||||
if FeatureFlag.showAccountDeletion {
|
||||
NavigationLink(
|
||||
destination: ManageAccountView(handleAccountDeletion: {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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) {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
22
apple/OmnivoreKit/Sources/Models/ArticleContent.swift
Normal file
22
apple/OmnivoreKit/Sources/Models/ArticleContent.swift
Normal 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) ?? "[]"
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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> {
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -26,3 +26,4 @@ TWITTER_BEARER_TOKEN=
|
|||
PREVIEW_IMAGE_WRAPPER_ID='selected_highlight_wrapper'
|
||||
SEGMENT_WRITE_KEY='test'
|
||||
REMINDER_TASK_HANDLER_URL=http://localhost:4000/svc/reminders/trigger
|
||||
PUBSUB_VERIFICATION_TOKEN='123456'
|
||||
|
|
|
|||
|
|
@ -16,6 +16,18 @@ All operations on the database must be wrapped in Knex transaction on a resolver
|
|||
|
||||
Because we make use of Row Level Security in the database, - all operations typically begin with assuming the role for which policies exist via `omnivore.set_claims` database function.
|
||||
|
||||
## ElasticSearch
|
||||
|
||||
We use ElasticSearch to store page data in a distributed manner. This is a great way to store data that is not easily searchable.
|
||||
All the page data is stored in a single index `pages`. This index is then queried by the app to display the data.
|
||||
You need to make sure you have an elasticsearch instance running locally (or just use docker-compose).
|
||||
ES url is specified by `ES_URL` environment variable (username `ES_USERNAME` and password `ES_PASSWORD` can be random strings in local environment).
|
||||
|
||||
When you're running elastic for the very first time, you need to create indices and ingest existing data. This can be done by running `python elastic_migrate.py`.
|
||||
This operation is idempotent, so you can always run `python elastic_migrate.py` again to re-ingest all the data.
|
||||
|
||||
You can run ElasticSearch separately by using `docker-compose -f docker-compose.yml up -d elastic`.
|
||||
|
||||
## Image Proxy (optional for local dev)
|
||||
|
||||
Backend API server returns article image links using image proxy
|
||||
|
|
|
|||
|
|
@ -352,7 +352,7 @@ export const getPageByParam = async <K extends keyof ParamSet>(
|
|||
id: body.hits.hits[0]._id,
|
||||
} as Page
|
||||
} catch (e) {
|
||||
console.log('failed to search pages in elastic', e)
|
||||
console.error('failed to search pages in elastic', e)
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
|
@ -474,6 +474,7 @@ export const searchPages = async (
|
|||
return [
|
||||
response.body.hits.hits.map((hit: { _source: Page; _id: string }) => ({
|
||||
...hit._source,
|
||||
content: '',
|
||||
id: hit._id,
|
||||
})),
|
||||
response.body.hits.total.value,
|
||||
|
|
|
|||
|
|
@ -349,7 +349,13 @@ export const createArticleResolver = authorized<
|
|||
articleSavingRequest
|
||||
)
|
||||
}
|
||||
log.info('page created in elastic', articleToSave)
|
||||
log.info(
|
||||
'page created in elastic',
|
||||
pageId,
|
||||
articleToSave.url,
|
||||
articleToSave.slug,
|
||||
articleToSave.title
|
||||
)
|
||||
articleToSave.id = pageId
|
||||
}
|
||||
|
||||
|
|
@ -408,12 +414,7 @@ export const getArticleResolver: ResolverFn<
|
|||
})
|
||||
await createIntercomEvent('get-article', claims.uid)
|
||||
|
||||
console.log('start to get article', Date.now())
|
||||
|
||||
const page = await getPageByParam({ userId: claims.uid, slug })
|
||||
|
||||
console.log('get article from elastic', Date.now())
|
||||
|
||||
if (!page) {
|
||||
return { errorCodes: [ArticleErrorCode.NotFound] }
|
||||
}
|
||||
|
|
@ -440,8 +441,6 @@ export const getArticlesResolver = authorized<
|
|||
const startCursor = params.after || ''
|
||||
const first = params.first || 10
|
||||
|
||||
console.log('getArticlesResolver starts', Date.now())
|
||||
|
||||
// Perform basic sanitization. Right now we just allow alphanumeric, space and quote
|
||||
// so queries can contain phrases like "human race";
|
||||
// We can also split out terms like "label:unread".
|
||||
|
|
@ -461,8 +460,6 @@ export const getArticlesResolver = authorized<
|
|||
},
|
||||
})
|
||||
|
||||
console.log('parsed search query', Date.now())
|
||||
|
||||
await createIntercomEvent('search', claims.uid)
|
||||
|
||||
const [pages, totalCount] = (await searchPages(
|
||||
|
|
@ -485,8 +482,6 @@ export const getArticlesResolver = authorized<
|
|||
const hasNextPage = pages.length > first
|
||||
const endCursor = String(start + pages.length - (hasNextPage ? 1 : 0))
|
||||
|
||||
console.log('get search result', Date.now())
|
||||
|
||||
console.log(
|
||||
'start',
|
||||
start,
|
||||
|
|
|
|||
|
|
@ -131,6 +131,7 @@ export const mergeHighlightResolver = authorized<
|
|||
await models.highlight.deleteMany(overlapHighlightIdList, tx)
|
||||
return await models.highlight.create({
|
||||
...newHighlightInput,
|
||||
articleId: undefined,
|
||||
annotation: mergedAnnotation ? mergedAnnotation.join('\n') : null,
|
||||
userId: claims.uid,
|
||||
elasticPageId: newHighlightInput.articleId,
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import { generateUploadSignedUrl, uploadToSignedUrl } from '../../utils/uploads'
|
|||
import { v4 as uuidv4 } from 'uuid'
|
||||
import { env } from '../../env'
|
||||
import { Page } from '../../elastic/types'
|
||||
import { DateTime } from 'luxon'
|
||||
|
||||
export function pageServiceRouter() {
|
||||
const router = express.Router()
|
||||
|
|
@ -36,19 +37,25 @@ export function pageServiceRouter() {
|
|||
|
||||
const contentType = 'application/json'
|
||||
const bucketName = env.fileUpload.gcsUploadPrivateBucket
|
||||
|
||||
console.log('generate upload url')
|
||||
|
||||
const uploadUrl = await generateUploadSignedUrl(
|
||||
`${req.params.folder}/${
|
||||
data.userId
|
||||
}/${new Date().toDateString()}/${uuidv4()}.json`,
|
||||
`${req.params.folder}/${data.userId}/${DateTime.now().toFormat(
|
||||
'yyyy-LL-dd'
|
||||
)}/${uuidv4()}.json`,
|
||||
contentType,
|
||||
bucketName
|
||||
)
|
||||
|
||||
console.log('start uploading', uploadUrl)
|
||||
|
||||
await uploadToSignedUrl(
|
||||
uploadUrl,
|
||||
Buffer.from(msgStr, 'utf8'),
|
||||
contentType
|
||||
)
|
||||
res.status(200)
|
||||
res.status(200).send('OK')
|
||||
} catch (err) {
|
||||
console.log('upload page data failed', err)
|
||||
res.status(500).send(err)
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||
import { env } from '../env'
|
||||
import { GetSignedUrlConfig, Storage } from '@google-cloud/storage'
|
||||
import axios, { AxiosResponse } from 'axios'
|
||||
import axios from 'axios'
|
||||
|
||||
/* On GAE/Prod, we shall rely on default app engine service account credentials.
|
||||
* Two changes needed: 1) add default service account to our uploads GCS Bucket
|
||||
|
|
@ -102,12 +102,17 @@ export const uploadToSignedUrl = async (
|
|||
uploadUrl: string,
|
||||
data: Buffer,
|
||||
contentType: string
|
||||
): Promise<AxiosResponse> => {
|
||||
return axios.put(uploadUrl, data, {
|
||||
): Promise<void> => {
|
||||
if (env.dev.isLocal) {
|
||||
return
|
||||
}
|
||||
|
||||
await axios.put(uploadUrl, data, {
|
||||
headers: {
|
||||
'Content-Type': contentType,
|
||||
},
|
||||
maxBodyLength: 1000000000,
|
||||
maxContentLength: 100000000,
|
||||
timeout: 30000,
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -52,6 +52,43 @@ const createHighlightQuery = (
|
|||
`
|
||||
}
|
||||
|
||||
const mergeHighlightQuery = (
|
||||
pageId: string,
|
||||
highlightId: string,
|
||||
shortHighlightId: string,
|
||||
overlapHighlightIdList: string[],
|
||||
prefix = '_prefix',
|
||||
suffix = '_suffix',
|
||||
quote = '_quote',
|
||||
patch = '_patch'
|
||||
) => {
|
||||
return `
|
||||
mutation {
|
||||
mergeHighlight(
|
||||
input: {
|
||||
prefix: "${prefix}",
|
||||
suffix: "${suffix}",
|
||||
quote: "${quote}",
|
||||
id: "${highlightId}",
|
||||
shortId: "${shortHighlightId}",
|
||||
patch: "${patch}",
|
||||
articleId: "${pageId}",
|
||||
overlapHighlightIdList: "${overlapHighlightIdList}"
|
||||
}
|
||||
) {
|
||||
... on MergeHighlightSuccess {
|
||||
highlight {
|
||||
id
|
||||
}
|
||||
}
|
||||
... on MergeHighlightError {
|
||||
errorCodes
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
}
|
||||
|
||||
describe('Highlights API', () => {
|
||||
const username = 'fakeUser'
|
||||
let authToken: string
|
||||
|
|
@ -93,4 +130,35 @@ describe('Highlights API', () => {
|
|||
expect(res.body.data.createHighlight.highlight.id).to.eq(highlightId)
|
||||
})
|
||||
})
|
||||
|
||||
context('mergeHighlightMutation', () => {
|
||||
let highlightId: string
|
||||
|
||||
before(async () => {
|
||||
// create test highlight
|
||||
highlightId = generateFakeUuid()
|
||||
const shortHighlightId = '_short_id'
|
||||
const query = createHighlightQuery(
|
||||
authToken,
|
||||
pageId,
|
||||
highlightId,
|
||||
shortHighlightId
|
||||
)
|
||||
await graphqlRequest(query, authToken).expect(200)
|
||||
})
|
||||
|
||||
it('should not fail', async () => {
|
||||
const newHighlightId = generateFakeUuid()
|
||||
const newShortHighlightId = '_short_id_1'
|
||||
const query = mergeHighlightQuery(
|
||||
pageId,
|
||||
newHighlightId,
|
||||
newShortHighlightId,
|
||||
[highlightId]
|
||||
)
|
||||
const res = await graphqlRequest(query, authToken).expect(200)
|
||||
|
||||
expect(res.body.data.mergeHighlight.highlight.id).to.eq(newHighlightId)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
|||
23
packages/api/test/routers/pages.test.ts
Normal file
23
packages/api/test/routers/pages.test.ts
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
import { request } from '../util'
|
||||
import 'mocha'
|
||||
|
||||
describe('Pages Router', () => {
|
||||
const token = process.env.PUBSUB_VERIFICATION_TOKEN || ''
|
||||
|
||||
describe('upload', () => {
|
||||
it('upload data to GCS', async () => {
|
||||
const data = {
|
||||
message: {
|
||||
data: Buffer.from(JSON.stringify({ userId: 'userId' })).toString(
|
||||
'base64'
|
||||
),
|
||||
publishTime: new Date().toISOString(),
|
||||
},
|
||||
}
|
||||
await request
|
||||
.post(`/svc/pubsub/pages/upload/createdPage?token=${token}`)
|
||||
.send(data)
|
||||
.expect(200)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
9
packages/appreader/additional.d.ts
vendored
9
packages/appreader/additional.d.ts
vendored
|
|
@ -1,9 +0,0 @@
|
|||
export {}
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
omnivoreArticle?: any
|
||||
omnivoreEnv?: any
|
||||
fontSize?: number
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
@ -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"]
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue