Merge branch 'main' of github.com:omnivore-app/omnivore into feat/integrations-page

This commit is contained in:
Rupin Khandelwal 2022-10-21 17:02:55 -05:00
commit a496e0ac58
64 changed files with 9824 additions and 1356 deletions

View file

@ -17,8 +17,8 @@ android {
applicationId "app.omnivore.omnivore"
minSdk 23
targetSdk 32
versionCode 7
versionName "0.0.7"
versionCode 9
versionName "0.0.9"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables {

View file

@ -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 {

View file

@ -0,0 +1,184 @@
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
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
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.*
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
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) {
Dialog(onDismissRequest = { onDismiss() }) {
Surface(
shape = RoundedCornerShape(16.dp),
color = Color.White,
modifier = Modifier
.height(300.dp)
) {
WebPreferencesView(webReaderViewModel)
}
}
}
@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
.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")
}
Column(
modifier = Modifier
.verticalScroll(rememberScrollState())
) {
Stepper(
label = "Font Size:",
onIncrease = { webReaderViewModel.updateFontSize(isIncrease = true) },
onDecrease = { webReaderViewModel.updateFontSize(isIncrease = false) }
)
Stepper(
label = "Margin:",
onIncrease = { webReaderViewModel.updateMaxWidthPercentage(isIncrease = false) },
onDecrease = { webReaderViewModel.updateMaxWidthPercentage(isIncrease = true) }
)
Stepper(
label = "Line Spacing:",
onIncrease = { webReaderViewModel.updateLineSpacing(isIncrease = true) },
onDecrease = { webReaderViewModel.updateLineSpacing(isIncrease = false) }
)
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)
}
)
}
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 = {
webReaderViewModel.applyWebFont(it)
selectedWebFontRawValue.value = it.rawValue
})
) {
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
)
}
}
}
}
}
}
}
@Composable
fun Stepper(label: String, onIncrease: () -> Unit, onDecrease: () -> Unit) {
Row(verticalAlignment = Alignment.CenterVertically) {
Text(
text = label,
modifier = Modifier
.padding(bottom = 6.dp)
)
Spacer(modifier = Modifier.weight(1.0F))
IconButton(onClick = { onDecrease() }) {
Icon(
painter = painterResource(id = R.drawable.minus),
contentDescription = null
)
}
Divider(
color = Color.Black,
modifier = Modifier
.height(20.dp)
.width(1.dp)
)
IconButton(onClick = { onIncrease() }) {
Icon(
painter = painterResource(id = R.drawable.plus),
contentDescription = null
)
}
}
}
data class WebPreferences(
val textFontSize: Int,
val lineHeight: Int,
val maxWidthPercentage: Int,
val themeKey: String,
val fontFamily: WebFont,
val prefersHighContrastText: Boolean
)

View file

@ -8,28 +8,115 @@ 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.runtime.Composable
import androidx.compose.runtime.getValue
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.*
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.dp
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
@Composable
fun WebReaderLoadingContainer(slug: String, webReaderViewModel: WebReaderViewModel) {
var showWebPreferencesDialog by remember { mutableStateOf(false ) }
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() }
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 newHeight = toolbarHeightPx.value + delta
toolbarHeightPx.value = newHeight.coerceIn(0f, maxToolbarHeightPx)
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 = maxToolbarHeight)
) {
}
WebReader(webReaderParams!!, webReaderViewModel.storedWebPreferences(), webReaderViewModel)
}
TopAppBar(
modifier = Modifier
.height(height = with(LocalDensity.current) {
toolbarHeightPx.value.roundToInt().toDp()
} ),
backgroundColor = MaterialTheme.colorScheme.surfaceVariant,
title = {},
actions = {
IconButton(onClick = { showWebPreferencesDialog = true }) {
Icon(
imageVector = Icons.Filled.Settings,
contentDescription = null
)
}
}
)
if (showWebPreferencesDialog) {
WebPreferencesDialog(
onDismiss = {
showWebPreferencesDialog = false
},
webReaderViewModel = webReaderViewModel
)
}
if (annotation != null) {
AnnotationEditView(
initialAnnotation = annotation!!,
onSave = {
webReaderViewModel.saveAnnotation(it)
},
onCancel = {
webReaderViewModel.cancelAnnotationEdit()
}
)
}
}
} else {
// TODO: add a proper loading view
Text("Loading...")
@ -38,23 +125,22 @@ fun WebReaderLoadingContainer(slug: String, webReaderViewModel: WebReaderViewMod
@SuppressLint("SetJavaScriptEnabled")
@Composable
fun WebReader(params: WebReaderParams, webReaderViewModel: WebReaderViewModel) {
// TODO: maybe handle cases where js can be queued up?
val javascriptToExecute = remember { mutableStateOf<String?>(null) }
val annotation: String? by webReaderViewModel.annotationLiveData.observeAsState(null)
fun WebReader(
params: WebReaderParams,
preferences: WebPreferences,
webReaderViewModel: WebReaderViewModel
) {
val javascriptActionLoopUUID: UUID by webReaderViewModel
.javascriptActionLoopUUIDLiveData
.observeAsState(UUID.randomUUID())
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()
@ -100,24 +186,14 @@ fun WebReader(params: WebReaderParams, webReaderViewModel: WebReaderViewModel) {
)
}
}, update = {
if (javascriptToExecute.value != null) {
it.evaluateJavascript(javascriptToExecute.value!!, null)
if (javascriptActionLoopUUID != webReaderViewModel.lastJavascriptActionLoopUUID) {
for (script in webReaderViewModel.javascriptDispatchQueue) {
Log.d("js", "executing script: $script")
it.evaluateJavascript(script, null)
}
webReaderViewModel.resetJavascriptDispatchQueue()
}
})
if (annotation != null) {
AnnotationEditView(
initialAnnotation = annotation!!,
onSave = {
val script = "var event = new Event('saveAnnotation');event.annotation = '$it';document.dispatchEvent(event);"
javascriptToExecute.value = script
webReaderViewModel.cancelAnnotationEdit()
},
onCancel = {
webReaderViewModel.cancelAnnotationEdit()
}
)
}
}
}

View file

@ -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) {
@ -39,29 +37,28 @@ 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 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 """
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name='viewport' content='width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, user-scalable=no' />
<style>
@import url("highlight${if (themeKey == "Gray") "-dark" else ""}.css");
@import url("highlightCssFilePath");
</style>
</head>
<body>
@ -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
</script>
<script src="bundle.js"></script>
@ -109,7 +106,5 @@ data class WebReaderContent(
</body>
</html>
"""
return content
}
}

View file

@ -1,16 +1,20 @@
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
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
data class WebReaderParams(
@ -27,8 +31,13 @@ class WebReaderViewModel @Inject constructor(
private val datastoreRepo: DatastoreRepository,
private val networker: Networker
): ViewModel() {
var lastJavascriptActionLoopUUID: UUID = UUID.randomUUID()
var javascriptDispatchQueue: MutableList<String> = mutableListOf()
var scrollState = ScrollState(0)
val webReaderParamsLiveData = MutableLiveData<WebReaderParams?>(null)
val annotationLiveData = MutableLiveData<String?>(null)
val javascriptActionLoopUUIDLiveData = MutableLiveData(lastJavascriptActionLoopUUID)
fun loadItem(slug: String) {
viewModelScope.launch {
@ -90,9 +99,110 @@ class WebReaderViewModel @Inject constructor(
fun reset() {
webReaderParamsLiveData.value = null
annotationLiveData.value = null
scrollState = ScrollState(0)
javascriptDispatchQueue = mutableListOf()
}
fun resetJavascriptDispatchQueue() {
lastJavascriptActionLoopUUID = javascriptActionLoopUUIDLiveData.value ?: UUID.randomUUID()
javascriptDispatchQueue = mutableListOf()
}
fun saveAnnotation(annotation: String) {
val script = "var event = new Event('saveAnnotation');event.annotation = '$annotation';document.dispatchEvent(event);"
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) ?: 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", // TODO: match system value
fontFamily = storedWebFont,
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)
}
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)
}
fun applyWebFont(font: WebFont) {
runBlocking {
datastoreRepo.putString(DatastoreKeys.preferredWebFontFamily, font.rawValue)
}
val script = "var event = new Event('updateFontFamily');event.fontFamily = '${font.rawValue}';document.dispatchEvent(event);"
enqueueScript(script)
}
}

View file

@ -0,0 +1 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:width="24dp" android:viewportWidth="24" android:viewportHeight="24"><path android:fillColor="#000" android:pathData="M19,13H5V11H19V13Z"/></vector>

View file

@ -0,0 +1 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:width="24dp" android:viewportWidth="24" android:viewportHeight="24"><path android:fillColor="#000" android:pathData="M19,13H13V19H11V13H5V11H11V5H13V11H19V13Z"/></vector>

View file

@ -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 = "";

View file

@ -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",

View file

@ -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
}

View file

@ -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)
}

View file

@ -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 {

View file

@ -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 },
@ -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 }
}
}
}

View file

@ -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)
}
}
}

View file

@ -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 },

View file

@ -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
}
}

View file

@ -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()
)
}

View file

@ -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) },

View file

@ -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",

View file

@ -54,6 +54,7 @@ import { ContentParseError } from '../../utils/errors'
import {
authorized,
generateSlug,
isBase64Image,
isParsingTimeout,
pageError,
stringToHash,
@ -436,15 +437,21 @@ 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,
},
includeOriginalHtml
)) ||
(await getPageByParam(
{ userId: claims.uid, _id: slug },
{
userId: claims.uid,
_id: slug,
},
includeOriginalHtml
))
if (!page) {
if (!page || page.state === ArticleSavingRequestStatus.Deleted) {
return { errorCodes: [ArticleErrorCode.NotFound] }
}
@ -642,10 +649,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) {
@ -889,6 +902,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 +917,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,
}

View file

@ -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) {

View file

@ -72,6 +72,7 @@ export async function createMobileEmailSignUpResponse(
json: {},
}
} catch (e) {
console.log('error', e)
return signUpFailedPayload
}
}

View file

@ -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
}
}
@ -116,11 +121,7 @@ const addPopularReads = async (
export const addPopularReadsForNewUser = async (
userId: string
): Promise<void> => {
const defaultReads = [
'omnivore_get_started',
'power_read_it_later',
'omnivore_organize',
]
const defaultReads = ['omnivore_organize', 'power_read_it_later']
// get client from request context
const client = httpContext.get('client') as string | undefined
@ -137,6 +138,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)
}
@ -152,6 +156,17 @@ 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',

View file

