Merge pull request #3023 from omnivore-app/main

Web production deployment
This commit is contained in:
Jackson Harper 2023-10-26 17:58:30 +08:00 committed by GitHub
commit 1bede65407
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
145 changed files with 4810 additions and 3289 deletions

View file

@ -11,10 +11,16 @@ on:
paths-ignore:
- 'apple/**'
env:
NEXT_PUBLIC_APP_ENV: prod
NEXT_PUBLIC_BASE_URL: http://localhost:3000
NEXT_PUBLIC_SERVER_BASE_URL: http://localhost:4000
NEXT_PUBLIC_HIGHLIGHTS_BASE_URL: http://localhost:3000
jobs:
run-code-tests:
name: Run Codebase tests
runs-on: ubuntu-latest-m
runs-on: ${{ github.repository_owner == 'omnivore-app' && 'ubuntu-latest-m' || 'ubuntu-latest' }}
services:
postgres:
image: ankane/pgvector
@ -59,8 +65,9 @@ jobs:
yarn install --frozen-lockfile
- name: Database Migration
run: |
psql -h localhost -p ${{ job.services.postgres.ports[5432] }} -U postgres -c "CREATE USER app_user WITH ENCRYPTED PASSWORD 'app_pass';"
yarn workspace @omnivore/db migrate
psql -h localhost -p ${{ job.services.postgres.ports[5432] }} -U postgres -c "CREATE USER app_user WITH ENCRYPTED PASSWORD 'app_pass';GRANT omnivore_user to app_user;"
psql -h localhost -p ${{ job.services.postgres.ports[5432] }} -U postgres -c "GRANT omnivore_user to app_user;"
env:
PG_HOST: localhost
PG_PORT: ${{ job.services.postgres.ports[5432] }}

2
.gitignore vendored
View file

@ -69,3 +69,5 @@ data.json
# android
*.aab
*.apk
tsconfig.tsbuildinfo

View file

@ -237,13 +237,12 @@ import Utils
let pageIndex = Int(event.pageIndex)
if let totalPageCount = controller.document?.pageCount {
let percent = min(100, max(0, ((Double(pageIndex) + 1.0) / Double(totalPageCount)) * 100.0))
if percent > self.viewModel.pdfItem.readingProgress {
self.viewModel.updateItemReadProgress(
dataService: dataService,
percent: percent,
anchorIndex: pageIndex
)
}
self.viewModel.updateItemReadProgress(
dataService: dataService,
percent: percent,
anchorIndex: pageIndex,
force: true
)
}
}
}.store(in: &subscriptions)

View file

@ -76,11 +76,12 @@ final class PDFViewerViewModel: ObservableObject {
}
}
func updateItemReadProgress(dataService: DataService, percent: Double, anchorIndex: Int) {
func updateItemReadProgress(dataService: DataService, percent: Double, anchorIndex: Int, force: Bool = false) {
dataService.updateLinkReadingProgress(
itemID: pdfItem.itemID,
readingProgress: percent,
anchorIndex: anchorIndex
anchorIndex: anchorIndex,
force: force
)
}

View file

@ -356,34 +356,11 @@ import Views
}
func markRead(dataService: DataService, item: LinkedItem) {
dataService.updateLinkReadingProgress(itemID: item.unwrappedID, readingProgress: 100, anchorIndex: 0)
dataService.updateLinkReadingProgress(itemID: item.unwrappedID, readingProgress: 100, anchorIndex: 0, force: true)
}
func markUnread(dataService: DataService, item: LinkedItem) {
dataService.updateLinkReadingProgress(itemID: item.unwrappedID, readingProgress: 0, anchorIndex: 0)
}
func snoozeUntil(dataService: DataService, linkId: String, until: Date, successMessage: String?) async {
isLoading = true
if let itemIndex = items.firstIndex(where: { $0.id == linkId }) {
items.remove(at: itemIndex)
}
do {
try await dataService.createReminder(
reminderItemId: .link(id: linkId),
remindAt: until
)
if let message = successMessage {
snackbar(message)
}
} catch {
NSNotification.operationFailed(message: "Failed to snooze")
}
isLoading = false
dataService.updateLinkReadingProgress(itemID: item.unwrappedID, readingProgress: 0, anchorIndex: 0, force: true)
}
private var queryContainsFilter: Bool {
@ -408,12 +385,22 @@ import Views
if !selectedLabels.isEmpty {
query.append(" label:")
query.append(selectedLabels.map { $0.name != nil ? "\"\(String(describing: $0.name))\"" : "" }.joined(separator: ","))
query.append(selectedLabels.compactMap { label in
if let name = label.name {
return "\"\(name)\""
}
return nil
}.joined(separator: ","))
}
if !negatedLabels.isEmpty {
query.append(" !label:")
query.append(negatedLabels.map { $0.name != nil ? "\"\(String(describing: $0.name))\"" : "" }.joined(separator: ","))
query.append(negatedLabels.compactMap { label in
if let name = label.name {
return "\"\(name)\""
}
return nil
}.joined(separator: ","))
}
print("QUERY: `\(query)`")

View file

@ -39,7 +39,8 @@ import Views
dataService.updateLinkReadingProgress(
itemID: itemID,
readingProgress: isItemRead ? 0 : 100,
anchorIndex: 0
anchorIndex: 0,
force: false
)
}

View file

