From 541719bc598cd2c12a71c0c46fe1de151e69490f Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Mon, 10 Oct 2022 21:12:25 -0700 Subject: [PATCH 01/53] bump android version --- android/Omnivore/app/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/android/Omnivore/app/build.gradle b/android/Omnivore/app/build.gradle index 66cccad8f..4bfbabe93 100644 --- a/android/Omnivore/app/build.gradle +++ b/android/Omnivore/app/build.gradle @@ -17,8 +17,8 @@ android { applicationId "app.omnivore.omnivore" minSdk 23 targetSdk 32 - versionCode 7 - versionName "0.0.7" + versionCode 8 + versionName "0.0.8" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" vectorDrawables { From 9d741ff0d9e9b4feca453764f779c4c6b71c01dd Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Tue, 11 Oct 2022 11:17:26 -0700 Subject: [PATCH 02/53] change navbar offset on scroll --- .../omnivore/omnivore/ui/reader/WebReader.kt | 74 ++++++++++++++++++- .../omnivore/ui/reader/WebReaderViewModel.kt | 3 + 2 files changed, 74 insertions(+), 3 deletions(-) diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/WebReader.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/WebReader.kt index 20e3e0439..2927040e4 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/WebReader.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/WebReader.kt @@ -8,28 +8,96 @@ import android.view.* import android.webkit.JavascriptInterface import android.webkit.WebView import android.webkit.WebViewClient -import androidx.compose.foundation.layout.Box -import androidx.compose.material3.Text +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.TopAppBar +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Settings +import androidx.compose.material3.* import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.input.nestedscroll.NestedScrollConnection +import androidx.compose.ui.input.nestedscroll.NestedScrollSource +import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.unit.IntOffset +import androidx.compose.ui.unit.dp import androidx.compose.ui.viewinterop.AndroidView +import androidx.lifecycle.viewmodel.compose.viewModel import app.omnivore.omnivore.R import com.google.gson.Gson +import kotlin.math.roundToInt @Composable fun WebReaderLoadingContainer(slug: String, webReaderViewModel: WebReaderViewModel) { val webReaderParams: WebReaderParams? by webReaderViewModel.webReaderParamsLiveData.observeAsState(null) + val toolbarHeight = 48.dp + val toolbarHeightPx = with(LocalDensity.current) { toolbarHeight.roundToPx().toFloat() } + + // Offset to collapse toolbar + val toolbarOffsetHeightPx = remember { mutableStateOf(0f) } + + // Create a connection to the nested scroll system and listen to the scroll happening inside child Column + val nestedScrollConnection = remember { + object : NestedScrollConnection { + override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset { + val delta = available.y + val newOffset = toolbarOffsetHeightPx.value + delta + toolbarOffsetHeightPx.value = newOffset.coerceIn(-toolbarHeightPx, 0f) + return Offset.Zero + } + } + } + if (webReaderParams == null) { webReaderViewModel.loadItem(slug = slug) } if (webReaderParams != null) { - WebReader(webReaderParams!!, webReaderViewModel) + Box( + modifier = Modifier + .fillMaxSize() + .nestedScroll(nestedScrollConnection) + ) { + Column( + modifier = Modifier + .fillMaxSize() + .verticalScroll(webReaderViewModel.scrollState) + + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .requiredHeight(height = toolbarHeight) + ) { + } + WebReader(webReaderParams!!, webReaderViewModel) + } + + TopAppBar( + modifier = Modifier + .height(height = toolbarHeight) + .offset { IntOffset(x = 0, y = toolbarOffsetHeightPx.value.roundToInt()) }, + backgroundColor = MaterialTheme.colorScheme.surfaceVariant, + elevation = (-10).dp, + title = {}, + navigationIcon = { + IconButton(onClick = {}) { + Icon( + imageVector = Icons.Filled.Settings, + contentDescription = null + ) + } + } + ) + } } else { // TODO: add a proper loading view Text("Loading...") diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/WebReaderViewModel.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/WebReaderViewModel.kt index e9a6f4aaf..7a5bc60c6 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/WebReaderViewModel.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/WebReaderViewModel.kt @@ -1,6 +1,7 @@ package app.omnivore.omnivore.ui.reader import android.util.Log +import androidx.compose.foundation.ScrollState import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope @@ -27,6 +28,7 @@ class WebReaderViewModel @Inject constructor( private val datastoreRepo: DatastoreRepository, private val networker: Networker ): ViewModel() { + var scrollState = ScrollState(0) val webReaderParamsLiveData = MutableLiveData(null) val annotationLiveData = MutableLiveData(null) @@ -90,6 +92,7 @@ class WebReaderViewModel @Inject constructor( fun reset() { webReaderParamsLiveData.value = null annotationLiveData.value = null + scrollState = ScrollState(0) } fun cancelAnnotationEdit() { From 80ba68768e3c9362745e16c071093168b6413d70 Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Tue, 11 Oct 2022 14:12:53 -0700 Subject: [PATCH 03/53] scale nav bar height as web reader is scrolled --- .../omnivore/omnivore/ui/reader/WebReader.kt | 24 ++++++++----------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/WebReader.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/WebReader.kt index 2927040e4..8e30c80c4 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/WebReader.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/WebReader.kt @@ -25,10 +25,8 @@ import androidx.compose.ui.input.nestedscroll.NestedScrollConnection import androidx.compose.ui.input.nestedscroll.NestedScrollSource import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.dp import androidx.compose.ui.viewinterop.AndroidView -import androidx.lifecycle.viewmodel.compose.viewModel import app.omnivore.omnivore.R import com.google.gson.Gson import kotlin.math.roundToInt @@ -38,19 +36,17 @@ import kotlin.math.roundToInt fun WebReaderLoadingContainer(slug: String, webReaderViewModel: WebReaderViewModel) { val webReaderParams: WebReaderParams? by webReaderViewModel.webReaderParamsLiveData.observeAsState(null) - val toolbarHeight = 48.dp - val toolbarHeightPx = with(LocalDensity.current) { toolbarHeight.roundToPx().toFloat() } - - // Offset to collapse toolbar - val toolbarOffsetHeightPx = remember { mutableStateOf(0f) } + val maxToolbarHeight = 48.dp + val maxToolbarHeightPx = with(LocalDensity.current) { maxToolbarHeight.roundToPx().toFloat() } + val toolbarHeightPx = remember { mutableStateOf(maxToolbarHeightPx) } // Create a connection to the nested scroll system and listen to the scroll happening inside child Column val nestedScrollConnection = remember { object : NestedScrollConnection { override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset { val delta = available.y - val newOffset = toolbarOffsetHeightPx.value + delta - toolbarOffsetHeightPx.value = newOffset.coerceIn(-toolbarHeightPx, 0f) + val newHeight = toolbarHeightPx.value + delta + toolbarHeightPx.value = newHeight.coerceIn(0f, maxToolbarHeightPx) return Offset.Zero } } @@ -75,7 +71,7 @@ fun WebReaderLoadingContainer(slug: String, webReaderViewModel: WebReaderViewMod Row( modifier = Modifier .fillMaxWidth() - .requiredHeight(height = toolbarHeight) + .requiredHeight(height = maxToolbarHeight) ) { } WebReader(webReaderParams!!, webReaderViewModel) @@ -83,12 +79,12 @@ fun WebReaderLoadingContainer(slug: String, webReaderViewModel: WebReaderViewMod TopAppBar( modifier = Modifier - .height(height = toolbarHeight) - .offset { IntOffset(x = 0, y = toolbarOffsetHeightPx.value.roundToInt()) }, + .height(height = with(LocalDensity.current) { + toolbarHeightPx.value.roundToInt().toDp() + } ), backgroundColor = MaterialTheme.colorScheme.surfaceVariant, - elevation = (-10).dp, title = {}, - navigationIcon = { + actions = { IconButton(onClick = {}) { Icon( imageVector = Icons.Filled.Settings, From 883ee2a431e7beb694a5543f2fee472a463ac2a0 Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Tue, 11 Oct 2022 15:56:07 -0700 Subject: [PATCH 04/53] structure web preerences into their own data class --- .../omnivore/omnivore/ui/reader/WebReader.kt | 46 ++++++++++++++----- .../omnivore/ui/reader/WebReaderContent.kt | 15 +++--- 2 files changed, 40 insertions(+), 21 deletions(-) diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/WebReader.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/WebReader.kt index 8e30c80c4..f0176c790 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/WebReader.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/WebReader.kt @@ -14,11 +14,8 @@ import androidx.compose.material.TopAppBar import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Settings import androidx.compose.material3.* -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue +import androidx.compose.runtime.* import androidx.compose.runtime.livedata.observeAsState -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.Offset import androidx.compose.ui.input.nestedscroll.NestedScrollConnection @@ -27,6 +24,7 @@ import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.unit.dp import androidx.compose.ui.viewinterop.AndroidView +import androidx.lifecycle.viewmodel.compose.viewModel import app.omnivore.omnivore.R import com.google.gson.Gson import kotlin.math.roundToInt @@ -34,6 +32,19 @@ import kotlin.math.roundToInt @Composable fun WebReaderLoadingContainer(slug: String, webReaderViewModel: WebReaderViewModel) { + // TODO: maybe move to web reader view model? + val defaultWebPreferences = WebPreferences( + textFontSize = 12, + lineHeight = 150, + maxWidthPercentage = 100, + themeKey = "LightGray", + fontFamily = WebFont.SYSTEM, + prefersHighContrastText = false + ) + + var showWebPreferencesDialog by remember { mutableStateOf(false ) } + var webPreferences by remember { mutableStateOf(defaultWebPreferences ) } + val webReaderParams: WebReaderParams? by webReaderViewModel.webReaderParamsLiveData.observeAsState(null) val maxToolbarHeight = 48.dp @@ -74,7 +85,7 @@ fun WebReaderLoadingContainer(slug: String, webReaderViewModel: WebReaderViewMod .requiredHeight(height = maxToolbarHeight) ) { } - WebReader(webReaderParams!!, webReaderViewModel) + WebReader(webReaderParams!!, webPreferences, webReaderViewModel) } TopAppBar( @@ -85,7 +96,7 @@ fun WebReaderLoadingContainer(slug: String, webReaderViewModel: WebReaderViewMod backgroundColor = MaterialTheme.colorScheme.surfaceVariant, title = {}, actions = { - IconButton(onClick = {}) { + IconButton(onClick = { showWebPreferencesDialog = true }) { Icon( imageVector = Icons.Filled.Settings, contentDescription = null @@ -93,6 +104,17 @@ fun WebReaderLoadingContainer(slug: String, webReaderViewModel: WebReaderViewMod } } ) + + if (showWebPreferencesDialog) { + WebPreferencesDialog { preferences -> + if (preferences != null) { + webPreferences = preferences!! + showWebPreferencesDialog = false + } + + showWebPreferencesDialog = false + } + } } } else { // TODO: add a proper loading view @@ -102,7 +124,11 @@ fun WebReaderLoadingContainer(slug: String, webReaderViewModel: WebReaderViewMod @SuppressLint("SetJavaScriptEnabled") @Composable -fun WebReader(params: WebReaderParams, webReaderViewModel: WebReaderViewModel) { +fun WebReader( + params: WebReaderParams, + preferences: WebPreferences, + webReaderViewModel: WebReaderViewModel +) { // TODO: maybe handle cases where js can be queued up? val javascriptToExecute = remember { mutableStateOf(null) } @@ -111,14 +137,10 @@ fun WebReader(params: WebReaderParams, webReaderViewModel: WebReaderViewModel) { WebView.setWebContentsDebuggingEnabled(true) val webReaderContent = WebReaderContent( - textFontSize = 12, - lineHeight = 150, - maxWidthPercentage = 100, + preferences = preferences, item = params.item, themeKey = "LightGray", - fontFamily = WebFont.SYSTEM , articleContent = params.articleContent, - prefersHighContrastText = false, ) val styledContent = webReaderContent.styledContent() diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/WebReaderContent.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/WebReaderContent.kt index 599c55078..45aee447a 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/WebReaderContent.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/WebReaderContent.kt @@ -39,20 +39,17 @@ data class ArticleContent( } data class WebReaderContent( - val textFontSize: Int, - val lineHeight: Int, - val maxWidthPercentage: Int, + val preferences: WebPreferences, val item: LinkedItem, val themeKey: String, - val fontFamily: WebFont, val articleContent: ArticleContent, - val prefersHighContrastText: Boolean ) { fun styledContent(): String { // TODO: Kotlinize these three values (pasted from Swift) val savedAt = "new Date(1662571290735.0).toISOString()" val createdAt = "new Date().toISOString()" val publishedAt = "new Date().toISOString()" //if (item.publishDate != null) "new Date((item.publishDate!.timeIntervalSince1970 * 1000)).toISOString()" else "undefined" + val textFontSize = preferences.textFontSize val content = """ @@ -96,11 +93,11 @@ data class WebReaderContent( } window.fontSize = $textFontSize - window.fontFamily = "${fontFamily.rawValue}" - window.maxWidthPercentage = $maxWidthPercentage - window.lineHeight = $lineHeight + window.fontFamily = "${preferences.fontFamily.rawValue}" + window.maxWidthPercentage = $preferences.maxWidthPercentage + window.lineHeight = $preferences.lineHeight window.localStorage.setItem("theme", "$themeKey") - window.prefersHighContrastFont = $prefersHighContrastText + window.prefersHighContrastFont = $preferences.prefersHighContrastText window.enableHighlightBar = false From 3142ff82c52ab576fc29402f249e1ea68fbf3b63 Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Tue, 11 Oct 2022 15:56:21 -0700 Subject: [PATCH 05/53] add a web prefs dialog --- .../ui/reader/WebPreferencesDialog.kt | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/WebPreferencesDialog.kt diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/WebPreferencesDialog.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/WebPreferencesDialog.kt new file mode 100644 index 000000000..d2d1e31e4 --- /dev/null +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/WebPreferencesDialog.kt @@ -0,0 +1,35 @@ +package app.omnivore.omnivore.ui.reader + +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.Text +import androidx.compose.material3.Surface +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Dialog + +@Composable +fun WebPreferencesDialog(onDismiss: (WebPreferences?) -> Unit) { + Dialog(onDismissRequest = { onDismiss(null) }) { + Surface( + shape = RoundedCornerShape(16.dp), + color = Color.White + ) { + WebPreferencesView(onDismiss) + } + } +} + +@Composable +fun WebPreferencesView(onDismiss: (WebPreferences?) -> Unit) { + Text("Web Prefs") +} + +data class WebPreferences( + val textFontSize: Int, + val lineHeight: Int, + val maxWidthPercentage: Int, + val themeKey: String, + val fontFamily: WebFont, + val prefersHighContrastText: Boolean +) From 50681697156e9c9e70777894a4121d9149048f94 Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Tue, 11 Oct 2022 22:09:10 -0700 Subject: [PATCH 06/53] use queue of js ops in web reader view model --- .../omnivore/omnivore/ui/reader/WebReader.kt | 19 +++++++++++-------- .../omnivore/ui/reader/WebReaderViewModel.kt | 18 ++++++++++++++++++ 2 files changed, 29 insertions(+), 8 deletions(-) diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/WebReader.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/WebReader.kt index f0176c790..d08c4b857 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/WebReader.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/WebReader.kt @@ -27,6 +27,7 @@ import androidx.compose.ui.viewinterop.AndroidView import androidx.lifecycle.viewmodel.compose.viewModel import app.omnivore.omnivore.R import com.google.gson.Gson +import java.util.* import kotlin.math.roundToInt @@ -129,11 +130,12 @@ fun WebReader( preferences: WebPreferences, webReaderViewModel: WebReaderViewModel ) { - // TODO: maybe handle cases where js can be queued up? - val javascriptToExecute = remember { mutableStateOf(null) } - val annotation: String? by webReaderViewModel.annotationLiveData.observeAsState(null) + val javascriptActionLoopUUID: UUID by webReaderViewModel + .javascriptActionLoopUUIDLiveData + .observeAsState(UUID.randomUUID()) + WebView.setWebContentsDebuggingEnabled(true) val webReaderContent = WebReaderContent( @@ -186,8 +188,11 @@ fun WebReader( ) } }, update = { - if (javascriptToExecute.value != null) { - it.evaluateJavascript(javascriptToExecute.value!!, null) + if (javascriptActionLoopUUID != webReaderViewModel.lastJavascriptActionLoopUUID) { + for (script in webReaderViewModel.javascriptDispatchQueue) { + it.evaluateJavascript(script, null) + } + webReaderViewModel.resetJavascriptDispatchQueue() } }) @@ -195,9 +200,7 @@ fun WebReader( AnnotationEditView( initialAnnotation = annotation!!, onSave = { - val script = "var event = new Event('saveAnnotation');event.annotation = '$it';document.dispatchEvent(event);" - javascriptToExecute.value = script - webReaderViewModel.cancelAnnotationEdit() + webReaderViewModel.saveAnnotation(it) }, onCancel = { webReaderViewModel.cancelAnnotationEdit() diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/WebReaderViewModel.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/WebReaderViewModel.kt index 7a5bc60c6..d1f2c7ed9 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/WebReaderViewModel.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/WebReaderViewModel.kt @@ -12,6 +12,7 @@ import com.google.gson.Gson import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.launch import org.json.JSONObject +import java.util.* import javax.inject.Inject data class WebReaderParams( @@ -28,9 +29,13 @@ class WebReaderViewModel @Inject constructor( private val datastoreRepo: DatastoreRepository, private val networker: Networker ): ViewModel() { + var lastJavascriptActionLoopUUID = UUID.randomUUID() + var javascriptDispatchQueue: MutableList = mutableListOf() var scrollState = ScrollState(0) + val webReaderParamsLiveData = MutableLiveData(null) val annotationLiveData = MutableLiveData(null) + val javascriptActionLoopUUIDLiveData = MutableLiveData(lastJavascriptActionLoopUUID) fun loadItem(slug: String) { viewModelScope.launch { @@ -93,6 +98,19 @@ class WebReaderViewModel @Inject constructor( webReaderParamsLiveData.value = null annotationLiveData.value = null scrollState = ScrollState(0) + javascriptDispatchQueue = mutableListOf() + } + + fun resetJavascriptDispatchQueue() { + lastJavascriptActionLoopUUID = javascriptActionLoopUUIDLiveData.value + javascriptDispatchQueue = mutableListOf() + } + + fun saveAnnotation(annotation: String) { + val script = "var event = new Event('saveAnnotation');event.annotation = '$annotation';document.dispatchEvent(event);" + javascriptDispatchQueue.add(script) + javascriptActionLoopUUIDLiveData.value = UUID.randomUUID() + cancelAnnotationEdit() } fun cancelAnnotationEdit() { From 84de7ea640b8ee0debeed54a34fd8b458cfd509a Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Tue, 11 Oct 2022 22:13:31 -0700 Subject: [PATCH 07/53] move annotation view into web reader container --- .../omnivore/omnivore/ui/reader/WebReader.kt | 27 +++++++++---------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/WebReader.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/WebReader.kt index d08c4b857..8aeb8e619 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/WebReader.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/WebReader.kt @@ -47,6 +47,7 @@ fun WebReaderLoadingContainer(slug: String, webReaderViewModel: WebReaderViewMod var webPreferences by remember { mutableStateOf(defaultWebPreferences ) } val webReaderParams: WebReaderParams? by webReaderViewModel.webReaderParamsLiveData.observeAsState(null) + val annotation: String? by webReaderViewModel.annotationLiveData.observeAsState(null) val maxToolbarHeight = 48.dp val maxToolbarHeightPx = with(LocalDensity.current) { maxToolbarHeight.roundToPx().toFloat() } @@ -116,6 +117,18 @@ fun WebReaderLoadingContainer(slug: String, webReaderViewModel: WebReaderViewMod showWebPreferencesDialog = false } } + + if (annotation != null) { + AnnotationEditView( + initialAnnotation = annotation!!, + onSave = { + webReaderViewModel.saveAnnotation(it) + }, + onCancel = { + webReaderViewModel.cancelAnnotationEdit() + } + ) + } } } else { // TODO: add a proper loading view @@ -130,8 +143,6 @@ fun WebReader( preferences: WebPreferences, webReaderViewModel: WebReaderViewModel ) { - val annotation: String? by webReaderViewModel.annotationLiveData.observeAsState(null) - val javascriptActionLoopUUID: UUID by webReaderViewModel .javascriptActionLoopUUIDLiveData .observeAsState(UUID.randomUUID()) @@ -195,18 +206,6 @@ fun WebReader( webReaderViewModel.resetJavascriptDispatchQueue() } }) - - if (annotation != null) { - AnnotationEditView( - initialAnnotation = annotation!!, - onSave = { - webReaderViewModel.saveAnnotation(it) - }, - onCancel = { - webReaderViewModel.cancelAnnotationEdit() - } - ) - } } } From 41e5d3000b6ecba8c8448ce54868886590493b93 Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Wed, 12 Oct 2022 08:04:14 -0700 Subject: [PATCH 08/53] remove param from web pref dialog onDismiss call --- .../omnivore/ui/reader/WebPreferencesDialog.kt | 8 ++++---- .../java/app/omnivore/omnivore/ui/reader/WebReader.kt | 10 ++-------- 2 files changed, 6 insertions(+), 12 deletions(-) diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/WebPreferencesDialog.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/WebPreferencesDialog.kt index d2d1e31e4..d576e2164 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/WebPreferencesDialog.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/WebPreferencesDialog.kt @@ -9,19 +9,19 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.window.Dialog @Composable -fun WebPreferencesDialog(onDismiss: (WebPreferences?) -> Unit) { - Dialog(onDismissRequest = { onDismiss(null) }) { +fun WebPreferencesDialog(onDismiss: () -> Unit) { + Dialog(onDismissRequest = { onDismiss() }) { Surface( shape = RoundedCornerShape(16.dp), color = Color.White ) { - WebPreferencesView(onDismiss) + WebPreferencesView() } } } @Composable -fun WebPreferencesView(onDismiss: (WebPreferences?) -> Unit) { +fun WebPreferencesView() { Text("Web Prefs") } diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/WebReader.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/WebReader.kt index 8aeb8e619..9a0b6bd36 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/WebReader.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/WebReader.kt @@ -44,7 +44,6 @@ fun WebReaderLoadingContainer(slug: String, webReaderViewModel: WebReaderViewMod ) var showWebPreferencesDialog by remember { mutableStateOf(false ) } - var webPreferences by remember { mutableStateOf(defaultWebPreferences ) } val webReaderParams: WebReaderParams? by webReaderViewModel.webReaderParamsLiveData.observeAsState(null) val annotation: String? by webReaderViewModel.annotationLiveData.observeAsState(null) @@ -87,7 +86,7 @@ fun WebReaderLoadingContainer(slug: String, webReaderViewModel: WebReaderViewMod .requiredHeight(height = maxToolbarHeight) ) { } - WebReader(webReaderParams!!, webPreferences, webReaderViewModel) + WebReader(webReaderParams!!, defaultWebPreferences, webReaderViewModel) } TopAppBar( @@ -108,12 +107,7 @@ fun WebReaderLoadingContainer(slug: String, webReaderViewModel: WebReaderViewMod ) if (showWebPreferencesDialog) { - WebPreferencesDialog { preferences -> - if (preferences != null) { - webPreferences = preferences!! - showWebPreferencesDialog = false - } - + WebPreferencesDialog { showWebPreferencesDialog = false } } From fcef8e14e7142189fc9bc3bdb7c0c1e04f9ff928 Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Fri, 14 Oct 2022 13:44:04 -0700 Subject: [PATCH 09/53] make sure analytics is setup before calling show on Intercom --- packages/web/pages/support.tsx | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/packages/web/pages/support.tsx b/packages/web/pages/support.tsx index 78d295140..bd6a68a16 100644 --- a/packages/web/pages/support.tsx +++ b/packages/web/pages/support.tsx @@ -1,11 +1,21 @@ -import { useEffect } from 'react' +import { useEffect, useCallback } from 'react' import { SettingsLayout } from '../components/templates/SettingsLayout' +import { setupAnalytics } from '../lib/analytics' export default function Support(): JSX.Element { - useEffect(() => { + const initAnalytics = useCallback(() => { + setupAnalytics() window.Intercom('show') }, []) + useEffect(() => { + initAnalytics() + window.addEventListener('load', initAnalytics) + return () => { + window.removeEventListener('load', initAnalytics) + } + }, [initAnalytics]) + return ( <> From b42ef5a5bc9e8a03250d1f164e9e8bd7988a0004 Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Fri, 14 Oct 2022 14:50:00 -0700 Subject: [PATCH 10/53] only call Intercom after load event is dispatched --- packages/web/pages/support.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/web/pages/support.tsx b/packages/web/pages/support.tsx index bd6a68a16..6a5915767 100644 --- a/packages/web/pages/support.tsx +++ b/packages/web/pages/support.tsx @@ -9,7 +9,6 @@ export default function Support(): JSX.Element { }, []) useEffect(() => { - initAnalytics() window.addEventListener('load', initAnalytics) return () => { window.removeEventListener('load', initAnalytics) From 6a063acb23a04f6d5b95882410d5033644828cf1 Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Fri, 14 Oct 2022 15:01:02 -0700 Subject: [PATCH 11/53] add button to open chat in case it fails to load initially --- packages/web/pages/support.tsx | 30 +++++++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/packages/web/pages/support.tsx b/packages/web/pages/support.tsx index 6a5915767..98e25f366 100644 --- a/packages/web/pages/support.tsx +++ b/packages/web/pages/support.tsx @@ -1,4 +1,6 @@ import { useEffect, useCallback } from 'react' +import { Button } from '../components/elements/Button' +import { HStack } from '../components/elements/LayoutPrimitives' import { SettingsLayout } from '../components/templates/SettingsLayout' import { setupAnalytics } from '../lib/analytics' @@ -17,7 +19,33 @@ export default function Support(): JSX.Element { return ( - <> + + + ) } From 53c40c15509e7ee80795aded0acdeca256148334 Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Sat, 15 Oct 2022 19:51:57 -0700 Subject: [PATCH 12/53] use nsworkspace to deep link to mac app --- .../App/AppExtensions/Share/ShareExtensionViewModel.swift | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/apple/OmnivoreKit/Sources/App/AppExtensions/Share/ShareExtensionViewModel.swift b/apple/OmnivoreKit/Sources/App/AppExtensions/Share/ShareExtensionViewModel.swift index be5320762..9fb741e57 100644 --- a/apple/OmnivoreKit/Sources/App/AppExtensions/Share/ShareExtensionViewModel.swift +++ b/apple/OmnivoreKit/Sources/App/AppExtensions/Share/ShareExtensionViewModel.swift @@ -22,7 +22,13 @@ public class ShareExtensionViewModel: ObservableObject { let deepLinkUrl = NSURL(string: "omnivore://shareExtensionRequestID/\(requestId)") application.perform(NSSelectorFromString("openURL:"), with: deepLinkUrl) } + #else + if let workspace = NSWorkspace.value(forKeyPath: #keyPath(NSWorkspace.shared)) as? NSWorkspace { + let deepLinkUrl = NSURL(string: "omnivore://shareExtensionRequestID/\(requestId)") + workspace.perform(NSSelectorFromString("openURL:"), with: deepLinkUrl) + } #endif + extensionContext?.completeRequest(returningItems: [], completionHandler: nil) } From 68a304664c7b549de6dbf62db640adbd96f68e56 Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Mon, 17 Oct 2022 15:13:05 +0800 Subject: [PATCH 13/53] Empty state for the Highlights View --- .../Views/Highlights/HighlightsListView.swift | 44 ++++++++++++------- .../App/Views/Home/HomeFeedViewIOS.swift | 2 +- .../Views/WebReader/WebReaderContainer.swift | 2 +- .../Sources/Views/FeedItem/GridCard.swift | 2 +- 4 files changed, 31 insertions(+), 19 deletions(-) diff --git a/apple/OmnivoreKit/Sources/App/Views/Highlights/HighlightsListView.swift b/apple/OmnivoreKit/Sources/App/Views/Highlights/HighlightsListView.swift index b16a0c3bf..50de7f421 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Highlights/HighlightsListView.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Highlights/HighlightsListView.swift @@ -12,7 +12,35 @@ struct HighlightsListView: View { let itemObjectID: NSManagedObjectID @Binding var hasHighlightMutations: Bool + var emptyView: some View { + Text(""" + You have not added any highlights to this page. + """) + .multilineTextAlignment(.center) + .padding(16) + } + var innerBody: some View { + (viewModel.highlightItems.count > 0 ? AnyView(listView) : AnyView(emptyView)) + .navigationTitle("Highlights & Notes") + .listStyle(PlainListStyle()) + #if os(iOS) + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .navigationBarTrailing) { + dismissButton + } + } + #else + .toolbar { + ToolbarItemGroup { + dismissButton + } + } + #endif + } + + var listView: some View { List { Section { ForEach(viewModel.highlightItems) { highlightParams in @@ -38,22 +66,6 @@ struct HighlightsListView: View { } } } - .navigationTitle("Highlights") - .listStyle(PlainListStyle()) - #if os(iOS) - .navigationBarTitleDisplayMode(.inline) - .toolbar { - ToolbarItem(placement: .navigationBarTrailing) { - dismissButton - } - } - #else - .toolbar { - ToolbarItemGroup { - dismissButton - } - } - #endif } var dismissButton: some View { diff --git a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift index e6a508038..37f7df362 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift @@ -294,7 +294,7 @@ import Views .contextMenu { Button( action: { viewModel.itemForHighlightsView = item }, - label: { Label("View Highlights", systemImage: "highlighter") } + label: { Label("View Highlights & Notes", systemImage: "highlighter") } ) Button( action: { viewModel.itemUnderTitleEdit = item }, diff --git a/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderContainer.swift b/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderContainer.swift index 00891c3d0..31e410553 100644 --- a/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderContainer.swift +++ b/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderContainer.swift @@ -152,7 +152,7 @@ struct WebReaderContainerView: View { Group { Button( action: { showHighlightsView = true }, - label: { Label("View Highlights", systemImage: "highlighter") } + label: { Label("View Highlights & Notes", systemImage: "highlighter") } ) Button( action: { showTitleEdit = true }, diff --git a/apple/OmnivoreKit/Sources/Views/FeedItem/GridCard.swift b/apple/OmnivoreKit/Sources/Views/FeedItem/GridCard.swift index 99223de4d..57e7e86c1 100644 --- a/apple/OmnivoreKit/Sources/Views/FeedItem/GridCard.swift +++ b/apple/OmnivoreKit/Sources/Views/FeedItem/GridCard.swift @@ -48,7 +48,7 @@ public struct GridCard: View { Group { Button( action: { menuActionHandler(.viewHighlights) }, - label: { Label("View Highlights", systemImage: "highlighter") } + label: { Label("View Highlights & Notes", systemImage: "highlighter") } ) Button( action: { menuActionHandler(.editTitle) }, From fa35168bb45f2d49a358e5bdfe897f68a3d5b0d9 Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Mon, 17 Oct 2022 16:54:37 +0800 Subject: [PATCH 14/53] Display subscription icons in the iOS subscription view --- .../App/Views/Profile/Subscriptions.swift | 25 +++++++++++++++++-- .../Models/DataModels/Subscription.swift | 5 +++- .../Queries/SubscriptionsQuery.swift | 3 ++- 3 files changed, 29 insertions(+), 4 deletions(-) diff --git a/apple/OmnivoreKit/Sources/App/Views/Profile/Subscriptions.swift b/apple/OmnivoreKit/Sources/App/Views/Profile/Subscriptions.swift index 69d11b7a8..9fa414f79 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Profile/Subscriptions.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Profile/Subscriptions.swift @@ -123,7 +123,7 @@ struct SubscriptionCell: View { let subscription: Subscription var body: some View { - VStack { + HStack { VStack(alignment: .leading, spacing: 6) { Text(subscription.name) .font(.appCallout) @@ -140,7 +140,28 @@ struct SubscriptionCell: View { } .multilineTextAlignment(.leading) .padding(.vertical, 8) - .frame(minHeight: 50) + + Spacer() + + Group { + if let icon = subscription.icon, let imageURL = URL(string: icon) { + AsyncImage(url: imageURL) { phase in + if let image = phase.image { + image + .resizable() + .aspectRatio(contentMode: .fill) + .frame(width: 40, height: 40) + .cornerRadius(6) + } else if phase.error != nil { + EmptyView().frame(width: 40, height: 40, alignment: .top) + } else { + Color.appButtonBackground + .frame(width: 40, height: 40) + .cornerRadius(2) + } + } + } + }.frame(minHeight: 50) } } } diff --git a/apple/OmnivoreKit/Sources/Models/DataModels/Subscription.swift b/apple/OmnivoreKit/Sources/Models/DataModels/Subscription.swift index 10d6208a9..6179318df 100644 --- a/apple/OmnivoreKit/Sources/Models/DataModels/Subscription.swift +++ b/apple/OmnivoreKit/Sources/Models/DataModels/Subscription.swift @@ -11,6 +11,7 @@ public struct Subscription { public let unsubscribeMailTo: String? public let updatedAt: Date? public let url: String? + public let icon: String? public init( createdAt: Date?, @@ -22,7 +23,8 @@ public struct Subscription { unsubscribeHttpUrl: String?, unsubscribeMailTo: String?, updatedAt: Date?, - url: String? + url: String?, + icon: String? ) { self.createdAt = createdAt self.description = description @@ -34,6 +36,7 @@ public struct Subscription { self.unsubscribeMailTo = unsubscribeMailTo self.updatedAt = updatedAt self.url = url + self.icon = icon } } diff --git a/apple/OmnivoreKit/Sources/Services/DataService/Queries/SubscriptionsQuery.swift b/apple/OmnivoreKit/Sources/Services/DataService/Queries/SubscriptionsQuery.swift index 2c2976126..a78045904 100644 --- a/apple/OmnivoreKit/Sources/Services/DataService/Queries/SubscriptionsQuery.swift +++ b/apple/OmnivoreKit/Sources/Services/DataService/Queries/SubscriptionsQuery.swift @@ -20,7 +20,8 @@ public extension DataService { unsubscribeHttpUrl: try $0.unsubscribeHttpUrl(), unsubscribeMailTo: try $0.unsubscribeMailTo(), updatedAt: try $0.updatedAt().value, - url: try $0.url() + url: try $0.url(), + icon: try $0.icon() ) } From 08a1d4af91848baeb454b5230ba7a72444eb2454 Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Mon, 17 Oct 2022 17:10:27 +0800 Subject: [PATCH 15/53] Update the Getting started guide for new users --- .../omnivore_get_started-content.html | 423 +++- .../omnivore_get_started-original.html | 1744 ++++++++++----- .../expected-metadata.json | 4 +- .../omnivore_getting_started/expected.html | 271 ++- .../omnivore_getting_started/source.html | 1984 ++++++++++++++--- .../omnivore_getting_started/url.txt | 2 +- 6 files changed, 3337 insertions(+), 1091 deletions(-) diff --git a/packages/api/src/services/popular_reads/omnivore_get_started-content.html b/packages/api/src/services/popular_reads/omnivore_get_started-content.html index 1e214dd7e..843076a29 100644 --- a/packages/api/src/services/popular_reads/omnivore_get_started-content.html +++ b/packages/api/src/services/popular_reads/omnivore_get_started-content.html @@ -1,103 +1,330 @@
-
-

Omnivore is a home for everything you read. We keep it safe, organized, and easy to share.

-

When you start using Omnivore, it is important to figure out the best way to save content to your library.

-

- Saving from your iPhone -

-

If you are using an iPhone or iPad, the best way to save links is by installing the iOS app. You can find the iOS app here:

-

- https://omnivore.app/install/ios -

-

With the iOS Share extension installed, you can save links from Safari or any other app that supports sharing links.

+
-
- - Saving with the Omnivore iOS Share Extension - -
Saving with the Omnivore iOS Share Extension
-
+

Omnivore is a read-it-later app that lets you save and organize everything you read online.

-

When you first install the app, Omnivore might not show up in your list of share targets. To make sure Omnivore is easy to access, scroll to the far right, click More, and add Omnivore.

-

Saving from your Android Device

-

If you are using an Android device you can install the Omnivore Progressive Web App. After logging in to Omnivore in Chrome you should see an “Install Omnivore” option. Most Android versions display this at the bottom of the screen.

-
-
- - - -
+
+

This guide will show you how to use Omnivore’s basic functions and advanced features, divided into four main activities:

+
    +
  • +

    Saving

    +
  • +
  • +

    Reading

    +
  • +
  • +

    Organizing

    +
  • +
  • +

    Integrations

    +
  • +
+

+ The Library is the center of your Omnivore experience, where you can quickly access any links you have saved. Saved links remain in your Library forever unless you delete them. +

+

There are five ways to save links to pages or articles that you wish to read later:

+
    +
  • +

    Saving from Your Omnivore Library

    +
  • +
  • +

    Saving from a Browser 

    +
  • +
  • +

    Saving from a Phone or Tablet (iOS or Android)

    +
  • +
  • +

    Newsletter Subscriptions via Email

    +
  • +
  • +

    + Saving PDFs from a Mac
    +

    +
  • +
+

Saving from Your Omnivore Library

+

+ 1. In the upper right corner of your Library, tap the Add Link button.
+ 2. Enter the URL you wish to save and tap Add Link.
+ 3. The link will appear in your Library the next time you refresh it.
+

+

Saving from a Browser

+

1. Download and install the Omnivore extension for your browser:

+ +

+ 2. Navigate to the page you wish to save and tap the Omnivore button in your browser’s toolbar or Extensions menu.
+ 3. Alternatively, you can right-click (command+click on Mac) on any hyperlink and select Save to Omnivore from the menu.
+ 4. The link will appear in your Library the next time you refresh it.
+

+

Saving from a Phone or Tablet

+

The best way to save links from your mobile device is via the Omnivore app. You can download the app here:

+ +

Once the mobile app is installed:

+
    +
  1. +

    + In your browser, navigate to the page you wish to save and tap the Share button. +

    +
  2. +
  3. +

    + Tap the Omnivore icon in the Share menu. +

    +
  4. +
  5. +

    The link will appear in your Library the next time you refresh it.

    +
  6. +
+

Newsletter Subscriptions via Email

+

+ 1. On the Omnivore website or app, tap your photo, initial, or avatar in the top right corner to access the profile menu. Select Emails from the menu. +

+

+ 2. Tap Create a New Email Address to add a new email address (ex: username-123abc@inbox.omnivore.app) to the list. +

+

3. Click the Copy icon next to the email address.

+

+ 4. Navigate to the signup page for the newsletter you wish to subscribe to.
+ 5. Paste the Omnivore email address into the signup form. +

+

6. New newsletters will be automatically delivered to your Omnivore inbox.

+

Saving PDFs from a Mac 

+
    +
  1. +

    + Install the Mac App +

    +
  2. +
  3. +

    On your Mac, locate the PDF you wish to save and right-click or ctrl+click on the file name.

    +
  4. +
  5. +

    + Select Share from the menu and choose Omnivore. +

    +
  6. +
  7. +

    The link will appear in your Library the next time you refresh it.

    +
  8. +
+

Reading

+

Click any link saved in your Library to enter the Reader view. 

+

Omnivore formats pages for easy reading and highlighting, removing ads and clutter for distraction-free reading. The text-focused view also makes articles smaller and quicker to load.

+

While reading, you can:

+
    +
  • +

    Change Formatting

    +
  • +
  • +

    Highlight Text

    +
  • +
  • +

    Add Notes

    +
  • +
  • +

    View All Saved Highlights and Notes

    +
  • +
  • +

    Track Reading Progress

    +
  • +
+

Change Formatting 

+
    +
  1. +

    + Theme: Tap your photo, initial, or avatar  in the top right corner to access the profile menu. Select the white or black thumbnail to choose the Light or Dark theme. +

    +
  2. +
  3. +

    + Text Formatting: Tap the Aa icon to adjust the text size, font, margins, and line spacing. +

    +
  4. +
+

Highlight Text

+
    +
  1. +

    Select the text you wish to highlight.

    +
  2. +
  3. +

    + Tap the Highlight button. +

    +
  4. +
  5. +

    The text will appear highlighted next time you view the article.

    +
  6. +
+

Add Notes

+
    +
  1. +

    Highlight a section of text where you wish to add a note.

    +
  2. +
  3. +

    + Tap the Note button, type your note, and tap Save. +

    +
  4. +
  5. +

    The Note icon will appear next time you view this article.

    +
  6. +
+

View All Saved Highlights and Notes

+
    +
  1. +

    Tap the Highlight/Note icon to see a list of all the highlighted text and notes you have added to this page.

    +
  2. +
  3. +

    To remove a note or highlight, select it from the list and tap the Trash icon.

    +
  4. +
+

Track Reading Progress

+

Omnivore automatically keeps track of your reading progress across your different devices so you can easily pick up where you left off. A progress bar will appear at the top of each link in your Library after you have started reading.

+

Organizing

+

By default, the Library inbox displays all links you have saved. To manage your list and keep your reading organized, Omnivore provides the following actions: 

+
    +
  • +

    Archiving

    +
  • +
  • +

    Labels

    +
  • +
  • +

    Search

    +
  • +
  • +

    Filters

    +
  • +
+

Archiving

+
    +
  1. +

    Tap the Menu icon next to the link you wish to archive (on the mobile app, long press the link to open the menu).

    +
  2. +
  3. +

    + Select Archive. +

    +
  4. +
  5. +

    The link will disappear from the default Library view, but will show up if you select the Archived filter (see Filters below).

    +
  6. +
+

+ Labels +

+
    +
  1. +

    + Tap the Menu icon next to any link and select Set Labels. +

    +
  2. +
  3. +

    + Select an existing label from the list or tap Edit Labels to create a new one. +

    +
  4. +
  5. +

    The label will appear next to the link in your Library. Tap it to view all links with the same label.

    +
  6. +
  7. +

    + Omnivore mobile app only: tap Labels to see a complete list of all labels you have used; tap one to view all links with the same label +

    +
  8. +
  9. +

    Note: Omnivore will automatically assign some labels, such as “Newsletters.”

    +
  10. +
+

Search

+
    +
  1. +

    To search through all your saved links, enter a keyword or phrase in the search bar. 

    +
  2. +
  3. +

    + You can combine keywords with labels and filters to focus your search even further. Learn more about advanced search. +

    +
  4. +
+

Filters

+
    +
  1. +

    + Use the Filters menu to refine your Library view (some filters may be visible by default). +

    +
  2. +
  3. +

    + Select Read Later to view a list of all your non-archived links except Newsletters. +

    +
  4. +
  5. +

    + Select Highlights to view the text selections you have highlighted in all your saved pages.  +

    +
  6. +
  7. +

    + Select Today to view a list of links you saved today. +

    +
  8. +
  9. +

    + Select Newsletters to view links saved via your newsletter subscriptions. +

    +
  10. +
+

Integrations

+

Omnivore allows integrations with knowledge bases and note-taking apps including:

+
    +
  • +

    Logseq

    +
  • +
  • +

    Webhooks

    +
  • +
+

Logseq

+

+ With Omnivore's Logseq plugin you can sync all your saved articles, highlights, and notes into Logseq, a popular knowledge base. For information on setting up and using the Logseq plugin, please refer to this helpful Omnivore for Logseq Plugin Guide. +

+

Webhooks

+

+ Omnivore can trigger webhooks when you save a link or add highlights to a page you are reading. This example shows webhooks being used to write all saved links to a Google Sheets spreadsheet stored on a Google Drive. +

-

After installing Omnivore as a Progressive Web App it will be displayed in your Sharing Menu on Chrome.

-
-
- - - -
-
-

- Saving from your computer -

-

If you are saving from a computer, you will need to install the Omnivore extension for the web browser(s) you use.

-

The browser extensions are available here:

- -

With the browser extension(s) of your choice installed, you can tap the Omnivore button on any page to save your link.

-
-
- - Saving with the Omnivore Browser Extension - -
Saving with the Omnivore Browser Extension
-
-
-

- Saving PDFs with the Mac App -

-

https://omnivore.app/install/mac

-

With the MacOS App installed you can upload PDFs from your computer to your Omnivore library by right-clicking and sharing to Omnivore.

-
-
- - Sharing a PDF with Omnivore - -
Sharing a PDF with Omnivore
-
-
-

You can enable sharing from Finder on the Mac in the Extensions section of System Preferences.

-
-
- - - -
-
-

Using Omnivore

-

We created a quick tour of Omnivore to get your started with all the other features. This video demonstrates search, archiving, and keyboard commands in the library and sharing highlights from the reader.

-
-
- - - Screenshot of video demonstrating Omnivore - - -
-
-
+
\ No newline at end of file diff --git a/packages/api/src/services/popular_reads/omnivore_get_started-original.html b/packages/api/src/services/popular_reads/omnivore_get_started-original.html index 8270ed1ad..c0175ab57 100644 --- a/packages/api/src/services/popular_reads/omnivore_get_started-original.html +++ b/packages/api/src/services/popular_reads/omnivore_get_started-original.html @@ -5,8 +5,8 @@ - - + + @@ -176,19 +176,22 @@ - + + - - - + + + + - - + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - -
- -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file + #nojs-banner { + position: fixed; + bottom: 0; + left: 0; + padding: 16px 16px 16px 32px; + width: 100%; + box-sizing: border-box; + background: red; + color: white; + font-family: -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; + font-size: 13px; + line-height: 13px; + } + #nojs-banner a { + color: inherit; + text-decoration: underline; + } + ]]> + + + +
+
+ + +
+ + + + +
+
+
+ +
+
+
+
+
+
+
+ +
+
+
+ +
+
+ Learn the best ways to save links with Omnivore +
+
+
+
+ Omnivore +
+
+ +
+ + + + +
+ 2 +
+ + + + + +
+
+
+
+
+ +
+
+ Highlighted <code> in Omnivore +
+
+
+
+ Omnivore +
+
+ +
+ + + + +
+ 1 +
+ + + + + +
+
+
+
+
+ +
+
+ Add to your library with your Omnivore email address +
+
+
+
+ Omnivore +
+
+ +
+ + + + +
+ 1 +
+ + + + + +
+
+
See all + + + + +
+
+
+
+
+ +
+ +
+ +
+
+
+ + + + + + + diff --git a/packages/readabilityjs/test/test-pages/omnivore_getting_started/url.txt b/packages/readabilityjs/test/test-pages/omnivore_getting_started/url.txt index 8656d054d..e34bf71bb 100644 --- a/packages/readabilityjs/test/test-pages/omnivore_getting_started/url.txt +++ b/packages/readabilityjs/test/test-pages/omnivore_getting_started/url.txt @@ -1 +1 @@ -https://blog.omnivore.app/p/d0d30ea5-49aa-4c04-8fae-c004be9c51b9 \ No newline at end of file +https://blog.omnivore.app/p/getting-started-with-omnivore-382 \ No newline at end of file From 37439c662276a213025f5f63f77333a79212c71c Mon Sep 17 00:00:00 2001 From: Hongbo Wu Date: Mon, 17 Oct 2022 17:59:27 +0800 Subject: [PATCH 16/53] Save fs.blog newsletter correctly --- .../src/newsletters/convertkit-handler.ts | 9 +- .../test/data/convertkit-newsletter.html | 1292 +++++++++++++++++ .../content-handler/test/newsletter.test.ts | 36 + 3 files changed, 1334 insertions(+), 3 deletions(-) create mode 100644 packages/content-handler/test/data/convertkit-newsletter.html diff --git a/packages/content-handler/src/newsletters/convertkit-handler.ts b/packages/content-handler/src/newsletters/convertkit-handler.ts index 72e65f5da..a77951178 100644 --- a/packages/content-handler/src/newsletters/convertkit-handler.ts +++ b/packages/content-handler/src/newsletters/convertkit-handler.ts @@ -8,10 +8,13 @@ export class ConvertkitHandler extends ContentHandler { } findNewsletterHeaderHref(dom: Document): string | undefined { - const readOnline = dom.querySelectorAll('table tr td a') + const readOnline = dom.querySelectorAll('a') let res: string | undefined = undefined readOnline.forEach((e) => { - if (e.textContent === 'View this email in your browser') { + if ( + e.textContent === 'View this email in your browser' || + e.textContent === 'Read on FS' + ) { res = e.getAttribute('href') || undefined } }) @@ -27,7 +30,7 @@ export class ConvertkitHandler extends ContentHandler { const dom = parseHTML(input.html).document return Promise.resolve( dom.querySelectorAll( - 'img[src*="convertkit.com"], img[src*="convertkit-mail.com"]' + 'img[src*="convertkit.com"], img[src*="convertkit-mail"]' ).length > 0 ) } diff --git a/packages/content-handler/test/data/convertkit-newsletter.html b/packages/content-handler/test/data/convertkit-newsletter.html new file mode 100644 index 000000000..ab2131adb --- /dev/null +++ b/packages/content-handler/test/data/convertkit-newsletter.html @@ -0,0 +1,1292 @@ + + + + + + +
+ + There are a lot of things in life that only work when you commit. + + + +  ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ + +
+ +
+

+ FS | BRAIN FOOD +

+
+ + + +
+

+ No. 494 — October 16th, 2022 — Read on FS +

+

+ Brain Food is a weekly newsletter with the insights you need.
+

+

+ FS +

+

+ One of the biggest things that hold people back they're not aware + of: +

+

+ When we want to improve our value to an organization, we gravitate + toward the skills we need to develop, working extra hours or increasing + our responsibilities. We miss the secret hiding in plain sight: reducing + friction. +

+

+ — + Source +

+

+ TKP +

+

+ Marshall Goldsmith on self-limiting beliefs: +

+

+ "I think we have a lot of self-limiting beliefs. And the self-limiting + beliefs, a lot of these come from inside us. Basically, I can’t do this. + I can’t do that. This is just the way I am. One of the most common + problems is, this is just the way I am as if we have some “real” fixed + identity that lives throughout time. And I have to really work on people + to change that. Even smart people say things like this, “I can’t listen. + I can’t listen. I’ve never been able to listen.” I’ll look in their + ears. “Why not? You got something stuck in there? Why can’t you listen? + Do you have an incurable genetic defect that is prohibiting you from + listening?” As long as we tell ourselves, “That’s the way I am.” Two + things happen, both bad. One, we inhibit the odds of ever getting + better. Two, even if we do change our behavior we don’t seem authentic + to ourselves. We feel like a phony because if the real me can’t listen + and you say, “I’m a good listener. You know what I’m thinking?” Well, + that’s not the real me. I’m just pretending to be a good listener + because the real me is no good at that.” +

+

+ — + Listen and Learn + or + read the transcript. +

+

+ Insight +

+

+ Jeff Bezos on wandering as a counter-balance to efficiency: +

+

+ "Sometimes (often actually) in business, you do know where you’re going, + and when you do, you can be efficient. Put in place a plan and execute. + In contrast, wandering in business is not efficient … but it’s also not + random. It’s guided – by hunch, gut, intuition, curiosity, and powered + by a deep conviction that the prize for customers is big enough that + it’s worth being a little messy and tangential to find our way there. + Wandering is an essential counterbalance to efficiency. You need to + employ both. The outsized discoveries – the “non-linear” ones – are + highly likely to require wandering." +

+

+ — + Source +

+

+ Tiny Thought +

+

+ There are a lot of things in life that only work when you commit. +

+

+ I don’t mean dabble. I don’t mean half-in. I mean commit. +

+

+ Commitment means all in, all the time. +

+

+ It’s easy to trick yourself into thinking that if you put in half the + effort, you can get 80 percent of the results. While that might work for + some things, it doesn’t work for anything important. +

+

+ If you’re half trustworthy, you’re not trustworthy. +

+

+ If you’re often reliable, you’re not reliable. +

+

+ If you’re mostly consistent, you’re not consistent. +

+

+ The key to doing anything well is commitment. Not only does commitment + help you become better at what you do, but it also makes other people + want to help you. +

+

+ If you see your job as punching the clock, not only will you never be + great at it, but your employer won’t invest in you. The best + relationships are the ones where both partners go all in all the time to + make the relationship amazing. +

+

+ If committing sounds like a lot of work, it is. That’s why so many + people are half-in. The problem with half-in and half-committed is that + it doesn’t get you the results you want. If you're not committed, get + out. +

+

+ The committed person gets both the opportunity and the results. +

+

+ All in, all the time. +

+

+ (Share this Tiny Thought on Twitter)
+

+

+ Etc. +

+

+ Rafal Nadal on why hard is good: +

+

+ "One lesson I’ve learned is that if the job I do were easy, I wouldn’t + derive so much satisfaction from it. The thrill of winning is in direct + proportion to the effort I put in before. I also know, from long + experience, that if you make an effort in training when you don’t + especially feel like making it, the payoff is that you will win games + when you are not feeling your best. That is how you win championships, + that is what separates the great player from the merely good player. The + difference lies in how well you’ve prepared." +

+

+ — + Source +

+

+ Fatal flaws that everyone sees: +

+

+ "Everyone knew, deep-down, that middle-school geometry doomed the + design, but everyone also fervently believed that it could somehow be + overcome by sheer will, or hard work, or a stroke of genius. The theme + here is that the cultures that arise around products, methods, and + inventions often grow to exclude discussion of their fatal flaws, and + instead find elaborate ways to paper over them -- to find more and more + clever ways to pretend they don't exist." +

+

+ — + Source +

+

+
+

+ + + + + + +
+ Sponsored by Royce Investment Partners +
+

+ +

+

+ +

+

+ The Royce Funds. + Small-cap specialists. + +

+

+

+ Cheers,
— Shane +

+

+ P.S. Brilliant (and surprising) + design. +

+

+ P.P.S. “Most successful people are just an anxiety disorder + harnessed for productivity.” +

+

+
+

+ Free Version +

+

+ You're getting the Free version. Not only do members support our free + content, but they also get access to a community, hand-edited + transcripts, ad-free everything, and early access. +

+

+ See What You're Missing +

+
+

+

+ +

+ +

+
+

+

+
+

+

+
+

+

+
+

+

+
+

+

+
+

+ + + + + + + +
+ + + diff --git a/packages/content-handler/test/newsletter.test.ts b/packages/content-handler/test/newsletter.test.ts index 46e3eb3d4..4a9524498 100644 --- a/packages/content-handler/test/newsletter.test.ts +++ b/packages/content-handler/test/newsletter.test.ts @@ -12,6 +12,7 @@ import nock from 'nock' import { generateUniqueUrl } from '../src/content-handler' import fs from 'fs' import { BeehiivHandler } from '../src/newsletters/beehiiv-handler' +import { ConvertkitHandler } from '../src/newsletters/convertkit-handler' chai.use(chaiAsPromised) chai.use(chaiString) @@ -147,6 +148,17 @@ describe('Newsletter email test', () => { }) ).to.eventually.be.true }) + it('returns true for convertkit newsletter', async () => { + const html = load('./test/data/convertkit-newsletter.html') + await expect( + new ConvertkitHandler().isNewsletter({ + html, + postHeader: '', + from: '', + unSubHeader: '', + }) + ).to.eventually.be.true + }) }) describe('findNewsletterUrl', async () => { @@ -203,6 +215,30 @@ describe('Newsletter email test', () => { }).timeout(10000) }) + context('when email is from convertkit', () => { + before(() => { + nock('https://u25184427.ct.sendgrid.net') + .head( + '/ls/click?upn=MnmHBiCwIPe9TmIJeskmA7wDTJvdVU1ACmSJ753YuhScf71JWthxqM8RnVh-2FZG0rYzrbR04P99S2ld2OkTtQmrx2FDwArpYdk5N0jVpN9dLBZ-2BdPNqkRHxNvuygY8-2F-2FtRNFoPjxjtTuyWM6L3tcYDYnAnL2xCueddWcFlUNrQWsvLotmgvC-2BrQc7bxsZhW0pUBmS_vVXscVLXlj5UtQe3aqo5RMTdTq2PepdZjP86UOmA8nzSBnnaDN-2FNHWDodWnbUOPZ063v3w3z8QtcaPpE1qNu8xYkNJJFb-2F1uZEG-2BzsLfyDkjvvVX5zYs5OyyRYlhMOlXDJcr4-2FtMrFwii0uFAvwbhxDdnTxEpi-2F7maufyH39AEO-2BtCeSUg5V4FM43UpI1zUSXeWK-2Fh5JumSmR5XhrrRAig-3D-3D' + ) + .reply(302, undefined, { + Location: 'https://fs.blog/brain-food/october-16-2022/', + }) + .get('/brain-food/october-16-2022/') + .reply(200, '') + }) + + after(() => { + nock.restore() + }) + + it('gets the URL from the header', async () => { + const html = load('./test/data/convertkit-newsletter.html') + const url = await new ConvertkitHandler().findNewsletterUrl(html) + expect(url).to.startWith('https://fs.blog/brain-food/october-16-2022/') + }).timeout(10000) + }) + it('returns undefined if it is not a newsletter', async () => { const html = load('./test/data/substack-forwarded-welcome-email.html') const url = await new SubstackHandler().findNewsletterUrl(html) From d8c5c97960ef9fcf1d369aa238951ec6d45d3315 Mon Sep 17 00:00:00 2001 From: Hongbo Wu Date: Mon, 17 Oct 2022 19:24:11 +0800 Subject: [PATCH 17/53] Save newsletters hosted by ghost.org correctly --- .../content-handler/src/content-handler.ts | 3 +- packages/content-handler/src/index.ts | 6 +- .../src/newsletters/beehiiv-handler.ts | 5 +- .../src/newsletters/convertkit-handler.ts | 5 +- .../src/newsletters/ghost-handler.ts | 32 ++ .../src/newsletters/revue-handler.ts | 5 +- .../src/newsletters/substack-handler.ts | 6 +- .../test/data/ghost-newsletter.html | 361 ++++++++++++++++++ .../content-handler/test/newsletter.test.ts | 56 ++- 9 files changed, 458 insertions(+), 21 deletions(-) create mode 100644 packages/content-handler/src/newsletters/ghost-handler.ts create mode 100644 packages/content-handler/test/data/ghost-newsletter.html diff --git a/packages/content-handler/src/content-handler.ts b/packages/content-handler/src/content-handler.ts index 317f3d2d9..fd1fe5291 100644 --- a/packages/content-handler/src/content-handler.ts +++ b/packages/content-handler/src/content-handler.ts @@ -78,7 +78,8 @@ export abstract class ContentHandler { postHeader: string from: string unSubHeader: string - html?: string + html: string + dom: Document }): Promise { const re = new RegExp(this.senderRegex) return Promise.resolve( diff --git a/packages/content-handler/src/index.ts b/packages/content-handler/src/index.ts index 5a144b2ee..8e32f1cb3 100644 --- a/packages/content-handler/src/index.ts +++ b/packages/content-handler/src/index.ts @@ -24,6 +24,8 @@ import { BloombergNewsletterHandler } from './newsletters/bloomberg-newsletter-h import { BeehiivHandler } from './newsletters/beehiiv-handler' import { ConvertkitHandler } from './newsletters/convertkit-handler' import { RevueHandler } from './newsletters/revue-handler' +import { GhostHandler } from './newsletters/ghost-handler' +import { parseHTML } from 'linkedom' const validateUrlString = (url: string) => { const u = new URL(url) @@ -70,6 +72,7 @@ const newsletterHandlers: ContentHandler[] = [ new BeehiivHandler(), new ConvertkitHandler(), new RevueHandler(), + new GhostHandler(), ] export const preHandleContent = async ( @@ -122,8 +125,9 @@ export const preParseContent = async ( export const handleNewsletter = async ( input: NewsletterInput ): Promise => { + const dom = parseHTML(input.html).document for (const handler of newsletterHandlers) { - if (await handler.isNewsletter(input)) { + if (await handler.isNewsletter({ ...input, dom })) { return handler.handleNewsletter(input) } } diff --git a/packages/content-handler/src/newsletters/beehiiv-handler.ts b/packages/content-handler/src/newsletters/beehiiv-handler.ts index e0cf5c687..332e7a050 100644 --- a/packages/content-handler/src/newsletters/beehiiv-handler.ts +++ b/packages/content-handler/src/newsletters/beehiiv-handler.ts @@ -1,5 +1,4 @@ import { ContentHandler } from '../content-handler' -import { parseHTML } from 'linkedom' export class BeehiivHandler extends ContentHandler { constructor() { @@ -22,9 +21,9 @@ export class BeehiivHandler extends ContentHandler { postHeader: string from: string unSubHeader: string - html: string + dom: Document }): Promise { - const dom = parseHTML(input.html).document + const dom = input.dom if (dom.querySelectorAll('img[src*="beehiiv.net"]').length > 0) { const beehiivUrl = this.findNewsletterHeaderHref(dom) if (beehiivUrl) { diff --git a/packages/content-handler/src/newsletters/convertkit-handler.ts b/packages/content-handler/src/newsletters/convertkit-handler.ts index a77951178..15aebae26 100644 --- a/packages/content-handler/src/newsletters/convertkit-handler.ts +++ b/packages/content-handler/src/newsletters/convertkit-handler.ts @@ -1,5 +1,4 @@ import { ContentHandler } from '../content-handler' -import { parseHTML } from 'linkedom' export class ConvertkitHandler extends ContentHandler { constructor() { @@ -25,9 +24,9 @@ export class ConvertkitHandler extends ContentHandler { postHeader: string from: string unSubHeader: string - html: string + dom: Document }): Promise { - const dom = parseHTML(input.html).document + const dom = input.dom return Promise.resolve( dom.querySelectorAll( 'img[src*="convertkit.com"], img[src*="convertkit-mail"]' diff --git a/packages/content-handler/src/newsletters/ghost-handler.ts b/packages/content-handler/src/newsletters/ghost-handler.ts new file mode 100644 index 000000000..ca3e35a85 --- /dev/null +++ b/packages/content-handler/src/newsletters/ghost-handler.ts @@ -0,0 +1,32 @@ +import { ContentHandler } from '../content-handler' + +export class GhostHandler extends ContentHandler { + constructor() { + super() + this.name = 'ghost' + } + + findNewsletterHeaderHref(dom: Document): string | undefined { + const readOnline = dom.querySelector('.view-online-link') + return readOnline?.getAttribute('href') || undefined + } + + async isNewsletter(input: { + postHeader: string + from: string + unSubHeader: string + dom: Document + }): Promise { + const dom = input.dom + return Promise.resolve( + dom.querySelectorAll('img[src*="ghost.org"]').length > 0 + ) + } + + async parseNewsletterUrl( + postHeader: string, + html: string + ): Promise { + return this.findNewsletterUrl(html) + } +} diff --git a/packages/content-handler/src/newsletters/revue-handler.ts b/packages/content-handler/src/newsletters/revue-handler.ts index d8c8f911c..cd22d314c 100644 --- a/packages/content-handler/src/newsletters/revue-handler.ts +++ b/packages/content-handler/src/newsletters/revue-handler.ts @@ -1,5 +1,4 @@ import { ContentHandler } from '../content-handler' -import { parseHTML } from 'linkedom' export class RevueHandler extends ContentHandler { constructor() { @@ -22,9 +21,9 @@ export class RevueHandler extends ContentHandler { postHeader: string from: string unSubHeader: string - html: string + dom: Document }): Promise { - const dom = parseHTML(input.html).document + const dom = input.dom if ( dom.querySelectorAll('img[src*="getrevue.co"], img[src*="revue.email"]') .length > 0 diff --git a/packages/content-handler/src/newsletters/substack-handler.ts b/packages/content-handler/src/newsletters/substack-handler.ts index e90168c8e..e82d6d1cf 100644 --- a/packages/content-handler/src/newsletters/substack-handler.ts +++ b/packages/content-handler/src/newsletters/substack-handler.ts @@ -1,6 +1,5 @@ import addressparser from 'addressparser' import { ContentHandler } from '../content-handler' -import { parseHTML } from 'linkedom' export class SubstackHandler extends ContentHandler { constructor() { @@ -50,17 +49,16 @@ export class SubstackHandler extends ContentHandler { async isNewsletter({ postHeader, - html, + dom, }: { postHeader: string from: string unSubHeader: string - html: string + dom: Document }): Promise { if (postHeader) { return Promise.resolve(true) } - const dom = parseHTML(html).document // substack newsletter emails have tables with a *post-meta class if (dom.querySelector('table[class$="post-meta"]')) { return true diff --git a/packages/content-handler/test/data/ghost-newsletter.html b/packages/content-handler/test/data/ghost-newsletter.html new file mode 100644 index 000000000..0caf6932a --- /dev/null +++ b/packages/content-handler/test/data/ghost-newsletter.html @@ -0,0 +1,361 @@ + + + + + + + why ish / 2022-10-14 + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/content-handler/test/newsletter.test.ts b/packages/content-handler/test/newsletter.test.ts index 4a9524498..f34760971 100644 --- a/packages/content-handler/test/newsletter.test.ts +++ b/packages/content-handler/test/newsletter.test.ts @@ -13,6 +13,8 @@ import { generateUniqueUrl } from '../src/content-handler' import fs from 'fs' import { BeehiivHandler } from '../src/newsletters/beehiiv-handler' import { ConvertkitHandler } from '../src/newsletters/convertkit-handler' +import { parseHTML } from 'linkedom' +import { GhostHandler } from '../src/newsletters/ghost-handler' chai.use(chaiAsPromised) chai.use(chaiString) @@ -93,9 +95,10 @@ describe('Newsletter email test', () => { describe('isProbablyNewsletter', () => { it('returns true for substack newsletter', async () => { const html = load('./test/data/substack-forwarded-newsletter.html') + const dom = parseHTML(html).document await expect( new SubstackHandler().isNewsletter({ - html, + dom, postHeader: '', from: '', unSubHeader: '', @@ -106,9 +109,10 @@ describe('Newsletter email test', () => { const html = load( './test/data/substack-private-forwarded-newsletter.html' ) + const dom = parseHTML(html).document await expect( new SubstackHandler().isNewsletter({ - html, + dom, postHeader: '', from: '', unSubHeader: '', @@ -117,9 +121,10 @@ describe('Newsletter email test', () => { }) it('returns false for substack welcome email', async () => { const html = load('./test/data/substack-forwarded-welcome-email.html') + const dom = parseHTML(html).document await expect( new SubstackHandler().isNewsletter({ - html, + dom, postHeader: '', from: '', unSubHeader: '', @@ -128,9 +133,10 @@ describe('Newsletter email test', () => { }) it('returns true for beehiiv.com newsletter', async () => { const html = load('./test/data/beehiiv-newsletter.html') + const dom = parseHTML(html).document await expect( new BeehiivHandler().isNewsletter({ - html, + dom, postHeader: '', from: '', unSubHeader: '', @@ -139,9 +145,22 @@ describe('Newsletter email test', () => { }) it('returns true for milkroad newsletter', async () => { const html = load('./test/data/milkroad-newsletter.html') + const dom = parseHTML(html).document await expect( new BeehiivHandler().isNewsletter({ - html, + dom, + postHeader: '', + from: '', + unSubHeader: '', + }) + ).to.eventually.be.true + }) + it('returns true for ghost newsletter', async () => { + const html = load('./test/data/ghost-newsletter.html') + const dom = parseHTML(html).document + await expect( + new GhostHandler().isNewsletter({ + dom, postHeader: '', from: '', unSubHeader: '', @@ -150,9 +169,10 @@ describe('Newsletter email test', () => { }) it('returns true for convertkit newsletter', async () => { const html = load('./test/data/convertkit-newsletter.html') + const dom = parseHTML(html).document await expect( new ConvertkitHandler().isNewsletter({ - html, + dom, postHeader: '', from: '', unSubHeader: '', @@ -244,6 +264,30 @@ describe('Newsletter email test', () => { const url = await new SubstackHandler().findNewsletterUrl(html) expect(url).to.be.undefined }) + + context('when email is from ghost', () => { + before(() => { + nock('https://u25184427.ct.sendgrid.net') + .head( + '/ls/click?upn=MnmHBiCwIPe9TmIJeskmA9nRLefEmmgrd5xWS-2Bc39wxPBpwDRny1FmWt1H0FpgKAz1dv_vVXscVLXlj5UtQe3aqo5RMTdTq2PepdZjP86UOmA8nzulL-2F3YyC-2FHgJV0JnOPtjNvgjHSaQVfisQ15hPQtnlo4t73zgTQL4QnDoer4qJ3-2F2Lf-2F2ElFMF3NyoUD4eqWCWwUM0w4P9Feaeo-2BolkySAB611BySXRt6V3Z-2F7mQcpcRX3D9zV-2B-2FdRY0Vn30aR-2BKY8qpTFuivxzF19UkQGjK5srg-3D-3D' + ) + .reply(302, undefined, { + Location: 'https://www.openml.fyi/2022-10-14/', + }) + .get('/2022-10-14/') + .reply(200, '') + }) + + after(() => { + nock.restore() + }) + + it('gets the URL from the header', async () => { + const html = load('./test/data/ghost-newsletter.html') + const url = await new GhostHandler().findNewsletterUrl(html) + expect(url).to.startWith('https://www.openml.fyi/2022-10-14/') + }).timeout(10000) + }) }) describe('generateUniqueUrl', () => { From 5312c31ea747cd2c6e455ecab469caef9b79c35c Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Mon, 17 Oct 2022 20:45:04 +0800 Subject: [PATCH 18/53] Dont crash if a popular read is not available on signup --- .../api/src/routers/auth/mobile/sign_up.ts | 1 + packages/api/src/services/popular_reads.ts | 35 +++++++++++-------- 2 files changed, 21 insertions(+), 15 deletions(-) diff --git a/packages/api/src/routers/auth/mobile/sign_up.ts b/packages/api/src/routers/auth/mobile/sign_up.ts index fea03ef97..9bfc36c78 100644 --- a/packages/api/src/routers/auth/mobile/sign_up.ts +++ b/packages/api/src/routers/auth/mobile/sign_up.ts @@ -72,6 +72,7 @@ export async function createMobileEmailSignUpResponse( json: {}, } } catch (e) { + console.log('error', e) return signUpFailedPayload } } diff --git a/packages/api/src/services/popular_reads.ts b/packages/api/src/services/popular_reads.ts index db2933c3d..4adb3f569 100644 --- a/packages/api/src/services/popular_reads.ts +++ b/packages/api/src/services/popular_reads.ts @@ -32,22 +32,27 @@ const popularRead = (key: string): PopularRead | undefined => { return undefined } - const content = readFileSync( - path.resolve(__dirname, `popular_reads/${key}-content.html`), - 'utf8' - ) - const originalHtml = readFileSync( - path.resolve(__dirname, `./popular_reads/${key}-original.html`), - 'utf8' - ) - if (!content || !originalHtml) { - return undefined - } + try { + const content = readFileSync( + path.resolve(__dirname, `popular_reads/${key}-content.html`), + 'utf8' + ) + const originalHtml = readFileSync( + path.resolve(__dirname, `./popular_reads/${key}-original.html`), + 'utf8' + ) + if (!content || !originalHtml) { + return undefined + } - return { - ...metadata, - content, - originalHtml, + return { + ...metadata, + content, + originalHtml, + } + } catch (e) { + console.log('error adding popular read', e) + return undefined } } From 3d653240b8894751e01ae5ec030a35250a9adaa8 Mon Sep 17 00:00:00 2001 From: Hongbo Wu Date: Mon, 17 Oct 2022 22:45:41 +0800 Subject: [PATCH 19/53] Copy html files to dist folder --- packages/api/package.json | 6 +++-- yarn.lock | 54 ++++++++++++++++++++++++++++++++++++--- 2 files changed, 55 insertions(+), 5 deletions(-) diff --git a/packages/api/package.json b/packages/api/package.json index 70fa43d9e..ffd240c8f 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -3,12 +3,13 @@ "version": "1.0.0", "license": "UNLICENSED", "scripts": { - "build": "tsc", + "build": "tsc && yarn copy-files", "dev": "ts-node-dev --files src/server.ts", "start": "node dist/server.js", "lint": "eslint src --ext ts,js,tsx,jsx", "lint:fix": "eslint src --fix --ext ts,js,tsx,jsx", - "test": "nyc mocha -r ts-node/register --config mocha-config.json --timeout 10000" + "test": "nyc mocha -r ts-node/register --config mocha-config.json --timeout 10000", + "copy-files": "copyfiles -u 1 src/**/*.html dist/" }, "dependencies": { "@elastic/elasticsearch": "~7.12.0", @@ -126,6 +127,7 @@ "chai-as-promised": "^7.1.1", "chai-string": "^1.5.0", "circular-dependency-plugin": "^5.2.0", + "copyfiles": "^2.4.1", "mocha": "^9.0.1", "mocha-unfunk-reporter": "^0.4.0", "nock": "^13.2.4", diff --git a/yarn.lock b/yarn.lock index 272f859a8..a90c7857b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -11547,6 +11547,19 @@ copy-to-clipboard@^3.3.1: dependencies: toggle-selection "^1.0.6" +copyfiles@^2.4.1: + version "2.4.1" + resolved "https://registry.yarnpkg.com/copyfiles/-/copyfiles-2.4.1.tgz#d2dcff60aaad1015f09d0b66e7f0f1c5cd3c5da5" + integrity sha512-fereAvAvxDrQDOXybk3Qu3dPbOoKoysFMWtkY3mv5BsL8//OSZVL5DCLYqgRfY5cWirgRzlC+WSrxp6Bo3eNZg== + dependencies: + glob "^7.0.5" + minimatch "^3.0.3" + mkdirp "^1.0.4" + noms "0.0.0" + through2 "^2.0.1" + untildify "^4.0.0" + yargs "^16.1.0" + core-js-compat@^3.20.2, core-js-compat@^3.21.0: version "3.21.1" resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.21.1.tgz#cac369f67c8d134ff8f9bd1623e3bc2c42068c82" @@ -14586,6 +14599,18 @@ glob@7.2.0, glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6, glo once "^1.3.0" path-is-absolute "^1.0.0" +glob@^7.0.5: + version "7.2.3" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b" + integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.1.1" + once "^1.3.0" + path-is-absolute "^1.0.0" + global-dirs@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/global-dirs/-/global-dirs-3.0.0.tgz#70a76fe84ea315ab37b1f5576cbde7d48ef72686" @@ -18720,7 +18745,7 @@ minimatch@5.0.1: dependencies: brace-expansion "^2.0.1" -minimatch@^3.0.2, minimatch@^3.0.4: +minimatch@^3.0.2, minimatch@^3.0.3, minimatch@^3.0.4, minimatch@^3.1.1: version "3.1.2" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== @@ -19454,6 +19479,14 @@ nodemon@^2.0.15: undefsafe "^2.0.5" update-notifier "^5.1.0" +noms@0.0.0: + version "0.0.0" + resolved "https://registry.yarnpkg.com/noms/-/noms-0.0.0.tgz#da8ebd9f3af9d6760919b27d9cdc8092a7332859" + integrity sha512-lNDU9VJaOPxUmXcLb+HQFeUgQQPtMI24Gt6hgfuMHRJgMRHMF/qZ4HJD3GDru4sSw9IQl2jPjAYnQrdIeLbwow== + dependencies: + inherits "^2.0.1" + readable-stream "~1.0.31" + nopt@^4.0.1: version "4.0.3" resolved "https://registry.yarnpkg.com/nopt/-/nopt-4.0.3.tgz#a375cad9d02fd921278d954c2254d5aa57e15e48" @@ -21876,6 +21909,16 @@ readable-stream@3, readable-stream@^3.0.0, readable-stream@^3.0.2, readable-stre string_decoder "^1.1.1" util-deprecate "^1.0.1" +readable-stream@~1.0.31: + version "1.0.34" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.0.34.tgz#125820e34bc842d2f2aaafafe4c2916ee32c157c" + integrity sha512-ok1qVCJuRkNmvebYikljxJA/UEsKwLl2nI1OmaqAu4/UE+h0wKCHok4XkL/gvi39OacXvw59RJUOFUkDib2rHg== + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.1" + isarray "0.0.1" + string_decoder "~0.10.x" + readdir-scoped-modules@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/readdir-scoped-modules/-/readdir-scoped-modules-1.1.0.tgz#8d45407b4f870a0dcaebc0e28670d18e74514309" @@ -23549,6 +23592,11 @@ string_decoder@^1.0.0, string_decoder@^1.1.1: dependencies: safe-buffer "~5.2.0" +string_decoder@~0.10.x: + version "0.10.31" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94" + integrity sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ== + string_decoder@~1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" @@ -24083,7 +24131,7 @@ throttleit@^1.0.0: resolved "https://registry.yarnpkg.com/throttleit/-/throttleit-1.0.0.tgz#9e785836daf46743145a5984b6268d828528ac6c" integrity sha1-nnhYNtr0Z0MUWlmEtiaNgoUorGw= -through2@^2.0.0: +through2@^2.0.0, through2@^2.0.1: version "2.0.5" resolved "https://registry.yarnpkg.com/through2/-/through2-2.0.5.tgz#01c1e39eb31d07cb7d03a96a70823260b23132cd" integrity sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ== @@ -25977,7 +26025,7 @@ yargs-unparser@2.0.0: flat "^5.0.2" is-plain-obj "^2.1.0" -yargs@16.2.0, yargs@^16.0.0, yargs@^16.1.1, yargs@^16.2.0: +yargs@16.2.0, yargs@^16.0.0, yargs@^16.1.0, yargs@^16.1.1, yargs@^16.2.0: version "16.2.0" resolved "https://registry.yarnpkg.com/yargs/-/yargs-16.2.0.tgz#1c82bf0f6b6a66eafce7ef30e376f49a12477f66" integrity sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw== From 66cec7f9eabd2582552d2778fab4f6a4719b39ac Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Mon, 17 Oct 2022 13:13:30 -0700 Subject: [PATCH 20/53] bump apple versions to 1.17.0 --- apple/Omnivore.xcodeproj/project.pbxproj | 36 ++++++++++++------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/apple/Omnivore.xcodeproj/project.pbxproj b/apple/Omnivore.xcodeproj/project.pbxproj index 9627432c5..56e6d8ece 100644 --- a/apple/Omnivore.xcodeproj/project.pbxproj +++ b/apple/Omnivore.xcodeproj/project.pbxproj @@ -1307,7 +1307,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 75; + CURRENT_PROJECT_VERSION = 76; DEVELOPMENT_TEAM = QJF2XZ86HB; ENABLE_HARDENED_RUNTIME = YES; INFOPLIST_FILE = InfoPlists/ShareExtensionMac.plist; @@ -1317,7 +1317,7 @@ "@executable_path/../../../../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 12.0; - MARKETING_VERSION = 1.16.0; + MARKETING_VERSION = 1.17.0; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = "app.omnivore.app.ShareExtension-Mac"; @@ -1339,7 +1339,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 75; + CURRENT_PROJECT_VERSION = 76; DEVELOPMENT_TEAM = QJF2XZ86HB; ENABLE_HARDENED_RUNTIME = YES; INFOPLIST_FILE = InfoPlists/ShareExtensionMac.plist; @@ -1349,7 +1349,7 @@ "@executable_path/../../../../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 12.0; - MARKETING_VERSION = 1.16.0; + MARKETING_VERSION = 1.17.0; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = "app.omnivore.app.ShareExtension-Mac"; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -1421,7 +1421,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 75; + CURRENT_PROJECT_VERSION = 76; DEVELOPMENT_ASSET_PATHS = ""; DEVELOPMENT_TEAM = QJF2XZ86HB; ENABLE_HARDENED_RUNTIME = YES; @@ -1432,7 +1432,7 @@ "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 12.0; - MARKETING_VERSION = 1.16.0; + MARKETING_VERSION = 1.17.0; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = app.omnivore.app; @@ -1455,7 +1455,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 75; + CURRENT_PROJECT_VERSION = 76; DEVELOPMENT_ASSET_PATHS = ""; DEVELOPMENT_TEAM = QJF2XZ86HB; ENABLE_HARDENED_RUNTIME = YES; @@ -1466,7 +1466,7 @@ "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 12.0; - MARKETING_VERSION = 1.16.0; + MARKETING_VERSION = 1.17.0; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = app.omnivore.app; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -1521,7 +1521,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.16.0; + MARKETING_VERSION = 1.17.0; PRODUCT_BUNDLE_IDENTIFIER = app.omnivore.app; PRODUCT_NAME = Omnivore; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -1600,7 +1600,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.16.0; + MARKETING_VERSION = 1.17.0; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; OTHER_LDFLAGS = ( @@ -1639,7 +1639,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.16.0; + MARKETING_VERSION = 1.17.0; MTL_FAST_MATH = YES; OTHER_LDFLAGS = ( "-framework", @@ -1665,7 +1665,7 @@ CODE_SIGN_ENTITLEMENTS = "Entitlements/SafariExtension-Mac.entitlements"; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 75; + CURRENT_PROJECT_VERSION = 76; DEVELOPMENT_TEAM = QJF2XZ86HB; ENABLE_HARDENED_RUNTIME = YES; GENERATE_INFOPLIST_FILE = YES; @@ -1678,7 +1678,7 @@ "@executable_path/../../../../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 12.0; - MARKETING_VERSION = 1.16.0; + MARKETING_VERSION = 1.17.0; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; OTHER_LDFLAGS = ( @@ -1704,7 +1704,7 @@ CODE_SIGN_ENTITLEMENTS = "Entitlements/SafariExtension-Mac.entitlements"; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 75; + CURRENT_PROJECT_VERSION = 76; DEVELOPMENT_TEAM = QJF2XZ86HB; ENABLE_HARDENED_RUNTIME = YES; GENERATE_INFOPLIST_FILE = YES; @@ -1717,7 +1717,7 @@ "@executable_path/../../../../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 12.0; - MARKETING_VERSION = 1.16.0; + MARKETING_VERSION = 1.17.0; MTL_FAST_MATH = YES; OTHER_LDFLAGS = ( "-framework", @@ -1804,7 +1804,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.16.0; + MARKETING_VERSION = 1.17.0; PRODUCT_BUNDLE_IDENTIFIER = "app.omnivore.app.share-extension"; PRODUCT_NAME = ShareExtension; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -1859,7 +1859,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.16.0; + MARKETING_VERSION = 1.17.0; PRODUCT_BUNDLE_IDENTIFIER = app.omnivore.app; PRODUCT_NAME = Omnivore; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -1888,7 +1888,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.16.0; + MARKETING_VERSION = 1.17.0; PRODUCT_BUNDLE_IDENTIFIER = "app.omnivore.app.share-extension"; PRODUCT_NAME = ShareExtension; PROVISIONING_PROFILE_SPECIFIER = ""; From cbc290e8e2901f824083161f2de774173dcf9e4c Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Mon, 17 Oct 2022 13:26:05 -0700 Subject: [PATCH 21/53] update pspdfkit conditional import in package.swift --- apple/OmnivoreKit/Package.swift | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/apple/OmnivoreKit/Package.swift b/apple/OmnivoreKit/Package.swift index 58f0d3de0..e72036ffe 100644 --- a/apple/OmnivoreKit/Package.swift +++ b/apple/OmnivoreKit/Package.swift @@ -55,9 +55,9 @@ let package = Package( var appPackageDependencies: [Target.Dependency] { var deps: [Target.Dependency] = ["Views", "Services", "Models", "Utils"] -// #if canImport(UIKit) -// deps.append(.product(name: "PSPDFKit", package: "PSPDFKit-SP")) -// #endif + #if canImport(UIKit) + deps.append(.product(name: "PSPDFKit", package: "PSPDFKit-SP")) + #endif return deps } @@ -69,8 +69,8 @@ var dependencies: [Package.Dependency] { .package(url: "git@github.com:segmentio/analytics-swift.git", .upToNextMajor(from: "1.0.0")), .package(url: "https://github.com/google/GoogleSignIn-iOS", from: "6.2.2") ] -// #if canImport(UIKit) -// deps.append(.package(url: "https://github.com/PSPDFKit/PSPDFKit-SP", branch: "master")) -// #endif + #if canImport(UIKit) + deps.append(.package(url: "https://github.com/PSPDFKit/PSPDFKit-SP", branch: "master")) + #endif return deps } From f73ae0a02a820474b0f65bbdeb20db6b8200eb62 Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Mon, 17 Oct 2022 14:23:16 -0700 Subject: [PATCH 22/53] revert the #ifCanImport conditional. Doesn't work :( --- .../xcshareddata/swiftpm/Package.resolved | 9 +++++++++ apple/OmnivoreKit/Package.swift | 10 ++++------ 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/apple/Omnivore.xcworkspace/xcshareddata/swiftpm/Package.resolved b/apple/Omnivore.xcworkspace/xcshareddata/swiftpm/Package.resolved index 5090a25ab..0667008b4 100644 --- a/apple/Omnivore.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/apple/Omnivore.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -153,6 +153,15 @@ "version" : "2.1.0" } }, + { + "identity" : "pspdfkit-sp", + "kind" : "remoteSourceControl", + "location" : "https://github.com/PSPDFKit/PSPDFKit-SP", + "state" : { + "branch" : "master", + "revision" : "e9757beadad1b30de84073d3c33c1cd0f7a94b80" + } + }, { "identity" : "sovran-swift", "kind" : "remoteSourceControl", diff --git a/apple/OmnivoreKit/Package.swift b/apple/OmnivoreKit/Package.swift index e72036ffe..54299201b 100644 --- a/apple/OmnivoreKit/Package.swift +++ b/apple/OmnivoreKit/Package.swift @@ -55,9 +55,8 @@ let package = Package( var appPackageDependencies: [Target.Dependency] { var deps: [Target.Dependency] = ["Views", "Services", "Models", "Utils"] - #if canImport(UIKit) - deps.append(.product(name: "PSPDFKit", package: "PSPDFKit-SP")) - #endif + // Comment out following line for macOS build + deps.append(.product(name: "PSPDFKit", package: "PSPDFKit-SP")) return deps } @@ -69,8 +68,7 @@ var dependencies: [Package.Dependency] { .package(url: "git@github.com:segmentio/analytics-swift.git", .upToNextMajor(from: "1.0.0")), .package(url: "https://github.com/google/GoogleSignIn-iOS", from: "6.2.2") ] - #if canImport(UIKit) - deps.append(.package(url: "https://github.com/PSPDFKit/PSPDFKit-SP", branch: "master")) - #endif + // Comment out following line for macOS build + deps.append(.package(url: "https://github.com/PSPDFKit/PSPDFKit-SP", branch: "master")) return deps } From 57676d381cef0626805b7079ef1bc1255eaa41c1 Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Tue, 18 Oct 2022 12:13:54 +0800 Subject: [PATCH 23/53] Better handle +
+
+

Across industries, executive after executive has chosen the PRC over free speech

+
+
+

Dozens of different controversies are swirling around Elon Musk at any given time, most of them related to people being angry and/or thrilled by the idea of a rich businessman vocalizing right-of-center political opinions.

+

+ What has gotten lost in this discourse is a more boring — but, I think, more significant — concern about his possible takeover of Twitter. This is because Musk, like most global manufacturing executives these days, has extensive business dealings with China. And while there’s nothing wrong with that per se, it means Musk has to watch what he says regarding the PRC, not just in his personal capacity as a business executive but potentially in his institutional role as well. And he’s not alone; Apple TV+, for example, has a rule that none of its content can portray China negatively. +

+

That’s an unfortunate but straightforward consequence of Apple TV+ being so small compared to Apple’s core business of making and selling smartphones: they compromise the content business for the sake of the manufacturing business. The good news for the world is that Apple TV+ is a very small share of western cultural output. They’re doing well with niche content (I love “For All Mankind”), and they won an Oscar for “Coda.” But it’s a small service in the scheme of things.

+

+ The problem for the world is that Twitter would be the Apple TV+ of Elon Musk’s enterprises, much smaller and less important than Tesla, so its interests will always be sacrificed to advance Tesla’s interests. And Tesla, like Apple’s hardware business, is deeply enmeshed in China. But Twitter is much more important to global politics and culture than Apple TV+. That’s the whole reason the Musk/Twitter saga has been such a subject of fascination. Twitter is one of a handful of other influential media properties — The New York Times, the three cable networks, AM talk radio stations — that exert a cultural and political influence that far exceeds their modest financial footprints. Apple executives are much less polarizing and controversial than Musk. But pretty much everyone on both the left and right knows they’re a bit squirrelly about China for business reasons. And if they bought the New York Times, that would have dire implications for the integrity of their China coverage. +

+

Musk is mercurial and I won’t pretend to be able to predict what he will do. But I think his business relationships with China and tendency to take pro-PRC positions in his public statements raise some disturbing questions about the future of Twitter that deserve much more scrutiny relative to the concern that he won’t be strict enough in policing hate speech.

+

China leans very effectively on western businesses

+

We don’t know exactly why Apple adopted its “no China” content policy, but we can come up with a pretty clear guess based on a handful of high-profile blowups. Back in 2018, for example, Mercedes Benz posted a Dalai Lama quote in English that had nothing to do with China.

+ +

+ The Chinese government flipped out, and Mercedes officially apologized, even though they obviously didn’t do anything wrong. +

+

+ What was Mercedes so worried about? Well, we know that when Daryl Morey tweeted in English something supportive of protestors in Hong Kong, the PRC didn’t hesitate to wreck the Houston Rockets’ access to the Chinese television market. Other NBA players — including some of the league’s most outspoken and socially conscious stars — sided with China over Morey. +

+

When celebrities do this stuff, it often attracts criticism from Republican China hawks.

+
+

+ Because conservatives agree with Musk about the importance of making social media a welcoming place for transphobic jokes, they haven’t been nearly as quick to condemn him telling the Financial Times that Taiwan ought to become a Beijing-ruled Special Administrative Region. But when it comes to a random celebrity like John Cena, conservatives understand the basic dynamic perfectly well — money talks. +

+
+
+
+
+ + Image + +
+
+
+

But this applies to corporate entities like Apple and Mercedes-Benz even more than it does to individuals like Cena. An individual always has the option of voluntarily earning less money in order to take a principled stand on human rights. If Apple executives started “doing the right thing” in a way that seriously impaired the company’s stock price, they’d be vulnerable to activist investor campaigns and potentially even litigation.

+

+ This strikes me as a fairly profound policy problem. Part of the promise of the old bipartisan consensus in favor of trade with China was that economic integration would help spur the export of American speech norms. Not only has that not worked out, but the practical consequences have been the opposite. I’m sure that if you asked Intel executives whether it’s good that China is perpetrating a genocide and using forced labor in Xinjiang, the vast majority of them would say, in private, that it’s actually bad. But in practice, Intel has apologized for past statements about Xinjiang. Because for all the virtues of capitalism and the for-profit business corporation, it also has at its core an amorality that can produce really bad results when it interfaces with a large despotic regime. +

+

Elon Musk does a ton of business in China

+

+ The global electronics industry is famously exposed to the China market. But as we saw with Mercedes Benz, China is a really big deal for automakers as well. Tesla is setting sales records in China, which is obviously only possible because the Chinese government lets Tesla sell cars there. +

+ +

+ Like many global manufacturing companies, Tesla also builds things in China — including what Musk projects will be the company’s largest factory in the world. +

+

There’s nothing particularly untoward or suspect about this. Many more people live in East Asia than live in North America, so it’s natural that over the long term any manufacturer of large objects is going to want to have their biggest production facilities in Asia.

+

+ But Tesla has received unusually generous treatment from the Chinese government. Part of China’s strategy for industrializing has been to say to foreign companies operating in key sectors that if they want to be in the China market, they need to operate as a joint venture with a Chinese-owned firm. It’s become pretty clear that this is a mechanism for Chinese companies to learn foreign technology and business methods, and ultimately pull Chinese-owned companies up the value chain. Foreign companies don’t like it, but in most cases the lure of the China market is just too good to resist. Telsa, though, managed to become the first automaker to receive an exemption from the joint venture requirement. +

+

+ That’s a huge coup for Musk and a testament both to the quality of Tesla’s products and also to Musk’s shrewdness and skill as a businessman. But without being a hater, I have to say that “shrewdness and skill at making Xi Jinping like you and want to give you favorable treatment” is a mixed ethical bag. The New York Times is blocked in China as part of their censorship regime. So is Substack. It would be better for their businesses if A.G. Sulzberger and Chris Best were as skilled as Musk at getting Xi to do special favors for their companies. But achieving that goal would require them to fundamentally compromise the integrity of editorial businesses in a way that I think would outweigh the benefits. More fundamentally, as a non-corporate person, I would be thrilled to have Chinese readers but also profoundly embarrassed if this site managed to become popular in China without getting censored. That would suggest I’m doing a really bad job of standing up for core values that Americans on the left and right broadly agree on. +

+

Elon Musk’s stated views are very pro-PRC

+

+ Ten years ago, Tesla was a very small company that Mitt Romney denounced as the kind of “loser” that was dependent on unwise subsidies from the Obama administration. At the time, liberals were generally enthusiastic about the idea of an electric vehicle startup. And to the extent that Musk had any known political views, he seemed like a pretty mainstream Democrat. +

+

He also had very normal geopolitical opinions like “foreign dictatorships are bad” that a person who doesn’t have extensive Chinese business interests is free to express on Twitter (which, of course, is banned in China).

+
+

But as Musk’s business grew, he became more China-friendly in his public statements. Initially this took the relatively sensible form adopted by many who need to come up with something nice to say about the PRC: praise for their extensive infrastructure buildout. This is something I’ve done myself — it’s incredibly impressive that China built mid-sized metro systems in a couple of dozen cities over the course of time that it took the United States to build three miles of subway on the Upper East Side.

+
+

But in more recent years, Musk’s thinking on China has evolved in less defensible directions. We know, for example, that part of his rightward political evolution over the past several years was driven by the restrictiveness of California’s Covid-19 protocols. Yet apart from a token March 2020 gesture of praise for Xi’s success in controlling the virus, he’s had absolutely nothing to say about China’s Covid Zero policies that have been much more draconian than anything offered in the United States.

+
+

The same is true, though obviously in a much broader sense, of Musk’s emergence as a champion of free speech who is skeptical of heavy-handed content moderation policies by big tech platforms. Reasonable people can disagree as to exactly where Twitter and Facebook should draw these lines, and I tend to agree with Musk about some of the specifics here. But obviously the speech restrictions on Twitter and Facebook are much less onerous than the ones the PRC imposes on their domestic internet. That’s part of the reason Twitter and Facebook are banned in China! But Musk has never said or done anything to champion free speech in China. That’s not surprising — nobody with business interests in China criticizes the PRC’s speech repression. But it is a reminder that Musk, like many business people, prioritizes being in the good graces of the dictators who run China over maintaining his commitment to free speech.

+

+ And, again, Musk’s silence on free speech in China isn’t because he doesn’t talk about China. He does talk about China — including publicly advocating for a PRC takeover of Taiwan just one day before Tesla buyers were made eligible for an important PRC tax break. +

+

What does this mean for Twitter?

+

Does this mean that a Musk-run Twitter is going to impose China-friendly censorship as heavy-handed as the self-censorship Musk practices in his personal commentary? That he’s going to turn it into a China-free zone the way that Apple has with its content business?

+

Probably not.

+

I really don’t know what Musk’s business ties to China mean for Twitter. And I wish that more of the reporting and commentary on this takeover proposal focused on this. After all, here are some things that we know:

+
    +
  • +

    China is very willing to threaten western companies over their China-related speech practices.

    +
  • +
  • +

    China takes a very expansive view of what speech should be prohibited by people who want to do business in China, including speech via English-language communications on platforms that are banned in China.

    +
  • +
  • +

    Western business enterprises — including car companies — have been willing to do what the PRC asks, even in ridiculous cases like apologizing for an innocuous Dalai Lama quote.

    +
  • +
  • +

    Tesla has both sought and received favorable treatment from the Chinese government and views China as a crucial market for both sales and production — one whose importance is likely to grow in the future.

    +
  • +
  • +

    Musk has, in his personal communication, stopped criticizing the PRC on any topic, including on matters like free speech and Covid protocols where he’s been vocal about U.S. policies, and he has instead praised the PRC government and endorsed its views on geopolitical conflicts.

    +
  • +
+

So what follows from this? I don’t know, in part because Musk is mercurial and hard to predict. But also in part because it’s not clear exactly how Chinese censorship exporting works. I assume nobody had to explicitly tell Musk that criticizing Covid Zero would be bad for business — it’s just something he knows, so he doesn’t do it. By the same token, I doubt there was ever an explicit conversation between Tim Cook and someone from the Chinese government about Apple TV+ programming. Apple just knows the score.

+

Media brands with integrity, including Substack and the New York Times and Twitter, just proceed and accept the censorship hit. Industrial brands like Mercedes stifle speech for the sake of sales. When an industrial brand is also a media brand, like Apple, the industrial brand makes the media brand do what’s good for business. So what does Twitter do once it’s owned by the CEO of a major industrial brand? I don’t know, but I wish the reporters on the Musk beat would ask. And frankly, I wish Congress and the FTC would, too.

+
+
+
\ No newline at end of file diff --git a/packages/readabilityjs/test/test-pages/slowboring/source.html b/packages/readabilityjs/test/test-pages/slowboring/source.html new file mode 100644 index 000000000..c6345994a --- /dev/null +++ b/packages/readabilityjs/test/test-pages/slowboring/source.html @@ -0,0 +1,1830 @@ + + + + + + + + + + + + + + + + + + + + + + + + + Elon Musk’s business ties deserve more scrutiny + + + + + + + + + + + + + + + + + + + + + +
+
+ + +
+ + + + +
+
+
+ +
+
+
+
+
+
+ 234 Comments +
+
+
+
+
+ +
+
+ +
+
+
+
+ + + +
+
+
+
+
+
+ + + + + +
+
+ +
+
+ +
+

+ >>>Because conservatives agree with Musk about the importance of making social media a welcoming place for transphobic jokes, they haven’t been nearly as quick to condemn him telling the Financial Times that Taiwan ought to become a Beijing-ruled Special Administrative Region. But when it comes to a random celebrity like John Cena, conservatives understand the basic dynamic perfectly well — money talks.<<< +

+

+ Matt's being too polite here. Republicans are deeply unserious and/or profoundly duplicitous about China policy. Pretty much everything the GOP does on China is intended for domestic political audiences only. Strengthening America to compete in the new Cold War would mean things like: +

+

+ *Addressing US demographic decline via immigration (Republicans want to do just the opposite, and push our growth levels down to PRC levels). +

+

+ *Competing for the world's best and brightest to strengthen America's STEM sector (Republicans want to do just the opposite, and make it harder for talented foreigners to get work permits, green cards or student visas). +

+

+ *Leading on Pacific Rim economic integration (Republicans have become the more protectionist of our two parties, and gave Obama essentially zero support on TPP, despite the obvious boost joining this group would have given to US efforts to contest the PRC's growing influence in this critical region). +

+

+ *Bolstering US democratic norms (something cold warriors in the 1950s and 1960s understood: a nontrivial degree of support for the Civil Rights movement was generated by the need to compete for global hearts and minds, and to demonstrate the superiority of liberal democracy over totalitarianism). Republicans won't even disavow insurrectionists. +

+

+ *Readying the country for its next, inevitable rendez-vous with a pandemic. (Republicans have mostly gone full anti-vax nutter). +

+

+ I could also cite climate change, infrastructure and various other areas where the US should play a leading role and/or demonstrate robust state capacity. The PRC is a formidable adversary: surely a lot tougher opponent that the USSR ever was. Republicans aren't serious about any of it, though admittedly they do a bang up job hurling racist invective (Kung Flu, etc) on Twitter, so at least there's that! +

+
+
+ Expand full comment +
+
+
+ +
+ +
+
+
+
+
+
+
+ + + + + +
+
+ +
+
+ +
+

+ Matt overrates the cultural importance of Twitter. It has a tenth as many users as FB. The vast majority of users get a really shitty experience: tweeting is about as likely to engage others as sitting alone and screaming at your TV. Twitter has become a forum for elites to spar with each other in public view and that’s not nothing, but it isn’t so different from dueling press releases or cable news appearances, and it could all be transferred to FB pretty easily. Bottom line, Twitter is much less integrated into normal peoples’ lives than FB or Instagram and there isn’t an obvious path for it to broadly engage normal people. +

+
+
+ Expand full comment +
+
+
+ +
+ +
+
+
+
+
+
232 more comments… +
+
+
+
+
+
+
+ +
+
+
+ +
+
+ The launch of a new experiment +
+
+
+ + + +
+ + +
+ + + + +
+ 594 +
+ + + + +
+ 1,037 +
+ + + + + +
+
+
+
+
+ +
+
+ A huge fuckup, with perhaps not-so-huge policy stakes +
+
+ + +
+ + + + +
+ 177 +
+ + + + +
+ 484 +
+ + + + + +
+
+
+
+
+ +
+
+ Prestigious universities and worthy nonprofits shouldn't push nonsense +
+
+ + +
+ + + + +
+ 403 +
+ + + + +
+ 380 +
+ + + + + +
+
+
See all + + + + +
+
+
+
+
+ +
+
+ +
+
+
+ + + + + + + diff --git a/packages/readabilityjs/test/test-pages/slowboring/url.txt b/packages/readabilityjs/test/test-pages/slowboring/url.txt new file mode 100644 index 000000000..53de21b75 --- /dev/null +++ b/packages/readabilityjs/test/test-pages/slowboring/url.txt @@ -0,0 +1 @@ +https://www.slowboring.com/p/elon-musks-business-ties-deserve \ No newline at end of file From 86337d5d0125d3570129d3f1c74518acf757d107 Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Mon, 17 Oct 2022 21:23:39 -0700 Subject: [PATCH 24/53] add steppers on web prefs dialog --- .../ui/reader/WebPreferencesDialog.kt | 63 ++++++++++++++++++- 1 file changed, 62 insertions(+), 1 deletion(-) diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/WebPreferencesDialog.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/WebPreferencesDialog.kt index d576e2164..ad925c59f 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/WebPreferencesDialog.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/WebPreferencesDialog.kt @@ -1,9 +1,16 @@ package app.omnivore.omnivore.ui.reader +import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.Text +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Add +import androidx.compose.material3.Divider +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton import androidx.compose.material3.Surface import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp import androidx.compose.ui.window.Dialog @@ -22,7 +29,61 @@ fun WebPreferencesDialog(onDismiss: () -> Unit) { @Composable fun WebPreferencesView() { - Text("Web Prefs") + Column { + Text("Web Preferences") + // Font Size: Stepper + Stepper( + label = "Font Size:", + onIncrease = {}, + onDecrease = {} + ) + + // Margin: Slider + Stepper( + label = "Margin:", + onIncrease = {}, + onDecrease = {} + ) + + // Line Spacing: Slider + Stepper( + label = "Line Spacing:", + onIncrease = {}, + onDecrease = {} + ) + + // High Contrast Text: Switch + // Reader Font: List of Fonts + } +} + +@Composable +fun Stepper(label: String, onIncrease: () -> Unit, onDecrease: () -> Unit) { + Row { + Text(text = label) + Spacer(modifier = Modifier.weight(1.0F)) + + IconButton(onClick = { onDecrease() }) { + Icon( + imageVector = Icons.Filled.Add, + contentDescription = null + ) + } + + Divider( + color = Color.Black, + modifier = Modifier + .height(40.dp) + .width(1.dp) + ) + + IconButton(onClick = { onIncrease() }) { + Icon( + imageVector = Icons.Filled.Add, + contentDescription = null + ) + } + } } data class WebPreferences( From 7a1e66ba5f8d10056b795bf654de8b70773f99d8 Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Mon, 17 Oct 2022 21:37:38 -0700 Subject: [PATCH 25/53] style web prefs popover --- .../ui/reader/WebPreferencesDialog.kt | 35 ++++++++++++++----- 1 file changed, 27 insertions(+), 8 deletions(-) diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/WebPreferencesDialog.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/WebPreferencesDialog.kt index ad925c59f..346011b72 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/WebPreferencesDialog.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/WebPreferencesDialog.kt @@ -4,16 +4,19 @@ import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.Text import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Add +import androidx.compose.material.icons.filled.KeyboardArrowDown +import androidx.compose.material.icons.filled.KeyboardArrowUp import androidx.compose.material3.Divider import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.Surface import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp import androidx.compose.ui.window.Dialog +import com.pspdfkit.ui.note.AlignedAnnotationHinterDrawable @Composable fun WebPreferencesDialog(onDismiss: () -> Unit) { @@ -29,8 +32,19 @@ fun WebPreferencesDialog(onDismiss: () -> Unit) { @Composable fun WebPreferencesView() { - Column { - Text("Web Preferences") + Column( + modifier = Modifier + .padding(top = 6.dp, start = 6.dp, end = 6.dp, bottom = 6.dp) + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(top = 12.dp, bottom = 12.dp), + horizontalArrangement = Arrangement.Center + ) { + Text("Web Preferences") + } + // Font Size: Stepper Stepper( label = "Font Size:", @@ -59,13 +73,18 @@ fun WebPreferencesView() { @Composable fun Stepper(label: String, onIncrease: () -> Unit, onDecrease: () -> Unit) { - Row { - Text(text = label) + Row(verticalAlignment = Alignment.CenterVertically) { + Text( + text = label, + modifier = Modifier + .padding(bottom = 6.dp) + ) + Spacer(modifier = Modifier.weight(1.0F)) IconButton(onClick = { onDecrease() }) { Icon( - imageVector = Icons.Filled.Add, + imageVector = Icons.Filled.KeyboardArrowDown, contentDescription = null ) } @@ -73,13 +92,13 @@ fun Stepper(label: String, onIncrease: () -> Unit, onDecrease: () -> Unit) { Divider( color = Color.Black, modifier = Modifier - .height(40.dp) + .height(20.dp) .width(1.dp) ) IconButton(onClick = { onIncrease() }) { Icon( - imageVector = Icons.Filled.Add, + imageVector = Icons.Filled.KeyboardArrowUp, contentDescription = null ) } From 59427fa8962f1ff02edff21ddce652ae0512fce6 Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Tue, 18 Oct 2022 12:51:50 +0800 Subject: [PATCH 26/53] Mock the substack domain also when testing redirects We were making the actual call to the redirected URL here, instead of mocking it. We need to mock both the embedded email domain, and the domain we are redirected to. --- packages/content-handler/test/newsletter.test.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/content-handler/test/newsletter.test.ts b/packages/content-handler/test/newsletter.test.ts index 4a9524498..ad57a64aa 100644 --- a/packages/content-handler/test/newsletter.test.ts +++ b/packages/content-handler/test/newsletter.test.ts @@ -172,8 +172,9 @@ describe('Newsletter email test', () => { Location: 'https://newsletter.slowchinese.net/p/companies-that-eat-people-217', }) - .get('/p/companies-that-eat-people-217') - .reply(200, '') + nock('https://newsletter.slowchinese.net') + .head('/p/companies-that-eat-people-217') + .reply(200, '') }) after(() => { nock.restore() @@ -186,7 +187,7 @@ describe('Newsletter email test', () => { expect(url).to.startWith( 'https://newsletter.slowchinese.net/p/companies-that-eat-people-217' ) - }).timeout(10000) + }) }) context('when email is from beehiiv', () => { From 3f82c0af01ce5d695eb0f049657e01c6babcf33c Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Tue, 18 Oct 2022 13:46:37 +0800 Subject: [PATCH 27/53] Handle cases where tweet items dont have parents --- packages/readabilityjs/Readability.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/readabilityjs/Readability.js b/packages/readabilityjs/Readability.js index ec174853b..ffd4f34d6 100644 --- a/packages/readabilityjs/Readability.js +++ b/packages/readabilityjs/Readability.js @@ -2231,12 +2231,12 @@ Readability.prototype = { // remove all containers the tweet is nested in (if they contain the tweet only) let tweetParent = tweet.parentElement || tweet.parentNode; - while (tweetParent && tweetParent.children.length === 1) { + while (tweetParent && tweetParent.children.length === 1 && tweetParent.parentNode) { tweetParent.parentNode.replaceChild(tweet, tweetParent); tweetParent = tweet.parentElement || tweet.parentNode; } - if (tweetParent && tweetParent.className.includes('twitter-tweet')) { + if (tweetParent && tweetParent.className.includes('twitter-tweet') && tweetParent.parentNode) { tweetParent.parentNode.replaceChild(tweet, tweetParent); } } else if (element.parentNode && element.parentNode.className === 'tweet') { From 14505ef17231d28153290318d77101aaf413b082 Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Tue, 18 Oct 2022 13:56:31 +0800 Subject: [PATCH 28/53] Remove placeholder text that was showing up on loading page --- packages/web/components/templates/SavingRequest.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/web/components/templates/SavingRequest.tsx b/packages/web/components/templates/SavingRequest.tsx index 04604ad04..b915e4dc8 100644 --- a/packages/web/components/templates/SavingRequest.tsx +++ b/packages/web/components/templates/SavingRequest.tsx @@ -33,7 +33,7 @@ export function Loader(): JSX.Element { '&:after': { width: '10px', display: 'inline-block', - content: 'test', + content: '', animation: `${breathe} steps(1,end) 2s infinite`, }, }}>Saving Link From 1fda6946bb40cfb947369b269f0db3172141164a Mon Sep 17 00:00:00 2001 From: Hongbo Wu Date: Tue, 18 Oct 2022 09:45:33 +0800 Subject: [PATCH 29/53] Save newsletters hosted by cooper-press.com correctly --- .../src/newsletters/cooper-press-handler.ts | 39 +++ .../test/data/node-weekly-newsletter.html | 250 ++++++++++++++++++ .../content-handler/test/newsletter.test.ts | 36 +++ 3 files changed, 325 insertions(+) create mode 100644 packages/content-handler/src/newsletters/cooper-press-handler.ts create mode 100644 packages/content-handler/test/data/node-weekly-newsletter.html diff --git a/packages/content-handler/src/newsletters/cooper-press-handler.ts b/packages/content-handler/src/newsletters/cooper-press-handler.ts new file mode 100644 index 000000000..1740c39f8 --- /dev/null +++ b/packages/content-handler/src/newsletters/cooper-press-handler.ts @@ -0,0 +1,39 @@ +import { ContentHandler } from '../content-handler' +import { parseHTML } from 'linkedom' + +export class CooperPressHandler extends ContentHandler { + constructor() { + super() + this.name = 'cooper-press' + } + + findNewsletterHeaderHref(dom: Document): string | undefined { + const readOnline = dom.querySelectorAll('a') + let res: string | undefined = undefined + readOnline.forEach((e) => { + if (e.textContent === 'Read on the Web') { + res = e.getAttribute('href') || undefined + } + }) + return res + } + + async isNewsletter(input: { + postHeader: string + from: string + unSubHeader: string + html: string + }): Promise { + const dom = parseHTML(input.html).document + return Promise.resolve( + dom.querySelectorAll('a[href*="cooperpress.com"]').length > 0 + ) + } + + async parseNewsletterUrl( + postHeader: string, + html: string + ): Promise { + return this.findNewsletterUrl(html) + } +} diff --git a/packages/content-handler/test/data/node-weekly-newsletter.html b/packages/content-handler/test/data/node-weekly-newsletter.html new file mode 100644 index 000000000..01173902f --- /dev/null +++ b/packages/content-handler/test/data/node-weekly-newsletter.html @@ -0,0 +1,250 @@ + + + + + + + + + + + + +
Plus Node 16.18.0, an IP info database, turning cron expressions into English, and 2FA with Twilio. |
+ + + +
+
+ + + +

#​458 — October 13, 2022

Read on the Web

+ + +
+ + + + + +
Together with  + + Userfront + +
+
+
Node.js Weekly
+
+ +
+ +
+ +

njt: Quick Navigation to npm Package Resources — Provides a rapid way to jump to various destinations related to npm packages (such as a project’s homepage, repo, issues, or even a package cost estimation). You can install it for use in your terminal, as a Chrome or Firefox search, via VS Code’s command palette (via LaunchX) or you can even use it directly on the Web here. – GitHub repo.

+

Alexander Kachkaev

+
+ +
+ +

Knip: Find Unused Files, Dependencies and Exports in TypeScript Projects — Knip’s creator tells us it’s Dutch for “cut” which is quite appropriate as it’s a new tool for trimming away things that aren’t being used in your project. If you just want to compare it to similar existing tools, there’s a handy comparison chart.

+

Lars Kappert

+
+ +
+ +

Node Authentication, Simplified — In this article, we lay out a new approach to authentication (plus access control & SSO) in Node.js applications.

+

Userfront sponsor

+
+ +
+ +

Node v16.18.0 (LTS) Released — Largely backported fixes and tweaks – no big headlines here.

+

Juan José (Node Core Team)

+
+ +
+ +

How to Write CommonJS Exports That Can Be Name-Imported from ESM — If you’ve ever got tangled up between using CommonJS and ES modules (I sure have!) Dr. Axel clears up a key cross-compatibility concern here.

+

Dr. Axel Rauschmayer

+
+ +
+ +

Adding Observability to Jest Tests — A look at how to get a bit more out of your Jest-based testing by keeping an eye on things.

+

Eliran Maman (Sprkl)

+
+ +
+ +

🔐  Node.js Authentication with Twilio Verify — If you’re happy using a third party service, bringing two-factor auth into your Express.js app needn’t be too hard. The author demonstrates the creation of a simple app that authenticates users using password-based authentication with an extra layer of OTPs (One-Time Passcodes) powered by Twilio’s Verify service.

+

Alexander Godwin

+
+
+ +

🛠 Code & Tools

+
+ +
+ +
+ +

IP Index: A Fast IP Lookup Web Service + Library — Returns blacklist status, detects VPN/hosting and shows geo and ASN info. The repo gets updated every day too.

+

Mykhailo Gorianskyi

+
+ +
+ +

cRonstrue: Library to Convert cron Expressions into Human Readable Form — Love the project name! The idea is given something like */10 * * * *, it will return “Every 10 minutes”. No dependencies.

+

Brady Holt

+
+ +
+ +

Dynaboard: The Pro-Code Web App Builder Made for Developers — Build high performance public and private web apps in a collaborative — code forward — WYSIWYG environment.

+

Dynaboard sponsor

+
+ +
+ +

Whoiser: A WHOIS Client for Node.js — Given a domain name, TLD, or IP address, it queries online WHOIS databases for info.

+

Andrei Igna

+
+ +
+ +

Print Ready: A JS-Powered CLI for Converting HTML Into PDFs — Uses Paged.js to render your HTML file inside Puppeteer, then exports a PDF from Puppeteer.

+

Nicholas C. Zakas

+
+ +
+ +

Check HTML Links: A Fast Checker for Broken Links/References in HTML — An npm package you can run on static pages to find broken links in href, src, and srcset, and can process 500-1000 documents in seconds.

+

Modern Web

+
+ +
+ +

human-signals: Human-Friendly Process Signal Info — Basically a JavaScript object that contains info about the various POSIX signals (SIGHUP, SIGINT, et al.)

+

ehmicky

+
+ +
+ +

Need to Upgrade Your Node.js App? Hire Us to Do It for You

+

UpgradeJS․com - The JS Upgrade Service by OmbuLabs sponsor

+
+ +
+

Flyweight: A Brand New ORM for SQLite — Early days but provides some extra abstraction around SQLite you might appreciate. +
Andrew Jones +

+
+
+
    +
  • +

    AdminJS 6.4
    + ↳ Admin panel / UI for Node apps.

    +
  • +
  • +

    Faker 7.6
    + ↳ Generate large amounts of fake data.

    +
  • +
  • +

    Middy 3.6
    + ↳ Node middleware engine for AWS Lambda.

    +
  • +
  • +

    quagga2 1.7.5
    + ↳ Advanced barcode scanning for browser and Node.

    +
  • +
  • +

    node-jira-client 8.2
    + ↳ Node wrapper for Jira's REST API.

    +
  • +
  • +

    RedisSMQ 7.1.1
    + ↳ High-performance Redis message queue.

    +
  • +
+
+
+ +

💻 Jobs

+ +
+

Full-Stack Engineer (NYC / Remote) — 100M+ devices, 100B+ API calls. Radar is looking for Product Engineers to build geospatial dev tools. +
Radar +

+
+ +
+

Find Tech Jobs with Hired — Create a profile on Hired to connect with hiring managers at growing startups and Fortune 500 companies. It's free for job-seekers. +
Hired +

+
+
+
+
+ +
+
+ + +n + diff --git a/packages/content-handler/test/newsletter.test.ts b/packages/content-handler/test/newsletter.test.ts index d4a1f2562..8393bce0e 100644 --- a/packages/content-handler/test/newsletter.test.ts +++ b/packages/content-handler/test/newsletter.test.ts @@ -15,6 +15,7 @@ import { BeehiivHandler } from '../src/newsletters/beehiiv-handler' import { ConvertkitHandler } from '../src/newsletters/convertkit-handler' import { parseHTML } from 'linkedom' import { GhostHandler } from '../src/newsletters/ghost-handler' +import { CooperPressHandler } from '../src/newsletters/cooper-press-handler' chai.use(chaiAsPromised) chai.use(chaiString) @@ -179,6 +180,17 @@ describe('Newsletter email test', () => { }) ).to.eventually.be.true }) + it('returns true for node-weekly newsletter', async () => { + const html = load('./test/data/node-weekly-newsletter.html') + await expect( + new CooperPressHandler().isNewsletter({ + html, + postHeader: '', + from: '', + unSubHeader: '', + }) + ).to.eventually.be.true + }) }) describe('findNewsletterUrl', async () => { @@ -289,6 +301,30 @@ describe('Newsletter email test', () => { expect(url).to.startWith('https://www.openml.fyi/2022-10-14/') }).timeout(10000) }) + + context('when email is from cooper press', () => { + before(() => { + nock('https://u25184427.ct.sendgrid.net') + .head( + '/ls/click?upn=MnmHBiCwIPe9TmIJeskmA7mFdqmsIs-2B5Xs-2FNpSIs56o0z9xhskaXR4aYohHPLtwRHfml_vVXscVLXlj5UtQe3aqo5RMTdTq2PepdZjP86UOmA8nxtQVfuqJiLh7Fio3fEtt5ouN4IH56AfszUQpxY-2FQ233kp0bjSZhBBVWAB43dgKumQkDW-2BxDFnQIUpvhmEgzSJq-2FMRG00GM7fkZVuPU-2BX8cdg8AGRHUU9Qhw6W67XEMkJVygTdm70Mo9ypNi8N33hgmhM3F6un9s7p1K1Gq-2FunslA-3D-3D' + ) + .reply(302, undefined, { + Location: 'https://nodeweekly.com/issues/458', + }) + .get('/issues/458') + .reply(200, '') + }) + + after(() => { + nock.restore() + }) + + it('gets the URL from the header', async () => { + const html = load('./test/data/node-weekly-newsletter.html') + const url = await new CooperPressHandler().findNewsletterUrl(html) + expect(url).to.startWith('https://nodeweekly.com/issues/458') + }).timeout(10000) + }) }) describe('generateUniqueUrl', () => { From 706607ea2bd89acb693f8f66814a4b780f694f2e Mon Sep 17 00:00:00 2001 From: Hongbo Wu Date: Tue, 18 Oct 2022 14:54:33 +0800 Subject: [PATCH 30/53] Rebase main --- .../src/newsletters/cooper-press-handler.ts | 5 ++--- packages/content-handler/test/newsletter.test.ts | 9 +++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/content-handler/src/newsletters/cooper-press-handler.ts b/packages/content-handler/src/newsletters/cooper-press-handler.ts index 1740c39f8..d0f39f4c2 100644 --- a/packages/content-handler/src/newsletters/cooper-press-handler.ts +++ b/packages/content-handler/src/newsletters/cooper-press-handler.ts @@ -1,5 +1,4 @@ import { ContentHandler } from '../content-handler' -import { parseHTML } from 'linkedom' export class CooperPressHandler extends ContentHandler { constructor() { @@ -22,9 +21,9 @@ export class CooperPressHandler extends ContentHandler { postHeader: string from: string unSubHeader: string - html: string + dom: Document }): Promise { - const dom = parseHTML(input.html).document + const dom = input.dom return Promise.resolve( dom.querySelectorAll('a[href*="cooperpress.com"]').length > 0 ) diff --git a/packages/content-handler/test/newsletter.test.ts b/packages/content-handler/test/newsletter.test.ts index 8393bce0e..c3a628055 100644 --- a/packages/content-handler/test/newsletter.test.ts +++ b/packages/content-handler/test/newsletter.test.ts @@ -182,9 +182,10 @@ describe('Newsletter email test', () => { }) it('returns true for node-weekly newsletter', async () => { const html = load('./test/data/node-weekly-newsletter.html') + const dom = parseHTML(html).document await expect( new CooperPressHandler().isNewsletter({ - html, + dom, postHeader: '', from: '', unSubHeader: '', @@ -204,9 +205,9 @@ describe('Newsletter email test', () => { Location: 'https://newsletter.slowchinese.net/p/companies-that-eat-people-217', }) - nock('https://newsletter.slowchinese.net') - .head('/p/companies-that-eat-people-217') - .reply(200, '') + nock('https://newsletter.slowchinese.net') + .head('/p/companies-that-eat-people-217') + .reply(200, '') }) after(() => { nock.restore() From 57846a1c5e5ed036daa19821eb4b05b32267574a Mon Sep 17 00:00:00 2001 From: Hongbo Wu Date: Tue, 18 Oct 2022 15:14:38 +0800 Subject: [PATCH 31/53] Fix tests --- .../test/apple-news-handler.test.ts | 5 ++ .../content-handler/test/newsletter.test.ts | 49 ++++++------------- 2 files changed, 20 insertions(+), 34 deletions(-) diff --git a/packages/content-handler/test/apple-news-handler.test.ts b/packages/content-handler/test/apple-news-handler.test.ts index 1584f9e28..058ece4f4 100644 --- a/packages/content-handler/test/apple-news-handler.test.ts +++ b/packages/content-handler/test/apple-news-handler.test.ts @@ -1,6 +1,11 @@ import { AppleNewsHandler } from '../src/websites/apple-news-handler' +import nock from 'nock' describe('open a simple web page', () => { + before(() => { + nock('https://apple.news').get('/AxjzaZaPvSn23b67LhXI5EQ').reply(200, '') + }) + it('should return a response', async () => { const response = await new AppleNewsHandler().preHandle( 'https://apple.news/AxjzaZaPvSn23b67LhXI5EQ' diff --git a/packages/content-handler/test/newsletter.test.ts b/packages/content-handler/test/newsletter.test.ts index c3a628055..11777c420 100644 --- a/packages/content-handler/test/newsletter.test.ts +++ b/packages/content-handler/test/newsletter.test.ts @@ -201,7 +201,7 @@ describe('Newsletter email test', () => { .head( '/c/eJxNkk2TojAQhn-N3KTyQfg4cGDGchdnYcsZx9K5UCE0EMVAkTiKv36iHnarupNUd7rfVJ4W3EDTj1M89No496Uw0wCxgovuwBgYnbOGsZBVjDHzKPWYU8VehUMWOlIX9Qhw4rKLzXgGZziXnRTcyF7dK0iIGMVOG_OS1aTmKPRDilgVhTQUPCQIcE0x-MFTmJ8rCUpA3KtuenR2urg1ZtAzmszI0tq_Z7m66y-ilQo0uAqMTQ7WRX8auJKg56blZg7WB-iHDuYEBzO6NP0R1IwuYFphQbbTjnTH9NBfs80nym4Zyj8uUvyKbtUyGr5eUz9fNDQ7JCxfJDo9dW1lY9lmj_JNivPbGmf2Pt_lN9tDit9b-WeTetni85Z9pDpVOd7L1E_Vy7egayNO23ZP34eSeLJeux1b0rer_xaZ7ykS78nuSjMY-nL98rparNZNcv07JCjN06_EkTFBxBqOUMACErnELUNMSxTUjLDQZwzcqa4bRjCfeejUEFefS224OLr2S5wxPtij7lVrs80d2CNseRV2P52VNFMBipcdVE-U5jkRD7hFAwpGOylVwU2Mfc9qBh7DoR89yVnWXhgQFHnIsbpVb6tU_B-hH_2yzWY' ) - .reply(302, undefined, { + .reply(301, undefined, { Location: 'https://newsletter.slowchinese.net/p/companies-that-eat-people-217', }) @@ -209,9 +209,6 @@ describe('Newsletter email test', () => { .head('/p/companies-that-eat-people-217') .reply(200, '') }) - after(() => { - nock.restore() - }) it('gets the URL from the header', async () => { const html = load('./test/data/substack-forwarded-newsletter.html') @@ -229,24 +226,21 @@ describe('Newsletter email test', () => { .head( '/ss/c/AX1lEgEQaxtvFxLaVo0GBo_geajNrlI1TGeIcmMViR3pL3fEDZnbbkoeKcaY62QZk0KPFudUiUXc_uMLerV4nA/3k5/3TFZmreTR0qKSCgowABnVg/h30/zzLik7UXd1H_n4oyd5W8Xu639AYQQB2UXz-CsssSnno' ) - .reply(302, undefined, { + .reply(301, undefined, { Location: 'https://www.milkroad.com/p/talked-guy-spent-30m-beeple', }) - .get('/p/talked-guy-spent-30m-beeple') + nock('https://www.milkroad.com') + .head('/p/talked-guy-spent-30m-beeple') .reply(200, '') }) - after(() => { - nock.restore() - }) - it('gets the URL from the header', async () => { const html = load('./test/data/beehiiv-newsletter.html') const url = await new BeehiivHandler().findNewsletterUrl(html) expect(url).to.startWith( 'https://www.milkroad.com/p/talked-guy-spent-30m-beeple' ) - }).timeout(10000) + }) }) context('when email is from convertkit', () => { @@ -255,22 +249,19 @@ describe('Newsletter email test', () => { .head( '/ls/click?upn=MnmHBiCwIPe9TmIJeskmA7wDTJvdVU1ACmSJ753YuhScf71JWthxqM8RnVh-2FZG0rYzrbR04P99S2ld2OkTtQmrx2FDwArpYdk5N0jVpN9dLBZ-2BdPNqkRHxNvuygY8-2F-2FtRNFoPjxjtTuyWM6L3tcYDYnAnL2xCueddWcFlUNrQWsvLotmgvC-2BrQc7bxsZhW0pUBmS_vVXscVLXlj5UtQe3aqo5RMTdTq2PepdZjP86UOmA8nzSBnnaDN-2FNHWDodWnbUOPZ063v3w3z8QtcaPpE1qNu8xYkNJJFb-2F1uZEG-2BzsLfyDkjvvVX5zYs5OyyRYlhMOlXDJcr4-2FtMrFwii0uFAvwbhxDdnTxEpi-2F7maufyH39AEO-2BtCeSUg5V4FM43UpI1zUSXeWK-2Fh5JumSmR5XhrrRAig-3D-3D' ) - .reply(302, undefined, { + .reply(301, undefined, { Location: 'https://fs.blog/brain-food/october-16-2022/', }) - .get('/brain-food/october-16-2022/') + nock('https://fs.blog') + .head('/brain-food/october-16-2022/') .reply(200, '') }) - after(() => { - nock.restore() - }) - it('gets the URL from the header', async () => { const html = load('./test/data/convertkit-newsletter.html') const url = await new ConvertkitHandler().findNewsletterUrl(html) expect(url).to.startWith('https://fs.blog/brain-food/october-16-2022/') - }).timeout(10000) + }) }) it('returns undefined if it is not a newsletter', async () => { @@ -285,22 +276,17 @@ describe('Newsletter email test', () => { .head( '/ls/click?upn=MnmHBiCwIPe9TmIJeskmA9nRLefEmmgrd5xWS-2Bc39wxPBpwDRny1FmWt1H0FpgKAz1dv_vVXscVLXlj5UtQe3aqo5RMTdTq2PepdZjP86UOmA8nzulL-2F3YyC-2FHgJV0JnOPtjNvgjHSaQVfisQ15hPQtnlo4t73zgTQL4QnDoer4qJ3-2F2Lf-2F2ElFMF3NyoUD4eqWCWwUM0w4P9Feaeo-2BolkySAB611BySXRt6V3Z-2F7mQcpcRX3D9zV-2B-2FdRY0Vn30aR-2BKY8qpTFuivxzF19UkQGjK5srg-3D-3D' ) - .reply(302, undefined, { + .reply(301, undefined, { Location: 'https://www.openml.fyi/2022-10-14/', }) - .get('/2022-10-14/') - .reply(200, '') - }) - - after(() => { - nock.restore() + nock('https://www.openml.fyi').head('/2022-10-14/').reply(200, '') }) it('gets the URL from the header', async () => { const html = load('./test/data/ghost-newsletter.html') const url = await new GhostHandler().findNewsletterUrl(html) expect(url).to.startWith('https://www.openml.fyi/2022-10-14/') - }).timeout(10000) + }) }) context('when email is from cooper press', () => { @@ -309,22 +295,17 @@ describe('Newsletter email test', () => { .head( '/ls/click?upn=MnmHBiCwIPe9TmIJeskmA7mFdqmsIs-2B5Xs-2FNpSIs56o0z9xhskaXR4aYohHPLtwRHfml_vVXscVLXlj5UtQe3aqo5RMTdTq2PepdZjP86UOmA8nxtQVfuqJiLh7Fio3fEtt5ouN4IH56AfszUQpxY-2FQ233kp0bjSZhBBVWAB43dgKumQkDW-2BxDFnQIUpvhmEgzSJq-2FMRG00GM7fkZVuPU-2BX8cdg8AGRHUU9Qhw6W67XEMkJVygTdm70Mo9ypNi8N33hgmhM3F6un9s7p1K1Gq-2FunslA-3D-3D' ) - .reply(302, undefined, { + .reply(301, undefined, { Location: 'https://nodeweekly.com/issues/458', }) - .get('/issues/458') - .reply(200, '') - }) - - after(() => { - nock.restore() + nock('https://nodeweekly.com').head('/issues/458').reply(200, '') }) it('gets the URL from the header', async () => { const html = load('./test/data/node-weekly-newsletter.html') const url = await new CooperPressHandler().findNewsletterUrl(html) expect(url).to.startWith('https://nodeweekly.com/issues/458') - }).timeout(10000) + }) }) }) From 990759da73597e4fc1cc4dd755898b1460ce7f85 Mon Sep 17 00:00:00 2001 From: Hongbo Wu Date: Tue, 18 Oct 2022 12:01:52 +0800 Subject: [PATCH 32/53] Save base64 encoded image site icon in page --- packages/readabilityjs/Readability.js | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/packages/readabilityjs/Readability.js b/packages/readabilityjs/Readability.js index ffd4f34d6..07cfc11bf 100644 --- a/packages/readabilityjs/Readability.js +++ b/packages/readabilityjs/Readability.js @@ -1909,10 +1909,21 @@ Readability.prototype = { values["og:site_name"] || null; // get website icon - const iconLink = this._doc.querySelector( + const siteIcon = this._doc.querySelector( "link[rel='apple-touch-icon'], link[rel='shortcut icon'], link[rel='icon']" ); - metadata.siteIcon = iconLink?.href; + if (siteIcon) { + const iconHref = siteIcon.getAttribute("href"); + if (iconHref) { + if (this.REGEXPS.b64DataUrl.test(iconHref)) { + // base64 encoded image + metadata.siteIcon = iconHref; + } else { + // allow relative URLs + metadata.siteIcon = this.toAbsoluteURI(iconHref); + } + } + } // get published date metadata.publishedDate = jsonld.publishedDate || From 22e92256ebbe0008d73f2ebf5720c70c388c13ba Mon Sep 17 00:00:00 2001 From: Hongbo Wu Date: Tue, 18 Oct 2022 12:03:23 +0800 Subject: [PATCH 33/53] Fetch subscription's favicon if the site icon is base64 encoded image --- packages/api/src/services/save_newsletter_email.ts | 5 +++-- packages/api/src/utils/helpers.ts | 4 ++++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/api/src/services/save_newsletter_email.ts b/packages/api/src/services/save_newsletter_email.ts index 84daf9a40..0f505c53b 100644 --- a/packages/api/src/services/save_newsletter_email.ts +++ b/packages/api/src/services/save_newsletter_email.ts @@ -14,6 +14,7 @@ import { saveSubscription } from './subscriptions' import { NewsletterEmail } from '../entity/newsletter_email' import { fetchFavicon } from '../utils/parser' import { updatePage } from '../elastic/pages' +import { isBase64Image } from '../utils/helpers' interface NewsletterMessage { email: string @@ -69,8 +70,8 @@ export const saveNewsletterEmail = async ( return false } - if (!page.siteIcon) { - // fetch favicon if not already set + if (!page.siteIcon || isBase64Image(page.siteIcon)) { + // fetch favicon if not already set or is a base64 image const favicon = await fetchFavicon(page.url) if (favicon) { page.siteIcon = favicon diff --git a/packages/api/src/utils/helpers.ts b/packages/api/src/utils/helpers.ts index c85903660..a3137fbfb 100644 --- a/packages/api/src/utils/helpers.ts +++ b/packages/api/src/utils/helpers.ts @@ -269,3 +269,7 @@ export const wordsCount = (text: string, isHtml = true): number => { return 0 } } + +export const isBase64Image = (str: string): boolean => { + return str.startsWith('data:image/') +} From a7e92addb0bd4a3a3b583f43d640dfa6e5aeb1ac Mon Sep 17 00:00:00 2001 From: Hongbo Wu Date: Tue, 18 Oct 2022 12:04:22 +0800 Subject: [PATCH 34/53] Create 128 * 128 proxy image for the site icon --- packages/api/src/resolvers/article/index.ts | 7 ++++++- packages/api/src/resolvers/subscriptions/index.ts | 2 +- packages/api/src/utils/parser.ts | 2 +- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/packages/api/src/resolvers/article/index.ts b/packages/api/src/resolvers/article/index.ts index 1414ca56e..5b55a6d19 100644 --- a/packages/api/src/resolvers/article/index.ts +++ b/packages/api/src/resolvers/article/index.ts @@ -54,6 +54,7 @@ import { ContentParseError } from '../../utils/errors' import { authorized, generateSlug, + isBase64Image, isParsingTimeout, pageError, stringToHash, @@ -889,6 +890,10 @@ export const searchResolver = authorized< } const edges = results.map((r) => { + let siteIcon = r.siteIcon + if (siteIcon && !isBase64Image(siteIcon)) { + siteIcon = createImageProxyUrl(siteIcon, 128, 128) + } return { node: { ...r, @@ -900,7 +905,7 @@ export const searchResolver = authorized< publishedAt: validatedDate(r.publishedAt), ownedByViewer: r.userId === claims.uid, pageType: r.pageType || PageType.Highlights, - siteIcon: r.siteIcon && createImageProxyUrl(r.siteIcon, 32, 32), + siteIcon, } as SearchItem, cursor: endCursor, } diff --git a/packages/api/src/resolvers/subscriptions/index.ts b/packages/api/src/resolvers/subscriptions/index.ts index eb416b868..66ef3b2f4 100644 --- a/packages/api/src/resolvers/subscriptions/index.ts +++ b/packages/api/src/resolvers/subscriptions/index.ts @@ -60,7 +60,7 @@ export const subscriptionsResolver = authorized< return { subscriptions: subscriptions.map((s) => ({ ...s, - icon: s.icon && createImageProxyUrl(s.icon, 32, 32), + icon: s.icon && createImageProxyUrl(s.icon, 128, 128), })), } } catch (error) { diff --git a/packages/api/src/utils/parser.ts b/packages/api/src/utils/parser.ts index eee86b417..291fa5d4f 100644 --- a/packages/api/src/utils/parser.ts +++ b/packages/api/src/utils/parser.ts @@ -442,7 +442,7 @@ export const fetchFavicon = async ( const response = await axios.head(url, { timeout: 5000 }) const realUrl = response.request.res.responseUrl const domain = new URL(realUrl).hostname - return `https://api.faviconkit.com/${domain}/32` + return `https://api.faviconkit.com/${domain}/128` } catch (e) { console.log('Error fetching favicon', e) return undefined From 3b53f523d8ada913606f071c8f10fb58c8c24578 Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Tue, 18 Oct 2022 15:58:50 +0800 Subject: [PATCH 35/53] Set background colours on email forms as these pages arent themed --- .../web/components/templates/auth/EmailForgotPassword.tsx | 1 + packages/web/components/templates/auth/EmailLogin.tsx | 2 ++ packages/web/components/templates/auth/EmailResetPassword.tsx | 1 + packages/web/components/templates/auth/EmailSignup.tsx | 4 ++++ 4 files changed, 8 insertions(+) diff --git a/packages/web/components/templates/auth/EmailForgotPassword.tsx b/packages/web/components/templates/auth/EmailForgotPassword.tsx index e641a6b3f..888cee302 100644 --- a/packages/web/components/templates/auth/EmailForgotPassword.tsx +++ b/packages/web/components/templates/auth/EmailForgotPassword.tsx @@ -51,6 +51,7 @@ export function EmailForgotPassword(): JSX.Element { name="email" value={email} placeholder="Email" + css={{ bg: 'white '}} onChange={(e) => { e.preventDefault(); setEmail(e.target.value); }} /> diff --git a/packages/web/components/templates/auth/EmailLogin.tsx b/packages/web/components/templates/auth/EmailLogin.tsx index e0332c87a..63541f4e0 100644 --- a/packages/web/components/templates/auth/EmailLogin.tsx +++ b/packages/web/components/templates/auth/EmailLogin.tsx @@ -55,6 +55,7 @@ export function EmailLogin(): JSX.Element { name="email" value={email} placeholder="Email" + css={{ bg: 'white '}} onChange={(e) => { e.preventDefault(); setEmail(e.target.value); }} /> @@ -67,6 +68,7 @@ export function EmailLogin(): JSX.Element { name="password" value={password} placeholder="Password" + css={{ bg: 'white '}} onChange={(e) => setPassword(e.target.value)} /> diff --git a/packages/web/components/templates/auth/EmailResetPassword.tsx b/packages/web/components/templates/auth/EmailResetPassword.tsx index 43b8e65d8..67d97ead8 100644 --- a/packages/web/components/templates/auth/EmailResetPassword.tsx +++ b/packages/web/components/templates/auth/EmailResetPassword.tsx @@ -61,6 +61,7 @@ export function EmailResetPassword(): JSX.Element { name="password" value={password} placeholder="Password" + css={{ bg: 'white '}} onChange={(e) => { e.preventDefault(); setPassword(e.target.value); }} /> (Password must be at least 8 chars) diff --git a/packages/web/components/templates/auth/EmailSignup.tsx b/packages/web/components/templates/auth/EmailSignup.tsx index 6a5c3259d..3139b3b74 100644 --- a/packages/web/components/templates/auth/EmailSignup.tsx +++ b/packages/web/components/templates/auth/EmailSignup.tsx @@ -73,6 +73,7 @@ export function EmailSignup(): JSX.Element { name="email" value={email} placeholder="Email" + css={{ bg: 'white '}} onChange={(e) => { e.preventDefault(); setEmail(e.target.value); }} /> @@ -85,6 +86,7 @@ export function EmailSignup(): JSX.Element { name="password" value={password} placeholder="Password" + css={{ bg: 'white '}} onChange={(e) => setPassword(e.target.value)} /> @@ -97,6 +99,7 @@ export function EmailSignup(): JSX.Element { name="name" value={fullname} placeholder="Full Name" + css={{ bg: 'white '}} onChange={(e) => setFullname(e.target.value)} /> @@ -109,6 +112,7 @@ export function EmailSignup(): JSX.Element { name="username" value={username} placeholder="Username" + css={{ bg: 'white '}} onChange={handleUsernameChange} /> From d34e5aff0464a06bd998cd539871352d7bf65a45 Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Tue, 18 Oct 2022 17:34:57 +0800 Subject: [PATCH 36/53] Change the order of the new user welcome articles In the UI it will appear: - Getting Started - Power of Read it Later - Organize with Labels - (optional) iOS/Android/Web --- packages/api/src/services/popular_reads.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/api/src/services/popular_reads.ts b/packages/api/src/services/popular_reads.ts index 4adb3f569..655b3d72d 100644 --- a/packages/api/src/services/popular_reads.ts +++ b/packages/api/src/services/popular_reads.ts @@ -122,9 +122,9 @@ export const addPopularReadsForNewUser = async ( userId: string ): Promise => { const defaultReads = [ - 'omnivore_get_started', - 'power_read_it_later', 'omnivore_organize', + 'power_read_it_later', + 'omnivore_get_started', ] // get client from request context From a341475438006226a22f1de210c463d42a037a36 Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Tue, 18 Oct 2022 16:25:40 -0700 Subject: [PATCH 37/53] store web prefs in dataStore. dispatch changes to webview --- .../java/app/omnivore/omnivore/Constants.kt | 7 ++ .../ui/reader/WebPreferencesDialog.kt | 19 +++-- .../omnivore/omnivore/ui/reader/WebReader.kt | 22 ++---- .../omnivore/ui/reader/WebReaderViewModel.kt | 76 ++++++++++++++++++- 4 files changed, 96 insertions(+), 28 deletions(-) diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/Constants.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/Constants.kt index 3b219d6a3..075eb297b 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/Constants.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/Constants.kt @@ -1,5 +1,7 @@ package app.omnivore.omnivore +import app.omnivore.omnivore.ui.reader.WebFont + object Constants { const val apiURL = BuildConfig.OMNIVORE_API_URL const val webURL = BuildConfig.OMNIVORE_WEB_URL @@ -10,6 +12,11 @@ object DatastoreKeys { const val omnivoreAuthToken = "omnivoreAuthToken" const val omnivoreAuthCookieString = "omnivoreAuthCookieString" const val omnivorePendingUserToken = "omnivorePendingUserToken" + const val preferredWebFontSize = "preferredWebFontSize" + const val preferredWebLineHeight = "preferredWebLineHeight" + const val preferredWebMaxWidthPercentage = "preferredWebMaxWidthPercentage" + const val preferredWebFontFamily = "preferredWebFontFamily" + const val prefersWebHighContrastText = "prefersWebHighContrastText" } object AppleConstants { diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/WebPreferencesDialog.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/WebPreferencesDialog.kt index 346011b72..550162d53 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/WebPreferencesDialog.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/WebPreferencesDialog.kt @@ -16,22 +16,21 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp import androidx.compose.ui.window.Dialog -import com.pspdfkit.ui.note.AlignedAnnotationHinterDrawable @Composable -fun WebPreferencesDialog(onDismiss: () -> Unit) { +fun WebPreferencesDialog(onDismiss: () -> Unit, webReaderViewModel: WebReaderViewModel) { Dialog(onDismissRequest = { onDismiss() }) { Surface( shape = RoundedCornerShape(16.dp), color = Color.White ) { - WebPreferencesView() + WebPreferencesView(webReaderViewModel) } } } @Composable -fun WebPreferencesView() { +fun WebPreferencesView(webReaderViewModel: WebReaderViewModel) { Column( modifier = Modifier .padding(top = 6.dp, start = 6.dp, end = 6.dp, bottom = 6.dp) @@ -48,22 +47,22 @@ fun WebPreferencesView() { // Font Size: Stepper Stepper( label = "Font Size:", - onIncrease = {}, - onDecrease = {} + onIncrease = { webReaderViewModel.updateFontSize(isIncrease = true) }, + onDecrease = { webReaderViewModel.updateFontSize(isIncrease = false) } ) // Margin: Slider Stepper( label = "Margin:", - onIncrease = {}, - onDecrease = {} + onIncrease = { webReaderViewModel.updateMaxWidthPercentage(isIncrease = false) }, + onDecrease = { webReaderViewModel.updateMaxWidthPercentage(isIncrease = true) } ) // Line Spacing: Slider Stepper( label = "Line Spacing:", - onIncrease = {}, - onDecrease = {} + onIncrease = { webReaderViewModel.updateLineSpacing(isIncrease = true) }, + onDecrease = { webReaderViewModel.updateLineSpacing(isIncrease = false) } ) // High Contrast Text: Switch diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/WebReader.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/WebReader.kt index 9a0b6bd36..afff5de0f 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/WebReader.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/WebReader.kt @@ -33,16 +33,6 @@ import kotlin.math.roundToInt @Composable fun WebReaderLoadingContainer(slug: String, webReaderViewModel: WebReaderViewModel) { - // TODO: maybe move to web reader view model? - val defaultWebPreferences = WebPreferences( - textFontSize = 12, - lineHeight = 150, - maxWidthPercentage = 100, - themeKey = "LightGray", - fontFamily = WebFont.SYSTEM, - prefersHighContrastText = false - ) - var showWebPreferencesDialog by remember { mutableStateOf(false ) } val webReaderParams: WebReaderParams? by webReaderViewModel.webReaderParamsLiveData.observeAsState(null) @@ -86,7 +76,7 @@ fun WebReaderLoadingContainer(slug: String, webReaderViewModel: WebReaderViewMod .requiredHeight(height = maxToolbarHeight) ) { } - WebReader(webReaderParams!!, defaultWebPreferences, webReaderViewModel) + WebReader(webReaderParams!!, webReaderViewModel.storedWebPreferences(), webReaderViewModel) } TopAppBar( @@ -107,9 +97,12 @@ fun WebReaderLoadingContainer(slug: String, webReaderViewModel: WebReaderViewMod ) if (showWebPreferencesDialog) { - WebPreferencesDialog { - showWebPreferencesDialog = false - } + WebPreferencesDialog( + onDismiss = { + showWebPreferencesDialog = false + }, + webReaderViewModel = webReaderViewModel + ) } if (annotation != null) { @@ -195,6 +188,7 @@ fun WebReader( }, update = { if (javascriptActionLoopUUID != webReaderViewModel.lastJavascriptActionLoopUUID) { for (script in webReaderViewModel.javascriptDispatchQueue) { + Log.d("js", "executing script: $script") it.evaluateJavascript(script, null) } webReaderViewModel.resetJavascriptDispatchQueue() diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/WebReaderViewModel.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/WebReaderViewModel.kt index d1f2c7ed9..4ce99a090 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/WebReaderViewModel.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/WebReaderViewModel.kt @@ -5,12 +5,14 @@ import androidx.compose.foundation.ScrollState import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import app.omnivore.omnivore.DatastoreKeys import app.omnivore.omnivore.DatastoreRepository import app.omnivore.omnivore.models.LinkedItem import app.omnivore.omnivore.networking.* import com.google.gson.Gson import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking import org.json.JSONObject import java.util.* import javax.inject.Inject @@ -29,7 +31,7 @@ class WebReaderViewModel @Inject constructor( private val datastoreRepo: DatastoreRepository, private val networker: Networker ): ViewModel() { - var lastJavascriptActionLoopUUID = UUID.randomUUID() + var lastJavascriptActionLoopUUID: UUID = UUID.randomUUID() var javascriptDispatchQueue: MutableList = mutableListOf() var scrollState = ScrollState(0) @@ -102,18 +104,84 @@ class WebReaderViewModel @Inject constructor( } fun resetJavascriptDispatchQueue() { - lastJavascriptActionLoopUUID = javascriptActionLoopUUIDLiveData.value + lastJavascriptActionLoopUUID = javascriptActionLoopUUIDLiveData.value ?: UUID.randomUUID() javascriptDispatchQueue = mutableListOf() } fun saveAnnotation(annotation: String) { val script = "var event = new Event('saveAnnotation');event.annotation = '$annotation';document.dispatchEvent(event);" - javascriptDispatchQueue.add(script) - javascriptActionLoopUUIDLiveData.value = UUID.randomUUID() + enqueueScript(script) cancelAnnotationEdit() } fun cancelAnnotationEdit() { annotationLiveData.value = null } + + private fun enqueueScript(javascript: String) { + javascriptDispatchQueue.add(javascript) + javascriptActionLoopUUIDLiveData.value = UUID.randomUUID() + } + + fun storedWebPreferences(): WebPreferences = runBlocking { + val storedFontSize = datastoreRepo.getInt(DatastoreKeys.preferredWebFontSize) + val storedLineHeight = datastoreRepo.getInt(DatastoreKeys.preferredWebLineHeight) + val storedMaxWidth = datastoreRepo.getInt(DatastoreKeys.preferredWebMaxWidthPercentage) + val storedFontFamily = datastoreRepo.getString(DatastoreKeys.preferredWebFontFamily) + val prefersHighContrastFont = datastoreRepo.getString(DatastoreKeys.prefersWebHighContrastText) == "true" + + WebPreferences( + textFontSize = storedFontSize ?: 12, + lineHeight = storedLineHeight ?: 150, + maxWidthPercentage = storedMaxWidth ?: 100, + themeKey = "LightGray", + fontFamily = WebFont.SYSTEM, + prefersHighContrastText = prefersHighContrastFont + ) + } + + fun updateFontSize(isIncrease: Boolean) { + val delta = if (isIncrease) 2 else -2 + var newFontSize: Int + + runBlocking { + val storedFontSize = datastoreRepo.getInt(DatastoreKeys.preferredWebFontSize) + newFontSize = ((storedFontSize ?: 12) + delta).coerceIn(8, 28) + datastoreRepo.putInt(DatastoreKeys.preferredWebFontSize, newFontSize) + } + + // Get value from data store and then update it + val script = "var event = new Event('updateFontSize');event.fontSize = '$newFontSize';document.dispatchEvent(event);" + enqueueScript(script) + } + + fun updateMaxWidthPercentage(isIncrease: Boolean) { + val delta = if (isIncrease) 10 else -10 + var newMaxWidthPercentageValue: Int + + runBlocking { + val storedWidth = datastoreRepo.getInt(DatastoreKeys.preferredWebMaxWidthPercentage) + newMaxWidthPercentageValue = ((storedWidth ?: 100) + delta).coerceIn(40, 100) + datastoreRepo.putInt(DatastoreKeys.preferredWebMaxWidthPercentage, newMaxWidthPercentageValue) + } + + // Get value from data store and then update it + val script = "var event = new Event('updateMaxWidthPercentage');event.maxWidthPercentage = '$newMaxWidthPercentageValue';document.dispatchEvent(event);" + enqueueScript(script) + } + + fun updateLineSpacing(isIncrease: Boolean) { + val delta = if (isIncrease) 25 else -25 + var newLineHeight: Int + + runBlocking { + val storedHeight = datastoreRepo.getInt(DatastoreKeys.preferredWebLineHeight) + newLineHeight = ((storedHeight ?: 150) + delta).coerceIn(100, 300) + datastoreRepo.putInt(DatastoreKeys.preferredWebLineHeight, newLineHeight) + } + + // Get value from data store and then update it + val script = "var event = new Event('updateLineHeight');event.lineHeight = '$newLineHeight';document.dispatchEvent(event);" + enqueueScript(script) + } } From 0ad8558e92823c9022f21234bebc9c8d54901273 Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Wed, 19 Oct 2022 17:22:21 +0800 Subject: [PATCH 38/53] Add the iOS Saving Links article as a default read for new Omnivore users on iOS This also re-orders things a bit, making sure the getting started guide is always the topmost in a user's library. --- packages/api/src/services/popular_reads.ts | 14 +- .../popular_reads/omnivore_ios-content.html | 94 ++ .../popular_reads/omnivore_ios-original.html | 1456 +++++++++++++++++ 3 files changed, 1563 insertions(+), 1 deletion(-) create mode 100644 packages/api/src/services/popular_reads/omnivore_ios-content.html create mode 100644 packages/api/src/services/popular_reads/omnivore_ios-original.html diff --git a/packages/api/src/services/popular_reads.ts b/packages/api/src/services/popular_reads.ts index 655b3d72d..5a063521d 100644 --- a/packages/api/src/services/popular_reads.ts +++ b/packages/api/src/services/popular_reads.ts @@ -124,7 +124,6 @@ export const addPopularReadsForNewUser = async ( const defaultReads = [ 'omnivore_organize', 'power_read_it_later', - 'omnivore_get_started', ] // get client from request context @@ -142,6 +141,9 @@ export const addPopularReadsForNewUser = async ( break } + // We always want this to be the top-most article in the user's + // list. So we save it last to have the greatest saved_at + defaultReads.push('omnivore_get_started') await addPopularReads(userId, defaultReads) } @@ -157,6 +159,16 @@ const popularReads = [ publishedAt: new Date('2021-10-13'), siteName: 'Omnivore Blog', }, + { + key: 'omnivore_ios', + url: 'https://blog.omnivore.app/p/saving-links-from-your-iphone-or', + title: "Saving Links from Your iPhone or iPad", + author: 'Omnivore', + description: 'Learn how to save articles on iOS.', + previewImage: 'https://proxy-prod.omnivore-image-cache.app/260x260,suM2fz_-6_1PDsQDursGPD2bQqnpgGH9Ymj-IVb5dUR4/https://substackcdn.com/image/youtube/w_728,c_limit/k6RkIqepAig', + publishedAt: new Date('2021-10-19'), + siteName: 'Omnivore Blog' + }, { key: 'omnivore_organize', url: 'https://blog.omnivore.app/p/organize-your-omnivore-library-with', diff --git a/packages/api/src/services/popular_reads/omnivore_ios-content.html b/packages/api/src/services/popular_reads/omnivore_ios-content.html new file mode 100644 index 000000000..2f79a0603 --- /dev/null +++ b/packages/api/src/services/popular_reads/omnivore_ios-content.html @@ -0,0 +1,94 @@ +
+
+
+

+ With the Omnivore app for iOS, it’s easy to save web pages and articles or archive web content to read later. +

+

+ The Omnivore app uses the iOS Share System, which lets you send items from one app (such as Safari) to another (such as Messages or Mail).  +

+
    +
  • +

    Step 1: Log in to the Omnivore app.

    +
  • +
  • +

    Step 2: Add Omnivore to your Share menu favorites.

    +
  • +
  • +

    Step 3: Save links to your Omnivore Library.

    +
  • +
+
+

+ +

+
+

You must be logged in before you can save links via the Share menu. If you don’t already have an Omnivore account, you can sign up for free from the login screen.

+

+ Note: If you haven’t installed the iOS app, download it here: https://omnivore.app/install/ios +

+

+ Step 2: Add Omnivore to your Share menu favorites. +

+

Start by viewing the Share menu from within any supported iOS app (we’ve used Safari for this example).

+
    +
  1. +

    + Tap the Share icon at the bottom of the screen. +

    +
  2. +
  3. +

    + Swipe left to the end of the list of app icons and tap More. +

    +
  4. +
  5. +

    + Tap Edit at the top of the screen. +

    +
  6. +
  7. +

    + Scroll down until you see the Omnivore icon and tap the + icon next to it.  +

    +
  8. +
  9. +

    + Press and hold the three-bar icon and drag Omnivore to one of the top positions under Favorites. Tap Done to close the menu. +

    +
  10. +
  11. +

    + Omnivore will appear as one of the first options the next time you use the Share feature (you may need to restart Safari). +

    +
  12. +
+

+ Step 3: Save links to your Omnivore Library +

+

+ Start by navigating to the page or article you wish to save. Please note that Omnivore will save the content that appears on your screen (not just a link), so if the page is behind a paywall and you are logged into the paywalled site, you will save the paid content.  +

+
    +
  1. +

    + While viewing the page you’d like to save, tap the Share icon. +

    +
  2. +
  3. +

    + Tap the Omnivore icon in the Share menu. +

    +
  4. +
  5. +

    + Tag the article with one or more labels (optional) and tap Read Now or Read Later +

    +
  6. +
  7. +

    If you choose Read Later, the link will appear in your Library the next time you open the Omnivore app.

    +
  8. +
+
+
+
\ No newline at end of file diff --git a/packages/api/src/services/popular_reads/omnivore_ios-original.html b/packages/api/src/services/popular_reads/omnivore_ios-original.html new file mode 100644 index 000000000..86daf6739 --- /dev/null +++ b/packages/api/src/services/popular_reads/omnivore_ios-original.html @@ -0,0 +1,1456 @@ + + + + + + + + + + + + + + + + + + + + + + + + + Saving Links from Your iPhone or iPad - Omnivore + + + + + + + + + + + + + + + + + + + + + +
+
+ + +
+ + + + +
+
+
+ +
+
+
+
+
+
+ Comments +
+
+
+
+
+ +
+
+ +
+
+
+
+ + + +
+
+
+
+
+
+
+
+ +
+
+
+ +
+
+ Learn the best ways to save links with Omnivore +
+
+
+
+ Omnivore +
+
+ +
+ + + + +
+ 2 +
+ + + + + +
+
+
+
+
+ +
+
+ Highlighted <code> in Omnivore +
+
+
+
+ Omnivore +
+
+ +
+ + + + +
+ 1 +
+ + + + + +
+
+
+
+
+ +
+
+ Add to your library with your Omnivore email address +
+
+
+
+ Omnivore +
+
+ +
+ + + + +
+ 1 +
+ + + + + +
+
+
See all + + + + +
+
+
+
+
+ +
+ +
+ +
+
+
+ + + + + + + From 675845bebc1365ccd0bd30fa2123eeb689b85350 Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Wed, 19 Oct 2022 17:29:22 +0800 Subject: [PATCH 39/53] Force non-themed colours on login forms as these pages are unthemed --- .../web/components/templates/ConfirmProfileModal.tsx | 2 +- .../templates/auth/EmailForgotPassword.tsx | 2 +- .../web/components/templates/auth/EmailLogin.tsx | 4 ++-- .../components/templates/auth/EmailResetPassword.tsx | 2 +- .../web/components/templates/auth/EmailSignup.tsx | 12 ++++++------ 5 files changed, 11 insertions(+), 11 deletions(-) diff --git a/packages/web/components/templates/ConfirmProfileModal.tsx b/packages/web/components/templates/ConfirmProfileModal.tsx index 6c36c580a..72a939947 100644 --- a/packages/web/components/templates/ConfirmProfileModal.tsx +++ b/packages/web/components/templates/ConfirmProfileModal.tsx @@ -139,7 +139,7 @@ export function ConfirmProfileModal(): JSX.Element { {isUsernameValid && ( Username is available. diff --git a/packages/web/components/templates/auth/EmailForgotPassword.tsx b/packages/web/components/templates/auth/EmailForgotPassword.tsx index 888cee302..11867cce5 100644 --- a/packages/web/components/templates/auth/EmailForgotPassword.tsx +++ b/packages/web/components/templates/auth/EmailForgotPassword.tsx @@ -51,7 +51,7 @@ export function EmailForgotPassword(): JSX.Element { name="email" value={email} placeholder="Email" - css={{ bg: 'white '}} + css={{ bg: 'white', color: 'black' }} onChange={(e) => { e.preventDefault(); setEmail(e.target.value); }} /> diff --git a/packages/web/components/templates/auth/EmailLogin.tsx b/packages/web/components/templates/auth/EmailLogin.tsx index 63541f4e0..ced660e4d 100644 --- a/packages/web/components/templates/auth/EmailLogin.tsx +++ b/packages/web/components/templates/auth/EmailLogin.tsx @@ -55,7 +55,7 @@ export function EmailLogin(): JSX.Element { name="email" value={email} placeholder="Email" - css={{ bg: 'white '}} + css={{ backgroundColor: 'white', color: 'black' }} onChange={(e) => { e.preventDefault(); setEmail(e.target.value); }} /> @@ -68,7 +68,7 @@ export function EmailLogin(): JSX.Element { name="password" value={password} placeholder="Password" - css={{ bg: 'white '}} + css={{ bg: 'white', color: 'black' }} onChange={(e) => setPassword(e.target.value)} /> diff --git a/packages/web/components/templates/auth/EmailResetPassword.tsx b/packages/web/components/templates/auth/EmailResetPassword.tsx index 67d97ead8..e58dbbf75 100644 --- a/packages/web/components/templates/auth/EmailResetPassword.tsx +++ b/packages/web/components/templates/auth/EmailResetPassword.tsx @@ -61,7 +61,7 @@ export function EmailResetPassword(): JSX.Element { name="password" value={password} placeholder="Password" - css={{ bg: 'white '}} + css={{ bg: 'white', color: 'black' }} onChange={(e) => { e.preventDefault(); setPassword(e.target.value); }} /> (Password must be at least 8 chars) diff --git a/packages/web/components/templates/auth/EmailSignup.tsx b/packages/web/components/templates/auth/EmailSignup.tsx index 3139b3b74..9a8a9b9c5 100644 --- a/packages/web/components/templates/auth/EmailSignup.tsx +++ b/packages/web/components/templates/auth/EmailSignup.tsx @@ -63,7 +63,7 @@ export function EmailSignup(): JSX.Element { return (
- Sign Up + Sign Up Email @@ -73,7 +73,7 @@ export function EmailSignup(): JSX.Element { name="email" value={email} placeholder="Email" - css={{ bg: 'white '}} + css={{ backgroundColor: 'white', color: 'black' }} onChange={(e) => { e.preventDefault(); setEmail(e.target.value); }} /> @@ -86,7 +86,7 @@ export function EmailSignup(): JSX.Element { name="password" value={password} placeholder="Password" - css={{ bg: 'white '}} + css={{ bg: 'white', color: 'black' }} onChange={(e) => setPassword(e.target.value)} /> @@ -99,7 +99,7 @@ export function EmailSignup(): JSX.Element { name="name" value={fullname} placeholder="Full Name" - css={{ bg: 'white '}} + css={{ bg: 'white', color: 'black' }} onChange={(e) => setFullname(e.target.value)} /> @@ -112,7 +112,7 @@ export function EmailSignup(): JSX.Element { name="username" value={username} placeholder="Username" - css={{ bg: 'white '}} + css={{ bg: 'white', color: 'black' }} onChange={handleUsernameChange} /> @@ -132,7 +132,7 @@ export function EmailSignup(): JSX.Element { {isUsernameValid && ( Username is available. From d5c7aa2b8f6f23e941d22ecccedb331210672c4e Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Wed, 19 Oct 2022 17:36:58 +0800 Subject: [PATCH 40/53] Fix linting --- packages/api/src/services/popular_reads.ts | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/packages/api/src/services/popular_reads.ts b/packages/api/src/services/popular_reads.ts index 5a063521d..499dd019f 100644 --- a/packages/api/src/services/popular_reads.ts +++ b/packages/api/src/services/popular_reads.ts @@ -121,10 +121,7 @@ const addPopularReads = async ( export const addPopularReadsForNewUser = async ( userId: string ): Promise => { - const defaultReads = [ - 'omnivore_organize', - 'power_read_it_later', - ] + const defaultReads = ['omnivore_organize', 'power_read_it_later'] // get client from request context const client = httpContext.get('client') as string | undefined @@ -162,12 +159,13 @@ const popularReads = [ { key: 'omnivore_ios', url: 'https://blog.omnivore.app/p/saving-links-from-your-iphone-or', - title: "Saving Links from Your iPhone or iPad", + title: 'Saving Links from Your iPhone or iPad', author: 'Omnivore', description: 'Learn how to save articles on iOS.', - previewImage: 'https://proxy-prod.omnivore-image-cache.app/260x260,suM2fz_-6_1PDsQDursGPD2bQqnpgGH9Ymj-IVb5dUR4/https://substackcdn.com/image/youtube/w_728,c_limit/k6RkIqepAig', - publishedAt: new Date('2021-10-19'), - siteName: 'Omnivore Blog' + previewImage: + 'https://proxy-prod.omnivore-image-cache.app/260x260,suM2fz_-6_1PDsQDursGPD2bQqnpgGH9Ymj-IVb5dUR4/https://substackcdn.com/image/youtube/w_728,c_limit/k6RkIqepAig', + publishedAt: new Date('2021-10-19'), + siteName: 'Omnivore Blog', }, { key: 'omnivore_organize', From 69d50bcc6ed50826ea4825154efa56efc4e9d650 Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Wed, 19 Oct 2022 17:48:22 +0800 Subject: [PATCH 41/53] Set the proper number of expected reads for iOS now that article is available --- packages/api/test/routers/auth.test.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/api/test/routers/auth.test.ts b/packages/api/test/routers/auth.test.ts index 09ee056ae..ed8560ddd 100644 --- a/packages/api/test/routers/auth.test.ts +++ b/packages/api/test/routers/auth.test.ts @@ -626,8 +626,7 @@ describe('auth router', () => { 0, ] - // TODO: update this when we have more iOS popular reads - expect(count).to.eql(3) + expect(count).to.eql(4) }) }) }) From 3513e5e97599bb192e8e01379b65d1504bd35dd8 Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Wed, 19 Oct 2022 12:44:18 -0700 Subject: [PATCH 42/53] add plus and minus icons for steppers --- .../app/omnivore/omnivore/ui/reader/WebPreferencesDialog.kt | 6 ++++-- android/Omnivore/app/src/main/res/drawable-v24/minus.xml | 1 + android/Omnivore/app/src/main/res/drawable-v24/plus.xml | 1 + 3 files changed, 6 insertions(+), 2 deletions(-) create mode 100644 android/Omnivore/app/src/main/res/drawable-v24/minus.xml create mode 100644 android/Omnivore/app/src/main/res/drawable-v24/plus.xml diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/WebPreferencesDialog.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/WebPreferencesDialog.kt index 550162d53..0b89c1c62 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/WebPreferencesDialog.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/WebPreferencesDialog.kt @@ -14,8 +14,10 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource import androidx.compose.ui.unit.dp import androidx.compose.ui.window.Dialog +import app.omnivore.omnivore.R @Composable fun WebPreferencesDialog(onDismiss: () -> Unit, webReaderViewModel: WebReaderViewModel) { @@ -83,7 +85,7 @@ fun Stepper(label: String, onIncrease: () -> Unit, onDecrease: () -> Unit) { IconButton(onClick = { onDecrease() }) { Icon( - imageVector = Icons.Filled.KeyboardArrowDown, + painter = painterResource(id = R.drawable.minus), contentDescription = null ) } @@ -97,7 +99,7 @@ fun Stepper(label: String, onIncrease: () -> Unit, onDecrease: () -> Unit) { IconButton(onClick = { onIncrease() }) { Icon( - imageVector = Icons.Filled.KeyboardArrowUp, + painter = painterResource(id = R.drawable.plus), contentDescription = null ) } diff --git a/android/Omnivore/app/src/main/res/drawable-v24/minus.xml b/android/Omnivore/app/src/main/res/drawable-v24/minus.xml new file mode 100644 index 000000000..7ac604f6a --- /dev/null +++ b/android/Omnivore/app/src/main/res/drawable-v24/minus.xml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/android/Omnivore/app/src/main/res/drawable-v24/plus.xml b/android/Omnivore/app/src/main/res/drawable-v24/plus.xml new file mode 100644 index 000000000..a476f560f --- /dev/null +++ b/android/Omnivore/app/src/main/res/drawable-v24/plus.xml @@ -0,0 +1 @@ + \ No newline at end of file From 6c0676dbc9a90773522fe202751fd1955198c18b Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Wed, 19 Oct 2022 13:01:55 -0700 Subject: [PATCH 43/53] add switch for toggling high contrast font on android --- .../omnivore/ui/reader/WebPreferencesDialog.kt | 18 ++++++++++++++++++ .../omnivore/ui/reader/WebReaderViewModel.kt | 9 +++++++++ 2 files changed, 27 insertions(+) diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/WebPreferencesDialog.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/WebPreferencesDialog.kt index 0b89c1c62..ee50009ea 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/WebPreferencesDialog.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/WebPreferencesDialog.kt @@ -2,6 +2,7 @@ package app.omnivore.omnivore.ui.reader import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.Switch import androidx.compose.material.Text import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.KeyboardArrowDown @@ -11,6 +12,8 @@ import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.Surface import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color @@ -33,6 +36,9 @@ fun WebPreferencesDialog(onDismiss: () -> Unit, webReaderViewModel: WebReaderVie @Composable fun WebPreferencesView(webReaderViewModel: WebReaderViewModel) { + val currentWebPreferences = webReaderViewModel.storedWebPreferences() + val highContrastTextSwitchState = remember { mutableStateOf(currentWebPreferences.prefersHighContrastText) } + Column( modifier = Modifier .padding(top = 6.dp, start = 6.dp, end = 6.dp, bottom = 6.dp) @@ -68,6 +74,18 @@ fun WebPreferencesView(webReaderViewModel: WebReaderViewModel) { ) // High Contrast Text: Switch + Row(verticalAlignment = Alignment.CenterVertically) { + Text("High Contrast Text") + Spacer(modifier = Modifier.weight(1.0F)) + Switch( + checked = highContrastTextSwitchState.value, + onCheckedChange = { + highContrastTextSwitchState.value = it + webReaderViewModel.updateHighContrastTextPreference(it) + } + ) + } + // Reader Font: List of Fonts } } diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/WebReaderViewModel.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/WebReaderViewModel.kt index 4ce99a090..639b7714e 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/WebReaderViewModel.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/WebReaderViewModel.kt @@ -184,4 +184,13 @@ class WebReaderViewModel @Inject constructor( val script = "var event = new Event('updateLineHeight');event.lineHeight = '$newLineHeight';document.dispatchEvent(event);" enqueueScript(script) } + + fun updateHighContrastTextPreference(prefersHighContrastText: Boolean) { + runBlocking { + datastoreRepo.putString(DatastoreKeys.prefersWebHighContrastText, prefersHighContrastText.toString()) + } + val fontContrastValue = if (prefersHighContrastText) "high" else "normal" + val script = "var event = new Event('handleFontContrastChange');event.fontContrast = '$fontContrastValue';document.dispatchEvent(event);" + enqueueScript(script) + } } From 370649573fbfe2022d4e24d279007de891542a62 Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Wed, 19 Oct 2022 13:32:34 -0700 Subject: [PATCH 44/53] add vertical scroll to web prefs dialog --- .../ui/reader/WebPreferencesDialog.kt | 77 +++++++++++-------- 1 file changed, 44 insertions(+), 33 deletions(-) diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/WebPreferencesDialog.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/WebPreferencesDialog.kt index ee50009ea..4e8c10b7c 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/WebPreferencesDialog.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/WebPreferencesDialog.kt @@ -1,7 +1,10 @@ package app.omnivore.omnivore.ui.reader import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll import androidx.compose.material.Switch import androidx.compose.material.Text import androidx.compose.material.icons.Icons @@ -27,7 +30,9 @@ fun WebPreferencesDialog(onDismiss: () -> Unit, webReaderViewModel: WebReaderVie Dialog(onDismissRequest = { onDismiss() }) { Surface( shape = RoundedCornerShape(16.dp), - color = Color.White + color = Color.White, + modifier = Modifier + .height(250.dp) ) { WebPreferencesView(webReaderViewModel) } @@ -52,41 +57,47 @@ fun WebPreferencesView(webReaderViewModel: WebReaderViewModel) { Text("Web Preferences") } - // Font Size: Stepper - Stepper( - label = "Font Size:", - onIncrease = { webReaderViewModel.updateFontSize(isIncrease = true) }, - onDecrease = { webReaderViewModel.updateFontSize(isIncrease = false) } - ) + Column( + modifier = Modifier + .verticalScroll(rememberScrollState()) + ) { - // Margin: Slider - Stepper( - label = "Margin:", - onIncrease = { webReaderViewModel.updateMaxWidthPercentage(isIncrease = false) }, - onDecrease = { webReaderViewModel.updateMaxWidthPercentage(isIncrease = true) } - ) - - // Line Spacing: Slider - Stepper( - label = "Line Spacing:", - onIncrease = { webReaderViewModel.updateLineSpacing(isIncrease = true) }, - onDecrease = { webReaderViewModel.updateLineSpacing(isIncrease = false) } - ) - - // High Contrast Text: Switch - Row(verticalAlignment = Alignment.CenterVertically) { - Text("High Contrast Text") - Spacer(modifier = Modifier.weight(1.0F)) - Switch( - checked = highContrastTextSwitchState.value, - onCheckedChange = { - highContrastTextSwitchState.value = it - webReaderViewModel.updateHighContrastTextPreference(it) - } + // Font Size: Stepper + Stepper( + label = "Font Size:", + onIncrease = { webReaderViewModel.updateFontSize(isIncrease = true) }, + onDecrease = { webReaderViewModel.updateFontSize(isIncrease = false) } ) - } - // Reader Font: List of Fonts + // Margin: Slider + Stepper( + label = "Margin:", + onIncrease = { webReaderViewModel.updateMaxWidthPercentage(isIncrease = false) }, + onDecrease = { webReaderViewModel.updateMaxWidthPercentage(isIncrease = true) } + ) + + // Line Spacing: Slider + Stepper( + label = "Line Spacing:", + onIncrease = { webReaderViewModel.updateLineSpacing(isIncrease = true) }, + onDecrease = { webReaderViewModel.updateLineSpacing(isIncrease = false) } + ) + + // High Contrast Text: Switch + Row(verticalAlignment = Alignment.CenterVertically) { + Text("High Contrast Text") + Spacer(modifier = Modifier.weight(1.0F)) + Switch( + checked = highContrastTextSwitchState.value, + onCheckedChange = { + highContrastTextSwitchState.value = it + webReaderViewModel.updateHighContrastTextPreference(it) + } + ) + } + + // Reader Font: List of Fonts + } } } From 49df480e2b990996d43fa13620298f43ca2cd10e Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Wed, 19 Oct 2022 16:05:23 -0700 Subject: [PATCH 45/53] display list of font families in web pref dialog --- .../ui/reader/WebPreferencesDialog.kt | 61 +++++++++++++++---- .../omnivore/ui/reader/WebReaderViewModel.kt | 5 ++ 2 files changed, 55 insertions(+), 11 deletions(-) diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/WebPreferencesDialog.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/WebPreferencesDialog.kt index 4e8c10b7c..4f9d01c4a 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/WebPreferencesDialog.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/WebPreferencesDialog.kt @@ -1,5 +1,7 @@ package app.omnivore.omnivore.ui.reader +import android.util.Log +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.rememberScrollState @@ -8,12 +10,11 @@ import androidx.compose.foundation.verticalScroll import androidx.compose.material.Switch import androidx.compose.material.Text import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Check import androidx.compose.material.icons.filled.KeyboardArrowDown +import androidx.compose.material.icons.filled.KeyboardArrowRight import androidx.compose.material.icons.filled.KeyboardArrowUp -import androidx.compose.material3.Divider -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.Surface +import androidx.compose.material3.* import androidx.compose.runtime.Composable import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -32,7 +33,7 @@ fun WebPreferencesDialog(onDismiss: () -> Unit, webReaderViewModel: WebReaderVie shape = RoundedCornerShape(16.dp), color = Color.White, modifier = Modifier - .height(250.dp) + .height(300.dp) ) { WebPreferencesView(webReaderViewModel) } @@ -42,7 +43,9 @@ fun WebPreferencesDialog(onDismiss: () -> Unit, webReaderViewModel: WebReaderVie @Composable fun WebPreferencesView(webReaderViewModel: WebReaderViewModel) { val currentWebPreferences = webReaderViewModel.storedWebPreferences() + val isFontListExpanded = remember { mutableStateOf(false) } val highContrastTextSwitchState = remember { mutableStateOf(currentWebPreferences.prefersHighContrastText) } + val selectedWebFontRawValue = remember { mutableStateOf(currentWebPreferences.fontFamily.rawValue) } Column( modifier = Modifier @@ -61,29 +64,24 @@ fun WebPreferencesView(webReaderViewModel: WebReaderViewModel) { modifier = Modifier .verticalScroll(rememberScrollState()) ) { - - // Font Size: Stepper Stepper( label = "Font Size:", onIncrease = { webReaderViewModel.updateFontSize(isIncrease = true) }, onDecrease = { webReaderViewModel.updateFontSize(isIncrease = false) } ) - // Margin: Slider Stepper( label = "Margin:", onIncrease = { webReaderViewModel.updateMaxWidthPercentage(isIncrease = false) }, onDecrease = { webReaderViewModel.updateMaxWidthPercentage(isIncrease = true) } ) - // Line Spacing: Slider Stepper( label = "Line Spacing:", onIncrease = { webReaderViewModel.updateLineSpacing(isIncrease = true) }, onDecrease = { webReaderViewModel.updateLineSpacing(isIncrease = false) } ) - // High Contrast Text: Switch Row(verticalAlignment = Alignment.CenterVertically) { Text("High Contrast Text") Spacer(modifier = Modifier.weight(1.0F)) @@ -96,7 +94,48 @@ fun WebPreferencesView(webReaderViewModel: WebReaderViewModel) { ) } - // Reader Font: List of Fonts + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .clickable(onClick = { isFontListExpanded.value = !isFontListExpanded.value }) + ) { + Text("Font Family") + Spacer(modifier = Modifier.weight(1.0F)) + Icon( + imageVector = + if (isFontListExpanded.value) + Icons.Filled.KeyboardArrowDown + else + Icons.Filled.KeyboardArrowRight, + contentDescription = null + ) + } + + if (isFontListExpanded.value) { + WebFont.values().forEach { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .clickable(onClick = { + selectedWebFontRawValue.value = it.rawValue + webReaderViewModel.applyWebFont(it) + }) + ) { + Text( + it.displayText, + modifier = Modifier + .padding(top = 6.dp, start = 6.dp, end = 6.dp, bottom = 6.dp) + ) + Spacer(modifier = Modifier.weight(1.0F)) + if (it.rawValue == selectedWebFontRawValue.value) { + Icon( + imageVector = Icons.Filled.Check, + contentDescription = null + ) + } + } + } + } } } } diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/WebReaderViewModel.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/WebReaderViewModel.kt index 639b7714e..355bcb760 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/WebReaderViewModel.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/WebReaderViewModel.kt @@ -193,4 +193,9 @@ class WebReaderViewModel @Inject constructor( val script = "var event = new Event('handleFontContrastChange');event.fontContrast = '$fontContrastValue';document.dispatchEvent(event);" enqueueScript(script) } + + fun applyWebFont(font: WebFont) { + // TODO: update value in datastore and dispatch update to web view + Log.d("Font", "Web Font selected: ${font.displayText}") + } } From e1de55bea0c7e1d000c2adb802ab5e7578f79132 Mon Sep 17 00:00:00 2001 From: Hongbo Wu Date: Thu, 20 Oct 2022 11:16:49 +0800 Subject: [PATCH 46/53] Delete highlights, labels and other metadata when soft delete pages --- packages/api/src/resolvers/article/index.ts | 22 +++++++++++++++++---- packages/api/test/resolvers/article.test.ts | 1 + 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/packages/api/src/resolvers/article/index.ts b/packages/api/src/resolvers/article/index.ts index 5b55a6d19..7671116be 100644 --- a/packages/api/src/resolvers/article/index.ts +++ b/packages/api/src/resolvers/article/index.ts @@ -437,11 +437,19 @@ export const getArticleResolver: ResolverFn< // We allow the backend to use the ID instead of a slug to fetch the article const page = (await getPageByParam( - { userId: claims.uid, slug }, + { + userId: claims.uid, + slug, + state: ArticleSavingRequestStatus.Succeeded, + }, includeOriginalHtml )) || (await getPageByParam( - { userId: claims.uid, _id: slug }, + { + userId: claims.uid, + _id: slug, + state: ArticleSavingRequestStatus.Succeeded, + }, includeOriginalHtml )) @@ -643,10 +651,16 @@ export const setBookmarkArticleResolver = authorized< return { errorCodes: [SetBookmarkArticleErrorCode.NotFound] } } - // delete the page + // delete the page and its metadata const deleted = await updatePage( pageRemoved.id, - { state: ArticleSavingRequestStatus.Deleted }, + { + state: ArticleSavingRequestStatus.Deleted, + labels: [], + highlights: [], + readingProgressAnchorIndex: 0, + readingProgressPercent: 0, + }, { pubsub, uid } ) if (!deleted) { diff --git a/packages/api/test/resolvers/article.test.ts b/packages/api/test/resolvers/article.test.ts index 54fe8dc02..440b8107b 100644 --- a/packages/api/test/resolvers/article.test.ts +++ b/packages/api/test/resolvers/article.test.ts @@ -688,6 +688,7 @@ describe('Article API', () => { await graphqlRequest(query, authToken).expect(200) const page = await getPageById(articleId) expect(page?.state).to.eql(ArticleSavingRequestStatus.Deleted) + expect(page?.highlights).to.eql([]) }) }) }) From a2f31c943f62679b781ac3f70b1b401fa22f9a44 Mon Sep 17 00:00:00 2001 From: Hongbo Wu Date: Thu, 20 Oct 2022 11:31:39 +0800 Subject: [PATCH 47/53] Fix tests --- packages/api/src/resolvers/article/index.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/api/src/resolvers/article/index.ts b/packages/api/src/resolvers/article/index.ts index 7671116be..9d02d9d26 100644 --- a/packages/api/src/resolvers/article/index.ts +++ b/packages/api/src/resolvers/article/index.ts @@ -440,7 +440,6 @@ export const getArticleResolver: ResolverFn< { userId: claims.uid, slug, - state: ArticleSavingRequestStatus.Succeeded, }, includeOriginalHtml )) || @@ -448,12 +447,11 @@ export const getArticleResolver: ResolverFn< { userId: claims.uid, _id: slug, - state: ArticleSavingRequestStatus.Succeeded, }, includeOriginalHtml )) - if (!page) { + if (!page || page.state === ArticleSavingRequestStatus.Deleted) { return { errorCodes: [ArticleErrorCode.NotFound] } } From 37286f8d5eb1fa9690ba8d8b66a290e30f2473e3 Mon Sep 17 00:00:00 2001 From: Hongbo Wu Date: Thu, 20 Oct 2022 12:17:02 +0800 Subject: [PATCH 48/53] Mention highlights be deleted too --- .../templates/homeFeed/HomeFeedContainer.tsx | 24 +++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/packages/web/components/templates/homeFeed/HomeFeedContainer.tsx b/packages/web/components/templates/homeFeed/HomeFeedContainer.tsx index c3be5cc91..73e5d3daa 100644 --- a/packages/web/components/templates/homeFeed/HomeFeedContainer.tsx +++ b/packages/web/components/templates/homeFeed/HomeFeedContainer.tsx @@ -346,8 +346,22 @@ export function HomeFeedContainer(): JSX.Element { } const modalTargetItem = useMemo(() => { - return (labelsTarget || snoozeTarget || shareTarget || linkToEdit || linkToRemove || linkToUnsubscribe) - }, [labelsTarget, snoozeTarget, shareTarget, linkToEdit, linkToRemove, linkToUnsubscribe]) + return ( + labelsTarget || + snoozeTarget || + shareTarget || + linkToEdit || + linkToRemove || + linkToUnsubscribe + ) + }, [ + labelsTarget, + snoozeTarget, + shareTarget, + linkToEdit, + linkToRemove, + linkToUnsubscribe, + ]) useKeyboardShortcuts( libraryListCommands((action) => { @@ -638,7 +652,7 @@ function HomeFeedGrid(props: HomeFeedContentProps): JSX.Element { const [showRemoveLinkConfirmation, setShowRemoveLinkConfirmation] = useState(false) const [showUnsubscribeConfirmation, setShowUnsubscribeConfirmation] = - useState(false) + useState(false) const updateLayout = useCallback( async (newLayout: LayoutType) => { @@ -975,7 +989,9 @@ function HomeFeedGrid(props: HomeFeedContentProps): JSX.Element { )} {showRemoveLinkConfirmation && ( setShowRemoveLinkConfirmation(false)} /> From 81b55cae0c975d285425907ef053987ea43ab211 Mon Sep 17 00:00:00 2001 From: Hongbo Wu Date: Thu, 20 Oct 2022 12:22:57 +0800 Subject: [PATCH 49/53] Update wording --- .../web/components/templates/homeFeed/HomeFeedContainer.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/web/components/templates/homeFeed/HomeFeedContainer.tsx b/packages/web/components/templates/homeFeed/HomeFeedContainer.tsx index 73e5d3daa..2869846d1 100644 --- a/packages/web/components/templates/homeFeed/HomeFeedContainer.tsx +++ b/packages/web/components/templates/homeFeed/HomeFeedContainer.tsx @@ -990,7 +990,7 @@ function HomeFeedGrid(props: HomeFeedContentProps): JSX.Element { {showRemoveLinkConfirmation && ( setShowRemoveLinkConfirmation(false)} From bca82068bd127914ddd751e39f417dea3ea6cc14 Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Thu, 20 Oct 2022 12:26:34 +0800 Subject: [PATCH 50/53] Fix alert/row flicker when removing items on iOS This also updates the message to be consistent with Web. --- .../App/Views/Home/HomeFeedViewIOS.swift | 67 ++++++++++--------- 1 file changed, 34 insertions(+), 33 deletions(-) diff --git a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift index 37f7df362..17662712f 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift @@ -323,8 +323,10 @@ import Views itemToRemove = item confirmationShown = true }, - label: { Label("Delete", systemImage: "trash") } - ) + label: { + Label("Remove Item", systemImage: "trash") + } + ).tint(.red) if FeatureFlag.enableSnooze { Button { viewModel.itemToSnoozeID = item.id @@ -340,26 +342,23 @@ import Views } .swipeActions(edge: .trailing, allowsFullSwipe: true) { if !item.isArchived { - Button { + Button(action: { withAnimation(.linear(duration: 0.4)) { viewModel.setLinkArchived(dataService: dataService, objectID: item.objectID, archived: true) } - } label: { + }, label: { Label("Archive", systemImage: "archivebox") - }.tint(.green) + }).tint(.green) } else { - Button { + Button(action: { withAnimation(.linear(duration: 0.4)) { viewModel.setLinkArchived(dataService: dataService, objectID: item.objectID, archived: false) } - } label: { + }, label: { Label("Unarchive", systemImage: "tray.and.arrow.down.fill") - }.tint(.indigo) + }).tint(.indigo) } - } - .swipeActions(edge: .trailing, allowsFullSwipe: true) { Button( - role: .destructive, action: { itemToRemove = item confirmationShown = true @@ -367,17 +366,7 @@ import Views label: { Image(systemName: "trash") } - ) - }.alert("Are you sure?", isPresented: $confirmationShown) { - Button("Remove Link", role: .destructive) { - if let itemToRemove = itemToRemove { - withAnimation { - viewModel.removeLink(dataService: dataService, objectID: itemToRemove.objectID) - } - } - self.itemToRemove = nil - } - Button("Cancel", role: .cancel) { self.itemToRemove = nil } + ).tint(.red) } .swipeActions(edge: .leading, allowsFullSwipe: true) { if FeatureFlag.enableSnooze { @@ -393,6 +382,18 @@ import Views } .padding(.top, 0) .listStyle(PlainListStyle()) + .alert("Are you sure you want to remove this item? All associated notes and highlights will be deleted.", + isPresented: $confirmationShown) { + Button("Remove Item") { + if let itemToRemove = itemToRemove { + withAnimation { + viewModel.removeLink(dataService: dataService, objectID: itemToRemove.objectID) + } + } + self.itemToRemove = nil + } + Button("Cancel", role: .cancel) { self.itemToRemove = nil } + } } } } @@ -448,17 +449,6 @@ import Views isContextMenuOpen: $isContextMenuOpen, viewModel: viewModel ) - .alert("Are you sure?", isPresented: $confirmationShown) { - Button("Remove Link", role: .destructive) { - if let itemToRemove = itemToRemove { - withAnimation { - viewModel.removeLink(dataService: dataService, objectID: itemToRemove.objectID) - } - } - self.itemToRemove = nil - } - Button("Cancel", role: .cancel) { self.itemToRemove = nil } - } } } .padding() @@ -483,6 +473,17 @@ import Views } } } + .alert("Are you sure you want to remove this item? All associated notes and highlights will be deleted.", isPresented: $confirmationShown) { + Button("Remove Item", role: .destructive) { + if let itemToRemove = itemToRemove { + withAnimation { + viewModel.removeLink(dataService: dataService, objectID: itemToRemove.objectID) + } + } + self.itemToRemove = nil + } + Button("Cancel", role: .cancel) { self.itemToRemove = nil } + } } } From 0ec409cad4ead70ef53ecc893b9a11813177cd0c Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Thu, 20 Oct 2022 13:02:22 -0700 Subject: [PATCH 51/53] save font preference in datastore --- .../omnivore/ui/reader/WebPreferencesDialog.kt | 2 +- .../omnivore/ui/reader/WebReaderContent.kt | 12 +++++------- .../omnivore/ui/reader/WebReaderViewModel.kt | 17 ++++++++++++----- 3 files changed, 18 insertions(+), 13 deletions(-) diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/WebPreferencesDialog.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/WebPreferencesDialog.kt index 4f9d01c4a..c2466dbe0 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/WebPreferencesDialog.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/WebPreferencesDialog.kt @@ -117,8 +117,8 @@ fun WebPreferencesView(webReaderViewModel: WebReaderViewModel) { verticalAlignment = Alignment.CenterVertically, modifier = Modifier .clickable(onClick = { - selectedWebFontRawValue.value = it.rawValue webReaderViewModel.applyWebFont(it) + selectedWebFontRawValue.value = it.rawValue }) ) { Text( diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/WebReaderContent.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/WebReaderContent.kt index 45aee447a..69bfb484f 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/WebReaderContent.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/WebReaderContent.kt @@ -1,6 +1,5 @@ package app.omnivore.omnivore.ui.reader -import android.util.Log import app.omnivore.omnivore.models.Highlight import app.omnivore.omnivore.models.LinkedItem import com.google.gson.Gson @@ -15,7 +14,6 @@ enum class WebFont(val displayText: String, val rawValue: String) { ROBOTO("Roboto", "Roboto"), CRIMSON_TEXT("Crimson Text", "Crimson Text"), SOURCE_SERIF_PRO("Source Serif Pro", "Source Serif Pro"), - Inter("Inter", "Inter"), } enum class ArticleContentStatus(val rawValue: String) { @@ -48,17 +46,19 @@ data class WebReaderContent( // TODO: Kotlinize these three values (pasted from Swift) val savedAt = "new Date(1662571290735.0).toISOString()" val createdAt = "new Date().toISOString()" - val publishedAt = "new Date().toISOString()" //if (item.publishDate != null) "new Date((item.publishDate!.timeIntervalSince1970 * 1000)).toISOString()" else "undefined" + val publishedAt = + "new Date().toISOString()" //if (item.publishDate != null) "new Date((item.publishDate!.timeIntervalSince1970 * 1000)).toISOString()" else "undefined" val textFontSize = preferences.textFontSize + val highlightCssFilePath = "highlight${if (themeKey == "Gray") "-dark" else ""}.css" - val content = """ + return """ @@ -106,7 +106,5 @@ data class WebReaderContent( """ - - return content } } diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/WebReaderViewModel.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/WebReaderViewModel.kt index 355bcb760..fa816a96f 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/WebReaderViewModel.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/WebReaderViewModel.kt @@ -127,15 +127,18 @@ class WebReaderViewModel @Inject constructor( val storedFontSize = datastoreRepo.getInt(DatastoreKeys.preferredWebFontSize) val storedLineHeight = datastoreRepo.getInt(DatastoreKeys.preferredWebLineHeight) val storedMaxWidth = datastoreRepo.getInt(DatastoreKeys.preferredWebMaxWidthPercentage) - val storedFontFamily = datastoreRepo.getString(DatastoreKeys.preferredWebFontFamily) + + val storedFontFamily = datastoreRepo.getString(DatastoreKeys.preferredWebFontFamily) ?: WebFont.SYSTEM.rawValue + val storedWebFont = WebFont.values().first { it.rawValue == storedFontFamily } + val prefersHighContrastFont = datastoreRepo.getString(DatastoreKeys.prefersWebHighContrastText) == "true" WebPreferences( textFontSize = storedFontSize ?: 12, lineHeight = storedLineHeight ?: 150, maxWidthPercentage = storedMaxWidth ?: 100, - themeKey = "LightGray", - fontFamily = WebFont.SYSTEM, + themeKey = "LightGray", // TODO: match system value + fontFamily = storedWebFont, prefersHighContrastText = prefersHighContrastFont ) } @@ -195,7 +198,11 @@ class WebReaderViewModel @Inject constructor( } fun applyWebFont(font: WebFont) { - // TODO: update value in datastore and dispatch update to web view - Log.d("Font", "Web Font selected: ${font.displayText}") + runBlocking { + datastoreRepo.putString(DatastoreKeys.preferredWebFontFamily, font.rawValue) + } + + val script = "var event = new Event('updateFontFamily');event.fontFamily = '${font.rawValue}';document.dispatchEvent(event);" + enqueueScript(script) } } From 18819b205a0a270d928719cacc0f5f3a8ce41efb Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Thu, 20 Oct 2022 13:06:16 -0700 Subject: [PATCH 52/53] update android build number to 9 --- android/Omnivore/app/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/android/Omnivore/app/build.gradle b/android/Omnivore/app/build.gradle index 4bfbabe93..ef847fd4d 100644 --- a/android/Omnivore/app/build.gradle +++ b/android/Omnivore/app/build.gradle @@ -17,8 +17,8 @@ android { applicationId "app.omnivore.omnivore" minSdk 23 targetSdk 32 - versionCode 8 - versionName "0.0.8" + versionCode 9 + versionName "0.0.9" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" vectorDrawables { From 31bc9fe5bec94eeaf91981668a8a55345c29ad22 Mon Sep 17 00:00:00 2001 From: Hongbo Wu Date: Fri, 21 Oct 2022 09:57:05 +0800 Subject: [PATCH 53/53] Fix node-weekly newsletter getting forwarded * This error is caused by a previous rebase conflict and resolving the conflict caused an important line missing * Revert the change to add CooperPressHandler to the array of newsletter handlers * Updated the test to get the correct handler from email content to prevent such cases --- packages/content-handler/src/index.ts | 26 +- .../test/data/node-weekly-newsletter.html | 230 ++++++++++-------- .../content-handler/test/newsletter.test.ts | 163 ++++++------- 3 files changed, 233 insertions(+), 186 deletions(-) diff --git a/packages/content-handler/src/index.ts b/packages/content-handler/src/index.ts index 8e32f1cb3..052642a50 100644 --- a/packages/content-handler/src/index.ts +++ b/packages/content-handler/src/index.ts @@ -26,6 +26,7 @@ import { ConvertkitHandler } from './newsletters/convertkit-handler' import { RevueHandler } from './newsletters/revue-handler' import { GhostHandler } from './newsletters/ghost-handler' import { parseHTML } from 'linkedom' +import { CooperPressHandler } from './newsletters/cooper-press-handler' const validateUrlString = (url: string) => { const u = new URL(url) @@ -73,6 +74,7 @@ const newsletterHandlers: ContentHandler[] = [ new ConvertkitHandler(), new RevueHandler(), new GhostHandler(), + new CooperPressHandler(), ] export const preHandleContent = async ( @@ -122,21 +124,37 @@ export const preParseContent = async ( return undefined } -export const handleNewsletter = async ( - input: NewsletterInput -): Promise => { +export const getNewsletterHandler = async (input: { + postHeader: string + from: string + unSubHeader: string + html: string +}): Promise => { const dom = parseHTML(input.html).document for (const handler of newsletterHandlers) { if (await handler.isNewsletter({ ...input, dom })) { - return handler.handleNewsletter(input) + return handler } } return undefined } +export const handleNewsletter = async ( + input: NewsletterInput +): Promise => { + const handler = await getNewsletterHandler(input) + if (handler) { + console.log('handleNewsletter', handler.name, input.title) + return handler.handleNewsletter(input) + } + + return undefined +} + module.exports = { preHandleContent, handleNewsletter, preParseContent, + getNewsletterHandler, } diff --git a/packages/content-handler/test/data/node-weekly-newsletter.html b/packages/content-handler/test/data/node-weekly-newsletter.html index 01173902f..397faa2e1 100644 --- a/packages/content-handler/test/data/node-weekly-newsletter.html +++ b/packages/content-handler/test/data/node-weekly-newsletter.html @@ -45,15 +45,15 @@ -
Plus Node 16.18.0, an IP info database, turning cron expressions into English, and 2FA with Twilio. |
+
Plus choosing the best Node Docker image to use, and a way to embed Node and V8 into JVM apps. |
- - + +

#​458 — October 13, 2022

Read on the Web

#​459 — October 20, 2022

Read on the Web

@@ -71,135 +71,175 @@
@@ -61,8 +61,8 @@
Together with  - - Userfront + + Memetria
Node.js Weekly
- + +
+ +

Node.js 19 Released

+
+

As an odd-numbered release, Node 19 will never become an 'active LTS' version, but sits as the 'current' release that gets all the tastiest new features until early 2023. It then reaches 'end of life' on June 1, 2023. "If you’re interested in getting access to features early, Node.js 19 is ready,” says Rafael Gonzaga of the core team.

+

New features this time around include:

+
    +
  • +

    Watch mode. An experimental --watch Nodemon-esque mode for 'watching' files and restarting the process when imported files change. (Node 18.11.0 (LTS) also gains this feature.)

    +
  • +
  • +

    HTTP KeepAlive is now enabled by default. It's always been an option but now it's set to true by default. The default duration is 5 seconds.

    +
  • +
  • +

    V8 10.7. Node bumps up to the latest version of the V8 engine. It's not a big jump but does introduce Intl.NumberFormat.

    +
  • +
  • +

    The WebCrypto API is now stable (with the exception of Ed25519, Ed448, X25519, and X448).

    +
  • +
  • +

    Some other dependency upgrades, such as to npm 8.19.2 and llhttp 8.1.0.

    +
  • +
+

As things stand, we're in the odd position of Node 18.x and 19.x both being the 'Current' release, but Node 18 begins its role as an LTS release on October 25. More info in the release policies here and the OpenJS Foundation has extra detail in its release post too.

+
+ +

The Node.js Team

+ +
+ +

Memetria: Secure, Scalable, Full-Featured Redis 7 Hosting — The latest Redis features, instrumented and scaled with the tools teams need as they grow.

+

Memetria sponsor

-

njt: Quick Navigation to npm Package Resources — Provides a rapid way to jump to various destinations related to npm packages (such as a project’s homepage, repo, issues, or even a package cost estimation). You can install it for use in your terminal, as a Chrome or Firefox search, via VS Code’s command palette (via LaunchX) or you can even use it directly on the Web here. – GitHub repo.

-

Alexander Kachkaev

+

Choosing the Best Node.js Docker Image — If you feel tempted to just throw FROM node into your Dockerfile, think again – there are other options to consider.

+

Liran Tal (Snyk)

+
+ +
+ +

▶  Effortless End-to-End Type-Safety with Phero — A demonstration of a library providing a type-safe TypeScript-based way to communicate between frontend and backend. GitHub repo.

+

Jasper Haggenburg

+
+
+

IN BRIEF:

+
-

Knip: Find Unused Files, Dependencies and Exports in TypeScript Projects — Knip’s creator tells us it’s Dutch for “cut” which is quite appropriate as it’s a new tool for trimming away things that aren’t being used in your project. If you just want to compare it to similar existing tools, there’s a handy comparison chart.

-

Lars Kappert

+

PowerShell, NPM Scripts, and Silently Dropped Arguments — If you’re a Powershell user and you’re finding that some arguments aren’t being passed to your Node scripts run through npm run, Lloyd explains what’s going on.

+

Lloyd Atkinson

-
- -

Node Authentication, Simplified — In this article, we lay out a new approach to authentication (plus access control & SSO) in Node.js applications.

-

Userfront sponsor

+
+ +

▶  A Next.js Crash Course — There are a lot of such videos, but this is a well recorded and up to date one so it might help you get the lay of the Next.js land if you’re just starting out with it. (2 hours 30 minutes)

+

Anson Foong

-

Node v16.18.0 (LTS) Released — Largely backported fixes and tweaks – no big headlines here.

-

Juan José (Node Core Team)

+

Web Scraping Google Maps with Puppeteer — We’d be surprised if you’d get away with this for long given there’s an official API, but it’s always interesting to see how it’s done.

+

Darshan Khandelwal

-

How to Write CommonJS Exports That Can Be Name-Imported from ESM — If you’ve ever got tangled up between using CommonJS and ES modules (I sure have!) Dr. Axel clears up a key cross-compatibility concern here.

-

Dr. Axel Rauschmayer

+

Your Step by Step Guide to Containerizing Node.js Web Applications

+

Snyk sponsor

-
- -

Adding Observability to Jest Tests — A look at how to get a bit more out of your Jest-based testing by keeping an eye on things.

-

Eliran Maman (Sprkl)

+
+

Sending UDP Messages without DNS Lookups +
Herman J. Radtke III +

-
- -

🔐  Node.js Authentication with Twilio Verify — If you’re happy using a third party service, bringing two-factor auth into your Express.js app needn’t be too hard. The author demonstrates the creation of a simple app that authenticates users using password-based authentication with an extra layer of OTPs (One-Time Passcodes) powered by Twilio’s Verify service.

-

Alexander Godwin

+
+

How Wix Uses Threading in Node Apps to Cut Kubernetes Pod Costs +
Jessica Wachtel (The New Stack) +

🛠 Code & Tools

-
- +
+ +
+
+ +
+ +

Javet 2.0.0: Embed Node and V8 in Java Apps — Lets you spin up V8 interpreters or full Node.js runtimes within JVM-based apps. There’s a slide presentation to sell you on the idea and demonstrate how the integration works. (The name Javet comes from Java, V, and Eight.)

+

Sam Cao

-

IP Index: A Fast IP Lookup Web Service + Library — Returns blacklist status, detects VPN/hosting and shows geo and ASN info. The repo gets updated every day too.

-

Mykhailo Gorianskyi

+

Editly 0.14.0: Declarative Command Line Video Editing — Brings Node and FFmpeg together to let you more programatically edit and construct videos instead of wrangling with arcane ffmpeg command line options.

+

Mikael Finstad

-

cRonstrue: Library to Convert cron Expressions into Human Readable Form — Love the project name! The idea is given something like */10 * * * *, it will return “Every 10 minutes”. No dependencies.

-

Brady Holt

-
- -
- -

Dynaboard: The Pro-Code Web App Builder Made for Developers — Build high performance public and private web apps in a collaborative — code forward — WYSIWYG environment.

-

Dynaboard sponsor

-
- -
- -

Whoiser: A WHOIS Client for Node.js — Given a domain name, TLD, or IP address, it queries online WHOIS databases for info.

-

Andrei Igna

-
- -
- -

Print Ready: A JS-Powered CLI for Converting HTML Into PDFs — Uses Paged.js to render your HTML file inside Puppeteer, then exports a PDF from Puppeteer.

-

Nicholas C. Zakas

-
- -
- -

Check HTML Links: A Fast Checker for Broken Links/References in HTML — An npm package you can run on static pages to find broken links in href, src, and srcset, and can process 500-1000 documents in seconds.

-

Modern Web

-
- -
- -

human-signals: Human-Friendly Process Signal Info — Basically a JavaScript object that contains info about the various POSIX signals (SIGHUP, SIGINT, et al.)

-

ehmicky

-
- -
- -

Need to Upgrade Your Node.js App? Hire Us to Do It for You

-

UpgradeJS․com - The JS Upgrade Service by OmbuLabs sponsor

+

Send Email, Push and SMS with Smart Routing, with Just 8 Lines of Code — Are you stuck using marketing tools like salesforce to contact your users? Send notifications from right within your application using the Courier API.

+

Courier.com sponsor

-

Flyweight: A Brand New ORM for SQLite — Early days but provides some extra abstraction around SQLite you might appreciate. -
Andrew Jones +

lady-gg: Simple TypeScript gRPC Client +
Mish Ushakov

-
+
    -
  • -

    AdminJS 6.4
    - ↳ Admin panel / UI for Node apps.

    +
  • +

    Awilix 8.0
    + ↳ Inversion of Control (IoC) container for Node.

  • -
  • -

    Faker 7.6
    - ↳ Generate large amounts of fake data.

    +
  • +

    Nx 15.0
    + ↳ Smart, fast and extensible build system.

  • -
  • -

    Middy 3.6
    - ↳ Node middleware engine for AWS Lambda.

    +
  • +

    Nightwatch 2.4
    + ↳ End-to-end testing framework, now with improved component testing support.

  • -
  • -

    quagga2 1.7.5
    - ↳ Advanced barcode scanning for browser and Node.

    +
  • +

    lowdb 4.0
    + ↳ Simple to use local JSON database.

  • -
  • -

    node-jira-client 8.2
    - ↳ Node wrapper for Jira's REST API.

    +
  • +

    Prisma 4.5
    + ↳ Next-generation ORM. There's a lot new here.

  • -
  • -

    RedisSMQ 7.1.1
    - ↳ High-performance Redis message queue.

    +
  • +

    PSD 0.3
    + ↳ Zero-dependency PSD/Photoshop file parser.

    +
  • +
  • +

    fdir 5.3
    + ↳ Performance-oriented directory crawler and globbing library.

    +
  • +
  • +

    google-translate 2.0
    + ↳ Consume Google's Translate API.

    +
  • +
  • +

    Mercurius 11.1
    + ↳ Implement GraphQL servers with Fastify.

    +
  • +
  • +

    Mongoist 2.5.6
    + ↳ MongoDB driver built with async/await in mind.

    +
  • +
  • +

    Mojo.js 1.7
    + ↳ Web framework inspired by Perl's Mojolicious.

@@ -208,13 +248,13 @@

💻 Jobs

-

Full-Stack Engineer (NYC / Remote) — 100M+ devices, 100B+ API calls. Radar is looking for Product Engineers to build geospatial dev tools. -
Radar +

Doppler - A SecretOps Platform Built by Developers for Developers — Doppler’s looking for Sr. Full-Stack Engineers to help shape the future of security devtools. TypeScript, React, Express, and Go, apply here. +
Doppler

-

Find Tech Jobs with Hired — Create a profile on Hired to connect with hiring managers at growing startups and Fortune 500 companies. It's free for job-seekers. +

Find Tech Jobs with Hired — Create a profile on Hired to connect with hiring managers at growing startups and Fortune 500 companies. It's free for job-seekers.
Hired

@@ -233,9 +273,7 @@

Published by Cooper Press Ltd.
Fairfield Enterprise Centre, Louth, LN11 0LS, United Kingdom

-

Cancel your subscription or change your address.

- - +

Cancel your subscription or change your address.

@@ -246,5 +284,5 @@ -n +n diff --git a/packages/content-handler/test/newsletter.test.ts b/packages/content-handler/test/newsletter.test.ts index 11777c420..d8eb84d00 100644 --- a/packages/content-handler/test/newsletter.test.ts +++ b/packages/content-handler/test/newsletter.test.ts @@ -13,9 +13,9 @@ import { generateUniqueUrl } from '../src/content-handler' import fs from 'fs' import { BeehiivHandler } from '../src/newsletters/beehiiv-handler' import { ConvertkitHandler } from '../src/newsletters/convertkit-handler' -import { parseHTML } from 'linkedom' import { GhostHandler } from '../src/newsletters/ghost-handler' import { CooperPressHandler } from '../src/newsletters/cooper-press-handler' +import { getNewsletterHandler } from '../src' chai.use(chaiAsPromised) chai.use(chaiString) @@ -93,104 +93,95 @@ describe('Newsletter email test', () => { }) }) - describe('isProbablyNewsletter', () => { - it('returns true for substack newsletter', async () => { + describe('getNewsletterHandler', () => { + it('returns substack newsletter handler', async () => { const html = load('./test/data/substack-forwarded-newsletter.html') - const dom = parseHTML(html).document - await expect( - new SubstackHandler().isNewsletter({ - dom, - postHeader: '', - from: '', - unSubHeader: '', - }) - ).to.eventually.be.true + const handler = await getNewsletterHandler({ + html, + postHeader: '', + from: '', + unSubHeader: '', + }) + expect(handler).to.be.instanceOf(SubstackHandler) }) - it('returns true for private forwarded substack newsletter', async () => { + + it('returns SubstackHandler for private forwarded substack newsletter', async () => { const html = load( './test/data/substack-private-forwarded-newsletter.html' ) - const dom = parseHTML(html).document - await expect( - new SubstackHandler().isNewsletter({ - dom, - postHeader: '', - from: '', - unSubHeader: '', - }) - ).to.eventually.be.true + const handler = await getNewsletterHandler({ + html, + postHeader: '', + from: '', + unSubHeader: '', + }) + expect(handler).to.be.instanceOf(SubstackHandler) }) - it('returns false for substack welcome email', async () => { + + it('returns undefined for substack welcome email', async () => { const html = load('./test/data/substack-forwarded-welcome-email.html') - const dom = parseHTML(html).document - await expect( - new SubstackHandler().isNewsletter({ - dom, - postHeader: '', - from: '', - unSubHeader: '', - }) - ).to.eventually.be.false + const handler = await getNewsletterHandler({ + html, + postHeader: '', + from: '', + unSubHeader: '', + }) + expect(handler).to.be.undefined }) - it('returns true for beehiiv.com newsletter', async () => { + + it('returns BeehiivHandler for beehiiv.com newsletter', async () => { const html = load('./test/data/beehiiv-newsletter.html') - const dom = parseHTML(html).document - await expect( - new BeehiivHandler().isNewsletter({ - dom, - postHeader: '', - from: '', - unSubHeader: '', - }) - ).to.eventually.be.true + const handler = await getNewsletterHandler({ + html, + postHeader: '', + from: '', + unSubHeader: '', + }) + expect(handler).to.be.instanceOf(BeehiivHandler) }) - it('returns true for milkroad newsletter', async () => { + + it('returns BeehiivHandler for milkroad newsletter', async () => { const html = load('./test/data/milkroad-newsletter.html') - const dom = parseHTML(html).document - await expect( - new BeehiivHandler().isNewsletter({ - dom, - postHeader: '', - from: '', - unSubHeader: '', - }) - ).to.eventually.be.true + const handler = await getNewsletterHandler({ + html, + postHeader: '', + from: '', + unSubHeader: '', + }) + expect(handler).to.be.instanceOf(BeehiivHandler) }) - it('returns true for ghost newsletter', async () => { + + it('returns GhostHandler for ghost newsletter', async () => { const html = load('./test/data/ghost-newsletter.html') - const dom = parseHTML(html).document - await expect( - new GhostHandler().isNewsletter({ - dom, - postHeader: '', - from: '', - unSubHeader: '', - }) - ).to.eventually.be.true + const handler = await getNewsletterHandler({ + html, + postHeader: '', + from: '', + unSubHeader: '', + }) + expect(handler).to.be.instanceOf(GhostHandler) }) - it('returns true for convertkit newsletter', async () => { + + it('returns ConvertkitHandler for convertkit newsletter', async () => { const html = load('./test/data/convertkit-newsletter.html') - const dom = parseHTML(html).document - await expect( - new ConvertkitHandler().isNewsletter({ - dom, - postHeader: '', - from: '', - unSubHeader: '', - }) - ).to.eventually.be.true + const handler = await getNewsletterHandler({ + html, + postHeader: '', + from: '', + unSubHeader: '', + }) + expect(handler).to.be.instanceOf(ConvertkitHandler) }) - it('returns true for node-weekly newsletter', async () => { + + it('returns CooperPressHandler for node-weekly newsletter', async () => { const html = load('./test/data/node-weekly-newsletter.html') - const dom = parseHTML(html).document - await expect( - new CooperPressHandler().isNewsletter({ - dom, - postHeader: '', - from: '', - unSubHeader: '', - }) - ).to.eventually.be.true + const handler = await getNewsletterHandler({ + html, + postHeader: '', + from: '', + unSubHeader: '', + }) + expect(handler).to.be.instanceOf(CooperPressHandler) }) }) @@ -293,18 +284,18 @@ describe('Newsletter email test', () => { before(() => { nock('https://u25184427.ct.sendgrid.net') .head( - '/ls/click?upn=MnmHBiCwIPe9TmIJeskmA7mFdqmsIs-2B5Xs-2FNpSIs56o0z9xhskaXR4aYohHPLtwRHfml_vVXscVLXlj5UtQe3aqo5RMTdTq2PepdZjP86UOmA8nxtQVfuqJiLh7Fio3fEtt5ouN4IH56AfszUQpxY-2FQ233kp0bjSZhBBVWAB43dgKumQkDW-2BxDFnQIUpvhmEgzSJq-2FMRG00GM7fkZVuPU-2BX8cdg8AGRHUU9Qhw6W67XEMkJVygTdm70Mo9ypNi8N33hgmhM3F6un9s7p1K1Gq-2FunslA-3D-3D' + '/ls/click?upn=MnmHBiCwIPe9TmIJeskmA7mFdqmsIs-2B5Xs-2FNpSIs56obSPDXnoBEjufvIqRCEJUf5Uqg_vVXscVLXlj5UtQe3aqo5RMTdTq2PepdZjP86UOmA8nyFE700vlj1-2FK27spZiPEiEkDU3SIXWGeoiU60KFhM-2B-2Bxx5yiL8KKbAV6oFceRi8O1gMc3mdwg5D8FaaM3PublaX24iAcVbn99PzxJaPuVrU6xDWbRovw2UgGTIoEI-2BBO-2B0qzi2wv5c6yJTkUGZOcsJ6xGLXO1BO-2BHSbyZMZV4NMw-3D-3D' ) .reply(301, undefined, { - Location: 'https://nodeweekly.com/issues/458', + Location: 'https://nodeweekly.com/issues/459', }) - nock('https://nodeweekly.com').head('/issues/458').reply(200, '') + nock('https://nodeweekly.com').head('/issues/459').reply(200, '') }) it('gets the URL from the header', async () => { const html = load('./test/data/node-weekly-newsletter.html') const url = await new CooperPressHandler().findNewsletterUrl(html) - expect(url).to.startWith('https://nodeweekly.com/issues/458') + expect(url).to.startWith('https://nodeweekly.com/issues/459') }) }) })