@ -1,103 +1,330 @@
<DIV class="page" id="readability-page-1">
<div dir="auto">
<p> Omnivore is a home for everything you read. We keep it safe, organized, and easy to share. </p>
<p> When you start using Omnivore, it is important to figure out the best way to save content to your library. </p>
<h2>
<strong>Saving from your iPhone</strong>
</h2>
<p> 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: </p>
<p>
<a href="https://omnivore.app/install/ios" rel="">https://omnivore.app/install/ios</a>
</p>
<p> With the iOS Share extension installed, you can save links from Safari or any other app that supports sharing links. </p>
<article>
<div>
<figure>
<picture>
<source type="image/webp" srcset="https://proxy-prod.omnivore-image-cache.app/424x0,s3Tp35mZSk4rZUx1FW-ZQQTrrVqh_Ku6got0mUF9rdUg/https://substackcdn.com/image/fetch/w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fb116ac39-34b8-4868-a2b0-b20c79f610c3_256x419.gif 424w,https://proxy-prod.omnivore-image-cache.app/848x0,sTsUyTN49MKzYrTJUThPqtdCs9es_-fekzpEzDuiZhLg/https://substackcdn.com/image/fetch/w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fb116ac39-34b8-4868-a2b0-b20c79f610c3_256x419.gif 848w,https://proxy-prod.omnivore-image-cache.app/1272x0,sW0LM6cgEIbKPsy2g1Mc2lkwu4ADwS87O7Xci9JnoPmg/https://substackcdn.com/image/fetch/w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fb116ac39-34b8-4868-a2b0-b20c79f610c3_256x419.gif 1272w,https://proxy-prod.omnivore-image-cache.app/1456x0,sDGDvjDYg95UJXisBaJvD8_9F4s0iQ3-TJ-YfcbG23lY/https://substackcdn.com/image/fetch/w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fb116ac39-34b8-4868-a2b0-b20c79f610c3_256x419.gif 1456w," sizes="100vw"><img src="https://proxy-prod.omnivore-image-cache.app/320x0,sf_mzRoppyTjr9vx0vC29wATzQaPGmTjHpJIZJIRBCdk/https://substackcdn.com/image/fetch/w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fb116ac39-34b8-4868-a2b0-b20c79f610c3_256x419.gif" width="320" height="523.75" data-attrs="{&quot;src&quot;:&quot;https://bucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com/public/images/b116ac39-34b8-4868-a2b0-b20c79f610c3_256x419.gif&quot;,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:419,&quot;width&quot;:256,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:275502,&quot;alt&quot;:&quot;Saving with the Omnivore iOS Share Extension&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/gif&quot;,&quot;href&quot;:null}" alt="Saving with the Omnivore iOS Share Extension" title="Saving with the Omnivore iOS Share Extension" srcset="https://proxy-prod.omnivore-image-cache.app/424x0,sfmyhHAQbi2vXSQD65Z65hrRtwHPHe8Ji5_fFTV6_H3c/https://substackcdn.com/image/fetch/w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fb116ac39-34b8-4868-a2b0-b20c79f610c3_256x419.gif 424w,https://proxy-prod.omnivore-image-cache.app/848x0,s2eC-K0H5HwOdpIfsyHlKtpsc42bwHlrjEqEFNr85dIo/https://substackcdn.com/image/fetch/w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fb116ac39-34b8-4868-a2b0-b20c79f610c3_256x419.gif 848w,https://proxy-prod.omnivore-image-cache.app/1272x0,s_YqgWoN_nP6DQkyLWxtzSyTyCP_JGR42u8yixMN5oyc/https://substackcdn.com/image/fetch/w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fb116ac39-34b8-4868-a2b0-b20c79f610c3_256x419.gif 1272w,https://proxy-prod.omnivore-image-cache.app/1456x0,swID7GBlKSzLKtLJybFkwKBYQfKe0yV_FDJHf_Lyb4pc/https://substackcdn.com/image/fetch/w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fb116ac39-34b8-4868-a2b0-b20c79f610c3_256x419.gif 1456w," sizes="100vw">
</picture>
<figcaption> Saving with the Omnivore iOS Share Extension </figcaption>
</figure>
<h3> Omnivore is a read-it-later app that lets you save and organize everything you read online. </h3>
</div>
<p> 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. </p>
<h2> Saving from your Android Device </h2>
<p> 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. </p>
<div>
<figure>
<picture>
<source type="image/webp" srcset="https://proxy-prod.omnivore-image-cache.app/424x0,szEAueNB_CWvi6XLTaIHyPaLKExVerj5KjqEDja2hwZ8/https://substackcdn.com/image/fetch/w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F4255167b-bc82-4187-a043-3150cbbdc17d_320x152.png 424w,https://proxy-prod.omnivore-image-cache.app/848x0,sw3OfocZ3zAXx3GVn2um35SR2IVyfAgv4zZw125xctDU/https://substackcdn.com/image/fetch/w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F4255167b-bc82-4187-a043-3150cbbdc17d_320x152.png 848w,https://proxy-prod.omnivore-image-cache.app/1272x0,s5BkKwkT3vCr3jSd0_dmKHyP2_w5tagoyORcHw9A5UNY/https://substackcdn.com/image/fetch/w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F4255167b-bc82-4187-a043-3150cbbdc17d_320x152.png 1272w,https://proxy-prod.omnivore-image-cache.app/1456x0,slfWbCJPzTm5byTRWyYYeX9vhv4lblHDx_OiWC6tkbxA/https://substackcdn.com/image/fetch/w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F4255167b-bc82-4187-a043-3150cbbdc17d_320x152.png 1456w," sizes="100vw"><img src="https://proxy-prod.omnivore-image-cache.app/320x152,sPncH0Qkdv4WBNQ1awMjFhOIVz9J0UFDY4tqEFUKTKJg/https://substackcdn.com/image/fetch/w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F4255167b-bc82-4187-a043-3150cbbdc17d_320x152.png" width="320" height="152" data-attrs="{&quot;src&quot;:&quot;https://bucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com/public/images/4255167b-bc82-4187-a043-3150cbbdc17d_320x152.png&quot;,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:152,&quot;width&quot;:320,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:15668,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null}" alt="" srcset="https://proxy-prod.omnivore-image-cache.app/424x0,st5cAO-AGN34FoQmDFIqEPiKHnsBLCMnmOdYlkBwd3a8/https://substackcdn.com/image/fetch/w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F4255167b-bc82-4187-a043-3150cbbdc17d_320x152.png 424w,https://proxy-prod.omnivore-image-cache.app/848x0,sjMu4nqXk2JDim0iMoacpelpfqWRowg9BeogD2Z5g7Ac/https://substackcdn.com/image/fetch/w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F4255167b-bc82-4187-a043-3150cbbdc17d_320x152.png 848w,https://proxy-prod.omnivore-image-cache.app/1272x0,s3eN_DlmCzDVfDHg3AANU9gNvFQWDHDxu1-LpkrxlkcE/https://substackcdn.com/image/fetch/w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F4255167b-bc82-4187-a043-3150cbbdc17d_320x152.png 1272w,https://proxy-prod.omnivore-image-cache.app/1456x0,sq3HWgKRD7jKl-Z1DvN7Oz3LlMK_AjHY58PHlIx3cvQM/https://substackcdn.com/image/fetch/w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F4255167b-bc82-4187-a043-3150cbbdc17d_320x152.png 1456w," sizes="100vw">
</picture>
</figure>
<div dir="auto">
<p> This guide will show you how to use Omnivores basic functions and advanced features, divided into four main activities: </p>
<ul>
<li>
<p> Saving </p>
</li>
<li>
<p> Reading </p>
</li>
<li>
<p> Organizing </p>
</li>
<li>
<p> Integrations </p>
</li>
</ul>
<p>
<span>The</span> <strong>Library</strong> <span>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.</span>
</p>
<p> There are five ways to save links to pages or articles that you wish to read later: </p>
<ul>
<li>
<p> Saving from Your Omnivore Library </p>
</li>
<li>
<p> Saving from a Browser&nbsp; </p>
</li>
<li>
<p> Saving from a Phone or Tablet (iOS or Android) </p>
</li>
<li>
<p> Newsletter Subscriptions via Email </p>
</li>
<li>
<p>
<span>Saving PDFs from a Mac</span><br>
</p>
</li>
</ul>
<h3> Saving from Your Omnivore Library </h3>
<p>
<span>1. In the upper right corner of your Library, tap the</span> <strong>Add Link</strong> <span>button.</span><br>
<span>2. Enter the URL you wish to save and tap</span> <strong>Add Link</strong><span>.</span><br>
<span>3. The link will appear in your Library the next time you refresh it.</span><br>
</p>
<h3> Saving from a Browser </h3>
<p> 1. Download and install the Omnivore extension for your browser: </p>
<ul>
<li>
<p>
<a href="https://omnivore.app/install/chrome" rel="">Chrome&nbsp;</a>
</p>
</li>
<li>
<p>
<a href="https://omnivore.app/install/edge" rel="">Edge</a>
</p>
</li>
<li>
<p>
<a href="https://omnivore.app/install/firefox" rel="">Firefox</a>
</p>
</li>
<li>
<p>
<a href="https://omnivore.app/install/safari" rel="">Safari</a>
</p>
</li>
</ul>
<p>
<span>2. Navigate to the page you wish to save and tap the Omnivore button in your browsers toolbar or Extensions menu.</span><br>
<span>3. Alternatively, you can right-click (command+click on Mac) on any hyperlink and select</span> <strong>Save to Omnivore</strong> <span>from the menu.</span><br>
<span>4. The link will appear in your Library the next time you refresh it.</span><br>
</p>
<h3> Saving from a Phone or Tablet </h3>
<p> The best way to save links from your mobile device is via the Omnivore app. You can download the app here: </p>
<ul>
<li>
<p>
<a href="https://omnivore.app/install/ios" rel="">iOS (iPhone or iPad)</a>
</p>
</li>
<li>
<p>
<a href="https://play.google.com/store/apps/details?id=app.omnivore.omnivore" rel="">Android (Currently in pre-release)</a>
</p>
</li>
</ul>
<p> Once the mobile app is installed: </p>
<ol>
<li>
<p>
<span>In your browser, navigate to the page you wish to save and tap the</span> <strong>Share</strong> <span>button.</span>
</p>
</li>
<li>
<p>
<span>Tap the</span> <strong>Omnivore</strong> <span>icon in the Share menu.</span>
</p>
</li>
<li>
<p> The link will appear in your Library the next time you refresh it. </p>
</li>
</ol>
<h3> Newsletter Subscriptions via Email </h3>
<p>
<span>1. On the Omnivore website or app, tap your photo, initial, or avatar in the top right corner to access the profile menu. Select</span> <strong>Emails</strong> <span>from the menu.</span>
</p>
<p>
<span>2. Tap</span> <strong>Create a New Email Address</strong> <span>to add a new email address (ex: username-123abc@inbox.omnivore.app) to the list.</span>
</p>
<p> 3. Click the Copy icon next to the email address. </p>
<p>
<span>4. Navigate to the signup page for the newsletter you wish to subscribe to.</span><br>
<span>5. Paste the Omnivore email address into the signup form.</span>
</p>
<p> 6. New newsletters will be automatically delivered to your Omnivore inbox. </p>
<h3> Saving PDFs from a Mac&nbsp; </h3>
<ol>
<li>
<p>
<span>Install the</span> <a href="https://omnivore.app/install/mac" rel="">Mac App</a><span>.&nbsp;</span>
</p>
</li>
<li>
<p> On your Mac, locate the PDF you wish to save and right-click or ctrl+click on the file name. </p>
</li>
<li>
<p>
<span>Select</span> <strong>Share</strong> <span>from the menu and choose</span> <strong>Omnivore</strong><span>.</span>
</p>
</li>
<li>
<p> The link will appear in your Library the next time you refresh it. </p>
</li>
</ol>
<h2> Reading </h2>
<p> Click any link saved in your Library to enter the Reader view.&nbsp; </p>
<p> 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. </p>
<p> While reading, you can: </p>
<ul>
<li>
<p> Change Formatting </p>
</li>
<li>
<p> Highlight Text </p>
</li>
<li>
<p> Add Notes </p>
</li>
<li>
<p> View All Saved Highlights and Notes </p>
</li>
<li>
<p> Track Reading Progress </p>
</li>
</ul>
<h3> Change Formatting&nbsp; </h3>
<ol>
<li>
<p>
<em><strong>Theme:</strong></em> <span>Tap your photo, initial, or avatar&nbsp; in the top right corner to access the profile menu. Select the white or black thumbnail to choose the Light or Dark theme.</span>
</p>
</li>
<li>
<p>
<em><strong>Text Formatting:</strong></em> <span>Tap the Aa icon to adjust the text size, font, margins, and line spacing.</span>
</p>
</li>
</ol>
<h3> Highlight Text </h3>
<ol>
<li>
<p> Select the text you wish to highlight. </p>
</li>
<li>
<p>
<span>Tap the</span> <strong>Highlight</strong> <span>button.</span>
</p>
</li>
<li>
<p> The text will appear highlighted next time you view the article. </p>
</li>
</ol>
<h3> Add Notes </h3>
<ol>
<li>
<p> Highlight a section of text where you wish to add a note. </p>
</li>
<li>
<p>
<span>Tap the</span> <strong>Note</strong> <span>button, type your note, and tap</span> <strong>Save</strong><span>.</span>
</p>
</li>
<li>
<p> The Note icon will appear next time you view this article. </p>
</li>
</ol>
<h3> View All Saved Highlights and Notes </h3>
<ol>
<li>
<p> Tap the Highlight/Note icon to see a list of all the highlighted text and notes you have added to this page. </p>
</li>
<li>
<p> To remove a note or highlight, select it from the list and tap the Trash icon. </p>
</li>
</ol>
<h3> Track Reading Progress </h3>
<p> 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. </p>
<h2> Organizing </h2>
<p> 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:&nbsp; </p>
<ul>
<li>
<p> Archiving </p>
</li>
<li>
<p> Labels </p>
</li>
<li>
<p> Search </p>
</li>
<li>
<p> Filters </p>
</li>
</ul>
<h2> Archiving </h2>
<ol>
<li>
<p> Tap the Menu icon next to the link you wish to archive (on the mobile app, long press the link to open the menu). </p>
</li>
<li>
<p>
<span>Select</span> <strong>Archive</strong><span>.</span>
</p>
</li>
<li>
<p> The link will disappear from the default Library view, but will show up if you select the Archived filter (see Filters below). </p>
</li>
</ol>
<h3>
<strong>Labels</strong>
</h3>
<ol>
<li>
<p>
<span>Tap the Menu icon next to any link and select</span> <strong>Set Label</strong><span>s.</span>
</p>
</li>
<li>
<p>
<span>Select an existing label from the list or tap</span> <strong>Edit Labels</strong> <span>to create a new one.</span>
</p>
</li>
<li>
<p> The label will appear next to the link in your Library. Tap it to view all links with the same label. </p>
</li>
<li>
<p>
<em>Omnivore mobile app only</em><span>: tap</span> <strong>Labels</strong> <span>to see a complete list of all labels you have used; tap one to view all links with the same label</span>
</p>
</li>
<li>
<p> Note: Omnivore will automatically assign some labels, such as “Newsletters.” </p>
</li>
</ol>
<h3> Search </h3>
<ol>
<li>
<p> To search through all your saved links, enter a keyword or phrase in the search bar.&nbsp; </p>
</li>
<li>
<p>
<span>You can combine keywords with labels and filters to focus your search even further.</span> <a href="https://omnivore.app/help/search" rel="">Learn more about advanced search</a><span>.</span>
</p>
</li>
</ol>
<h3> Filters </h3>
<ol>
<li>
<p>
<span>Use the</span> <strong>Filters</strong> <span>menu to refine your Library view (some filters may be visible by default).</span>
</p>
</li>
<li>
<p>
<span>Select</span> <strong>Read Later</strong> <span>to view a list of all your non-archived links except Newsletters.</span>
</p>
</li>
<li>
<p>
<span>Select</span> <strong>Highlights</strong> <span>to view the text selections you have highlighted in all your saved pages.&nbsp;</span>
</p>
</li>
<li>
<p>
<span>Select</span> <strong>Today</strong> <span>to view a list of links you saved today.</span>
</p>
</li>
<li>
<p>
<span>Select</span> <strong>Newsletters</strong> <span>to view links saved via your newsletter subscriptions.</span>
</p>
</li>
</ol>
<h2> Integrations </h2>
<p> Omnivore allows integrations with knowledge bases and note-taking apps including: </p>
<ul>
<li>
<p> Logseq </p>
</li>
<li>
<p> Webhooks </p>
</li>
</ul>
<h3> Logseq </h3>
<p>
<span>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</span> <a href="https://briansunter.com/graph/%23/page/omnivore-logseq-guide" rel="">Omnivore for Logseq Plugin Guide</a><span>.</span>
</p>
<h3> Webhooks </h3>
<p>
<span>Omnivore can trigger webhooks when you save a link or add highlights to a page you are reading.</span> <a href="https://blog.omnivore.app/p/syncing-all-your-notes-to-google" rel="">This example</a> <span>shows webhooks being used to write all saved links to a Google Sheets spreadsheet stored on a Google Drive.</span>
</p>
</div>
<p> After installing Omnivore as a Progressive Web App it will be displayed in your Sharing Menu on Chrome. </p>
<div>
<figure>
<picture>
<source type="image/webp" srcset="https://proxy-prod.omnivore-image-cache.app/424x0,s9sYo3GPZeuGlPsv1BKOm0LXMs1_JEKyITuSkq_QhzTw/https://substackcdn.com/image/fetch/w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Ff382aca6-3fbf-4a0c-a69e-8270c1554e24_320x693.png 424w,https://proxy-prod.omnivore-image-cache.app/848x0,sOYYuoQ-C9pgzD63t3-AWAGXCqsMvP-CdVbehupWCth8/https://substackcdn.com/image/fetch/w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Ff382aca6-3fbf-4a0c-a69e-8270c1554e24_320x693.png 848w,https://proxy-prod.omnivore-image-cache.app/1272x0,sZ9tAqODgaH78rD4TdUHY_EK7elhadPtgO4t2P2CLU8w/https://substackcdn.com/image/fetch/w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Ff382aca6-3fbf-4a0c-a69e-8270c1554e24_320x693.png 1272w,https://proxy-prod.omnivore-image-cache.app/1456x0,s2xN29p0scG798MeSLt8FVN_IjW_DFmUS0XfJ0WIYjH0/https://substackcdn.com/image/fetch/w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Ff382aca6-3fbf-4a0c-a69e-8270c1554e24_320x693.png 1456w," sizes="100vw"><img src="https://proxy-prod.omnivore-image-cache.app/320x693,sVrI20nc-6zjZA4CCPXIDJZ09RRm04UWELUF4G6KjUOQ/https://substackcdn.com/image/fetch/w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Ff382aca6-3fbf-4a0c-a69e-8270c1554e24_320x693.png" width="320" height="693" data-attrs="{&quot;src&quot;:&quot;https://bucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com/public/images/f382aca6-3fbf-4a0c-a69e-8270c1554e24_320x693.png&quot;,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:693,&quot;width&quot;:320,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:43062,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null}" alt="" srcset="https://proxy-prod.omnivore-image-cache.app/424x0,sWX6aBN9SJT5iRiDyPLI6w5VpVXn0CsiUM-JF-WS9vwc/https://substackcdn.com/image/fetch/w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Ff382aca6-3fbf-4a0c-a69e-8270c1554e24_320x693.png 424w,https://proxy-prod.omnivore-image-cache.app/848x0,s_B_jBJ6Ox8lGxCJH8xQ8JAtUQxhM1CYkY-WdD086NCE/https://substackcdn.com/image/fetch/w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Ff382aca6-3fbf-4a0c-a69e-8270c1554e24_320x693.png 848w,https://proxy-prod.omnivore-image-cache.app/1272x0,salzGsLiq0tbhDzc5ZA-VkE5yT0CI6SsPytcagDtS1U8/https://substackcdn.com/image/fetch/w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Ff382aca6-3fbf-4a0c-a69e-8270c1554e24_320x693.png 1272w,https://proxy-prod.omnivore-image-cache.app/1456x0,s_q0Iha8X3hDAxr91-x4e4y7JT3csQc_GCQ8lDmgQLC8/https://substackcdn.com/image/fetch/w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Ff382aca6-3fbf-4a0c-a69e-8270c1554e24_320x693.png 1456w," sizes="100vw">
</picture>
</figure>
</div>
<h2>
<strong>Saving from your computer</strong>
</h2>
<p> If you are saving from a computer, you will need to install the Omnivore extension for the web browser(s) you use. </p>
<p> The browser extensions are available here: </p>
<ul>
<li>
<p> Chrome: <a href="https://omnivore.app/install/chrome" rel="">https://omnivore.app/install/chrome</a>
</p>
</li>
<li>
<p> Edge: <a href="https://omnivore.app/install/edge" rel="">https://omnivore.app/install/edge</a>
</p>
</li>
<li>
<p> Firefox: <a href="https://omnivore.app/install/firefox" rel="">https://omnivore.app/install/firefox</a>
</p>
</li>
<li>
<p> Safari: https://omnivore.app/install/safari </p>
</li>
</ul>
<p> With the browser extension(s) of your choice installed, you can tap the Omnivore button on any page to save your link. </p>
<div>
<figure>
<picture>
<source type="image/webp" srcset="https://proxy-prod.omnivore-image-cache.app/424x0,s4C2lK-YKv_uCIU7rxRReB4StTUXpqKTo1fGzGsZyD44/https://substackcdn.com/image/fetch/w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F658efff4-341a-4720-8cf6-9b2bdbedfaa7_800x668.gif 424w,https://proxy-prod.omnivore-image-cache.app/848x0,sQs3W_TI1zJQf9pKaHU_vxoLS5LFa55lPXfeA0lchSaY/https://substackcdn.com/image/fetch/w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F658efff4-341a-4720-8cf6-9b2bdbedfaa7_800x668.gif 848w,https://proxy-prod.omnivore-image-cache.app/1272x0,snk5hXTLVpI5waYpkhQq0g58WClFxzebG7xTwAO-WnMU/https://substackcdn.com/image/fetch/w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F658efff4-341a-4720-8cf6-9b2bdbedfaa7_800x668.gif 1272w,https://proxy-prod.omnivore-image-cache.app/1456x0,sfw20Guzp6RxXw2VqIsacGvfK2ZBKWiqc3hMDM-t1evA/https://substackcdn.com/image/fetch/w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F658efff4-341a-4720-8cf6-9b2bdbedfaa7_800x668.gif 1456w," sizes="100vw"><img src="https://proxy-prod.omnivore-image-cache.app/800x668,syfEIKlv_v8Mj692y-EBGA-OdbVfk3-jn7jNQpJ02PHc/https://substackcdn.com/image/fetch/w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F658efff4-341a-4720-8cf6-9b2bdbedfaa7_800x668.gif" width="800" height="668" data-attrs="{&quot;src&quot;:&quot;https://bucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com/public/images/658efff4-341a-4720-8cf6-9b2bdbedfaa7_800x668.gif&quot;,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:668,&quot;width&quot;:800,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:148745,&quot;alt&quot;:&quot;Saving with the Omnivore Browser Extension&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/gif&quot;,&quot;href&quot;:null}" alt="Saving with the Omnivore Browser Extension" title="Saving with the Omnivore Browser Extension" srcset="https://proxy-prod.omnivore-image-cache.app/424x0,syEiwBEWo09bdiA4QNjNUCdw3A43IY6T5AmtRjUGaGxE/https://substackcdn.com/image/fetch/w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F658efff4-341a-4720-8cf6-9b2bdbedfaa7_800x668.gif 424w,https://proxy-prod.omnivore-image-cache.app/848x0,smvewZboJkofL1PyGKR8Fv-tHMDFx3B7fFwJhnvn8hxo/https://substackcdn.com/image/fetch/w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F658efff4-341a-4720-8cf6-9b2bdbedfaa7_800x668.gif 848w,https://proxy-prod.omnivore-image-cache.app/1272x0,sHoCROP5pmimLw--CPw5zHfrWfzdUZW5lX0CHOraaQE0/https://substackcdn.com/image/fetch/w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F658efff4-341a-4720-8cf6-9b2bdbedfaa7_800x668.gif 1272w,https://proxy-prod.omnivore-image-cache.app/1456x0,syEVSV26jnlEevguxBxPuWogsJkj4cUESy5LbJaX5lWA/https://substackcdn.com/image/fetch/w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F658efff4-341a-4720-8cf6-9b2bdbedfaa7_800x668.gif 1456w," sizes="100vw">
</picture>
<figcaption> Saving with the Omnivore Browser Extension </figcaption>
</figure>
</div>
<h2>
<strong>Saving PDFs with the Mac App</strong>
</h2>
<p> https://omnivore.app/install/mac </p>
<p> With the MacOS App installed you can upload PDFs from your computer to your Omnivore library by right-clicking and sharing to Omnivore. </p>
<div>
<figure>
<picture>
<source type="image/webp" srcset="https://proxy-prod.omnivore-image-cache.app/424x0,sKPkqmx11iOOzY-zmRC3L-2XbZc4FUZTqZ3TpDcksEfM/https://substackcdn.com/image/fetch/w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F598e774d-1c20-43e3-bccf-be875f7b6616_1400x955.png 424w,https://proxy-prod.omnivore-image-cache.app/848x0,s60LiESEeql8T0I_mZGTp9f_28uyTsbTuUD4jwEqw0pw/https://substackcdn.com/image/fetch/w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F598e774d-1c20-43e3-bccf-be875f7b6616_1400x955.png 848w,https://proxy-prod.omnivore-image-cache.app/1272x0,sU6aRizGQDyMotuYm9Nuc1-Hib1uAYelPmBe2QfKSyXo/https://substackcdn.com/image/fetch/w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F598e774d-1c20-43e3-bccf-be875f7b6616_1400x955.png 1272w,https://proxy-prod.omnivore-image-cache.app/1456x0,szSath43fdzFfDlfsn54VpmXLvBLhd8wk544gk_bB1qM/https://substackcdn.com/image/fetch/w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F598e774d-1c20-43e3-bccf-be875f7b6616_1400x955.png 1456w," sizes="100vw"><img src="https://proxy-prod.omnivore-image-cache.app/1400x955,sfJHCkK4TRBim-NbM6ZaY2KzSSobzyblQM-ToG01PgXs/https://substackcdn.com/image/fetch/w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F598e774d-1c20-43e3-bccf-be875f7b6616_1400x955.png" width="1400" height="955" data-attrs="{&quot;src&quot;:&quot;https://bucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com/public/images/598e774d-1c20-43e3-bccf-be875f7b6616_1400x955.png&quot;,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:955,&quot;width&quot;:1400,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:297887,&quot;alt&quot;:&quot;Sharing a PDF with Omnivore&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null}" alt="Sharing a PDF with Omnivore" title="Sharing a PDF with Omnivore" srcset="https://proxy-prod.omnivore-image-cache.app/424x0,sTDmW6U5TxZceILtK9hJ_8C2tjNFETTbqdnWRQn50iTs/https://substackcdn.com/image/fetch/w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F598e774d-1c20-43e3-bccf-be875f7b6616_1400x955.png 424w,https://proxy-prod.omnivore-image-cache.app/848x0,sqGAC0DRqQaxjizImG52_vltO8Hr508qpsgiK3Pg5yBY/https://substackcdn.com/image/fetch/w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F598e774d-1c20-43e3-bccf-be875f7b6616_1400x955.png 848w,https://proxy-prod.omnivore-image-cache.app/1272x0,sg5WIB77CKA51z5ZEC2MKHuNpr0493qRtlRiEzxZe5dI/https://substackcdn.com/image/fetch/w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F598e774d-1c20-43e3-bccf-be875f7b6616_1400x955.png 1272w,https://proxy-prod.omnivore-image-cache.app/1456x0,sHtc6OFpttTWoDv2rQgA0eu3XPvtJJIdbWAN4nLxNJvQ/https://substackcdn.com/image/fetch/w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F598e774d-1c20-43e3-bccf-be875f7b6616_1400x955.png 1456w," sizes="100vw">
</picture>
<figcaption> Sharing a PDF with Omnivore </figcaption>
</figure>
</div>
<p> You can enable sharing from Finder on the Mac in the Extensions section of System Preferences. </p>
<div>
<figure>
<picture>
<source type="image/webp" srcset="https://proxy-prod.omnivore-image-cache.app/424x0,sZbrx9ma1-45DGxZH3wetVw-rpsiymZSh_hwBz-ZHMHc/https://substackcdn.com/image/fetch/w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fe109c534-a502-49cb-a245-3deba005fe84_960x709.gif 424w,https://proxy-prod.omnivore-image-cache.app/848x0,s5vU-otQ38M3ehBk5Owh0qnsDPsy3HtYyilJK7S6Fo_s/https://substackcdn.com/image/fetch/w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fe109c534-a502-49cb-a245-3deba005fe84_960x709.gif 848w,https://proxy-prod.omnivore-image-cache.app/1272x0,szHgrFpkjdn4xlMSEH_tnU9p9ZK7oliNbjSg-hAG4m_I/https://substackcdn.com/image/fetch/w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fe109c534-a502-49cb-a245-3deba005fe84_960x709.gif 1272w,https://proxy-prod.omnivore-image-cache.app/1456x0,s3RUMJtmhNMqmoWu-AoHYV8OF-iuW_csnuQJwMBCU4vA/https://substackcdn.com/image/fetch/w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fe109c534-a502-49cb-a245-3deba005fe84_960x709.gif 1456w," sizes="100vw"><img src="https://proxy-prod.omnivore-image-cache.app/960x709,sUxZ9zqwTgxwlkISiiu-xpktG6c5OzjgaPCo1oKi6wiY/https://substackcdn.com/image/fetch/w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fe109c534-a502-49cb-a245-3deba005fe84_960x709.gif" width="960" height="709" alt="" srcset="https://proxy-prod.omnivore-image-cache.app/424x0,sa-3LVIkJeZL0Y62rNoc1NsUMCaeNnYX5ieC_yH2A9t8/https://substackcdn.com/image/fetch/w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fe109c534-a502-49cb-a245-3deba005fe84_960x709.gif 424w,https://proxy-prod.omnivore-image-cache.app/848x0,s6YHkdqtOoZK_PT6DRuUVbFO2JWfr1Th65gi0GInHp88/https://substackcdn.com/image/fetch/w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fe109c534-a502-49cb-a245-3deba005fe84_960x709.gif 848w,https://proxy-prod.omnivore-image-cache.app/1272x0,siLpHytIdAcX2AZprrBCrSVJMJtrZc-vhdIRhZjUq1C4/https://substackcdn.com/image/fetch/w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fe109c534-a502-49cb-a245-3deba005fe84_960x709.gif 1272w,https://proxy-prod.omnivore-image-cache.app/1456x0,sKC2OyN0txcrNKqSF816noWAaN822HEy7jYSjd-TwEbU/https://substackcdn.com/image/fetch/w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fe109c534-a502-49cb-a245-3deba005fe84_960x709.gif 1456w," sizes="100vw">
</picture>
</figure>
</div>
<h2> Using Omnivore </h2>
<p> 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. </p>
<div>
<figure>
<a target="_blank" rel="nofollow" href="https://www.loom.com/share/a9afdd040dd349e28317430e1a178acc">
<picture>
<source type="image/webp" srcset="https://proxy-prod.omnivore-image-cache.app/424x0,s9K-C3RZ0LQ2sqDTN_X3zsE0aAq0pe6HdudoFwvTrk3U/https://substackcdn.com/image/fetch/w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F59cc33d5-9a9f-4e40-b4a3-c7a399cc293f_576x360.gif 424w,https://proxy-prod.omnivore-image-cache.app/848x0,sGI0UuupoL7QzREdVAJHA4Oov19UHAaaOj2kcLiKqxWM/https://substackcdn.com/image/fetch/w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F59cc33d5-9a9f-4e40-b4a3-c7a399cc293f_576x360.gif 848w,https://proxy-prod.omnivore-image-cache.app/1272x0,skeBVS515IZTWMqQRh7Uh4AhgUNAhNqnq0pSVac1rDo8/https://substackcdn.com/image/fetch/w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F59cc33d5-9a9f-4e40-b4a3-c7a399cc293f_576x360.gif 1272w,https://proxy-prod.omnivore-image-cache.app/1456x0,sSPX-knSBcazKJi8y3ROFYDNiKkdh8yfXLLJePV83614/https://substackcdn.com/image/fetch/w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F59cc33d5-9a9f-4e40-b4a3-c7a399cc293f_576x360.gif 1456w," sizes="100vw"><img src="https://proxy-prod.omnivore-image-cache.app/576x360,ss3YRRdFbuwvYjLh0ERvkCl9x98HDXfvciDUksKHovTc/https://substackcdn.com/image/fetch/w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F59cc33d5-9a9f-4e40-b4a3-c7a399cc293f_576x360.gif" width="576" height="360" alt="Screenshot of video demonstrating Omnivore" title="Screenshot of video demonstrating Omnivore" srcset="https://proxy-prod.omnivore-image-cache.app/424x0,s1RYNee_TMnFnrp4c1LB72unRCIYcs-Hf217H4eAMpAY/https://substackcdn.com/image/fetch/w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F59cc33d5-9a9f-4e40-b4a3-c7a399cc293f_576x360.gif 424w,https://proxy-prod.omnivore-image-cache.app/848x0,sELtfdjSgvtUnXXyo0O3hTXZwREx-kPVcx7Em8Zkex-w/https://substackcdn.com/image/fetch/w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F59cc33d5-9a9f-4e40-b4a3-c7a399cc293f_576x360.gif 848w,https://proxy-prod.omnivore-image-cache.app/1272x0,sOiGR4hcRTXxZHPE7ZXKTOqm7xvpZmUTRysPp30oY3F0/https://substackcdn.com/image/fetch/w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F59cc33d5-9a9f-4e40-b4a3-c7a399cc293f_576x360.gif 1272w,https://proxy-prod.omnivore-image-cache.app/1456x0,sGZGbsFhN5g5hoiAF_1VjMvS7umVSCD6NPqNWx1eoxGE/https://substackcdn.com/image/fetch/w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F59cc33d5-9a9f-4e40-b4a3-c7a399cc293f_576x360.gif 1456w," sizes="100vw">
</picture>
</a>
</figure>
</div>
</div>
</article>
</DIV>

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,94 @@
<DIV class="page" id="readability-page-1">
<article>
<div dir="auto">
<p>
<span>With the</span> <a href="https://omnivore.app/install/ios" rel="">Omnivore app for iOS</a><span>, its easy to save web pages and articles or archive web content to read later.</span>
</p>
<p>
<span>The Omnivore app uses the</span> <em>iOS Share System</em><span>, which lets you send items from one app (such as Safari) to another (such as Messages or Mail).&nbsp;</span>
</p>
<ul>
<li>
<p> Step 1: Log in to the Omnivore app. </p>
</li>
<li>
<p> Step 2: Add Omnivore to your Share menu favorites. </p>
</li>
<li>
<p> Step 3: Save links to your Omnivore Library. </p>
</li>
</ul>
<div id="youtube2-k6RkIqepAig" data-attrs="{&quot;videoId&quot;:&quot;k6RkIqepAig&quot;,&quot;startTime&quot;:null,&quot;endTime&quot;:null}">
<P>
<iframe src="https://www.youtube-nocookie.com/embed/k6RkIqepAig?rel=0&autoplay=0&showinfo=0&enablejsapi=0" frameborder="0" loading="lazy" gesture="media" allow="autoplay; fullscreen" allowautoplay="true" allowfullscreen="true" width="728" height="409"></iframe>
</P>
</div>
<p> You must be logged in before you can save links via the Share menu. If you dont already have an Omnivore account, you can sign up for free from the login screen. </p>
<p>
<em>Note:</em> <span>If you havent installed the iOS app, download it here:</span> <a href="https://omnivore.app/install/ios" rel="">https://omnivore.app/install/ios</a>
</p>
<h2>
<strong>Step 2: Add Omnivore to your Share menu favorites.</strong>
</h2>
<p> Start by viewing the Share menu from within any supported iOS app (weve used Safari for this example). </p>
<ol>
<li>
<p>
<span>Tap the</span> <strong>Share</strong> <span>icon at the bottom of the screen.</span>
</p>
</li>
<li>
<p>
<span>Swipe left to the end of the list of app icons and tap</span> <strong>More</strong><span>.</span>
</p>
</li>
<li>
<p>
<span>Tap</span> <strong>Edit</strong> <span>at the top of the screen.</span>
</p>
</li>
<li>
<p>
<span>Scroll down until you see the</span> <strong>Omnivore</strong> <span>icon and tap the</span> <strong>+</strong> <span>icon next to it.&nbsp;</span>
</p>
</li>
<li>
<p>
<span>Press and hold the three-bar icon and drag Omnivore to one of the top positions under Favorites. Tap</span> <strong>Done</strong> <span>to close the menu.</span>
</p>
</li>
<li>
<p>
<strong>Omnivore</strong> <span>will appear as one of the first options the next time you use the Share feature (you may need to restart Safari).</span>
</p>
</li>
</ol>
<h2>
<strong>Step 3: Save links to your Omnivore Library</strong>
</h2>
<p>
<span>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</span> <em>if the page is behind a paywall and you are logged into the paywalled site, you will save the paid content.</em><span>&nbsp;</span>
</p>
<ol>
<li>
<p>
<span>While viewing the page youd like to save, tap the</span> <strong>Share</strong> <span>icon.</span>
</p>
</li>
<li>
<p>
<span>Tap the</span> <strong>Omnivore</strong> <span>icon in the Share menu.</span>
</p>
</li>
<li>
<p>
<span>Tag the article with one or more labels (optional) and tap</span> <strong>Read Now</strong> <span>or</span> <strong>Read Later</strong><span>.&nbsp;</span>
</p>
</li>
<li>
<p> If you choose Read Later, the link will appear in your Library the next time you open the Omnivore app. </p>
</li>
</ol>
</div>
</article>
</DIV>