@ -233,7 +233,7 @@ struct WebReaderContainerView: View {
)
Button(
action: {
dataService.updateLinkReadingProgress(itemID: item.unwrappedID, readingProgress: 0, anchorIndex: 0)
dataService.updateLinkReadingProgress(itemID: item.unwrappedID, readingProgress: 0, anchorIndex: 0, force: true)
},
label: { Label("Reset Read Location", systemImage: "arrow.counterclockwise.circle") }
)
@ -432,7 +432,7 @@ struct WebReaderContainerView: View {
#endif
.onAppear {
if item.isUnread {
dataService.updateLinkReadingProgress(itemID: item.unwrappedID, readingProgress: 0.1, anchorIndex: 0)
dataService.updateLinkReadingProgress(itemID: item.unwrappedID, readingProgress: 0.1, anchorIndex: 0, force: false)
}
Task {
await audioController.preload(itemIDs: [item.unwrappedID])

View file

@ -171,7 +171,7 @@ struct SafariWebLink: Identifiable {
return
}
dataService.updateLinkReadingProgress(itemID: itemID, readingProgress: readingProgress, anchorIndex: anchorIndex)
dataService.updateLinkReadingProgress(itemID: itemID, readingProgress: readingProgress, anchorIndex: anchorIndex, force: false)
replyHandler(["result": true], nil)
}

View file

@ -740,7 +740,7 @@
let anchorIndex = Int((player?.currentItem as? SpeechPlayerItem)?.speechItem.htmlIdx ?? "") ?? 0
if let itemID = itemAudioProperties?.itemID {
dataService.updateLinkReadingProgress(itemID: itemID, readingProgress: percentProgress, anchorIndex: anchorIndex)
dataService.updateLinkReadingProgress(itemID: itemID, readingProgress: percentProgress, anchorIndex: anchorIndex, force: true)
}
if let itemID = itemAudioProperties?.itemID, let player = player, let currentItem = player.currentItem {

View file

@ -29,6 +29,7 @@ public enum VoiceCategory: String, CaseIterable {
case enIN = "English (India)"
case enSG = "English (Singapore)"
case enUK = "English (UK)"
case frFR = "French (France)"
case deDE = "German (Germany)"
case hiIN = "Hindi (India)"
case itIT = "Italian (Italy)"
@ -70,6 +71,7 @@ public enum Voices {
English,
VoiceLanguage(key: "zh", name: "Chinese", defaultVoice: "zh-CN-XiaochenNeural", categories: [.zhCN]),
VoiceLanguage(key: "de", name: "German", defaultVoice: "de-CH-JanNeural", categories: [.deDE]),
VoiceLanguage(key: "fr", name: "French", defaultVoice: "fr-FR-HenriNeural", categories: [.frFR]),
VoiceLanguage(key: "hi", name: "Hindi", defaultVoice: "hi-IN-MadhurNeural", categories: [.hiIN]),
VoiceLanguage(key: "it", name: "Italian", defaultVoice: "it-IT-BenignoNeural", categories: [.itIT]),
VoiceLanguage(key: "ja", name: "Japanese", defaultVoice: "ja-JP-NanamiNeural", categories: [.jaJP]),
@ -88,6 +90,7 @@ public enum Voices {
VoicePair(firstKey: "en-IE-ConnorNeural", secondKey: "en-IE-EmilyNeural", firstName: "Connor", secondName: "Emily", language: "en-IE", category: .enIE),
VoicePair(firstKey: "en-IN-NeerjaNeural", secondKey: "en-IN-PrabhatNeural", firstName: "Neerja", secondName: "Prabhat", language: "en-IN", category: .enIN),
VoicePair(firstKey: "en-SG-LunaNeural", secondKey: "en-SG-WayneNeural", firstName: "Luna", secondName: "Wayne", language: "en-SG", category: .enSG),
VoicePair(firstKey: "fr-FR-HenriNeural", secondKey: "fr-FR-DeniseNeural", firstName: "Henri", secondName: "Denise", language: "en-FR", category: .frFR),
VoicePair(firstKey: "zh-CN-XiaochenNeural", secondKey: "zh-CN-XiaohanNeural", firstName: "Xiaochen", secondName: "Xiaohan", language: "zh-CN", category: .zhCN),
VoicePair(firstKey: "zh-CN-XiaoxiaoNeural", secondKey: "zh-CN-YunyangNeural", firstName: "Xiaoxiao", secondName: "Yunyang", language: "zh-CN", category: .zhCN),
VoicePair(firstKey: "es-ES-AlvaroNeural", secondKey: "es-ES-ElviraNeural", firstName: "Alvaro", secondName: "Elvira", language: "es-ES", category: .esES),

File diff suppressed because it is too large Load diff

View file

@ -1,79 +1 @@
import Foundation
import Models
import SwiftGraphQL
public enum ReminderItemId {
case clientRequest(id: String)
case link(id: String)
var linkId: String? {
switch self {
case .clientRequest:
return nil
case let .link(id):
return id
}
}
var clientRequestId: String? {
switch self {
case .link:
return nil
case let .clientRequest(id):
return id
}
}
}
public extension DataService {
func createReminder(
reminderItemId: ReminderItemId,
remindAt: Date
) async throws {
enum MutationResult {
case complete(id: String)
case error(errorCode: Enums.CreateReminderErrorCode)
}
let selection = Selection<MutationResult, Unions.CreateReminderResult> {
try $0.on(
createReminderError: .init { .error(errorCode: try $0.errorCodes().first ?? .badRequest) },
createReminderSuccess: .init {
.complete(id: try $0.reminder(selection: Selection.Reminder { try $0.id() }))
}
)
}
let mutation = Selection.Mutation {
try $0.createReminder(
input: InputObjects.CreateReminderInput(
archiveUntil: true,
clientRequestId: OptionalArgument(reminderItemId.clientRequestId),
linkId: OptionalArgument(reminderItemId.linkId),
remindAt: DateTime(from: remindAt),
sendNotification: true
),
selection: selection
)
}
let path = appEnvironment.graphqlPath
let headers = networker.defaultHeaders
return try await withCheckedThrowingContinuation { continuation in
send(mutation, to: path, headers: headers) { queryResult in
guard let payload = try? queryResult.get() else {
continuation.resume(throwing: BasicError.message(messageText: "network error"))
return
}
switch payload.data {
case .complete:
continuation.resume()
case let .error(errorCode: errorCode):
continuation.resume(throwing: BasicError.message(messageText: errorCode.rawValue))
}
}
}
}
}

View file

@ -4,13 +4,15 @@ import Models
import SwiftGraphQL
extension DataService {
public func updateLinkReadingProgress(itemID: String, readingProgress: Double, anchorIndex: Int) {
public func updateLinkReadingProgress(itemID: String, readingProgress: Double, anchorIndex: Int, force: Bool?) {
backgroundContext.perform { [weak self] in
guard let self = self else { return }
guard let linkedItem = LinkedItem.lookup(byID: itemID, inContext: self.backgroundContext) else { return }
if readingProgress != 0, readingProgress < linkedItem.readingProgress {
return
if let force = force, !force {
if readingProgress != 0, readingProgress < linkedItem.readingProgress {
return
}
}
print("updating reading progress: ", readingProgress, anchorIndex)
@ -24,12 +26,13 @@ extension DataService {
self.syncLinkReadingProgress(
itemID: linkedItem.unwrappedID,
readingProgress: readingProgress,
anchorIndex: anchorIndex
anchorIndex: anchorIndex,
force: force
)
}
}
func syncLinkReadingProgress(itemID: String, readingProgress: Double, anchorIndex: Int) {
func syncLinkReadingProgress(itemID: String, readingProgress: Double, anchorIndex: Int, force: Bool?) {
enum MutationResult {
case saved(readAt: Date?)
case error(errorCode: Enums.SaveArticleReadingProgressErrorCode)
@ -49,8 +52,9 @@ extension DataService {
let mutation = Selection.Mutation {
try $0.saveArticleReadingProgress(
input: InputObjects.SaveArticleReadingProgressInput(
force: OptionalArgument(force),
id: itemID,
readingProgressAnchorIndex: anchorIndex,
readingProgressAnchorIndex: OptionalArgument(anchorIndex),
readingProgressPercent: readingProgress
),
selection: selection

View file

@ -154,7 +154,8 @@ public extension DataService {
syncLinkReadingProgress(
itemID: item.unwrappedID,
readingProgress: item.readingProgress,
anchorIndex: Int(item.readingProgressAnchor)
anchorIndex: Int(item.readingProgressAnchor),
force: item.isPDF
)
}
}

View file

@ -26,7 +26,7 @@ extension DataService {
createdAt: try $0.createdAt().value ?? Date(),
savedAt: try $0.savedAt().value ?? Date(),
readAt: try $0.readAt()?.value,
updatedAt: try $0.updatedAt().value ?? Date(),
updatedAt: try $0.updatedAt()?.value ?? Date(),
state: try $0.state()?.rawValue.asArticleContentStatus ?? .succeeded,
readingProgress: try $0.readingProgressPercent(),
readingProgressAnchor: try $0.readingProgressAnchorIndex(),

View file

@ -258,7 +258,7 @@ private let libraryArticleSelection = Selection.Article {
createdAt: try $0.createdAt().value ?? Date(),
savedAt: try $0.savedAt().value ?? Date(),
readAt: try $0.readAt()?.value,
updatedAt: try $0.updatedAt().value ?? Date(),
updatedAt: try $0.updatedAt()?.value ?? Date(),
state: try $0.state()?.rawValue.asArticleContentStatus ?? .succeeded,
readingProgress: try $0.readingProgressPercent(),
readingProgressAnchor: try $0.readingProgressAnchorIndex(),

View file

@ -19,7 +19,7 @@ public extension DataService {
status: try SubscriptionStatus.make(from: $0.status()),
unsubscribeHttpUrl: try $0.unsubscribeHttpUrl(),
unsubscribeMailTo: try $0.unsubscribeMailTo(),
updatedAt: try $0.updatedAt().value,
updatedAt: try $0.updatedAt()?.value ?? Date(),
url: try $0.url(),
icon: try $0.icon()
)

View file

@ -1,3 +1,4 @@
import Foundation
import Models
import SwiftGraphQL
@ -22,7 +23,7 @@ let highlightSelection = Selection.Highlight {
patch: try $0.patch() ?? "",
annotation: try $0.annotation(),
createdAt: try $0.createdAt().value,
updatedAt: try $0.updatedAt().value,
updatedAt: try $0.updatedAt()?.value ?? Date(),
createdByMe: try $0.createdByMe(),
createdBy: try $0.user(selection: userProfileSelection),
positionPercent: try $0.highlightPositionPercent(),

View file

@ -10,5 +10,8 @@
"express": "^4.18.1",
"express-graphql": "^0.12.0",
"graphql": "^16.8.1"
},
"volta": {
"extends": "../../package.json"
}
}
}

View file

@ -0,0 +1,158 @@
# ओम्निवोर के साथ शुरुआत करना
ओम्निवोर एक ***रीड-इट-लेटर*** ऐप है जो आपको ऑनलाइन पढ़ी गई हर चीज को सहेजने और व्यवस्थित करने की सुविधा देता है।
यह मार्गदर्शिका आपको दिखाएगी कि ओम्निवोर के बुनियादी कार्यों और उन्नत सुविधाओं का उपयोग कैसे करें, जो चार मुख्य गतिविधियों में विभाजित हैं:
- सहेजा जा रहा है
- पढ़ना
- आयोजन
- एकीकरण
लाइब्रेरी आपके सर्वभक्षी अनुभव का केंद्र है, जहां आप अपने द्वारा सहेजे गए किसी भी लिंक तक तुरंत पहुंच सकते हैं। सहेजे गए लिंक आपकी लाइब्रेरी में हमेशा के लिए रहते हैं जब तक कि आप उन्हें हटा नहीं देते।
## सहेजें
उन पृष्ठों या लेखों के लिंक सहेजने के पाँच मुख्य तरीके हैं जिन्हें आप बाद में पढ़ना चाहते हैं:
- आपकी सर्वभक्षी लाइब्रेरी से बचत
- ब्राउज़र से सहेजा जा रहा है
- फ़ोन या टैबलेट से बचत (iOS या Android)
- ईमेल के माध्यम से न्यूज़लेटर सदस्यताएँ
- मैक से पीडीएफ़ सहेजना
### आपकी सर्वभक्षी लाइब्रेरी से बचत
1. अपनी लाइब्रेरी के ऊपरी दाएं कोने में, लिंक जोड़ें बटन पर टैप करें।
2. वह यूआरएल दर्ज करें जिसे आप सहेजना चाहते हैं और लिंक जोड़ें पर टैप करें।
3. अगली बार जब आप इसे रीफ्रेश करेंगे तो लिंक आपकी लाइब्रेरी में दिखाई देगा।
### ब्राउज़र से सहेजा जा रहा है
1. अपने ब्राउज़र के लिए ओम्निवोर एक्सटेंशन डाउनलोड और इंस्टॉल करें:
- [ क्रोम ](https://omnivore.app/install/chrome)
- [ एज ](https://omnivore.app/install/edge)
- [ फ़ायरफ़ॉक्स ](https://omnivore.app/install/firefox)
- [ सफारी ](https://omnivore.app/install/safari)
2. उस पृष्ठ पर जाएँ जिसे आप सहेजना चाहते हैं और अपने ब्राउज़र के टूलबार या एक्सटेंशन मेनू में ओम्निवोर बटन पर टैप करें।
3. वैकल्पिक रूप से, आप किसी भी हाइपरलिंक पर राइट-क्लिक (मैक पर कमांड+क्लिक) कर सकते हैं और मेनू से सेव टू ओम्निवोर का चयन कर सकते हैं।
4. अगली बार जब आप इसे रीफ्रेश करेंगे तो लिंक आपकी लाइब्रेरी में दिखाई देगा।
### फ़ोन या टेबलेट से सहेजा जा रहा है
अपने मोबाइल डिवाइस से लिंक सहेजने का सबसे अच्छा तरीका ओम्निवोर ऐप है। आप यहां ऐप डाउनलोड कर सकते हैं:
- [ आईओएस (आईफोन या आईपैड) ](https://omnivore.app/install/ios)
- एंड्रॉयड
एक बार मोबाइल ऐप इंस्टॉल हो जाए:
1. अपने ब्राउज़र में, उस पृष्ठ पर जाएँ जिसे आप सहेजना चाहते हैं और शेयर बटन पर टैप करें।
2. शेयर मेनू में सर्वाहारी आइकन पर टैप करें।
3. अगली बार जब आप इसे रीफ्रेश करेंगे तो लिंक आपकी लाइब्रेरी में दिखाई देगा।
### ईमेल के माध्यम से न्यूज़लेटर सदस्यताएँ
1. ओम्निवोर वेबसाइट या ऐप पर, प्रोफ़ाइल मेनू तक पहुंचने के लिए ऊपरी दाएं कोने में अपनी फोटो, नाम के पहले अक्षर या अवतार पर टैप करें। मेनू से ईमेल चुनें.
2. सूची में नया ईमेल पता (उदा: username-123_abc@inbox.omnivore.app) जोड़ने के लिए नया ईमेल पता बनाएं पर टैप करें।
3. ईमेल पते के आगे कॉपी आइकन पर क्लिक करें।
4. जिस न्यूज़लेटर की आप सदस्यता लेना चाहते हैं उसके लिए साइनअप पृष्ठ पर जाएँ।
5. ओम्निवोर ईमेल पते को साइनअप फॉर्म में चिपकाएँ।
6. नए न्यूज़लेटर स्वचालित रूप से आपके ओमनिवोर इनबॉक्स में वितरित किए जाएंगे।
### मैक से पीडीएफ़ सहेजना
1. मैक ऐप इंस्टॉल करें। [मैक ऐप](https://omnivore.app/install/mac)
2. अपने मैक पर, वह पीडीएफ ढूंढें जिसे आप सहेजना चाहते हैं और फ़ाइल नाम पर राइट-क्लिक करें या Ctrl+click करें।
3. मेनू से शेयर चुनें और ओम्निवोर चुनें।
4. गली बार जब आप इसे रीफ्रेश करेंगे तो लिंक आपकी लाइब्रेरी में दिखाई देगा।
## पढ़ना
रीडर दृश्य में प्रवेश करने के लिए अपनी लाइब्रेरी में सहेजे गए किसी भी लिंक पर क्लिक करें।
आसानी से पढ़ने और हाइलाइट करने के लिए, विकर्षण-मुक्त पढ़ने के लिए विज्ञापनों और अव्यवस्थाओं को हटाने के लिए ओमनिवोर पृष्ठों को प्रारूपित करता है। टेक्स्ट-केंद्रित दृश्य लेखों को छोटा और लोड करने में तेज़ बनाता है।
पढ़ते समय, आप यह कर सकते हैं:
- <span style="text-decoration:underline;">फ़ॉर्मेटिंग बदलें</span>
- <span style="text-decoration:underline;">टेक्स्ट हाइलाइट करें</span>
- <span style="text-decoration:underline;">नोट्स जोड़ें</span>
- <span style="text-decoration:underline;">सभी सहेजे गए हाइलाइट्स और नोट्स देखें</span>
- <span style="text-decoration:underline;">ट्रैक रीडिंग प्रोग्रेस</span>
### फ़ॉर्मेटिंग बदलें
1. **_थीम:_** प्रोफ़ाइल मेनू तक पहुंचने के लिए ऊपरी दाएं कोने में अपनी फ़ोटो, नाम के पहले अक्षर या अवतार पर टैप करें। लाइट या डार्क थीम चुनने के लिए सफेद या काले थंबनेल का चयन करें।
2. **_टेक्स्ट फ़ॉर्मेटिंग:_** टेक्स्ट आकार, फ़ॉन्ट, मार्जिन और लाइन स्पेसिंग को समायोजित करने के लिए एए आइकन पर टैप करें।
### टेक्स्ट हाइलाइट करें
1. वह टेक्स्ट चुनें जिसे आप हाइलाइट करना चाहते हैं.
2. **हाइलाइट** बटन पर टैप करें।
3. अगली बार जब आप लेख देखेंगे तो पाठ हाइलाइट किया हुआ दिखाई देगा।
### नोट्स जोड़ें
1. टेक्स्ट के उस भाग को हाइलाइट करें जहाँ आप नोट जोड़ना चाहते हैं।
2. **नोट** बटन पर टैप करें, अपना नोट टाइप करें और सेव पर टैप करें।
3. अगली बार जब आप यह लेख देखेंगे तो नोट आइकन दिखाई देगा।
### सभी सहेजे गए हाइलाइट्स और नोट्स देखें
1. इस पृष्ठ पर आपके द्वारा जोड़े गए सभी हाइलाइट किए गए टेक्स्ट और नोट्स की सूची देखने के लिए हाइलाइट/नोट आइकन पर टैप करें।
2. किसी नोट या हाइलाइट को हटाने के लिए, उसे सूची से चुनें और ट्रैश आइकन पर टैप करें।
### ट्रैक रीडिंग प्रोग्रेस
ओम्निवोर स्वचालित रूप से आपके विभिन्न उपकरणों पर आपकी पढ़ने की प्रगति पर नज़र रखता है ताकि आप आसानी से वहीं से शुरू कर सकें जहाँ आपने छोड़ा था। आपके पढ़ने शुरू करने के बाद आपकी लाइब्रेरी में प्रत्येक लिंक के शीर्ष पर एक प्रगति बार दिखाई देगा।
## आयोजन
डिफ़ॉल्ट रूप से, लाइब्रेरी इनबॉक्स आपके द्वारा सहेजे गए सभी लिंक प्रदर्शित करता है। आपकी सूची प्रबंधित करने और आपके पढ़ने को व्यवस्थित रखने के लिए, ओम्निवोर निम्नलिखित क्रियाएं प्रदान करता है:
- <span style="text-decoration:underline;">संग्रह</span>
- <span style="text-decoration:underline;">लेबल</span>
- <span style="text-decoration:underline;">खोज</span>
- <span style="text-decoration:underline;">फिल्टर</span>
### संग्रह
1. जिस लिंक को आप संग्रहित करना चाहते हैं उसके बगल में स्थित मेनू आइकन पर टैप करें (मोबाइल ऐप पर, मेनू खोलने के लिए लिंक को देर तक दबाएं)।
2. **आर्किव** चुनें.
3. लिंक डिफ़ॉल्ट लाइब्रेरी दृश्य से गायब हो जाएगा, लेकिन यदि आप संग्रहीत फ़िल्टर का चयन करते हैं तो दिखाई देगा (नीचे फ़िल्टर देखें)।
### लेबल
1. किसी भी लिंक के आगे मेनू आइकन टैप करें और लेबल सेट करें चुनें।
2. सूची से किसी मौजूदा लेबल का चयन करें या नया लेबल बनाने के लिए लेबल संपादित करें पर टैप करें।
3. लेबल आपकी लाइब्रेरी में लिंक के बगल में दिखाई देगा। समान लेबल वाले सभी लिंक देखने के लिए इसे टैप करें।
4. केवल ओमनिवोर मोबाइल ऐप: आपके द्वारा उपयोग किए गए सभी लेबलों की पूरी सूची देखने के लिए **लेबल** पर टैप करें; समान लेबल वाले सभी लिंक देखने के लिए एक टैप करें
5. ध्यान दें: ओमनिवोर स्वचालित रूप से कुछ लेबल निर्दिष्ट करेगा, जैसे "न्यूज़लेटर्स।"
### खोज
1. अपने सभी सहेजे गए लिंक को खोजने के लिए, खोज बार में एक कीवर्ड या वाक्यांश दर्ज करें।
2. आप अपनी खोज को और भी अधिक केंद्रित करने के लिए कीवर्ड को लेबल और फ़िल्टर के साथ जोड़ सकते हैं। उन्नत खोज के बारे में और जानें.
### फिल्टर
1. अपने लाइब्रेरी दृश्य को परिष्कृत करने के लिए **फ़िल्टर** मेनू का उपयोग करें (कुछ फ़िल्टर डिफ़ॉल्ट रूप से दिखाई दे सकते हैं)।
2. न्यूज़लेटर्स को छोड़कर अपने सभी गैर-संग्रहीत लिंक की सूची देखने के लिए बाद में पढ़ें का चयन करें।
3. आपके द्वारा अपने सभी सहेजे गए पृष्ठों में हाइलाइट किए गए टेक्स्ट चयन को देखने के लिए हाइलाइट्स का चयन करें।
4. आज आपके द्वारा सहेजे गए लिंक की सूची देखने के लिए आज का चयन करें।
5. अपने न्यूज़लेटर सदस्यता के माध्यम से सहेजे गए लिंक देखने के लिए न्यूज़लेटर्स का चयन करें।
## एकीकरण
ओमनिवोर ज्ञान आधारों और नोट लेने वाले ऐप्स के साथ एकीकरण की अनुमति देता है जिनमें शामिल हैं:
- लोगसेक
- वेबहुक
### लॉगसेक
ओमनिवोर के लॉगसेक प्लगइन के साथ आप अपने सभी सहेजे गए लेख, हाइलाइट्स और नोट्स को एक लोकप्रिय ज्ञान आधार लॉगसेक में सिंक कर सकते हैं। लॉगसेक प्लगइन की स्थापना और उपयोग के बारे में जानकारी के लिए, कृपया इस सहायक को देखें [Omnivore for Logseq Plugin Guide](https://briansunter.com/graph/#/page/omnivore-logseq-guide).
### वेबहुक
जब आप कोई लिंक सहेजते हैं या जिस पृष्ठ को आप पढ़ रहे हैं उसमें हाइलाइट्स जोड़ते हैं तो ओम्निवोर वेबहुक को ट्रिगर कर सकता है। <span style="text-decoration:underline;">यह उदाहरण </span> Google ड्राइव पर संग्रहीत Google शीट स्प्रेडशीट में सभी सहेजे गए लिंक लिखने के लिए वेबहुक का उपयोग किया जा रहा है।

View file

@ -3,6 +3,5 @@
"packages/*"
],
"version": "1.0.0",
"npmClient": "yarn",
"useWorkspaces": true
}
"npmClient": "yarn"
}

View file

@ -6,16 +6,16 @@
"workspaces": [
"packages/*"
],
"license": "UNLICENSED",
"license": "AGPL-3.0-only",
"scripts": {
"test": "lerna run --no-bail test --ignore @omnivore/web",
"lint": "lerna run lint --ignore @omnivore/web",
"build": "lerna run build --ignore @omnivore/web",
"bootstrap": "lerna bootstrap",
"test": "lerna run --no-bail test",
"lint": "lerna run lint",
"build": "lerna run build",
"test:scoped:example": "lerna run test --scope={@omnivore/pdf-handler,@omnivore/web}",
"gql-typegen": "graphql-codegen",
"deploy:web": "vercel --prod"
},
"dependencies": {},
"devDependencies": {
"@ardatan/aggregate-error": "^0.0.6",
"@graphql-codegen/cli": "^2.6.2",
@ -31,13 +31,12 @@
"eslint-plugin-prettier": "^4.0.0",
"graphql": "^15.3.0",
"graphql-tag": "^2.11.0",
"lerna": "^4.0.0",
"lerna": "^7.4.1",
"prettier": "^2.5.1",
"typescript": "^4.4.3"
"typescript": "4.5.2"
},
"volta": {
"node": "18.16.1",
"yarn": "1.22.19"
},
"dependencies": {}
}
}
}

View file

@ -1,9 +1,9 @@
FROM node:18.16-alpine as builder
FROM node:18.16 as builder
WORKDIR /app
ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD true
RUN apk add g++ make python3
RUN apt-get update && apt-get install -y g++ make python3
COPY package.json .
COPY yarn.lock .
@ -32,7 +32,9 @@ RUN rm -rf /app/packages/api/node_modules
RUN rm -rf /app/node_modules
RUN yarn install --pure-lockfile --production
FROM node:18.16-alpine as runner
FROM node:18.16 as runner
RUN apt-get update && apt-get install -y netcat-openbsd
WORKDIR /app

View file

@ -8,6 +8,7 @@
"start": "node dist/server.js",
"lint": "eslint src --ext ts,js,tsx,jsx",
"lint:fix": "eslint src --fix --ext ts,js,tsx,jsx",
"test:typecheck": "tsc --noEmit",
"test": "nyc mocha -r ts-node/register --config mocha-config.json --timeout 10000",
"copy-files": "copyfiles -u 1 src/**/*.html dist/"
},
@ -145,7 +146,6 @@
"node": "18.16.1"
},
"volta": {
"node": "18.16.1",
"yarn": "1.22.19"
"extends": "../../package.json"
}
}

View file

@ -2,6 +2,7 @@ import * as httpContext from 'express-http-context2'
import { EntityManager, EntityTarget, Repository } from 'typeorm'
import { appDataSource } from '../data_source'
import { Claims } from '../resolvers/types'
import { SetClaimsRole } from '../utils/dictionary'
export const getColumns = <T>(repository: Repository<T>): (keyof T)[] => {
return repository.metadata.columns.map(
@ -12,8 +13,10 @@ export const getColumns = <T>(repository: Repository<T>): (keyof T)[] => {
export const setClaims = async (
manager: EntityManager,
uid = '00000000-0000-0000-0000-000000000000',
dbRole = 'omnivore_user'
userRole = 'user'
): Promise<unknown> => {
const dbRole =
userRole === SetClaimsRole.ADMIN ? 'omnivore_admin' : 'omnivore_user'
return manager.query('SELECT * from omnivore.set_claims($1, $2)', [
uid,
dbRole,

View file

@ -0,0 +1,70 @@
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
import cors from 'cors'
import express from 'express'
import { LessThan } from 'typeorm'
import { StatusType } from '../../entity/user'
import { readPushSubscription } from '../../pubsub'
import { deleteUsers } from '../../services/user'
import { corsConfig } from '../../utils/corsConfig'
import { logger } from '../../utils/logger'
type CleanupMessage = {
subDays: number
}
const isCleanupMessage = (obj: any): obj is CleanupMessage =>
'subDays' in obj && !isNaN(obj.subDays)
const getCleanupMessage = (msgStr: string): CleanupMessage => {
try {
const obj = JSON.parse(msgStr) as unknown
if (isCleanupMessage(obj)) {
return obj
}
} catch (err) {
console.log('error deserializing event: ', { msgStr, err })
}
return {
subDays: 0, // default to 0
}
}
export function userServiceRouter() {
const router = express.Router()
router.post('/prune', cors<express.Request>(corsConfig), async (req, res) => {
logger.info('prune soft deleted users')
const { message: msgStr, expired } = readPushSubscription(req)
if (!msgStr) {
return res.status(200).send('Bad Request')
}
if (expired) {
logger.info('discarding expired message')
return res.status(200).send('Expired')
}
const cleanupMessage = getCleanupMessage(msgStr)
const subTime = cleanupMessage.subDays * 1000 * 60 * 60 * 24 // convert days to milliseconds
try {
const result = await deleteUsers({
status: StatusType.Deleted,
updatedAt: LessThan(new Date(Date.now() - subTime)), // subDays ago
})
logger.info('prune result', result)
return res.sendStatus(200)
} catch (error) {
logger.error('error prune users', error)
return res.sendStatus(500)
}
})
return router
}

View file

@ -31,6 +31,7 @@ import { newsletterServiceRouter } from './routers/svc/newsletters'
// import { remindersServiceRouter } from './routers/svc/reminders'
import { rssFeedRouter } from './routers/svc/rss_feed'
import { uploadServiceRouter } from './routers/svc/upload'
import { userServiceRouter } from './routers/svc/user'
import { webhooksServiceRouter } from './routers/svc/webhooks'
import { textToSpeechRouter } from './routers/text_to_speech'
import { userRouter } from './routers/user_router'
@ -121,6 +122,7 @@ export const createApp = (): {
app.use('/svc/pubsub/webhooks', webhooksServiceRouter())
app.use('/svc/pubsub/integrations', integrationsServiceRouter())
app.use('/svc/pubsub/rss-feed', rssFeedRouter())
app.use('/svc/pubsub/user', userServiceRouter())
// app.use('/svc/reminders', remindersServiceRouter())
app.use('/svc/email-attachment', emailAttachmentRouter())

View file

@ -148,6 +148,8 @@ export class ReadwiseIntegration extends IntegrationService {
)
return response.status === 200
} catch (error) {
logger.error(error)
if (axios.isAxiosError(error)) {
if (error.response?.status === 429 && retryCount < 3) {
logger.info('Readwise API rate limit exceeded, retrying...')
@ -157,10 +159,6 @@ export class ReadwiseIntegration extends IntegrationService {
await wait(parseInt(retryAfter, 10) * 1000)
return this.syncWithReadwise(token, highlights, retryCount + 1)
}
logger.error(error.message)
} else {
logger.error(error)
}
return false

View file

@ -1,6 +1,8 @@
import { DeepPartial, FindOptionsWhere, In } from 'typeorm'
import { StatusType, User } from '../entity/user'
import { authTrx } from '../repository'
import { userRepository } from '../repository/user'
import { SetClaimsRole } from '../utils/dictionary'
export const deleteUser = async (userId: string) => {
await authTrx(
@ -20,6 +22,28 @@ export const updateUser = async (userId: string, update: Partial<User>) => {
)
}
export const findUser = async (id: string): Promise<User | null> => {
export const findActiveUser = async (id: string): Promise<User | null> => {
return userRepository.findOneBy({ id, status: StatusType.Active })
}
export const findUsersById = async (ids: string[]): Promise<User[]> => {
return userRepository.findBy({ id: In(ids) })
}
export const deleteUsers = async (criteria: FindOptionsWhere<User>) => {
return authTrx(
async (t) => t.getRepository(User).delete(criteria),
undefined,
undefined,
SetClaimsRole.ADMIN
)
}
export const createUsers = async (users: DeepPartial<User>[]) => {
return authTrx(
async (t) => t.getRepository(User).save(users),
undefined,
undefined,
SetClaimsRole.ADMIN
)
}

View file

@ -6,7 +6,7 @@ import {
UpdateUserProfileErrorCode,
} from '../../src/generated/graphql'
import { findProfile } from '../../src/services/profile'
import { deleteUser, findUser } from '../../src/services/user'
import { deleteUser, findActiveUser } from '../../src/services/user'
import { hashPassword } from '../../src/utils/auth'
import { createTestUser } from '../db'
import { generateFakeUuid, graphqlRequest, request } from '../util'
@ -98,7 +98,7 @@ describe('User API', () => {
it('updates user and responds with status code 200', async () => {
const response = await graphqlRequest(query, authToken).expect(200)
const user = await findUser(response.body.data.updateUser.user.id)
const user = await findActiveUser(response.body.data.updateUser.user.id)
expect(user?.name).to.eql(name)
})
})

View file

@ -0,0 +1,65 @@
import { expect } from 'chai'
import 'mocha'
import { In } from 'typeorm'
import { StatusType } from '../../src/entity/user'
import {
createUsers,
deleteUsers,
findUsersById,
} from '../../src/services/user'
import { request } from '../util'
describe('User Service Router', () => {
const token = process.env.PUBSUB_VERIFICATION_TOKEN || ''
describe('prune', () => {
let toDeleteUserIds: string[] = []
before(async () => {
// create test users
const users = await createUsers([
{
name: 'user_1',
email: 'user_1@omnivore.app',
status: StatusType.Deleted,
updatedAt: new Date(Date.now() - 1000 * 60 * 60 * 24 * 2), // 2 days ago
source: 'GOOGLE',
sourceUserId: '123',
},
{
name: 'user_2',
email: 'user_2@omnivore.app',
status: StatusType.Deleted,
updatedAt: new Date(Date.now() - 1000 * 60 * 60 * 24 * 2), // 2 days ago
source: 'GOOGLE',
sourceUserId: '456',
},
])
toDeleteUserIds = users.map((u) => u.id)
})
after(async () => {
// delete test users
await deleteUsers({ id: In(toDeleteUserIds) })
})
it('prunes soft deleted users a day ago', async () => {
const data = {
message: {
data: Buffer.from(
JSON.stringify({ subDays: 1 }) // 1 day ago
).toString('base64'),
publishTime: new Date().toISOString(),
},
}
await request
.post('/svc/pubsub/user/prune?token=' + token)
.send(data)
.expect(200)
const deletedUsers = await findUsersById(toDeleteUserIds)
expect(deletedUsers.length).to.equal(0)
})
})
})

View file

@ -32,7 +32,6 @@
"webpack-dev-server": "^4.7.4"
},
"volta": {
"node": "18.16.0",
"yarn": "1.22.10"
"extends": "../../package.json"
}
}
}

View file

@ -60,6 +60,11 @@ const config: Configuration = {
],
resolve: {
extensions: ['.tsx', '.ts', '.js'],
fallback: {
stream: false,
fs: false,
zlib: false,
}
},
output: {
path: path.resolve(__dirname, 'build'),

View file

@ -1,13 +1,9 @@
FROM node:18.16-alpine
FROM node:18.16
# Installs latest Chromium (92) package.
RUN apk add --no-cache \
RUN apt-get update && apt-get install -y \
chromium \
nss \
freetype \
harfbuzz \
ca-certificates \
ttf-freefont \
nodejs \
yarn \
g++ \
@ -16,7 +12,7 @@ RUN apk add --no-cache \
WORKDIR /app
ENV CHROMIUM_PATH /usr/bin/chromium-browser
ENV CHROMIUM_PATH /usr/bin/chromium
ENV LAUNCH_HEADLESS=true
COPY package.json .

View file

@ -20,5 +20,8 @@
"start_gcf": "npx functions-framework --port=9090 --target=puppeteer",
"start_preview": "npx functions-framework --target=preview",
"test": "mocha test/*.js"
},
"volta": {
"extends": "../../package.json"
}
}

View file

@ -10,6 +10,7 @@
"license": "Apache-2.0",
"scripts": {
"test": "yarn mocha -r ts-node/register --config mocha-config.json",
"test:typecheck": "tsc --noEmit",
"lint": "eslint src --ext ts,js,tsx,jsx",
"compile": "tsc",
"build": "tsc"
@ -38,5 +39,8 @@
"redis": "^4.3.1",
"underscore": "^1.13.6",
"uuid": "^9.0.0"
},
"volta": {
"extends": "../../package.json"
}
}

View file

@ -228,7 +228,7 @@ const getTweetIds = async (
timeout: 60000, // 60 seconds
})
return (await page.evaluate(async (author) => {
return await page.evaluate(async (author) => {
/**
* Wait for `ms` amount of milliseconds
* @param {number} ms
@ -278,7 +278,7 @@ const getTweetIds = async (
}
return ids
}, author)) as string[]
}, author)
} catch (error) {
console.error('Error getting tweets', error)

View file

@ -11,7 +11,6 @@
},
"devDependencies": {},
"volta": {
"node": "18.16.1",
"yarn": "1.22.10"
"extends": "../../package.json"
}
}
}

View file

@ -1,4 +1,4 @@
FROM node:18.16-alpine
FROM node:18.16
WORKDIR /app
@ -8,7 +8,9 @@ COPY tsconfig.json .
COPY /packages/db/package.json ./packages/db/package.json
RUN apk --no-cache --virtual build-dependencies add postgresql uuidgen
RUN apt-get update && apt-get install -y \
postgresql \
uuid-runtime
RUN yarn install

View file

@ -0,0 +1,9 @@
-- Type: DO
-- Name: add_index_for_cleanup_to_user
-- Description: Add index of status and updated_at to omnivore.user table for cleanup of deleted users
BEGIN;
CREATE INDEX IF NOT EXISTS user_status_updated_at_idx ON omnivore.user (status, updated_at);
COMMIT;

View file

@ -0,0 +1,9 @@
-- Type: UNDO
-- Name: add_index_for_cleanup_to_user
-- Description: Add index of status and updated_at to omnivore.user table for cleanup of deleted users
BEGIN;
DROP INDEX IF EXISTS user_status_updated_at_idx;
COMMIT;

View file

@ -0,0 +1,19 @@
-- Type: DO
-- Name: create_omnivore_admin_role
-- Description: Create omnivore_admin role with admin permissions
BEGIN;
CREATE ROLE omnivore_admin;
GRANT omnivore_admin TO app_user;
GRANT ALL PRIVILEGES ON SCHEMA omnivore TO omnivore_admin;
GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA omnivore TO omnivore_admin;
CREATE POLICY user_admin_policy on omnivore.user
FOR ALL
TO omnivore_admin
USING (true);
COMMIT;

View file

@ -0,0 +1,16 @@
-- Type: UNDO
-- Name: create_omnivore_admin_role
-- Description: Create omnivore_admin role with admin permissions
BEGIN;
DROP POLICY user_admin_policy ON omnivore.user;
REVOKE ALL PRIVILEGES on omnivore.user from omnivore_admin;
REVOKE ALL PRIVILEGES on SCHEMA omnivore from omnivore_admin;
DROP OWNED BY omnivore_admin;
DROP ROLE IF EXISTS omnivore_admin;
COMMIT;

View file

@ -4,6 +4,7 @@
"description": "",
"scripts": {
"migrate": "ts-node ./migrate.ts",
"test:typecheck": "tsc --noEmit",
"generate": "plop"
},
"author": "",

View file

@ -10,6 +10,7 @@
"license": "Apache-2.0",
"scripts": {
"test": "yarn mocha -r ts-node/register --config mocha-config.json",
"test:typecheck": "tsc --noEmit",
"lint": "eslint src --ext ts,js,tsx,jsx",
"compile": "tsc",
"build": "tsc && yarn copy-files",
@ -52,5 +53,8 @@
"unzip-stream": "^0.3.1",
"urlsafe-base64": "^1.0.0",
"uuid": "^9.0.0"
},
"volta": {
"extends": "../../package.json"
}
}

View file

@ -11,6 +11,7 @@
"keywords": [],
"scripts": {
"test": "yarn mocha -r ts-node/register --config mocha-config.json",
"test:typecheck": "tsc --noEmit",
"lint": "eslint src --ext ts,js,tsx,jsx",
"compile": "tsc",
"build": "tsc",
@ -42,5 +43,8 @@
"parse-multipart-data": "^1.2.1",
"rfc2047": "^4.0.1",
"showdown": "^2.1.0"
},
"volta": {
"extends": "../../package.json"
}
}

View file

@ -11,6 +11,7 @@
"keywords": [],
"scripts": {
"test": "yarn mocha -r ts-node/register --config mocha-config.json",
"test:typecheck": "tsc --noEmit",
"lint": "eslint src --ext ts,js,tsx,jsx",
"compile": "tsc",
"build": "tsc",
@ -33,5 +34,8 @@
"axios": "^0.27.2",
"concurrently": "^7.0.0",
"pdfjs-dist": "^2.9.359"
},
"volta": {
"extends": "../../package.json"
}
}

View file

@ -25,5 +25,8 @@
},
"scripts": {
"test": "mocha test/*.js"
},
"volta": {
"extends": "../../package.json"
}
}

View file

@ -8,6 +8,7 @@
"license": "Apache-2.0",
"scripts": {
"test": "yarn mocha -r ts-node/register --config mocha-config.json",
"test:typecheck": "tsc --noEmit",
"lint": "eslint src --ext ts,js,tsx,jsx",
"compile": "tsc",
"build": "tsc",
@ -28,5 +29,8 @@
"axios": "^1.4.0",
"dotenv": "^16.0.1",
"jsonwebtoken": "^8.5.1"
},
"volta": {
"extends": "../../package.json"
}
}

View file

@ -41,5 +41,8 @@
"dependencies": {
"html-entities": "^2.3.2",
"parse-srcset": "^1.0.2"
},
"volta": {
"extends": "../../package.json"
}
}

View file

@ -8,6 +8,7 @@
"license": "Apache-2.0",
"scripts": {
"test": "yarn mocha -r ts-node/register --config mocha-config.json",
"test:typecheck": "tsc --noEmit",
"lint": "eslint src --ext ts,js,tsx,jsx",
"compile": "tsc",
"build": "tsc",
@ -28,5 +29,8 @@
"dotenv": "^16.0.1",
"jsonwebtoken": "^8.5.1",
"rss-parser": "^3.13.0"
},
"volta": {
"extends": "../../package.json"
}
}

View file

@ -8,6 +8,7 @@
"license": "Apache-2.0",
"scripts": {
"test": "yarn mocha -r ts-node/register --config mocha-config.json",
"test:typecheck": "tsc --noEmit",
"lint": "eslint src --ext ts,js,tsx,jsx",
"compile": "tsc",
"build": "tsc",
@ -26,5 +27,8 @@
"dotenv": "^16.0.1",
"jsonwebtoken": "^8.5.1",
"search-query-parser": "^1.6.0"
},
"volta": {
"extends": "../../package.json"
}
}

View file

@ -11,6 +11,7 @@
"keywords": [],
"scripts": {
"test": "yarn mocha -r ts-node/register --config mocha-config.json",
"test:typecheck": "tsc --noEmit",
"lint": "eslint src --ext ts,js,tsx,jsx",
"compile": "tsc",
"build": "tsc",
@ -45,5 +46,8 @@
"natural": "^6.2.0",
"redis": "^4.3.1",
"underscore": "^1.13.4"
},
"volta": {
"extends": "../../package.json"
}
}

View file

@ -8,6 +8,7 @@
"license": "Apache-2.0",
"scripts": {
"test": "yarn mocha -r ts-node/register --config mocha-config.json",
"test:typecheck": "tsc --noEmit",
"lint": "eslint src --ext ts,js,tsx,jsx",
"compile": "tsc",
"build": "tsc",
@ -30,5 +31,8 @@
"jsonwebtoken": "^8.5.1",
"linkedom": "^0.14.26",
"urlsafe-base64": "^1.0.0"
},
"volta": {
"extends": "../../package.json"
}
}

View file

@ -12,6 +12,8 @@ ENV NEXT_PUBLIC_BASE_URL=$BASE_URL
ENV NEXT_PUBLIC_SERVER_BASE_URL=$SERVER_BASE_URL
ENV NEXT_PUBLIC_HIGHLIGHTS_BASE_URL=$HIGHLIGHTS_BASE_URL
RUN apk add g++ make python3
WORKDIR /app
COPY package.json .

View file

@ -28,11 +28,9 @@ export const Button = styled('button', {
color: '#3D3D3D',
bg: '#FFEA9F',
p: '10px 15px',
'&:hover': {
'&:hover, &:focus': {
bg: '$omnivoreCtaYellow',
},
'&:focus': {
border: '1px solid $omnivoreCtaYellow',
outline: '1px solid $omnivoreCtaYellow',
},
},
cancelGeneric: {
@ -45,18 +43,13 @@ export const Button = styled('button', {
border: '1px solid transparent',
p: '10px 15px',
bg: 'transparent',
'&:hover': {
'&:hover, &:focus': {
bg: '#EBEBEB',
},
'&:focus': {
outline: 'none !important',
border: '1px solid $omnivoreCtaYellow',
outline: '1px solid $omnivoreCtaYellow',
},
},
ctaOutlineYellow: {
boxSizing: 'border-box',
'-moz-box-sizing': 'border-box',
'-webkit-box-sizing': 'border-box',
borderColor: 'unset',
border: '1px solid $omnivoreCtaYellow',
fontSize: '14px',
@ -159,7 +152,6 @@ export const Button = styled('button', {
outlineColor: 'rgba(0, 0, 0, 0)',
border: '1px solid rgba(0, 0, 0, 0.06)',
cursor: 'pointer',
'&:focus': { outline: 'none' },
},
ctaModal: {
height: '32px',
@ -172,7 +164,6 @@ export const Button = styled('button', {
border: '1px solid $grayBorder',
cursor: 'pointer',
borderRadius: '8px',
'&:focus': { outline: 'none' },
},
ctaSecondary: {
color: '$grayText',
@ -245,7 +236,6 @@ export const Button = styled('button', {
'&:hover': {
opacity: 0.7,
},
'&:focus': { outline: 'none' },
},
highlightBarIcon: {
p: '0px',
@ -257,7 +247,6 @@ export const Button = styled('button', {
opacity: 0.5,
},
'&:focus': { outline: 'none' },
},
articleActionIcon: {
bg: 'transparent',

View file

@ -11,37 +11,35 @@ type HeaderNavLinkProps = {
export function HeaderNavLink(props: HeaderNavLinkProps): JSX.Element {
return (
<Link passHref href={props.href}>
<a style={{ textDecoration: 'none' }}>
<VStack
alignment="center"
distribution="center"
<Link passHref href={props.href} style={{ textDecoration: 'none' }}>
<VStack
alignment="center"
distribution="center"
css={{
cursor: 'pointer',
px: '$3',
color: 'inherit',
textDecoration: 'inherit',
fontFamily: 'inherit',
fontSize: '100%',
}}
>
<StyledText style="navLink">{props.text}</StyledText>
<Box
css={{
cursor: 'pointer',
px: '$3',
color: 'inherit',
textDecoration: 'inherit',
fontFamily: 'inherit',
fontSize: '100%',
width: '100%',
bg: 'rgb(255, 210, 52)',
height: '2px',
opacity: props.isActive ? 1 : 0,
display: props.isActive ? 'unset' : 'none',
mt: '4px',
animation: `${expandWidthAnim('0%', '100%')} 0.2s ease-out`,
'&:hover': {
opacity: props.isActive ? 0.7 : 0,
},
}}
>
<StyledText style="navLink">{props.text}</StyledText>
<Box
css={{
width: '100%',
bg: 'rgb(255, 210, 52)',
height: '2px',
opacity: props.isActive ? 1 : 0,
display: props.isActive ? 'unset' : 'none',
mt: '4px',
animation: `${expandWidthAnim('0%', '100%')} 0.2s ease-out`,
'&:hover': {
opacity: props.isActive ? 0.7 : 0,
},
}}
/>
</VStack>
</a>
/>
</VStack>
</Link>
)
}

View file

@ -1,4 +1,4 @@
import React, { useRef, useState } from 'react'
import { useRef, useState } from 'react'
import { styled } from '../tokens/stitches.config'
import { Box, HStack } from './LayoutPrimitives'
import { StyledText } from './StyledText'
@ -6,9 +6,14 @@ import {
LabelColorDropdownProps,
LabelOptionProps,
} from '../../utils/settings-page/labels/types'
import { TwitterPicker } from 'react-color'
import { TwitterPicker as TwitterPicker_, TwitterPickerProps } from 'react-color'
import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu'
// TwitterPicker is a Class component, but the types are broken in React 18.
// TODO: Maybe move away from this component, since it hasn't been updated for 3 years.
// https://github.com/casesandberg/react-color/issues/883
const TwitterPicker = TwitterPicker_ as unknown as React.FunctionComponent<TwitterPickerProps>
const DropdownMenuContent = styled(DropdownMenuPrimitive.Content, {
borderRadius: 6,
backgroundColor: '$grayBg',

View file

@ -1,4 +1,4 @@
import AutosizeInput from 'react-input-autosize'
import AutosizeInput_, { AutosizeInputProps } from 'react-input-autosize'
import { Box, SpanBox } from './LayoutPrimitives'
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { Label } from '../../lib/networking/fragments/labelFragment'
@ -8,6 +8,11 @@ import { EditLabelChip } from './EditLabelChip'
import { LabelsDispatcher } from '../../lib/hooks/useSetPageLabels'
import { EditLabelChipStack } from './EditLabelChipStack'
// AutosizeInput is a Class component, but the types are broken in React 18.
// TODO: Maybe move away from this component, since it hasn't been updated for 3 years.
// https://github.com/JedWatson/react-input-autosize/issues
const AutosizeInput = AutosizeInput_ as unknown as React.FunctionComponent<AutosizeInputProps>
const MaxUnstackedLabels = 7
type LabelsPickerProps = {

View file

@ -40,14 +40,14 @@ const InternalOrExternalLink = (props: InternalOrExternalLinkProps) => {
}}
>
{!isExternal ? (
<Link href={props.link}>{props.children}</Link>
<Link href={props.link} legacyBehavior>{props.children}</Link>
) : (
<a href={props.link} target="_blank" rel="noreferrer">
{props.children}
</a>
)}
</SpanBox>
)
);
}
export const SuggestionBox = (props: SuggestionBoxProps) => {

View file

@ -53,6 +53,7 @@ export const TooltipContent = StyledContent
export const TooltipArrow = StyledArrow
type TooltipWrappedProps = {
children: React.ReactNode;
tooltipContent: string;
active?: boolean;
tooltipSide?: TooltipPrimitive.TooltipContentProps['side']

View file

@ -1,5 +1,5 @@
import Image from 'next/image'
export function ChromeIcon(): JSX.Element {
return <Image src="/static/icons/chrome@2x.png" width="24" height="24" />
return <Image src="/static/icons/chrome@2x.png" width="24" height="24" alt="" />
}

View file

@ -1,5 +1,5 @@
import Image from 'next/image'
export function EdgeIcon(): JSX.Element {
return <Image src="/static/icons/edge@2x.png" width="24" height="24" />
return <Image src="/static/icons/edge@2x.png" width="24" height="24" alt="" />
}

View file

@ -1,5 +1,5 @@
import Image from 'next/image'
export function FirefoxIcon(): JSX.Element {
return <Image src="/static/icons/firefox@2x.png" width="24" height="24" />
return <Image src="/static/icons/firefox@2x.png" width="24" height="24" alt="" />
}

View file

@ -14,20 +14,22 @@ export function OmnivoreFestiveLogo(
const href = props.href || '/home'
return (
<Link passHref href={href}>
<a
style={{
textDecoration: 'none',
display: 'flex',
alignItems: 'center',
}}
>
<Image
src="/static/images/omnivore-logo-santa.png"
width="27"
height="27"
/>
</a>
</Link>
)
(<Link
passHref
href={href}
style={{
textDecoration: 'none',
display: 'flex',
alignItems: 'center',
}}>
<Image
src="/static/images/omnivore-logo-santa.png"
width="27"
height="27"
alt=""
/>
</Link>)
);
}

View file

@ -1,8 +1,5 @@
import Link from 'next/link'
import { useRouter } from 'next/router'
import { ReactChildren } from 'react'
import { config } from '../../tokens/stitches.config'
export type OmnivoreLogoBaseProps = {
color?: string
href?: string
@ -15,23 +12,25 @@ export function OmnivoreLogoBase(props: OmnivoreLogoBaseProps): JSX.Element {
const router = useRouter()
return (
<Link passHref href={href}>
<a
style={{
textDecoration: 'none',
display: 'flex',
alignItems: 'center',
}}
onClick={(event) => {
const query = window.sessionStorage.getItem('q')
if (query) {
router.push(`/home?${query}`)
event.preventDefault()
}
}}
>
{props.children}
</a>
<Link
passHref
href={href}
style={{
textDecoration: 'none',
display: 'flex',
alignItems: 'center',
}}
onClick={(event) => {
const query = window.sessionStorage.getItem('q')
if (query) {
router.push(`/home?${query}`)
event.preventDefault()
}
}}
tabIndex={-1}
aria-label="Omnivore logo"
>
{props.children}
</Link>
)
}

View file

@ -1,5 +1,5 @@
import Image from 'next/image'
export function SafariIcon(): JSX.Element {
return <Image src="/static/icons/safari@2x.png" width="24" height="24" />
return <Image src="/static/icons/safari@2x.png" width="24" height="24" alt="" />
}

View file

@ -40,7 +40,7 @@ type NoteSectionProps = {
export function ArticleNotes(props: NoteSectionProps): JSX.Element {
const saveText = useCallback(
(text) => {
(text: string) => {
props.saveText(text)
},
[props]
@ -78,7 +78,7 @@ export function HighlightViewNote(props: HighlightViewNoteProps): JSX.Element {
const [lastSaved, setLastSaved] = useState<Date | undefined>(undefined)
const saveText = useCallback(
(text) => {
(text: string) => {
;(async () => {
const success = await updateHighlightMutation({
annotation: text,

View file

@ -48,7 +48,7 @@ export function HighlightViewNote(props: HighlightViewNoteProps): JSX.Element {
const [errorSaving, setErrorSaving] = useState<string | undefined>(undefined)
const saveText = useCallback(
(text, updateTime, interactive) => {
(text: string, updateTime: Date, interactive: boolean) => {
;(async () => {
const success = await updateHighlightMutation({
annotation: text,

View file

@ -30,10 +30,10 @@ export function LibraryHighlightGridCard(
props: LibraryHighlightGridCardProps
): JSX.Element {
const [expanded, setExpanded] = useState(false)
const higlightCount = props.item.highlights?.length ?? 0
const highlightCount = props.item.highlights?.length ?? 0
const router = useRouter()
const viewInReader = useCallback(
(highlightId) => {
(highlightId: string) => {
if (!router || !router.isReady || !props.viewer) {
showErrorToast('Error navigating to highlight')
return
@ -197,7 +197,7 @@ export function LibraryHighlightGridCard(
event.preventDefault()
}}
>
{`View ${higlightCount} highlight${higlightCount > 1 ? 's' : ''}`}
{`View ${highlightCount} highlight${highlightCount > 1 ? 's' : ''}`}
<CaretDown
size={10}
weight="bold"

View file

@ -33,13 +33,14 @@ export function About(props: AboutProps): JSX.Element {
}}
>
<Box
as="p"
css={{
fontWeight: '700',
color: '#3D3D3D',
fontSize: 45,
lineHeight: '53px',
padding: '10px',
paddingBottom: '0px',
padding: '10px 10px 0',
margin: 0,
textAlign: 'center',
}}
>
@ -49,9 +50,11 @@ export function About(props: AboutProps): JSX.Element {
readers.`}
</Box>
<Box
as="p"
css={{
color: 'rgb(125, 125, 125)',
padding: '10px',
margin: 0,
textAlign: 'center',
width: '100%',
fontWeight: '600',
@ -64,9 +67,11 @@ export function About(props: AboutProps): JSX.Element {
</Box>
<Box
as="p"
css={{
color: 'rgb(125, 125, 125)',
padding: '10px',
margin: 0,
textAlign: 'center',
}}
>

View file

@ -32,11 +32,11 @@ export function ErrorLayout(props: ErrorLayoutProps): JSX.Element {
</StyledText>
</HStack>
<SpanBox css={{ height: '64px' }} />
<Link passHref href={viewerData?.me ? '/home' : '/login'}>
<Link passHref href={viewerData?.me ? '/home' : '/login'} legacyBehavior>
<Button style="ctaDarkYellow">
{viewerData?.me ? 'Go Home' : 'Login'}
</Button>
</Link>
</VStack>
)
);
}

View file

@ -50,21 +50,21 @@ export function LoginForm(props: LoginFormProps): JSX.Element {
>
Save articles and read them later in our distraction-free reader.
</StyledText>
<Link passHref href="/about">
<a style={{ textDecoration: 'none' }}>
<StyledText
css={{
fontStyle: 'normal',
fontWeight: '400',
fontSize: '18px',
lineHeight: '120%',
m: '0px',
color: '$omnivoreGray',
}}
>
Learn More -&gt;
</StyledText>
</a>
<Link passHref href="/about" style={{ textDecoration: 'none' }}>
<StyledText
css={{
fontStyle: 'normal',
fontWeight: '400',
fontSize: '18px',
lineHeight: '120%',
m: '0px',
color: '$omnivoreGray',
}}
>
Learn More -&gt;
</StyledText>
</Link>
<SpanBox css={{ height: '24px' }} />
@ -103,7 +103,7 @@ export function LoginForm(props: LoginFormProps): JSX.Element {
/>
</Box>
)}
<Link href="/auth/email-login" passHref>
<Link href="/auth/email-login" passHref legacyBehavior>
<StyledTextSpan
style="actionLink"
css={{ color: '$omnivoreGray', pt: '12px' }}
@ -114,7 +114,7 @@ export function LoginForm(props: LoginFormProps): JSX.Element {
</VStack>
<TermAndConditionsFooter />
</VStack>
)
);
}
function GoogleAuthButton() {
@ -154,17 +154,17 @@ export function TermAndConditionsFooter(): JSX.Element {
}}
>
By signing up, you agree to Omnivores{' '}
<Link href="/terms" passHref>
<Link href="/terms" passHref legacyBehavior>
<StyledTextSpan style="captionLink" css={{ color: '$omnivoreGray' }}>
Terms of Service
</StyledTextSpan>
</Link>{' '}
and{' '}
<Link href="/privacy" passHref>
<Link href="/privacy" passHref legacyBehavior>
<StyledTextSpan style="captionLink" css={{ color: '$omnivoreGray' }}>
Privacy Policy
</StyledTextSpan>
</Link>
</StyledText>
)
);
}

View file

@ -1,3 +1,4 @@
import { unstable_getImgProps as getImgProps } from 'next/image'
import {
Box,
HStack,
@ -9,6 +10,9 @@ import type { LoginFormProps } from './LoginForm'
import { OmnivoreNameLogo } from '../elements/images/OmnivoreNameLogo'
import { theme } from '../tokens/stitches.config'
import featureFullWidthImage from '../../public/static/images/login/login-feature-image-full.png'
import featureHalfWidthImage from '../../public/static/images/login/login-feature-image-half.png'
export function LoginLayout(props: LoginFormProps): JSX.Element {
return (
<>
@ -86,7 +90,30 @@ function MediumLoginLayout(props: LoginFormProps) {
)
}
const srcSetToImageSet = (srcFallback: string, srcSet?: string): string => {
if (!srcSet) return `url(${srcFallback})`
return `image-set( ${srcSet
.split(', ')
.map((subSrc) => {
const [src, resolution] = subSrc.split(' ')
return `url("${decodeURIComponent(src)}") ${resolution}`
})
.join(',')}
)`
}
function OmnivoreIllustration() {
const { props: halfWidthImgProps } = getImgProps({
src: featureHalfWidthImage,
alt: '',
})
const { props: fullWidthImgProps } = getImgProps({
src: featureFullWidthImage,
alt: '',
})
return (
<Box
css={{
@ -95,14 +122,12 @@ function OmnivoreIllustration() {
marginLeft: 'auto',
backgroundSize: 'cover',
backgroundRepeat: 'no-repeat',
backgroundImage: `-webkit-image-set(
url('/static/images/landingPage-feature@1x.png') 1x,
url('/static/landing/landingPage-feature@2x.png') 2x
)`,
'background-image': `image-set(
url('/static/images/landingPage-feature@1x.png') 1x,
url('/static/landing/landingPage-feature@2x.png') 2x
)`,
backgroundPosition: 'left',
backgroundImage: srcSetToImageSet(halfWidthImgProps.src, halfWidthImgProps.srcSet),
'@media (min-aspect-ratio: 2/1)': {
backgroundImage: srcSetToImageSet(fullWidthImgProps.src, fullWidthImgProps.srcSet),
},
}}
/>
)

View file

@ -204,7 +204,7 @@ function SettingsButton(props: SettingsButtonProps): JSX.Element {
}, [props, router])
return (
<Link href={props.destination} passHref title={props.name}>
<Link href={props.destination} passHref title={props.name} legacyBehavior>
<SpanBox
css={{
mx: '10px',
@ -249,5 +249,5 @@ function SettingsButton(props: SettingsButtonProps): JSX.Element {
{props.name}
</SpanBox>
</Link>
)
);
}

View file

@ -110,7 +110,7 @@ export function UploadModal(props: UploadModalProps): JSX.Element {
const dropzoneRef = useRef<DropzoneRef | null>(null)
const openDialog = useCallback(
(event) => {
(event: React.MouseEvent) => {
if (dropzoneRef.current) {
dropzoneRef.current.open()
}

View file

@ -163,7 +163,7 @@ export function NotebookContent(props: NotebookContentProps): JSX.Element {
}, [highlights])
const handleSaveNoteText = useCallback(
(text) => {
(text: string) => {
const changeTime = new Date()
setLastChanged(changeTime)

View file

@ -49,7 +49,7 @@ export function NotebookModal(props: NotebookModalProps): JSX.Element {
props.onClose(allAnnotations ?? [], deletedHighlights ?? [])
}, [props, allAnnotations])
const handleAnnotationsChange = useCallback((allAnnotations) => {
const handleAnnotationsChange = useCallback((allAnnotations: Highlight[]) => {
setAllAnnotations(allAnnotations)
}, [])
@ -66,7 +66,7 @@ export function NotebookModal(props: NotebookModalProps): JSX.Element {
}, [allAnnotations])
const viewInReader = useCallback(
(highlightId) => {
(highlightId: string) => {
props.viewHighlightInReader(highlightId)
handleClose()
},

View file

@ -292,7 +292,7 @@ function FontControls(props: FontControlsProps): JSX.Element {
})
const handleFontSizeChange = useCallback(
(value) => {
(value: number) => {
readerSettings.actionHandler('setFontSize', value)
},
[readerSettings]
@ -399,7 +399,7 @@ function LayoutControls(props: LayoutControlsProps): JSX.Element {
const { readerSettings } = props
const handleMarginWidthChange = useCallback(
(value) => {
(value: number) => {
readerSettings.setMarginWidth(value)
},
[readerSettings]

View file

@ -122,7 +122,7 @@ export function EmailLogin(): JSX.Element {
}}
>
Don&apos;t have an account?{' '}
<Link href="/auth/email-signup" passHref>
<Link href="/auth/email-signup" passHref legacyBehavior>
<StyledTextSpan style="actionLink" css={{ color: '$omnivoreGray' }}>
Sign up
</StyledTextSpan>
@ -140,7 +140,7 @@ export function EmailLogin(): JSX.Element {
}}
>
Forgot your password?{' '}
<Link href="/auth/forgot-password" passHref>
<Link href="/auth/forgot-password" passHref legacyBehavior>
<StyledTextSpan style="actionLink" css={{ color: '$omnivoreGray' }}>
Click here
</StyledTextSpan>
@ -148,5 +148,5 @@ export function EmailLogin(): JSX.Element {
</StyledText>
</VStack>
</form>
)
);
}

View file

@ -206,7 +206,7 @@ export function EmailSignup(): JSX.Element {
}}
>
Already have an account?{' '}
<Link href="/auth/email-login" passHref>
<Link href="/auth/email-login" passHref legacyBehavior>
<StyledTextSpan style="actionLink" css={{ color: '$omnivoreGray' }}>
Login instead
</StyledTextSpan>
@ -215,5 +215,5 @@ export function EmailSignup(): JSX.Element {
<TermAndConditionsFooter />
</VStack>
</form>
)
);
}

View file

@ -108,7 +108,7 @@ export function HighlightsMenu(props: HighlightsMenuProps): JSX.Element {
<DropdownSeparator />
<Link
href={`/${props.viewer.profile.username}/${props.item.slug}#${props.highlight.id}`}
>
legacyBehavior>
<StyledLinkItem
onClick={(event) => {
console.log('event.ctrlKey: ', event.ctrlKey, event.metaKey)
@ -129,7 +129,7 @@ export function HighlightsMenu(props: HighlightsMenuProps): JSX.Element {
</Link>
</Dropdown>
</VStack>
)
);
}
const sortHighlights = (highlights: Highlight[]) => {

View file

@ -368,7 +368,7 @@ function HighlightList(props: HighlightListProps): JSX.Element {
}, [props.item.node.highlights])
const viewInReader = useCallback(
(highlightId) => {
(highlightId: string) => {
if (!router || !router.isReady || !props.viewer) {
showErrorToast('Error navigating to highlight')
return

View file

@ -351,7 +351,7 @@ export function HomeFeedContainer(): JSX.Element {
}, [libraryItems, activeCardId])
const getItem = useCallback(
(itemId) => {
(itemId: string) => {
return libraryItems.find((item) => item.node.id === itemId)
},
[libraryItems]

View file

@ -553,7 +553,7 @@ type EditButtonProps = {
function EditButton(props: EditButtonProps): JSX.Element {
return (
<Link href={props.destination} passHref>
<Link href={props.destination} passHref legacyBehavior>
<SpanBox
css={{
ml: '10px',
@ -584,5 +584,5 @@ function EditButton(props: EditButtonProps): JSX.Element {
{props.title}
</SpanBox>
</Link>
)
);
}

View file

@ -140,7 +140,7 @@ export function Webhooks(): JSX.Element {
}}
>
<h3>{item.method}</h3>
<p>{item.createdAt}</p>
<p>{item.createdAt?.toLocaleDateString()}</p>
</Box>
</Box>
)

View file

@ -1,4 +1,4 @@
import { HStack, VStack } from '../../elements/LayoutPrimitives'
import { Box, HStack, VStack } from '../../elements/LayoutPrimitives'
import { styled } from '../../tokens/stitches.config'
import { StyledText } from '../../elements/StyledText'
@ -30,11 +30,29 @@ export function LandingFooter(): JSX.Element {
textDecoration: 'underline',
},
},
'@mdDown': {
columns: 2,
width: '100%',
marginTop: "8px",
marginBottom: "30px",
},
})
return (
<HStack css={containerStyles}>
<HStack css={{ width: '100%', maxWidth: '1224px' }}>
<Box
css={{
display: 'flex',
flexDirection: 'column',
width: '100%',
maxWidth: '1024px',
'@md': {
flexDirection: 'row',
alignItems: 'flex-start',
justifyContent: 'space-between',
},
}}
>
<VStack>
<StyledText style="aboutFooter">Install</StyledText>
<FooterList>
@ -77,7 +95,7 @@ export function LandingFooter(): JSX.Element {
</a>
</li>
<li>
<a href="mailto:feedback@omnivore.app">Contact us via email</a>
<a href="mailto:feedback@omnivore.app">Contact&nbsp;us via&nbsp;email</a>
</li>
<li>
<a href="https://discord.gg/h2z5rppzz9">
@ -112,7 +130,7 @@ export function LandingFooter(): JSX.Element {
</li>
</FooterList>
</VStack>
</HStack>
</Box>
</HStack>
)
}

View file

@ -1,16 +1,18 @@
import { Box } from '../../elements/LayoutPrimitives'
import { OmnivoreNameLogo } from '../../elements/images/OmnivoreNameLogo'
import { Button } from '../../elements/Button'
import Link from 'next/link'
const LoginButton = (): JSX.Element => {
return (
<Button
as={Link}
href="/login"
style="ctaDarkYellow"
css={{
display: 'flex',
marginLeft: 'auto',
borderRadius: 4,
border: 'unset',
background: 'unset',
color: '#3D3D3D',
height: '42px',
@ -19,10 +21,8 @@ const LoginButton = (): JSX.Element => {
fontWeight: 'normal',
alignItems: 'center',
justifyContent: 'center',
}}
onClick={(e) => {
document.location.href = '/login'
e.preventDefault()
textDecoration: "none",
transition: "all ease-in 50ms"
}}
>
Login

View file

@ -1,50 +1,11 @@
import { HStack, VStack, Box } from '../../elements/LayoutPrimitives'
type LandingSectionProps = {
export interface LandingSectionProps {
titleText: string
descriptionText: React.ReactElement
descriptionText: React.ReactElement | string | number
icon?: React.ReactElement
image: React.ReactElement
}
const titleTextStyles = {
fontWeight: '700',
color: '#3D3D3D',
lineHeight: 1.25,
'@mdDown': {
fontSize: 24,
},
'@md': {
fontSize: '$5',
},
'@xl': {
fontSize: 45,
},
}
const imageContainerStyles = {
display: 'flex',
width: '49%',
alignSelf: 'center',
justifyContent: 'center',
'@md': {
marginBottom: '60px',
},
'@mdDown': {
width: '100%',
},
}
const layoutStyles = {
width: '49%',
alignSelf: 'start',
'@mdDown': {
width: '100%',
paddingTop: '30px',
},
paddingLeft: '30px',
paddingRight: '30px',
paddingBottom: '30px',
imagePosition?: 'left' | 'right'
}
export function LandingSection(props: LandingSectionProps): JSX.Element {
@ -53,24 +14,72 @@ export function LandingSection(props: LandingSectionProps): JSX.Element {
css={{
width: '100%',
flexWrap: 'wrap',
flexDirection: 'row-reverse',
flexDirection: (props?.imagePosition ?? 'left') === 'left' ? 'row-reverse' : 'row',
marginBottom: 20,
'@mdDown': {
width: '100%',
},
}}
>
<VStack distribution="center" alignment={'center'} css={layoutStyles}>
<Box css={titleTextStyles}>{props.titleText}</Box>
<VStack
distribution="center"
alignment="center"
css={{
width: '49%',
alignSelf: 'start',
'@mdDown': {
width: '100%',
paddingTop: '30px',
},
paddingLeft: '30px',
paddingRight: '30px',
paddingBottom: '30px',
}}
>
<Box
as="h2"
css={{
color: 'rgb(125, 125, 125)',
fontWeight: '700',
color: '#3D3D3D',
lineHeight: 1.25,
'@mdDown': {
fontSize: 24,
},
'@md': {
fontSize: '$5',
},
'@xl': {
fontSize: 45,
},
}}
>
{props.titleText}
</Box>
<Box
as="p"
css={{
color: '#666',
}}
>
{props.descriptionText}
</Box>
</VStack>
<Box css={imageContainerStyles}>{props.image}</Box>
<Box
css={{
display: 'flex',
width: '49%',
alignSelf: 'center',
justifyContent: 'center',
'@md': {
marginBottom: '60px',
},
'@mdDown': {
width: '100%',
},
}}
>
{props.image}
</Box>
</HStack>
)
}

View file

@ -1,29 +1,35 @@
import Image from 'next/image'
import { VStack, Box } from '../../elements/LayoutPrimitives'
import { Button } from '../../elements/Button'
import { LandingSection } from './LandingSection'
type GetStartedButtonProps = {
lang: 'en' | 'zh'
}
import landingPageHeroImage from '../../../public/static/images/landing/landing-00-hero.png'
import landingSection1Image from '../../../public/static/images/landing/landing-01-save-it-now.png'
import landingSection2Image from '../../../public/static/images/landing/landing-02-newsletters.png'
import landingSection3Image from '../../../public/static/images/landing/landing-03-organisation.png'
import landingSection4Image from '../../../public/static/images/landing/landing-04-highlights-and-notes.png'
import landingSection5Image from '../../../public/static/images/landing/landing-05-sync.png'
import landingSection6Image from '../../../public/static/images/landing/landing-06-tts.png'
import landingSection7Image from '../../../public/static/images/landing/landing-07-oss.png'
import Link from 'next/link'
export function GetStartedButton(props: GetStartedButtonProps): JSX.Element {
export function GetStartedButton(props: { lang: 'en' | 'zh' }): JSX.Element {
return (
<Button
as={Link}
href="/login"
style="ctaDarkYellow"
css={{
display: 'flex',
borderRadius: 4,
background: 'rgb(255, 210, 52)',
background: '$omnivoreCtaYellow',
padding: '12px 25px',
color: '#3D3D3D',
width: '172px',
height: '42px',
alignItems: 'center',
justifyContent: 'center',
fontWeight: '600',
}}
onClick={(e) => {
document.location.href = '/login'
e.preventDefault()
textDecoration: 'none',
transition: 'background-color ease-out 50ms',
'&:hover': {
backgroundColor: '$omnivoreYellow',
},
}}
>
{props.lang == 'zh' ? `免费注册` : `Sign Up for Free`}
@ -50,38 +56,6 @@ const containerStyles = {
},
}
const callToActionStyles = {
background: 'white',
borderRadius: '24px',
boxSizing: 'border-box',
border: '1px solid #D8D7D5',
boxShadow:
'0px 7px 8px rgba(32, 31, 29, 0.03), 0px 18px 24px rgba(32, 31, 29, 0.03)',
padding: 40,
marginTop: 64,
minheight: 330,
width: 'inherit',
'@md': {
width: '100%',
},
'@xl': {
width: '95%',
},
}
const callToActionText = {
color: '#3D3D3D',
fontWeight: '700',
fontSize: 64,
lineHeight: '1.25',
textAlign: 'center',
paddingBottom: '20px',
'@mdDown': {
fontSize: '32px',
},
}
type LandingSectionsContainerProps = {
lang: 'en' | 'zh'
}
@ -98,7 +72,8 @@ const sections = [
titleText: `先保存,后阅读`,
descriptionText: `看到有趣的内容但没时间阅读不论是文章、PDF或是推特线程只需将它们保存下来等稍后有空再阅读。Omnivore 应用程序适用于iOS、Android 和主要网络浏览扩展程序。`,
},
imageIdx: `03`,
image: landingSection1Image,
imageAlt: '',
},
{
en: {
@ -111,7 +86,8 @@ const sections = [
titleText: `集合邮件订阅`,
descriptionText: `您不再需要到不同收件箱提取订阅的邮件,只要将它们发送到 Omnivore Library即可在同一处随心阅读不受其他电子邮件或 substack 的干扰。`,
},
imageIdx: `04`,
image: landingSection2Image,
imageAlt: '',
},
{
en: {
@ -125,7 +101,8 @@ const sections = [
titleText: `按喜好组织阅读系统`,
descriptionText: `我们不会限定您如何组织系统,只提供您所需的工具,如标签、过滤器和完整的文本索引搜索,让您按自己的喜好和需求设定组织规则。`,
},
imageIdx: `05`,
image: landingSection3Image,
imageAlt: '',
},
{
en: {
@ -139,7 +116,8 @@ const sections = [
titleText: `添加高亮和注释`,
descriptionText: `想提高阅读效率?积极动用大脑,为关键的段落添加高亮或注释,能提高您阅读记忆的保留。这些标注将永久保存在文件里,方便您随时搜索使用。`,
},
imageIdx: `06`,
image: landingSection4Image,
imageAlt: '',
},
{
en: {
@ -152,7 +130,8 @@ const sections = [
titleText: `与您的“第二大脑”同步`,
descriptionText: `Omnivore 应用程序能与个人知识管理系统如 Logseq 和 Obsidian 同步,让您轻而易举地综合所有保存文章、高亮和注释。`,
},
imageIdx: `07`,
image: landingSection5Image,
imageAlt: '',
},
{
en: {
@ -165,8 +144,8 @@ const sections = [
titleText: `使用 text-to-speech 功能聆听阅读`,
descriptionText: `使用TTS逼真、自然的人工智能声音为您阅读待读列表中的读物让眼睛好好休息一下。这便是我们iOS 版Omnivore 应用程序的独家功能。`,
},
imageIdx: `08`,
maxWidth: '85%',
image: landingSection6Image,
imageAlt: '',
},
{
en: {
@ -180,7 +159,8 @@ const sections = [
titleText: `开源软件给予您控制权`,
descriptionText: `阅读是终身的活动,不应担心失去自己多年辛苦建立的图书馆。我们的开源平台,就是为了确保您的阅读不会受限于任何专有系统。`,
},
imageIdx: `09`,
image: landingSection7Image,
imageAlt: '',
},
]
export function LandingSectionsContainer(
@ -198,42 +178,69 @@ export function LandingSectionsContainer(
},
}}
>
<img
height="647"
width="1015"
srcSet="/static/landing/landingPage-feature@1x.png,
/static/landing/landingPage-feature@2x.png 2x,
/static/landing/landingPage-feature@3x.png 3x"
alt="landingHero-1"
<Image
src={landingPageHeroImage}
alt="Hero image"
sizes="(max-width: 1024px) 100vw"
style={{
width: '85%',
maxWidth: '85%',
height: 'auto',
}}
priority
blurDataURL="/static/images/landing/landing-00-hero-blurred.png"
/>
</Box>
{sections.map((section) => {
{sections.map((section, sectionIndex) => {
return (
<LandingSection
key={section.imageIdx}
key={sectionIndex}
titleText={section[props.lang].titleText}
descriptionText={<p>{section[props.lang].descriptionText}</p>}
descriptionText={section[props.lang].descriptionText}
imagePosition={sectionIndex % 2 ? 'left' : 'right'}
image={
<img
srcSet={`/static/landing/landingPage-${section.imageIdx}@1x.png,
/static/landing/landingPage-${section.imageIdx}@2x.png 2x,
/static/landing/landingPage-${section.imageIdx}@3x.png 3x`}
alt={`landing-${section.imageIdx}`}
style={{ maxWidth: section.maxWidth ?? '100%' }}
<Image
alt={section.imageAlt}
src={section.image}
sizes="(max-width: 512px) 50vw, (max-width: 512px) 100vw"
style={{ maxWidth: '100%', height: 'auto' }}
/>
}
/>
)
})}
<VStack alignment="center" css={callToActionStyles}>
<VStack
alignment="center"
css={{
width: '100vw',
backgroundColor: '#fff',
paddingBottom: '40px',
marginTop: '40px',
borderTop: '1px solid var(--colors-omnivoreYellow)',
borderBottom: '1px solid var(--colors-omnivoreYellow)',
'@md': {
marginTop: 0,
},
}}
>
{props.lang == 'en' && (
<Box css={callToActionText}>Get Started With Omnivore Today</Box>
<Box
as="p"
css={{
color: '#3D3D3D',
fontWeight: '700',
fontSize: '2.5rem',
lineHeight: '1.25',
textAlign: 'center',
marginBottom: '40px',
'@mdDown': {
fontSize: '2rem',
},
}}
>
Get Started With Omnivore Today
</Box>
)}
<GetStartedButton lang={props.lang} />
</VStack>

View file

@ -269,7 +269,7 @@ const darkThemeSpec = {
labelButtonsBg: '#5F5E58',
// New theme, special naming to keep things straigh
// New theme, special naming to keep things straight
// once all switch over, we will rename
// DARK
colorScheme: 'dark',

View file

@ -23,7 +23,7 @@ function disableWordSnap(str: string): boolean {
}
const isWhitespace = (c?: string): boolean => {
return !!c && /\u2014|\u2013|,|\s/.test(c)
return !!c && /\u2014|\u2013|,|\s/.test(c);
}
function findNextWord(

View file

@ -25,7 +25,7 @@ export function useSelection(
)
const handleFinishTouch = useCallback(
async (mouseEvent) => {
async (mouseEvent: any) => {
let wasDragEvent = false
const tapAttributes = {
tapX: mouseEvent.clientX,

View file

@ -105,7 +105,7 @@ export const useKeyboardShortcuts = (commands: KeyboardCommand[]): void => {
)
const keydownListener = useCallback(
(keydownEvent) => {
(keydownEvent: any) => {
const { target } = keydownEvent
if (!keydownEvent.key) return
const key = keydownEvent.key.toLowerCase()
@ -129,7 +129,7 @@ export const useKeyboardShortcuts = (commands: KeyboardCommand[]): void => {
)
const keyupListener = useCallback(
(keyupEvent) => {
(keyupEvent: any) => {
if (!keyupEvent.key) return
const key = keyupEvent.key.toLowerCase()
if (keys[key] === undefined) return

View file

@ -17,6 +17,7 @@ const ContentSecurityPolicy = `
const moduleExports = {
images: {
formats: ['image/avif', 'image/webp'],
domains: [
'proxy-demo.omnivore-image-cache.app',
'proxy-dev.omnivore-image-cache.app',
@ -56,7 +57,7 @@ const moduleExports = {
},
],
},
]
];
},
async redirects() {
return [

View file

@ -13,6 +13,7 @@
"test": "jest",
"test:watch": "jest --watch",
"test:build": "jest && next build",
"test:typecheck": "tsc --noEmit",
"upgrade-psdpdfkit": "cp -R '../../node_modules/pspdfkit/dist/pspdfkit-lib' public/pspdfkit-lib",
"storybook": "start-storybook -p 6006 -s ./public",
"build-storybook": "build-storybook -s public"
@ -32,7 +33,6 @@
"@radix-ui/react-tooltip": "^0.1.7",
"@sentry/nextjs": "^7.42.0",
"@stitches/react": "^1.2.5",
"@types/react-input-autosize": "^2.2.1",
"antd": "4.24.3",
"axios": "^1.2.0",
"color2k": "^2.0.0",
@ -47,16 +47,16 @@
"markdown-it": "^13.0.1",
"match-sorter": "^6.3.1",
"nanoid": "^3.1.29",
"next": "^12.1.0",
"next": "^13.5.6",
"node-html-markdown": "^1.3.0",
"papaparse": "^5.4.1",
"phosphor-react": "^1.4.0",
"posthog-js": "^1.78.2",
"pspdfkit": "^2022.2.3",
"react": "^17.0.2",
"react": "^18.2.0",
"react-color": "^2.19.3",
"react-colorful": "^5.5.1",
"react-dom": "^17.0.2",
"react-dom": "^18.2.0",
"react-dropzone": "^14.2.3",
"react-hot-toast": "^2.1.1",
"react-input-autosize": "^3.0.0",
@ -68,6 +68,7 @@
"react-super-responsive-table": "^5.2.1",
"react-topbar-progress-indicator": "^4.1.1",
"remark-gfm": "^3.0.1",
"sharp": "^0.32.6",
"swr": "^1.0.1",
"uuid": "^8.3.2",
"yet-another-react-lightbox": "^3.12.0"
@ -93,14 +94,14 @@
"@types/lodash.debounce": "^4.0.6",
"@types/markdown-it": "^12.2.3",
"@types/papaparse": "^5.3.7",
"@types/react": "17.0.2",
"@types/react-color": "^3.0.6",
"@types/react-dom": "^17.0.2",
"@types/react": "^18.2.0",
"@types/react-color": "^3.0.9",
"@types/react-dom": "^18.2.0",
"@types/react-input-autosize": "^2.2.1",
"@types/uuid": "^8.3.1",
"babel-jest": "^27.4.5",
"babel-loader": "^8.2.3",
"eslint-config-next": "12.0.7",
"eslint-config-next": "^13.5.6",
"eslint-plugin-functional": "^4.0.2",
"eslint-plugin-react": "^7.28.0",
"graphql": "^15.6.1",
@ -108,7 +109,6 @@
"storybook-addon-next-router": "^3.1.1"
},
"volta": {
"node": "18.16.1",
"yarn": "1.22.10"
"extends": "../../package.json"
}
}
}

View file

@ -4,8 +4,11 @@ import { locale, timeZone } from '../../lib/dateFormatting'
import { SaveResponseData } from '../../lib/networking/mutations/saveUrlMutation'
import { ssrFetcher } from '../../lib/networking/networkHelpers'
type Request = NextApiRequest & { cookies: { [key: string]: string } }
type Response = NextApiResponse
const saveUrl = async (
req: NextApiRequest,
req: Request,
url: URL,
labels: string[] | undefined,
state: string | undefined,
@ -53,11 +56,7 @@ const saveUrl = async (
}
}
// eslint-disable-next-line import/no-anonymous-default-export
export default async (
req: NextApiRequest,
res: NextApiResponse
): Promise<void> => {
export default async function handler(req: Request, res: Response) {
const urlStr = req.query['url']
if (req.query['labels'] && typeof req.query['labels'] === 'string') {
req.query['labels'] = [req.query['labels']]

View file

@ -32,7 +32,7 @@ export default function InvitePage(): JSX.Element {
}, [isLoading, router, viewerData, viewerDataError])
const acceptClicked = useCallback(
(event) => {
(event: any) => {
event?.stopPropagation()
if (!router.isReady) {
@ -63,108 +63,106 @@ export default function InvitePage(): JSX.Element {
[router, inviteCode]
)
return (
<>
<PageMetaData title="Accept Invite - Omnivore" path="/invite" />
<ProfileLayout>
<VStack
alignment="center"
return <>
<PageMetaData title="Accept Invite - Omnivore" path="/invite" />
<ProfileLayout>
<VStack
alignment="center"
css={{
padding: '16px',
background: 'white',
minWidth: '340px',
width: '70vw',
maxWidth: '576px',
borderRadius: '8px',
border: '1px solid #3D3D3D',
boxShadow: '#B1B1B1 9px 9px 9px -9px',
}}
>
<StyledText style="subHeadline" css={{ color: '$omnivoreGray' }}>
You&apos;re invited
</StyledText>
<StyledText
style="action"
css={{
padding: '16px',
background: 'white',
minWidth: '340px',
width: '70vw',
maxWidth: '576px',
borderRadius: '8px',
border: '1px solid #3D3D3D',
boxShadow: '#B1B1B1 9px 9px 9px -9px',
mt: '0px',
pt: '4px',
width: '100%',
color: '$omnivoreLightGray',
textAlign: 'center',
whiteSpace: 'normal',
}}
>
<StyledText style="subHeadline" css={{ color: '$omnivoreGray' }}>
You&apos;re invited
</StyledText>
You have been invited to join a recommendation group on Omnivore.
Recommendation groups allow you to share articles with other group
members.
</StyledText>
{errorMessage && (
<StyledText
style="action"
style="error"
css={{
mt: '0px',
pt: '4px',
width: '100%',
color: '$omnivoreLightGray',
textAlign: 'center',
whiteSpace: 'normal',
}}
>
You have been invited to join a recommendation group on Omnivore.
Recommendation groups allow you to share articles with other group
members.
{errorMessage}
</StyledText>
{errorMessage && (
<StyledText
style="error"
css={{
mt: '0px',
pt: '4px',
width: '100%',
textAlign: 'center',
whiteSpace: 'normal',
}}
>
{errorMessage}
</StyledText>
)}
)}
<HStack
alignment="center"
distribution="center"
css={{
gap: '10px',
width: '100%',
height: '80px',
}}
>
{viewerData?.me ? (
<Button
type="submit"
style={'ctaDarkYellow'}
onClick={acceptClicked}
>
Accept Invite
</Button>
) : (
<Button
type="submit"
style={'ctaDarkYellow'}
onClick={() => router.push('/login')}
>
Login
</Button>
)}
</HStack>
<StyledText
style="action"
css={{
m: '0px',
pt: '16px',
width: '100%',
color: '$omnivoreLightGray',
textAlign: 'center',
whiteSpace: 'normal',
}}
>
Don&apos;t have an Omnivore account?{' '}
<Link href="/login" passHref>
<StyledTextSpan
style="actionLink"
css={{ color: '$omnivoreGray' }}
>
Signup
</StyledTextSpan>
</Link>
</StyledText>
</VStack>
<div data-testid="invite-page-tag" />
</ProfileLayout>
</>
)
<HStack
alignment="center"
distribution="center"
css={{
gap: '10px',
width: '100%',
height: '80px',
}}
>
{viewerData?.me ? (
<Button
type="submit"
style={'ctaDarkYellow'}
onClick={acceptClicked}
>
Accept Invite
</Button>
) : (
<Button
type="submit"
style={'ctaDarkYellow'}
onClick={() => router.push('/login')}
>
Login
</Button>
)}
</HStack>
<StyledText
style="action"
css={{
m: '0px',
pt: '16px',
width: '100%',
color: '$omnivoreLightGray',
textAlign: 'center',
whiteSpace: 'normal',
}}
>
Don&apos;t have an Omnivore account?{' '}
<Link href="/login" passHref legacyBehavior>
<StyledTextSpan
style="actionLink"
css={{ color: '$omnivoreGray' }}
>
Signup
</StyledTextSpan>
</Link>
</StyledText>
</VStack>
<div data-testid="invite-page-tag" />
</ProfileLayout>
</>;
}

Some files were not shown because too many files have changed in this diff Show more