File diff suppressed because one or more lines are too long

View file

@ -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

View file

@ -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/')
}

View file

@ -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

View file

@ -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([])
})
})
})

View file

@ -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)
})
})
})

View file

@ -78,7 +78,8 @@ export abstract class ContentHandler {
postHeader: string
from: string
unSubHeader: string
html?: string
html: string
dom: Document
}): Promise<boolean> {
const re = new RegExp(this.senderRegex)
return Promise.resolve(

View file

@ -24,6 +24,9 @@ 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'
import { CooperPressHandler } from './newsletters/cooper-press-handler'
const validateUrlString = (url: string) => {
const u = new URL(url)
@ -70,6 +73,8 @@ const newsletterHandlers: ContentHandler[] = [
new BeehiivHandler(),
new ConvertkitHandler(),
new RevueHandler(),
new GhostHandler(),
new CooperPressHandler(),
]
export const preHandleContent = async (
@ -119,13 +124,29 @@ export const preParseContent = async (
return undefined
}
export const getNewsletterHandler = async (input: {
postHeader: string
from: string
unSubHeader: string
html: string
}): Promise<ContentHandler | undefined> => {
const dom = parseHTML(input.html).document
for (const handler of newsletterHandlers) {
if (await handler.isNewsletter({ ...input, dom })) {
return handler
}
}
return undefined
}
export const handleNewsletter = async (
input: NewsletterInput
): Promise<NewsletterResult | undefined> => {
for (const handler of newsletterHandlers) {
if (await handler.isNewsletter(input)) {
return handler.handleNewsletter(input)
}
const handler = await getNewsletterHandler(input)
if (handler) {
console.log('handleNewsletter', handler.name, input.title)
return handler.handleNewsletter(input)
}
return undefined
@ -135,4 +156,5 @@ module.exports = {
preHandleContent,
handleNewsletter,
preParseContent,
getNewsletterHandler,
}

View file

@ -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<boolean> {
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) {

View file

@ -1,5 +1,4 @@
import { ContentHandler } from '../content-handler'
import { parseHTML } from 'linkedom'
export class ConvertkitHandler extends ContentHandler {
constructor() {
@ -8,10 +7,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
}
})
@ -22,12 +24,12 @@ export class ConvertkitHandler extends ContentHandler {
postHeader: string
from: string
unSubHeader: string
html: string
dom: Document
}): Promise<boolean> {
const dom = parseHTML(input.html).document
const dom = input.dom
return Promise.resolve(
dom.querySelectorAll(
'img[src*="convertkit.com"], img[src*="convertkit-mail.com"]'
'img[src*="convertkit.com"], img[src*="convertkit-mail"]'
).length > 0
)
}

View file

@ -0,0 +1,38 @@
import { ContentHandler } from '../content-handler'
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
dom: Document
}): Promise<boolean> {
const dom = input.dom
return Promise.resolve(
dom.querySelectorAll('a[href*="cooperpress.com"]').length > 0
)
}
async parseNewsletterUrl(
postHeader: string,
html: string
): Promise<string | undefined> {
return this.findNewsletterUrl(html)
}
}

View file

@ -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<boolean> {
const dom = input.dom
return Promise.resolve(
dom.querySelectorAll('img[src*="ghost.org"]').length > 0
)
}
async parseNewsletterUrl(
postHeader: string,
html: string
): Promise<string | undefined> {
return this.findNewsletterUrl(html)
}
}

View file

@ -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<boolean> {
const dom = parseHTML(input.html).document
const dom = input.dom
if (
dom.querySelectorAll('img[src*="getrevue.co"], img[src*="revue.email"]')
.length > 0

View file

@ -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<boolean> {
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

View file

@ -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'

File diff suppressed because it is too large Load diff

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,288 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
<meta name="format-detection" content="date=no">
<meta name="format-detection" content="telephone=no">
<style>body {
margin: 0; padding: 0; width: 100%; background-color: white; -webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; text-rendering: optimizeLegibility; direction: ltr;
}
body {
font-family: -apple-system,BlinkMacSystemFont,Helvetica,sans-serif; font-size: 15px; line-height: 1.48em;
}
img {
border: 0; outline: none; height: auto; text-decoration: none; max-width: 100%; line-height: 100%;
}
@media screen and (max-width: 600px) {
table[id="main"] {
max-width: 600px !important; width: 100% !important; min-width: 100% !important;
}
.nomob {
display: none !important;
}
.onlymob {
display: inline-block !important;
}
.rightifmob {
text-align: right !important;
}
.som {
max-width: 33% !important;
}
div.footer p {
text-align: left !important;
}
div.footer td {
text-align: left !important;
}
.el-columns .column {
width: 100% !important;
}
}
</style>
</head>
<body style="width: 100%; background-color: white; -webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; text-rendering: optimizeLegibility; direction: ltr; font-family: -apple-system,BlinkMacSystemFont,Helvetica,sans-serif; font-size: 15px; line-height: 1.48em; margin-top: 0; margin-right: 0; margin-bottom: 0; margin-left: 0; padding-top: 0; padding-right: 0; padding-bottom: 0; padding-left: 0;">
<div id="preview" class="preheader noarchive" style="border-top-width: 0px; border-right-width: 0px; border-bottom-width: 0px; border-left-width: 0px; color: white; font-family: helvetica, arial; line-height: 0px; height: 0px; margin-top: 0; margin-right: 0; margin-bottom: 0; margin-left: 0; overflow-y: hidden; padding-top: 0; padding-right: 0; padding-bottom: 0; padding-left: 0;">Plus choosing the best Node Docker image to use, and a way to embed Node and V8 into JVM apps. | </div>
<!--[if (gte mso 9)|(IE)]>
<table cellpadding="0" cellspacing="0" align="center" bgcolor="#ffffff" width="600"><tr><td valign="top" style="width: 600px;" bgcolor="#ffffff"><![endif]-->
<table align="center" border="0" cellpadding="0" cellspacing="0" id="main" width="100%" style="table-layout: fixed; font-family: -apple-system,BlinkMacSystemFont,Helvetica,sans-serif; font-size: 15px; line-height: 1.48em; border-collapse: collapse; max-width: 600px; height: 100% !important; direction: ltr; padding-top: 0px; padding-right: 0px; padding-bottom: 0px; padding-left: 0px;">
<tr><td style="font-family: -apple-system,BlinkMacSystemFont,Helvetica,sans-serif; font-size: 15px; line-height: 1.48em; border-collapse: collapse;">
<div id="content"> <!-- left/right splitbar -->
<table class="el-splitbar" width="100%" cellpadding="0" cellspacing="0" style="border-collapse: collapse;"><tr>
<td width="50%" align="left" style="padding-left: 4px; font-family: -apple-system,BlinkMacSystemFont,Helvetica,sans-serif; font-size: 15px; line-height: 1.48em; border-collapse: collapse;"><p style="font-size: 12px; color: #999999; text-transform: uppercase; line-height: 1.0em; margin-top: 0.8em; margin-bottom: 0.8em;">#459 — October 20, 2022</p></td>
<td width="50%" align="right" style="padding-right: 4px; font-family: -apple-system,BlinkMacSystemFont,Helvetica,sans-serif; font-size: 15px; line-height: 1.48em; border-collapse: collapse;"><p style="font-size: 12px; text-transform: uppercase; line-height: 1.0em; margin-top: 0.8em; margin-bottom: 0.8em;"><a href="https://u25184427.ct.sendgrid.net/ls/click?upn=MnmHBiCwIPe9TmIJeskmA7mFdqmsIs-2B5Xs-2FNpSIs56obSPDXnoBEjufvIqRCEJUf5Uqg_vVXscVLXlj5UtQe3aqo5RMTdTq2PepdZjP86UOmA8nyFE700vlj1-2FK27spZiPEiEkDU3SIXWGeoiU60KFhM-2B-2Bxx5yiL8KKbAV6oFceRi8O1gMc3mdwg5D8FaaM3PublaX24iAcVbn99PzxJaPuVrU6xDWbRovw2UgGTIoEI-2BBO-2B0qzi2wv5c6yJTkUGZOcsJ6xGLXO1BO-2BHSbyZMZV4NMw-3D-3D" style="text-decoration: none; color: #20824B;">Read on the Web</a></p></td>
</tr></table>
<table id="together" width="100%" align="center" style="text-align: center; border-collapse: collapse; margin-top: 0; margin-right: 0; margin-bottom: 0; margin-left: 0;">
<tr><td style="font-family: -apple-system,BlinkMacSystemFont,Helvetica,sans-serif; font-size: 15px; line-height: 1.48em; border-collapse: collapse; margin-top: 0; margin-right: 0; margin-bottom: 0; margin-left: 0; padding-top: 0; padding-right: 0; padding-bottom: 0; padding-left: 0;">
<table align="center" style="margin-top: 4px; margin-bottom: 4px; border-collapse: collapse;">
<tr>
<td style="vertical-align: middle; text-align: right; text-transform: uppercase; letter-spacing: -0.2px; font-weight: 500; color: #222; font-size: 0.9em; font-family: -apple-system,BlinkMacSystemFont,Helvetica,sans-serif; line-height: 1.48em; border-collapse: collapse;">Together with </td>
<td style="vertical-align: middle; text-align: left; font-family: -apple-system,BlinkMacSystemFont,Helvetica,sans-serif; font-size: 15px; line-height: 1.48em; border-collapse: collapse;">
<a href="https://u25184427.ct.sendgrid.net/ls/click?upn=MnmHBiCwIPe9TmIJeskmA7mFdqmsIs-2B5Xs-2FNpSIs56r6zTNF2xDv1ApolVrZ4iZ0HiftFJvNFqLdyt1qEIYOmw-3D-3DnmCs_vVXscVLXlj5UtQe3aqo5RMTdTq2PepdZjP86UOmA8nyFE700vlj1-2FK27spZiPEiE8NENjWQQqAEHbpzhUjH-2Bckj2r35R2Duk7-2FbrQ0LuGQfAPBldpsN7Qx2BrVm2a-2BFlyBXjJOtG89q3W4OYHxWzTrMPn7O5MODZDWI2rIAvBBT7nuLtOzHRyiixX-2BooIGNPoh5jlbPIXUYA7AdsiNLi8w-3D-3D" style="outline: none; text-decoration: none; color: #20824B; border-top-width: 0; border-right-width: 0; border-bottom-width: 0; border-left-width: 0;">
<img src="https://res.cloudinary.com/cpress/image/upload/c_fill,g_auto,w_600,h_145/e_make_transparent/co_white,e_outline:7/vvpdctzacmq3xehpvwps.png" style="max-width: 95px; outline: none; height: auto; text-decoration: none; line-height: 100%; border-top-width: 0; border-right-width: 0; border-bottom-width: 0; border-left-width: 0;" width="95" alt="Memetria">
</a>
</td>
</tr>
</table>
</td></tr>
</table>
<table width="100%" cellpadding="0" cellspacing="0" bgcolor="#6ca629" border="0" style="border-collapse: collapse;"><tr style="max-height: 90px;" width="100%"><td style="width: 100%; font-family: -apple-system,BlinkMacSystemFont,Helvetica,sans-serif; font-size: 15px; line-height: 1.48em; border-collapse: collapse; margin-top: 0; margin-right: 0; margin-bottom: 0; margin-left: 0; padding-top: 0; padding-right: 0; padding-bottom: 0; padding-left: 0;"><img src="https://res.cloudinary.com/cpress/image/upload/v1653576619/lgfqinzbdqttwmhvljxb.png" alt="Node.js Weekly" width="100%" style="max-width: 100%; outline: none; height: auto; text-decoration: none; line-height: 100%; border-top-width: 0; border-right-width: 0; border-bottom-width: 0; border-left-width: 0;"></td></tr></table>
<table width="100%" class="el-fullwidthimage " cellpadding="0" cellspacing="0" style="border-collapse: collapse;"><tr><td style="font-family: -apple-system,BlinkMacSystemFont,Helvetica,sans-serif; font-size: 15px; line-height: 1.48em; border-collapse: collapse;">
<a href="https://u25184427.ct.sendgrid.net/ls/click?upn=MnmHBiCwIPe9TmIJeskmA7mFdqmsIs-2B5Xs-2FNpSIs56ra-2BHbYUbZAgq0Awl4wQsazE1bhe-2BpCdYTFO-2BpNDWqk1g-3D-3Dx_-k_vVXscVLXlj5UtQe3aqo5RMTdTq2PepdZjP86UOmA8nyFE700vlj1-2FK27spZiPEiEywLHI2112PJhYLaCD2-2B4Qi2ns-2B9maxytQrBvMSB-2B8MfIUORT1WVSjPTq-2Bh4G3A3lBW-2Farr-2BCgaLkASB8cs72Etj9NzjvjLjq0RIF-2BRJpD59rWnNLk7xaqY7ME5U1vFkpFBhhj0ekUSlb3782-2B9K-2F8A-3D-3D" style="text-decoration: none; color: #20824B;"><img src="https://res.cloudinary.com/cpress/image/upload/w_1280,e_sharpen:60,q_auto/btuiykgloamnvwq81bqh.jpg" alt="" width="640" style="outline: none; height: auto; text-decoration: none; max-width: 100%; line-height: 100%; width: 100%; border-bottom-color: #6ca629; border-bottom-style: solid; border-top-width: 0; border-right-width: 0; border-bottom-width: 3px; border-left-width: 0;"></a>
</td></tr></table>
<!-- normal content section -->
<table width="100%" cellpadding="0" cellspacing="0" class="content el-content " style="color: #222; border-collapse: collapse;"><tr><td style="font-family: -apple-system,BlinkMacSystemFont,Helvetica,sans-serif; font-size: 15px; line-height: 1.48em; border-collapse: collapse; padding-top: 0px; padding-right: 15px; padding-bottom: 0px; padding-left: 15px;"><p style="text-align: center; font-weight: 500; font-size: 1.5em; line-height: 1.3em; margin-top: 0.8em; margin-bottom: 0.8em;"><a href="https://u25184427.ct.sendgrid.net/ls/click?upn=MnmHBiCwIPe9TmIJeskmA7mFdqmsIs-2B5Xs-2FNpSIs56ra-2BHbYUbZAgq0Awl4wQsazE1bhe-2BpCdYTFO-2BpNDWqk1g-3D-3DlwD6_vVXscVLXlj5UtQe3aqo5RMTdTq2PepdZjP86UOmA8nyFE700vlj1-2FK27spZiPEiE0eL0BNqOCtCV6HNY9PPUOyevCTulgFpYrJLPMrODUpo9ZlnK1AD-2Bniu050zhRPUxXdQw09PH0Oa-2FMnEJ6SEksBeVLjWhroT-2FdcjDCYGYJI3JXjG7w6y5Y1P2aKWESeTweor-2BgP5OwCb8PiyEYnvY4A-3D-3D" style="text-decoration: underline; color: #20824B; font-weight: 600;">Node.js 19 Released</a></p></td></tr></table>
<table width="100%" cellpadding="0" cellspacing="0" class="content el-md " style="border-collapse: collapse;"><tr><td style="font-family: -apple-system,BlinkMacSystemFont,Helvetica,sans-serif; font-size: 15px; line-height: 1.48em; border-collapse: collapse; padding-top: 0px; padding-right: 15px; padding-bottom: 0px; padding-left: 15px;">
<p style="margin-top: 0.8em; margin-bottom: 0.8em;">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. <em>"If youre interested in getting access to features early, Node.js 19 is ready,”</em> says Rafael Gonzaga of the core team.</p>
<p style="margin-top: 0.8em; margin-bottom: 0.8em;">New features this time around include:</p>
<ul>
<li style="color: #222; line-height: 1.35em; margin-bottom: 12px; margin-left: 0px !important;">
<p style="margin-top: 0.8em; margin-bottom: 0.8em;"><strong style="font-weight: 600;">Watch mode.</strong> An experimental <code>--watch</code> <a href="https://u25184427.ct.sendgrid.net/ls/click?upn=MnmHBiCwIPe9TmIJeskmA7mFdqmsIs-2B5Xs-2FNpSIs56ruxEVXc6zdHvXDjQWO6GWV1hsJF2jnSDVCPPE-2B46-2BEXw-3D-3DGDsQ_vVXscVLXlj5UtQe3aqo5RMTdTq2PepdZjP86UOmA8nyFE700vlj1-2FK27spZiPEiE85sNToKZbhkQgdcYiNYy09pXE7Mss-2FT-2BWWmPTtE16FZDw-2BBxRWItDkkpGsOs0l85XA7Qp0gH-2Bt0qMBFSdiqHiiwWyPYk8tkl2t-2FxUvceWQ4yFDgDNYGjAHwkbjkPy4W5Dwi-2F8YtQ6t6CA7T24-2Bj2-2Fw-3D-3D" style="text-decoration: none; color: #20824B; font-weight: 600; border-bottom-width: 1px; border-bottom-color: #ddd; border-bottom-style: solid;">Nodemon</a>-esque mode for 'watching' files and restarting the process when imported files change. <em>(<a href="https://u25184427.ct.sendgrid.net/ls/click?upn=MnmHBiCwIPe9TmIJeskmA7mFdqmsIs-2B5Xs-2FNpSIs56o5GgJdUG7iJcRdGpnWZ0KvXQBk9obJNHL5RxWsre5iZA-3D-3DqTrl_vVXscVLXlj5UtQe3aqo5RMTdTq2PepdZjP86UOmA8nyFE700vlj1-2FK27spZiPEiEpVnUiEbvkWa5gPeGUckbnl9bUC7BL2PhmnRo275gLDEvN-2FFEfGitBp9n7jRumgwOZAQXI29Up2JuJ2xhXWd5CQAYgr-2F6BF2YHBDEC6NQ1nXPj92WMk5Gg-2BsumDorduRpqNunGZ0-2FvoijqYWTY9i5fg-3D-3D" style="text-decoration: none; color: #20824B; font-weight: 600; border-bottom-width: 1px; border-bottom-color: #ddd; border-bottom-style: solid;">Node 18.11.0 (LTS)</a> also gains this feature.)</em></p>
</li>
<li style="color: #222; line-height: 1.35em; margin-bottom: 12px; margin-left: 0px !important;">
<p style="margin-top: 0.8em; margin-bottom: 0.8em;"><strong style="font-weight: 600;">HTTP KeepAlive is now enabled by default.</strong> It's always been an option but now it's set to <code>true</code> by default. The default duration is 5 seconds.</p>
</li>
<li style="color: #222; line-height: 1.35em; margin-bottom: 12px; margin-left: 0px !important;">
<p style="margin-top: 0.8em; margin-bottom: 0.8em;"><strong style="font-weight: 600;">V8 10.7.</strong> Node bumps up to the latest version of the V8 engine. It's not a big jump but does introduce <code>Intl.NumberFormat</code>.</p>
</li>
<li style="color: #222; line-height: 1.35em; margin-bottom: 12px; margin-left: 0px !important;">
<p style="margin-top: 0.8em; margin-bottom: 0.8em;">The WebCrypto API is now stable (with the exception of Ed25519, Ed448, X25519, and X448).</p>
</li>
<li style="color: #222; line-height: 1.35em; margin-bottom: 12px; margin-left: 0px !important;">
<p style="margin-top: 0.8em; margin-bottom: 0.8em;">Some other dependency upgrades, such as to npm 8.19.2 and llhttp 8.1.0.</p>
</li>
</ul>
<p style="margin-top: 0.8em; margin-bottom: 0.8em;">As things stand, we're in the odd position of Node 18.x and 19.x <em>both</em> being the 'Current' release, but Node 18 begins its role as an LTS release on October 25. More info in <a href="https://u25184427.ct.sendgrid.net/ls/click?upn=MnmHBiCwIPe9TmIJeskmA7mFdqmsIs-2B5Xs-2FNpSIs56oOd3d9g7syJCuuzhYGU5q0P3DGKSAer-2Flh-2BmEEnII5wA-3D-3DH0Dx_vVXscVLXlj5UtQe3aqo5RMTdTq2PepdZjP86UOmA8nyFE700vlj1-2FK27spZiPEiEiHRRXVZ11Fq4nkMyyEbbC1wA2pRpHWhC4llcA-2BYJ6q9ueHZ017rEmrt3CV1OmltIU9mtUtBHaGAZR5jSE4aE69XdpM-2F0LdDnoHRUT8GnHy-2BfMNXad5w-2Br1ntIspk30NhtUmq4-2BSMewSushKjXWBdHQ-3D-3D" style="text-decoration: none; color: #20824B; font-weight: 600;">the release policies here</a> and the OpenJS Foundation <a href="https://u25184427.ct.sendgrid.net/ls/click?upn=MnmHBiCwIPe9TmIJeskmA7mFdqmsIs-2B5Xs-2FNpSIs56qrMesfWVIK5SSGCZQ08d5nHLQmOP4ATLCQxUZOYB3ciw-3D-3D7mDZ_vVXscVLXlj5UtQe3aqo5RMTdTq2PepdZjP86UOmA8nyFE700vlj1-2FK27spZiPEiE9ONJiDUK-2FDTkiyXGqSRBferEuU50kXhjgqmiycgdeboszCUd5meSODSQD6i94wjUjxydF9vILLHutXJVOrX33LbaUEFZaDkM5ii-2BiUiqkJwoRTA7cN7fDMSrC-2BQDqJfHznVo0Bw4-2Bk1GVBbEuhARxw-3D-3D" style="text-decoration: none; color: #20824B; font-weight: 600;">has extra detail in its release post</a> too.</p>
</td></tr></table>
<!-- normal content section -->
<table width="100%" cellpadding="0" cellspacing="0" class="content el-content " style="color: #222; border-collapse: collapse;"><tr><td style="font-family: -apple-system,BlinkMacSystemFont,Helvetica,sans-serif; font-size: 15px; line-height: 1.48em; border-collapse: collapse; padding-top: 0px; padding-right: 15px; padding-bottom: 0px; padding-left: 15px;"><p style="margin-top: 0; font-size: 0.8em; text-transform: uppercase; color: #999; margin-bottom: 0.8em;">The Node.js Team</p></td></tr></table>
<table width="100%" cellpadding="0" cellspacing="0" class="el-item item " style="border-collapse: collapse; margin-bottom: 4px;"><tr><td style="font-family: -apple-system,BlinkMacSystemFont,Helvetica,sans-serif; font-size: 15px; line-height: 1.48em; border-collapse: collapse; padding-top: 0px; padding-right: 15px; padding-bottom: 0px; padding-left: 15px;">
<a target="_blank" href="https://u25184427.ct.sendgrid.net/ls/click?upn=MnmHBiCwIPe9TmIJeskmA7mFdqmsIs-2B5Xs-2FNpSIs56r6zTNF2xDv1ApolVrZ4iZ0HiftFJvNFqLdyt1qEIYOmw-3D-3DRMCc_vVXscVLXlj5UtQe3aqo5RMTdTq2PepdZjP86UOmA8nyFE700vlj1-2FK27spZiPEiE41hCIBcXkIer6RlARsr61xFJRVEQg0uvdeFx4QkB9sayXkded6W4yvqCkph-2BCeOuA3AS9pg25IkcniP4slO9Ld-2B5D1RLxw95o1VkwLwniSFUyUA2-2F3VpcbAALDBF8GMNgK4X4-2FHyjD4gfRv7nrJP1g-3D-3D" style="text-decoration: none; color: #20824B; border-bottom-width: 1px !important; border-bottom-color: #ddd !important; border-bottom-style: solid !important;"><img src="https://copm.s3.amazonaws.com/f6a5d26d.png" width="95" height="95" style="padding-top: 12px; padding-left: 12px; outline: none; height: auto; text-decoration: none; max-width: 100%; line-height: 100%; border-top-width: 0; border-right-width: 0; border-bottom-width: 0; border-left-width: 0;" align="right" alt="" class="som"></a>
<p class="desc" style="color: #222; margin-top: 0.8em; margin-bottom: 0; line-height: 1.6em !important; font-size: 15px !important;"><span style="font-weight: 500 !important; font-size: 18px !important; color: #000;" class="mainlink"><a href="https://u25184427.ct.sendgrid.net/ls/click?upn=MnmHBiCwIPe9TmIJeskmA7mFdqmsIs-2B5Xs-2FNpSIs56r6zTNF2xDv1ApolVrZ4iZ0HiftFJvNFqLdyt1qEIYOmw-3D-3DiAJ-_vVXscVLXlj5UtQe3aqo5RMTdTq2PepdZjP86UOmA8nyFE700vlj1-2FK27spZiPEiEnTYwb0bfkKt-2BTsRdM05zg1psxx-2B0TUxf-2B5ReFuFSUxaQhVo1mr86cfdMKIPpA-2Bvj5TmeS5N-2By2FMfRISarHe8lYAp3iM-2F2pCCHU89FDlDIbdy8DTU85grvwXdcP0Zj-2BHCC9OH5f1xhfApbr-2BtvTu8A-3D-3D" title="dashboard.memetria.com" style="text-decoration: none; color: #20824B; border-bottom-width: 1px !important; border-bottom-color: #ddd !important; border-bottom-style: solid !important; font-size: 1.1em; line-height: 1.4em;">Memetria: Secure, Scalable, Full-Featured Redis 7 Hosting</a></span> — The latest Redis features, instrumented and scaled with the tools teams need as they grow.</p>
<p class="name" style="color: #aaa !important; margin-top: 4px; margin-bottom: 0.8em; text-transform: uppercase; font-size: 12px; line-height: 1.2em;">Memetria <span style="text-transform: uppercase; margin-left: 4px; font-size: 0.9em; border-radius: 2px; background-color: #ff8; color: #997 !important; padding-top: 1px; padding-right: 4px; padding-bottom: 1px; padding-left: 4px; border-top-width: 1px; border-right-width: 1px; border-bottom-width: 1px; border-left-width: 1px; border-top-color: #eeb; border-right-color: #eeb; border-bottom-color: #eeb; border-left-color: #eeb; border-top-style: solid; border-right-style: solid; border-bottom-style: solid; border-left-style: solid;" class="tag-sponsor">sponsor</span></p>
</td></tr></table>
<table width="100%" cellpadding="0" cellspacing="0" class="el-item item " style="border-collapse: collapse; margin-bottom: 4px;"><tr><td style="font-family: -apple-system,BlinkMacSystemFont,Helvetica,sans-serif; font-size: 15px; line-height: 1.48em; border-collapse: collapse; padding-top: 0px; padding-right: 15px; padding-bottom: 0px; padding-left: 15px;">
<p class="desc" style="color: #222; margin-top: 0.8em; margin-bottom: 0; line-height: 1.6em !important; font-size: 15px !important;"><span style="font-weight: 500 !important; font-size: 18px !important; color: #000;" class="mainlink"><a href="https://u25184427.ct.sendgrid.net/ls/click?upn=MnmHBiCwIPe9TmIJeskmA7mFdqmsIs-2B5Xs-2FNpSIs56oiIcmdO2yLfzc-2B2dmuwutChcZrUvs-2FJsMh-2Fbx4RlOp-2Bg-3D-3DjC_V_vVXscVLXlj5UtQe3aqo5RMTdTq2PepdZjP86UOmA8nyFE700vlj1-2FK27spZiPEiEMbvGDsLC0D1Uxd-2BHK5i6FxjF9-2BDW-2Bm1TOiyFLdO96D4amv1GgN10JVLdZm54hEwMmuudjjakVDPtd4Kfd3pHNl1Nfj1X0YVM-2FkHMmTzl0xGEmk0-2Fc23UENZS-2Fqa-2BYLvjCCfet68lrKA5Slwz-2BqnMQg-3D-3D" title="snyk.io" style="text-decoration: none; color: #20824B; border-bottom-width: 1px !important; border-bottom-color: #ddd !important; border-bottom-style: solid !important;">Choosing the Best Node.js Docker Image</a></span> — If you feel tempted to just throw <code style="border-top-width: 1px; border-right-width: 1px; border-bottom-width: 1px; border-left-width: 1px; border-top-color: #44cc00; border-right-color: #44cc00; border-bottom-color: #44cc00; border-left-color: #44cc00; border-top-style: solid; border-right-style: solid; border-bottom-style: solid; border-left-style: solid; background-color: #fafffa;">FROM node</code> into your Dockerfile, think again there are other options to consider.</p>
<p class="name" style="color: #aaa !important; margin-top: 4px; margin-bottom: 0.8em; text-transform: uppercase; font-size: 12px; line-height: 1.2em;">Liran Tal (Snyk) </p>
</td></tr></table>
<table width="100%" cellpadding="0" cellspacing="0" class="el-item item video" style="border-collapse: collapse; margin-bottom: 4px;"><tr><td style="font-family: -apple-system,BlinkMacSystemFont,Helvetica,sans-serif; font-size: 15px; line-height: 1.48em; border-collapse: collapse; padding-top: 0px; padding-right: 15px; padding-bottom: 0px; padding-left: 15px;">
<p class="desc" style="color: #222; margin-top: 0.8em; margin-bottom: 0; line-height: 1.6em !important; font-size: 15px !important;"><span style="font-weight: 500 !important; font-size: 18px !important; color: #000;" class="mainlink">▶  <a href="https://u25184427.ct.sendgrid.net/ls/click?upn=MnmHBiCwIPe9TmIJeskmA7mFdqmsIs-2B5Xs-2FNpSIs56qEpyR0CkDGEzK0tRuGFlSvvWO8Ztem5Ty7OJokEMfauQ-3D-3DGt24_vVXscVLXlj5UtQe3aqo5RMTdTq2PepdZjP86UOmA8nyFE700vlj1-2FK27spZiPEiECvpY8GWhA5iH5KCmrSjup4rXJ3bXNCQEc61LrXRflOsQgMlzsU73Q6zIKHgsA33IN7O-2FOJVw-2Bt-2BqqguiMxNW096so9eUJ5V9VTD0QNiooeEItxwOhpaF-2F7kIrJp-2BQmv98wUVgdAFuoxruBYE-2FMJahA-3D-3D" title="www.youtube.com" style="text-decoration: none; color: #20824B; border-bottom-width: 1px !important; border-bottom-color: #ddd !important; border-bottom-style: solid !important;">Effortless End-to-End Type-Safety with Phero</a></span> — A demonstration of <a href="https://u25184427.ct.sendgrid.net/ls/click?upn=MnmHBiCwIPe9TmIJeskmA7mFdqmsIs-2B5Xs-2FNpSIs56o7nEYk0ZLSUtFq-2BAYb91c-2B4dnOuqNDG5GMudFLW-2FUDrw-3D-3Dd4VO_vVXscVLXlj5UtQe3aqo5RMTdTq2PepdZjP86UOmA8nyFE700vlj1-2FK27spZiPEiEORUVm-2BZJ2XcN22VonQLtBmuAWao9OZYseUY20aANHVf8CDV3rrpRp9Eqh7hgq3klG3d5QoPMoJi1lHWlqqowV6TXKksTN9N9U4gT4hAcRUwDAs107tUxsi2vSTin7TPPK2rAr8NdN-2FX9WWEU8TYD3g-3D-3D" style="text-decoration: none; color: #20824B; border-bottom-width: 1px !important; border-bottom-color: #ddd !important; border-bottom-style: solid !important;">a library</a> providing a type-safe TypeScript-based way to communicate between frontend and backend. <a href="https://u25184427.ct.sendgrid.net/ls/click?upn=MnmHBiCwIPe9TmIJeskmA7mFdqmsIs-2B5Xs-2FNpSIs56pu57-2FqlCUJj39kXRQgxghrMPmQcqzfjuqdd6mUvMolwA-3D-3DmCHK_vVXscVLXlj5UtQe3aqo5RMTdTq2PepdZjP86UOmA8nyFE700vlj1-2FK27spZiPEiEErCjJ0vDOOc1JpHffSWIUcUkQ0eEvhX9b9dA9cmmA6SMlrbIfNFdBhOrZKijIOLjV62WLI1hkD9SxdPm5kXKxHZURN-2F0wbYkYUuHnGb7k0fuAwEBCj5lhWmPvk0TOMketN0VM5m1o-2FIuA4aNSTkxGQ-3D-3D" style="text-decoration: none; color: #20824B; border-bottom-width: 1px !important; border-bottom-color: #ddd !important; border-bottom-style: solid !important;">GitHub repo</a>.</p>
<p class="name" style="color: #aaa !important; margin-top: 4px; margin-bottom: 0.8em; text-transform: uppercase; font-size: 12px; line-height: 1.2em;">Jasper Haggenburg </p>
</td></tr></table>
<table width="100%" cellpadding="0" cellspacing="0" class="content el-md " style="border-collapse: collapse;"><tr><td style="font-family: -apple-system,BlinkMacSystemFont,Helvetica,sans-serif; font-size: 15px; line-height: 1.48em; border-collapse: collapse; padding-top: 0px; padding-right: 15px; padding-bottom: 0px; padding-left: 15px;">
<p style="margin-top: 0.8em; margin-bottom: 0.8em;"><strong style="font-weight: 600;">IN BRIEF:</strong></p>
<ul>
<li style="color: #222; line-height: 1.35em; margin-bottom: 12px; margin-left: 0px !important;">
<p style="margin-top: 0.8em; margin-bottom: 0.8em;"><a href="https://u25184427.ct.sendgrid.net/ls/click?upn=MnmHBiCwIPe9TmIJeskmA7mFdqmsIs-2B5Xs-2FNpSIs56o5GgJdUG7iJcRdGpnWZ0KvXQBk9obJNHL5RxWsre5iZA-3D-3DXRY7_vVXscVLXlj5UtQe3aqo5RMTdTq2PepdZjP86UOmA8nyFE700vlj1-2FK27spZiPEiEwIwBXpMOErZx-2FqGnNqcQecfKHlhtTIqomF-2FePIShGjHReIDdWcAkCJ6nwLET7n1OZItRIpVN9UKvnmQ5nSahyFl3UHLyX5b-2BaOhkrNypLnq45FWXeCC2C6zSd7J-2FLZTQH91JgT0KaY9ZKKJWsWW7zw-3D-3D" style="text-decoration: none; color: #20824B; font-weight: 600; border-bottom-width: 1px; border-bottom-color: #ddd; border-bottom-style: solid;">Node 18.11.0 (Current)</a> was released last week. It shares Node 19's new <code>--watch</code> feature and Node 18 will become an active LTS release <em>next week.</em></p>
</li>
<li style="color: #222; line-height: 1.35em; margin-bottom: 12px; margin-left: 0px !important;">
<p style="margin-top: 0.8em; margin-bottom: 0.8em;">The OpenJS Foundation has notified us that <a href="https://u25184427.ct.sendgrid.net/ls/click?upn=MnmHBiCwIPe9TmIJeskmA7mFdqmsIs-2B5Xs-2FNpSIs56qUV-2BSh1TLqu4nUC47bRFmODv9-2F8ge44Nxnet2bo-2B9vCQ-3D-3Dxc2B_vVXscVLXlj5UtQe3aqo5RMTdTq2PepdZjP86UOmA8nyFE700vlj1-2FK27spZiPEiEMK0qbk8DOZO0rg-2Fn70aUcETXWQawfUrJRDUa-2FG-2BJh6-2FQ8HHBRPcaX3AHpFhlqf07eaxDpaoeTwzl0Fe5krdfkPsO0RgVCHDEueTASGaM4ZPuIhjM-2B6mCvYm8gRLigIGWdS8hiEZHtj9-2FsAYHKPviag-3D-3D" style="text-decoration: none; color: #20824B; font-weight: 600; border-bottom-width: 1px; border-bottom-color: #ddd; border-bottom-style: solid;">there's a big discount on two Node.js courses and certifications</a> until October 25.</p>
</li>
</ul>
</td></tr></table>
<table width="100%" cellpadding="0" cellspacing="0" class="el-item item " style="border-collapse: collapse; margin-bottom: 4px;"><tr><td style="font-family: -apple-system,BlinkMacSystemFont,Helvetica,sans-serif; font-size: 15px; line-height: 1.48em; border-collapse: collapse; padding-top: 0px; padding-right: 15px; padding-bottom: 0px; padding-left: 15px;">
<p class="desc" style="color: #222; margin-top: 0.8em; margin-bottom: 0; line-height: 1.6em !important; font-size: 15px !important;"><span style="font-weight: 500 !important; font-size: 18px !important; color: #000;" class="mainlink"><a href="https://u25184427.ct.sendgrid.net/ls/click?upn=MnmHBiCwIPe9TmIJeskmA7mFdqmsIs-2B5Xs-2FNpSIs56pgbt-2FySMq6TE09iMZu2f31rvDqLcToe-2F30wzttQjUAzA-3D-3DBjPi_vVXscVLXlj5UtQe3aqo5RMTdTq2PepdZjP86UOmA8nyFE700vlj1-2FK27spZiPEiE7W2Dr5r4L3sT7m5tNPcmGBoMgsI3-2FKxQEmXnfN2eC3AZyuxc9rY3cQvwR3JALkgz9uSi-2Bd-2FhPUlDFIq387uc2dQbrYlm4CLRbnNPzSE-2BcghrotWXVBAdtEVoie99d3Ea2omPTXNoSBsH-2BwYr8Qm1lQ-3D-3D" title="www.lloydatkinson.net" style="text-decoration: none; color: #20824B; border-bottom-width: 1px !important; border-bottom-color: #ddd !important; border-bottom-style: solid !important;">PowerShell, NPM Scripts, and Silently Dropped Arguments</a></span> — If youre a Powershell user and youre finding that some arguments arent being passed to your Node scripts run through <code style="border-top-width: 1px; border-right-width: 1px; border-bottom-width: 1px; border-left-width: 1px; border-top-color: #44cc00; border-right-color: #44cc00; border-bottom-color: #44cc00; border-left-color: #44cc00; border-top-style: solid; border-right-style: solid; border-bottom-style: solid; border-left-style: solid; background-color: #fafffa;">npm run</code>, Lloyd explains whats going on.</p>
<p class="name" style="color: #aaa !important; margin-top: 4px; margin-bottom: 0.8em; text-transform: uppercase; font-size: 12px; line-height: 1.2em;">Lloyd Atkinson </p>
</td></tr></table>
<table width="100%" cellpadding="0" cellspacing="0" class="el-item item video" style="border-collapse: collapse; margin-bottom: 4px;"><tr><td style="font-family: -apple-system,BlinkMacSystemFont,Helvetica,sans-serif; font-size: 15px; line-height: 1.48em; border-collapse: collapse; padding-top: 0px; padding-right: 15px; padding-bottom: 0px; padding-left: 15px;">
<p class="desc" style="color: #222; margin-top: 0.8em; margin-bottom: 0; line-height: 1.6em !important; font-size: 15px !important;"><span style="font-weight: 500 !important; font-size: 18px !important; color: #000;" class="mainlink">▶  <a href="https://u25184427.ct.sendgrid.net/ls/click?upn=MnmHBiCwIPe9TmIJeskmA7mFdqmsIs-2B5Xs-2FNpSIs56r8HVF8il4M7AdNF0iaS22Hww2wbq5ARCcyx3e8SMxYQA-3D-3DEM8X_vVXscVLXlj5UtQe3aqo5RMTdTq2PepdZjP86UOmA8nyFE700vlj1-2FK27spZiPEiEBtqmX6njGwwn4fsBv5dCE6cKsTlCIxFUhw5oKxZGMrNPRwb4mMR5KJMNVPYqt-2BZKeWHrKAGIObaPPga1hxZPWwiGNghddutMyGqNdHxGq3QWRO1DlnDz8R-2BfX6R94odYC4bi7BcZvpA5VCmP25Yuww-3D-3D" title="www.youtube.com" style="text-decoration: none; color: #20824B; border-bottom-width: 1px !important; border-bottom-color: #ddd !important; border-bottom-style: solid !important;">A Next.js Crash Course</a></span> — 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 youre just starting out with it. <em>(2 hours 30 minutes)</em></p>
<p class="name" style="color: #aaa !important; margin-top: 4px; margin-bottom: 0.8em; text-transform: uppercase; font-size: 12px; line-height: 1.2em;">Anson Foong </p>
</td></tr></table>
<table width="100%" cellpadding="0" cellspacing="0" class="el-item item " style="border-collapse: collapse; margin-bottom: 4px;"><tr><td style="font-family: -apple-system,BlinkMacSystemFont,Helvetica,sans-serif; font-size: 15px; line-height: 1.48em; border-collapse: collapse; padding-top: 0px; padding-right: 15px; padding-bottom: 0px; padding-left: 15px;">
<p class="desc" style="color: #222; margin-top: 0.8em; margin-bottom: 0; line-height: 1.6em !important; font-size: 15px !important;"><span style="font-weight: 500 !important; font-size: 18px !important; color: #000;" class="mainlink"><a href="https://u25184427.ct.sendgrid.net/ls/click?upn=MnmHBiCwIPe9TmIJeskmA7mFdqmsIs-2B5Xs-2FNpSIs56pJ8YlJIJ9WvtJBwvcosmZf31RLtinTKHpBKyBhSUvorQ-3D-3D59Oa_vVXscVLXlj5UtQe3aqo5RMTdTq2PepdZjP86UOmA8nyFE700vlj1-2FK27spZiPEiEi9roa4mmLsgNtqOzeNn01J1YAbtCNH-2B8aloOjPK88QUP7vc4LZx1D8-2B-2FB5yK6nNGPtm0UNeY5OCRmaCPZZz4DJQ3FZOMx0PT6NkCjvx5JGa7EvcYdYvbrIW3nSigx3-2FaNVARy9iDf0LSdGd4Y89Tmw-3D-3D" title="serpdog.io" style="text-decoration: none; color: #20824B; border-bottom-width: 1px !important; border-bottom-color: #ddd !important; border-bottom-style: solid !important;">Web Scraping Google Maps with Puppeteer</a></span> — Wed be surprised if youd get away with this for long given theres an official API, but its always interesting to see how its done.</p>
<p class="name" style="color: #aaa !important; margin-top: 4px; margin-bottom: 0.8em; text-transform: uppercase; font-size: 12px; line-height: 1.2em;">Darshan Khandelwal </p>
</td></tr></table>
<table width="100%" cellpadding="0" cellspacing="0" class="el-item item " style="border-collapse: collapse; margin-bottom: 4px;"><tr><td style="font-family: -apple-system,BlinkMacSystemFont,Helvetica,sans-serif; font-size: 15px; line-height: 1.48em; border-collapse: collapse; padding-top: 0px; padding-right: 15px; padding-bottom: 0px; padding-left: 15px;">
<p class="desc" style="color: #222; margin-top: 0.8em; margin-bottom: 0; line-height: 1.6em !important; font-size: 15px !important;"><span style="font-weight: 500 !important; font-size: 18px !important; color: #000;" class="mainlink"><a href="https://u25184427.ct.sendgrid.net/ls/click?upn=MnmHBiCwIPe9TmIJeskmA7mFdqmsIs-2B5Xs-2FNpSIs56oZg-2FkiLgU2V8f4woEYibxJHD8uLrNzPXOs1Ag1vo6JFQ-3D-3DzHHQ_vVXscVLXlj5UtQe3aqo5RMTdTq2PepdZjP86UOmA8nyFE700vlj1-2FK27spZiPEiEb4ODrRNEvsB4u6ffP-2BaP1XqgSFWAga6Slj8yjWtDlzkdrHRHZWYvQB4jtY1dirZvLRoO5bOBJWU-2FOYF-2BVRp9bQLRSzvFYzsJa-2FyZmh7NbOAwC5USrjB2dD9bW53LEQ-2BCNBjeqKZYeIW6OC4EbAU2wQ-3D-3D" title="snyk.io" style="text-decoration: none; color: #20824B; border-bottom-width: 1px !important; border-bottom-color: #ddd !important; border-bottom-style: solid !important;">Your Step by Step Guide to Containerizing Node.js Web Applications</a></span></p>
<p class="name" style="color: #aaa !important; margin-top: 4px; margin-bottom: 0.8em; text-transform: uppercase; font-size: 12px; line-height: 1.2em;">Snyk <span style="text-transform: uppercase; margin-left: 4px; font-size: 0.9em; border-radius: 2px; background-color: #ff8; color: #997 !important; padding-top: 1px; padding-right: 4px; padding-bottom: 1px; padding-left: 4px; border-top-width: 1px; border-right-width: 1px; border-bottom-width: 1px; border-left-width: 1px; border-top-color: #eeb; border-right-color: #eeb; border-bottom-color: #eeb; border-left-color: #eeb; border-top-style: solid; border-right-style: solid; border-bottom-style: solid; border-left-style: solid;" class="tag-sponsor">sponsor</span></p>
</td></tr></table>
<table width="100%" cellpadding="0" cellspacing="0" class="miniitem item " style="border-collapse: collapse; margin-bottom: 4px;"><tr><td style="font-family: -apple-system,BlinkMacSystemFont,Helvetica,sans-serif; font-size: 15px; line-height: 1.48em; border-collapse: collapse; padding-top: 0px; padding-right: 15px; padding-bottom: 0px; padding-left: 15px;">
<p class="desc" style="color: #5a5a5a; line-height: 1.5em !important; font-size: 0.9em !important; margin-top: 8px; margin-right: 0px; margin-bottom: 6px; margin-left: 0px;"><span style="font-weight: 600; font-size: 1.0em; color: #000;"><a href="https://u25184427.ct.sendgrid.net/ls/click?upn=MnmHBiCwIPe9TmIJeskmA7mFdqmsIs-2B5Xs-2FNpSIs56pbGJPd9-2Fk9QMCO9UqoTzlbeOrMrbj6gcPiBzVy3BuW8g-3D-3DMHs5_vVXscVLXlj5UtQe3aqo5RMTdTq2PepdZjP86UOmA8nyFE700vlj1-2FK27spZiPEiEQY9mKcMppAe8QFvMPKT3WKSEU7aO7GXRWkrX4UTIPKvaP7t30qqjsvx7Tj6dz5MV5BDR1m9IjyX0LYtNjEqGMZmomHOfZi0Jn2joYlUro7UkBlPBC-2BChB1aYflkjYpZcmbHbjzL2jJP3ar9ds38xLw-3D-3D" style="text-decoration: none; color: #20824B; font-size: 1.2em !important; font-weight: 400; border-bottom-width: 1px; border-bottom-color: #ddd; border-bottom-style: solid;">Sending UDP Messages without DNS Lookups</a></span>
<br><span class="name" style="color: #bbbbbb !important; margin-top: 4px; text-transform: uppercase; font-size: 12px; line-height: 1.2em;">Herman J. Radtke III</span>
</p>
</td></tr></table>
<table width="100%" cellpadding="0" cellspacing="0" class="miniitem item " style="border-collapse: collapse; margin-bottom: 4px;"><tr><td style="font-family: -apple-system,BlinkMacSystemFont,Helvetica,sans-serif; font-size: 15px; line-height: 1.48em; border-collapse: collapse; padding-top: 0px; padding-right: 15px; padding-bottom: 0px; padding-left: 15px;">
<p class="desc" style="color: #5a5a5a; line-height: 1.5em !important; font-size: 0.9em !important; margin-top: 8px; margin-right: 0px; margin-bottom: 6px; margin-left: 0px;"><span style="font-weight: 600; font-size: 1.0em; color: #000;"><a href="https://u25184427.ct.sendgrid.net/ls/click?upn=MnmHBiCwIPe9TmIJeskmA7mFdqmsIs-2B5Xs-2FNpSIs56qnkx7zCeqBic9p68wBkQfUE-2BzMKZCr0iiPuvv3WyCOuw-3D-3Dz1E7_vVXscVLXlj5UtQe3aqo5RMTdTq2PepdZjP86UOmA8nyFE700vlj1-2FK27spZiPEiEQTNbbCohlDVOQmzdJrngdW81lDdWUUXzVPWff4zPdFL7O2WTlvJ-2BHBW1Q0OLzA-2Bai-2B8wLqjgbdqMwAEvbr6PmYGGX80JegH-2Fl5kh-2FI44f-2BIkEc56lCLRw88HBpyJ3BGUtbWvKJK0263ue2bv8m28hg-3D-3D" style="text-decoration: none; color: #20824B; font-size: 1.2em !important; font-weight: 400; border-bottom-width: 1px; border-bottom-color: #ddd; border-bottom-style: solid;">How Wix Uses Threading in Node Apps to Cut Kubernetes Pod Costs</a></span>
<br><span class="name" style="color: #bbbbbb !important; margin-top: 4px; text-transform: uppercase; font-size: 12px; line-height: 1.2em;">Jessica Wachtel (The New Stack)</span>
</p>
</td></tr></table>
<table width="100%" cellpadding="0" cellspacing="0" style="border-collapse: collapse;"><tr><td style="height: 6px; font-family: -apple-system,BlinkMacSystemFont,Helvetica,sans-serif; font-size: 15px; line-height: 1.48em; border-collapse: collapse;"></td></tr></table>
<!-- normal content section -->
<table width="100%" cellpadding="0" cellspacing="0" class="content el-content " style="color: #222; border-collapse: collapse;"><tr><td style="font-family: -apple-system,BlinkMacSystemFont,Helvetica,sans-serif; font-size: 15px; line-height: 1.48em; border-collapse: collapse; padding-top: 0px; padding-right: 15px; padding-bottom: 0px; padding-left: 15px;"><p style="color: #46483d; font-size: 1.4em; margin-top: 14px; font-weight: 600; border-bottom-width: 4px; border-bottom-color: #eeeeee; border-bottom-style: solid; line-height: 1.4em; display: inline-block; margin-bottom: 10px;">🛠 Code &amp; Tools</p></td></tr></table>
<table width="100%" class="el-fullwidthimage " cellpadding="0" cellspacing="0" style="border-collapse: collapse;"><tr><td style="font-family: -apple-system,BlinkMacSystemFont,Helvetica,sans-serif; font-size: 15px; line-height: 1.48em; border-collapse: collapse;">
<a href="https://u25184427.ct.sendgrid.net/ls/click?upn=MnmHBiCwIPe9TmIJeskmA7mFdqmsIs-2B5Xs-2FNpSIs56obUL9N28GQk2jDIabY38rWiGYPofllyLLFcoRk-2B6PyWw-3D-3DZwDA_vVXscVLXlj5UtQe3aqo5RMTdTq2PepdZjP86UOmA8nyFE700vlj1-2FK27spZiPEiEMxB1Ex0Rb9NunayuZXgdiOXJ5q77sbWlJ-2F0LBVwVIwuKxKtNmoRQYO7JObxlRKuFP1wX0FK1s4QJhigO7-2BmaWj58uTiGF1Tc4FtQ8n9QBEippmidDWfpRJGpPoK0yVl4Q2absvQjm7l5xhGvO2qSPQ-3D-3D" style="text-decoration: none; color: #20824B;"><img src="https://res.cloudinary.com/cpress/image/upload/w_1280,e_sharpen:60,q_auto/ny0bnuedwfudc7fhqojv.jpg" alt="" width="640" style="border-top-color: #dddddd; border-top-style: solid; border-bottom-color: #dddddd; border-bottom-style: solid; outline: none; height: auto; text-decoration: none; max-width: 100%; line-height: 100%; width: 100%; border-top-width: 3px; border-right-width: 0; border-bottom-width: 3px; border-left-width: 0;"></a>
</td></tr></table>
<table width="100%" cellpadding="0" cellspacing="0" style="border-collapse: collapse;"><tr><td style="height: 2px; font-family: -apple-system,BlinkMacSystemFont,Helvetica,sans-serif; font-size: 15px; line-height: 1.48em; border-collapse: collapse;"></td></tr></table>
<table width="100%" cellpadding="0" cellspacing="0" class="el-item item " style="border-collapse: collapse; margin-bottom: 4px;"><tr><td style="font-family: -apple-system,BlinkMacSystemFont,Helvetica,sans-serif; font-size: 15px; line-height: 1.48em; border-collapse: collapse; padding-top: 0px; padding-right: 15px; padding-bottom: 0px; padding-left: 15px;">
<p class="desc" style="color: #222; margin-top: 0.8em; margin-bottom: 0; line-height: 1.6em !important; font-size: 15px !important;"><span style="font-weight: 500 !important; font-size: 18px !important; color: #000;" class="mainlink"><a href="https://u25184427.ct.sendgrid.net/ls/click?upn=MnmHBiCwIPe9TmIJeskmA7mFdqmsIs-2B5Xs-2FNpSIs56obUL9N28GQk2jDIabY38rWiGYPofllyLLFcoRk-2B6PyWw-3D-3DVv04_vVXscVLXlj5UtQe3aqo5RMTdTq2PepdZjP86UOmA8nyFE700vlj1-2FK27spZiPEiEYJooQOIMS3B1eG259-2BgO711KOnAMlYa4vQ-2BFjil81wOKdBTc-2Bpy5XEmhlSm2qnFiqILBkZwp77Yq-2BJnHdRKzQwQQHUU0WAsYg2fdPMhFz1fSto5-2FclJqiiL-2Fii4J-2FEiXuq0A-2BQfOQP9HvuXI9KUgaw-3D-3D" title="www.caoccao.com" style="text-decoration: none; color: #20824B; border-bottom-width: 1px !important; border-bottom-color: #ddd !important; border-bottom-style: solid !important;">Javet 2.0.0: Embed Node and V8 in Java Apps</a></span> — Lets you spin up V8 interpreters or full Node.js runtimes within JVM-based apps. Theres <a href="https://u25184427.ct.sendgrid.net/ls/click?upn=MnmHBiCwIPe9TmIJeskmA7mFdqmsIs-2B5Xs-2FNpSIs56qZBRT-2FC8gRDWONKGBZP-2FrlOzhcUwLUeh60If-2Fad1MFaQ-3D-3Dpuu8_vVXscVLXlj5UtQe3aqo5RMTdTq2PepdZjP86UOmA8nyFE700vlj1-2FK27spZiPEiEmjagHflkwe3kz5g7rZVtxY-2Bsq8pKK60-2BhYFt-2FTKjJA-2Br0ujFC2ySkSjtyQODkd3NJxOEJ07XfGIe-2BDRSStsKnl7a54dJfIZnoLKEvyLv8uyEoDd5XBKVpCSznYme31Q-2BW2IEDUCiybcDFjAGtPsmYQ-3D-3D" style="text-decoration: none; color: #20824B; border-bottom-width: 1px !important; border-bottom-color: #ddd !important; border-bottom-style: solid !important;">a slide presentation</a> to sell you on the idea and demonstrate how the integration works. <em>(The name Javet comes from Java, V, and Eight.)</em></p>
<p class="name" style="color: #aaa !important; margin-top: 4px; margin-bottom: 0.8em; text-transform: uppercase; font-size: 12px; line-height: 1.2em;">Sam Cao </p>
</td></tr></table>
<table width="100%" cellpadding="0" cellspacing="0" class="el-item item " style="border-collapse: collapse; margin-bottom: 4px;"><tr><td style="font-family: -apple-system,BlinkMacSystemFont,Helvetica,sans-serif; font-size: 15px; line-height: 1.48em; border-collapse: collapse; padding-top: 0px; padding-right: 15px; padding-bottom: 0px; padding-left: 15px;">
<p class="desc" style="color: #222; margin-top: 0.8em; margin-bottom: 0; line-height: 1.6em !important; font-size: 15px !important;"><span style="font-weight: 500 !important; font-size: 18px !important; color: #000;" class="mainlink"><a href="https://u25184427.ct.sendgrid.net/ls/click?upn=MnmHBiCwIPe9TmIJeskmA7mFdqmsIs-2B5Xs-2FNpSIs56pmi-2Foyxl43aHHHBd8Ih7RZN0-2FFTRrhuA2A2W-2BW4nCe2w-3D-3DaqiY_vVXscVLXlj5UtQe3aqo5RMTdTq2PepdZjP86UOmA8nyFE700vlj1-2FK27spZiPEiEaOinXu-2B4F8eHBhAJ1pAxJmXAdeQJYH6dSqx39Rkgb6xv3uD06e7MQXFuPzjBKRySIAd08x9nmzuPI2po-2B6K-2BggdtQAbL0Gq9-2F4CMnlAhiDRxk-2FrnQe7D9bRTNTwj8llKlbzXk380RUsAtPkI5-2FIyaA-3D-3D" title="github.com" style="text-decoration: none; color: #20824B; border-bottom-width: 1px !important; border-bottom-color: #ddd !important; border-bottom-style: solid !important;">Editly 0.14.0: Declarative Command Line Video Editing</a></span> — Brings Node and FFmpeg together to let you more programatically edit and construct videos instead of wrangling with arcane <code style="border-top-width: 1px; border-right-width: 1px; border-bottom-width: 1px; border-left-width: 1px; border-top-color: #44cc00; border-right-color: #44cc00; border-bottom-color: #44cc00; border-left-color: #44cc00; border-top-style: solid; border-right-style: solid; border-bottom-style: solid; border-left-style: solid; background-color: #fafffa;">ffmpeg</code> command line options.</p>
<p class="name" style="color: #aaa !important; margin-top: 4px; margin-bottom: 0.8em; text-transform: uppercase; font-size: 12px; line-height: 1.2em;">Mikael Finstad </p>
</td></tr></table>
<table width="100%" cellpadding="0" cellspacing="0" class="el-item item " style="border-collapse: collapse; margin-bottom: 4px;"><tr><td style="font-family: -apple-system,BlinkMacSystemFont,Helvetica,sans-serif; font-size: 15px; line-height: 1.48em; border-collapse: collapse; padding-top: 0px; padding-right: 15px; padding-bottom: 0px; padding-left: 15px;">
<p class="desc" style="color: #222; margin-top: 0.8em; margin-bottom: 0; line-height: 1.6em !important; font-size: 15px !important;"><span style="font-weight: 500 !important; font-size: 18px !important; color: #000;" class="mainlink"><a href="https://u25184427.ct.sendgrid.net/ls/click?upn=MnmHBiCwIPe9TmIJeskmA7mFdqmsIs-2B5Xs-2FNpSIs56qDmTAnhDeqHG5g4bhdOnOl-2Br9ql0hDRnWc0zkvjvOBag-3D-3DFzJy_vVXscVLXlj5UtQe3aqo5RMTdTq2PepdZjP86UOmA8nyFE700vlj1-2FK27spZiPEiEwFgMBPol2y4XWfrJGPDuKTB8sPjlanyrhwLnjzAq3uUm2XqJGoJ05wcauSDf9VM1xOqCY4kQSrepo0APylG1GdmOU-2FCBrymMr4HFU-2F0YIQ-2FdogieX3DdViTGMEDWRaCPbN3dru9lEPA7GvF6d4CNKA-3D-3D" title="www.courier.com" style="text-decoration: none; color: #20824B; border-bottom-width: 1px !important; border-bottom-color: #ddd !important; border-bottom-style: solid !important;">Send Email, Push and SMS with Smart Routing, with Just 8 Lines of Code</a></span> — Are you stuck using marketing tools like salesforce to contact your users? Send notifications from right within your application using the Courier API.</p>
<p class="name" style="color: #aaa !important; margin-top: 4px; margin-bottom: 0.8em; text-transform: uppercase; font-size: 12px; line-height: 1.2em;">Courier.com <span style="text-transform: uppercase; margin-left: 4px; font-size: 0.9em; border-radius: 2px; background-color: #ff8; color: #997 !important; padding-top: 1px; padding-right: 4px; padding-bottom: 1px; padding-left: 4px; border-top-width: 1px; border-right-width: 1px; border-bottom-width: 1px; border-left-width: 1px; border-top-color: #eeb; border-right-color: #eeb; border-bottom-color: #eeb; border-left-color: #eeb; border-top-style: solid; border-right-style: solid; border-bottom-style: solid; border-left-style: solid;" class="tag-sponsor">sponsor</span></p>
</td></tr></table>
<table width="100%" cellpadding="0" cellspacing="0" class="miniitem item " style="border-collapse: collapse; margin-bottom: 4px;"><tr><td style="font-family: -apple-system,BlinkMacSystemFont,Helvetica,sans-serif; font-size: 15px; line-height: 1.48em; border-collapse: collapse; padding-top: 0px; padding-right: 15px; padding-bottom: 0px; padding-left: 15px;">
<p class="desc" style="color: #5a5a5a; line-height: 1.5em !important; font-size: 0.9em !important; margin-top: 8px; margin-right: 0px; margin-bottom: 6px; margin-left: 0px;"><span style="font-weight: 600; font-size: 1.0em; color: #000;"><a href="https://u25184427.ct.sendgrid.net/ls/click?upn=MnmHBiCwIPe9TmIJeskmA7mFdqmsIs-2B5Xs-2FNpSIs56oi4WBOWKYkFSHAV9UWnU66V2qF9pwTUxOY-2BtVkmsVhqw-3D-3DQqbG_vVXscVLXlj5UtQe3aqo5RMTdTq2PepdZjP86UOmA8nyFE700vlj1-2FK27spZiPEiEmcF7sRkm-2FtgysFoOOaCMx7ERrB-2FV-2BU9kXphPEkd5FzKU0XWQBCaJ3oPGFJnxLnlywOr-2BAjLFHmoGpmwjOQgV5tSMzfn4k46mR4q8h3VbgkqSu5-2B9xfKf1-2BzkRu-2B7NPLVHMb1y-2FzDWwGxXVAz6XbEjg-3D-3D" style="text-decoration: none; color: #20824B; font-size: 1.2em !important; font-weight: 400; border-bottom-width: 1px; border-bottom-color: #ddd; border-bottom-style: solid;">lady-gg: Simple TypeScript gRPC Client</a></span>
<br><span class="name" style="color: #bbbbbb !important; margin-top: 4px; text-transform: uppercase; font-size: 12px; line-height: 1.2em;">Mish Ushakov</span>
</p>
</td></tr></table>
<table width="100%" cellpadding="0" cellspacing="0" class="content el-md flat" style="border-collapse: collapse;"><tr><td style="font-family: -apple-system,BlinkMacSystemFont,Helvetica,sans-serif; font-size: 15px; line-height: 1.48em; border-collapse: collapse; padding-top: 0px; padding-right: 15px; padding-bottom: 0px; padding-left: 15px;">
<ul style="padding-left: 0; margin-left: 0px; list-style-type: none; list-style-position: inside;">
<li style="color: #222; line-height: 1.35em; margin-bottom: 12px; margin-left: 0px !important;">
<p style="margin-top: 0.8em; margin-bottom: 0.8em;"><a href="https://u25184427.ct.sendgrid.net/ls/click?upn=MnmHBiCwIPe9TmIJeskmA7mFdqmsIs-2B5Xs-2FNpSIs56qOeR8dkPkAQ0Jc3M1LXMsC2n-2B4DdaMaRDxoanihfYO4Q-3D-3DlftC_vVXscVLXlj5UtQe3aqo5RMTdTq2PepdZjP86UOmA8nyFE700vlj1-2FK27spZiPEiEHpVuV0GjA3o-2BrwQOX0yXSY3B5t4FT9Hc3pZG70HAX-2BIVsZwXerWCC7PBmaQEbu-2FnK9MczZAUIomrs50pGNGJkpiQ233VQn1njoXpwSh3W9Dd-2BnVr4l8cVJLI9Yf9FRcQq5YhDrHkNmBLtUka9LlxzQ-3D-3D" style="text-decoration: none; color: #20824B; font-weight: 600; border-bottom-width: 1px; border-bottom-color: #ddd; border-bottom-style: solid;">Awilix 8.0</a><br>
↳ Inversion of Control (IoC) container for Node.</p>
</li>
<li style="color: #222; line-height: 1.35em; margin-bottom: 12px; margin-left: 0px !important;">
<p style="margin-top: 0.8em; margin-bottom: 0.8em;"><a href="https://u25184427.ct.sendgrid.net/ls/click?upn=MnmHBiCwIPe9TmIJeskmA7mFdqmsIs-2B5Xs-2FNpSIs56rvyEVtR04BspjpessX7NIiAFfQ-2FNALchpdiWJ9GoGxPg-3D-3DAzJx_vVXscVLXlj5UtQe3aqo5RMTdTq2PepdZjP86UOmA8nyFE700vlj1-2FK27spZiPEiEEGs-2FbNKfalgc1llqVE3T47xA8AMsJvDfWz8SHuluBer7ZwbLAnGKohGXwU4EqODaDd9gBUEiuInU8o2JJLg8jyCoSPsqdoo-2Bo3IS-2FYjaJFNq84QO33QKs-2Bm9ojOMUeB40b0HjTrK0V8UHLlSvzKiBA-3D-3D" style="text-decoration: none; color: #20824B; font-weight: 600; border-bottom-width: 1px; border-bottom-color: #ddd; border-bottom-style: solid;">Nx 15.0</a><br>
↳ Smart, fast and extensible build system.</p>
</li>
<li style="color: #222; line-height: 1.35em; margin-bottom: 12px; margin-left: 0px !important;">
<p style="margin-top: 0.8em; margin-bottom: 0.8em;"><a href="https://u25184427.ct.sendgrid.net/ls/click?upn=MnmHBiCwIPe9TmIJeskmA7mFdqmsIs-2B5Xs-2FNpSIs56rf0Y-2FoqotQ2mSPow42EISzYgNxsQXct6G8q7K8DEHQPA-3D-3DRgnU_vVXscVLXlj5UtQe3aqo5RMTdTq2PepdZjP86UOmA8nyFE700vlj1-2FK27spZiPEiERFHUm1paIHDjg5a7FnrOZZjbGb7mCWra1LOKrLsF6FvXb0h-2BnY7PAHXfSKeURuF7QxPIbufWEEZHSbmnUTK8mmrIcQP6VAksbqED6gED426rmi-2F-2Bq5K9pfnc2XzGDzRJ4DeVx4g-2BBw9HfBlsLO14bA-3D-3D" style="text-decoration: none; color: #20824B; font-weight: 600; border-bottom-width: 1px; border-bottom-color: #ddd; border-bottom-style: solid;">Nightwatch 2.4</a><br>
↳ End-to-end testing framework, now with improved component testing support.</p>
</li>
<li style="color: #222; line-height: 1.35em; margin-bottom: 12px; margin-left: 0px !important;">
<p style="margin-top: 0.8em; margin-bottom: 0.8em;"><a href="https://u25184427.ct.sendgrid.net/ls/click?upn=MnmHBiCwIPe9TmIJeskmA7mFdqmsIs-2B5Xs-2FNpSIs56pgNlYuMEBloKyCCGedODriBujo6avReiHzL6GJEVoEvA-3D-3DtBzD_vVXscVLXlj5UtQe3aqo5RMTdTq2PepdZjP86UOmA8nyFE700vlj1-2FK27spZiPEiEfo20fupbQYUe8XDME2riwHic86vKaZD3HH96n49T-2FlC3N4PtncOiGyu2lrzEJaPZJ4-2BBx96O2a5pGwY5ZEOhhJX-2BZ4eLGEfBTiuY45RRsQZi9NfektirJd7sYicWxhYGRVGpyWXm6a-2BEZq9Gtm23xA-3D-3D" style="text-decoration: none; color: #20824B; font-weight: 600; border-bottom-width: 1px; border-bottom-color: #ddd; border-bottom-style: solid;">lowdb 4.0</a><br>
↳ Simple to use local JSON database.</p>
</li>
<li style="color: #222; line-height: 1.35em; margin-bottom: 12px; margin-left: 0px !important;">
<p style="margin-top: 0.8em; margin-bottom: 0.8em;"><a href="https://u25184427.ct.sendgrid.net/ls/click?upn=MnmHBiCwIPe9TmIJeskmA7mFdqmsIs-2B5Xs-2FNpSIs56qQk-2BpW7foBNGey3rLzMuDo62zZWbrczt015Bj1WlNDBQ-3D-3Dm_3Q_vVXscVLXlj5UtQe3aqo5RMTdTq2PepdZjP86UOmA8nyFE700vlj1-2FK27spZiPEiEsvu6H9oknetn8jsJ2NuiZQd55Mea4qpyKMLbDkN-2BvTC-2FBd-2BWdFatd2Xrl-2BRFWG77QEtsRvdmm8717A0xsHaVh6tNDkLpo6aSpcmVSbIPOX5ook72haTTqHSXZO-2BHmgmhRwEpbRLMMMqVZkapq2gSMA-3D-3D" style="text-decoration: none; color: #20824B; font-weight: 600; border-bottom-width: 1px; border-bottom-color: #ddd; border-bottom-style: solid;">Prisma 4.5</a><br>
↳ Next-generation ORM. There's <a href="https://u25184427.ct.sendgrid.net/ls/click?upn=MnmHBiCwIPe9TmIJeskmA7mFdqmsIs-2B5Xs-2FNpSIs56qQk-2BpW7foBNGey3rLzMuDo62zZWbrczt015Bj1WlNDBQ-3D-3Dzxz3_vVXscVLXlj5UtQe3aqo5RMTdTq2PepdZjP86UOmA8nyFE700vlj1-2FK27spZiPEiEjYQc5ZwzDq6kBdevLrTkqLe1i3-2BTFdcCZxtr66gQyhRf-2BpynzjpmMMtda3K7MVyvOhoQG4k4KqM-2FwzqcxtIoNHfF-2F3kvn73uKwg2z88wcmD0RTrbjCK1CTtcw6qGZ7KMo7GGbkR-2FT3ju57ULscV0AA-3D-3D" style="text-decoration: none; color: #20824B; font-weight: 600; border-bottom-width: 1px; border-bottom-color: #ddd; border-bottom-style: solid;">a lot new here.</a></p>
</li>
<li style="color: #222; line-height: 1.35em; margin-bottom: 12px; margin-left: 0px !important;">
<p style="margin-top: 0.8em; margin-bottom: 0.8em;"><a href="https://u25184427.ct.sendgrid.net/ls/click?upn=MnmHBiCwIPe9TmIJeskmA7mFdqmsIs-2B5Xs-2FNpSIs56rSNyns-2FbWD08foeryG3PRyYFxJow9Y-2FQxDypnkYk9RxA-3D-3DQqJ4_vVXscVLXlj5UtQe3aqo5RMTdTq2PepdZjP86UOmA8nyFE700vlj1-2FK27spZiPEiEfFX6mz7iCRhL8yCQ2Y7vvv5MBMI-2BSSvYTXJf4BBNL8m-2F5w7VbqSEblu4KLM0cpHtYT9h96A-2Fq-2FkS7ajK-2BnI6iHYTYyY9mzuG5jOFIz563u-2Ftu1RxaPaI5gzf7M-2FduSlja8cPcz8z-2B9bww-2FxpBYibDA-3D-3D" style="text-decoration: none; color: #20824B; font-weight: 600; border-bottom-width: 1px; border-bottom-color: #ddd; border-bottom-style: solid;">PSD 0.3</a><br>
↳ Zero-dependency PSD/Photoshop file parser.</p>
</li>
<li style="color: #222; line-height: 1.35em; margin-bottom: 12px; margin-left: 0px !important;">
<p style="margin-top: 0.8em; margin-bottom: 0.8em;"><a href="https://u25184427.ct.sendgrid.net/ls/click?upn=MnmHBiCwIPe9TmIJeskmA7mFdqmsIs-2B5Xs-2FNpSIs56qk1TR1e5JGImoRII3FmCcfLMz14acCxyGpPNCcSSCeFg-3D-3D6S8P_vVXscVLXlj5UtQe3aqo5RMTdTq2PepdZjP86UOmA8nyFE700vlj1-2FK27spZiPEiED-2FFw5PhVn2VvXv5tuIgwfjRpdamvTvVOw3guLR3fUWKFse57-2BDwa-2BZay0G8uZBhzL5yF5iiPKHG1-2FbXLhCC2vaXisfDfVUb2euWpE769d93Lap8NPUHBvm6iC7LgnfcFKcaiYkh4mXIFdoB8ONURlQ-3D-3D" style="text-decoration: none; color: #20824B; font-weight: 600; border-bottom-width: 1px; border-bottom-color: #ddd; border-bottom-style: solid;">fdir 5.3</a><br>
↳ Performance-oriented directory crawler and globbing library.</p>
</li>
<li style="color: #222; line-height: 1.35em; margin-bottom: 12px; margin-left: 0px !important;">
<p style="margin-top: 0.8em; margin-bottom: 0.8em;"><a href="https://u25184427.ct.sendgrid.net/ls/click?upn=MnmHBiCwIPe9TmIJeskmA7mFdqmsIs-2B5Xs-2FNpSIs56rvrBGWXh8OvpamaF-2F9XMSlJHqCP4LlTvUqxgT-2BRA7vrw-3D-3DbaYf_vVXscVLXlj5UtQe3aqo5RMTdTq2PepdZjP86UOmA8nyFE700vlj1-2FK27spZiPEiEbZGielvStiDu5-2BXJ4OTyxG-2BNgh-2Fx70o5ouvVAZEExNV2xLG7Q2rvu1h0VfEIh4VAADpnbn0m4y-2Bhh8xVuySBV8bL5rz25sdMElmQmu8DRiL8sR3WKzRrbRBAWF-2BB3KeFndvfWxJTUwojd8AlucJIXQ-3D-3D" style="text-decoration: none; color: #20824B; font-weight: 600; border-bottom-width: 1px; border-bottom-color: #ddd; border-bottom-style: solid;">google-translate 2.0</a><br>
↳ Consume Google's <em>Translate API.</em></p>
</li>
<li style="color: #222; line-height: 1.35em; margin-bottom: 12px; margin-left: 0px !important;">
<p style="margin-top: 0.8em; margin-bottom: 0.8em;"><a href="https://u25184427.ct.sendgrid.net/ls/click?upn=MnmHBiCwIPe9TmIJeskmA7mFdqmsIs-2B5Xs-2FNpSIs56pqtvru3vdBpFR9D5FHRZdjLjVCbzNTC6R5tpIZaf77WQ-3D-3D8dq9_vVXscVLXlj5UtQe3aqo5RMTdTq2PepdZjP86UOmA8nyFE700vlj1-2FK27spZiPEiEgqafHY6jFbdTSvNleP6DWWCt4KFWx-2F41A-2BwoIA5LJA2-2FX32uUZpYZTIxnqs77b2IDe0HWMFiZ2ItWb4p25-2F-2B2y8v6L1QEXV-2Bg9d-2Bp7hfqWawGJnRkvqUFBfpg0og6ijGcMgIvH4i3rWQkRVjKgTmQg-3D-3D" style="text-decoration: none; color: #20824B; font-weight: 600; border-bottom-width: 1px; border-bottom-color: #ddd; border-bottom-style: solid;">Mercurius 11.1</a><br>
↳ Implement GraphQL servers with Fastify.</p>
</li>
<li style="color: #222; line-height: 1.35em; margin-bottom: 12px; margin-left: 0px !important;">
<p style="margin-top: 0.8em; margin-bottom: 0.8em;"><a href="https://u25184427.ct.sendgrid.net/ls/click?upn=MnmHBiCwIPe9TmIJeskmA7mFdqmsIs-2B5Xs-2FNpSIs56riH7Sj-2BEdAeaoRtj7QagB2G8aO8QqwKJkSIhJ96LGiGA-3D-3DI6w2_vVXscVLXlj5UtQe3aqo5RMTdTq2PepdZjP86UOmA8nyFE700vlj1-2FK27spZiPEiEwmQ4-2FnGqIgbgzSx0ws5zCsrkxFSbWG8rUm5Lnx4jCJsEvlO-2BRHt0nYEf8B6Sol6shjB3b7EmGk-2Bk1njKf3tu1RhXs6g9cVnqfhB8ISfhPwdDStqDRVQQCcsuOHtbLOpxEgx8Nbz6Ia-2FfxHPVaw7-2F2Q-3D-3D" style="text-decoration: none; color: #20824B; font-weight: 600; border-bottom-width: 1px; border-bottom-color: #ddd; border-bottom-style: solid;">Mongoist 2.5.6</a><br>
↳ MongoDB driver built with <code>async</code>/<code>await</code> in mind.</p>
</li>
<li style="color: #222; line-height: 1.35em; margin-bottom: 12px; margin-left: 0px !important;">
<p style="margin-top: 0.8em; margin-bottom: 0.8em;"><a href="https://u25184427.ct.sendgrid.net/ls/click?upn=MnmHBiCwIPe9TmIJeskmA7mFdqmsIs-2B5Xs-2FNpSIs56p-2Bvcoz4gSZMRw1zLNJmqpVZZpuFsV-2FeU0Qpx5T6NarKQ-3D-3D0k4f_vVXscVLXlj5UtQe3aqo5RMTdTq2PepdZjP86UOmA8nyFE700vlj1-2FK27spZiPEiEo4tWAkAnpPnGfOF-2FxNkDvuN7BJXTYUf8CxreuKsPc-2BYBXOFS6jSek5B9jso99JKhwS6-2FGJNrodiS5f88AEhyRnTm-2B5UD4HzTauv7UWZh6-2F4TeICFUR8T9hratmCeqPT9f61CRjaQqNrBGa91ROKg0Q-3D-3D" style="text-decoration: none; color: #20824B; font-weight: 600; border-bottom-width: 1px; border-bottom-color: #ddd; border-bottom-style: solid;">Mojo.js 1.7</a><br>
↳ Web framework inspired by Perl's Mojolicious.</p>
</li>
</ul>
</td></tr></table>
<table width="100%" cellpadding="0" cellspacing="0" class="el-subtable " style="background-color: #faffe6; margin-top: 15px; border-collapse: collapse;"><tr><td style="font-family: -apple-system,BlinkMacSystemFont,Helvetica,sans-serif; font-size: 15px; line-height: 1.48em; border-collapse: collapse; padding-top: 0px; padding-right: 0px; padding-bottom: 0px; padding-left: 0px;">
<!-- normal content section -->
<table width="100%" cellpadding="0" cellspacing="0" class="content el-content " style="color: #222; border-collapse: collapse; margin-top: 0 !important;"><tr><td style="font-family: -apple-system,BlinkMacSystemFont,Helvetica,sans-serif; font-size: 15px; line-height: 1.48em; border-collapse: collapse; padding-top: 0px; padding-right: 15px; padding-bottom: 0px; padding-left: 15px;"><p style="font-weight: 600; font-size: 1.05em; border-bottom-width: 4px; border-bottom-color: #ddeebb; border-bottom-style: solid; line-height: 1.6em; display: inline-block; margin-top: 0.8em; margin-bottom: 0.8em;">💻 Jobs</p></td></tr></table>
<table width="100%" cellpadding="0" cellspacing="0" class="miniitem item " style="border-collapse: collapse; margin-bottom: 4px;"><tr><td style="font-family: -apple-system,BlinkMacSystemFont,Helvetica,sans-serif; font-size: 15px; line-height: 1.48em; border-collapse: collapse; padding-top: 0px; padding-right: 15px; padding-bottom: 0px; padding-left: 15px;">
<p class="desc" style="color: #5a5a5a; line-height: 1.5em !important; font-size: 0.9em !important; margin-top: 8px; margin-right: 0px; margin-bottom: 6px; margin-left: 0px;"><span style="font-weight: 600; font-size: 1.0em; color: #000;"><a href="https://u25184427.ct.sendgrid.net/ls/click?upn=MnmHBiCwIPe9TmIJeskmA7mFdqmsIs-2B5Xs-2FNpSIs56qasaHBl0gs3SJ3vQnAMnJPU9aLvIxnDyLU79p-2B357hKQ-3D-3DUK0T_vVXscVLXlj5UtQe3aqo5RMTdTq2PepdZjP86UOmA8nyFE700vlj1-2FK27spZiPEiE9EG-2FlsMwSwzyj-2FLYV-2BOpCArV8KTUQMFghaDDivuU1JkIx7DA2-2FPKV5N6wl3-2BEpVv-2B0dnopBh1oMKWw4fvhotgaNkLd-2B3Mj3KBTQPBvRDNSJkZtmFxN5NrNgl67kyA09TFnW1NOdXRDrEFvWniM30wA-3D-3D" style="text-decoration: none; color: #20824B; font-size: 1.2em !important; font-weight: 400; border-bottom-width: 1px; border-bottom-color: #ddd; border-bottom-style: solid;">Doppler - A SecretOps Platform Built by Developers for Developers</a></span> — Dopplers looking for Sr. Full-Stack Engineers to help shape the future of security devtools. TypeScript, React, Express, and Go, <a href="https://u25184427.ct.sendgrid.net/ls/click?upn=MnmHBiCwIPe9TmIJeskmA7mFdqmsIs-2B5Xs-2FNpSIs56oSKY6DSK7ZLDxi40W6JInpRJP4scWVGwujayUkTEfTng-3D-3DIJw4_vVXscVLXlj5UtQe3aqo5RMTdTq2PepdZjP86UOmA8nyFE700vlj1-2FK27spZiPEiEbGvTYlQYJ8HAB-2FAFzp7QyFLv8yUn0YwGp1ugvsWb5tCEeW5TzrwvZkTs2yfYyBD7uDeIhEnR53wDbnTJYCOdAth8jiciYetIA25F4MKNEMMDwBJUP8nZg6q7GrK1EaLvo8lmq4KieLIiKbBovNHaGA-3D-3D" style="font-size: 1.0em !important; text-decoration: none; color: #20824B; font-weight: 400; border-bottom-width: 1px; border-bottom-color: #ddd; border-bottom-style: solid;">apply here</a>.
<br><span class="name" style="color: #bbbbbb !important; margin-top: 4px; text-transform: uppercase; font-size: 12px; line-height: 1.2em;">Doppler</span>
</p>
</td></tr></table>
<table width="100%" cellpadding="0" cellspacing="0" class="miniitem item " style="border-collapse: collapse; margin-bottom: 4px;"><tr><td style="font-family: -apple-system,BlinkMacSystemFont,Helvetica,sans-serif; font-size: 15px; line-height: 1.48em; border-collapse: collapse; padding-top: 0px; padding-right: 15px; padding-bottom: 0px; padding-left: 15px;">
<p class="desc" style="color: #5a5a5a; line-height: 1.5em !important; font-size: 0.9em !important; margin-top: 8px; margin-right: 0px; margin-bottom: 6px; margin-left: 0px;"><span style="font-weight: 600; font-size: 1.0em; color: #000;"><a href="https://u25184427.ct.sendgrid.net/ls/click?upn=MnmHBiCwIPe9TmIJeskmA7mFdqmsIs-2B5Xs-2FNpSIs56qaMsAShSLTzDOWuV-2FcVV7pRu4mvi82funiiImPN7jlsQ-3D-3D0hXk_vVXscVLXlj5UtQe3aqo5RMTdTq2PepdZjP86UOmA8nyFE700vlj1-2FK27spZiPEiEkoE7xVBorVeUpPXPHEhhga9ZIqhLuS5gW088J84nNV9kNrKsk3TLBHqmJ9WEqKhK2D1l6PXO9DLG6xJC-2FU8ZtcUxJFCVX8DWL-2Fb4yuQaH6gdKaNXrL4OEw8KACiH4Z3FLqvpGFY5WCiQ3UGR8D-2FLeA-3D-3D" style="text-decoration: none; color: #20824B; font-size: 1.2em !important; font-weight: 400; border-bottom-width: 1px; border-bottom-color: #ddd; border-bottom-style: solid;">Find Tech Jobs with Hired</a></span> — Create a profile on Hired to connect with hiring managers at growing startups and Fortune 500 companies. It's free for job-seekers.
<br><span class="name" style="color: #bbbbbb !important; margin-top: 4px; text-transform: uppercase; font-size: 12px; line-height: 1.2em;">Hired</span>
</p>
</td></tr></table>
<table width="100%" cellpadding="0" cellspacing="0" style="border-collapse: collapse;"><tr><td style="height: 10px; font-family: -apple-system,BlinkMacSystemFont,Helvetica,sans-serif; font-size: 15px; line-height: 1.48em; border-collapse: collapse;"></td></tr></table>
</td></tr></table>
<table width="100%" cellpadding="0" cellspacing="0" style="border-collapse: collapse;"><tr><td style="height: 20px; font-family: -apple-system,BlinkMacSystemFont,Helvetica,sans-serif; font-size: 15px; line-height: 1.48em; border-collapse: collapse;"></td></tr></table>
<table class="footer noarchive" width="100%" cellpadding="0" cellspacing="0" style="border-top-width: 3px; border-top-color: #dddddd; border-top-style: solid; border-collapse: collapse;"><tr><td style="font-family: -apple-system,BlinkMacSystemFont,Helvetica,sans-serif; font-size: 15px; line-height: 1.48em; border-collapse: collapse; padding-top: 10px; padding-right: 15px; padding-bottom: 10px; padding-left: 15px;">
<p style="line-height: 1.3em; margin-top: 1em; margin-right: 0; margin-bottom: 1em; margin-left: 0;"><strong style="text-transform: uppercase; font-weight: 600;">Got a link for us?</strong> Reply and tell us. We can't include everything but we'll look at anything you send. <em>Thanks!</em></p>
<hr style="width: 60px; margin-left: 0; border-right-width: 0; border-bottom-width: 0; border-left-width: 0; border-top-width: 2px; border-top-color: #ddd; border-top-style: solid;">
<p style="margin-top: 1em; margin-right: 0; margin-bottom: 1em; margin-left: 0;"><strong style="text-transform: uppercase; font-weight: 600;">Sponsorship:</strong> Email <code><a href="mailto:kristina@cooperpress.com" style="color: #000; text-decoration: none;">kristina@cooperpress.com</a></code> for details.</p>
<hr style="width: 60px; margin-left: 0; border-right-width: 0; border-bottom-width: 0; border-left-width: 0; border-top-width: 4px; border-top-color: #6ca629; border-top-style: solid;">
<p style="font-size: 12px; line-height: 18px; margin-top: 1em; margin-right: 0; margin-bottom: 1em; margin-left: 0;">Published by Cooper Press Ltd.<br>Fairfield Enterprise Centre, Louth, LN11 0LS, United Kingdom</p>
<p style="font-size: 0.9em; margin-top: 1em; margin-right: 0; margin-bottom: 1em; margin-left: 0;"><a href="https://u25184427.ct.sendgrid.net/ls/click?upn=MnmHBiCwIPe9TmIJeskmA7mFdqmsIs-2B5Xs-2FNpSIs56qPJxxKJ5QkQnu5plIhQMd7jm32aQFaVAX6ptRkyjULJQ-3D-3DeWxs_vVXscVLXlj5UtQe3aqo5RMTdTq2PepdZjP86UOmA8nyFE700vlj1-2FK27spZiPEiEEPl0SEXF91axrqwEC2oBokLdmWqeOCIIC537mbhMHd-2B0VxNs62uasjPHaU1IknHbyEINnqxjMM2VkFxwYVLb2ZlxFysQ5V7v2sJTZf-2FO1pD7TIGQPb5AsrYBQ8MbWmMj-2B58LRKsXz9dY5SE2F3h-2BBA-3D-3D" style="text-decoration: underline; color: #20824B;">Cancel your subscription</a> or <a href="https://u25184427.ct.sendgrid.net/ls/click?upn=MnmHBiCwIPe9TmIJeskmA7mFdqmsIs-2B5Xs-2FNpSIs56r2VS7bq5YsZ1xZ3760ZmCZ6VWi-2FtTs5RRBqRDcte3dGA-3D-3DgMfl_vVXscVLXlj5UtQe3aqo5RMTdTq2PepdZjP86UOmA8nyFE700vlj1-2FK27spZiPEiET-2Br9lddSxpGdW9pWgz1d5SMPt-2Fe3KQ4OmuqL7LJ4-2Fym2nXyQmuI0D1VkdJOW0df79l2GB0wXm7PR4Ac8i2itGyhK7zdrCusB7Fpou3pttBGEBmNDadBBwdczIlxUrEGgaPqavyN8bvx-2FzpCnFCTlKg-3D-3D" style="text-decoration: underline; color: #20824B;">change your address.</a></p>
</td></tr></table>
</div>
</td></tr>
</table>
<!--[if (gte mso 9)|(IE)]></td></tr></table><![endif]-->
<div id="footer" class="noarchive">
</div>
<!-- hey --><img src="https://nodeweekly.com/open/459/4f439e0f78" width="3" height="1" alt="n" /><!-- hey --><img src="https://u25184427.ct.sendgrid.net/wf/open?upn=VbY9PHrcT8wDX1sMvxaoDeFrnDggj0GS9qRxnZZ16E2kKWMv-2F0YoFNKe2ljrs6sKFjImwUy-2Fv-2F2cQdrb32UmSMoveT2xLj6qPsQWtfVmgGO2pRHfMPPUN7ty1sWIEPM8pd-2B8-2BaYYf-2F-2BzLw3biwQf4dnvS6hIWDsNTyMCJs3BqiZTMgikR-2BDVanVA0ig7GTNtppSdLc0DO7AtD-2FhU-2F2QBtXmajObwto-2BVJM-2BbIezjuB4-3D" alt="" width="1" height="1" border="0" style="height:1px !important;width:1px !important;border-width:0 !important;margin-top:0 !important;margin-bottom:0 !important;margin-right:0 !important;margin-left:0 !important;padding-top:0 !important;padding-bottom:0 !important;padding-right:0 !important;padding-left:0 !important;"/></body>
</html>

View file

@ -12,6 +12,10 @@ 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'
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)
@ -89,63 +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')
await expect(
new SubstackHandler().isNewsletter({
html,
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'
)
await expect(
new SubstackHandler().isNewsletter({
html,
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')
await expect(
new SubstackHandler().isNewsletter({
html,
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')
await expect(
new BeehiivHandler().isNewsletter({
html,
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')
await expect(
new BeehiivHandler().isNewsletter({
html,
postHeader: '',
from: '',
unSubHeader: '',
})
).to.eventually.be.true
const handler = await getNewsletterHandler({
html,
postHeader: '',
from: '',
unSubHeader: '',
})
expect(handler).to.be.instanceOf(BeehiivHandler)
})
it('returns GhostHandler for ghost newsletter', async () => {
const html = load('./test/data/ghost-newsletter.html')
const handler = await getNewsletterHandler({
html,
postHeader: '',
from: '',
unSubHeader: '',
})
expect(handler).to.be.instanceOf(GhostHandler)
})
it('returns ConvertkitHandler for convertkit newsletter', async () => {
const html = load('./test/data/convertkit-newsletter.html')
const handler = await getNewsletterHandler({
html,
postHeader: '',
from: '',
unSubHeader: '',
})
expect(handler).to.be.instanceOf(ConvertkitHandler)
})
it('returns CooperPressHandler for node-weekly newsletter', async () => {
const html = load('./test/data/node-weekly-newsletter.html')
const handler = await getNewsletterHandler({
html,
postHeader: '',
from: '',
unSubHeader: '',
})
expect(handler).to.be.instanceOf(CooperPressHandler)
})
})
@ -156,16 +192,14 @@ 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',
})
.get('/p/companies-that-eat-people-217')
nock('https://newsletter.slowchinese.net')
.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')
@ -174,7 +208,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', () => {
@ -183,24 +217,42 @@ 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', () => {
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(301, undefined, {
Location: 'https://fs.blog/brain-food/october-16-2022/',
})
nock('https://fs.blog')
.head('/brain-food/october-16-2022/')
.reply(200, '')
})
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/')
})
})
it('returns undefined if it is not a newsletter', async () => {
@ -208,6 +260,44 @@ 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(301, undefined, {
Location: 'https://www.openml.fyi/2022-10-14/',
})
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/')
})
})
context('when email is from cooper press', () => {
before(() => {
nock('https://u25184427.ct.sendgrid.net')
.head(
'/ls/click?upn=MnmHBiCwIPe9TmIJeskmA7mFdqmsIs-2B5Xs-2FNpSIs56obSPDXnoBEjufvIqRCEJUf5Uqg_vVXscVLXlj5UtQe3aqo5RMTdTq2PepdZjP86UOmA8nyFE700vlj1-2FK27spZiPEiEkDU3SIXWGeoiU60KFhM-2B-2Bxx5yiL8KKbAV6oFceRi8O1gMc3mdwg5D8FaaM3PublaX24iAcVbn99PzxJaPuVrU6xDWbRovw2UgGTIoEI-2BBO-2B0qzi2wv5c6yJTkUGZOcsJ6xGLXO1BO-2BHSbyZMZV4NMw-3D-3D'
)
.reply(301, undefined, {
Location: 'https://nodeweekly.com/issues/459',
})
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/459')
})
})
})
describe('generateUniqueUrl', () => {

View file

@ -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 ||
@ -2212,13 +2223,12 @@ Readability.prototype = {
_createPlaceholders: async function (e) {
for (const element of Array.from(e.getElementsByTagName('a'))) {
if (this.isEmbed(element)) {
return;
}
// Create tweets placeholders from links
if (element.href.includes('twitter.com') || element.parentNode.className === 'tweet') {
if (element.href.includes('twitter.com') || (element.parentNode && element.parentNode.className === 'tweet')) {
const link = element.href;
const regex = /(https?:\/\/twitter\.com\/\w+\/status\/)(\d+)/gm;
const match = regex.exec(link);
@ -2232,12 +2242,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') {

View file

@ -5,8 +5,8 @@
"excerpt": "Omnivore is a read-it-later app that lets you save and organize everything you read online.",
"siteName": "Omnivore",
"siteIcon": "https://bucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com/public/images/8f6c575c-355b-44a6-a8e6-67a1c3745b59/favicon.ico",
"previewImage": "https://substackcdn.com/image/fetch/w_1008,h_528,c_fill,f_jpg,q_auto:best,fl_progressive:steep/https%3A%2F%2Fblog.omnivore.app%2Ftwitter%2Fsubscribe-card.svg%3Fv%3De03aaf5cd51e4035f38f2aa621f6a29c%26version%3D7",
"publishedDate": null,
"previewImage": "https://substackcdn.com/image/fetch/w_1200,h_600,c_limit,f_jpg,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F4e9ae3f6-53b2-495d-9794-a9a1661c5a51_964x964.jpeg",
"publishedDate": "2022-10-17T08:48:56.000Z",
"language": "English",
"readerable": true
}

View file

@ -1,257 +1,330 @@
<DIV class="page" id="readability-page-1">
<article>
<div>
<h3>Omnivore is a read-it-later app that lets you save and organize everything you read online.</h3>
<h3> Omnivore is a read-it-later app that lets you save and organize everything you read online. </h3>
</div>
<div dir="auto">
<p>This guide will show you how to use Omnivores basic functions and advanced features, divided into four main activities:</p>
<p> This guide will show you how to use Omnivores basic functions and advanced features, divided into four main activities: </p>
<ul>
<li>
<p>Saving</p>
<p> Saving </p>
</li>
<li>
<p>Reading</p>
<p> Reading </p>
</li>
<li>
<p>Organizing</p>
<p> Organizing </p>
</li>
<li>
<p>Integrations</p>
<p> Integrations </p>
</li>
</ul>
<p><span>The </span><strong>Library </strong><span>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.</span></p>
<p>There are five ways to save links to pages or articles that you wish to read later:</p>
<p>
<span>The</span> <strong>Library</strong> <span>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.</span>
</p>
<p> There are five ways to save links to pages or articles that you wish to read later: </p>
<ul>
<li>
<p>Saving from Your Omnivore Library</p>
<p> Saving from Your Omnivore Library </p>
</li>
<li>
<p>Saving from a Browser&nbsp;</p>
<p> Saving from a Browser&nbsp; </p>
</li>
<li>
<p>Saving from a Phone or Tablet (iOS or Android)</p>
<p> Saving from a Phone or Tablet (iOS or Android) </p>
</li>
<li>
<p>Newsletter Subscriptions via Email</p>
<p> Newsletter Subscriptions via Email </p>
</li>
<li>
<p><span>Saving PDFs from a Mac </span><br></p>
<p>
<span>Saving PDFs from a Mac</span><br>
</p>
</li>
</ul>
<h3>Saving from Your Omnivore Library</h3>
<p><span>1. In the upper right corner of your Library, tap the </span><strong>Add Link</strong><span> button. </span><br><span>2. Enter the URL you wish to save and tap </span><strong>Add Link</strong><span>.</span><br><span>3. The link will appear in your Library the next time you refresh it.</span><br></p>
<h3>Saving from a Browser</h3>
<p>1. Download and install the Omnivore extension for your browser:</p>
<h3> Saving from Your Omnivore Library </h3>
<p>
<span>1. In the upper right corner of your Library, tap the</span> <strong>Add Link</strong> <span>button.</span><br>
<span>2. Enter the URL you wish to save and tap</span> <strong>Add Link</strong><span>.</span><br>
<span>3. The link will appear in your Library the next time you refresh it.</span><br>
</p>
<h3> Saving from a Browser </h3>
<p> 1. Download and install the Omnivore extension for your browser: </p>
<ul>
<li>
<p><a href="https://omnivore.app/install/chrome" rel="">Chrome&nbsp;</a></p>
<p>
<a href="https://omnivore.app/install/chrome" rel="">Chrome&nbsp;</a>
</p>
</li>
<li>
<p><a href="https://omnivore.app/install/edge" rel="">Edge</a></p>
<p>
<a href="https://omnivore.app/install/edge" rel="">Edge</a>
</p>
</li>
<li>
<p><a href="https://omnivore.app/install/firefox" rel="">Firefox</a></p>
<p>
<a href="https://omnivore.app/install/firefox" rel="">Firefox</a>
</p>
</li>
<li>
<p><a href="https://omnivore.app/install/safari" rel="">Safari</a></p>
<p>
<a href="https://omnivore.app/install/safari" rel="">Safari</a>
</p>
</li>
</ul>
<p><span>2. Navigate to the page you wish to save and tap the Omnivore button in your browsers toolbar or Extensions menu.</span><br><span>3. Alternatively, you can right-click (command+click on Mac) on any hyperlink and select </span><strong>Save to Omnivore</strong><span> from the menu.</span><br><span>4. The link will appear in your Library the next time you refresh it.</span><br></p>
<h3>Saving from a Phone or Tablet</h3>
<p>The best way to save links from your mobile device is via the Omnivore app. You can download the app here:</p>
<p>
<span>2. Navigate to the page you wish to save and tap the Omnivore button in your browsers toolbar or Extensions menu.</span><br>
<span>3. Alternatively, you can right-click (command+click on Mac) on any hyperlink and select</span> <strong>Save to Omnivore</strong> <span>from the menu.</span><br>
<span>4. The link will appear in your Library the next time you refresh it.</span><br>
</p>
<h3> Saving from a Phone or Tablet </h3>
<p> The best way to save links from your mobile device is via the Omnivore app. You can download the app here: </p>
<ul>
<li>
<p><a href="https://omnivore.app/install/ios" rel="">iOS (iPhone or iPad)</a></p>
<p>
<a href="https://omnivore.app/install/ios" rel="">iOS (iPhone or iPad)</a>
</p>
</li>
<li>
<p>Android</p>
<p>
<a href="https://play.google.com/store/apps/details?id=app.omnivore.omnivore" rel="">Android (Currently in pre-release)</a>
</p>
</li>
</ul>
<p>Once the mobile app is installed:</p>
<p> Once the mobile app is installed: </p>
<ol>
<li>
<p><span>In your browser, navigate to the page you wish to save and tap the </span><strong>Share</strong><span> button.</span></p>
<p>
<span>In your browser, navigate to the page you wish to save and tap the</span> <strong>Share</strong> <span>button.</span>
</p>
</li>
<li>
<p><span>Tap the </span><strong>Omnivore</strong><span> icon in the Share menu.</span></p>
<p>
<span>Tap the</span> <strong>Omnivore</strong> <span>icon in the Share menu.</span>
</p>
</li>
<li>
<p>The link will appear in your Library the next time you refresh it.</p>
<p> The link will appear in your Library the next time you refresh it. </p>
</li>
</ol>
<h3>Newsletter Subscriptions via Email</h3>
<p><span>1. On the Omnivore website or app, tap your photo, initial, or avatar in the top right corner to access the profile menu. Select </span><strong>Emails</strong><span> from the menu.</span></p>
<p><span>2. Tap </span><strong>Create a New Email Address</strong><span> to add a new email address (ex: username-123_abc@inbox.omnivore.app) to the list.</span></p>
<p>3. Click the Copy icon next to the email address.</p>
<p><span>4. Navigate to the signup page for the newsletter you wish to subscribe to.</span><br><span>5. Paste the Omnivore email address into the signup form.</span></p>
<p>6. New newsletters will be automatically delivered to your Omnivore inbox.</p>
<h3>Saving PDFs from a Mac&nbsp;</h3>
<h3> Newsletter Subscriptions via Email </h3>
<p>
<span>1. On the Omnivore website or app, tap your photo, initial, or avatar in the top right corner to access the profile menu. Select</span> <strong>Emails</strong> <span>from the menu.</span>
</p>
<p>
<span>2. Tap</span> <strong>Create a New Email Address</strong> <span>to add a new email address (ex: username-123abc@inbox.omnivore.app) to the list.</span>
</p>
<p> 3. Click the Copy icon next to the email address. </p>
<p>
<span>4. Navigate to the signup page for the newsletter you wish to subscribe to.</span><br>
<span>5. Paste the Omnivore email address into the signup form.</span>
</p>
<p> 6. New newsletters will be automatically delivered to your Omnivore inbox. </p>
<h3> Saving PDFs from a Mac&nbsp; </h3>
<ol>
<li>
<p><span>Install the </span><a href="https://omnivore.app/install/mac" rel="">Mac App</a><span>.&nbsp;</span></p>
<p>
<span>Install the</span> <a href="https://omnivore.app/install/mac" rel="">Mac App</a><span>.&nbsp;</span>
</p>
</li>
<li>
<p>On your Mac, locate the PDF you wish to save and right-click or ctrl+click on the file name.</p>
<p> On your Mac, locate the PDF you wish to save and right-click or ctrl+click on the file name. </p>
</li>
<li>
<p><span>Select </span><strong>Share</strong><span> from the menu and choose </span><strong>Omnivore</strong><span>.</span></p>
<p>
<span>Select</span> <strong>Share</strong> <span>from the menu and choose</span> <strong>Omnivore</strong><span>.</span>
</p>
</li>
<li>
<p>The link will appear in your Library the next time you refresh it.</p>
<p> The link will appear in your Library the next time you refresh it. </p>
</li>
</ol>
<h2>Reading</h2>
<p>Click any link saved in your Library to enter the Reader view.&nbsp;</p>
<p>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.</p>
<p>While reading, you can:</p>
<h2> Reading </h2>
<p> Click any link saved in your Library to enter the Reader view.&nbsp; </p>
<p> 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. </p>
<p> While reading, you can: </p>
<ul>
<li>
<p>Change Formatting</p>
<p> Change Formatting </p>
</li>
<li>
<p>Highlight Text</p>
<p> Highlight Text </p>
</li>
<li>
<p>Add Notes</p>
<p> Add Notes </p>
</li>
<li>
<p>View All Saved Highlights and Notes</p>
<p> View All Saved Highlights and Notes </p>
</li>
<li>
<p>Track Reading Progress</p>
<p> Track Reading Progress </p>
</li>
</ul>
<h3>Change Formatting&nbsp;</h3>
<h3> Change Formatting&nbsp; </h3>
<ol>
<li>
<p><em><strong>Theme:</strong></em><span> Tap your photo, initial, or avatar&nbsp; in the top right corner to access the profile menu. Select the white or black thumbnail to choose the Light or Dark theme.</span></p>
<p>
<em><strong>Theme:</strong></em> <span>Tap your photo, initial, or avatar&nbsp; in the top right corner to access the profile menu. Select the white or black thumbnail to choose the Light or Dark theme.</span>
</p>
</li>
<li>
<p><em><strong>Text Formatting:</strong></em><span> Tap the Aa icon to adjust the text size, font, margins, and line spacing.</span></p>
<p>
<em><strong>Text Formatting:</strong></em> <span>Tap the Aa icon to adjust the text size, font, margins, and line spacing.</span>
</p>
</li>
</ol>
<h3>Highlight Text</h3>
<h3> Highlight Text </h3>
<ol>
<li>
<p>Select the text you wish to highlight.</p>
<p> Select the text you wish to highlight. </p>
</li>
<li>
<p><span>Tap the </span><strong>Highlight </strong><span>button.</span></p>
<p>
<span>Tap the</span> <strong>Highlight</strong> <span>button.</span>
</p>
</li>
<li>
<p>The text will appear highlighted next time you view the article.</p>
<p> The text will appear highlighted next time you view the article. </p>
</li>
</ol>
<h3>Add Notes</h3>
<h3> Add Notes </h3>
<ol>
<li>
<p>Highlight a section of text where you wish to add a note.</p>
<p> Highlight a section of text where you wish to add a note. </p>
</li>
<li>
<p><span>Tap the </span><strong>Note</strong><span> button, type your note, and tap </span><strong>Save</strong><span>.</span></p>
<p>
<span>Tap the</span> <strong>Note</strong> <span>button, type your note, and tap</span> <strong>Save</strong><span>.</span>
</p>
</li>
<li>
<p>The Note icon will appear next time you view this article.</p>
<p> The Note icon will appear next time you view this article. </p>
</li>
</ol>
<h3>View All Saved Highlights and Notes</h3>
<h3> View All Saved Highlights and Notes </h3>
<ol>
<li>
<p>Tap the Highlight/Note icon to see a list of all the highlighted text and notes you have added to this page.</p>
<p> Tap the Highlight/Note icon to see a list of all the highlighted text and notes you have added to this page. </p>
</li>
<li>
<p>To remove a note or highlight, select it from the list and tap the Trash icon.</p>
<p> To remove a note or highlight, select it from the list and tap the Trash icon. </p>
</li>
</ol>
<h3>Track Reading Progress</h3>
<p>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.</p>
<h2>Organizing</h2>
<p>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:&nbsp;</p>
<h3> Track Reading Progress </h3>
<p> 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. </p>
<h2> Organizing </h2>
<p> 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:&nbsp; </p>
<ul>
<li>
<p>Archiving</p>
<p> Archiving </p>
</li>
<li>
<p>Labels</p>
<p> Labels </p>
</li>
<li>
<p>Search</p>
<p> Search </p>
</li>
<li>
<p>Filters</p>
<p> Filters </p>
</li>
</ul>
<h2>Archiving (Web)</h2>
<h2> Archiving </h2>
<ol>
<li>
<p>Tap the Menu icon next to the link you wish to archive (on the mobile app, long press the link to open the menu).</p>
<p> Tap the Menu icon next to the link you wish to archive (on the mobile app, long press the link to open the menu). </p>
</li>
<li>
<p><span>Select </span><strong>Archive</strong><span>.</span></p>
<p>
<span>Select</span> <strong>Archive</strong><span>.</span>
</p>
</li>
<li>
<p>The link will disappear from the default Library view, but will show up if you select the Archived filter (see Filters below).</p>
<p> The link will disappear from the default Library view, but will show up if you select the Archived filter (see Filters below). </p>
</li>
</ol>
<h3><strong>Labels</strong></h3>
<h3>
<strong>Labels</strong>
</h3>
<ol>
<li>
<p><span>Tap the Menu icon next to any link and select </span><strong>Set Label</strong><span>s.</span></p>
<p>
<span>Tap the Menu icon next to any link and select</span> <strong>Set Label</strong><span>s.</span>
</p>
</li>
<li>
<p><span>Select an existing label from the list or tap </span><strong>Edit Labels</strong><span> to create a new one.</span></p>
<p>
<span>Select an existing label from the list or tap</span> <strong>Edit Labels</strong> <span>to create a new one.</span>
</p>
</li>
<li>
<p>The label will appear next to the link in your Library. Tap it to view all links with the same label.</p>
<p> The label will appear next to the link in your Library. Tap it to view all links with the same label. </p>
</li>
<li>
<p><em>Omnivore mobile app only</em><span>: tap </span><strong>Labels </strong><span>to see a complete list of all labels you have used; tap one to view all links with the same label</span></p>
<p>
<em>Omnivore mobile app only</em><span>: tap</span> <strong>Labels</strong> <span>to see a complete list of all labels you have used; tap one to view all links with the same label</span>
</p>
</li>
<li>
<p>Note: Omnivore will automatically assign some labels, such as “Newsletters.”</p>
<p> Note: Omnivore will automatically assign some labels, such as “Newsletters.” </p>
</li>
</ol>
<h3>Search</h3>
<h3> Search </h3>
<ol>
<li>
<p>To search through all your saved links, enter a keyword or phrase in the search bar.&nbsp;</p>
<p> To search through all your saved links, enter a keyword or phrase in the search bar.&nbsp; </p>
</li>
<li>
<p><span>You can combine keywords with labels and filters to focus your search even further. </span><a href="https://omnivore.app/help/search" rel="">Learn more about advanced search</a><span>.</span></p>
<p>
<span>You can combine keywords with labels and filters to focus your search even further.</span> <a href="https://omnivore.app/help/search" rel="">Learn more about advanced search</a><span>.</span>
</p>
</li>
</ol>
<h3>Filters</h3>
<h3> Filters </h3>
<ol>
<li>
<p><span>Use the </span><strong>Filters </strong><span>menu to refine your Library view (some filters may be visible by default).</span></p>
<p>
<span>Use the</span> <strong>Filters</strong> <span>menu to refine your Library view (some filters may be visible by default).</span>
</p>
</li>
<li>
<p><span>Select </span><strong>Read Later</strong><span> to view a list of all your non-archived links except Newsletters.</span></p>
<p>
<span>Select</span> <strong>Read Later</strong> <span>to view a list of all your non-archived links except Newsletters.</span>
</p>
</li>
<li>
<p><span>Select </span><strong>Highlights</strong><span> to view the text selections you have highlighted in all your saved pages.&nbsp;</span></p>
<p>
<span>Select</span> <strong>Highlights</strong> <span>to view the text selections you have highlighted in all your saved pages.&nbsp;</span>
</p>
</li>
<li>
<p><span>Select </span><strong>Today</strong><span> to view a list of links you saved today.</span></p>
<p>
<span>Select</span> <strong>Today</strong> <span>to view a list of links you saved today.</span>
</p>
</li>
<li>
<p><span>Select </span><strong>Newsletters</strong><span> to view links saved via your newsletter subscriptions.</span></p>
<p>
<span>Select</span> <strong>Newsletters</strong> <span>to view links saved via your newsletter subscriptions.</span>
</p>
</li>
</ol>
<h2>Integrations</h2>
<p>Omnivore allows integrations with knowledge bases and note-taking apps including:</p>
<h2> Integrations </h2>
<p> Omnivore allows integrations with knowledge bases and note-taking apps including: </p>
<ul>
<li>
<p>Logseq</p>
<p> Logseq </p>
</li>
<li>
<p>Webhooks</p>
<p> Webhooks </p>
</li>
</ul>
<h3>Logseq</h3>
<p><span>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 </span><a href="https://briansunter.com/graph/%23/page/omnivore-logseq-guide" rel="">Omnivore for Logseq Plugin Guide</a><span>.</span></p>
<h3>Webhooks</h3>
<p>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.</p>
<h3> Logseq </h3>
<p>
<span>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</span> <a href="https://briansunter.com/graph/%23/page/omnivore-logseq-guide" rel="">Omnivore for Logseq Plugin Guide</a><span>.</span>
</p>
<h3> Webhooks </h3>
<p>
<span>Omnivore can trigger webhooks when you save a link or add highlights to a page you are reading.</span> <a href="https://blog.omnivore.app/p/syncing-all-your-notes-to-google" rel="">This example</a> <span>shows webhooks being used to write all saved links to a Google Sheets spreadsheet stored on a Google Drive.</span>
</p>
</div>
</article>
</DIV>

File diff suppressed because one or more lines are too long

View file

@ -1 +1 @@
https://blog.omnivore.app/p/d0d30ea5-49aa-4c04-8fae-c004be9c51b9
https://blog.omnivore.app/p/getting-started-with-omnivore-382

View file

@ -0,0 +1,12 @@
{
"title": "Elon Musks business ties deserve more scrutiny",
"byline": "Matthew Yglesias",
"dir": null,
"excerpt": "Across industries, executive after executive has chosen the PRC over free speech",
"siteName": "Slow Boring",
"siteIcon": "https://bucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com/public/images/886619a6-6fbb-4b2b-a1ab-cf25e43979c4/favicon.ico",
"previewImage": "https://substackcdn.com/image/fetch/w_1200,h_600,c_limit,f_jpg,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Ffc981d70-7927-41ec-be3c-844a830a058a_4000x2713.jpeg",
"publishedDate": "2022-10-17T10:01:29.000Z",
"language": "English",
"readerable": true
}

View file

@ -0,0 +1,118 @@
<DIV class="page" id="readability-page-1">
<article>
<div>
<h3> Across industries, executive after executive has chosen the PRC over free speech </h3>
</div>
<div dir="auto">
<p> 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. </p>
<p>
<span>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 theres 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 hes not alone; Apple TV+, for example, has a rule that</span> <a href="https://www.nytimes.com/2020/12/13/business/media/apple-gawker-tim-cook.html" rel="">none of its content can portray China negatively</a><span>.</span>
</p>
<p> Thats an unfortunate but straightforward consequence of Apple TV+ being so small compared to Apples 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. Theyre doing well with niche content (I love “For All Mankind”), and they won an Oscar for “Coda.” But its a small service in the scheme of things. </p>
<p>
<span>The problem for the world is that Twitter would be the Apple TV+ of Elon Musks enterprises, much smaller and less important than Tesla, so its interests will always be sacrificed to advance Teslas interests. And Tesla, like Apples hardware business, is deeply enmeshed in China. But Twitter is</span> <em>much</em> <span>more important to global politics and culture than Apple TV+. Thats 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 theyre 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.</span>
</p>
<p> Musk is mercurial and I wont 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 wont be strict enough in policing hate speech. </p>
<h4> China leans very effectively on western businesses </h4>
<p> We dont 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. </p>
<div>
<figure>
<a target="_blank" href="https://substackcdn.com/image/fetch/f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fcf961dc1-2440-4e54-976a-bb7fa8115e99_730x1000.png" rel="">
<div>
<picture>
<source type="image/webp" srcset="https://substackcdn.com/image/fetch/w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fcf961dc1-2440-4e54-976a-bb7fa8115e99_730x1000.png 424w, https://substackcdn.com/image/fetch/w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fcf961dc1-2440-4e54-976a-bb7fa8115e99_730x1000.png 848w, https://substackcdn.com/image/fetch/w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fcf961dc1-2440-4e54-976a-bb7fa8115e99_730x1000.png 1272w, https://substackcdn.com/image/fetch/w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fcf961dc1-2440-4e54-976a-bb7fa8115e99_730x1000.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fcf961dc1-2440-4e54-976a-bb7fa8115e99_730x1000.png" width="730" height="1000" data-attrs="{&quot;src&quot;:&quot;https://bucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com/public/images/cf961dc1-2440-4e54-976a-bb7fa8115e99_730x1000.png&quot;,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1000,&quot;width&quot;:730,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:658567,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;internalRedirect&quot;:null}" alt="" srcset="https://substackcdn.com/image/fetch/w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fcf961dc1-2440-4e54-976a-bb7fa8115e99_730x1000.png 424w, https://substackcdn.com/image/fetch/w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fcf961dc1-2440-4e54-976a-bb7fa8115e99_730x1000.png 848w, https://substackcdn.com/image/fetch/w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fcf961dc1-2440-4e54-976a-bb7fa8115e99_730x1000.png 1272w, https://substackcdn.com/image/fetch/w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fcf961dc1-2440-4e54-976a-bb7fa8115e99_730x1000.png 1456w" sizes="100vw">
</picture>
</div>
</a>
</figure>
</div>
<p>
<span>The Chinese government flipped out, and</span> <a href="https://www.reuters.com/article/us-mercedes-benz-china-gaffe/mercedes-benz-apologizes-to-chinese-for-quoting-dalai-lama-idUSKBN1FQ1FJ" rel="">Mercedes officially apologized</a><span>, even though they obviously didnt do anything wrong.</span>
</p>
<p>
<span>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 didnt hesitate to wreck the Houston Rockets access to the Chinese television market. Other NBA players —</span> <a href="https://www.si.com/nba/2019/10/15/lebron-james-daryl-morey-misinformed-china-tweet" rel="">including some of the leagues most outspoken and socially conscious stars</a> <span>— sided with China over Morey.</span>
</p>
<p> When celebrities do this stuff, it often attracts criticism from Republican China hawks. </p>
<div data-tweet-id="1397182150164680704" class="tweet-placeholder"></div>
<p>
<span>Because conservatives agree with Musk about the importance of making social media a welcoming place for transphobic jokes, they havent been nearly as quick to condemn him</span> <a href="https://www.bbc.com/news/business-63196452" rel="">telling the Financial Times that Taiwan ought to become a Beijing-ruled Special Administrative Region</a><span>. But when it comes to a random celebrity like John Cena, conservatives understand the basic dynamic perfectly well — money talks.</span>
</p>
<div data-attrs="{&quot;url&quot;:&quot;https://twitter.com/realDailyWire/status/1397228259755110402?ref_src=twsrc%5Etfw%7Ctwcamp%5Etweetembed%7Ctwterm%5E1397228259755110402%7Ctwgr%5E7a62f3d20b5f7d0cd3d02d57b779fb8f7110071c%7Ctwcon%5Es1_&ref_url=https%3A%2F%2Fwww.salon.com%2F2021%2F05%2F25%2Fjohn-cena-china-apology-taiwan-controversy%2F&quot;,&quot;full_text&quot;:&quot;John Cena Begging China For Forgiveness Proves That Money Beats Morality Every Time <a class=\&quot;tweet-url\&quot; href=\&quot;http://dlvr.it/S0NzBW\&quot;>dlvr.it/S0NzBW</a> &quot;,&quot;username&quot;:&quot;realDailyWire&quot;,&quot;name&quot;:&quot;Daily Wire&quot;,&quot;date&quot;:&quot;Tue May 25 16:29:11 +0000 2021&quot;,&quot;photos&quot;:[{&quot;img_url&quot;:&quot;https://pbs.substack.com/media/E2PzsqWUYAQFpdA.jpg&quot;,&quot;link_url&quot;:&quot;https://t.co/smDNnC4kfb&quot;,&quot;alt_text&quot;:null}],&quot;quoted_tweet&quot;:{},&quot;retweet_count&quot;:280,&quot;like_count&quot;:1862,&quot;expanded_url&quot;:{},&quot;video_url&quot;:null,&quot;belowTheFold&quot;:true}">
<div data-tweet-id="1397228259755110402" class="tweet-placeholder"></div>
<div data-tweet-id="1397228259755110402" class="tweet-placeholder"></div>
<div>
<picture>
<source type="image/webp" srcset="https://substackcdn.com/image/fetch/w_600,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fpbs.substack.com%2Fmedia%2FE2PzsqWUYAQFpdA.jpg"><img src="https://substackcdn.com/image/fetch/w_600,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fpbs.substack.com%2Fmedia%2FE2PzsqWUYAQFpdA.jpg" alt="Image" loading="lazy">
</picture>
</div>
<div data-tweet-id="1397228259755110402" class="tweet-placeholder"></div>
</div>
<p> 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 companys stock price, theyd be vulnerable to activist investor campaigns and potentially even litigation. </p>
<p>
<span>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. Im sure that if you asked Intel executives whether its good that China is perpetrating a genocide and using forced labor in Xinjiang, the vast majority of them would say, in private, that its actually bad. But in practice, Intel has</span> <a href="https://www.nytimes.com/2021/12/23/business/intel-apology-china-xinjiang.html" rel="">apologized for past statements about Xinjiang</a><span>. 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.</span>
</p>
<h4> Elon Musk does a ton of business in China </h4>
<p>
<span>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</span> <a href="https://www.reuters.com/business/autos-transportation/tesla-sells-record-china-made-vehicles-september-following-shanghai-factory-2022-10-09/" rel="">setting sales records in China</a><span>, which is obviously only possible because the Chinese government lets Tesla sell cars there.</span>
</p>
<div>
<figure>
<a target="_blank" href="https://substackcdn.com/image/fetch/f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F83868ad9-201b-45c3-81bd-e32d187a3249_1220x398.png" rel="">
<div>
<picture>
<source type="image/webp" srcset="https://substackcdn.com/image/fetch/w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F83868ad9-201b-45c3-81bd-e32d187a3249_1220x398.png 424w, https://substackcdn.com/image/fetch/w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F83868ad9-201b-45c3-81bd-e32d187a3249_1220x398.png 848w, https://substackcdn.com/image/fetch/w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F83868ad9-201b-45c3-81bd-e32d187a3249_1220x398.png 1272w, https://substackcdn.com/image/fetch/w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F83868ad9-201b-45c3-81bd-e32d187a3249_1220x398.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F83868ad9-201b-45c3-81bd-e32d187a3249_1220x398.png" width="1220" height="398" data-attrs="{&quot;src&quot;:&quot;https://bucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com/public/images/83868ad9-201b-45c3-81bd-e32d187a3249_1220x398.png&quot;,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:398,&quot;width&quot;:1220,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:83077,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;internalRedirect&quot;:null}" alt="" srcset="https://substackcdn.com/image/fetch/w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F83868ad9-201b-45c3-81bd-e32d187a3249_1220x398.png 424w, https://substackcdn.com/image/fetch/w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F83868ad9-201b-45c3-81bd-e32d187a3249_1220x398.png 848w, https://substackcdn.com/image/fetch/w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F83868ad9-201b-45c3-81bd-e32d187a3249_1220x398.png 1272w, https://substackcdn.com/image/fetch/w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F83868ad9-201b-45c3-81bd-e32d187a3249_1220x398.png 1456w" sizes="100vw" loading="lazy">
</picture>
</div>
</a>
</figure>
</div>
<p>
<span>Like many global manufacturing companies, Tesla also builds things in China — including what Musk projects will be</span> <a href="https://insideevs.com/news/583769/new-tesla-plant-make-shanghai-world-largest-vehicle-export-hub/" rel="">the companys largest factory in the world</a><span>.</span>
</p>
<p> Theres nothing particularly untoward or suspect about this. Many more people live in East Asia than live in North America, so its natural that over the long term any manufacturer of large objects is going to want to have their biggest production facilities in Asia. </p>
<p>
<span>But Tesla has received unusually generous treatment from the Chinese government. Part of Chinas 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. Its 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 dont 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</span> <a href="https://www.reuters.com/business/autos-transportation/tesla-invest-188-mln-expand-shanghai-factory-capacity-beijing-daily-2021-11-26/" rel="">exemption from the joint venture requirement</a><span>.</span>
</p>
<p>
<span>Thats a huge coup for Musk and a testament both to the quality of Teslas products and also to Musks 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</span> <a href="https://www.nytco.com/press/new-york-times-response-to-senator-rubios-letter/" rel="">New York Times is blocked in China</a> <span>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 Im doing a really bad job of standing up for core values that Americans on the left and right broadly agree on.</span>
</p>
<h4> Elon Musks stated views are very pro-PRC </h4>
<p>
<span>Ten years ago, Tesla was a very small company that</span> <a href="https://slate.com/technology/2012/10/mitt-romney-calls-tesla-loser-like-solyndra-in-presidential-debate.html" rel="">Mitt Romney denounced as the kind of “loser” that was dependent on unwise subsidies from the Obama administration</a><span>. 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.</span>
</p>
<p> He also had very normal geopolitical opinions like “foreign dictatorships are bad” that a person who doesnt have extensive Chinese business interests is free to express on Twitter (which, of course, is banned in China). </p>
<div data-tweet-id="167105458753118208" class="tweet-placeholder"></div>
<p> But as Musks 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 Ive done myself — its 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. </p>
<div data-tweet-id="968608879914270721" class="tweet-placeholder"></div>
<p> But in more recent years, Musks 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 Californias Covid-19 protocols. Yet apart from a token March 2020 gesture of praise for Xis success in controlling the virus, hes had absolutely nothing to say about Chinas Covid Zero policies that have been much more draconian than anything offered in the United States. </p>
<div data-tweet-id="1240753430001356801" class="tweet-placeholder"></div>
<p> The same is true, though obviously in a much broader sense, of Musks 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. Thats 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. Thats not surprising — nobody with business interests in China criticizes the PRCs 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. </p>
<p>
<span>And, again, Musks silence on free speech in China isnt because he doesnt talk about China. He</span> <em>does</em> <span>talk about China — including</span> <a href="https://www.cnn.com/2022/10/10/media/elon-musk-china-taiwan-intl-hnk/index.html" rel="">publicly advocating for a PRC takeover of Taiwan</a> <span>just one day before Tesla buyers were made eligible for an</span> <a href="https://seekingalpha.com/news/3890358-tesla-receives-tax-exemption-from-chinese-authorities" rel="">important PRC tax break</a><span>.</span>
</p>
<h4> What does this mean for Twitter? </h4>
<p> 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 hes going to turn it into a China-free zone the way that Apple has with its content business? </p>
<p> Probably not. </p>
<p> I really dont know what Musks 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: </p>
<ul>
<li>
<p> China is very willing to threaten western companies over their China-related speech practices. </p>
</li>
<li>
<p> 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. </p>
</li>
<li>
<p> 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. </p>
</li>
<li>
<p> 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. </p>
</li>
<li>
<p> Musk has, in his personal communication, stopped criticizing the PRC on any topic, including on matters like free speech and Covid protocols where hes been vocal about U.S. policies, and he has instead praised the PRC government and endorsed its views on geopolitical conflicts. </p>
</li>
</ul>
<p> So what follows from this? I dont know, in part because Musk is mercurial and hard to predict. But also in part because its 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 — its just something he knows, so he doesnt 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. </p>
<p> 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 whats good for business. So what does Twitter do once its owned by the CEO of a major industrial brand? I dont know, but I wish the reporters on the Musk beat would ask. And frankly, I wish Congress and the FTC would, too. </p>
</div>
</article>
</DIV>

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1 @@
https://www.slowboring.com/p/elon-musks-business-ties-deserve

View file

@ -139,7 +139,7 @@ export function ConfirmProfileModal(): JSX.Element {
{isUsernameValid && (
<StyledText
style="caption"
css={{ m: 0, pl: '$2', alignSelf: 'flex-start' }}
css={{ m: 0, pl: '$2', alignSelf: 'flex-start', color: '$omnivoreGray' }}
>
Username is available.
</StyledText>

View file

@ -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</StyledText>

View file

@ -51,6 +51,7 @@ export function EmailForgotPassword(): JSX.Element {
name="email"
value={email}
placeholder="Email"
css={{ bg: 'white', color: 'black' }}
onChange={(e) => { e.preventDefault(); setEmail(e.target.value); }}
/>
</SpanBox>

View file

@ -55,6 +55,7 @@ export function EmailLogin(): JSX.Element {
name="email"
value={email}
placeholder="Email"
css={{ backgroundColor: 'white', color: 'black' }}
onChange={(e) => { e.preventDefault(); setEmail(e.target.value); }}
/>
</SpanBox>
@ -67,6 +68,7 @@ export function EmailLogin(): JSX.Element {
name="password"
value={password}
placeholder="Password"
css={{ bg: 'white', color: 'black' }}
onChange={(e) => setPassword(e.target.value)}
/>
</SpanBox>

View file

@ -61,6 +61,7 @@ export function EmailResetPassword(): JSX.Element {
name="password"
value={password}
placeholder="Password"
css={{ bg: 'white', color: 'black' }}
onChange={(e) => { e.preventDefault(); setPassword(e.target.value); }}
/>
<FormLabel css={{ fontSize: '12px' }}>(Password must be at least 8 chars)</FormLabel>

View file

@ -63,7 +63,7 @@ export function EmailSignup(): JSX.Element {
return (
<form action={`${fetchEndpoint}/auth/email-signup`} method="POST">
<VStack alignment="center" css={{ padding: '16px' }}>
<StyledText style="subHeadline">Sign Up</StyledText>
<StyledText style="subHeadline" css={{ color: '$omnivoreGray' }}>Sign Up</StyledText>
<VStack css={{ width: '100%', minWidth: '320px', gap: '16px', pb: '16px' }}>
<SpanBox css={{ width: '100%' }}>
<FormLabel>Email</FormLabel>
@ -73,6 +73,7 @@ export function EmailSignup(): JSX.Element {
name="email"
value={email}
placeholder="Email"
css={{ backgroundColor: 'white', color: 'black' }}
onChange={(e) => { e.preventDefault(); setEmail(e.target.value); }}
/>
</SpanBox>
@ -85,6 +86,7 @@ export function EmailSignup(): JSX.Element {
name="password"
value={password}
placeholder="Password"
css={{ bg: 'white', color: 'black' }}
onChange={(e) => setPassword(e.target.value)}
/>
</SpanBox>
@ -97,6 +99,7 @@ export function EmailSignup(): JSX.Element {
name="name"
value={fullname}
placeholder="Full Name"
css={{ bg: 'white', color: 'black' }}
onChange={(e) => setFullname(e.target.value)}
/>
</SpanBox>
@ -109,6 +112,7 @@ export function EmailSignup(): JSX.Element {
name="username"
value={username}
placeholder="Username"
css={{ bg: 'white', color: 'black' }}
onChange={handleUsernameChange}
/>
</SpanBox>
@ -128,7 +132,7 @@ export function EmailSignup(): JSX.Element {
{isUsernameValid && (
<StyledText
style="caption"
css={{ m: 0, pl: '$2', alignSelf: 'flex-start' }}
css={{ m: 0, pl: '$2', alignSelf: 'flex-start', color: '$omnivoreGray' }}
>
Username is available.
</StyledText>

View file

@ -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 && (
<ConfirmationModal
message={'Are you sure you want to remove this link?'}
message={
'Are you sure you want to remove this item? All associated notes and highlights will be deleted.'
}
onAccept={removeItem}
onOpenChange={() => setShowRemoveLinkConfirmation(false)}
/>

View file

@ -1,14 +1,51 @@
import { useEffect } from 'react'
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'
export default function Support(): JSX.Element {
useEffect(() => {
const initAnalytics = useCallback(() => {
setupAnalytics()
window.Intercom('show')
}, [])
useEffect(() => {
window.addEventListener('load', initAnalytics)
return () => {
window.removeEventListener('load', initAnalytics)
}
}, [initAnalytics])
return (
<SettingsLayout title="Support">
<></>
<HStack
alignment="center"
distribution="end"
css={{
pr: '$3',
height: '80px',
'input:focus': {
outline: '5px auto -webkit-focus-ring-color',
},
'button:focus': {
outline: '5px auto -webkit-focus-ring-color',
},
}}
>
<Button
style={'ctaOutlineYellow'}
type="button"
onClick={(event) => {
event.preventDefault()
if (window.Intercom) {
window.Intercom('show')
}
}}
>
{'Open Chat Window'}
</Button>
</HStack>
</SettingsLayout>
)
}

View file

@ -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==